[Scummvm-git-logs] scummvm master -> 9e6422b20a72b3e6107217c7feba9b0227ecf7f9

sev- noreply at scummvm.org
Sun May 31 14:12:44 UTC 2026


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

Summary:
a9cc4b1243 SCUMM: RA2: Add initial level 1 loading code
9aba962606 SCUMM: RA2: Add missing init variables
32a4ba152c SCUMM: RA2: Deduplicate level object setup
b1e18defe1 SCUMM: RA2: Remove redundant NutRenderer drawFrame overload
de41cd69ab SCUMM: RA2: Avoid crash during shutdown
34e92ca251 SCUMM: RA2: Implement IACT dependency links
ad764f728c SCUMM: RA2: Implement player damage from enemies
d94e6274e1 SCUMM: RA2: Implement enemy explosion slots
a8843cc899 SCUMM: RA2: Implement laser animations
2619d4afc6 SCUMM: RA2: Refactor enemy state handling
764ecbf459 SCUMM: RA2: Remove obsolete enemy code
bd56fe06cc SCUMM: RA2: Add low-difficulty enemy indicators
6482cf87c4 SCUMM: RA2: Simplify enemy handling
512ae9f8f1 SCUMM: RA2: Implement cockpit movement effect
fd596d3dda SCUMM: RA2: Refine cockpit movement effect
ea21cbd852 SCUMM: RA2: Improve laser beam rendering
474e3d5b7b SCUMM: RA2: Refine laser beam rendering
b33180f25f SCUMM: RA2: Complete laser beam rendering
0e87f2c99c SCUMM: RA2: Play intro before level 1
77722bbfed SCUMM: RA2: Remove obsolete setup code
6b45c55368 SCUMM: RA2: Clean up state initialization
df10c54615 SCUMM: RA2: Remove unused state fields
2cebdfc1fc SCUMM: RA2: Implement damage bar
1ada210184 SCUMM: RA2: Refactor opcode 2 handling
c1bc5f218a SCUMM: RA2: Move SMUSH hooks out of SmushPlayer
d78e69c275 SCUMM: RA2: Refactor opcode 6 handling
404037ab75 SCUMM: RA2: Fix post-rendering state updates
ea84ceebdf SCUMM: RA2: Skip non-gameplay sounds
353416241d SCUMM: RA2: Implement score and mouse pointers
deb62f97e1 SCUMM: RA2: Implement subtitles
a3676cab6e SCUMM: RA2: Add dedicated audio system
f3bd96abae SCUMM: RA2: Implement main menu
f91918ff34 SCUMM: RA2: Implement additional menus
14884b6d74 SCUMM: RA2: Implement level loading
ead8b2a3ed SCUMM: RA2: Clear screen before missions
2878546d81 SCUMM: RA2: Implement pause and skip controls
1fa930ed85 SCUMM: RA2: Load directional ship sprites
db7b16c044 SCUMM: RA2: Fix level 1 loading regression
ac86b2729c SCUMM: RA2: Implement collision zones
a4bbf9032e SCUMM: RA2: Load low-resolution cockpit HUD
5473605500 SCUMM: RA2: Fix low-resolution cockpit HUD loading
8718764edf SCUMM: RA2: Add level 2 video decoders
0a05f1f519 SCUMM: RA2: Fix missing handler 1 crash
54610c886d SCUMM: RA2: Refactor level object handling
0642d63e38 SCUMM: RA2: Refactor level state handling
6da03acf19 SCUMM: RA2: Refactor gameplay handlers
5c0b23415f SCUMM: RA2: Reduce shared SMUSH dependencies
04aef6e229 SCUMM: RA2: Read level 2 backgrounds correctly
3abc8c5bc6 SCUMM: RA2: Improve level 2 backgrounds
b621b9cc6e SCUMM: RA2: Improve level 2 player sprites
9ce2e159b1 SCUMM: RA2: Implement level selection screen
bb9375faec SCUMM: RA2: Improve font rendering
6239b89fbb SCUMM: RA2: Improve shot rendering and terminology
0dad1b0270 SCUMM: RA2: Improve level selection screen
7e80992c72 SCUMM: RA2: Check whether the player can shoot
07f891177c SCUMM: RA2: Animate level 2 player correctly
05b0dc540a SCUMM: RA2: Improve bit handling
30795feeb5 SCUMM: RA2: Improve level 2 rendering
0983d2c688 SCUMM: RA2: Improve main menu
4dd1e28b95 SCUMM: RA2: Play intro correctly from main menu
84cf81e8e6 SCUMM: RA2: Load menu strings from TRS files
79010d6604 SCUMM: RA2: Support colored fonts
2bc1457374 SCUMM: RA2: Read fonts correctly
8ea7634eee SCUMM: RA2: Implement damage effect
1f53cb0d7e SCUMM: RA2: Improve pilot menu
267ddb67fe SCUMM: RA2: Implement difficulty selection menu
9b76b0247a SCUMM: RA2: Improve level menu
1ae852f6e9 SCUMM: RA2: Improve level 2
f2c9358f1b SCUMM: RA2: Refactor level 2 rendering code
41488ed0aa SCUMM: RA2: Implement per-enemy explosion handling
bcd6615899 SCUMM: RA2: Implement damage handling
ca8874e261 SCUMM: RA2: Handle damage in level 3
7d61726513 SCUMM: RA2: Improve ship control for level 3
1f23ead15c SCUMM: RA2: Implement ship explosions for level 3
1e2e7e108e SCUMM: RA2: Add explosion sounds
07c45898d7 SCUMM: RA2: Add shooting sounds
31a1ecf0cf SCUMM: RA2: Render gun shooting
285f7ff2b1 SCUMM: RA2: Prevent damage while covered
713aa7bd91 SCUMM: RA2: Fix covered and uncovered phase cycling
7ae1ddbd5a SCUMM: RA2: Reset handlers around level 2 cinematics
9130d5bd4b SCUMM: RA2: Fix SKIP and opcode 2 bit handling
899667bc95 SCUMM: RA2: Clear IACT bits for level 2 waves
6af2ff75d6 SCUMM: RA2: Apply FOBJ offsets to enemy overlays
6da68ffd2b SCUMM: RA2: Implement additional level handlers
06cc4d01d4 SCUMM: RA2: Improve menu video handling
c9a07cc6a1 SCUMM: RA2: Clean up menu handling
20b45a1ce0 SCUMM: RA2: Split implementation into subsystem files
274be9ad63 SCUMM: RA2: Improve intro playback
8723af81db SCUMM: RA2: Improve subtitles
49e3be755d SCUMM: RA2: Guard wave initialization in opcode 6
3d79ed0526 SCUMM: RA2: Minimize impact outside the RA2 engine
38af869f7c SCUMM: RA2: Improve UI rendering
696c68cd85 SCUMM: RA2: Add pilot and difficulty menus
9f75d49f08 SCUMM: RA2: Tune difficulty parameters
9506f4af42 SCUMM: RA2: Apply code conventions
a03ea4774f SCUMM: RA2: Improve game detection
de4c041f00 SCUMM: RA2: Reset enemy state for level 2 waves
c4e1df0f2f SCUMM: RA2: Add intro level text overlay
da4840d6b2 SCUMM: RA2: Refactor ship sprite rendering
528969fe79 SCUMM: RA2: Add basic unlocking and password handling
52fcf017dc SCUMM: RA2: Improve password handling
7c74f5e134 SCUMM: RA2: Refactor player shot rendering
fd61a9b4f3 SCUMM: RA2: Guard background copies by buffer pitch
15c5136ca5 SCUMM: RA2: Position ship sprites with viewport offsets
972dba7f59 SCUMM: RA2: Fix Escape key bug
ce750db5b1 SCUMM: RA2: Use a large enough bitmap
f72c255c6a SCUMM: RA2: Fix crash in smushDecodeSkipRLE
0ef613d4c1 SCUMM: RA2: Update death handling and level progression
825c3c8547 SCUMM: RA2: Improve sound handling
589580d5b6 SCUMM: RA2: Fix handler 25 ship rendering
a97db6f7fd SCUMM: RA2: Improve shot beam rendering
0aacf5d751 SCUMM: RA2: Improve level selection
3653d5839f SCUMM: RA2: Fix score font rendering
8d927d5377 SCUMM: RA2: Fix clipped sprite decoding
a988ea1aae SCUMM: RA2: Improve collisions and shadow indicators
8c4dfa645a SCUMM: RA2: Remove debug code
25cb578e5f SCUMM: RA2: Remove remaining debug code
b8518e989e SCUMM: RA2: Add top pilots screen
b3a0be791d SCUMM: RA2: Improve top pilots screen
632c63e17e SCUMM: RA2: Add options menu
0f3ad175bc SCUMM: RA2: Standardize declarations and comments
c390c411cf SCUMM: RA2: Standardize menu and rendering comments
60c4652e2a SCUMM: RA2: Standardize IACT and runlevel comments
523bd629c6 SCUMM: RA2: Clean up variable names
74da777074 SCUMM: RA1: Add initial proof of concept
4c5afc1201 SCUMM: RA1: Extend initial proof of concept
8917d7f009 SCUMM: RA1: Add initial UI rendering code
3ac282b4a8 SCUMM: RA1: Add mouse handling
73fbf2b8d5 SCUMM: RA1: Add sound support
4ed9384177 SCUMM: RA1: Improve controls
4c5eda6d07 SCUMM: RA1: Add basic damage handling
c8264eba60 SCUMM: RA1: Play additional videos
3073b37e79 SCUMM: RA1: Improve UI
7fee7db76e SCUMM: RA1: Add menu
50959ca0d3 SCUMM: RA1: Improve video decoding
591fc69205 SCUMM: RA1: Add sound and subtitles
d1a0b4d9bf SCUMM: RA1: Add damage handling
ecfc1eefad SCUMM: RA1: Improve level 1 loop
01e04961b0 SCUMM: RA1: Add right-path branching in level 1
8074a8c460 SCUMM: RA1: Refine level 1 flight handling
eca65436dd SCUMM: RA1: Add options
5a0f9d09a8 SCUMM: RA1: Add branching
c0146e48e0 SCUMM: RA1: Add level 2 and HUD features
c33805415a SCUMM: RA2: Capture mouse
2d99feb58f SCUMM: RA2: Add level execution helpers
1969645fbb SCUMM: RA2: Add credits
6e1e672ffd SCUMM: RA2: Improve pause menu layout
86c9b2d979 SCUMM: RA: Split Rebel Assault engine files
f9aa62e2bc SCUMM: RA2: Parse level 2 video chunks correctly
0aa0739701 SCUMM: RA1: Add level selection and level 2 pointer handling
3e45c5db23 SCUMM: RA1: Render level 2 lasers
a854f3fd00 SCUMM: RA1: Fix level 1 targeting pipeline
1f6fdf96c8 SCUMM: RA1: Fix interactive video ghosting
43a9aeb6ee SCUMM: RA1: Fix asteroid view offsets
19140d2d21 SCUMM: RA1: Clear oversized frame buffers
9a2fd5d3e2 SCUMM: RA1: Fix UI rendering
8210b3de83 SCUMM: RA1: Fix level 1 stage transition
8434efb10b SCUMM: RA1: Fix level 1 retry flow
da5c1e19e0 SCUMM: RA1: Add subtitle fonts
360d4911f1 SCUMM: RA1: Fix subtitle positioning
7cad2386cf SCUMM: RA1: Transform GAME zones with camera offsets
08430ff3f9 SCUMM: RA1: Preserve left-path branching
151bad125b SCUMM: RA1: Fix level 1 second phase
fe86583657 SCUMM: RA1: Refine level 1 second phase
a5dd30066d SCUMM: RA1: Improve level 2 perspective and movement
a17bfbcf4b SCUMM: RA1: Refactor SMUSH integration
133d39acac SCUMM: RA: Add additional level handlers
26a309a37a SCUMM: RA1: Add projection table for gameplay points
4ceacc55a2 SCUMM: RA1: Track frame object hit state
851bb8fcba SCUMM: RA1: Add hitboxes
cecedef4f7 SCUMM: RA1: Apply overlay Y offsets
c5b5b86f8d SCUMM: RA1: Add level 7 through 10 handlers
560d6f9ca5 SCUMM: RA1: Center interactive FTCH placement
2a4d224a45 SCUMM: RA1: Render HUD plate opaquely
ad1713032f SCUMM: RA1: Enable controls in level 3
ce45d205dd SCUMM: RA1: Refine mouse and frame opcode state
2a7ffb4168 SCUMM: RA1: Trigger death video immediately
a6ab490d52 SCUMM: RA1: Add additional level implementations
53bc68ed7b SCUMM: RA1: Add missing sound effects
26edb60399 SCUMM: RA1: Use named parameters
0d84e60b0b SCUMM: RA1: Add chapter names in intro
cf25b976b0 SCUMM: RA1: Improve level selection menu navigation
595f2a1fcb SCUMM: RA1: Improve level 9 rendering
0235c711e1 SCUMM: RA1: Improve on-foot level rendering
4dcc5d8d2d SCUMM: RA1: Improve walker level rendering
403d9675a9 SCUMM: RA1: Complete menu handling
3ec6f05b03 SCUMM: RA1: Add level 11 through 15 handlers
730896f1c3 SCUMM: RA1: Add missing globals
a8581ef1f3 SCUMM: RA: Add basic joystick support
51052ab299 SCUMM: RA1: Improve torpedo handling
ad7549ed3c SCUMM: RA1: Improve Escape key handling
326181870b SCUMM: RA1: Implement level 5
35220322a7 SCUMM: RA1: Improve collisions
aaa9ef1714 SCUMM: RA1: Fix level 4 progression
b3a580ac85 SCUMM: RA1: Remove level 4 workaround
22518d3549 SCUMM: RA1: Improve difficulty and energy checks
03a3e02a5a SCUMM: RA1: Add skip-RLE line update decoder
c33d5d1c06 SCUMM: RA1: Fix level 8 armor targets
d0f4a422ec SCUMM: RA1: Fix level 7 video transition
cea798d8c7 SCUMM: RA2: Move main loop out of scumm.cpp
00f9be188b SCUMM: RA: Move Rebel Assault SMUSH logic into RA players
90cab45dae SCUMM: RA: Move RA1 SMUSH frame handling into RA player
f3e88ea732 SCUMM: RA: Move text and GOST handlers into RA SMUSH players
cbbdde480b SCUMM: RA: Move RA1 codec 1 decoder into RA player
f854095da7 SCUMM: RA: Move RA1 auxiliary decoders into RA player
31d2c9a573 SCUMM: RA: Move specific class definitions out of smush_player.h
3dbe2d7eb7 SCUMM: RA2: Remove redundant code
3350b7b032 SCUMM: RA2: Reduce StringResource change impact
58b9cc95c4 SCUMM: RA: Reduce insane.h change impact
091dcd3018 SCUMM: RA: Reduce NutRenderer change impact
90cfb6851d SCUMM: RA: Move Rebel Assault code into rebel directories
dafba02a3b SCUMM: RA1: Fix regression in RA1 block decoder
d208eb1f6b SCUMM: RA1: Fix RA1 block decoder edge cases
f86c294c78 SCUMM: RA: Remove useless static qualifiers
8b81bcfb80 SCUMM: RA2: Fix mixed indentation
41a8508ee0 SCUMM: RA: Clean up spacing and indentation
10d5865288 SCUMM: RA: Improve descriptions and error messages
96ad3cc3b9 SCUMM: RA: Update hashes in scumm-md5.txt
5a64207352 SCUMM: RA2: Make codec 45 line-size and probe fields signed
804223f422 SCUMM: RA2: Clean up codec 45 debug statements
865d55c54b SCUMM: RA2: Remove goto and redundant seek
2e39caffe5 SCUMM: Remove extra blank lines from NutRenderer
e4ee27d61f SCUMM: RA2: Remove codec duplication
8b11f84c8d SCUMM: RA2: Add missing header
695edd8a79 SCUMM: RA2: Refactor embedded frame region blitting
5568808962 SCUMM: RA2: Harden embedded SAN loading
b1fe2f8fd0 SCUMM: RA2: Refactor level 2 background loading
3881b4371b SCUMM: RA: Pass data size to line update decoder
0bf0542235 SCUMM: RA2: Remove unused code and old TODOs
4ab5b29537 SCUMM: RA2: Remove remaining dead code
14f6c845da SCUMM: RA2: Use playLevelSegment in runlevels
6fae78c49d SCUMM: RA2: Use calculateAccuracy in runlevels
de00bf9dd4 SCUMM: RA2: Refactor explosion rendering
c6e257251e SCUMM: RA2: Remove remaining gotos
ae5c073de3 SCUMM: RA2: Deduplicate runlevel helpers
4879d743c6 SCUMM: RA2: Refactor wave handling
6eab30feed SCUMM: RA2: Split opcode 6 IACT handling
9092da7f16 SCUMM: RA2: Split opcode 8 handling
79342da6bc SCUMM: RA2: Split post-render handling
d90e9a159a SCUMM: RA2: Split handler 7 code
1da3f8d580 SCUMM: RA2: Use original cursor instead of generic one
d027a4158d SCUMM: RA2: Render full shooting rays
2771d282b9 SCUMM: RA1: Improve level 8 UI rendering
e11c594c4f SCUMM: RA1: Fix invisible cockpit in level 8 sections
6e7020314e SCUMM: RA1: Trigger damage in level 8
47e86512bc SCUMM: RA1: Improve level 8 comments
357f9ca51e SCUMM: RA1: Render SMUSH_CODEC_SKIP_RLE correctly
876fcfa145 SCUMM: RA1: Avoid trapping ScummVM menu during gameplay
a264d2921c SCUMM: RA1: Implement movement and damage for level 1 part 2
b45fc2804c SCUMM: RA1: Allow destroying targets in level 1 part 2
361d886ac7 SCUMM: RA1: Implement damage in level 2
03b63a2b5b SCUMM: RA1: Add missing damage sounds
b31651a76c SCUMM: RA1: Implement level 13
dfeb600867 SCUMM: RA1: Implement level 12
c079896afd SCUMM: RA1: Implement level 14
e94cfd3f51 SCUMM: RA1: Implement level 11 hits overlay
2efc01e655 SCUMM: RA1: Implement level 14 splice
6f800ee60d SCUMM: RA1: Implement level 15 summary and scoring
988fc5be0a SCUMM: RA1: Implement generic level summary and scoring
2b08e14c07 SCUMM: RA1: Add initial joystick support
d0cb0eea1f SCUMM: RA1: Improve menu code
b96a94011e SCUMM: RA1: Allow switching back to original input style
559b588ebf SCUMM: RA1: Implement missing chunks
c47a0c95a4 SCUMM: RA1: Improve HandleGameOp19 accuracy
e62919da5b SCUMM: RA1: Refactor laser rendering
7c868c8408 SCUMM: RA1: Implement password selection correctly
d9df04cc21 SCUMM: RA1: Add missing cutscenes
b55c73fea5 SCUMM: RA1: Avoid crash in handleGameAdjustCoords
c29ef27990 SCUMM: RA1: Fix level 14 frame object state checks
e5324c6080 SCUMM: RA1: Correct level names and tuning selectors
571e6bbc83 SCUMM: RA1: Improve level 13 controls
298177ad65 SCUMM: RA1: Render HUD only during active gameplay
24f60c962a SCUMM: RA1: Improve torpedo mode rendering
ec4cff3a96 SCUMM: RA1: Fix level 9 on-foot targeting
8565fdbda2 SCUMM: RA1: Seed random path selection deterministically
aa24c70839 SCUMM: RA1: Allow navigation in level 7
10feabc9da SCUMM: RA1: Refine navigation in level 7
aacaf1cdd7 SCUMM: RA1: Deduplicate runLevel handlers
625f0e9324 SCUMM: RA1: Extract codecs
0e4cfe0eed SCUMM: RA1: Split handleFrame
1c4b900883 SCUMM: RA1: Split playInteractiveVideo
3fc5813aa3 SCUMM: RA1: Refactor menu handling and rendering
9abca5775a SCUMM: RA: Extract common audio helpers
088825f217 SCUMM: RA1: Extract common iterator code
915b9dab5f SCUMM: RA1: Refactor handleGameOpcode
54fe61ff8d SCUMM: RA1: Render number of pilots left correctly
dee67a5319 SCUMM: RA1: Add basic save/load support
9e6422b20a SCUMM: RA2: Avoid direct OSystem mouse call


Commit: a9cc4b1243e11fb01df409ec519eae05132f92bb
    https://github.com/scummvm/scummvm/commit/a9cc4b1243e11fb01df409ec519eae05132f92bb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:42+02:00

Commit Message:
SCUMM: RA2: Add initial level 1 loading code

Changed paths:
  A engines/scumm/insane/insane_rebel.cpp
  A engines/scumm/insane/insane_rebel.h
    engines/scumm/detection.h
    engines/scumm/detection_tables.h
    engines/scumm/imuse_digi/dimuse_engine.cpp
    engines/scumm/insane/insane.cpp
    engines/scumm/insane/insane.h
    engines/scumm/module.mk
    engines/scumm/nut_renderer.cpp
    engines/scumm/nut_renderer.h
    engines/scumm/scumm-md5.h
    engines/scumm/scumm.cpp
    engines/scumm/scumm_v7.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 52da1fb5d4b..fbf94c37023 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -221,6 +221,7 @@ enum ScummGameId {
 	GID_CMI,
 	GID_DIG,
 	GID_FT,
+	GID_REBEL2,
 	GID_INDY3,
 	GID_INDY4,
 	GID_LOOM,
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 93916efc104..ced21e29990 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -72,6 +72,7 @@ static const PlainGameDescriptor gameDescriptions[] = {
 	{ "indyzak", "Indiana Jones and the Last Crusade & Zak McKracken" },
 	{ "zakloom", "Zak McKracken & Loom" },
 	{ "ft", "Full Throttle" },
+	{ "rebel2", "Star Wars: Rebel Assault II: The Hidden Empire" },
 	{ "dig", "The Dig" },
 	{ "comi", "The Curse of Monkey Island" },
 
@@ -226,6 +227,9 @@ static const GameSettings gameVariantsTable[] = {
 	{"ft",   "Remastered", 0, GID_FT,  7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"ft",   "Demo", 0, GID_FT,  7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 
+	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"dig",  "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"dig",  "Steam", "steam", GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -494,6 +498,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 	{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
 	{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
 
+	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, 0 },
+
 	{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
 
 	{ "activity", "activity", kGenHEPC, UNK_LANG, UNK, 0 },
diff --git a/engines/scumm/imuse_digi/dimuse_engine.cpp b/engines/scumm/imuse_digi/dimuse_engine.cpp
index 54e22b002d1..3c21ee194d3 100644
--- a/engines/scumm/imuse_digi/dimuse_engine.cpp
+++ b/engines/scumm/imuse_digi/dimuse_engine.cpp
@@ -59,7 +59,7 @@ IMuseDigital::IMuseDigital(ScummEngine_v7 *scumm, int sampleRate, Audio::Mixer *
 	}
 
 	_splayer = nullptr;
-	_isEarlyDiMUSE = (_vm->_game.id == GID_FT || (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO));
+	_isEarlyDiMUSE = (_vm->_game.id == GID_FT || (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO) || _vm->_game.id == GID_REBEL2);
 
 	if (_isEarlyDiMUSE) {
 		memset(_ftCrossfadeBuffer, 0, sizeof(_ftCrossfadeBuffer));
diff --git a/engines/scumm/insane/insane.cpp b/engines/scumm/insane/insane.cpp
index 5d8698ab3f9..e4ea4e0bece 100644
--- a/engines/scumm/insane/insane.cpp
+++ b/engines/scumm/insane/insane.cpp
@@ -184,7 +184,7 @@ void Insane::initvars() {
 		for (j = 0; j < 9; j++)
 			_enHdlVar[i][j] = 0;
 
-	for (i = 0; i < 0x80; i++)
+	for (i = 0; i < 0x200; i++)
 		_iactBits[i] = 0;
 
 
@@ -1264,7 +1264,7 @@ void Insane::smlayer_soundSetPriority(int32 soundId, int32 priority) {
 void Insane::smlayer_drawSomething(byte *renderBitmap, int32 codecparam,
 			   int32 x, int32 y, int32 arg_10, NutRenderer *nutfile,
 			   int32 c, int32 arg_1C, int32 arg_20) {
-	nutfile->drawFrame(renderBitmap, c, x, y);
+	nutfile->drawFrame(renderBitmap, c, x, y, codecparam); // codecparam appears to be pitch
 }
 
 void Insane::smlayer_overrideDrawActorAt(byte *arg_0, byte arg_4, byte arg_8) {
@@ -1357,13 +1357,13 @@ void Insane::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 }
 
 bool Insane::isBitSet(int n) {
-	assert (n < 0x80);
+	assert (n < 0x200);
 
 	return (_iactBits[n] != 0);
 }
 
 void Insane::setBit(int n) {
-	assert (n < 0x80);
+	assert (n < 0x200);
 
 	_iactBits[n] = 1;
 }
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index 4ec46f839f0..19382f85576 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -26,6 +26,9 @@
 
 #include "scumm/smush/smush_player.h"
 
+#include "common/list.h"
+#include "common/rect.h"
+
 namespace Scumm {
 
 #define INV_CHAIN    0
@@ -49,18 +52,19 @@ namespace Scumm {
 #define EN_BEN       9 // used only with handler
 
 class Insane {
- public:
-	Insane(ScummEngine_v7 *scumm);
-	~Insane();
+public:
+	Insane() {};
+    Insane(ScummEngine_v7 *scumm);
+	virtual ~Insane();
 
 	void setSmushParams(int speed);
 	void setSmushPlayer(SmushPlayer *player);
 	void runScene(int arraynum);
 
 	void procPreRendering();
-	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	void virtual procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 						   int32 setupsan13, int32 curFrame, int32 maxFrame);
-	void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	void virtual procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags, int16 par1,
 				  int16 par2, int16 par3, int16 par4);
 	void procSKIP(int32 subSize, Common::SeekableReadStream &b);
@@ -69,7 +73,7 @@ class Insane {
 	bool isInsaneActive() { return _insaneIsRunning; }
 	void syncCurrentSanFlags();
 
- private:
+ protected:
 
 	ScummEngine_v7 *_vm;
 	SmushPlayer *_player;
@@ -154,7 +158,7 @@ class Insane {
 	int16 _smush_frameNum2;
 	byte _smush_earlyFluContents[0x31a];
 	int16 _enemyState[10][10];
-	byte _iactBits[0x80];
+	byte _iactBits[0x200];
 	int16 _mainRoadPos;
 	int16 _posBrokenCar;
 	int16 _posBrokenTruck;
@@ -421,7 +425,7 @@ class Insane {
 	void actor10Reaction(int32 buttons);
 	int32 actionEnemy();
 	int32 processKeyboard();
-	int32 processMouse();
+	int32 virtual processMouse();
 	void setEnemyAnimation(int32 actornum, int anim);
 	void chooseEnemyWeaponAnim(int32 buttons);
 	void switchEnemyWeapon();
@@ -447,12 +451,22 @@ class Insane {
 	void iactScene21(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 				  int16 par1, int16 par2, int16 par3, int16 par4);
-	bool isBitSet(int n);
-	void setBit(int n);
+	bool virtual isBitSet(int n);
+	void virtual setBit(int n);
 	void clearBit(int n);
 	void chooseEnemy();
 	void removeEmptyEnemies();
 	void removeEnemyFromMetList(int32);
+
+ public:
+
+	bool virtual shouldSkipFrameUpdate(int left, int top, int width, int height) { 
+		return false; 
+	};
+
+	void virtual loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
+		// Nothing by default
+	};
 };
 } // End of namespace Insane
 
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
new file mode 100644
index 00000000000..f1d72aa578d
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -0,0 +1,822 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+
+#include "engines/engine.h"
+#include "common/system.h"
+#include "common/memstream.h"
+
+#include "scumm/actor.h"
+#include "scumm/file.h"
+#include "scumm/resource.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/sound.h"
+
+#include "scumm/imuse/imuse.h"
+#include "scumm/imuse_digi/dimuse_engine.h"
+
+#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_font.h"
+
+#include "scumm/insane/insane_rebel.h"
+
+
+namespace Scumm {
+
+InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
+	_vm = scumm;
+	// Rebel Assault 2 specific initialization can go here
+	_rebelHudPrimary = nullptr;
+	_rebelHudSecondary = nullptr;
+	_rebelHudCockpit = nullptr;
+	_rebelHudExplosion = nullptr;
+	_rebelHudDamage = nullptr;
+	_rebelHudEffects = nullptr;
+	_rebelHudHiRes = nullptr;
+
+	// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
+	// CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
+	// For now, use CPITIMAG since the game runs at 320x200
+	_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
+	_smush_icons2Nut = nullptr;  // Not used for Rebel2
+	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
+
+	_rebelEnemies.clear();
+	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
+	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
+}
+
+
+InsaneRebel2::~InsaneRebel2() {
+	delete _rebelHudPrimary;
+	delete _rebelHudSecondary;
+	delete _rebelHudCockpit;
+	delete _rebelHudExplosion;
+	delete _rebelHudDamage;
+	delete _rebelHudEffects;
+	delete _rebelHudHiRes;
+}
+
+
+int32 InsaneRebel2::processMouse() {
+	int32 buttons = 0;
+
+	// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
+	static bool wasPressed = false;
+	bool isPressed = (_vm->_system->getEventManager()->getButtonState() & 1) != 0;
+	
+	// Edge detection: only trigger on button press (not hold)
+	if (isPressed && !wasPressed) {
+		Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
+		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d", 
+			mousePos.x, mousePos.y, _rebelEnemies.size());
+
+		// Check for hit on any active enemy
+		Common::List<RebelEnemy>::iterator it;
+		for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+			debug("  Enemy ID=%d active=%d destroyed=%d rect=(%d,%d)-(%d,%d) contains=%d",
+				it->id, it->active, it->destroyed,
+				it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
+				it->rect.contains(mousePos));
+				
+			if (it->active && it->rect.contains(mousePos)) {
+				// Enemy hit!
+				it->active = false;
+				it->destroyed = true;  // Mark as destroyed so IACT won't re-activate
+				it->explosionFrame = 0;  // Start explosion animation
+				it->explosionComplete = false;  // Explosion not yet finished
+				debug("Rebel2: HIT enemy ID=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)", 
+					it->id, mousePos.x, mousePos.y,
+					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
+
+				// Note: Background saving and masking is handled in procPostRendering
+				// where we have access to the render bitmap
+				// TODO: Play explosion sound
+				// TODO: Update score
+				// Only hit one enemy per click
+				break;
+			}
+		}
+	}
+	wasPressed = isPressed;
+	return buttons;
+}
+
+bool InsaneRebel2::shouldSkipFrameUpdate(int left, int top, int width, int height) {
+	// Only check for Rebel2
+	if (_vm->_game.id != GID_REBEL2) {
+		return false;
+	}
+	
+	Common::Rect updateRect(left, top, left + width, top + height);
+	int updateArea = width * height;
+	
+	// Check if this update region significantly overlaps with any destroyed enemy
+	Common::List<RebelEnemy>::iterator it;
+	for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+		if (it->destroyed) {
+			// Calculate the intersection of the update rect and enemy rect
+			Common::Rect enemyRect = it->rect;
+			
+			if (updateRect.intersects(enemyRect)) {
+				// Calculate the intersection area
+				int intLeft = MAX(updateRect.left, enemyRect.left);
+				int intTop = MAX(updateRect.top, enemyRect.top);
+				int intRight = MIN(updateRect.right, enemyRect.right);
+				int intBottom = MIN(updateRect.bottom, enemyRect.bottom);
+				int intArea = (intRight - intLeft) * (intBottom - intTop);
+				
+				// Require at least 70% overlap to skip the update
+				// This prevents unrelated frame updates from being incorrectly skipped
+				if (intArea * 100 >= updateArea * 70) {
+					debug("Rebel2: Skipping frame update (%d,%d %dx%d) - %d%% overlap with destroyed enemy ID=%d",
+						left, top, width, height, (intArea * 100) / updateArea, it->id);
+					return true;
+				}
+			}
+		}
+	}
+	
+	return false;
+}
+
+bool InsaneRebel2::isBitSet(int n) {
+	assert (n < 0x200);
+
+	return (_iactBits[n] != 0);
+}
+
+void InsaneRebel2::setBit(int n) {
+	assert (n < 0x200);
+
+	_iactBits[n] = 1;
+}
+
+void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+					  int16 par1, int16 par2, int16 par3, int16 par4) {
+	if (_keyboardDisable)
+		return;
+
+	if (_currSceneId == 1)
+		iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
+}
+
+
+void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+				  int16 par1, int16 par2, int16 par3, int16 par4) {
+	// par1 is the Opcode (word at offset +0)
+	// par2 is word at offset +2
+	// par3 is word at offset +4
+	// par4 is word at offset +6
+	//
+	// Based on disassembly of FUN_4028C5 and FUN_4033CF:
+	// 
+	// For IACT opcode 4 (enemy position update), the structure is:
+	//   Offset +0x06: Type/SubType (par3)
+	//   Offset +0x08: Enemy ID
+	//   Offset +0x0A: X position
+	//   Offset +0x0C: Y position
+	//   Offset +0x0E: Width
+	//   Offset +0x10: Height
+	//
+	// The original game calculates bounding box center:
+	//   centerX = X + (Width / 2)
+	//   centerY = Y + (Height / 2)
+	// Then subtracts scroll offsets:
+	//   screenX = centerX - DAT_0043e006 (scrollX)
+	//   screenY = centerY - DAT_0043e008 (scrollY)
+
+	if (par1 == 4) {
+		// Opcode 4: Enemy position update
+		// Read 5 shorts from the stream (offset +8 through +16)
+		int16 enemyId = b.readSint16LE();  // Offset +8
+		int16 x = b.readSint16LE();        // Offset +10 (0x0A)
+		int16 y = b.readSint16LE();        // Offset +12 (0x0C)
+		int16 w = b.readSint16LE();        // Offset +14 (0x0E) - Width
+		int16 h = b.readSint16LE();        // Offset +16 (0x10) - Height
+
+		// The disassembly shows half-width/half-height are used for centering:
+		//   halfW = w >> 1
+		//   halfH = h >> 1
+		//   centerX = x + halfW
+		//   centerY = y + halfH
+		// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
+		
+		//debug("Rebel2 IACT Opcode 4: ID=%d X=%d Y=%d W=%d H=%d (par2=%d par3=%d par4=%d)", 
+		//	enemyId, x, y, w, h, par2, par3, par4);
+
+		// Update RebelEnemy list for hit detection
+		bool found = false;
+		Common::List<RebelEnemy>::iterator it;
+		for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+			if (it->id == enemyId) {
+				it->rect = Common::Rect(x, y, x + w, y + h);
+				// Only re-activate if not destroyed
+				if (!it->destroyed) {
+					it->active = true;
+				}
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			RebelEnemy e;
+			e.id = enemyId;
+			e.rect = Common::Rect(x, y, x + w, y + h);
+			e.active = true;
+			e.destroyed = false;
+			e.explosionFrame = -1;  // No explosion playing
+			_rebelEnemies.push_back(e);
+		}
+
+		// Render Bounding Box for visual verification
+		// Use player dimensions for clipping
+		int bufWidth = (_player && _player->_width > 0) ? _player->_width : 320;
+		int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+		int pitch = bufWidth;
+		
+		// Draw the bounding box in color 255 (bright white) - only for non-destroyed enemies
+		// Skip drawing if this enemy has been destroyed
+		if (renderBitmap && w > 0 && h > 0 && !found) {
+			// Only draw for newly created enemies (not destroyed ones)
+			drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
+		} else if (found && !it->destroyed) {
+			// Draw for existing non-destroyed enemies
+			drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
+		}
+
+		// Debug for potential HUD Setup (Opcode 4 with word at 8 == 1)
+		if (enemyId == 1) {
+			debug("Rebel2 IACT Opcode 4: HUD Setup Candidate? (enemyId/val at 8 == 1). par4=%d", par4);
+		}
+
+	} else if (par1 == 2) {
+		// Opcode 2: Often used for bit setting
+		// Disassembly shows this is handled but we don't have full context
+		debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
+		
+	} else if (par1 == 3) {
+		// Opcode 3: Often used for clearing/resetting
+		debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
+		
+	} else if (par1 == 5) {
+		// Opcode 5: Special handling based on par2 value
+		// Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
+		debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
+		
+	} else if (par1 == 6) {
+		// Opcode 6: Scene trigger / mode switch
+		// Disassembly shows it sets DAT_0047ee84 (handler type) to par2 for values 7, 8, 0x19, 0x26
+		// This determines which rendering handler and crosshair sprite to use
+		// par3 is stored as DAT_004436de (level type) which affects HUD sprite selection
+		
+		// Update handler type if par2 is a known handler value
+		if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
+			_rebelHandler = par2;
+			_rebelLevelType = par3;  // Store level type (affects HUD sprite: 5 vs 53)
+			debug("Rebel2 IACT Opcode 6: Setting handler=%d levelType=%d (par4=%d)", par2, par3, par4);
+		}
+		
+		// Check for Status Bar Enable trigger (par4 == 1 means enable status bar drawing)
+		// Based on FUN_407FCB line 79: if (param_5[4] == 1) { FUN_0040bb87(...) }
+		// Note: Sprite 5 is a 136x13 status bar, NOT the full cockpit (which is baked into video)
+		if (par4 == 1) {
+			debug("Rebel2 IACT Opcode 6: Status Bar ENABLED - will draw sprite %d (136x13) from CPITIMAG.NUT", 
+				(_rebelLevelType == 5) ? 53 : 5);
+		}
+		
+		// Debug: Confirm handler type after update
+		if (_rebelHandler == 0x26) {
+			debug("Rebel2: Handler set to TURRET (0x26/38) - status bar sprite 5, crosshair sprites 48+");
+		}
+
+	} else if (par1 == 8) {
+		// TODO
+	} else if (par1 == 9) {
+		// Opcode 9: Text/subtitle display
+		debug("Rebel2 IACT Opcode 9: par2=%d par3=%d par4=%d (text)", par2, par3, par4);
+		
+	} else if (par1 == 0 || par1 == 1) {
+		// Low Opcodes seen in logs
+		debug("Rebel2 IACT: Low Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
+	} else {
+		debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
+	}
+}
+
+void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
+	// Validate userId (1-4 for HUD slots)
+	if (userId < 1 || userId > 4 || !animData || size < 32) {
+		debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
+		return;
+	}
+	
+	// Parse the embedded ANIM structure
+	// Format: ANIM (4 bytes) + size (4 bytes BE) + AHDR (4 bytes) + size (4 bytes BE) + ...
+	Common::MemoryReadStream stream(animData, size);
+	
+	// Read ANIM header
+	uint32 animTag = stream.readUint32BE();
+	if (animTag != MKTAG('A','N','I','M')) {
+		debug("Rebel2: Embedded SAN missing ANIM tag, got 0x%08X", animTag);
+		return;
+	}
+	uint32 animSize = stream.readUint32BE();
+	debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
+	
+	// Read AHDR header
+	uint32 ahdrTag = stream.readUint32BE();
+	if (ahdrTag != MKTAG('A','H','D','R')) {
+		debug("Rebel2: Embedded SAN missing AHDR tag, got 0x%08X", ahdrTag);
+		return;
+	}
+	uint32 ahdrSize = stream.readUint32BE();
+	
+	// Skip AHDR content (palette etc)
+	stream.skip(ahdrSize);
+	if (ahdrSize & 1) stream.skip(1);  // Padding
+	
+	// Look for first FRME
+	while (!stream.eos() && stream.pos() < size) {
+		uint32 tag = stream.readUint32BE();
+		uint32 chunkSize = stream.readUint32BE();
+		
+		if (tag == MKTAG('F','R','M','E')) {
+			debug("Rebel2: Found FRME in embedded SAN, size=%u", chunkSize);
+			
+			// Parse FRME content to find FOBJ
+			int64 frmeEnd = stream.pos() + chunkSize;
+			
+			while (stream.pos() < frmeEnd && !stream.eos()) {
+				uint32 subTag = stream.readUint32BE();
+				uint32 subSize = stream.readUint32BE();
+				
+				if (subTag == MKTAG('F','O','B','J')) {
+					debug("Rebel2: Found FOBJ in embedded SAN, size=%u", subSize);
+					
+					// Read FOBJ header
+					int codec = stream.readUint16LE();
+					int left = stream.readUint16LE();
+					int top = stream.readUint16LE();
+					int width = stream.readUint16LE();
+					int height = stream.readUint16LE();
+					stream.readUint16LE();  // unknown
+					stream.readUint16LE();  // unknown
+					
+					debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d", 
+						userId, width, height, left, top, codec);
+					
+					// Allocate storage for the decoded frame
+					EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
+					frame.clear();
+					
+					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
+						frame.pixels = (byte *)calloc(width * height, 1);
+						frame.width = width;
+						frame.height = height;
+						
+						// Use the left/top from FOBJ header as render position
+						// These are the exact screen coordinates for the HUD overlay
+						// The FOBJ header specifies where this frame should be composited
+						frame.renderX = left;
+						frame.renderY = top;
+						
+						// Read the raw FOBJ data
+						int32 dataSize = subSize - 14;
+						if (dataSize > 0) {
+							byte *fobjData = (byte *)malloc(dataSize);
+							stream.read(fobjData, dataSize);
+							
+							// Decode based on codec
+							// Codec 1 = RLE, Codec 3 = raw, Codec 21 = line update, Codec 45 = block delta
+							if (codec == 1) {
+								// RLE decode using SMUSH BOMP format
+								// Each line has a 16-bit size header, then BOMP-encoded data
+								// BOMP format: code byte, then data
+								//   code & 1 == 1: fill mode, next byte repeated (code>>1)+1 times
+								//   code & 1 == 0: copy mode, copy next (code>>1)+1 bytes
+								byte *srcPtr = fobjData;
+								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
+									int lineSize = READ_LE_UINT16(srcPtr);
+									srcPtr += 2;
+									byte *lineEnd = srcPtr + lineSize;
+									byte *lineDst = frame.pixels + row * width;
+									int remaining = width;
+									
+									while (srcPtr < lineEnd && remaining > 0) {
+										byte code = *srcPtr++;
+										int num = (code >> 1) + 1;
+										if (num > remaining)
+											num = remaining;
+										remaining -= num;
+										
+										if (code & 1) {
+											// Fill mode: repeat next byte
+											byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
+											memset(lineDst, color, num);
+											lineDst += num;
+										} else {
+											// Copy mode: copy next bytes
+											for (int i = 0; i < num && srcPtr < lineEnd; i++) {
+												*lineDst++ = *srcPtr++;
+											}
+										}
+									}
+									srcPtr = lineEnd;
+								}
+								frame.valid = true;
+								debug("Rebel2: Decoded embedded HUD (codec 1/RLE): %dx%d", width, height);
+							} else if (codec == 3 || codec == 20) {
+								// Uncompressed - direct copy
+								int copySize = MIN(dataSize, width * height);
+								memcpy(frame.pixels, fobjData, copySize);
+								frame.valid = true;
+								debug("Rebel2: Decoded embedded HUD (codec %d/raw): %dx%d", codec, width, height);
+							} else if (codec == 21 || codec == 44) {
+								// Codec 21/44: Line update codec
+								// Format: For each line: 
+								//   16-bit line data size
+								//   Line data: pairs of (16-bit skip, 16-bit count-1, pixels[count])
+								byte *srcPtr = fobjData;
+								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
+									int lineDataSize = READ_LE_UINT16(srcPtr);
+									srcPtr += 2;
+									byte *lineEnd = srcPtr + lineDataSize;
+									byte *lineDst = frame.pixels + row * width;
+									int x = 0;
+									while (srcPtr < lineEnd && x < width) {
+										int skip = READ_LE_UINT16(srcPtr);
+										srcPtr += 2;
+										x += skip;
+										if (srcPtr >= lineEnd) break;
+										int count = READ_LE_UINT16(srcPtr) + 1;
+										srcPtr += 2;
+										while (count-- > 0 && x < width && srcPtr < lineEnd) {
+											lineDst[x++] = *srcPtr++;
+										}
+									}
+									srcPtr = lineEnd;
+								}
+								frame.valid = true;
+								debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
+							} else if (codec == 45) {
+								// Codec 45: Similar to codec 47 but simpler - block delta
+								// For embedded HUD use, try simple raw copy as fallback
+								// This codec appears in small sprites (11x26, 17x53)
+								int copySize = MIN(dataSize, width * height);
+								memcpy(frame.pixels, fobjData, copySize);
+								frame.valid = true;
+								debug("Rebel2: Decoded embedded HUD (codec 45/block): %dx%d", width, height);
+							} else {
+								debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
+								// For now, mark as invalid but keep dimensions
+								frame.valid = false;
+							}
+							
+							// Draw immediately to renderBitmap if valid
+							if (frame.valid && renderBitmap) {
+								int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+								int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+								
+								for (int y = 0; y < height && (frame.renderY + y) < bufHeight; y++) {
+									for (int x = 0; x < width && (frame.renderX + x) < pitch; x++) {
+										byte pixel = frame.pixels[y * width + x];
+										if (pixel != 0) {  // 0 = transparent
+											int destX = frame.renderX + x;
+											int destY = frame.renderY + y;
+											if (destX >= 0 && destY >= 0) {
+												renderBitmap[destY * pitch + destX] = pixel;
+											}
+										}
+									}
+								}
+								debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
+							}
+							
+							free(fobjData);
+						}
+					}
+					
+					return;  // Found and processed FOBJ
+				} else {
+					// Skip other sub-chunks
+					stream.skip(subSize);
+					if (subSize & 1) stream.skip(1);
+				}
+			}
+		} else {
+			// Skip non-FRME chunks
+			stream.skip(chunkSize);
+			if (chunkSize & 1) stream.skip(1);
+		}
+	}
+	
+	debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
+}
+
+
+void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
+
+	processMouse();
+
+	// Determine correct pitch for the video buffer (usually 320 for Rebel2)
+	int width = _player->_width;
+	int height = _player->_height;
+	if (width == 0) width = _vm->_screenWidth;
+	if (height == 0) height = _vm->_screenHeight;
+	int pitch = width;
+
+	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
+	// Based on FUN_004089ab:
+	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
+	// 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
+	// 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
+	//
+	// In FUN_0041c012:
+	// - Sprites are drawn to buffer DAT_00482204 at position (0,0)
+	// - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
+	// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
+	//
+	// For ScummVM, we draw directly to screen at Y=180
+	
+	// Use video content coordinates, NOT buffer coordinates
+	const int videoWidth = 320;    // Native video width
+	const int videoHeight = 200;   // Native video height  
+	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
+	
+	// ============================================================
+	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
+	// ============================================================
+	// Clear the status bar area at Y=180-199 with background color
+	// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
+	// This fills width=320, height=20 starting at Y=180 with color index 4
+	const byte statusBarBgColor = 4;
+	for (int y = statusBarY; y < videoHeight && y < height; y++) {
+		for (int x = 0; x < videoWidth && x < pitch; x++) {
+			renderBitmap[y * pitch + x] = statusBarBgColor;
+		}
+	}
+	
+	// ============================================================
+	// STEP 1: Draw embedded SAN cockpit overlay FIRST (from IACT chunks)
+	// ============================================================
+	// The cockpit overlay forms the decorative frame at the bottom
+	// userId 1: Left piece at X=0
+	// userId 2: Right piece at X=slot1Width
+	// These are drawn BEFORE the status bar so status bar appears on top
+	
+	int slot1Width = 0;
+	if (_rebelEmbeddedHud[1].valid && _rebelEmbeddedHud[1].width > 0) {
+		slot1Width = _rebelEmbeddedHud[1].width;
+	}
+	
+	for (int hudSlot = 1; hudSlot <= 2; hudSlot++) {
+		EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
+		if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
+			int destX, destY;
+			
+			// Position the two HUD pieces horizontally adjacent
+			if (hudSlot == 1) {
+				destX = 0;
+			} else {
+				destX = slot1Width;
+			}
+			
+			// Position at bottom of 320x200 video content
+			// The cockpit overlay sits at Y = 200 - frameHeight
+			destY = statusBarY - frame.height;
+			if (destY < 0) destY = 0;
+			
+			// Draw frame with transparency (pixel 0 = transparent)
+			for (int y = 0; y < frame.height && (destY + y) < height; y++) {
+				for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
+					byte pixel = frame.pixels[y * frame.width + x];
+					if (pixel != 0) {  // Skip transparent pixels
+						int fx = destX + x;
+						int fy = destY + y;
+						if (fx >= 0 && fy >= 0) {
+							renderBitmap[fy * pitch + fx] = pixel;
+						}
+					}
+				}
+			}
+			
+			static bool debugEmbeddedOnce[5] = {false};
+			if (!debugEmbeddedOnce[hudSlot]) {
+				debug("Rebel2: Drawing embedded HUD slot %d (%dx%d) at (%d,%d)", 
+					hudSlot, frame.width, frame.height, destX, destY);
+				debugEmbeddedOnce[hudSlot] = true;
+			}
+		}
+	}
+	
+	// ============================================================
+	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
+	// ============================================================
+	// DISPFONT.NUT contains status bar elements - drawn ON TOP of cockpit overlay
+	// From assembly FUN_0041c012:
+	//   - Sprite 1: Status bar background frame (full width, drawn at 0,0)
+	//   - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
+	//   - Sprite 6: Shield bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
+	//   - Sprite 7: Shield alert (flashing red when shields critical)
+	//
+	// All sprites are drawn to buffer at (0,0), buffer composited at Y=180
+	// For ScummVM, we draw directly at Y=statusBarY
+	if (_smush_cockpitNut) {
+		// Debug: Log DISPFONT.NUT sprite info once
+		static bool loggedDispfont = false;
+		if (!loggedDispfont) {
+			int numSprites = _smush_cockpitNut->getNumChars();
+			debug("Rebel2: DISPFONT.NUT has %d sprites, statusBarY=%d:", numSprites, statusBarY);
+			for (int i = 0; i < numSprites && i < 10; i++) {
+				int sw = _smush_cockpitNut->getCharWidth(i);
+				int sh = _smush_cockpitNut->getCharHeight(i);
+				debug("  Sprite %d: %dx%d", i, sw, sh);
+			}
+			loggedDispfont = true;
+		}
+		
+		// Draw status bar background frame (sprite 1) at (0, statusBarY)
+		// This sprite is the full-width status bar background
+		if (_smush_cockpitNut->getNumChars() > 1) {
+			smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 1, 0, 0);
+		}
+		
+		// Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
+		// Sprite index = difficulty + 2; capped at 4 max difficulty (sprite 5)
+		// Assembly draws at (0,0) in buffer - same position as sprite 1
+		int difficulty = 0;  // TODO: Read from game state (DAT_0047a7fa)
+		if (difficulty > 3) difficulty = 3;
+		int difficultySprite = difficulty + 2;  // sprites 2, 3, 4, or 5
+		if (_smush_cockpitNut->getNumChars() > difficultySprite) {
+			smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
+		}
+		
+		// Draw shield bar (sprite 6) 
+		// Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
+		// The width is scaled based on shield value (param_1 >> 2)
+		// For now, draw at position (0, statusBarY) - sprite has internal positioning
+		if (_smush_cockpitNut->getNumChars() > 6) {
+			smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 6, 0, 0);
+		}
+		
+		// Draw shield alert overlay (sprite 7) when shields critical (> 0xAA = 170)
+		// Only draws when frame counter bit 3 is clear (every 8 frames)
+		// For now, skip - TODO: implement shield critical flashing
+		
+		// Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
+		// Uses sprite 1 again with different clip rect
+		// TODO: Implement lives rendering
+		
+		// Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
+		// TODO: Implement score rendering
+	} else {
+		static bool warnedNullOnce = false;
+		if (!warnedNullOnce) {
+			debug("Rebel2: WARNING - _smush_cockpitNut (DISPFONT.NUT) is null!");
+			warnedNullOnce = true;
+		}
+	}
+
+	// Debug: Draw bounding boxes for enemies
+	// width/height/pitch already calculated above
+
+	// Debug: Verify buffer format and drawing capability
+	static uint32 lastDebugTime = 0;
+	if ((_vm->_system->getMillis() - lastDebugTime) > 2000) {
+		lastDebugTime = _vm->_system->getMillis();
+		debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _rebelEnemies.size());
+	}
+
+	Common::List<RebelEnemy>::iterator it;
+	for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+		Common::Rect r = it->rect;
+		
+		// Clip the rect to screen bounds for safety
+		Common::Rect clipped = r;
+		clipped.clip(Common::Rect(0, 0, width, height));
+
+		if (!clipped.isValidRect()) continue;
+		
+		if (it->destroyed) {
+			// Handle destroyed enemies - draw explosion animation
+			// The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
+			
+			// Draw explosion animation - only while animation is in progress
+			// CPITIMAG.NUT indices 9-41 contain explosion frames, but we only use 8 for a quick effect
+			const int EXPLOSION_FRAMES = 8;
+			if (it->explosionFrame >= 0 && it->explosionFrame < EXPLOSION_FRAMES) {
+				if (_smush_iconsNut) {
+					int explosionSpriteIndex = 9 + it->explosionFrame;
+					if (_smush_iconsNut->getNumChars() > explosionSpriteIndex) {
+						int ew = _smush_iconsNut->getCharWidth(explosionSpriteIndex);
+						int eh = _smush_iconsNut->getCharHeight(explosionSpriteIndex);
+						// Center explosion on the enemy bounding box
+						int cx = (r.left + r.right) / 2 - ew / 2;
+						int cy = (r.top + r.bottom) / 2 - eh / 2;
+						smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, explosionSpriteIndex, 0, 0);
+					}
+				}
+				
+				// Advance explosion frame
+				it->explosionFrame++;
+				
+				// Check if explosion is now complete
+				if (it->explosionFrame >= EXPLOSION_FRAMES) {
+					it->explosionFrame = -1;  // Mark as done - prevents any further animation
+					it->explosionComplete = true;
+					debug("Rebel2: Explosion complete for enemy ID=%d", it->id);
+				}
+			}
+			// After explosion is complete (explosionFrame == -1), do nothing
+		} else if (it->active) {
+			// Draw bounding box outline for active enemies (debug visualization)
+			// Draw Top
+			if (r.top >= 0 && r.top < height) {
+				for (int x = clipped.left; x < clipped.right; x++) {
+					renderBitmap[r.top * pitch + x] = 255;
+				}
+			}
+			// Draw Bottom
+			if (r.bottom > 0 && r.bottom <= height) {
+				for (int x = clipped.left; x < clipped.right; x++) {
+					renderBitmap[(r.bottom - 1) * pitch + x] = 255;
+				}
+			}
+			// Draw Left
+			if (r.left >= 0 && r.left < width) {
+				for (int y = clipped.top; y < clipped.bottom; y++) {
+					renderBitmap[y * pitch + r.left] = 255;
+				}
+			}
+			// Draw Right
+			if (r.right > 0 && r.right <= width) {
+				for (int y = clipped.top; y < clipped.bottom; y++) {
+					renderBitmap[y * pitch + (r.right - 1)] = 255;
+				}
+			}
+		}
+	}
+
+	// Draw Crosshair/Reticle cursor
+	// Sprite indices based on handler type (from original game disassembly):
+	// - Handler 8 (ground vehicle): Index 0x2E (46)
+	// - Handler 7 (space flight): Index 0x2F (47)
+	// - Handler 0x19 (mixed/turret view): Index 0x2F (47)
+	// - Handler 0x26 (full turret): Index 0x30+ (48+) with animation
+	if (_smush_iconsNut) {
+		//CursorMan.showMouse(false);
+		int reticleIndex;
+		switch (_rebelHandler) {
+		case 7:   // Space flight
+		case 0x19: // Mixed/turret view
+			reticleIndex = 47;  // 0x2F
+			break;
+		case 0x26: // Full turret (with animation - simplified for now)
+			reticleIndex = 48;  // 0x30
+			break;
+		case 8:   // Ground vehicle (Level 1) - FALLTHROUGH
+		default:
+			reticleIndex = 46;  // 0x2E
+			break;
+		}
+		if (_smush_iconsNut->getNumChars() > reticleIndex) {
+			int cw = _smush_iconsNut->getCharWidth(reticleIndex);
+			int ch = _smush_iconsNut->getCharHeight(reticleIndex);
+			
+			static bool debugCrosshairOnce = false;
+			if (!debugCrosshairOnce) {
+				debug("Rebel2: Drawing crosshair sprite %d at (%d,%d) size %dx%d", 
+					reticleIndex, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, cw, ch);
+				debugCrosshairOnce = true;
+			}
+			
+			// Center the crosshair on mouse position
+			smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, 0, _smush_iconsNut, reticleIndex, 0, 0);
+		}
+	}
+}
+
+}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
new file mode 100644
index 00000000000..00b41865c06
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel.h
@@ -0,0 +1,171 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined(SCUMM_INSANE_REBEL_H) && defined(ENABLE_SCUMM_7_8)
+#define SCUMM_INSANE_REBEL_H
+
+#include "scumm/nut_renderer.h"
+
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane.h"
+
+#include "common/list.h"
+#include "common/rect.h"
+
+namespace Scumm {
+
+class InsaneRebel2 : public Insane {
+
+ 	// Helper for debugging IACT boxes
+	void drawRect(byte *dst, int pitch, int x, int y, int w, int h, byte color, int bufWidth, int bufHeight) {
+		Common::Rect r(x, y, x + w, y + h);
+		Common::Rect screen(0, 0, bufWidth, bufHeight);
+		Common::Rect clipped = r;
+		clipped.clip(screen);
+
+		if (!clipped.isValidRect()) return;
+
+		// Top
+		if (r.top >= 0 && r.top < bufHeight) {
+			int startX = MAX((int)r.left, 0);
+			int endX = MIN((int)r.right, bufWidth);
+			for (int k = startX; k < endX; k++) dst[r.top * pitch + k] = color;
+		}
+		// Bottom
+		if (r.bottom > 0 && r.bottom <= bufHeight) {
+			int startX = MAX((int)r.left, 0);
+			int endX = MIN((int)r.right, bufWidth);
+			for (int k = startX; k < endX; k++) dst[(r.bottom - 1) * pitch + k] = color;
+		}
+		// Left
+		if (r.left >= 0 && r.left < bufWidth) {
+			int startY = MAX((int)r.top, 0);
+			int endY = MIN((int)r.bottom, bufHeight);
+			for (int k = startY; k < endY; k++) dst[k * pitch + r.left] = color;
+		}
+		// Right
+		if (r.right > 0 && r.right <= bufWidth) {
+			int startY = MAX((int)r.top, 0);
+			int endY = MIN((int)r.bottom, bufHeight);
+			for (int k = startY; k < endY; k++) dst[k * pitch + (r.right - 1)] = color;
+		}
+	}
+
+public:
+	InsaneRebel2(ScummEngine_v7 *scumm);
+	~InsaneRebel2();
+
+	NutRenderer *_smush_cockpitNut;
+	NutRenderer *_smush_dispfontNut;  // DAT_00482200 - DISPFONT.NUT for status bar (difficulty, shields, lives, score)
+	
+	// Rebel Assault 2: Dynamically loaded HUD overlays (from CHK scripts)
+	// These correspond to the original game's global variables
+	NutRenderer *_rebelHudPrimary;     // DAT_00482240 - Primary HUD overlay
+	NutRenderer *_rebelHudSecondary;   // DAT_00482238 - Secondary HUD graphics
+	NutRenderer *_rebelHudCockpit;     // DAT_00482268 - Ship cockpit frame overlay
+	NutRenderer *_rebelHudExplosion;   // DAT_00482250 - Explosion overlay sprites
+	NutRenderer *_rebelHudDamage;      // DAT_00482248 - Damage indicator sprites
+	NutRenderer *_rebelHudEffects;     // DAT_00482258 - Additional effects
+	NutRenderer *_rebelHudHiRes;       // DAT_00482260 - High-res HUD alternative
+
+	int32 processMouse() override;
+	bool isBitSet(int n) override;
+	void setBit(int n) override;
+
+	void iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+				  int16 par1, int16 par2, int16 par3, int16 par4);
+
+	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+							   int32 setupsan13, int32 curFrame, int32 maxFrame) override;
+
+	void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+					  int16 par1, int16 par2, int16 par3, int16 par4) override;
+
+	struct RebelEnemy {
+		int id;
+		Common::Rect rect;
+		bool active;
+		bool destroyed;           // Set when enemy is shot - prevents re-activation
+		int explosionFrame;       // Current explosion animation frame (0-32, -1 = done)
+		bool explosionComplete;   // True when explosion animation has finished
+		byte *savedBackground;    // Saved background pixels at moment of destruction
+		int savedBgWidth;         // Width of saved background
+		int savedBgHeight;        // Height of saved background
+		
+		RebelEnemy() : id(0), active(false), destroyed(false), explosionFrame(-1),
+		               explosionComplete(false), savedBackground(nullptr), 
+		               savedBgWidth(0), savedBgHeight(0) {}
+		~RebelEnemy() { free(savedBackground); }
+	};
+
+	Common::List<RebelEnemy> _rebelEnemies;
+	
+	// Current handler type for Rebel Assault 2 (determines crosshair sprite)
+	// Handler 0: Background only
+	// Handler 7: Space flight - uses crosshair sprite 0x2F (47)
+	// Handler 8: Ground vehicle - uses crosshair sprite 0x2E (46)  
+	// Handler 0x19: Mixed/turret view - uses crosshair sprite 0x2F (47)
+	// Handler 0x26: Full turret - crosshair varies by level type
+	int _rebelHandler;
+	
+	// Level type from IACT opcode 6 par3 (corresponds to DAT_004436de)
+	// Determines crosshair variant for turret mode:
+	// - levelType == 5: Use sprites 0x30+ (48+) for crosshair
+	// - levelType != 5: Use sprites 0-3 for crosshair (with animation)
+	int _rebelLevelType;
+
+	// Status bar sprite index (5 or 53) triggered by Opcode 6 par4
+	// 0 = disabled
+	int _rebelStatusBarSprite;
+
+	// Embedded SAN HUD overlays (extracted from IACT chunks)
+	// These are decoded frame buffers from embedded ANIM data
+	// Slots 1-4 correspond to userId in the IACT wrapper
+	struct EmbeddedSanFrame {
+		byte *pixels;      // Decoded frame pixels (8-bit indexed)
+		int width;         // Frame width
+		int height;        // Frame height
+		int renderX;       // X position to render (0 = centered based on slot)
+		int renderY;       // Y position to render
+		bool valid;        // True if this slot has valid data
+		
+		EmbeddedSanFrame() : pixels(nullptr), width(0), height(0), 
+		                     renderX(0), renderY(0), valid(false) {}
+		~EmbeddedSanFrame() { free(pixels); }
+		void clear() { free(pixels); pixels = nullptr; width = height = 0; valid = false; }
+	};
+	
+	EmbeddedSanFrame _rebelEmbeddedHud[5];  // Index 0 unused, 1-4 for userId slots
+	
+	// Check if a partial frame update should be skipped (overlaps with destroyed enemy)
+	bool shouldSkipFrameUpdate(int left, int top, int width, int height) override;
+	
+	// Load and decode an embedded SAN animation from IACT chunk data
+	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
+	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
+};
+
+} // End of namespace Insane
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index c3fadf55853..318e1a99231 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,6 +133,7 @@ MODULE_OBJS += \
 	insane/insane_enemy.o \
 	insane/insane_scenes.o \
 	insane/insane_iact.o \
+	insane/insane_rebel.o \
 	smush/codec1.o \
 	smush/codec20.o \
 	smush/codec37.o \
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 40dabc98a64..bb11b39c98f 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -207,13 +207,16 @@ int NutRenderer::getCharHeight(byte c) const {
 	return _chars[c].height;
 }
 
-void NutRenderer::drawFrame(byte *dst, int c, int x, int y) {
+void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
 	const int width = MIN((int)_chars[c].width, _vm->_screenWidth - x);
 	const int height = MIN((int)_chars[c].height, _vm->_screenHeight - y);
 	const byte *src = _chars[c].src;
 	const int srcPitch = _chars[c].width;
 	byte bits = 0;
 
+	if (pitch == -1)
+		pitch = _vm->_screenWidth;
+
 	const int minX = x < 0 ? -x : 0;
 	const int minY = y < 0 ? -y : 0;
 
@@ -221,10 +224,42 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y) {
 		return;
 	}
 
-	dst += _vm->_screenWidth * y + x;
+	dst += pitch * y + x;
 	if (minY) {
 		src += minY * srcPitch;
-		dst += minY * _vm->_screenWidth;
+		dst += minY * pitch;
+	}
+
+	for (int ty = minY; ty < height; ty++) {
+		for (int tx = minX; tx < width; tx++) {
+			bits = src[tx];
+			if (bits != 231 && bits) {
+				dst[tx] = bits;
+			}
+		}
+		src += srcPitch;
+		dst += pitch;
+	}
+}
+
+void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight) {
+	const int width = MIN((int)_chars[c].width, maxWidth - x);
+	const int height = MIN((int)_chars[c].height, maxHeight - y);
+	const byte *src = _chars[c].src;
+	const int srcPitch = _chars[c].width;
+	byte bits = 0;
+
+	const int minX = x < 0 ? -x : 0;
+	const int minY = y < 0 ? -y : 0;
+
+	if (height <= 0 || width <= 0) {
+		return;
+	}
+
+	dst += pitch * y + x;
+	if (minY) {
+		src += minY * srcPitch;
+		dst += minY * pitch;
 	}
 
 	for (int ty = minY; ty < height; ty++) {
@@ -235,7 +270,7 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y) {
 			}
 		}
 		src += srcPitch;
-		dst += _vm->_screenWidth;
+		dst += pitch;
 	}
 }
 
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index c17457b2dcb..01749c1bf0a 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -68,7 +68,8 @@ public:
 	virtual ~NutRenderer();
 	int getNumChars() const { return _numChars; }
 
-	void drawFrame(byte *dst, int c, int x, int y);
+	void drawFrame(byte *dst, int c, int x, int y, int pitch = -1);
+	void drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight);
 	int draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr);
 	int drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors = false, bool smushColorMode = false);
 
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index b9e7434618d..90088d90b8a 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -373,6 +373,7 @@ static const MD5Table md5table[] = {
 	{ "6b27dbcd8d5697d5c918eeca0f68ef6a", "puttrace", "HE CUP", "Preview", 3901484, Common::UNK_LANG, Common::kPlatformUnknown },
 	{ "6b3ec67da214f558dc5ceaa2acd47453", "indy3", "EGA", "EGA", 5361, Common::EN_ANY, Common::kPlatformDOS },
 	{ "6b5a3fef241e90d4b2e77f1e222773ee", "maniac", "NES", "", 2082, Common::SV_SWE, Common::kPlatformNES },
+	{ "TODO", "rebel2", "VGA", "VGA", 9430, Common::EN_ANY, Common::kPlatformDOS },
 	{ "6bca7a1a96d16e52b8f3c42b50dbdca3", "fbear", "HE 62", "", -1, Common::JA_JPN, Common::kPlatform3DO },
 	{ "6bf70eee5de3d24d2403e0dd3d267e8a", "spyfox", "", "", 49221, Common::EN_USA, Common::kPlatformWindows },
 	{ "6c2bff0e327f2962e809c2e1a82d7309", "monkey", "VGA", "", 8347, Common::EN_ANY, Common::kPlatformAmiga },
@@ -743,6 +744,7 @@ static const MD5Table md5table[] = {
 	{ "e9271b3d0694c7101f10d675ab7c0133", "freddi4", "HE 99", "", -1, Common::IT_ITA, Common::kPlatformWindows },
 	{ "e94c7cc3686fce406d3c91b5eae5a72d", "zak", "V2", "V2", 1916, Common::EN_ANY, Common::kPlatformAmiga },
 	{ "e95cf980719c0be078fb68a67af97b4a", "funpack", "", "", -1, Common::JA_JPN, Common::kPlatform3DO },
+	{ "e977ed046b88e75b645491caeff37b0c", "rebel2", "Demo", "Demo", 313689, Common::EN_ANY, Common::kPlatformDOS },
 	{ "e98b982ceaf9d253d730bde8903233d6", "monkey", "EGA", "EGA", 8357, Common::DE_DEU, Common::kPlatformDOS },
 	{ "eae95b2b3546d8ba86ae1d397c383253", "dog", "", "", 19681, Common::EN_ANY, Common::kPlatformUnknown },
 	{ "eb700bb73ca1cc44a1ad5e4b1a4bdeaf", "indy3", "EGA", "EGA", 5361, Common::DE_DEU, Common::kPlatformDOS },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 1b65e51cf16..3ebc7429d00 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -52,6 +52,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/players/player_towns.h"
 #include "scumm/insane/insane.h"
+#include "scumm/insane/insane_rebel.h"
 #include "scumm/he/animation_he.h"
 #include "scumm/he/font_he.h"
 #include "scumm/he/intern_he.h"
@@ -1157,6 +1158,8 @@ Common::Error ScummEngine::init() {
 
 			_filenamePattern.pattern = "%.2d.LFL";
 			_filenamePattern.genMethod = kGenRoomNum;
+		} else if (_game.id == GID_REBEL2) {
+			_fileHandle = new ScummFile(this);
 		} else if (_game.platform == Common::kPlatformMacintosh) {
 			// The mac versions of Indy4, Sam&Max, DOTT, FT and The Dig used a
 			// special meta (container) file format to store the actual SCUMM data
@@ -1504,6 +1507,11 @@ Common::Error ScummEngine::init() {
 
 	setupScumm(macResourceFile);
 
+	if (_game.id == GID_REBEL2) {
+		_setupIsComplete = true;
+		return Common::kNoError;
+	}
+
 	readIndexFile();
 
 	// Create the debugger now that _numVariables has been set
@@ -1778,6 +1786,38 @@ void ScummEngine::setupScumm(const Common::Path &macResourceFile) {
 
 #ifdef ENABLE_SCUMM_7_8
 void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
+	if (_game.id == GID_REBEL2) {
+		_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
+		initScreens(0, 200);
+
+		_numVariables = 256;
+		_scummVars = (int32 *)calloc(_numVariables, sizeof(int32));
+
+		_numArray = 50;
+		_res->allocResTypeData(rtString, 0, _numArray, kDynamicResTypeMode);
+		_res->allocResTypeData(rtSound, 0, 200, kDynamicResTypeMode);
+		_res->allocResTypeData(rtCostume, 0, 200, kDynamicResTypeMode);
+		_res->allocResTypeData(rtRoom, 0, 20, kDynamicResTypeMode);
+
+		defineArray(0, kIntArray, 0, 1000);
+		_numActors = 0;
+
+		setupScummVars();
+
+		_useOriginalGUI = false;
+
+		_sound = new Sound(this, _mixer, false);
+		_musicEngine = _imuseDigital = new IMuseDigital(this, 11025, _mixer, &_resourceAccessMutex, false);
+		_insane = new InsaneRebel2(this);
+		_splayer = new SmushPlayer(this, _imuseDigital, _insane);
+
+		// Initialize cursor
+		_macGui = nullptr; // Ensure this is null as we don't want MacGui behavior
+		_charset = new CharsetRendererV7(this); // Just in case
+
+		initBanners();
+		return;
+	}
 
 	// The object line toggle is always synchronized from the main game to
 	// our internal Game Options; at startup we do the opposite, since an user
@@ -2576,8 +2616,22 @@ void ScummEngine_v7::syncSoundSettings() {
 	if (!_setupIsComplete)
 		return;
 
+	if (_game.id == GID_REBEL2) {
+		ScummEngine::syncSoundSettings();
+		if (_imuseDigital) {
+			_imuseDigital->diMUSESetMusicGroupVol(ConfMan.getInt("music_volume") / 2);
+			_imuseDigital->diMUSESetVoiceGroupVol(ConfMan.getInt("speech_volume") / 2);
+			_imuseDigital->diMUSESetSFXGroupVol(ConfMan.getInt("sfx_volume") / 2);
+		}
+		return;
+	}
+
 	if (!isUsingOriginalGUI()) {
 		ScummEngine::syncSoundSettings();
+		if (_splayer) {
+			_splayer->setChanFlag(0, true);
+			_splayer->setChanFlag(2, true);
+		}
 		return;
 	}
 
@@ -2628,6 +2682,18 @@ int ScummEngine::getTalkSpeed() {
 #pragma mark -
 
 Common::Error ScummEngine::go() {
+#ifdef ENABLE_SCUMM_7_8
+	if (_game.id == GID_REBEL2) {
+		// Use 12 FPS as default, same as The Dig. FT uses 10.
+		// Since we don't have the standard scripts initializing this, we pass it here.
+		if (_game.features & GF_DEMO)
+			((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
+		else
+			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
+		return Common::kNoError;
+	}
+#endif
+
 	setTotalPlayTime();
 
 	_lastWaitTime = _system->getMillis();
@@ -4045,6 +4111,9 @@ void ScummEngine_v7::scummLoop_handleSound() {
 		_imuseDigital->refreshScripts();
 	}
 
+	if (_game.id == GID_REBEL2)
+		return;
+
 	_splayer->setChanFlag(0, VAR(VAR_VOICE_MODE) != 0);
 	_splayer->setChanFlag(2, VAR(VAR_VOICE_MODE) != 2);
 }
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index aa56a411a1e..101e58a9611 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -38,6 +38,7 @@ class TextRenderer_v7;
 class ScummEngine_v7 : public ScummEngine_v6 {
 	friend class SmushPlayer;
 	friend class Insane;
+	friend class InsaneRebel2;
 public:
 	ScummEngine_v7(OSystem *syst, const DetectorResult &dr);
 	~ScummEngine_v7() override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 31c322a7e59..edcf9c62d91 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -364,6 +364,52 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
 	assert(subSize >= 8);
 
+	// For Rebel2 large IACT chunks, check for embedded SAN animations
+	if (_vm->_game.id == GID_REBEL2 && subSize > 1000) {
+		int64 startPos = b.pos();
+		
+		// Read header to check for embedded ANIM
+		byte header[64];
+		int bytesRead = b.read(header, MIN<int32>(64, subSize));
+		
+		// Check for ANIM at offset 18 (embedded SAN location)
+		uint32 magicAt18 = (bytesRead >= 22) ? READ_BE_UINT32(header + 18) : 0;
+		
+		if (magicAt18 == MKTAG('A','N','I','M')) {
+			// This IACT contains an embedded SAN animation!
+			uint32 animSize = (bytesRead >= 26) ? READ_BE_UINT32(header + 22) : 0;
+			
+			// Read the userId to determine which HUD slot (1-4)
+			int code = READ_LE_UINT16(header);
+			int flags = READ_LE_UINT16(header + 2);
+			int userId = READ_LE_UINT16(header + 6);
+			
+			debug("Rebel2: Extracting embedded SAN from IACT: userId=%d, animSize=%u", userId, animSize);
+			
+			// Extract the embedded ANIM data (starts at offset 18 from IACT start)
+			b.seek(startPos + 18);
+			
+			int32 embeddedSize = subSize - 18;
+			if (embeddedSize > 0 && _insane) {
+				byte *animData = (byte *)malloc(embeddedSize);
+				if (animData) {
+					b.read(animData, embeddedSize);
+					
+					// Pass to Insane for decoding
+					_insane->loadEmbeddedSan(userId, animData, embeddedSize, _dst);
+					
+					free(animData);
+				}
+			}
+			
+			// Return - don't process as audio
+			return;
+		}
+		
+		// Seek back to start for normal processing
+		b.seek(startPos);
+	}
+
 	int code = b.readUint16LE();
 	int flags = b.readUint16LE();
 	int unknown = b.readSint16LE();
@@ -759,6 +805,19 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		if (_specialBuffer == 0)
 			_specialBuffer = (byte *)malloc(242 * 384);
 		_dst = _specialBuffer;
+	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+		// For Rebel2, check if this partial update overlaps with a destroyed enemy
+		// If so, skip the update entirely to prevent showing the enemy sprite
+		if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
+			return;  // Skip this frame update
+		}
+		
+		if (_specialBuffer == 0) {
+			_specialBuffer = (byte *)malloc(width * height);
+			_width = width;
+			_height = height;
+		}
+		_dst = _specialBuffer;
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;
 	// FT Insane uses smaller frames to draw overlays with moving objects
@@ -770,15 +829,22 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if ((height == 242) && (width == 384)) {
 		_width = width;
 		_height = height;
+	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+		// Do not update _width/_height here to preserve original video size
+		// if frames are partial updates
 	} else {
 		_width = _vm->_screenWidth;
 		_height = _vm->_screenHeight;
 	}
 
+	int pitch = _vm->_screenWidth;
+	if (_dst == _specialBuffer)
+		pitch = _width;
+
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
 	case SMUSH_CODEC_RLE_ALT:
-		smushDecodeRLE(_dst, src, left, top, width, height, _vm->_screenWidth);
+		smushDecodeRLE(_dst, src, left, top, width, height, pitch);
 		break;
 	case SMUSH_CODEC_DELTA_BLOCKS:
 		if (!_deltaBlocksCodec)
@@ -1214,6 +1280,9 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 	// Hide mouse
 	bool oldMouseState = CursorMan.showMouse(false);
+	if (_vm->_game.id == GID_REBEL2) {
+		insanity(true);
+	}
 
 	// Load the video
 	_seekFile = filename;
@@ -2135,4 +2204,30 @@ bool SmushPlayer::isAudioCallbackEnabled() {
 	return _smushAudioCallbackEnabled;
 }
 
+// Only used by Rebel Assault 2
+void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
+	// Check if the region already exists
+	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+		if (*it == rect) {
+			return; // Already exists
+		}
+	}
+	_maskedRegions.push_back(rect);
+}
+
+// Only used by Rebel Assault 2
+void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
+	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+		if (*it == rect) {
+			_maskedRegions.erase(it);
+			return;
+		}
+	}
+}
+
+// Only used by Rebel Assault 2
+void SmushPlayer::clearMaskedRegions() {
+	_maskedRegions.clear();
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 557203d9776..32d33b2af15 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -23,6 +23,8 @@
 #define SCUMM_SMUSH_PLAYER_H
 
 #include "common/util.h"
+#include "common/list.h"
+#include "common/rect.h"
 
 namespace Audio {
 class SoundHandle;
@@ -90,6 +92,7 @@ class Insane;
 
 class SmushPlayer {
 	friend class Insane;
+	friend class InsaneRebel2;
 private:
 	struct SmushAudioDispatch {
 		uint8 *headerPtr;
@@ -209,6 +212,14 @@ public:
 	byte *getVideoPalette();
 	void setCurVideoFlags(int16 flags);
 
+	// Masked regions - areas where video should not update (e.g., destroyed enemies)
+	// The Insane class can add/remove regions, and decodeFrameObject will restore
+	// these areas from the previous frame after decoding
+	void addMaskedRegion(const Common::Rect &rect);
+	void removeMaskedRegion(const Common::Rect &rect);
+	void clearMaskedRegions();
+	Common::List<Common::Rect> _maskedRegions;
+
 
 protected:
 	int _width, _height;


Commit: 9aba962606d5aa25961aa99f8233916b7d02667d
    https://github.com/scummvm/scummvm/commit/9aba962606d5aa25961aa99f8233916b7d02667d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:42+02:00

Commit Message:
SCUMM: RA2: Add missing init variables

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index f1d72aa578d..98126582fe9 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -63,6 +63,84 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelEnemies.clear();
 	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
+
+	_speed = 12;
+	_insaneIsRunning = false;
+
+	_numberArray = 0;
+	_emulateInterrupt = 0;
+	_flag1d = 0;
+	_objArray1Idx = 0;
+	_objArray1Idx2 = 0;
+	_objArray2Idx = 0;
+	_objArray2Idx2 = 0;
+	_currSceneId = 1;
+	_timer6Id = 0;
+	_timerSpriteId = 0;
+	_temp2SceneId = 0;
+	_tempSceneId = 0;
+	_currEnemy = -1;
+	_currScenePropIdx = 0;
+	_currScenePropSubIdx = 0;
+	_currTrsMsg = 0;
+	_sceneData2Loaded = 0;
+	_sceneData1Loaded = 0;
+	_keyboardDisable = 0;
+	_needSceneSwitch = false;
+	_idx2Exceeded = 0;
+	_tiresRustle = false;
+	_keybOldDx = 0;
+	_keybOldDy = 0;
+	_velocityX = 0;
+	_velocityY = 0;
+	_keybX = 0;
+	_keybY = 0;
+	_firstBattle = false;
+	_battleScene = true;
+	_kickBenProgress = false;
+	_weaponBenJustSwitched = false;
+	_kickEnemyProgress = false;
+	_weaponEnemyJustSwitched = false;
+	_beenCheated = 0;
+	_posBrokenTruck = 0;
+	_posBrokenCar = 0;
+	_posFatherTorque = 0;
+	_posCave = 0;
+	_posVista = 0;
+	_roadBranch = false;
+	_roadStop = false;
+	_carIsBroken = false;
+	_benHasGoggles = false;
+	_mineCaveIsNear = false;
+	_objectDetected = false;
+	_approachAnim = -1;
+	_val54d = 0;
+	_val57d = 0;
+	_val115_ = false;
+	_roadBumps = false;
+	_val211d = 0;
+	_val213d = 0;
+	_metEnemiesListTail = 0;
+	_smlayer_room = 0;
+	_smlayer_room2 = 0;
+	_isBenCut = 0;
+	_continueFrame = 0;
+	_continueFrame1 = 0;
+	_counter1 = 0;
+	_iactSceneId = 0;
+	_iactSceneId2 = 0;
+
+	int i, j;
+	
+	for (i = 0; i < 12; i++)
+		_metEnemiesList[i] = 0;
+
+	for (i = 0; i < 9; i++)
+		for (j = 0; j < 9; j++)
+			_enHdlVar[i][j] = 0;
+
+	for (i = 0; i < 0x200; i++)
+		_iactBits[i] = 0;
 }
 
 


Commit: 32a4ba152c15e34e89b649d439d82140a867d3d4
    https://github.com/scummvm/scummvm/commit/32a4ba152c15e34e89b649d439d82140a867d3d4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:43+02:00

Commit Message:
SCUMM: RA2: Deduplicate level object setup

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 98126582fe9..41d4d69af9d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -403,6 +403,10 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	}
 }
 
+// External helpers from smush_player.cpp but we are already in Scumm namespace
+extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 	// Validate userId (1-4 for HUD slots)
 	if (userId < 1 || userId > 4 || !animData || size < 32) {
@@ -410,8 +414,6 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 		return;
 	}
 	
-	// Parse the embedded ANIM structure
-	// Format: ANIM (4 bytes) + size (4 bytes BE) + AHDR (4 bytes) + size (4 bytes BE) + ...
 	Common::MemoryReadStream stream(animData, size);
 	
 	// Read ANIM header
@@ -423,36 +425,21 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 	uint32 animSize = stream.readUint32BE();
 	debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
 	
-	// Read AHDR header
-	uint32 ahdrTag = stream.readUint32BE();
-	if (ahdrTag != MKTAG('A','H','D','R')) {
-		debug("Rebel2: Embedded SAN missing AHDR tag, got 0x%08X", ahdrTag);
-		return;
-	}
-	uint32 ahdrSize = stream.readUint32BE();
-	
-	// Skip AHDR content (palette etc)
-	stream.skip(ahdrSize);
-	if (ahdrSize & 1) stream.skip(1);  // Padding
-	
-	// Look for first FRME
+	// Iterate through chunks to find FRME -> FOBJ
 	while (!stream.eos() && stream.pos() < size) {
 		uint32 tag = stream.readUint32BE();
 		uint32 chunkSize = stream.readUint32BE();
-		
+		int32 nextChunkPos = stream.pos() + chunkSize;
+
 		if (tag == MKTAG('F','R','M','E')) {
-			debug("Rebel2: Found FRME in embedded SAN, size=%u", chunkSize);
-			
-			// Parse FRME content to find FOBJ
-			int64 frmeEnd = stream.pos() + chunkSize;
-			
-			while (stream.pos() < frmeEnd && !stream.eos()) {
+			// Iterate sub-chunks in FRME
+			while (stream.pos() < nextChunkPos && !stream.eos()) {
 				uint32 subTag = stream.readUint32BE();
 				uint32 subSize = stream.readUint32BE();
-				
+				int32 nextSubPos = stream.pos() + subSize;
+
 				if (subTag == MKTAG('F','O','B','J')) {
-					debug("Rebel2: Found FOBJ in embedded SAN, size=%u", subSize);
-					
+					// Found FOBJ - Embedded HUD Frame
 					// Read FOBJ header
 					int codec = stream.readUint16LE();
 					int left = stream.readUint16LE();
@@ -467,16 +454,16 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 					
 					// Allocate storage for the decoded frame
 					EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
-					frame.clear();
 					
 					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
-						frame.pixels = (byte *)calloc(width * height, 1);
-						frame.width = width;
-						frame.height = height;
+						if (frame.width != width || frame.height != height || !frame.pixels) {
+							frame.clear();
+							frame.pixels = (byte *)calloc(width * height, 1);
+							frame.width = width;
+							frame.height = height;
+						}
 						
-						// Use the left/top from FOBJ header as render position
-						// These are the exact screen coordinates for the HUD overlay
-						// The FOBJ header specifies where this frame should be composited
+						// Update render position from FOBJ header
 						frame.renderX = left;
 						frame.renderY = top;
 						
@@ -487,55 +474,18 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 							stream.read(fobjData, dataSize);
 							
 							// Decode based on codec
-							// Codec 1 = RLE, Codec 3 = raw, Codec 21 = line update, Codec 45 = block delta
-							if (codec == 1) {
-								// RLE decode using SMUSH BOMP format
-								// Each line has a 16-bit size header, then BOMP-encoded data
-								// BOMP format: code byte, then data
-								//   code & 1 == 1: fill mode, next byte repeated (code>>1)+1 times
-								//   code & 1 == 0: copy mode, copy next (code>>1)+1 bytes
-								byte *srcPtr = fobjData;
-								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
-									int lineSize = READ_LE_UINT16(srcPtr);
-									srcPtr += 2;
-									byte *lineEnd = srcPtr + lineSize;
-									byte *lineDst = frame.pixels + row * width;
-									int remaining = width;
-									
-									while (srcPtr < lineEnd && remaining > 0) {
-										byte code = *srcPtr++;
-										int num = (code >> 1) + 1;
-										if (num > remaining)
-											num = remaining;
-										remaining -= num;
-										
-										if (code & 1) {
-											// Fill mode: repeat next byte
-											byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
-											memset(lineDst, color, num);
-											lineDst += num;
-										} else {
-											// Copy mode: copy next bytes
-											for (int i = 0; i < num && srcPtr < lineEnd; i++) {
-												*lineDst++ = *srcPtr++;
-											}
-										}
-									}
-									srcPtr = lineEnd;
-								}
+							if (codec == 1 || codec == 3) {
+								// RLE use existing decoder
+								smushDecodeRLE(frame.pixels, fobjData, 0, 0, width, height, width);
 								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec 1/RLE): %dx%d", width, height);
-							} else if (codec == 3 || codec == 20) {
-								// Uncompressed - direct copy
-								int copySize = MIN(dataSize, width * height);
-								memcpy(frame.pixels, fobjData, copySize);
+								debug("Rebel2: Decoded embedded HUD (codec %d/RLE): %dx%d", codec, width, height);
+							} else if (codec == 20) {
+								// Uncompressed
+								smushDecodeUncompressed(frame.pixels, fobjData, 0, 0, width, height, width);
 								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec %d/raw): %dx%d", codec, width, height);
+								debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
 							} else if (codec == 21 || codec == 44) {
 								// Codec 21/44: Line update codec
-								// Format: For each line: 
-								//   16-bit line data size
-								//   Line data: pairs of (16-bit skip, 16-bit count-1, pixels[count])
 								byte *srcPtr = fobjData;
 								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
 									int lineDataSize = READ_LE_UINT16(srcPtr);
@@ -559,16 +509,13 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
 							} else if (codec == 45) {
-								// Codec 45: Similar to codec 47 but simpler - block delta
-								// For embedded HUD use, try simple raw copy as fallback
-								// This codec appears in small sprites (11x26, 17x53)
-								int copySize = MIN(dataSize, width * height);
+								// Codec 45: Block delta (simple copy for now)
+								int copySize = MIN((int)dataSize, width * height);
 								memcpy(frame.pixels, fobjData, copySize);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 45/block): %dx%d", width, height);
 							} else {
 								debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
-								// For now, mark as invalid but keep dimensions
 								frame.valid = false;
 							}
 							
@@ -596,21 +543,25 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 						}
 					}
 					
-					return;  // Found and processed FOBJ
+					// Done with FOBJ - assume only one relevant frame per embedded SAN
+					stream.seek(nextChunkPos);
+					goto end_parsing;
 				} else {
-					// Skip other sub-chunks
-					stream.skip(subSize);
+					// Skip other sub-chunks (AHDR inside FRME?) or padding
+					stream.seek(nextSubPos);
 					if (subSize & 1) stream.skip(1);
 				}
 			}
 		} else {
-			// Skip non-FRME chunks
-			stream.skip(chunkSize);
+			// Skip non-FRME chunks (AHDR, etc at top level)
+			stream.seek(nextChunkPos);
 			if (chunkSize & 1) stream.skip(1);
 		}
 	}
 	
 	debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
+
+end_parsing:;
 }
 
 


Commit: b1e18defe18f1f5b095d3c982b30dffc40560afa
    https://github.com/scummvm/scummvm/commit/b1e18defe18f1f5b095d3c982b30dffc40560afa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:43+02:00

Commit Message:
SCUMM: RA2: Remove redundant NutRenderer drawFrame overload

Changed paths:
    engines/scumm/nut_renderer.cpp
    engines/scumm/nut_renderer.h


diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index bb11b39c98f..3bac87e4fb0 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -242,38 +242,6 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
 	}
 }
 
-void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight) {
-	const int width = MIN((int)_chars[c].width, maxWidth - x);
-	const int height = MIN((int)_chars[c].height, maxHeight - y);
-	const byte *src = _chars[c].src;
-	const int srcPitch = _chars[c].width;
-	byte bits = 0;
-
-	const int minX = x < 0 ? -x : 0;
-	const int minY = y < 0 ? -y : 0;
-
-	if (height <= 0 || width <= 0) {
-		return;
-	}
-
-	dst += pitch * y + x;
-	if (minY) {
-		src += minY * srcPitch;
-		dst += minY * pitch;
-	}
-
-	for (int ty = minY; ty < height; ty++) {
-		for (int tx = minX; tx < width; tx++) {
-			bits = src[tx];
-			if (bits != 231 && bits) {
-				dst[tx] = bits;
-			}
-		}
-		src += srcPitch;
-		dst += pitch;
-	}
-}
-
 int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors, bool smushColorMode) {
 	if (_direction < 0)
 		x -= _chars[chr].width;
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index 01749c1bf0a..d7f92bf1076 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -69,7 +69,6 @@ public:
 	int getNumChars() const { return _numChars; }
 
 	void drawFrame(byte *dst, int c, int x, int y, int pitch = -1);
-	void drawFrame(byte *dst, int c, int x, int y, int pitch, int maxWidth, int maxHeight);
 	int draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr);
 	int drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors = false, bool smushColorMode = false);
 


Commit: de41cd69ab7767926e9786f8a5458e2fa7988c4d
    https://github.com/scummvm/scummvm/commit/de41cd69ab7767926e9786f8a5458e2fa7988c4d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:43+02:00

Commit Message:
SCUMM: RA2: Avoid crash during shutdown

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 41d4d69af9d..1c35c38041a 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -44,6 +44,21 @@ namespace Scumm {
 
 InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_vm = scumm;
+
+	// Initialize parent class pointers to nullptr to avoid crash in ~Insane()
+	// because Insane() default constructor leaves them uninitialized.
+	_smush_roadrashRip = nullptr;
+	_smush_roadrsh2Rip = nullptr;
+	_smush_roadrsh3Rip = nullptr;
+	_smush_goglpaltRip = nullptr;
+	_smush_tovista1Flu = nullptr;
+	_smush_tovista2Flu = nullptr;
+	_smush_toranchFlu = nullptr;
+	_smush_minedrivFlu = nullptr;
+	_smush_minefiteFlu = nullptr;
+	_smush_bencutNut = nullptr;
+	_smush_bensgoggNut = nullptr;
+
 	// Rebel Assault 2 specific initialization can go here
 	_rebelHudPrimary = nullptr;
 	_rebelHudSecondary = nullptr;


Commit: 34e92ca251b4138f249699f804d7b62500c15229
    https://github.com/scummvm/scummvm/commit/34e92ca251b4138f249699f804d7b62500c15229
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:44+02:00

Commit Message:
SCUMM: RA2: Implement IACT dependency links

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 1c35c38041a..0482872c0c2 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -156,6 +156,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	for (i = 0; i < 0x200; i++)
 		_iactBits[i] = 0;
+
+	for (i = 0; i < 512; i++) {
+		_rebelLinks[i][0] = 0;
+		_rebelLinks[i][1] = 0;
+		_rebelLinks[i][2] = 0;
+	}
 }
 
 
@@ -201,6 +207,29 @@ int32 InsaneRebel2::processMouse() {
 					it->id, mousePos.x, mousePos.y,
 					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
 
+				// Disable self
+				setBit(it->id);
+
+				// Handle dependencies
+				int id = it->id;
+				if (id >= 0 && id < 512) {
+					// Slot 2: Enable (Explosion?)
+					if (_rebelLinks[id][2] != 0) {
+						clearBit(_rebelLinks[id][2]); 
+						debug("Rebel2: Enabled dependency Slot 2 (ID=%d) for Parent %d", _rebelLinks[id][2], id);
+					}
+					// Slot 1: Enable (Explosion?)
+					if (_rebelLinks[id][1] != 0) {
+						clearBit(_rebelLinks[id][1]);
+						debug("Rebel2: Enabled dependency Slot 1 (ID=%d) for Parent %d", _rebelLinks[id][1], id);
+					}
+					// Slot 0: Disable (Shots?)
+					if (_rebelLinks[id][0] != 0) {
+						setBit(_rebelLinks[id][0]);
+						debug("Rebel2: Disabled dependency Slot 0 (ID=%d) for Parent %d", _rebelLinks[id][0], id);
+					}
+				}
+
 				// Note: Background saving and masking is handled in procPostRendering
 				// where we have access to the render bitmap
 				// TODO: Play explosion sound
@@ -264,6 +293,12 @@ void InsaneRebel2::setBit(int n) {
 	_iactBits[n] = 1;
 }
 
+void InsaneRebel2::clearBit(int n) {
+	assert (n < 0x200);
+
+	_iactBits[n] = 0;
+}
+
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) {
@@ -300,15 +335,28 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	//   screenX = centerX - DAT_0043e006 (scrollX)
 	//   screenY = centerY - DAT_0043e008 (scrollY)
 
+	//   screenX = centerX - DAT_0043e006 (scrollX)
+	//   screenY = centerY - DAT_0043e008 (scrollY)
+
 	if (par1 == 4) {
 		// Opcode 4: Enemy position update
 		// Read 5 shorts from the stream (offset +8 through +16)
 		int16 enemyId = b.readSint16LE();  // Offset +8
 		int16 x = b.readSint16LE();        // Offset +10 (0x0A)
+		
+		// If enemy is disabled in bit table, skip update
+		bool disabled = isBitSet(enemyId);
+		
 		int16 y = b.readSint16LE();        // Offset +12 (0x0C)
 		int16 w = b.readSint16LE();        // Offset +14 (0x0E) - Width
 		int16 h = b.readSint16LE();        // Offset +16 (0x10) - Height
 
+		// If disabled, stop processing this object
+		if (disabled) {
+			// debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
+			return; 
+		}
+
 		// The disassembly shows half-width/half-height are used for centering:
 		//   halfW = w >> 1
 		//   halfH = h >> 1
@@ -367,7 +415,39 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	} else if (par1 == 2) {
 		// Opcode 2: Often used for bit setting
 		// Disassembly shows this is handled but we don't have full context
-		debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
+		// debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
+		
+		// Handle dependency linking (par3 == 4)
+		if (par3 == 4) {
+			int16 childId = b.readSint16LE(); // Offset +8
+			int16 parentId = b.readSint16LE(); // Offset +10
+			
+			if (parentId >= 0 && parentId < 512) {
+				// Shift links
+				_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
+				_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
+				_rebelLinks[parentId][0] = childId;
+				
+				// Apply initial state based on parent state
+				// If parent is ALIVE (Bit Clear) -> Disable new Child (Set Bit)
+				// If parent is DEAD (Bit Set) -> Enable new Child (Clear Bit)
+				if (!isBitSet(parentId)) {
+					setBit(childId);
+					debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
+				} else {
+					clearBit(childId);
+					debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
+				}
+			}
+		} else {
+			// Skip extra data if any? Opcode 2 might vary in size.
+			// Based on disassembly, it accesses offset +8 and +10 for case 4.
+			// Currently we don't know size for other cases, but hopefully they don't crash stream reading.
+			// 'b' is SeekableReadStream, so we might need to skip if we don't read?
+			// The caller `procIACT` passes `size` but that's for the WHOLE chunk.
+			// We can't skip unknown sub-opcodes easily without size map.
+			// Assuming only par3=4 has extra data for now.
+		}
 		
 	} else if (par1 == 3) {
 		// Opcode 3: Often used for clearing/resetting
@@ -795,7 +875,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				}
 			}
 			// After explosion is complete (explosionFrame == -1), do nothing
-		} else if (it->active) {
+			// After explosion is complete (explosionFrame == -1), do nothing
+		} else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
 			// Draw bounding box outline for active enemies (debug visualization)
 			// Draw Top
 			if (r.top >= 0 && r.top < height) {
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 00b41865c06..3fc14b2cc5b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -164,6 +164,10 @@ public:
 	// Load and decode an embedded SAN animation from IACT chunk data
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
 	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
+
+	int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
+	void clearBit(int n);
+
 };
 
 } // End of namespace Insane


Commit: ad764f728c25ce20a043ea0490dce3ecee40fc3c
    https://github.com/scummvm/scummvm/commit/ad764f728c25ce20a043ea0490dce3ecee40fc3c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:44+02:00

Commit Message:
SCUMM: RA2: Implement player damage from enemies

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/nut_renderer.cpp
    engines/scumm/nut_renderer.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 0482872c0c2..3f1e062b3e6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -79,6 +79,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
 
+	_playerDamage = 0;
+	_playerLives = 3;
+	_playerScore = 0;
+
 	_speed = 12;
 	_insaneIsRunning = false;
 
@@ -451,7 +455,62 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		
 	} else if (par1 == 3) {
 		// Opcode 3: Often used for clearing/resetting
-		debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
+		// debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
+		
+		// Handle damage dealing (Subcode 5 in FUN_401234)
+		// Opcode 3 is complicated:
+		// Based on FUN_401234, case 1 (Opcode 3), checks local_c (offset +4 / par2?)
+		// In iactRebel2Scene1, par1=Opcode.
+		// If par1 == 1 (Opcode 1 / FUN_401234 case 1):
+		//   It checks local_c which is from offset +4 (par3).
+		//   If par3 == 5 -> Damage Logic.
+		// 
+		// Actually, FUN_401234 switches on `*local_14 - 2`.
+		// If `*local_14` (Opcode) is 3, switch value is 1.
+		// So Opcode 3 -> Case 1.
+		
+		if (par1 == 3) {
+			// Inside Case 1: local_c = local_14[2] which is offset +4 (par3)
+			if (par3 == 5) {
+				// Damage Logic
+				// bVar1 = FUN_423970(local_14[5]); // Check if source enemy (offset +10 / par6?) is active
+				// Read extra params from stream
+				// Note: `procIACT` already read parems into par1..par4.
+				// But Opcode 3 logic in FUN_401234 uses offset +10 (par6) which isn't passed to `procIACT` signature fully?
+				// Wait, `procIACT` reads 4 shorts.
+				// par1 (+0), par2 (+2), par3 (+4), par4 (+6).
+				// We need +8 and +10.
+				
+				b.skip(2); // Offset +8 (par5 used in assembly but not here yet)
+				int16 par6 = b.readSint16LE(); // Offset +10 (Enemy ID?)
+				
+				// Check if enemy is disabled (processed in FUN_423970)
+				bool enemyDisabled = isBitSet(par6);
+				
+				if (!enemyDisabled) {
+					// Probability check: Random(100) < Limit
+					// The limit seems to come from a table or fixed value?
+					// In FUN_401234: `sVar2 = *(short *)(&DAT_0047e0fc + ...)`
+					// For now, let's just use a fixed probability or assume successful hit
+					// if (rnd < prob) ...
+					
+					// Increment damage
+					// In assembly: DAT_0047a7ec += damage
+					// Damage amount also comes from table?
+					int damageAmount = 5; // Placeholder
+					
+					// Only apply damage occasionally to simulate probability
+					if ((_vm->_system->getMillis() % 100) < 20) { // 20% chance per frame (approx)
+						_playerDamage += damageAmount;
+						if (_playerDamage > 255) _playerDamage = 255;
+						
+						debug("Rebel2: Player HIT by Enemy %d. Damage=%d", par6, _playerDamage);
+						
+						// TODO: Flash screen red / shake
+					}
+				}
+			}
+		}
 		
 	} else if (par1 == 5) {
 		// Opcode 5: Special handling based on par2 value
@@ -803,7 +862,49 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// The width is scaled based on shield value (param_1 >> 2)
 		// For now, draw at position (0, statusBarY) - sprite has internal positioning
 		if (_smush_cockpitNut->getNumChars() > 6) {
-			smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 6, 0, 0);
+			// Calculate width based on damage. 
+			// Assuming max damage 255 = empty bar.
+			// Bar max width is 64 pixels.
+			// Damage 0 = Width 64. Damage 255 = Width 0.
+			// Width = 64 - (Damage / 4)
+			int barWidth = 64 - (_playerDamage / 4);
+			if (barWidth < 0) barWidth = 0;
+			
+			// We need to draw a partial sprite or use a clip rect.
+			// smlayer_drawSomething supports scaling/clip?
+			// The current implementation of smlayer_drawSomething just draws the whole sprite.
+			// We can pass a "frame" or clip rect if we modify the function or use a lower level draw.
+			// For now, let's just draw the full sprite if damage < 255, to verify it appears.
+			// Ideally we should implement clipping.
+			
+			// NOTE: smlayer_drawSomething calls `_smush_cockpitNut->draw(...)`
+			// We can't easily clip without modifying NutRenderer or using a custom draw loop.
+			// Let's implement a custom draw loop for the shield bar here since it's simple.
+			
+			// Sprite 6 data
+			const byte *src = _smush_cockpitNut->getCharData(6);
+			int sw = _smush_cockpitNut->getCharWidth(6);
+			int sh = _smush_cockpitNut->getCharHeight(6);
+			int sx = 63; // Hardcoded X offset from assembly
+			int sy = 9;  // Hardcoded Y offset (relative to status bar top)
+			
+			// Draw clipped width
+			if (src && sw > 0 && sh > 0) {
+				int drawWidth = MIN(barWidth, sw);
+				for (int y = 0; y < sh; y++) {
+					for (int x = 0; x < drawWidth; x++) {
+						// Render to (0 + sx + x, statusBarY + sy + y)
+						int destX = sx + x;
+						int destY = statusBarY + sy + y;
+						if (destX < pitch && destY < height) {
+							byte pixel = src[y * sw + x];
+							if (pixel != 0) {
+								renderBitmap[destY * pitch + destX] = pixel;
+							}
+						}
+					}
+				}
+			}
 		}
 		
 		// Draw shield alert overlay (sprite 7) when shields critical (> 0xAA = 170)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 3fc14b2cc5b..520c4321e92 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -165,9 +165,16 @@ public:
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
 	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
 
+
+
 	int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
 	void clearBit(int n);
 
+	int16 _playerDamage;  // 0 to 255 (Accumulated damage)
+	int16 _playerLives;
+	int32 _playerScore;
+
+
 };
 
 } // End of namespace Insane
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 3bac87e4fb0..2213db9947d 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -207,6 +207,13 @@ int NutRenderer::getCharHeight(byte c) const {
 	return _chars[c].height;
 }
 
+const byte *NutRenderer::getCharData(byte c) {
+	if (c >= _numChars)
+		error("invalid character in NutRenderer::getCharData : %d (%d)", c, _numChars);
+
+	return _chars[c].src;
+}
+
 void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
 	const int width = MIN((int)_chars[c].width, _vm->_screenWidth - x);
 	const int height = MIN((int)_chars[c].height, _vm->_screenHeight - y);
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index d7f92bf1076..8077816ca35 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -74,6 +74,7 @@ public:
 
 	int getCharWidth(byte c) const;
 	int getCharHeight(byte c) const;
+	const byte *getCharData(byte c);
 
 	int getFontHeight() const { return _fontHeight; }
 };


Commit: d94e6274e107f13740418ba2aa680bfeca31ee2f
    https://github.com/scummvm/scummvm/commit/d94e6274e107f13740418ba2aa680bfeca31ee2f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:44+02:00

Commit Message:
SCUMM: RA2: Implement enemy explosion slots

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3f1e062b3e6..081178f69e8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -166,6 +166,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_rebelLinks[i][1] = 0;
 		_rebelLinks[i][2] = 0;
 	}
+
+	for (i = 0; i < 5; i++) {
+		_explosions[i].active = false;
+		_explosions[i].counter = 0;
+	}
 }
 
 
@@ -205,12 +210,16 @@ int32 InsaneRebel2::processMouse() {
 				// Enemy hit!
 				it->active = false;
 				it->destroyed = true;  // Mark as destroyed so IACT won't re-activate
-				it->explosionFrame = 0;  // Start explosion animation
-				it->explosionComplete = false;  // Explosion not yet finished
 				debug("Rebel2: HIT enemy ID=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)", 
 					it->id, mousePos.x, mousePos.y,
 					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
 
+				// Spawn explosion using native system
+				// Use width / 2 as the scale parameter
+				spawnExplosion((it->rect.left + it->rect.right) / 2, 
+							   (it->rect.top + it->rect.bottom) / 2, 
+							   it->rect.width() / 2);
+
 				// Disable self
 				setBit(it->id);
 
@@ -718,6 +727,21 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 end_parsing:;
 }
 
+void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
+	// Find first free slot (FUN_40A2E0 logic)
+	for (int i = 0; i < 5; i++) {
+		if (!_explosions[i].active || _explosions[i].counter <= 0) {
+			_explosions[i].active = true;
+			_explosions[i].counter = 10;
+			_explosions[i].x = x;
+			_explosions[i].y = y;
+			_explosions[i].scale = objectHalfWidth;
+			// TODO: Play sound via FUN_0041189e equivalent
+			break;
+		}
+	}
+}
+
 
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -948,35 +972,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		if (it->destroyed) {
 			// Handle destroyed enemies - draw explosion animation
 			// The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
-			
-			// Draw explosion animation - only while animation is in progress
-			// CPITIMAG.NUT indices 9-41 contain explosion frames, but we only use 8 for a quick effect
-			const int EXPLOSION_FRAMES = 8;
-			if (it->explosionFrame >= 0 && it->explosionFrame < EXPLOSION_FRAMES) {
-				if (_smush_iconsNut) {
-					int explosionSpriteIndex = 9 + it->explosionFrame;
-					if (_smush_iconsNut->getNumChars() > explosionSpriteIndex) {
-						int ew = _smush_iconsNut->getCharWidth(explosionSpriteIndex);
-						int eh = _smush_iconsNut->getCharHeight(explosionSpriteIndex);
-						// Center explosion on the enemy bounding box
-						int cx = (r.left + r.right) / 2 - ew / 2;
-						int cy = (r.top + r.bottom) / 2 - eh / 2;
-						smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, explosionSpriteIndex, 0, 0);
-					}
-				}
-				
-				// Advance explosion frame
-				it->explosionFrame++;
-				
-				// Check if explosion is now complete
-				if (it->explosionFrame >= EXPLOSION_FRAMES) {
-					it->explosionFrame = -1;  // Mark as done - prevents any further animation
-					it->explosionComplete = true;
-					debug("Rebel2: Explosion complete for enemy ID=%d", it->id);
-				}
-			}
-			// After explosion is complete (explosionFrame == -1), do nothing
-			// After explosion is complete (explosionFrame == -1), do nothing
+			// Note: Explosion rendering is now handled by the separate 5-slot system
 		} else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
 			// Draw bounding box outline for active enemies (debug visualization)
 			// Draw Top
@@ -1006,6 +1002,45 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
+	// Draw 5-slot Explosion System
+	if (_smush_iconsNut) {
+		for (int i = 0; i < 5; i++) {
+			if (_explosions[i].active) {
+				if (_explosions[i].counter <= 0) {
+					_explosions[i].active = false;
+					continue;
+				}
+
+				// Determine base sprite index based on scale (FUN_409FBC logic)
+				int baseIndex;
+				if (_explosions[i].scale < 11) {
+					baseIndex = 9;  // Small/Medium transition
+				} else if (_explosions[i].scale < 21) {
+					baseIndex = 19; // Medium/Large transition
+				} else {
+					baseIndex = 29; // Large/XL transition
+				}
+				
+				// Formula: Base + (12 - Counter)
+				// Counter goes 10 -> 1.
+				// Frame goes Base+2 -> Base+11.
+				int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+				
+				if (_smush_iconsNut->getNumChars() > spriteIndex) {
+					int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+					int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+					int cx = _explosions[i].x - ew / 2;
+					int cy = _explosions[i].y - eh / 2;
+					
+					// Draw explosion
+					smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, spriteIndex, 0, 0);
+				}
+
+				_explosions[i].counter--;
+			}
+		}
+	}
+
 	// Draw Crosshair/Reticle cursor
 	// Sprite indices based on handler type (from original game disassembly):
 	// - Handler 8 (ground vehicle): Index 0x2E (46)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 520c4321e92..add5db5ddab 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -170,6 +170,17 @@ public:
 	int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
 	void clearBit(int n);
 
+	struct Explosion {
+		int x, y;
+		int width, height;
+		int counter;     // Duration counter (starts at 10)
+		int scale;       // Determines sprite set (small/med/large)
+		bool active;
+	};
+	
+	Explosion _explosions[5];
+	void spawnExplosion(int x, int y, int objectHalfWidth);
+
 	int16 _playerDamage;  // 0 to 255 (Accumulated damage)
 	int16 _playerLives;
 	int32 _playerScore;


Commit: a8843cc89970243453ca66f5c8febabc4ce16205
    https://github.com/scummvm/scummvm/commit/a8843cc89970243453ca66f5c8febabc4ce16205
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00

Commit Message:
SCUMM: RA2: Implement laser animations

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 081178f69e8..98ca2ecf7f6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -171,6 +171,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_explosions[i].active = false;
 		_explosions[i].counter = 0;
 	}
+
+	for (i = 0; i < 2; i++) {
+		_shots[i].active = false;
+		_shots[i].counter = 0;
+	}
 }
 
 
@@ -198,6 +203,9 @@ int32 InsaneRebel2::processMouse() {
 		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d", 
 			mousePos.x, mousePos.y, _rebelEnemies.size());
 
+		// Spawn visual shot immediately
+		spawnShot(mousePos.x, mousePos.y);
+
 		// Check for hit on any active enemy
 		Common::List<RebelEnemy>::iterator it;
 		for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
@@ -742,6 +750,117 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 	}
 }
 
+void InsaneRebel2::spawnShot(int x, int y) {
+	// Find free shot slot (2 slots total)
+	for (int i = 0; i < 2; i++) {
+		if (!_shots[i].active) {
+			_shots[i].active = true;
+			_shots[i].counter = 4; // Lasts 4 frames
+			_shots[i].x = x;
+			_shots[i].y = y;
+			// TODO: Play laser sound
+			break;
+		}
+	}
+}
+
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx) {
+	if (!nut || spriteIdx >= nut->getNumChars()) return;
+
+	const byte *srcData = nut->getCharData(spriteIdx);
+	int texW = nut->getCharWidth(spriteIdx);
+	int texH = nut->getCharHeight(spriteIdx);
+	
+	if (!srcData || texW <= 0 || texH <= 0) return;
+
+	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+	int err = dx + dy, e2;
+	
+	// Length of line for uv mapping
+	// float length = sqrt(dx*dx + dy*dy);
+	
+	int texCenterX = texW / 2;
+	int texCenterY = texH / 2;
+	byte color = srcData[texCenterY * texW + texCenterX]; 
+	
+	// If color is 0/transparent, try to find a valid one
+	if (color == 0) color = 255; // Fallback to white
+
+	// Standard Bresenham but with texture sampling attempt
+	// To make it look like a "beam", we need width.
+	
+	for (;;) {
+		// Draw a 3x3 blob or similar to make it thick
+		if (x0 >= 1 && x0 < width - 1 && y0 >= 1 && y0 < height - 1) {
+			dst[y0 * pitch + x0] = color;
+			// Glow effect (simulated with same color for now)
+			// dst[(y0+1) * pitch + x0] = color;
+			// dst[y0 * pitch + (x0+1)] = color;
+		}
+		
+		if (x0 == x1 && y0 == y1) break;
+		e2 = 2 * err;
+		if (e2 >= dy) { err += dy; x0 += sx; }
+		if (e2 <= dx) { err += dx; y0 += sy; }
+	}
+}
+
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx) {
+	if (!nut || spriteIdx >= nut->getNumChars()) return;
+
+	// Reverse-engineered math from FUN_0040bbf6
+	int texW = nut->getCharWidth(spriteIdx);
+	int texH = nut->getCharHeight(spriteIdx);
+	int param_11 = 12; // Standard value from assembly calls
+	if (texW == 0) return;
+
+	// sVar7 = (12 * texH * 16) / texW
+	int sVar7 = (param_11 * texH * 16) / texW;
+	
+	// Extend vector by (13/12)
+	int dx = x1 - x0;
+	int dy = y1 - y0;
+	int extDx = (dx * (param_11 + 1)) / param_11;
+	int extDy = (dy * (param_11 + 1)) / param_11;
+	
+	int xExt = x0 + extDx;
+	int yExt = y0 + extDy;
+
+	// Calculate New Start Point (sVar4, sVar5)
+	// Start = ExtEnd - (ExtEnd - Start) * (16 / (sVar7 + 16))
+	// Actually math is: sVar4 = (xExt) - (extDx * 16) / (sVar7 + 16)
+	int startFactor = sVar7 + 16;
+	if (startFactor == 0) startFactor = 1;
+	int startX = xExt - (extDx * 16) / startFactor;
+	int startY = yExt - (extDy * 16) / startFactor;
+
+	// Calculate New End Point (sVar6, sVar7 in asm) w.r.t param_9
+	// End = ExtEnd - (ExtEnd - Start) * (16 / (param_9 + sVar7 + 16))
+	int endFactor = param_9 + sVar7 + 16;
+	if (endFactor == 0) endFactor = 1; // Safety
+	int endX = xExt - (extDx * 16) / endFactor;
+	int endY = yExt - (extDy * 16) / endFactor;
+
+	// Draw the segment
+	drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
+}
+
+void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
+	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+	int err = dx + dy, e2;
+
+	for (;;) {
+		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height)
+			dst[y0 * pitch + x0] = color;
+		if (x0 == x1 && y0 == y1) break;
+		e2 = 2 * err;
+		if (e2 >= dy) { err += dy; x0 += sx; }
+		if (e2 <= dx) { err += dx; y0 += sy; }
+	}
+}
+
 
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
@@ -1041,6 +1160,73 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
+	// Draw Laser Shots
+	// Gun Positions (Approximate for Turret Mode / Default):
+	// Left: (10, 190), Right: (310, 190) - Adjusted for low-res
+	const int GUN_LEFT_X = 10;
+	const int GUN_LEFT_Y = 190;
+	const int GUN_RIGHT_X = 310;
+	const int GUN_RIGHT_Y = 190;
+
+	for (int i = 0; i < 2; i++) {
+		if (_shots[i].active) {
+			if (_shots[i].counter <= 0) {
+				_shots[i].active = false;
+				continue;
+			}
+			
+			bool drawn = false;
+			// Use Sprite 5 from CPITIMAG.NUT as the laser texture
+			// Confirmed by reverse engineering: Laser uses Sprite 5 (136x13)
+			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+				// Calculate param_9 equivalent (Counter dependent)
+				// Original: TableValue - Counter
+				// We don't have table, but assembly suggests values around 0x20-0x80 range?
+				// Math divides by (param_9 + offset). Higher param_9 = Longer beam.
+				// As counter decreases (4->0), value increases (26->30), beam grows/moves.
+				// Let's try base 30.
+				int base = 30; 
+				int param_9 = base - _shots[i].counter;
+
+				// Draw Left Beam
+				drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+				// Draw Right Beam
+				drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+
+				// Draw Projectile Impact
+				// Using Sprite 0 (small flash) or similar at impact point
+				smlayer_drawSomething(renderBitmap, pitch, _shots[i].x - 7, _shots[i].y - 7, 0, _smush_iconsNut, 0, 0, 0);
+				drawn = true;
+			}
+			
+			if (!drawn) {
+				
+				// Fallback if sprite 5 missing
+				static bool warnedOnce = false;
+				assert(false);
+				if (!warnedOnce) {
+					debug("Rebel2: Failed to draw textured laser. _smush_iconsNut=%p, numChars=%d", 
+						(void *)_smush_iconsNut, _smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
+					warnedOnce = true;
+					
+					// Attempt to lazy load CPITIMHI.NUT if we haven't tried
+					if (true) {
+						delete _smush_iconsNut; 
+						_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMHI.NUT");
+						debug("Rebel2: Attempted to fallback load SYSTM/CPITIMHI.NUT. New numChars=%d", 
+							_smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
+					}
+				}
+				
+				// Draw WHITE lines to be visible against any background
+				drawLine(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, 255);
+				drawLine(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, 255);
+			}
+
+			_shots[i].counter--;
+		}
+	}
+
 	// Draw Crosshair/Reticle cursor
 	// Sprite indices based on handler type (from original game disassembly):
 	// - Handler 8 (ground vehicle): Index 0x2E (46)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index add5db5ddab..e9a43428264 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -70,6 +70,10 @@ class InsaneRebel2 : public Insane {
 		}
 	}
 
+	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
+	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+
 public:
 	InsaneRebel2(ScummEngine_v7 *scumm);
 	~InsaneRebel2();
@@ -186,6 +190,14 @@ public:
 	int32 _playerScore;
 
 
+	struct Shot {
+		bool active;
+		int counter;
+		int x, y;       // Target position
+	};
+	Shot _shots[2];
+	void spawnShot(int x, int y);
+
 };
 
 } // End of namespace Insane


Commit: 2619d4afc6c4f5cc8c7254fb37206047fee6c4eb
    https://github.com/scummvm/scummvm/commit/2619d4afc6c4f5cc8c7254fb37206047fee6c4eb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00

Commit Message:
SCUMM: RA2: Refactor enemy state handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 98ca2ecf7f6..58aa24aab2d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -360,79 +360,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	//   screenY = centerY - DAT_0043e008 (scrollY)
 
 	if (par1 == 4) {
-		// Opcode 4: Enemy position update
-		// Read 5 shorts from the stream (offset +8 through +16)
-		int16 enemyId = b.readSint16LE();  // Offset +8
-		int16 x = b.readSint16LE();        // Offset +10 (0x0A)
-		
-		// If enemy is disabled in bit table, skip update
-		bool disabled = isBitSet(enemyId);
-		
-		int16 y = b.readSint16LE();        // Offset +12 (0x0C)
-		int16 w = b.readSint16LE();        // Offset +14 (0x0E) - Width
-		int16 h = b.readSint16LE();        // Offset +16 (0x10) - Height
-
-		// If disabled, stop processing this object
-		if (disabled) {
-			// debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
-			return; 
-		}
-
-		// The disassembly shows half-width/half-height are used for centering:
-		//   halfW = w >> 1
-		//   halfH = h >> 1
-		//   centerX = x + halfW
-		//   centerY = y + halfH
-		// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
-		
-		//debug("Rebel2 IACT Opcode 4: ID=%d X=%d Y=%d W=%d H=%d (par2=%d par3=%d par4=%d)", 
-		//	enemyId, x, y, w, h, par2, par3, par4);
-
-		// Update RebelEnemy list for hit detection
-		bool found = false;
-		Common::List<RebelEnemy>::iterator it;
-		for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
-			if (it->id == enemyId) {
-				it->rect = Common::Rect(x, y, x + w, y + h);
-				// Only re-activate if not destroyed
-				if (!it->destroyed) {
-					it->active = true;
-				}
-				found = true;
-				break;
-			}
-		}
-		if (!found) {
-			RebelEnemy e;
-			e.id = enemyId;
-			e.rect = Common::Rect(x, y, x + w, y + h);
-			e.active = true;
-			e.destroyed = false;
-			e.explosionFrame = -1;  // No explosion playing
-			_rebelEnemies.push_back(e);
-		}
-
-		// Render Bounding Box for visual verification
-		// Use player dimensions for clipping
-		int bufWidth = (_player && _player->_width > 0) ? _player->_width : 320;
-		int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-		int pitch = bufWidth;
-		
-		// Draw the bounding box in color 255 (bright white) - only for non-destroyed enemies
-		// Skip drawing if this enemy has been destroyed
-		if (renderBitmap && w > 0 && h > 0 && !found) {
-			// Only draw for newly created enemies (not destroyed ones)
-			drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
-		} else if (found && !it->destroyed) {
-			// Draw for existing non-destroyed enemies
-			drawRect(renderBitmap, pitch, x, y, w, h, 255, bufWidth, bufHeight);
-		}
-
-		// Debug for potential HUD Setup (Opcode 4 with word at 8 == 1)
-		if (enemyId == 1) {
-			debug("Rebel2 IACT Opcode 4: HUD Setup Candidate? (enemyId/val at 8 == 1). par4=%d", par4);
-		}
-
+		enemyUpdate(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 2) {
 		// Opcode 2: Often used for bit setting
 		// Disassembly shows this is handled but we don't have full context
@@ -574,6 +502,57 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	}
 }
 
+void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Opcode 4: Enemy position update
+	// Read 5 shorts from the stream (offset +8 through +16)
+	int16 enemyId = b.readSint16LE();  // Offset +8
+	int16 x = b.readSint16LE();        // Offset +10 (0x0A)
+
+	// If enemy is disabled in bit table, skip update
+	bool disabled = isBitSet(enemyId);
+
+	int16 y = b.readSint16LE();        // Offset +12 (0x0C)
+	int16 w = b.readSint16LE();        // Offset +14 (0x0E) - Width
+	int16 h = b.readSint16LE();        // Offset +16 (0x10) - Height
+
+	// If disabled, stop processing this object
+	if (disabled) {
+		// debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
+		return;
+	}
+
+	// The disassembly shows half-width/half-height are used for centering:
+	//   halfW = w >> 1
+	//   halfH = h >> 1
+	//   centerX = x + halfW
+	//   centerY = y + halfH
+	// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
+
+	// Update RebelEnemy list for hit detection
+	bool found = false;
+	Common::List<RebelEnemy>::iterator it;
+	for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+		if (it->id == enemyId) {
+			it->rect = Common::Rect(x, y, x + w, y + h);
+			// Only re-activate if not destroyed
+			if (!it->destroyed) {
+				it->active = true;
+			}
+			found = true;
+			break;
+		}
+	}
+	if (!found) {
+		RebelEnemy e;
+		e.id = enemyId;
+		e.rect = Common::Rect(x, y, x + w, y + h);
+		e.active = true;
+		e.destroyed = false;
+		e.explosionFrame = -1;  // No explosion playing
+		_rebelEnemies.push_back(e);
+	}
+}
+
 // External helpers from smush_player.cpp but we are already in Scumm namespace
 extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index e9a43428264..c3c1c44edbd 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -35,45 +35,6 @@ namespace Scumm {
 
 class InsaneRebel2 : public Insane {
 
- 	// Helper for debugging IACT boxes
-	void drawRect(byte *dst, int pitch, int x, int y, int w, int h, byte color, int bufWidth, int bufHeight) {
-		Common::Rect r(x, y, x + w, y + h);
-		Common::Rect screen(0, 0, bufWidth, bufHeight);
-		Common::Rect clipped = r;
-		clipped.clip(screen);
-
-		if (!clipped.isValidRect()) return;
-
-		// Top
-		if (r.top >= 0 && r.top < bufHeight) {
-			int startX = MAX((int)r.left, 0);
-			int endX = MIN((int)r.right, bufWidth);
-			for (int k = startX; k < endX; k++) dst[r.top * pitch + k] = color;
-		}
-		// Bottom
-		if (r.bottom > 0 && r.bottom <= bufHeight) {
-			int startX = MAX((int)r.left, 0);
-			int endX = MIN((int)r.right, bufWidth);
-			for (int k = startX; k < endX; k++) dst[(r.bottom - 1) * pitch + k] = color;
-		}
-		// Left
-		if (r.left >= 0 && r.left < bufWidth) {
-			int startY = MAX((int)r.top, 0);
-			int endY = MIN((int)r.bottom, bufHeight);
-			for (int k = startY; k < endY; k++) dst[k * pitch + r.left] = color;
-		}
-		// Right
-		if (r.right > 0 && r.right <= bufWidth) {
-			int startY = MAX((int)r.top, 0);
-			int endY = MIN((int)r.bottom, bufHeight);
-			for (int k = startY; k < endY; k++) dst[k * pitch + (r.right - 1)] = color;
-		}
-	}
-
-	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
-	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
-	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
-
 public:
 	InsaneRebel2(ScummEngine_v7 *scumm);
 	~InsaneRebel2();
@@ -106,6 +67,10 @@ public:
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
+	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
+	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+
 	struct RebelEnemy {
 		int id;
 		Common::Rect rect;
@@ -123,6 +88,8 @@ public:
 		~RebelEnemy() { free(savedBackground); }
 	};
 
+	void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+
 	Common::List<RebelEnemy> _rebelEnemies;
 	
 	// Current handler type for Rebel Assault 2 (determines crosshair sprite)


Commit: 764ecbf4591bb35959e3c7c713c4a857006fd4ca
    https://github.com/scummvm/scummvm/commit/764ecbf4591bb35959e3c7c713c4a857006fd4ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00

Commit Message:
SCUMM: RA2: Remove obsolete enemy code

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 58aa24aab2d..67270f11833 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -825,22 +825,6 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, in
 	drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
 }
 
-void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
-	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
-	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
-	int err = dx + dy, e2;
-
-	for (;;) {
-		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height)
-			dst[y0 * pitch + x0] = color;
-		if (x0 == x1 && y0 == y1) break;
-		e2 = 2 * err;
-		if (e2 >= dy) { err += dy; x0 += sx; }
-		if (e2 <= dx) { err += dx; y0 += sy; }
-	}
-}
-
-
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
 
@@ -1154,7 +1138,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				continue;
 			}
 			
-			bool drawn = false;
 			// Use Sprite 5 from CPITIMAG.NUT as the laser texture
 			// Confirmed by reverse engineering: Laser uses Sprite 5 (136x13)
 			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
@@ -1175,33 +1158,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				// Draw Projectile Impact
 				// Using Sprite 0 (small flash) or similar at impact point
 				smlayer_drawSomething(renderBitmap, pitch, _shots[i].x - 7, _shots[i].y - 7, 0, _smush_iconsNut, 0, 0, 0);
-				drawn = true;
 			}
 			
-			if (!drawn) {
-				
-				// Fallback if sprite 5 missing
-				static bool warnedOnce = false;
-				assert(false);
-				if (!warnedOnce) {
-					debug("Rebel2: Failed to draw textured laser. _smush_iconsNut=%p, numChars=%d", 
-						(void *)_smush_iconsNut, _smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
-					warnedOnce = true;
-					
-					// Attempt to lazy load CPITIMHI.NUT if we haven't tried
-					if (true) {
-						delete _smush_iconsNut; 
-						_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMHI.NUT");
-						debug("Rebel2: Attempted to fallback load SYSTM/CPITIMHI.NUT. New numChars=%d", 
-							_smush_iconsNut ? _smush_iconsNut->getNumChars() : -1);
-					}
-				}
-				
-				// Draw WHITE lines to be visible against any background
-				drawLine(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, 255);
-				drawLine(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, 255);
-			}
-
 			_shots[i].counter--;
 		}
 	}


Commit: bd56fe06cc8cca8bbadc657e8791634ef28a5fd4
    https://github.com/scummvm/scummvm/commit/bd56fe06cc8cca8bbadc657e8791634ef28a5fd4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:45+02:00

Commit Message:
SCUMM: RA2: Add low-difficulty enemy indicators

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 67270f11833..b7e57c03bd2 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -83,6 +83,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_playerLives = 3;
 	_playerScore = 0;
 
+	_difficulty = 1; // Default to Medium (1). TODO: Read from game config
+
 	_speed = 12;
 	_insaneIsRunning = false;
 
@@ -825,6 +827,49 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, in
 	drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
 }
 
+void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
+	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+	int err = dx + dy, e2;
+
+	for (;;) {
+		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
+			dst[y0 * pitch + x0] = color;
+		}
+		if (x0 == x1 && y0 == y1) break;
+		e2 = 2 * err;
+		if (e2 >= dy) { err += dy; x0 += sx; }
+		if (e2 <= dx) { err += dx; y0 += sy; }
+	}
+}
+
+void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color) {
+	// Draw L-shaped brackets at corners of the rect (x,y,w,h)
+	// Bracket size: approx 8 pixels
+	int armLen = 2;
+	if (armLen > w / 2) armLen = w / 2;
+	if (armLen > h / 2) armLen = h / 2;
+
+	int x2 = x + w - 1;
+	int y2 = y + h - 1;
+
+	// Top-Left Corner
+	drawLine(dst, pitch, width, height, x, y, x + armLen, y, color);
+	drawLine(dst, pitch, width, height, x, y, x, y + armLen, color);
+
+	// Top-Right Corner
+	drawLine(dst, pitch, width, height, x2 - armLen, y, x2, y, color);
+	drawLine(dst, pitch, width, height, x2, y, x2, y + armLen, color);
+
+	// Bottom-Left Corner
+	drawLine(dst, pitch, width, height, x, y2, x + armLen, y2, color);
+	drawLine(dst, pitch, width, height, x, y2 - armLen, x, y2, color);
+
+	// Bottom-Right Corner
+	drawLine(dst, pitch, width, height, x2 - armLen, y2, x2, y2, color);
+	drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
+}
+
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
 
@@ -1056,30 +1101,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			// The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
 			// Note: Explosion rendering is now handled by the separate 5-slot system
 		} else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
-			// Draw bounding box outline for active enemies (debug visualization)
-			// Draw Top
-			if (r.top >= 0 && r.top < height) {
-				for (int x = clipped.left; x < clipped.right; x++) {
-					renderBitmap[r.top * pitch + x] = 255;
-				}
-			}
-			// Draw Bottom
-			if (r.bottom > 0 && r.bottom <= height) {
-				for (int x = clipped.left; x < clipped.right; x++) {
-					renderBitmap[(r.bottom - 1) * pitch + x] = 255;
-				}
-			}
-			// Draw Left
-			if (r.left >= 0 && r.left < width) {
-				for (int y = clipped.top; y < clipped.bottom; y++) {
-					renderBitmap[y * pitch + r.left] = 255;
-				}
-			}
-			// Draw Right
-			if (r.right > 0 && r.right <= width) {
-				for (int y = clipped.top; y < clipped.bottom; y++) {
-					renderBitmap[y * pitch + (r.right - 1)] = 255;
-				}
+			// Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
+			// Hard (2) mode does not show indicators.
+			if (_difficulty < 2) {
+				const byte color = 5; // Green (Standard VGA Index 10)
+				// Clip the rect to screen bounds for drawing logic is handled inside drawLine if implemented safely,
+				// but drawCornerBrackets relies on drawLine which iterates.
+				// We pass full screen width/height to safe-guard.
+				drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
 			}
 		}
 	}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c3c1c44edbd..df1e231da83 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -165,6 +165,10 @@ public:
 	Shot _shots[2];
 	void spawnShot(int x, int y);
 
+	/* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
+	int _difficulty;
+	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
+
 };
 
 } // End of namespace Insane


Commit: 6482cf87c4144c5a64a9471520c697224338aec5
    https://github.com/scummvm/scummvm/commit/6482cf87c4144c5a64a9471520c697224338aec5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:46+02:00

Commit Message:
SCUMM: RA2: Simplify enemy handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index b7e57c03bd2..127f6959b72 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -75,7 +75,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_icons2Nut = nullptr;  // Not used for Rebel2
 	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
 
-	_rebelEnemies.clear();
+	_enemies.clear();
 	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
 
@@ -203,14 +203,14 @@ int32 InsaneRebel2::processMouse() {
 	if (isPressed && !wasPressed) {
 		Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
 		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d", 
-			mousePos.x, mousePos.y, _rebelEnemies.size());
+			mousePos.x, mousePos.y, _enemies.size());
 
 		// Spawn visual shot immediately
 		spawnShot(mousePos.x, mousePos.y);
 
 		// Check for hit on any active enemy
-		Common::List<RebelEnemy>::iterator it;
-		for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+		Common::List<enemy>::iterator it;
+		for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 			debug("  Enemy ID=%d active=%d destroyed=%d rect=(%d,%d)-(%d,%d) contains=%d",
 				it->id, it->active, it->destroyed,
 				it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
@@ -276,8 +276,8 @@ bool InsaneRebel2::shouldSkipFrameUpdate(int left, int top, int width, int heigh
 	int updateArea = width * height;
 	
 	// Check if this update region significantly overlaps with any destroyed enemy
-	Common::List<RebelEnemy>::iterator it;
-	for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+	Common::List<enemy>::iterator it;
+	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 		if (it->destroyed) {
 			// Calculate the intersection of the update rect and enemy rect
 			Common::Rect enemyRect = it->rect;
@@ -530,10 +530,10 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 	//   centerY = y + halfH
 	// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
 
-	// Update RebelEnemy list for hit detection
+	// Update enemy list for hit detection
 	bool found = false;
-	Common::List<RebelEnemy>::iterator it;
-	for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+	Common::List<enemy>::iterator it;
+	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 		if (it->id == enemyId) {
 			it->rect = Common::Rect(x, y, x + w, y + h);
 			// Only re-activate if not destroyed
@@ -545,16 +545,23 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 		}
 	}
 	if (!found) {
-		RebelEnemy e;
-		e.id = enemyId;
-		e.rect = Common::Rect(x, y, x + w, y + h);
-		e.active = true;
-		e.destroyed = false;
-		e.explosionFrame = -1;  // No explosion playing
-		_rebelEnemies.push_back(e);
+		init_enemyStruct(enemyId, x, y, w, h, true, false, -1);
 	}
 }
 
+void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame) {
+	enemy e;
+	e.id = id;
+	e.rect = Common::Rect(x, y, x + w, y + h);
+	e.active = active;
+	e.destroyed = destroyed;
+	e.explosionFrame = explosionFrame;
+	e.savedBackground = nullptr;
+	e.savedBgWidth = 0;
+	e.savedBgHeight = 0;
+	_enemies.push_back(e);
+}
+
 // External helpers from smush_player.cpp but we are already in Scumm namespace
 extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
@@ -1083,11 +1090,11 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	static uint32 lastDebugTime = 0;
 	if ((_vm->_system->getMillis() - lastDebugTime) > 2000) {
 		lastDebugTime = _vm->_system->getMillis();
-		debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _rebelEnemies.size());
+		debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _enemies.size());
 	}
 
-	Common::List<RebelEnemy>::iterator it;
-	for (it = _rebelEnemies.begin(); it != _rebelEnemies.end(); ++it) {
+	Common::List<enemy>::iterator it;
+	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 		Common::Rect r = it->rect;
 		
 		// Clip the rect to screen bounds for safety
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index df1e231da83..649f5df53e0 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -71,7 +71,7 @@ public:
 	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
 	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
 
-	struct RebelEnemy {
+	struct enemy {
 		int id;
 		Common::Rect rect;
 		bool active;
@@ -81,16 +81,12 @@ public:
 		byte *savedBackground;    // Saved background pixels at moment of destruction
 		int savedBgWidth;         // Width of saved background
 		int savedBgHeight;        // Height of saved background
-		
-		RebelEnemy() : id(0), active(false), destroyed(false), explosionFrame(-1),
-		               explosionComplete(false), savedBackground(nullptr), 
-		               savedBgWidth(0), savedBgHeight(0) {}
-		~RebelEnemy() { free(savedBackground); }
 	};
 
+	void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame);
 	void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
-	Common::List<RebelEnemy> _rebelEnemies;
+	Common::List<enemy> _enemies;
 	
 	// Current handler type for Rebel Assault 2 (determines crosshair sprite)
 	// Handler 0: Background only


Commit: 512ae9f8f16e5d2cdbe6700821eaa6a04f6c11d7
    https://github.com/scummvm/scummvm/commit/512ae9f8f16e5d2cdbe6700821eaa6a04f6c11d7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:46+02:00

Commit Message:
SCUMM: RA2: Implement cockpit movement effect

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 127f6959b72..4e529f66291 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -82,6 +82,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_playerDamage = 0;
 	_playerLives = 3;
 	_playerScore = 0;
+	_viewX = 0;
+	_viewY = 0;
 
 	_difficulty = 1; // Default to Medium (1). TODO: Read from game config
 
@@ -208,15 +210,18 @@ int32 InsaneRebel2::processMouse() {
 		// Spawn visual shot immediately
 		spawnShot(mousePos.x, mousePos.y);
 
+		// Calculate world position for hit testing
+		Common::Point worldMousePos(mousePos.x + _viewX, mousePos.y + _viewY);
+
 		// Check for hit on any active enemy
 		Common::List<enemy>::iterator it;
 		for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 			debug("  Enemy ID=%d active=%d destroyed=%d rect=(%d,%d)-(%d,%d) contains=%d",
 				it->id, it->active, it->destroyed,
 				it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
-				it->rect.contains(mousePos));
+				it->rect.contains(worldMousePos));
 				
-			if (it->active && it->rect.contains(mousePos)) {
+			if (it->active && it->rect.contains(worldMousePos)) {
 				// Enemy hit!
 				it->active = false;
 				it->destroyed = true;  // Mark as destroyed so IACT won't re-activate
@@ -744,8 +749,8 @@ void InsaneRebel2::spawnShot(int x, int y) {
 		if (!_shots[i].active) {
 			_shots[i].active = true;
 			_shots[i].counter = 4; // Lasts 4 frames
-			_shots[i].x = x;
-			_shots[i].y = y;
+			_shots[i].x = x + _viewX;
+			_shots[i].y = y + _viewY;
 			// TODO: Play laser sound
 			break;
 		}
@@ -880,8 +885,6 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
 
-	processMouse();
-
 	// Determine correct pitch for the video buffer (usually 320 for Rebel2)
 	int width = _player->_width;
 	int height = _player->_height;
@@ -889,6 +892,24 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _vm->_screenHeight;
 	int pitch = width;
 
+	// Calculate View/Scroll Offsets
+	// Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200)
+	// Map mouse X (0-320) to Scroll X (0-104)
+	// Map mouse Y (0-200) to Scroll Y (0-60)
+	int maxScrollX = width - _vm->_screenWidth;
+	int maxScrollY = height - _vm->_screenHeight;
+	
+	if (maxScrollX < 0) maxScrollX = 0;
+	if (maxScrollY < 0) maxScrollY = 0;
+	
+	// Simple linear mapping: Center of screen corresponds to center of buffer
+	_viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
+	_viewY = (_vm->_mouse.y * maxScrollY) / _vm->_screenHeight;
+	
+	_player->setScrollOffset(_viewX, _viewY);
+
+	processMouse();
+
 	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
 	// Based on FUN_004089ab:
 	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
@@ -900,7 +921,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
 	// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
 	//
-	// For ScummVM, we draw directly to screen at Y=180
+	// We draw directly to screen at Y=180
 	
 	// Use video content coordinates, NOT buffer coordinates
 	const int videoWidth = 320;    // Native video width
@@ -914,9 +935,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
 	// This fills width=320, height=20 starting at Y=180 with color index 4
 	const byte statusBarBgColor = 4;
-	for (int y = statusBarY; y < videoHeight && y < height; y++) {
-		for (int x = 0; x < videoWidth && x < pitch; x++) {
-			renderBitmap[y * pitch + x] = statusBarBgColor;
+
+	for (int y = statusBarY; y < videoHeight; y++) {
+		int destY = y + _viewY;
+		if (destY >= height) continue;
+		for (int x = 0; x < videoWidth; x++) {
+			int destX = x + _viewX;
+			if (destX >= pitch) continue;
+			renderBitmap[destY * pitch + destX] = statusBarBgColor;
 		}
 	}
 	
@@ -949,6 +975,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			// The cockpit overlay sits at Y = 200 - frameHeight
 			destY = statusBarY - frame.height;
 			if (destY < 0) destY = 0;
+
+			// Apply View Offset for static screen elements
+			destX += _viewX;
+			destY += _viewY;
 			
 			// Draw frame with transparency (pixel 0 = transparent)
 			for (int y = 0; y < frame.height && (destY + y) < height; y++) {
@@ -987,7 +1017,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// For ScummVM, we draw directly at Y=statusBarY
 	if (_smush_cockpitNut) {
 		// Debug: Log DISPFONT.NUT sprite info once
-		static bool loggedDispfont = false;
+		/*static bool loggedDispfont = false;
 		if (!loggedDispfont) {
 			int numSprites = _smush_cockpitNut->getNumChars();
 			debug("Rebel2: DISPFONT.NUT has %d sprites, statusBarY=%d:", numSprites, statusBarY);
@@ -997,12 +1027,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				debug("  Sprite %d: %dx%d", i, sw, sh);
 			}
 			loggedDispfont = true;
-		}
+		}*/
 		
 		// Draw status bar background frame (sprite 1) at (0, statusBarY)
 		// This sprite is the full-width status bar background
 		if (_smush_cockpitNut->getNumChars() > 1) {
-			smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, 1, 0, 0);
+			smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, 1, 0, 0);
 		}
 		
 		// Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
@@ -1012,7 +1042,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		if (difficulty > 3) difficulty = 3;
 		int difficultySprite = difficulty + 2;  // sprites 2, 3, 4, or 5
 		if (_smush_cockpitNut->getNumChars() > difficultySprite) {
-			smlayer_drawSomething(renderBitmap, pitch, 0, statusBarY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
+			smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
 		}
 		
 		// Draw shield bar (sprite 6) 
@@ -1051,9 +1081,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				int drawWidth = MIN(barWidth, sw);
 				for (int y = 0; y < sh; y++) {
 					for (int x = 0; x < drawWidth; x++) {
-						// Render to (0 + sx + x, statusBarY + sy + y)
-						int destX = sx + x;
-						int destY = statusBarY + sy + y;
+						// Render to (0 + sx + x + viewX, statusBarY + sy + y + viewY)
+						int destX = sx + x + _viewX;
+						int destY = statusBarY + sy + y + _viewY;
 						if (destX < pitch && destY < height) {
 							byte pixel = src[y * sw + x];
 							if (pixel != 0) {
@@ -1083,15 +1113,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
-	// Debug: Draw bounding boxes for enemies
-	// width/height/pitch already calculated above
 
-	// Debug: Verify buffer format and drawing capability
-	static uint32 lastDebugTime = 0;
-	if ((_vm->_system->getMillis() - lastDebugTime) > 2000) {
-		lastDebugTime = _vm->_system->getMillis();
-		debug("Rebel2 Debug: Buffer %dx%d, Pitch %d, BPP %d, Enemies: %d", width, height, pitch, _vm->_virtscr[kMainVirtScreen].format.bytesPerPixel, _enemies.size());
-	}
 
 	Common::List<enemy>::iterator it;
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -1187,9 +1209,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				int param_9 = base - _shots[i].counter;
 
 				// Draw Left Beam
-				drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X, GUN_LEFT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+				drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
 				// Draw Right Beam
-				drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X, GUN_RIGHT_Y, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+				drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
 
 				// Draw Projectile Impact
 				// Using Sprite 0 (small flash) or similar at impact point
@@ -1233,8 +1255,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				debugCrosshairOnce = true;
 			}
 			
-			// Center the crosshair on mouse position
-			smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, 0, _smush_iconsNut, reticleIndex, 0, 0);
+			// Center the crosshair on mouse position (in world coordinates)
+			smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, 0, _smush_iconsNut, reticleIndex, 0, 0);
 		}
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 649f5df53e0..d3efccf487b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -152,6 +152,9 @@ public:
 	int16 _playerLives;
 	int32 _playerScore;
 
+	int _viewX;
+	int _viewY;
+
 
 	struct Shot {
 		bool active;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index edcf9c62d91..3e1317c18bb 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -243,6 +243,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_compressedFileMode = false;
 	_width = 0;
 	_height = 0;
+	_scrollX = 0;
+	_scrollY = 0;
 	_IACTpos = 0;
 	_speed = -1;
 	_insanity = false;
@@ -380,8 +382,8 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 			uint32 animSize = (bytesRead >= 26) ? READ_BE_UINT32(header + 22) : 0;
 			
 			// Read the userId to determine which HUD slot (1-4)
-			int code = READ_LE_UINT16(header);
-			int flags = READ_LE_UINT16(header + 2);
+			READ_LE_UINT16(header);
+			READ_LE_UINT16(header + 2);
 			int userId = READ_LE_UINT16(header + 6);
 			
 			debug("Rebel2: Extracting embedded SAN from IACT: userId=%d, animSize=%u", userId, animSize);
@@ -1042,6 +1044,9 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			setDirtyColors(0, 255);
 		}
 
+		_width = READ_LE_UINT16(&headerContent[4]);
+		_height = READ_LE_UINT16(&headerContent[6]);
+
 		free(headerContent);
 	}
 }
@@ -1390,10 +1395,12 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 					int frameWidth = MIN(_width, _vm->_screenWidth);
 					int frameHeight = MIN(_height, _vm->_screenHeight);
 
+					const byte *dst = _dst + _scrollY * _width + _scrollX;
+
 					if (_vm->_macScreen) {
-						_vm->mac_drawBufferToScreen(_dst, frameWidth, 0, 0, frameWidth, frameHeight);
+						_vm->mac_drawBufferToScreen(dst, frameWidth, 0, 0, frameWidth, frameHeight);
 					} else {
-						_vm->_system->copyRectToScreen(_dst, _width, 0, 0, frameWidth, frameHeight);
+						_vm->_system->copyRectToScreen(dst, _width, 0, 0, frameWidth, frameHeight);
 					}
 
 					_vm->_system->updateScreen();
@@ -2230,4 +2237,10 @@ void SmushPlayer::clearMaskedRegions() {
 	_maskedRegions.clear();
 }
 
+// Only used by Rebel Assault 2
+void SmushPlayer::setScrollOffset(int x, int y) { 
+	_scrollX = x;
+	_scrollY = y;
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 32d33b2af15..b4d236abf09 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -229,11 +229,14 @@ protected:
 	uint32 _pauseStartTime;
 	uint32 _pauseTime;
 	int16 _curVideoFlags = 0;
+	int _scrollX;
+	int _scrollY;
 
 	void insanity(bool);
 	void setPalette(const byte *palette);
 	void setPaletteValue(int n, byte r, byte g, byte b);
 	void setDirtyColors(int min, int max);
+	void setScrollOffset(int x, int y);
 	void seekSan(const char *file, int32 pos, int32 contFrame);
 	const char *getString(int id);
 


Commit: fd596d3dda3fc112bd3826e398d72805906afc9c
    https://github.com/scummvm/scummvm/commit/fd596d3dda3fc112bd3826e398d72805906afc9c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:46+02:00

Commit Message:
SCUMM: RA2: Refine cockpit movement effect

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 4e529f66291..63c550c1dcd 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -882,6 +882,54 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
 	drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
 }
 
+void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
+	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
+
+	int w = nut->getCharWidth(spriteIdx);
+	int h = nut->getCharHeight(spriteIdx);
+	const byte *src = nut->getCharData(spriteIdx);
+
+	// Clipping
+	int drawX = x;
+	int drawY = y;
+	int drawW = w;
+	int drawH = h;
+	int srcOffsetX = 0;
+	int srcOffsetY = 0;
+
+	if (drawX < 0) {
+		srcOffsetX = -drawX;
+		drawW += drawX;
+		drawX = 0;
+	}
+	if (drawY < 0) {
+		srcOffsetY = -drawY;
+		drawH += drawY;
+		drawY = 0;
+	}
+
+	if (drawX + drawW > width) {
+		drawW = width - drawX;
+	}
+	if (drawY + drawH > height) {
+		drawH = height - drawY;
+	}
+
+	if (drawW <= 0 || drawH <= 0) return;
+
+	// Draw loop
+	for (int iy = 0; iy < drawH; iy++) {
+		const byte *s = src + (srcOffsetY + iy) * w + srcOffsetX;
+		byte *d = dst + (drawY + iy) * pitch + drawX;
+		for (int ix = 0; ix < drawW; ix++) {
+			byte px = s[ix];
+			if (px != 231 && px != 0) { // Check both 0 and 231 (0xE7) for transparency
+				d[ix] = px;
+			}
+		}
+	}
+}
+
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
 
@@ -1032,7 +1080,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// Draw status bar background frame (sprite 1) at (0, statusBarY)
 		// This sprite is the full-width status bar background
 		if (_smush_cockpitNut->getNumChars() > 1) {
-			smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, 1, 0, 0);
+			renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
 		}
 		
 		// Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
@@ -1042,7 +1090,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		if (difficulty > 3) difficulty = 3;
 		int difficultySprite = difficulty + 2;  // sprites 2, 3, 4, or 5
 		if (_smush_cockpitNut->getNumChars() > difficultySprite) {
-			smlayer_drawSomething(renderBitmap, pitch, _viewX, statusBarY + _viewY, 0, _smush_cockpitNut, difficultySprite, 0, 0);
+			renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
 		}
 		
 		// Draw shield bar (sprite 6) 
@@ -1173,7 +1221,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 					int cy = _explosions[i].y - eh / 2;
 					
 					// Draw explosion
-					smlayer_drawSomething(renderBitmap, pitch, cx, cy, 0, _smush_iconsNut, spriteIndex, 0, 0);
+					renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
 				}
 
 				_explosions[i].counter--;
@@ -1215,7 +1263,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 				// Draw Projectile Impact
 				// Using Sprite 0 (small flash) or similar at impact point
-				smlayer_drawSomething(renderBitmap, pitch, _shots[i].x - 7, _shots[i].y - 7, 0, _smush_iconsNut, 0, 0, 0);
+				renderNutSprite(renderBitmap, pitch, width, height, _shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
 			}
 			
 			_shots[i].counter--;
@@ -1256,7 +1304,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			}
 			
 			// Center the crosshair on mouse position (in world coordinates)
-			smlayer_drawSomething(renderBitmap, pitch, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, 0, _smush_iconsNut, reticleIndex, 0, 0);
+			renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
 		}
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index d3efccf487b..442dc2ac959 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -69,7 +69,9 @@ public:
 
 	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
 	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+
 	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+	void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
 
 	struct enemy {
 		int id;


Commit: ea21cbd85265721224829ae438a2f5efa45f2d70
    https://github.com/scummvm/scummvm/commit/ea21cbd85265721224829ae438a2f5efa45f2d70
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:47+02:00

Commit Message:
SCUMM: RA2: Improve laser beam rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 63c550c1dcd..01a1409b2f8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -757,7 +757,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
 	}
 }
 
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx) {
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u) {
 	if (!nut || spriteIdx >= nut->getNumChars()) return;
 
 	const byte *srcData = nut->getCharData(spriteIdx);
@@ -770,50 +770,62 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
 	int err = dx + dy, e2;
 	
-	// Length of line for uv mapping
-	// float length = sqrt(dx*dx + dy*dy);
+	// Total length approximation for UV mapping
+	// We use Manhattan distance or max dimension for interpolation
+	int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
+	if (totalDist == 0) totalDist = 1;
+
+	// If u not specified, use center
+	if (u < 0) u = texW / 2;
+	if (u >= texW) u = texW - 1;
 	
-	int texCenterX = texW / 2;
-	int texCenterY = texH / 2;
-	byte color = srcData[texCenterY * texW + texCenterX]; 
-	
-	// If color is 0/transparent, try to find a valid one
-	if (color == 0) color = 255; // Fallback to white
+	int currentDist = 0;
 
-	// Standard Bresenham but with texture sampling attempt
-	// To make it look like a "beam", we need width.
-	
 	for (;;) {
-		// Draw a 3x3 blob or similar to make it thick
-		if (x0 >= 1 && x0 < width - 1 && y0 >= 1 && y0 < height - 1) {
-			dst[y0 * pitch + x0] = color;
-			// Glow effect (simulated with same color for now)
-			// dst[(y0+1) * pitch + x0] = color;
-			// dst[y0 * pitch + (x0+1)] = color;
+		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
+			// Map currentDist/totalDist to 0..texH (Length of texture mapped to length of line)
+			int v = (currentDist * texH) / totalDist;
+			if (v >= texH) v = texH - 1;
+			
+			byte color = srcData[v * texW + u];
+			
+			// Check for transparency (0 and 231)
+			if (color != 0 && color != 231) { 
+				dst[y0 * pitch + x0] = color;
+			}
 		}
 		
 		if (x0 == x1 && y0 == y1) break;
 		e2 = 2 * err;
 		if (e2 >= dy) { err += dy; x0 += sx; }
 		if (e2 <= dx) { err += dx; y0 += sy; }
+		
+		currentDist++;
 	}
 }
 
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx) {
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx) {
 	if (!nut || spriteIdx >= nut->getNumChars()) return;
 
-	// Reverse-engineered math from FUN_0040bbf6
-	int texW = nut->getCharWidth(spriteIdx);
-	int texH = nut->getCharHeight(spriteIdx);
-	int param_11 = 12; // Standard value from assembly calls
-	if (texW == 0) return;
-
-	// sVar7 = (12 * texH * 16) / texW
-	int sVar7 = (param_11 * texH * 16) / texW;
+	// Reverse-engineered math from FUN_0040bbf6 and FUN_0040ad63
+	
+	// Rebel Assault 2 specific params
+	// param_11 is typically 12 for Level 1, 25 or other for different scenarios.
+	int param_11 = (_rebelLevelType == 1) ? 12 : 25; 
+	// param_9 is also passed as 12 in Level 1 (or 25 otherwise)
+	int param_9 = param_11;
+
+	// sVar7 calculation: (param_11 * progress * 16) / maxProgress
+	// progress = max - counter (goes 0 -> max)
+	if (maxProgress == 0) maxProgress = 1;
+	int sVar7 = (param_11 * progress * 16) / maxProgress;
 	
-	// Extend vector by (13/12)
+	// Extend vector by using param_11 factor
 	int dx = x1 - x0;
 	int dy = y1 - y0;
+	
+	// sVar6 / sVar1 calculation (Extended vector)
+	// extDx = (dx * (param_11 + 1)) / param_11
 	int extDx = (dx * (param_11 + 1)) / param_11;
 	int extDy = (dy * (param_11 + 1)) / param_11;
 	
@@ -835,8 +847,27 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, in
 	int endX = xExt - (extDx * 16) / endFactor;
 	int endY = yExt - (extDy * 16) / endFactor;
 
-	// Draw the segment
-	drawTexturedLine(dst, pitch, width, height, startX, startY, endX, endY, nut, spriteIdx);
+	// Draw the beam with thickness
+	// Iterate through the texture width columns and draw a line for each
+	int texW = nut->getCharWidth(spriteIdx);
+	if (texW > 0) {
+		for (int i = 0; i < texW; i++) {
+			int u = i;
+			int offset = i - texW / 2;
+			
+			// Calculate perpendicular offset
+			// Simple approximation: If beam is more vertical, offset X. If horizontal, offset Y.
+			int ox = 0, oy = 0;
+			if (abs(dy) > abs(dx)) {
+				ox = offset;
+			} else {
+				oy = offset;
+			}
+			
+			// Draw line for this texture column
+			drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, u);
+		}
+	}
 }
 
 void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
@@ -1244,22 +1275,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				continue;
 			}
 			
-			// Use Sprite 5 from CPITIMAG.NUT as the laser texture
-			// Confirmed by reverse engineering: Laser uses Sprite 5 (136x13)
-			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
-				// Calculate param_9 equivalent (Counter dependent)
-				// Original: TableValue - Counter
-				// We don't have table, but assembly suggests values around 0x20-0x80 range?
-				// Math divides by (param_9 + offset). Higher param_9 = Longer beam.
-				// As counter decreases (4->0), value increases (26->30), beam grows/moves.
-				// Let's try base 30.
-				int base = 30; 
-				int param_9 = base - _shots[i].counter;
+			// Use Sprite 0 from CPITIMAG.NUT as the laser texture (15x15 projectile)
+			// Confirmed by info.md: indexes 0-4 are laser/projectile
+			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 0) {
+				// Calculate progress
+				int maxProgress = 4;
+				int progress = maxProgress - _shots[i].counter;
 
 				// Draw Left Beam
-				drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+				drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
 				// Draw Right Beam
-				drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, param_9, _smush_iconsNut, 5);
+				drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
 
 				// Draw Projectile Impact
 				// Using Sprite 0 (small flash) or similar at impact point
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 442dc2ac959..92967c3a763 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,9 +68,9 @@ public:
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
 	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
-	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx);
+	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u = -1);
 
-	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int param_9, NutRenderer *nut, int spriteIdx);
+	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx);
 	void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
 
 	struct enemy {


Commit: 474e3d5b7b0644784eb0f891cb5c6d7f9a2a28bf
    https://github.com/scummvm/scummvm/commit/474e3d5b7b0644784eb0f891cb5c6d7f9a2a28bf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:47+02:00

Commit Message:
SCUMM: RA2: Refine laser beam rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 01a1409b2f8..37dad159f4c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -757,7 +757,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
 	}
 }
 
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u) {
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v) {
 	if (!nut || spriteIdx >= nut->getNumChars()) return;
 
 	const byte *srcData = nut->getCharData(spriteIdx);
@@ -765,27 +765,24 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	int texH = nut->getCharHeight(spriteIdx);
 	
 	if (!srcData || texW <= 0 || texH <= 0) return;
+	if (v < 0) v = 0;
+	if (v >= texH) v = texH - 1;
 
 	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
 	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
 	int err = dx + dy, e2;
 	
 	// Total length approximation for UV mapping
-	// We use Manhattan distance or max dimension for interpolation
 	int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
 	if (totalDist == 0) totalDist = 1;
-
-	// If u not specified, use center
-	if (u < 0) u = texW / 2;
-	if (u >= texW) u = texW - 1;
 	
 	int currentDist = 0;
 
 	for (;;) {
 		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
-			// Map currentDist/totalDist to 0..texH (Length of texture mapped to length of line)
-			int v = (currentDist * texH) / totalDist;
-			if (v >= texH) v = texH - 1;
+			// Map currentDist/totalDist to 0..texW (Run along texture width)
+			int u = (currentDist * texW) / totalDist;
+			if (u >= texW) u = texW - 1;
 			
 			byte color = srcData[v * texW + u];
 			
@@ -804,69 +801,67 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	}
 }
 
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx) {
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx) {
 	if (!nut || spriteIdx >= nut->getNumChars()) return;
 
-	// Reverse-engineered math from FUN_0040bbf6 and FUN_0040ad63
-	
-	// Rebel Assault 2 specific params
-	// param_11 is typically 12 for Level 1, 25 or other for different scenarios.
+	int texH = nut->getCharHeight(spriteIdx);
+	// param_11 is typically 12 for Level 1
 	int param_11 = (_rebelLevelType == 1) ? 12 : 25; 
-	// param_9 is also passed as 12 in Level 1 (or 25 otherwise)
-	int param_9 = param_11;
 
 	// sVar7 calculation: (param_11 * progress * 16) / maxProgress
-	// progress = max - counter (goes 0 -> max)
 	if (maxProgress == 0) maxProgress = 1;
 	int sVar7 = (param_11 * progress * 16) / maxProgress;
 	
-	// Extend vector by using param_11 factor
+	// Extend vector by (param_11 + 1) / param_11
 	int dx = x1 - x0;
 	int dy = y1 - y0;
-	
-	// sVar6 / sVar1 calculation (Extended vector)
-	// extDx = (dx * (param_11 + 1)) / param_11
 	int extDx = (dx * (param_11 + 1)) / param_11;
 	int extDy = (dy * (param_11 + 1)) / param_11;
 	
 	int xExt = x0 + extDx;
 	int yExt = y0 + extDy;
 
-	// Calculate New Start Point (sVar4, sVar5)
+	// Calculate New Start Point
 	// Start = ExtEnd - (ExtEnd - Start) * (16 / (sVar7 + 16))
-	// Actually math is: sVar4 = (xExt) - (extDx * 16) / (sVar7 + 16)
 	int startFactor = sVar7 + 16;
 	if (startFactor == 0) startFactor = 1;
 	int startX = xExt - (extDx * 16) / startFactor;
 	int startY = yExt - (extDy * 16) / startFactor;
 
-	// Calculate New End Point (sVar6, sVar7 in asm) w.r.t param_9
+	// Calculate New End Point
 	// End = ExtEnd - (ExtEnd - Start) * (16 / (param_9 + sVar7 + 16))
 	int endFactor = param_9 + sVar7 + 16;
-	if (endFactor == 0) endFactor = 1; // Safety
+	if (endFactor == 0) endFactor = 1; 
 	int endX = xExt - (extDx * 16) / endFactor;
 	int endY = yExt - (extDy * 16) / endFactor;
 
-	// Draw the beam with thickness
-	// Iterate through the texture width columns and draw a line for each
-	int texW = nut->getCharWidth(spriteIdx);
-	if (texW > 0) {
-		for (int i = 0; i < texW; i++) {
-			int u = i;
-			int offset = i - texW / 2;
-			
-			// Calculate perpendicular offset
-			// Simple approximation: If beam is more vertical, offset X. If horizontal, offset Y.
-			int ox = 0, oy = 0;
-			if (abs(dy) > abs(dx)) {
-				ox = offset;
-			} else {
-				oy = offset;
-			}
-			
-			// Draw line for this texture column
-			drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, u);
-		}
+	// Calculate perpendicular offsets for thickness
+	// Perpendicular vector (-dy, dx) normalized?
+	// Assembly just adds to Y or X depending on loop (simplified) but usually laser is line
+	// We'll use a simple approach: if mostly vertical, offset X. If mostly horizontal, offset Y.
+	int perX, perY;
+	if (abs(dx) > abs(dy)) {
+		perX = 0; perY = 1;
+	} else {
+		perX = 1; perY = 0;
+	}
+
+	// Iterate thickness (Rows of texture)
+	// Map loop index 0..thickness to texture row 0..texH
+	// This scales the texture height to the beam width
+	
+	int centerOffset = thickness / 2;
+
+	for (int i = 0; i < thickness; i++) {
+		// Calculate V coordinate: Scale i (0..thickness) to (0..texH)
+		int v = (i * texH) / thickness;
+		if (v >= texH) v = texH - 1;
+		
+		int off = i - centerOffset;
+		int ox = off * perX;
+		int oy = off * perY;
+		
+		drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, v);
 	}
 }
 
@@ -1279,13 +1274,43 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			// Confirmed by info.md: indexes 0-4 are laser/projectile
 			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 0) {
 				// Calculate progress
-				int maxProgress = 4;
+				int maxProgress = 4; // Max duration from table (supposedly)
 				int progress = maxProgress - _shots[i].counter;
 
-				// Draw Left Beam
-				drawLaserBeam(renderBitmap, pitch, width, height, GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
-				// Draw Right Beam
-				drawLaserBeam(renderBitmap, pitch, width, height, GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y, progress, maxProgress, _smush_iconsNut, 0);
+				// Draw Beams depending on Level Type
+				// Scene 1 (LevelType 1) has 3 beams: Right, Middle, Left
+				if (_rebelLevelType <= 1) { // Default or Type 1
+					// Right Beam: Origin(310, 170), Thickness 8, LengthFac 12
+					drawLaserBeam(renderBitmap, pitch, width, height, 
+						310 + _viewX, 170 + _viewY, 
+						_shots[i].x, _shots[i].y, 
+						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+					// Middle Beam: Origin(160, 380), Thickness 5, LengthFac 8
+					// Note: 380 is virtual origin below screen
+					drawLaserBeam(renderBitmap, pitch, width, height, 
+						160 + _viewX, 380 + _viewY, 
+						_shots[i].x, _shots[i].y, 
+						progress, maxProgress, 5, 8, _smush_iconsNut, 0);
+
+					// Left Beam: Origin(10, 170), Thickness 8, LengthFac 12
+					drawLaserBeam(renderBitmap, pitch, width, height, 
+						10 + _viewX, 170 + _viewY, 
+						_shots[i].x, _shots[i].y, 
+						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+						
+				} else {
+					// Fallback for other levels (2 beams)
+					drawLaserBeam(renderBitmap, pitch, width, height, 
+						GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, 
+						_shots[i].x, _shots[i].y, 
+						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+					drawLaserBeam(renderBitmap, pitch, width, height, 
+						GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, 
+						_shots[i].x, _shots[i].y, 
+						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+				}
 
 				// Draw Projectile Impact
 				// Using Sprite 0 (small flash) or similar at impact point
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 92967c3a763..73211c1e629 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,9 +68,9 @@ public:
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
 	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
-	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int u = -1);
+	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v);
 
-	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, NutRenderer *nut, int spriteIdx);
+	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx);
 	void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
 
 	struct enemy {


Commit: b33180f25f1c1cb7912038c19b5f0935bb722a67
    https://github.com/scummvm/scummvm/commit/b33180f25f1c1cb7912038c19b5f0935bb722a67
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:47+02:00

Commit Message:
SCUMM: RA2: Complete laser beam rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 37dad159f4c..5dffc5a725f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -757,7 +757,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
 	}
 }
 
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v) {
+void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
 	if (!nut || spriteIdx >= nut->getNumChars()) return;
 
 	const byte *srcData = nut->getCharData(spriteIdx);
@@ -786,8 +786,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 			
 			byte color = srcData[v * texW + u];
 			
-			// Check for transparency (0 and 231)
-			if (color != 0 && color != 231) { 
+			// Check for transparency (0 and optionally 231)
+			if (color != 0 && (!mask231 || color != 231)) { 
 				dst[y0 * pitch + x0] = color;
 			}
 		}
@@ -801,70 +801,186 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	}
 }
 
+// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
+static void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
+	// Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
+	int sVar4 = 0;                // left
+	int sVar1 = 0;                // top
+	int sVar7 = width - 1;        // right
+	int sVar10 = height - 1;      // bottom
+
+	int px0 = param_3;
+	int py0 = param_4;
+	int px1 = param_5;
+	int py1 = param_6;
+
+	// Clip against screen bounds (translation of original clipping logic)
+	if (px0 == px1) {
+		if (px0 < sVar4 || px0 > sVar7) return;
+	} else {
+		if (px0 < sVar4) {
+			if (px1 < sVar4) return;
+			py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
+			px0 = sVar4;
+		} else if (px0 > sVar7) {
+			if (px1 > sVar7) return;
+			py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
+			px0 = sVar7;
+		}
+		if (px1 < sVar4) {
+			py1 = py0 + ((py1 - py0) * (sVar4 - px0)) / (px1 - px0);
+			px1 = sVar4;
+		} else if (px1 > sVar7) {
+			py1 = py0 + ((py1 - py0) * (sVar7 - px0)) / (px1 - px0);
+			px1 = sVar7;
+		}
+	}
+
+	if (py0 == py1) {
+		if (py0 < sVar1 || py0 > sVar10) return;
+	} else {
+		if (py0 < sVar1) {
+			if (py1 < sVar1) return;
+			px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
+			py0 = sVar1;
+		} else if (py0 > sVar10) {
+			if (py1 > sVar10) return;
+			px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
+			py0 = sVar10;
+		}
+		if (py1 < sVar1) {
+			px1 = px0 + ((px1 - px0) * (sVar1 - py0)) / (py1 - py0);
+			py1 = sVar1;
+		} else if (py1 > sVar10) {
+			px1 = px0 + ((px1 - px0) * (sVar10 - py0)) / (py1 - py0);
+			py1 = sVar10;
+		}
+	}
+
+	int dx = px1 - px0;
+	int dy = py1 - py0;
+	int absdx = dx < 0 ? -dx : dx;
+	int absdy = dy < 0 ? -dy : dy;
+
+	// pointer into destination and texture
+	byte *baseDst = dst;
+	const byte *texPtr = param_8;
+
+	if (absdx == 0) {
+		if (absdy == 0) {
+			if (*texPtr != 0) baseDst[py0 * pitch + px0] = *texPtr;
+			return;
+		}
+		// vertical-ish
+		int step = absdy + 1;
+		int curY = py0;
+		int signY = dy > 0 ? 1 : -1;
+		int iVar9 = step; // adv counter
+		for (int i = 0; i < step; i++) {
+			if (*texPtr != 0) baseDst[curY * pitch + px0] = *texPtr;
+			curY += signY;
+			iVar9 -= param_7;
+			while (iVar9 < 0) { texPtr++; iVar9 += step; }
+		}
+		return;
+	}
+
+	if (absdy == 0) {
+		// horizontal-ish
+		int step = absdx + 1;
+		int curX = px0;
+		int signX = dx > 0 ? 1 : -1;
+		int iVar11 = step;
+		for (int i = 0; i < step; i++) {
+			if (*texPtr != 0) baseDst[py0 * pitch + curX] = *texPtr;
+			curX += signX;
+			iVar11 -= param_7;
+			while (iVar11 < 0) { texPtr++; iVar11 += step; }
+		}
+		return;
+	}
+
+	// general case
+	int steps = (absdx > absdy) ? absdx + 1 : absdy + 1;
+	int x = px0, y = py0;
+	int sx = dx > 0 ? 1 : -1;
+	int sy = dy > 0 ? 1 : -1;
+	int err = absdx - absdy;
+	int iVar12 = steps;
+
+	for (int i = 0; i < steps; i++) {
+		if (x >= 0 && x < width && y >= 0 && y < height) {
+			if (*texPtr != 0) baseDst[y * pitch + x] = *texPtr;
+		}
+		int e2 = 2 * err;
+		if (e2 > -absdy) { err -= absdy; x += sx; }
+		if (e2 < absdx) { err += absdx; y += sy; }
+		iVar12 -= param_7;
+		if (iVar12 < 0) { texPtr++; iVar12 += steps; }
+	}
+}
+
+
 void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx) {
 	if (!nut || spriteIdx >= nut->getNumChars()) return;
 
+	// Follow original FUN_0040BBF6 math precisely
+	int texW = nut->getCharWidth(spriteIdx);
 	int texH = nut->getCharHeight(spriteIdx);
-	// param_11 is typically 12 for Level 1
-	int param_11 = (_rebelLevelType == 1) ? 12 : 25; 
+	int param_11 = (_rebelLevelType <= 1) ? 12 : 25;
 
-	// sVar7 calculation: (param_11 * progress * 16) / maxProgress
 	if (maxProgress == 0) maxProgress = 1;
 	int sVar7 = (param_11 * progress * 16) / maxProgress;
-	
-	// Extend vector by (param_11 + 1) / param_11
+
 	int dx = x1 - x0;
 	int dy = y1 - y0;
-	int extDx = (dx * (param_11 + 1)) / param_11;
-	int extDy = (dy * (param_11 + 1)) / param_11;
-	
-	int xExt = x0 + extDx;
-	int yExt = y0 + extDy;
-
-	// Calculate New Start Point
-	// Start = ExtEnd - (ExtEnd - Start) * (16 / (sVar7 + 16))
-	int startFactor = sVar7 + 16;
-	if (startFactor == 0) startFactor = 1;
-	int startX = xExt - (extDx * 16) / startFactor;
-	int startY = yExt - (extDy * 16) / startFactor;
-
-	// Calculate New End Point
-	// End = ExtEnd - (ExtEnd - Start) * (16 / (param_9 + sVar7 + 16))
-	int endFactor = param_9 + sVar7 + 16;
-	if (endFactor == 0) endFactor = 1; 
-	int endX = xExt - (extDx * 16) / endFactor;
-	int endY = yExt - (extDy * 16) / endFactor;
-
-	// Calculate perpendicular offsets for thickness
-	// Perpendicular vector (-dy, dx) normalized?
-	// Assembly just adds to Y or X depending on loop (simplified) but usually laser is line
-	// We'll use a simple approach: if mostly vertical, offset X. If mostly horizontal, offset Y.
-	int perX, perY;
-	if (abs(dx) > abs(dy)) {
-		perX = 0; perY = 1;
+	int sVar6 = ((dx) * (param_11 + 1)) / param_11;
+	int sVar1 = ((dy) * (param_11 + 1)) / param_11;
+
+	int sVar4 = (sVar6 + x0) - (sVar6 * 16) / (sVar7 + 16);
+	int sVar5 = (sVar1 + y0) - (sVar1 * 16) / (sVar7 + 16);
+	int sVar6_end = (sVar6 + x0) - (sVar6 * 16) / (param_9 + sVar7 + 16);
+	int sVar7_end = (sVar1 + y0) - (sVar1 * 16) / (param_9 + sVar7 + 16);
+
+	const byte *srcBase = nut->getCharData(spriteIdx);
+	if (!srcBase || texW <= 0 || texH <= 0) return;
+
+	int iVar2 = abs(sVar5 - sVar7_end);
+	int iVar3 = abs(sVar4 - sVar6_end);
+
+	if (iVar2 < iVar3) {
+		// Column major case (wide)
+		iVar2 = abs(sVar4 - sVar6_end);
+		long long temp = (long long)iVar2 * (long long)texH * (long long)thickness;
+		// sVar1calc = (temp >> 3) / texW + 2
+		int sVar1calc = (int)((temp >> 3) / texW) + 2;
+		int local_24 = -sVar1calc;
+		int sVar8 = sVar1calc >> 1;
+		const byte *local_28 = srcBase;
+		for (int local_2c = 0; local_2c < sVar1calc; local_2c++) {
+			drawTexturedSegment(dst, pitch, width, height, sVar4, (sVar5 - sVar8) + local_2c,
+						 sVar6_end, (sVar7_end - sVar8) + local_2c, texW, local_28);
+			for (local_24 = texH + local_24; local_24 > 0; local_24 -= sVar1calc) {
+				local_28 += texW;
+			}
+		}
 	} else {
-		perX = 1; perY = 0;
-	}
-
-	// Iterate thickness (Rows of texture)
-	// Map loop index 0..thickness to texture row 0..texH
-	// This scales the texture height to the beam width
-	
-	int centerOffset = thickness / 2;
-
-	for (int i = 0; i < thickness; i++) {
-		// Calculate V coordinate: Scale i (0..thickness) to (0..texH)
-		int v = (i * texH) / thickness;
-		if (v >= texH) v = texH - 1;
-		
-		int off = i - centerOffset;
-		int ox = off * perX;
-		int oy = off * perY;
-		
-		drawTexturedLine(dst, pitch, width, height, startX + ox, startY + oy, endX + ox, endY + oy, nut, spriteIdx, v);
+		// Row major case (tall)
+		iVar2 = abs(sVar5 - sVar7_end);
+		int local_30 = (int)(((long long)iVar2 * (long long)texH) / texW) + 2;
+		if (texH < local_30) local_30 = texH;
+		int local_24 = -local_30;
+		const byte *local_28 = srcBase;
+		int sVar1_half = local_30 >> 1;
+		for (int local_2c = 0; local_2c < local_30; local_2c++) {
+			drawTexturedSegment(dst, pitch, width, height, (sVar4 - sVar1_half) + local_2c, sVar5,
+						 (sVar6_end - sVar1_half) + local_2c, sVar7_end, texW, local_28);
+			for (local_24 = texH + local_24; local_24 > 0; local_24 -= local_30) {
+				local_28 += texW;
+			}
+		}
 	}
 }
-
 void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
 	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
 	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 73211c1e629..abe4dd428a0 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,7 +68,8 @@ public:
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
 	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
-	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v);
+	// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
+	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
 
 	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx);
 	void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);


Commit: 0e87f2c99c8a51b9283038fd007789b7c7e977ba
    https://github.com/scummvm/scummvm/commit/0e87f2c99c8a51b9283038fd007789b7c7e977ba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:48+02:00

Commit Message:
SCUMM: RA2: Play intro before level 1

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5dffc5a725f..cbd3ecd607b 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -75,6 +75,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_icons2Nut = nullptr;  // Not used for Rebel2
 	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
 
+	// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
+	_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
+
 	_enemies.clear();
 	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -191,6 +194,7 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _rebelHudDamage;
 	delete _rebelHudEffects;
 	delete _rebelHudHiRes;
+	delete _rebelMsgFont;
 }
 
 
@@ -1117,7 +1121,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const int videoWidth = 320;    // Native video width
 	const int videoHeight = 200;   // Native video height  
 	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
-	
+
+	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
+	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+	if (!introPlaying) {
 	// ============================================================
 	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
 	// ============================================================
@@ -1477,3 +1484,5 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 }
 
 }
+
+}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index abe4dd428a0..121110b9532 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -41,6 +41,10 @@ public:
 
 	NutRenderer *_smush_cockpitNut;
 	NutRenderer *_smush_dispfontNut;  // DAT_00482200 - DISPFONT.NUT for status bar (difficulty, shields, lives, score)
+
+	// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
+	SmushFont *_rebelMsgFont;
+	bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
 	
 	// Rebel Assault 2: Dynamically loaded HUD overlays (from CHK scripts)
 	// These correspond to the original game's global variables
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 3ebc7429d00..cbc1a7ebad1 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2686,10 +2686,16 @@ Common::Error ScummEngine::go() {
 	if (_game.id == GID_REBEL2) {
 		// Use 12 FPS as default, same as The Dig. FT uses 10.
 		// Since we don't have the standard scripts initializing this, we pass it here.
-		if (_game.features & GF_DEMO)
+		if (_game.features & GF_DEMO) {
 			((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
-		else
+		} else {
+			// Mark the following SAN as an intro so HUD isn't rendered during it (bit 0x20)
+			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
+			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
+			// Clear intro flag and immediately start the mission SAN
+			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
 			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
+		}
 		return Common::kNoError;
 	}
 #endif


Commit: 77722bbfed261fc5295c72f9d22392effaff383c
    https://github.com/scummvm/scummvm/commit/77722bbfed261fc5295c72f9d22392effaff383c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:48+02:00

Commit Message:
SCUMM: RA2: Remove obsolete setup code

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cbd3ecd607b..74d11f5ec41 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -202,7 +202,7 @@ int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
 
 	// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
-	static bool wasPressed = false;
+	bool wasPressed = false;
 	bool isPressed = (_vm->_system->getEventManager()->getButtonState() & 1) != 0;
 	
 	// Edge detection: only trigger on button press (not hold)
@@ -806,7 +806,7 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 }
 
 // Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-static void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
+void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
 	// Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
 	int sVar4 = 0;                // left
 	int sVar1 = 0;                // top
@@ -1190,13 +1190,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 					}
 				}
 			}
-			
-			static bool debugEmbeddedOnce[5] = {false};
-			if (!debugEmbeddedOnce[hudSlot]) {
-				debug("Rebel2: Drawing embedded HUD slot %d (%dx%d) at (%d,%d)", 
-					hudSlot, frame.width, frame.height, destX, destY);
-				debugEmbeddedOnce[hudSlot] = true;
-			}
 		}
 	}
 	
@@ -1209,23 +1202,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	//   - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
 	//   - Sprite 6: Shield bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
 	//   - Sprite 7: Shield alert (flashing red when shields critical)
-	//
-	// All sprites are drawn to buffer at (0,0), buffer composited at Y=180
-	// For ScummVM, we draw directly at Y=statusBarY
 	if (_smush_cockpitNut) {
-		// Debug: Log DISPFONT.NUT sprite info once
-		/*static bool loggedDispfont = false;
-		if (!loggedDispfont) {
-			int numSprites = _smush_cockpitNut->getNumChars();
-			debug("Rebel2: DISPFONT.NUT has %d sprites, statusBarY=%d:", numSprites, statusBarY);
-			for (int i = 0; i < numSprites && i < 10; i++) {
-				int sw = _smush_cockpitNut->getCharWidth(i);
-				int sh = _smush_cockpitNut->getCharHeight(i);
-				debug("  Sprite %d: %dx%d", i, sw, sh);
-			}
-			loggedDispfont = true;
-		}*/
-		
 		// Draw status bar background frame (sprite 1) at (0, statusBarY)
 		// This sprite is the full-width status bar background
 		if (_smush_cockpitNut->getNumChars() > 1) {
@@ -1302,15 +1279,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		
 		// Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
 		// TODO: Implement score rendering
-	} else {
-		static bool warnedNullOnce = false;
-		if (!warnedNullOnce) {
-			debug("Rebel2: WARNING - _smush_cockpitNut (DISPFONT.NUT) is null!");
-			warnedNullOnce = true;
-		}
-	}
-
-
+	} 
 
 	Common::List<enemy>::iterator it;
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -1330,7 +1299,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			// Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
 			// Hard (2) mode does not show indicators.
 			if (_difficulty < 2) {
-				const byte color = 5; // Green (Standard VGA Index 10)
+				const byte color = 5; // Green color index for brackets
 				// Clip the rect to screen bounds for drawing logic is handled inside drawLine if implemented safely,
 				// but drawCornerBrackets relies on drawLine which iterates.
 				// We pass full screen width/height to safe-guard.
@@ -1469,14 +1438,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		if (_smush_iconsNut->getNumChars() > reticleIndex) {
 			int cw = _smush_iconsNut->getCharWidth(reticleIndex);
 			int ch = _smush_iconsNut->getCharHeight(reticleIndex);
-			
-			static bool debugCrosshairOnce = false;
-			if (!debugCrosshairOnce) {
-				debug("Rebel2: Drawing crosshair sprite %d at (%d,%d) size %dx%d", 
-					reticleIndex, _vm->_mouse.x - cw / 2, _vm->_mouse.y - ch / 2, cw, ch);
-				debugCrosshairOnce = true;
-			}
-			
 			// Center the crosshair on mouse position (in world coordinates)
 			renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
 		}
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index cbc1a7ebad1..020da441c8a 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2616,16 +2616,6 @@ void ScummEngine_v7::syncSoundSettings() {
 	if (!_setupIsComplete)
 		return;
 
-	if (_game.id == GID_REBEL2) {
-		ScummEngine::syncSoundSettings();
-		if (_imuseDigital) {
-			_imuseDigital->diMUSESetMusicGroupVol(ConfMan.getInt("music_volume") / 2);
-			_imuseDigital->diMUSESetVoiceGroupVol(ConfMan.getInt("speech_volume") / 2);
-			_imuseDigital->diMUSESetSFXGroupVol(ConfMan.getInt("sfx_volume") / 2);
-		}
-		return;
-	}
-
 	if (!isUsingOriginalGUI()) {
 		ScummEngine::syncSoundSettings();
 		if (_splayer) {
@@ -2690,8 +2680,8 @@ Common::Error ScummEngine::go() {
 			((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
 		} else {
 			// Mark the following SAN as an intro so HUD isn't rendered during it (bit 0x20)
-			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
-			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
+			//((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
+			//((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
 			// Clear intro flag and immediately start the mission SAN
 			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
 			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);


Commit: 6b45c553681dd0aa8ed99eac08def7c5ffb0fd53
    https://github.com/scummvm/scummvm/commit/6b45c553681dd0aa8ed99eac08def7c5ffb0fd53
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:48+02:00

Commit Message:
SCUMM: RA2: Clean up state initialization

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 74d11f5ec41..9709bf850f7 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -183,6 +183,15 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_shots[i].active = false;
 		_shots[i].counter = 0;
 	}
+
+	for (i = 0; i < 5; i++) {
+		_rebelEmbeddedHud[i].pixels = nullptr;
+		_rebelEmbeddedHud[i].width = 0;
+		_rebelEmbeddedHud[i].height = 0;
+		_rebelEmbeddedHud[i].renderX = 0;
+		_rebelEmbeddedHud[i].renderY = 0;
+		_rebelEmbeddedHud[i].valid = false;
+	}
 }
 
 
@@ -625,7 +634,6 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 					
 					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
 						if (frame.width != width || frame.height != height || !frame.pixels) {
-							frame.clear();
 							frame.pixels = (byte *)calloc(width * height, 1);
 							frame.width = width;
 							frame.height = height;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 121110b9532..13898780160 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -123,11 +123,6 @@ public:
 		int renderX;       // X position to render (0 = centered based on slot)
 		int renderY;       // Y position to render
 		bool valid;        // True if this slot has valid data
-		
-		EmbeddedSanFrame() : pixels(nullptr), width(0), height(0), 
-		                     renderX(0), renderY(0), valid(false) {}
-		~EmbeddedSanFrame() { free(pixels); }
-		void clear() { free(pixels); pixels = nullptr; width = height = 0; valid = false; }
 	};
 	
 	EmbeddedSanFrame _rebelEmbeddedHud[5];  // Index 0 unused, 1-4 for userId slots


Commit: df10c54615eb1cec5358b9eeaa604497499462ca
    https://github.com/scummvm/scummvm/commit/df10c54615eb1cec5358b9eeaa604497499462ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:49+02:00

Commit Message:
SCUMM: RA2: Remove unused state fields

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 9709bf850f7..f091ecc0f3c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -60,13 +60,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_bensgoggNut = nullptr;
 
 	// Rebel Assault 2 specific initialization can go here
-	_rebelHudPrimary = nullptr;
-	_rebelHudSecondary = nullptr;
-	_rebelHudCockpit = nullptr;
-	_rebelHudExplosion = nullptr;
-	_rebelHudDamage = nullptr;
-	_rebelHudEffects = nullptr;
-	_rebelHudHiRes = nullptr;
 
 	// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
 	// CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
@@ -196,13 +189,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 
 InsaneRebel2::~InsaneRebel2() {
-	delete _rebelHudPrimary;
-	delete _rebelHudSecondary;
-	delete _rebelHudCockpit;
-	delete _rebelHudExplosion;
-	delete _rebelHudDamage;
-	delete _rebelHudEffects;
-	delete _rebelHudHiRes;
 	delete _rebelMsgFont;
 }
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 13898780160..b75564b6486 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -46,15 +46,6 @@ public:
 	SmushFont *_rebelMsgFont;
 	bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
 	
-	// Rebel Assault 2: Dynamically loaded HUD overlays (from CHK scripts)
-	// These correspond to the original game's global variables
-	NutRenderer *_rebelHudPrimary;     // DAT_00482240 - Primary HUD overlay
-	NutRenderer *_rebelHudSecondary;   // DAT_00482238 - Secondary HUD graphics
-	NutRenderer *_rebelHudCockpit;     // DAT_00482268 - Ship cockpit frame overlay
-	NutRenderer *_rebelHudExplosion;   // DAT_00482250 - Explosion overlay sprites
-	NutRenderer *_rebelHudDamage;      // DAT_00482248 - Damage indicator sprites
-	NutRenderer *_rebelHudEffects;     // DAT_00482258 - Additional effects
-	NutRenderer *_rebelHudHiRes;       // DAT_00482260 - High-res HUD alternative
 
 	int32 processMouse() override;
 	bool isBitSet(int n) override;


Commit: 2cebdfc1fcebbb0d2bd802aaacbe77f4f15cb447
    https://github.com/scummvm/scummvm/commit/2cebdfc1fcebbb0d2bd802aaacbe77f4f15cb447
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:49+02:00

Commit Message:
SCUMM: RA2: Implement damage bar

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index f091ecc0f3c..804f524380f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -76,11 +76,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
 
 	_playerDamage = 0;
+	_playerShield = 255; // Full shields by default (255)
 	_playerLives = 3;
 	_playerScore = 0;
 	_viewX = 0;
 	_viewY = 0;
 
+	// Retail globals mapped: hit counter, cooldown, invulnerability flag
+	_rebelHitCounter = 0;
+	_rebelHitCooldown = 0;
+	_rebelInvulnerable = false;
+
 	_difficulty = 1; // Default to Medium (1). TODO: Read from game config
 
 	_speed = 12;
@@ -405,65 +411,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		}
 		
 	} else if (par1 == 3) {
-		// Opcode 3: Often used for clearing/resetting
-		// debug("Rebel2 IACT Opcode 3: par2=%d par3=%d par4=%d", par2, par3, par4);
-		
-		// Handle damage dealing (Subcode 5 in FUN_401234)
-		// Opcode 3 is complicated:
-		// Based on FUN_401234, case 1 (Opcode 3), checks local_c (offset +4 / par2?)
-		// In iactRebel2Scene1, par1=Opcode.
-		// If par1 == 1 (Opcode 1 / FUN_401234 case 1):
-		//   It checks local_c which is from offset +4 (par3).
-		//   If par3 == 5 -> Damage Logic.
-		// 
-		// Actually, FUN_401234 switches on `*local_14 - 2`.
-		// If `*local_14` (Opcode) is 3, switch value is 1.
-		// So Opcode 3 -> Case 1.
-		
-		if (par1 == 3) {
-			// Inside Case 1: local_c = local_14[2] which is offset +4 (par3)
-			if (par3 == 5) {
-				// Damage Logic
-				// bVar1 = FUN_423970(local_14[5]); // Check if source enemy (offset +10 / par6?) is active
-				// Read extra params from stream
-				// Note: `procIACT` already read parems into par1..par4.
-				// But Opcode 3 logic in FUN_401234 uses offset +10 (par6) which isn't passed to `procIACT` signature fully?
-				// Wait, `procIACT` reads 4 shorts.
-				// par1 (+0), par2 (+2), par3 (+4), par4 (+6).
-				// We need +8 and +10.
-				
-				b.skip(2); // Offset +8 (par5 used in assembly but not here yet)
-				int16 par6 = b.readSint16LE(); // Offset +10 (Enemy ID?)
-				
-				// Check if enemy is disabled (processed in FUN_423970)
-				bool enemyDisabled = isBitSet(par6);
-				
-				if (!enemyDisabled) {
-					// Probability check: Random(100) < Limit
-					// The limit seems to come from a table or fixed value?
-					// In FUN_401234: `sVar2 = *(short *)(&DAT_0047e0fc + ...)`
-					// For now, let's just use a fixed probability or assume successful hit
-					// if (rnd < prob) ...
-					
-					// Increment damage
-					// In assembly: DAT_0047a7ec += damage
-					// Damage amount also comes from table?
-					int damageAmount = 5; // Placeholder
-					
-					// Only apply damage occasionally to simulate probability
-					if ((_vm->_system->getMillis() % 100) < 20) { // 20% chance per frame (approx)
-						_playerDamage += damageAmount;
-						if (_playerDamage > 255) _playerDamage = 255;
-						
-						debug("Rebel2: Player HIT by Enemy %d. Damage=%d", par6, _playerDamage);
-						
-						// TODO: Flash screen red / shake
-					}
-				}
-			}
-		}
-		
-	} else if (par1 == 5) {
+		iactRebel2Opcode3(b, par2, par3, par4);
+	}
+	else if (par1 == 5) {
 		// Opcode 5: Special handling based on par2 value
 		// Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
 		debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
@@ -508,6 +458,73 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	}
 }
 
+void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Handle IACT opcode 3 subcases (damage, counters, special 100 branch)
+	// Mirrors retail FUN_0041CADB case 1 behavior where possible.
+
+	// Very small cooldown counter decremented on each IACT to emulate DAT_0045790a behavior
+	if (_rebelHitCooldown > 0) _rebelHitCooldown--;
+
+	// Subcase: par3 == 5 -> damage logic, expects extra param at +10 (source enemy ID)
+	if (par3 == 5) {
+		b.skip(2); // Offset +8
+		int16 srcId = b.readSint16LE(); // Offset +10 (Enemy ID)
+
+		// Only proceed if source is active (bit clear)
+		if (!isBitSet(srcId)) {
+			if (_rebelHitCooldown < 2) {
+				int limit = 20 + _difficulty * 20; // heuristic mapping for probability table
+				if (limit < 5) limit = 5;
+				if (limit > 90) limit = 90;
+				if (_vm->_rnd.getRandomNumber(100) < limit) {
+					// Apply damage unless invulnerable flag set (DAT_0047ab64)
+					if (!_rebelInvulnerable) {
+						int damageAmount = 5 + (_difficulty * 2);
+						// Apply to shields first (do not end game on depletion during tests)
+	
+	
+						// Update the retail-like damage accumulator (DAT_0047a7ec equivalent)
+						_playerDamage += damageAmount;
+						if (_playerDamage > 255) _playerDamage = 255;
+						debug("Rebel2: Damage HIT by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
+						// TODO: call UI update / flash screen / play sound to match retail (FUN_00420515 / FUN_0041189e)
+					}
+					// Impose short cooldown to prevent immediate repeated damage
+					_rebelHitCooldown = 6;
+				}
+			}
+		}
+	}
+	// Subcase: par3 == 1 -> increment hit counter when source active and par4 != 4
+	else if (par3 == 1) {
+		b.skip(2); // read extra param (source id)
+		int16 srcId = b.readSint16LE();
+		if (!isBitSet(srcId) && par4 != 4) {
+			_rebelHitCounter++;
+			debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d (source=%d)", _rebelHitCounter, srcId);
+		}
+	}
+	// Special-case branch when par2 == 100 (retail: triggers damage/sound via different offsets)
+	else if (par2 == 100) {
+		b.skip(2);
+		int16 srcId = b.readSint16LE();
+		if (!isBitSet(srcId)) {
+			int limit = 20 + _difficulty * 20;
+			if (_vm->_rnd.getRandomNumber(100) < limit) {
+				if (!_rebelInvulnerable) {
+					int damageAmount = 5 + (_difficulty * 2);
+					// Increment the retail-like damage accumulator (DAT_0047a7ec equivalent)
+					_playerDamage += damageAmount;
+					if (_playerDamage > 255) _playerDamage = 255;
+					debug("Rebel2: Damage HIT (special) by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
+				}
+				_rebelHitCooldown = 6;
+			}
+		}
+	}
+	// other subcases not implemented yet
+}
+
 void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Opcode 4: Enemy position update
 	// Read 5 shorts from the stream (offset +8 through +16)
@@ -1194,8 +1211,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// From assembly FUN_0041c012:
 	//   - Sprite 1: Status bar background frame (full width, drawn at 0,0)
 	//   - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
-	//   - Sprite 6: Shield bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
-	//   - Sprite 7: Shield alert (flashing red when shields critical)
+	//   - Sprite 6: Damage bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
+	//   - Sprite 7: Damage alert (flashing red when damage critical)
 	if (_smush_cockpitNut) {
 		// Draw status bar background frame (sprite 1) at (0, statusBarY)
 		// This sprite is the full-width status bar background
@@ -1213,18 +1230,16 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
 		}
 		
-		// Draw shield bar (sprite 6) 
-		// Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
-		// The width is scaled based on shield value (param_1 >> 2)
-		// For now, draw at position (0, statusBarY) - sprite has internal positioning
-		if (_smush_cockpitNut->getNumChars() > 6) {
-			// Calculate width based on damage. 
-			// Assuming max damage 255 = empty bar.
-			// Bar max width is 64 pixels.
-			// Damage 0 = Width 64. Damage 255 = Width 0.
-			// Width = 64 - (Damage / 4)
-			int barWidth = 64 - (_playerDamage / 4);
-			if (barWidth < 0) barWidth = 0;
+// Draw damage bar (sprite 6) 
+			// Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
+			// The width is scaled based on accumulated damage value (0..255)
+			// For now, draw at position (0, statusBarY) - sprite has internal positioning
+			if (_smush_cockpitNut->getNumChars() > 6) {
+				// Calculate width based on damage value.
+				// Damage range: 0..255 where 255 = full (Width 64)
+				int drawWidth = (64 * _playerDamage) / 255;
+			if (drawWidth < 0) drawWidth = 0;
+			if (drawWidth > 64) drawWidth = 64;
 			
 			// We need to draw a partial sprite or use a clip rect.
 			// smlayer_drawSomething supports scaling/clip?
@@ -1235,27 +1250,40 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			
 			// NOTE: smlayer_drawSomething calls `_smush_cockpitNut->draw(...)`
 			// We can't easily clip without modifying NutRenderer or using a custom draw loop.
-			// Let's implement a custom draw loop for the shield bar here since it's simple.
+			// Let's implement a custom draw loop for the damage bar here since it's simple.
 			
-			// Sprite 6 data
+			// Sprite 6 data (we need to copy a CLIP rect from within this sprite)
 			const byte *src = _smush_cockpitNut->getCharData(6);
 			int sw = _smush_cockpitNut->getCharWidth(6);
 			int sh = _smush_cockpitNut->getCharHeight(6);
-			int sx = 63; // Hardcoded X offset from assembly
-			int sy = 9;  // Hardcoded Y offset (relative to status bar top)
+			// Clip rect inside the sprite (from assembly): X=63, Y=9, W=64, H=6
+			const int clipX = 63;
+			const int clipY = 9;
+			const int clipW = 64;
+			const int clipH = 6;
 			
 			// Draw clipped width
 			if (src && sw > 0 && sh > 0) {
-				int drawWidth = MIN(barWidth, sw);
-				for (int y = 0; y < sh; y++) {
-					for (int x = 0; x < drawWidth; x++) {
-						// Render to (0 + sx + x + viewX, statusBarY + sy + y + viewY)
-						int destX = sx + x + _viewX;
-						int destY = statusBarY + sy + y + _viewY;
-						if (destX < pitch && destY < height) {
-							byte pixel = src[y * sw + x];
-							if (pixel != 0) {
-								renderBitmap[destY * pitch + destX] = pixel;
+				// Clamp drawWidth to the clip width and ensure we don't read past sprite bounds
+				int maxClipW = sw - clipX;
+				if (maxClipW < 0) maxClipW = 0;
+				int drawW = drawWidth;
+				if (drawW > clipW) drawW = clipW;
+				if (drawW > maxClipW) drawW = maxClipW;
+				if (drawW > 0) {
+					int drawH = clipH;
+					if (drawH > (sh - clipY)) drawH = sh - clipY;
+					if (drawH < 0) drawH = 0;
+					for (int y = 0; y < drawH; y++) {
+						for (int x = 0; x < drawW; x++) {
+							// Render to (clipX + x + viewX, statusBarY + clipY + y + viewY)
+							int destX = clipX + x + _viewX;
+							int destY = statusBarY + clipY + y + _viewY;
+							if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
+								byte pixel = src[(clipY + y) * sw + (clipX + x)];
+								if (pixel != 0) {
+									renderBitmap[destY * pitch + destX] = pixel;
+								}
 							}
 						}
 					}
@@ -1263,9 +1291,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			}
 		}
 		
-		// Draw shield alert overlay (sprite 7) when shields critical (> 0xAA = 170)
+		// Draw damage alert overlay (sprite 7) when damage is critical (> 0xAA = 170)
 		// Only draws when frame counter bit 3 is clear (every 8 frames)
-		// For now, skip - TODO: implement shield critical flashing
+		if (_smush_cockpitNut->getNumChars() > 7) {
+			if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
+				// Draw overlay sprite 7 at same general region (sx, sy)
+				renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
+			}
+		}
 		
 		// Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
 		// Uses sprite 1 again with different clip rect
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index b75564b6486..4f8cb388a01 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -55,6 +55,9 @@ public:
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 				  int16 par1, int16 par2, int16 par3, int16 par4);
 
+	// Handle IACT opcode 3 subcases (damage, hit-counters, special cases)
+	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) override;
 
@@ -141,13 +144,19 @@ public:
 	Explosion _explosions[5];
 	void spawnExplosion(int x, int y, int objectHalfWidth);
 
-	int16 _playerDamage;  // 0 to 255 (Accumulated damage)
+	int16 _playerDamage;  // Legacy damage counter (kept for compatibility/telemetry)
+	int16 _playerShield;  // Shields: 0..255 where 255 = full
 	int16 _playerLives;
 	int32 _playerScore;
 
 	int _viewX;
 	int _viewY;
 
+	// Rebel per-level counters / flags mapped from retail globals
+	int _rebelHitCounter;    // DAT_0047ab80 - hit counter / state tracker
+	int _rebelHitCooldown;   // DAT_0045790a - cooldown / timing for damage checks
+	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
+
 
 	struct Shot {
 		bool active;


Commit: 1ada210184ec23078dd8b8460ddfd1179b3760b8
    https://github.com/scummvm/scummvm/commit/1ada210184ec23078dd8b8460ddfd1179b3760b8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:49+02:00

Commit Message:
SCUMM: RA2: Refactor opcode 2 handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 804f524380f..ee0fa532732 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -87,6 +87,13 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelHitCooldown = 0;
 	_rebelInvulnerable = false;
 
+	// Initialize mirrored retail counters
+	for (int i = 0; i < 10; ++i) {
+		_rebelValueCounters[i] = 0;
+		_rebelMaskCounters[i] = 0;
+	}
+	_rebelLastCounter = 0;
+
 	_difficulty = 1; // Default to Medium (1). TODO: Read from game config
 
 	_speed = 12;
@@ -374,42 +381,8 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	if (par1 == 4) {
 		enemyUpdate(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 2) {
-		// Opcode 2: Often used for bit setting
-		// Disassembly shows this is handled but we don't have full context
-		// debug("Rebel2 IACT Opcode 2: par2=%d par3=%d par4=%d", par2, par3, par4);
-		
-		// Handle dependency linking (par3 == 4)
-		if (par3 == 4) {
-			int16 childId = b.readSint16LE(); // Offset +8
-			int16 parentId = b.readSint16LE(); // Offset +10
-			
-			if (parentId >= 0 && parentId < 512) {
-				// Shift links
-				_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
-				_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
-				_rebelLinks[parentId][0] = childId;
-				
-				// Apply initial state based on parent state
-				// If parent is ALIVE (Bit Clear) -> Disable new Child (Set Bit)
-				// If parent is DEAD (Bit Set) -> Enable new Child (Clear Bit)
-				if (!isBitSet(parentId)) {
-					setBit(childId);
-					debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
-				} else {
-					clearBit(childId);
-					debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
-				}
-			}
-		} else {
-			// Skip extra data if any? Opcode 2 might vary in size.
-			// Based on disassembly, it accesses offset +8 and +10 for case 4.
-			// Currently we don't know size for other cases, but hopefully they don't crash stream reading.
-			// 'b' is SeekableReadStream, so we might need to skip if we don't read?
-			// The caller `procIACT` passes `size` but that's for the WHOLE chunk.
-			// We can't skip unknown sub-opcodes easily without size map.
-			// Assuming only par3=4 has extra data for now.
-		}
-		
+		// Delegate handling to dedicated opcode 2 handler
+		iactRebel2Opcode2(b, par2, par3, par4);
 	} else if (par1 == 3) {
 		iactRebel2Opcode3(b, par2, par3, par4);
 	}
@@ -457,7 +430,82 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
 	}
 }
+void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Handle IACT opcode 2 subcases based on par3 (type). Mirrors FUN_00407fcb behavior where relevant.
+	// Keep existing linking behavior (par3 == 4) for compatibility.
+
+	// Link case: par3 == 4
+	if (par3 == 4) {
+		int16 childId = b.readSint16LE(); // Offset +8
+		int16 parentId = b.readSint16LE(); // Offset +10
+		
+		if (parentId >= 0 && parentId < 512) {
+			// Shift links
+			_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
+			_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
+			_rebelLinks[parentId][0] = childId;
+			
+			// Apply initial state based on parent state
+			if (!isBitSet(parentId)) {
+				setBit(childId);
+				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
+			} else {
+				clearBit(childId);
+				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
+			}
+		}
+		return;
+	} else if (par3 == 1) { // Probabilistic / counter cases: par3 == 1
+		int16 value = par4; // sVar6
+		int16 targetId = b.readSint16LE(); // Offset +8 (sVar7)
+		
+		if (targetId < 0 || targetId >= 0x200) 
+			return;
+		
+		if (value > 1 && value < 10) { // 1 < value < 10: random disable
+			if (_vm->_rnd.getRandomNumber(value) == 0) {
+				setBit(targetId);
+				debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
+			}
+		} else if (value > 10 && value < 20) { // 10 < value < 20: enable/disable with special value==11 = force enable
+			if (value == 11) {
+				clearBit(targetId);
+				debug("Rebel2 IACT Opcode2: FORCE ENABLE target=%d (value=11)", targetId);
+			} else {
+				if (_vm->_rnd.getRandomNumber(value - 10) == 0) {
+					clearBit(targetId);
+					debug("Rebel2 IACT Opcode2: Random ENABLE target=%d (value=%d)", targetId, value);
+				} else {
+					setBit(targetId);
+					debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
+				}
+			}
+		} else if (value > 99 && value < 110) { // 99 < value < 110: increment value counter if target active
+			if (!isBitSet(targetId)) {
+				int idx = value - 100;
+				if (idx >= 0 && idx < 10) {
+					_rebelValueCounters[idx]++;
+					_rebelLastCounter = _rebelValueCounters[idx];
+					debug("Rebel2 IACT Opcode2: Increment VAL counter[%d] -> %d (target=%d)", value, _rebelValueCounters[idx], targetId);
+				}
+			}
+
+		} else if (value > 0x3ff) { // Bitmask case: value > 0x3FF
+ 			for (int slot = 1; slot <= 9; ++slot) {
+				if ((value & (1 << (slot - 1))) != 0) {
+					if (!isBitSet(targetId)) {
+						_rebelMaskCounters[slot]++;
+						_rebelLastCounter = _rebelMaskCounters[slot];
+						debug("Rebel2 IACT Opcode2: Increment MASK counter[%d] -> %d (target=%d)", slot, _rebelMaskCounters[slot], targetId);
+					}
+				}
+			}
+		}
 
+		// Unknown sub-type: log and return
+		debug("Rebel2 IACT Opcode2: Unhandled par3=%d par4=%d", par3, par4);
+	}
+}
 void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Handle IACT opcode 3 subcases (damage, counters, special 100 branch)
 	// Mirrors retail FUN_0041CADB case 1 behavior where possible.
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 4f8cb388a01..28fe8553bcc 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -56,6 +56,7 @@ public:
 				  int16 par1, int16 par2, int16 par3, int16 par4);
 
 	// Handle IACT opcode 3 subcases (damage, hit-counters, special cases)
+	void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -157,6 +158,11 @@ public:
 	int _rebelHitCooldown;   // DAT_0045790a - cooldown / timing for damage checks
 	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
 
+	// Retail counters mirrored from DAT_00443618 (values 100..109) and DAT_004436e0 (mask counters 1..9)
+	short _rebelValueCounters[10]; // Index 0 -> value 100, ... Index 9 -> 109
+	short _rebelMaskCounters[10];  // Index 1..9 used; index 0 unused
+	int _rebelLastCounter;         // Mirrors DAT_0047ab90 (last updated counter)
+
 
 	struct Shot {
 		bool active;


Commit: c1bc5f218a0d566f24c27069d7d66ef5e7cdacf0
    https://github.com/scummvm/scummvm/commit/c1bc5f218a0d566f24c27069d7d66ef5e7cdacf0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:50+02:00

Commit Message:
SCUMM: RA2: Move SMUSH hooks out of SmushPlayer

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ee0fa532732..49b700adc89 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -417,6 +417,46 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 			debug("Rebel2: Handler set to TURRET (0x26/38) - status bar sprite 5, crosshair sprites 48+");
 		}
 
+		// Detect embedded ANIM (SAN) within the remaining IACT payload and load into HUD slots.
+		// Centralized here so SmushPlayer no longer needs game-specific checks.
+		{
+			int64 startPos = b.pos();
+			int64 totalSize = b.size();
+			if (totalSize >= 0 && totalSize > startPos) {
+				int64 remaining = totalSize - startPos;
+				int scanSize = (int)MIN<int64>(remaining, 65536);
+				byte *scanBuf = (byte *)malloc(scanSize);
+				if (scanBuf) {
+					int bytesRead = b.read(scanBuf, scanSize);
+					for (int i = 0; i + 8 <= bytesRead; ++i) {
+						if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+							int64 animStreamPos = startPos + i;
+							uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+							int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+							if (toCopy > 0) {
+								byte *animData = (byte *)malloc(toCopy);
+								if (animData) {
+									b.seek(animStreamPos);
+									b.read(animData, toCopy);
+									// par4 is the userId passed by SmushPlayer
+									loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
+									free(animData);
+								}
+							}
+							// Restore stream position and stop scanning after first ANIM
+							b.seek(startPos);
+							free(scanBuf);
+							goto after_anim_detection;
+						}
+					}
+					// No ANIM found: restore position
+					b.seek(startPos);
+					free(scanBuf);
+				}
+			}
+		}
+	after_anim_detection:;
+
 	} else if (par1 == 8) {
 		// TODO
 	} else if (par1 == 9) {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 3e1317c18bb..43fda940e68 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -366,51 +366,9 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
 	assert(subSize >= 8);
 
-	// For Rebel2 large IACT chunks, check for embedded SAN animations
-	if (_vm->_game.id == GID_REBEL2 && subSize > 1000) {
-		int64 startPos = b.pos();
-		
-		// Read header to check for embedded ANIM
-		byte header[64];
-		int bytesRead = b.read(header, MIN<int32>(64, subSize));
-		
-		// Check for ANIM at offset 18 (embedded SAN location)
-		uint32 magicAt18 = (bytesRead >= 22) ? READ_BE_UINT32(header + 18) : 0;
-		
-		if (magicAt18 == MKTAG('A','N','I','M')) {
-			// This IACT contains an embedded SAN animation!
-			uint32 animSize = (bytesRead >= 26) ? READ_BE_UINT32(header + 22) : 0;
-			
-			// Read the userId to determine which HUD slot (1-4)
-			READ_LE_UINT16(header);
-			READ_LE_UINT16(header + 2);
-			int userId = READ_LE_UINT16(header + 6);
-			
-			debug("Rebel2: Extracting embedded SAN from IACT: userId=%d, animSize=%u", userId, animSize);
-			
-			// Extract the embedded ANIM data (starts at offset 18 from IACT start)
-			b.seek(startPos + 18);
-			
-			int32 embeddedSize = subSize - 18;
-			if (embeddedSize > 0 && _insane) {
-				byte *animData = (byte *)malloc(embeddedSize);
-				if (animData) {
-					b.read(animData, embeddedSize);
-					
-					// Pass to Insane for decoding
-					_insane->loadEmbeddedSan(userId, animData, embeddedSize, _dst);
-					
-					free(animData);
-				}
-			}
-			
-			// Return - don't process as audio
-			return;
-		}
-		
-		// Seek back to start for normal processing
-		b.seek(startPos);
-	}
+	// Embedded SAN detection moved to InsaneRebel2::iactRebel2Scene1
+	// (previously handled here in SmushPlayer; centralized to the engine)
+
 
 	int code = b.readUint16LE();
 	int flags = b.readUint16LE();


Commit: d78e69c2753cb53af186c9338e8f96b44176e055
    https://github.com/scummvm/scummvm/commit/d78e69c2753cb53af186c9338e8f96b44176e055
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:50+02:00

Commit Message:
SCUMM: RA2: Refactor opcode 6 handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 49b700adc89..ee92909a873 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -87,6 +87,18 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelHitCooldown = 0;
 	_rebelInvulnerable = false;
 
+	// Opcode 6 state variables
+	_rebelAutopilot = 0;
+	_rebelDamageLevel = 0;
+	_rebelFlightDir = 0;
+	_rebelControlMode = 0;
+	_rebelViewOffsetX = 0;
+	_rebelViewOffsetY = 0;
+	_rebelViewOffset2X = 0;
+	_rebelViewOffset2Y = 0;
+	_rebelViewMode1 = 0;
+	_rebelViewMode2 = 0;
+
 	// Initialize mirrored retail counters
 	for (int i = 0; i < 10; ++i) {
 		_rebelValueCounters[i] = 0;
@@ -392,73 +404,11 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
 		
 	} else if (par1 == 6) {
-		// Opcode 6: Scene trigger / mode switch
-		// Disassembly shows it sets DAT_0047ee84 (handler type) to par2 for values 7, 8, 0x19, 0x26
-		// This determines which rendering handler and crosshair sprite to use
-		// par3 is stored as DAT_004436de (level type) which affects HUD sprite selection
-		
-		// Update handler type if par2 is a known handler value
-		if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
-			_rebelHandler = par2;
-			_rebelLevelType = par3;  // Store level type (affects HUD sprite: 5 vs 53)
-			debug("Rebel2 IACT Opcode 6: Setting handler=%d levelType=%d (par4=%d)", par2, par3, par4);
-		}
-		
-		// Check for Status Bar Enable trigger (par4 == 1 means enable status bar drawing)
-		// Based on FUN_407FCB line 79: if (param_5[4] == 1) { FUN_0040bb87(...) }
-		// Note: Sprite 5 is a 136x13 status bar, NOT the full cockpit (which is baked into video)
-		if (par4 == 1) {
-			debug("Rebel2 IACT Opcode 6: Status Bar ENABLED - will draw sprite %d (136x13) from CPITIMAG.NUT", 
-				(_rebelLevelType == 5) ? 53 : 5);
-		}
-		
-		// Debug: Confirm handler type after update
-		if (_rebelHandler == 0x26) {
-			debug("Rebel2: Handler set to TURRET (0x26/38) - status bar sprite 5, crosshair sprites 48+");
-		}
-
-		// Detect embedded ANIM (SAN) within the remaining IACT payload and load into HUD slots.
-		// Centralized here so SmushPlayer no longer needs game-specific checks.
-		{
-			int64 startPos = b.pos();
-			int64 totalSize = b.size();
-			if (totalSize >= 0 && totalSize > startPos) {
-				int64 remaining = totalSize - startPos;
-				int scanSize = (int)MIN<int64>(remaining, 65536);
-				byte *scanBuf = (byte *)malloc(scanSize);
-				if (scanBuf) {
-					int bytesRead = b.read(scanBuf, scanSize);
-					for (int i = 0; i + 8 <= bytesRead; ++i) {
-						if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
-							int64 animStreamPos = startPos + i;
-							uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-							int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
-							if (toCopy > 0) {
-								byte *animData = (byte *)malloc(toCopy);
-								if (animData) {
-									b.seek(animStreamPos);
-									b.read(animData, toCopy);
-									// par4 is the userId passed by SmushPlayer
-									loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
-									free(animData);
-								}
-							}
-							// Restore stream position and stop scanning after first ANIM
-							b.seek(startPos);
-							free(scanBuf);
-							goto after_anim_detection;
-						}
-					}
-					// No ANIM found: restore position
-					b.seek(startPos);
-					free(scanBuf);
-				}
-			}
-		}
-	after_anim_detection:;
-
+		// Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
+		iactRebel2Opcode6(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 8) {
-		// TODO
+		// Opcode 8: HUD resource loading (FUN_41CADB case 6)
+		iactRebel2Opcode8(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 9) {
 		// Opcode 9: Text/subtitle display
 		debug("Rebel2 IACT Opcode 9: par2=%d par3=%d par4=%d (text)", par2, par3, par4);
@@ -613,6 +563,275 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 	// other subcases not implemented yet
 }
 
+void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Opcode 6: Level setup / mode switch
+	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
+	//
+	// Assembly behavior:
+	// 1. If par4 == 1: Draw status bar sprite 5, clear link tables, reset state
+	// 2. Set level type (DAT_00457900 = par3)
+	// 3. Handle autopilot/control mode logic
+	// 4. Update damage level counter
+	// 5. Calculate view offsets based on level type
+
+	debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+	// Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
+	if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
+		_rebelHandler = par2;
+		debug("Rebel2 Opcode 6: Setting handler=%d", par2);
+	}
+
+	// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
+	if (par4 == 1) {
+		// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
+		_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+		debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
+
+		// Clear link tables (DAT_0045797c through DAT_0045917c)
+		// These are 4 tables of 0x400 (1024) shorts each
+		for (int i = 0; i < 512; i++) {
+			_rebelLinks[i][0] = 0;
+			_rebelLinks[i][1] = 0;
+			_rebelLinks[i][2] = 0;
+		}
+
+		// DAT_0047ab98 = DAT_0047ab9c (reset state flags)
+		// We don't have a direct equivalent, but we can reset relevant counters
+		_rebelHitCounter = 0;
+	}
+
+	// Step 2: Set level type (DAT_00457900 = par3)
+	_rebelLevelType = par3;
+
+	// Step 3: Autopilot/control mode logic (lines 123-146)
+	// This determines whether the ship flies on autopilot or manual control
+	if (!_rebelInvulnerable) {
+		// Normal mode: check control mode flags
+		if (_rebelAutopilot == 0) {
+			if ((_rebelControlMode & 2) != 0) {
+				_rebelAutopilot = 1;
+			}
+		} else {
+			if (_rebelControlMode != 0) {
+				_rebelAutopilot = 0;
+			}
+		}
+	} else {
+		// Invulnerable mode: random autopilot changes
+		if (_rebelAutopilot == 0) {
+			if (_vm->_rnd.getRandomNumber(100) == 0) {
+				_rebelAutopilot = 1;
+			}
+		} else {
+			if (_vm->_rnd.getRandomNumber(15) == 0) {
+				_rebelAutopilot = 0;
+				_rebelFlightDir = _vm->_rnd.getRandomNumber(2);
+			}
+		}
+	}
+
+	// Step 4: Update damage level counter (lines 147-154)
+	if (_rebelAutopilot == 0) {
+		if (_rebelDamageLevel > 0) {
+			_rebelDamageLevel--;
+		}
+	} else {
+		if (_rebelDamageLevel < 5) {
+			_rebelDamageLevel++;
+		}
+	}
+
+	// Handle level type 3 special direction logic (lines 155-181)
+	if (_rebelLevelType == 3) {
+		if (_rebelDamageLevel == 5) {
+			// Check for joystick/key input to change direction
+			// Simplified: use mouse position
+			if (_vm->_mouse.x > 75) {
+				_rebelFlightDir = 1;
+			}
+			if (_vm->_mouse.x < -75) {
+				_rebelFlightDir = 0;
+			}
+		}
+	} else {
+		_rebelFlightDir = 0;
+	}
+
+	// Step 5: Calculate view offsets based on level type (lines 182-213)
+	switch (_rebelLevelType) {
+	case 1:
+		// Type 1: Vertical movement
+		_rebelViewMode1 = 0x0e;
+		_rebelViewMode2 = 0;
+		_rebelViewOffsetX = _rebelDamageLevel * -5 - 0x0e;
+		_rebelViewOffset2X = _rebelDamageLevel * -0x16;
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2Y = 0;
+		break;
+
+	case 4:
+		// Type 4: Different vertical movement
+		_rebelViewMode1 = 0x22;
+		_rebelViewMode2 = 0;
+		_rebelViewOffsetX = _rebelDamageLevel * 10 - 0x10;
+		_rebelViewOffset2X = _rebelDamageLevel * 0x11 - 0x55;
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2Y = 0;
+		break;
+
+	case 2:
+		// Type 2: Horizontal movement
+		_rebelViewMode1 = 0;
+		_rebelViewMode2 = 0x0e;
+		_rebelViewOffsetY = _rebelDamageLevel * -5 - 0x0e;
+		_rebelViewOffset2Y = (5 - _rebelDamageLevel) * 0x0f - 0x3c;
+		_rebelViewOffsetX = 0;
+		_rebelViewOffset2X = 0;
+		break;
+
+	case 3:
+		// Type 3: Direction-based movement
+		_rebelViewMode1 = 0x0f;
+		_rebelViewMode2 = 0;
+		{
+			int dirFactor = (_rebelFlightDir == 0) ? 3 : -3;  // (-(ushort)(DAT_00457902 == 0) & 6) - 3
+			int dirFactor2 = (_rebelFlightDir == 0) ? 0x14 : -0x14;  // (-(ushort)(DAT_00457902 == 0) & 0x28) - 0x14
+			_rebelViewOffsetX = dirFactor * (5 - _rebelDamageLevel) - 0x0f;
+			_rebelViewOffset2X = dirFactor2 * (5 - _rebelDamageLevel);
+		}
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2Y = 0;
+		break;
+
+	default:
+		// Default: No special offsets
+		_rebelViewMode1 = 0;
+		_rebelViewMode2 = 0;
+		_rebelViewOffsetX = 0;
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2X = 0;
+		_rebelViewOffset2Y = 0;
+		break;
+	}
+
+	debug("Rebel2 Opcode 6: levelType=%d autopilot=%d damageLevel=%d viewOffset=(%d,%d)",
+		_rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
+
+	// Detect and load embedded ANIM (SAN) within the remaining IACT payload
+	{
+		int64 startPos = b.pos();
+		int64 totalSize = b.size();
+		if (totalSize >= 0 && totalSize > startPos) {
+			int64 remaining = totalSize - startPos;
+			int scanSize = (int)MIN<int64>(remaining, 65536);
+			byte *scanBuf = (byte *)malloc(scanSize);
+			if (scanBuf) {
+				int bytesRead = b.read(scanBuf, scanSize);
+				for (int i = 0; i + 8 <= bytesRead; ++i) {
+					if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+						int64 animStreamPos = startPos + i;
+						uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+						if (toCopy > 0) {
+							byte *animData = (byte *)malloc(toCopy);
+							if (animData) {
+								b.seek(animStreamPos);
+								b.read(animData, toCopy);
+								loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
+								free(animData);
+							}
+						}
+						b.seek(startPos);
+						free(scanBuf);
+						return;
+					}
+				}
+				b.seek(startPos);
+				free(scanBuf);
+			}
+		}
+	}
+}
+
+void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Opcode 8: HUD resource loading
+	// Based on FUN_41CADB case 6 (switch on *local_14 - 2 == 6, meaning opcode 8)
+	//
+	// par3 determines which HUD slot to load:
+	// case 1: DAT_00482240 - Primary HUD overlay (GRD001)
+	// case 2: DAT_00482238 - Secondary HUD graphics (GRD002)
+	// case 4: DAT_00482268 - Ship cockpit frame (GRD010)
+	// case 5: DAT_0048226c - Mask buffer (GRD011)
+	// case 6: DAT_00482250 - Explosion overlay (GRD003)
+	// case 7: DAT_00482248 - Damage indicator (GRD004)
+	// case 10: DAT_00482258 - Additional effects (GRD005)
+	// case 12/13: DAT_00482260 - High-res HUD alternative (GRD007)
+	// cases 21-27: Sound loading slot 0
+	// cases 31-37: Sound loading slot 1
+	// case 40: Sound loading slot 3
+	// cases 41-47: Sound loading slot 2
+
+	debug("Rebel2 IACT Opcode 8: par2=%d par3=%d par4=%d (HUD loading)", par2, par3, par4);
+
+	// Read the data size from the stream (at offset +14, which is local_14[7])
+	// The actual NUT/resource data starts at offset +18 (param_5 + 9 in assembly)
+	int64 startPos = b.pos();
+
+	// Skip to where embedded data would be and scan for ANIM tag
+	// The assembly shows data at param_5 + 9 (18 bytes from IACT header start)
+	// Since we're already past the header params, scan remaining data
+
+	int64 totalSize = b.size();
+	if (totalSize > startPos) {
+		int64 remaining = totalSize - startPos;
+		int scanSize = (int)MIN<int64>(remaining, 65536);
+		byte *scanBuf = (byte *)malloc(scanSize);
+		if (scanBuf) {
+			int bytesRead = b.read(scanBuf, scanSize);
+
+			// Look for ANIM tag (embedded SAN)
+			for (int i = 0; i + 8 <= bytesRead; ++i) {
+				if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+					int64 animStreamPos = startPos + i;
+					uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+					int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+					if (toCopy > 0) {
+						byte *animData = (byte *)malloc(toCopy);
+						if (animData) {
+							b.seek(animStreamPos);
+							b.read(animData, toCopy);
+							// Map par3 to userId for HUD slots
+							int userId = 0;
+							switch (par3) {
+							case 1: userId = 1; break;  // Primary HUD
+							case 2: userId = 2; break;  // Secondary HUD
+							case 4: userId = 3; break;  // Cockpit frame
+							case 6: userId = 4; break;  // Explosion overlay
+							default: userId = par3; break;
+							}
+							loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+							free(animData);
+						}
+					}
+					b.seek(startPos);
+					free(scanBuf);
+					return;
+				}
+			}
+
+			b.seek(startPos);
+			free(scanBuf);
+		}
+	}
+
+	// Handle sound loading cases (par3 21-47)
+	if (par3 >= 21 && par3 <= 47) {
+		debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
+		// TODO: Implement sound loading when needed
+	}
+}
+
 void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Opcode 4: Enemy position update
 	// Read 5 shorts from the stream (offset +8 through +16)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 28fe8553bcc..85c0cb24434 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -55,9 +55,11 @@ public:
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 				  int16 par1, int16 par2, int16 par3, int16 par4);
 
-	// Handle IACT opcode 3 subcases (damage, hit-counters, special cases)
+	// Handle IACT opcode subcases
 	void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+	void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) override;
@@ -158,6 +160,20 @@ public:
 	int _rebelHitCooldown;   // DAT_0045790a - cooldown / timing for damage checks
 	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
 
+	// Opcode 6 state variables (from FUN_41CADB case 4)
+	int _rebelAutopilot;     // DAT_00457904 - autopilot flag (0 or 1)
+	int _rebelDamageLevel;   // DAT_0045790a - damage level (0-5)
+	int _rebelFlightDir;     // DAT_00457902 - flight direction (0 or 1)
+	int _rebelControlMode;   // DAT_0047a7e4 - control mode flags
+
+	// View offset variables (calculated from level type)
+	int _rebelViewOffsetX;   // DAT_0045790c
+	int _rebelViewOffsetY;   // DAT_0045790e
+	int _rebelViewOffset2X;  // DAT_00457910
+	int _rebelViewOffset2Y;  // DAT_00457912
+	int _rebelViewMode1;     // DAT_00482270
+	int _rebelViewMode2;     // DAT_00482274
+
 	// Retail counters mirrored from DAT_00443618 (values 100..109) and DAT_004436e0 (mask counters 1..9)
 	short _rebelValueCounters[10]; // Index 0 -> value 100, ... Index 9 -> 109
 	short _rebelMaskCounters[10];  // Index 1..9 used; index 0 unused


Commit: 404037ab75a10ab1b59dcae9c5314b20857cacb4
    https://github.com/scummvm/scummvm/commit/404037ab75a10ab1b59dcae9c5314b20857cacb4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:50+02:00

Commit Message:
SCUMM: RA2: Fix post-rendering state updates

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ee92909a873..c5ff122c948 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1617,28 +1617,30 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	Common::List<enemy>::iterator it;
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
+		// Skip destroyed enemies (explosion handled by 5-slot system)
+		if (it->destroyed) continue;
+
+		// Skip inactive enemies or those disabled by IACT bit
+		if (!it->active || isBitSet(it->id)) continue;
+
 		Common::Rect r = it->rect;
-		
-		// Clip the rect to screen bounds for safety
-		Common::Rect clipped = r;
-		clipped.clip(Common::Rect(0, 0, width, height));
 
-		if (!clipped.isValidRect()) continue;
-		
-		if (it->destroyed) {
-			// Handle destroyed enemies - draw explosion animation
-			// The enemy frame updates are now skipped in decodeFrameObject via shouldSkipFrameUpdate
-			// Note: Explosion rendering is now handled by the separate 5-slot system
-		} else if (it->active && !isBitSet(it->id)) { // Only draw if active AND not disabled by IACT
-			// Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
-			// Hard (2) mode does not show indicators.
-			if (_difficulty < 2) {
-				const byte color = 5; // Green color index for brackets
-				// Clip the rect to screen bounds for drawing logic is handled inside drawLine if implemented safely,
-				// but drawCornerBrackets relies on drawLine which iterates.
-				// We pass full screen width/height to safe-guard.
-				drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
-			}
+		// Check if enemy rect is visible within the current view area
+		// (matching FUN_00428b90 clip logic: reject lines entirely outside clip rect)
+		// The visible area in video coordinates is the current scroll viewport
+		Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + videoHeight);
+
+		// If enemy rect doesn't intersect the view area at all, skip drawing
+		if (r.right <= viewRect.left || r.left >= viewRect.right ||
+		    r.bottom <= viewRect.top || r.top >= viewRect.bottom) {
+			continue;
+		}
+
+		// Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
+		// Hard (2) mode does not show indicators (matching FUN_00425d30(4) check)
+		if (_difficulty < 2) {
+			const byte color = 5; // Green color index for brackets
+			drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
 		}
 	}
 
@@ -1776,6 +1778,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
 		}
 	}
+
+	// ============================================================
+	// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
+	// ============================================================
+	// The original game rebuilds the object list from scratch each frame:
+	// - FUN_4033CF (IACT processing) adds objects to DAT_0043fb00 list
+	// - FUN_4092D9 iterates only over objects in the current frame's list
+	// - FUN_403240 (called at frame end) resets DAT_0047ee74 = 0 (clears list counter)
+	//
+	// We achieve the same by marking all non-destroyed enemies inactive at frame end.
+	// The next frame's IACT opcode 4 (enemyUpdate) will re-activate them if present.
+	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+		if (!it->destroyed) {
+			it->active = false;
+		}
+	}
 }
 
 }


Commit: ea84ceebdf96faf93e3c3c22355d199ad63ab5a9
    https://github.com/scummvm/scummvm/commit/ea84ceebdf96faf93e3c3c22355d199ad63ab5a9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:51+02:00

Commit Message:
SCUMM: RA2: Skip non-gameplay sounds

Changed paths:
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 43fda940e68..ccea514286f 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -914,6 +914,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleZlibFrameObject(subSize, b);
 			break;
 		case MKTAG('P','S','A','D'):
+			// Rebel Assault 2 only: Skip sound when _skipNext is set (enemy killed)
+			// This mirrors the original game's FUN_00423A50 behavior where
+			// SKIP tags cause subsequent PSAD/SAUD chunks to be skipped
+			if (_vm->_game.id == GID_REBEL2 && _skipNext) {
+				_skipNext = false;
+				break;
+			}
 			if (!_compressedFileMode) {
 				audioChunk = (uint8 *)malloc(subSize + 8);
 				b.seek(-8, SEEK_CUR);


Commit: 353416241d031ee59cb984bd6e9fd124751fca2c
    https://github.com/scummvm/scummvm/commit/353416241d031ee59cb984bd6e9fd124751fca2c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:51+02:00

Commit Message:
SCUMM: RA2: Implement score and mouse pointers

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c5ff122c948..bdcd272180e 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -68,6 +68,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_icons2Nut = nullptr;  // Not used for Rebel2
 	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
 
+	// Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
+	// This is used by FUN_0041c012 to render the score in the status bar
+	_smush_dispfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
+
 	// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
 	_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
 
@@ -107,6 +111,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelLastCounter = 0;
 
 	_difficulty = 1; // Default to Medium (1). TODO: Read from game config
+	_targetLockTimer = 0;  // DAT_00443676 equivalent
 
 	_speed = 12;
 	_insaneIsRunning = false;
@@ -215,8 +220,137 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 InsaneRebel2::~InsaneRebel2() {
 	delete _rebelMsgFont;
+	delete _smush_dispfontNut;
+}
+
+// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
+// These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
+// For simplicity, we use fixed values based on difficulty level
+// Values estimated from disassembly patterns (actual values would need extraction from game data)
+const int16 InsaneRebel2::kScoreTableEnemyDestroy[16] = {
+	100, 100, 100, 100,   // Easy (difficulty 0)
+	150, 150, 150, 150,   // Medium (difficulty 1)
+	200, 200, 200, 200,   // Hard (difficulty 2)
+	250, 250, 250, 250    // Expert (difficulty 3)
+};
+
+const int16 InsaneRebel2::kScoreTableSpecial[16] = {
+	50, 50, 50, 50,
+	75, 75, 75, 75,
+	100, 100, 100, 100,
+	125, 125, 125, 125
+};
+
+const int16 InsaneRebel2::kScoreTableTimeBonus[16] = {
+	1, 1, 1, 1,
+	2, 2, 2, 2,
+	3, 3, 3, 3,
+	4, 4, 4, 4
+};
+
+// Score system implementation (FUN_0041bf8d equivalent)
+// Adds points to score and awards bonus life when crossing threshold
+void InsaneRebel2::addScore(int points) {
+	// Calculate bonus life threshold based on difficulty (DAT_0047a7fa)
+	// From FUN_0041bf8d:
+	//   if (difficulty < 4) threshold = (difficulty * 5 + 5) * 1000
+	//   else threshold = 15000
+	int threshold;
+	if (_difficulty < 4) {
+		threshold = (_difficulty * 5 + 5) * 1000;  // 5000, 10000, 15000, 20000
+	} else {
+		threshold = 15000;
+	}
+
+	// Check if we're crossing a threshold (award bonus life)
+	// Formula: score / threshold < (score + points) / threshold
+	if (threshold > 0) {
+		int oldMilestone = _playerScore / threshold;
+		int newMilestone = (_playerScore + points) / threshold;
+		if (oldMilestone < newMilestone) {
+			// Award bonus life
+			_playerLives++;
+			// TODO: Play bonus life sound (FUN_0041189e(5, 0, 0x7f, 0, 0))
+			debug("Rebel2: BONUS LIFE! Score crossed %d threshold. Lives=%d", threshold, _playerLives);
+		}
+	}
+
+	// Add points to score
+	_playerScore += points;
+	debug("Rebel2: Score +%d = %d", points, _playerScore);
 }
 
+// Render score to HUD (part of FUN_0041c012)
+// Score is drawn using FUN_00434cb0 with format string "%07ld"
+// In retail, score is rendered to a status bar buffer, then blitted to screen at Y=180 (0xb4)
+// The text within the status bar is at local Y=4, so screen Y = 180 + 4 = 184
+void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY) {
+	// In retail, score is rendered by FUN_0041c012 which calls FUN_00434cb0
+	// The status bar is blitted to screen at Y = DAT_0047ab2c + 0xb4 (typically 0 + 180 = 180)
+	// Text position within status bar from FUN_0041c012 line 136-137:
+	//   X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 (257) for low-res
+	//   Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 for low-res
+	// So final screen position: X=257, Y=180+4=184
+	// Format: 7-digit zero-padded decimal ("%07ld")
+
+	(void)statusBarY; // Not used - we use fixed Y positions
+
+	// Use SMALFONT.NUT (NutRenderer) for rendering digits
+	// If not available, skip rendering
+	if (!_smush_dispfontNut) {
+		debug(1, "renderScoreHUD: _smush_dispfontNut is NULL!");
+		return;
+	}
+
+	// The SMUSH buffer is 424x260, but the visible screen is 320x200
+	// The view offset (_viewX, _viewY) determines where in the buffer the screen is showing
+	// To render at fixed screen positions, we add the view offset
+
+	// Convert score to 7-digit string
+	char scoreStr[16];
+	Common::sprintf_s(scoreStr, "%07d", _playerScore);
+
+	// Status bar is at Y=180 (0xb4), text within it at Y=4, so total Y=184
+	// Score X position is 257 (0x101)
+	const int STATUS_BAR_Y = 180;  // 0xb4 from FUN_41C012 line 149/152
+	const int SCORE_TEXT_Y = 4;    // Text position within status bar
+
+	int scoreX = 257 + _viewX;
+	int scoreY = STATUS_BAR_Y + SCORE_TEXT_Y + _viewY;
+
+	debug(5, "renderScoreHUD: Drawing score=%d at buffer(%d,%d) viewOffset(%d,%d)",
+		  _playerScore, scoreX, scoreY, _viewX, _viewY);
+
+	// Draw each character manually using NutRenderer::drawCharV7
+	Common::Rect clipRect(0, 0, width, height);
+	int x = scoreX;
+	for (int i = 0; scoreStr[i] != '\0'; i++) {
+		byte ch = (byte)scoreStr[i];
+		int charWidth = _smush_dispfontNut->getCharWidth(ch);
+		if (charWidth > 0) {
+			// Use drawCharV7 with color 255 (white) for visibility
+			_smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, scoreY, pitch, 255, kStyleAlignLeft, ch, true, true);
+			x += charWidth;
+		}
+	}
+
+	// Also draw lives counter - in status bar at X=168 (0xa8), Y=7 within bar
+	const int LIVES_TEXT_Y = 7;
+	char livesStr[8];
+	Common::sprintf_s(livesStr, "%d", _playerLives);
+	int livesX = 168 + _viewX;
+	int livesY = STATUS_BAR_Y + LIVES_TEXT_Y + _viewY;
+
+	x = livesX;
+	for (int i = 0; livesStr[i] != '\0'; i++) {
+		byte ch = (byte)livesStr[i];
+		int charWidth = _smush_dispfontNut->getCharWidth(ch);
+		if (charWidth > 0) {
+			_smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, livesY, pitch, 255, kStyleAlignLeft, ch, true, true);
+			x += charWidth;
+		}
+	}
+}
 
 int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
@@ -285,7 +419,14 @@ int32 InsaneRebel2::processMouse() {
 				// Note: Background saving and masking is handled in procPostRendering
 				// where we have access to the render bitmap
 				// TODO: Play explosion sound
-				// TODO: Update score
+
+				// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
+				// Score value comes from lookup table DAT_0047e0fe indexed by difficulty
+				int scoreIndex = _difficulty * 4;  // Simplified index
+				if (scoreIndex >= 0 && scoreIndex < 16) {
+					addScore(kScoreTableEnemyDestroy[scoreIndex]);
+				}
+
 				// Only hit one enemy per click
 				break;
 			}
@@ -1749,28 +1890,83 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
+	// ============================================================
+	// TARGET LOCK DETECTION (DAT_00443676 equivalent)
+	// ============================================================
+	// In retail: When crosshair is over an active enemy, set target lock timer to 7
+	// This triggers the animated crosshair cycling for Handler 0x26 (turret)
+	// Check from FUN_40A2E0 lines 127-143
+	{
+		Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
+		bool targetLocked = false;
+
+		for (Common::List<enemy>::iterator enemyIt = _enemies.begin(); enemyIt != _enemies.end(); ++enemyIt) {
+			if (enemyIt->active && !enemyIt->destroyed && enemyIt->rect.contains(worldMousePos)) {
+				targetLocked = true;
+				break;
+			}
+		}
+
+		// Update target lock timer
+		if (targetLocked) {
+			_targetLockTimer = 7;  // Set to 7 when over target (retail behavior)
+		} else if (_targetLockTimer > 0) {
+			_targetLockTimer--;  // Count down when not over target
+		}
+	}
+
 	// Draw Crosshair/Reticle cursor
-	// Sprite indices based on handler type (from original game disassembly):
+	// Sprite indices based on handler type (from original game disassembly FUN_004089ab, FUN_0040d836, etc):
 	// - Handler 8 (ground vehicle): Index 0x2E (46)
 	// - Handler 7 (space flight): Index 0x2F (47)
 	// - Handler 0x19 (mixed/turret view): Index 0x2F (47)
-	// - Handler 0x26 (full turret): Index 0x30+ (48+) with animation
+	// - Handler 0x26 (full turret): Index varies by levelType and animation
+	//
+	// For Handler 0x26 (turret), the crosshair sprite formula from FUN_004089ab line 192-193:
+	//   (-(ushort)(DAT_004436de == 5) & 0x30) + local_28
+	// where local_28 is an animation offset (0-3) based on target lock timer (DAT_00443676)
+	//
+	// When DAT_00443676 == 0 (no target locked): local_28 = 0
+	// When DAT_00443676 != 0: local_28 = 3 - (DAT_0047fe98 & 3) -- animated cycling
 	if (_smush_iconsNut) {
-		//CursorMan.showMouse(false);
 		int reticleIndex;
 		switch (_rebelHandler) {
-		case 7:   // Space flight
-		case 0x19: // Mixed/turret view
-			reticleIndex = 47;  // 0x2F
+		case 7:   // Space flight - uses crosshair sprite 0x2F (47)
+			reticleIndex = 47;
 			break;
-		case 0x26: // Full turret (with animation - simplified for now)
-			reticleIndex = 48;  // 0x30
+		case 0x19: // Mixed/turret view - uses crosshair sprite 0x2F (47)
+			reticleIndex = 47;
 			break;
-		case 8:   // Ground vehicle (Level 1) - FALLTHROUGH
+		case 0x26: { // Full turret mode - animated crosshair
+			// Calculate animation offset based on target lock state
+			// In retail: DAT_00443676 is the "target lock timer" (set to 7 when targeting)
+			// DAT_0047fe98 is a per-frame counter incremented each frame
+			static int turretAnimCounter = 0;
+			turretAnimCounter++;
+
+			int animOffset;
+			// Use actual target lock timer (DAT_00443676 equivalent)
+			if (_targetLockTimer == 0) {
+				animOffset = 0;  // No target - use static crosshair
+			} else {
+				animOffset = 3 - (turretAnimCounter & 3);  // Cycles 3, 2, 1, 0
+			}
+
+			// If levelType == 5, use sprites 0x30-0x33 (48-51)
+			// Otherwise use sprites 0-3 (basic targeting reticle)
+			if (_rebelLevelType == 5) {
+				reticleIndex = 0x30 + animOffset;  // 48-51
+			} else {
+				reticleIndex = animOffset;  // 0-3
+			}
+			break;
+		}
+		case 8:   // Ground vehicle - uses crosshair sprite 0x2E (46)
 		default:
-			reticleIndex = 46;  // 0x2E
+			reticleIndex = 46;
 			break;
 		}
+
 		if (_smush_iconsNut->getNumChars() > reticleIndex) {
 			int cw = _smush_iconsNut->getCharWidth(reticleIndex);
 			int ch = _smush_iconsNut->getCharHeight(reticleIndex);
@@ -1779,6 +1975,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
+	// ============================================================
+	// HUD SCORE/LIVES RENDERING (FUN_0041c012 equivalent)
+	// ============================================================
+	// Render score and lives counter on the status bar
+	// This is called after all other rendering so it appears on top
+	renderScoreHUD(renderBitmap, pitch, width, height, 0);
+
 	// ============================================================
 	// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
 	// ============================================================
@@ -1789,9 +1992,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	//
 	// We achieve the same by marking all non-destroyed enemies inactive at frame end.
 	// The next frame's IACT opcode 4 (enemyUpdate) will re-activate them if present.
-	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (!it->destroyed) {
-			it->active = false;
+	for (Common::List<enemy>::iterator resetIt = _enemies.begin(); resetIt != _enemies.end(); ++resetIt) {
+		if (!resetIt->destroyed) {
+			resetIt->active = false;
 		}
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 85c0cb24434..35e33260de1 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -40,7 +40,10 @@ public:
 	~InsaneRebel2();
 
 	NutRenderer *_smush_cockpitNut;
-	NutRenderer *_smush_dispfontNut;  // DAT_00482200 - DISPFONT.NUT for status bar (difficulty, shields, lives, score)
+
+	// Font used for HUD score/lives/damage display (SMALFONT.NUT)
+	// DAT_00482200 equivalent - used by FUN_0041c012 for status bar rendering
+	NutRenderer *_smush_dispfontNut;
 
 	// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
 	SmushFont *_rebelMsgFont;
@@ -192,6 +195,24 @@ public:
 	int _difficulty;
 	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
 
+	// Score system (FUN_0041bf8d equivalent)
+	// Adds points to score and awards bonus life when crossing threshold
+	void addScore(int points);
+
+	// Score lookup tables (indices into per-level point values)
+	// DAT_0047e0fe: Points for destroying enemies
+	// DAT_0047e100: Points for certain special events
+	// DAT_0047e102: Points awarded per frame (time bonus)
+	static const int16 kScoreTableEnemyDestroy[16];  // Per difficulty/level
+	static const int16 kScoreTableSpecial[16];
+	static const int16 kScoreTableTimeBonus[16];
+
+	// Render score text to HUD (called from procPostRendering)
+	void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
+
+	// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
+	int _targetLockTimer;
+
 };
 
 } // End of namespace Insane


Commit: deb62f97e1e5fc5def6980ef1b7095b7a4e6e3dd
    https://github.com/scummvm/scummvm/commit/deb62f97e1e5fc5def6980ef1b7095b7a4e6e3dd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:51+02:00

Commit Message:
SCUMM: RA2: Implement subtitles

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bdcd272180e..38190aafb90 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -78,6 +78,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_enemies.clear();
 	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
+	_introCursorPushed = false;  // Cursor state tracking for intro sequences
 
 	_playerDamage = 0;
 	_playerShield = 255; // Full shields by default (255)
@@ -552,8 +553,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		iactRebel2Opcode8(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 9) {
 		// Opcode 9: Text/subtitle display
-		debug("Rebel2 IACT Opcode 9: par2=%d par3=%d par4=%d (text)", par2, par3, par4);
-		
+		iactRebel2Opcode9(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 0 || par1 == 1) {
 		// Low Opcodes seen in logs
 		debug("Rebel2 IACT: Low Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
@@ -973,6 +973,162 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	}
 }
 
+void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Opcode 9: Text/Subtitle Display via IACT chunk
+	// Note: Most RA2 subtitles use TRES chunks handled by SmushPlayer::handleTextResource()
+	// This opcode handles inline text in IACT chunks (less common)
+	//
+	// IACT Chunk Layout (par1-par4 already read by handleIACT):
+	// +0x00 (2): opcode = 9 (par1, already read)
+	// +0x02 (2): par2 (already read)
+	// +0x04 (2): par3 (already read)
+	// +0x06 (2): par4 (already read)
+	// +0x08 onwards: Text data structure
+	//
+	// Text Data Structure:
+	// +0x00 (2): X position
+	// +0x02 (2): Y position
+	// +0x04 (2): flags (bit 0=center, bit 1=right, bit 2=wrap, bit 3=difficulty gated)
+	// +0x06 (2): clipX (when flag & 4)
+	// +0x08 (2): clipY
+	// +0x0A (2): clipW
+	// +0x0C (2): clipH
+	// +0x10 onwards: NUL-terminated text string
+
+	int64 startPos = b.pos();
+
+	// Check for "TRES" tag (0x54524553) indicating string resource lookup
+	uint32 tag = b.readUint32BE();
+
+	const char *textStr = nullptr;
+	char textBuffer[512];
+	int16 posX = 160;  // Default center position
+	int16 posY = 150;  // Default bottom-ish position
+	int16 textFlags = 1;  // Default: center aligned
+	int16 clipX = 16, clipY = 16, clipW = 288, clipH = 168;
+
+	if (tag == MKTAG('T','R','E','S')) {
+		// String resource lookup via TRES tag
+		// The string index follows after the tag
+		int32 stringIndex = b.readSint32LE();
+
+		// Try to get string from SMUSH player's string resource
+		if (_player && _player->getString(stringIndex)) {
+			textStr = _player->getString(stringIndex);
+			debug("Rebel2 Opcode 9: TRES string index=%d -> \"%s\"", stringIndex, textStr);
+		} else {
+			debug("Rebel2 Opcode 9: TRES string index=%d not found", stringIndex);
+			return;
+		}
+
+		// After TRES + index, read positioning data
+		// The remaining data contains X, Y, flags etc.
+		if (b.size() - b.pos() >= 14) {
+			posX = b.readSint16LE();
+			posY = b.readSint16LE();
+			textFlags = b.readSint16LE();
+			clipX = b.readSint16LE();
+			clipY = b.readSint16LE();
+			clipW = b.readSint16LE();
+			clipH = b.readSint16LE();
+		}
+	} else {
+		// Inline text data - go back and read positioning structure
+		b.seek(startPos);
+
+		// Read text data structure
+		posX = b.readSint16LE();      // +0x00
+		posY = b.readSint16LE();      // +0x02
+		textFlags = b.readSint16LE(); // +0x04
+		clipX = b.readSint16LE();     // +0x06
+		clipY = b.readSint16LE();     // +0x08
+		clipW = b.readSint16LE();     // +0x0A
+		clipH = b.readSint16LE();     // +0x0C
+		b.skip(2);                    // +0x0E padding
+
+		// Read inline text string (NUL-terminated)
+		int textLen = 0;
+		while (textLen < (int)sizeof(textBuffer) - 1) {
+			byte ch = b.readByte();
+			if (ch == 0 || b.eos()) break;
+			textBuffer[textLen++] = ch;
+		}
+		textBuffer[textLen] = '\0';
+		textStr = textBuffer;
+
+		debug("Rebel2 Opcode 9: Inline text at (%d,%d) flags=0x%x -> \"%s\"", posX, posY, textFlags, textStr);
+	}
+
+	if (!textStr || textStr[0] == '\0') {
+		debug("Rebel2 Opcode 9: Empty text string, skipping");
+		return;
+	}
+
+	// Check difficulty gate (flag bit 3 = 0x08)
+	// If set, only show text if difficulty check passes (we skip this check for simplicity)
+	// In retail: FUN_00425d30(0) is called
+
+	// Get render buffer dimensions
+	int width = (_player && _player->_width > 0) ? _player->_width : 320;
+	int height = (_player && _player->_height > 0) ? _player->_height : 200;
+
+	// Apply coordinate clamping (from FUN_004033cf disassembly)
+	// Low-res: X clamped to [16, 304], Y clamped to [16, 196]
+	if (posX < 16) posX = 16;
+	if (posX > 304) posX = 304;
+	if (posY < 16) posY = 16;
+	if (posY > 196) posY = 196;
+
+	// Use the message font loaded during initialization (DIHIFONT.NUT)
+	if (!_rebelMsgFont) {
+		debug("Rebel2 Opcode 9: No message font loaded (_rebelMsgFont is null)");
+		return;
+	}
+
+	// Calculate clipping rectangle
+	if (!(textFlags & 0x04)) {
+		// No clip rect specified, use default full-screen clip
+		clipX = 0;
+		clipY = 0;
+		clipW = width;
+		clipH = height;
+	}
+
+	Common::Rect clipRect(
+		MAX<int>(0, clipX),
+		MAX<int>(0, clipY),
+		MIN<int>(clipX + clipW, width),
+		MIN<int>(clipY + clipH, height)
+	);
+
+	// Determine text alignment flags
+	TextStyleFlags styleFlags = kStyleAlignLeft;
+	if (textFlags & 0x01) {
+		styleFlags = kStyleAlignCenter;
+	} else if (textFlags & 0x02) {
+		styleFlags = kStyleAlignRight;
+	}
+	if (textFlags & 0x04) {
+		styleFlags = (TextStyleFlags)(styleFlags | kStyleWordWrap);
+	}
+
+	// Use white color (index 255) for subtitle text
+	// The original uses colors from the palette, commonly white or yellow for subtitles
+	int16 textColor = 255;
+
+	// Draw the text string
+	if (textFlags & 0x04) {
+		// Word-wrapped text
+		_rebelMsgFont->drawStringWrap(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+	} else {
+		// Single-line text
+		_rebelMsgFont->drawString(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+	}
+
+	debug("Rebel2 Opcode 9: Rendered subtitle at (%d,%d) flags=0x%x clip=(%d,%d,%d,%d)",
+		posX, posY, textFlags, clipX, clipY, clipW, clipH);
+}
+
 void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Opcode 4: Enemy position update
 	// Read 5 shorts from the stream (offset +8 through +16)
@@ -1582,7 +1738,31 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
 
 	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
+	// The 0x20 flag indicates a non-interactive cutscene/intro sequence
 	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+
+	// During intro sequences:
+	// - Hide the crosshair/cursor (handled by not drawing it)
+	// - Skip all HUD/status bar rendering
+	// - Skip mouse input processing
+	// The mouse cursor is already hidden by SmushPlayer during video playback
+	if (introPlaying) {
+		// Track state transition for debugging
+		if (!_introCursorPushed) {
+			_introCursorPushed = true;
+			debug("Rebel2: Intro sequence detected (flags=0x%x) - HUD disabled", _player->_curVideoFlags);
+		}
+		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
+		return;
+	} else {
+		// Gameplay mode - restore normal rendering
+		if (_introCursorPushed) {
+			_introCursorPushed = false;
+			debug("Rebel2: Gameplay started - HUD enabled");
+		}
+	}
+
+	// From here on, we're in gameplay mode (not intro)
 	if (!introPlaying) {
 	// ============================================================
 	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 35e33260de1..5e7afaeadf7 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -63,6 +63,7 @@ public:
 	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+	void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) override;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 020da441c8a..4c6615fcc93 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2679,10 +2679,14 @@ Common::Error ScummEngine::go() {
 		if (_game.features & GF_DEMO) {
 			((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
 		} else {
-			// Mark the following SAN as an intro so HUD isn't rendered during it (bit 0x20)
-			//((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
-			//((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
-			// Clear intro flag and immediately start the mission SAN
+			// Play Level 1 intro cinematic (01BEG.SAN)
+			// Set video flags to 0x20 to mark this as a non-interactive intro sequence
+			// This flag tells procPostRendering to skip HUD/crosshair rendering
+			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
+			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
+
+			// After intro completes, start the gameplay mission video (01P01.SAN)
+			// Clear the intro flag so HUD renders during gameplay
 			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
 			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
 		}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index ccea514286f..59a1c332f24 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -54,7 +54,7 @@
 
 namespace Scumm {
 
-static const int MAX_STRINGS = 200;
+static const int MAX_STRINGS = 800;  // RA2 has ~658 strings
 static const int ETRS_HEADER_LENGTH = 16;
 
 class StringResource {
@@ -136,7 +136,24 @@ public:
 			}
 
 			data_end -= 2;
-			assert(data_end > data_start);
+			// Handle empty entries (e.g., RA2 TRS files have entries with no content)
+			if (data_end <= data_start) {
+				// Skip this entry - no content
+				def_start = strchr(def_end + 1, '#');
+				continue;
+			}
+
+			// Strip leading // from first line (RA2 format uses // prefix for content)
+			if (data_start[0] == '/' && data_start[1] == '/') {
+				data_start += 2;
+			}
+
+			// Recalculate length after stripping
+			if (data_end <= data_start) {
+				def_start = strchr(def_end + 1, '#');
+				continue;
+			}
+
 			char *value = new char[data_end - data_start + 1];
 			assert(value);
 			memcpy(value, data_start, data_end - data_start);
@@ -183,14 +200,16 @@ public:
 };
 
 static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_encoded) {
-	debugC(DEBUG_SMUSH, "trying to read text resources from %s", file);
+	debugC(DEBUG_SMUSH, "getStrings: trying to read text resources from %s", file);
 	ScummFile *theFile = vm->instantiateScummFile();
 
 	vm->openFile(*theFile, file);
 	if (!theFile->isOpen()) {
+		debugC(DEBUG_SMUSH, "getStrings: Failed to open %s", file);
 		delete theFile;
 		return 0;
 	}
+	debugC(DEBUG_SMUSH, "getStrings: Successfully opened %s", file);
 	int32 length = theFile->size();
 	char *filebuffer = new char [length + 1];
 	assert(filebuffer);
@@ -575,9 +594,18 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 		b.read(string, subSize - 16);
 	} else {
 		int string_id = b.readUint16LE();
-		if (!_strings)
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: TRES string_id=%d pos=(%d,%d) flags=0x%x clip=(%d,%d,%d,%d) _strings=%p",
+			  string_id, pos_x, pos_y, flags, left, top, width, height, (void*)_strings);
+		if (!_strings) {
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: _strings is null, cannot look up string");
 			return;
+		}
 		str = _strings->get(string_id);
+		if (str) {
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: Found string: \"%s\"", str);
+		} else {
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleTextResource: String ID %d not found", string_id);
+		}
 	}
 
 	// if subtitles disabled and bit 3 is set, then do not draw
@@ -1018,8 +1046,18 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 
 void SmushPlayer::setupAnim(const char *file) {
 	if (_insanity) {
-		if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS)))
+		if (_vm->_game.id == GID_REBEL2) {
+			// Rebel Assault 2 uses SYSTM/GAME.TRS for all subtitle strings
+			// The TRS file is ETRS-encoded (XOR with 0xCC)
+			_strings = getStrings(_vm, "SYSTM/GAME.TRS", true);
+			if (_strings) {
+				debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Loaded GAME.TRS string resources successfully");
+			} else {
+				debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Failed to load GAME.TRS!");
+			}
+		} else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
 			readString("mineroad.trs");
+		}
 	} else
 		readString(file);
 }
@@ -1047,6 +1085,26 @@ SmushFont *SmushPlayer::getFont(int font) {
 
 			_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
 		}
+	} else if (_vm->_game.id == GID_REBEL2) {
+		// Rebel Assault 2 fonts:
+		// font 0: TALKFONT.NUT - main dialog/subtitle font
+		// font 1: DIHIFONT.NUT - high-res dialog font
+		// font 2: TITLFONT.NUT - title font
+		// font 3: SMALFONT.NUT - small font for HUD
+		const char *ra2_fonts[] = {
+			"SYSTM/TALKFONT.NUT",
+			"SYSTM/DIHIFONT.NUT",
+			"SYSTM/TITLFONT.NUT",
+			"SYSTM/SMALFONT.NUT"
+		};
+		int numFonts = ARRAYSIZE(ra2_fonts);
+		if (font >= 0 && font < numFonts) {
+			_sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+		} else {
+			// Fallback to font 0 for unknown font indices
+			debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
+			_sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+		}
 	} else {
 		int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
 		assert(font >= 0 && font < numFonts);


Commit: a3676cab6e3312ae277d93f7107858c07f97fd61
    https://github.com/scummvm/scummvm/commit/a3676cab6e3312ae277d93f7107858c07f97fd61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:52+02:00

Commit Message:
SCUMM: RA2: Add dedicated audio system

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 38190aafb90..05e05f882d1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -39,6 +39,8 @@
 
 #include "scumm/insane/insane_rebel.h"
 
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
 
 namespace Scumm {
 
@@ -216,10 +218,18 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_rebelEmbeddedHud[i].renderY = 0;
 		_rebelEmbeddedHud[i].valid = false;
 	}
+
+	// Initialize audio system for RA2 (since we don't use iMUSE)
+	_audioSampleRate = 11025;  // RA2 audio is 11025 Hz, not 22050 Hz
+	for (i = 0; i < kRA2MaxAudioTracks; i++) {
+		_audioStreams[i] = nullptr;
+		_audioTrackActive[i] = false;
+	}
 }
 
 
 InsaneRebel2::~InsaneRebel2() {
+	terminateAudio();
 	delete _rebelMsgFont;
 	delete _smush_dispfontNut;
 }
@@ -2162,6 +2172,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// This is called after all other rendering so it appears on top
 	renderScoreHUD(renderBitmap, pitch, width, height, 0);
 
+	} // End of if (!introPlaying) block
+
 	// ============================================================
 	// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
 	// ============================================================
@@ -2179,6 +2191,227 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 }
 
+// ========== Audio Handling for Rebel Assault 2 ==========
+// RA2 doesn't use iMUSE - we handle audio directly through the mixer
+
+void InsaneRebel2::initAudio(int sampleRate) {
+	_audioSampleRate = sampleRate;
+	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+		_audioStreams[i] = nullptr;
+		_audioTrackActive[i] = false;
+	}
+}
+
+void InsaneRebel2::terminateAudio() {
+	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+		if (_audioTrackActive[i]) {
+			_vm->_mixer->stopHandle(_audioHandles[i]);
+			_audioTrackActive[i] = false;
+		}
+		if (_audioStreams[i]) {
+			_audioStreams[i]->finish();
+			_audioStreams[i] = nullptr;
+		}
+	}
 }
 
+void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+	if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
+		debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
+		return;
+	}
+
+	debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
+
+	// Create audio stream if not already active
+	if (!_audioStreams[trackIdx]) {
+		// RA2 audio is 8-bit unsigned mono at the track's sample rate
+		debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+		_audioTrackActive[trackIdx] = true;
+		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+								DisposeAfterUse::NO);
+	}
+
+	debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
+
+	// Copy the audio data since queueBuffer may need to own it
+	byte *audioCopy = (byte *)malloc(size);
+	if (!audioCopy) {
+		return;
+	}
+	memcpy(audioCopy, data, size);
+
+	// Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
+	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+	// Apply volume and pan to the channel
+	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+	int scaledPan = (pan * 127) / 128;  // Convert -128..127 to -127..127
+	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel2::processAudioFrame(int16 feedSize) {
+	if (!_player) {
+		return;
+	}
+
+	// Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
+	if (_player->_smushTracksNeedInit) {
+		_player->_smushTracksNeedInit = false;
+		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+			_player->_smushDispatch[i].fadeRemaining = 0;
+			_player->_smushDispatch[i].fadeVolume = 0;
+			_player->_smushDispatch[i].fadeSampleRate = 0;
+			_player->_smushDispatch[i].elapsedAudio = 0;
+			_player->_smushDispatch[i].audioLength = 0;
+		}
+	}
+
+	// Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
+	// Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
+	for (int i = 0; i < _player->_smushNumTracks; i++) {
+		SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
+		SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
+
+		if (track.state == TRK_STATE_INACTIVE) {
+			continue;
+		}
+
+		// Skip tracks that don't have valid buffer pointers yet
+		// Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
+		// so tracks in FADING state won't have it set yet - that's OK, they'll be
+		// transitioned below and then processed
+		if (!track.blockPtr) {
+			debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
+				  i, (void*)track.blockPtr, track.state);
+			continue;
+		}
+
+		// Check if this track type should be played
+		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
+
+		if (!isPlayableTrack) {
+			continue;
+		}
+
+		// Calculate base volume for this track type
+		int baseVolume;
+		switch (track.flags & TRK_TYPE_MASK) {
+		case IS_SFX:
+			baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
+			break;
+		case IS_BKG_MUSIC:
+			baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
+			break;
+		case IS_SPEECH:
+			baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
+			break;
+		default:
+			baseVolume = track.volume;
+			break;
+		}
+		int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
+
+		// Handle track state transitions: FADING -> PLAYING
+		if (track.state == TRK_STATE_FADING) {
+			dispatch.headerPtr = track.dataBuf;
+			dispatch.dataBuf = track.subChunkPtr;
+			dispatch.dataSize = track.dataSize;
+			dispatch.currentOffset = 0;
+			dispatch.audioLength = 0;
+			track.state = TRK_STATE_PLAYING;
+		}
+
+		// Process audio for this track
+		if (track.state != TRK_STATE_INACTIVE) {
+			int32 tmpFeedSize = feedSize;
+
+			while (tmpFeedSize > 0) {
+				int32 mixInFrameCount = dispatch.currentOffset;
+
+				// Use dispatch.dataBuf and dispatch.dataSize which are set consistently
+				// when the track transitions from FADING to PLAYING, and audioRemaining
+				// is calculated relative to these values by processAudioCodes
+				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+					// Ensure audioRemaining is non-negative for proper circular buffer access
+					if (dispatch.audioRemaining < 0) {
+						debug(5, "InsaneRebel2: Resetting negative audioRemaining=%d for track %d", dispatch.audioRemaining, i);
+						dispatch.audioRemaining = 0;
+					}
+					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+					// Limit to feed size proportional to sample rate
+					if (dispatch.sampleRate > 0 && _player->_smushAudioSampleRate > 0) {
+						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / _player->_smushAudioSampleRate;
+						if (mixInFrameCount > maxFrames) {
+							mixInFrameCount = maxFrames;
+						}
+					}
+
+					// Don't read past the buffer
+					if (offset + mixInFrameCount > dispatch.dataSize) {
+						mixInFrameCount = dispatch.dataSize - offset;
+					}
+
+					// Make sure we don't exceed available data
+					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+						if (mixInFrameCount <= 0) {
+							// Track is ending - no more data
+							track.state = TRK_STATE_ENDING;
+							break;
+						}
+					}
+
+					if (mixInFrameCount > 0) {
+						// Safety check: verify the pointer and offset are within bounds
+						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize) {
+							debug(1, "InsaneRebel2: Invalid audio buffer access track=%d dataBuf=%p offset=%d mixInFrameCount=%d dataSize=%d",
+								  i, (void*)dispatch.dataBuf, offset, mixInFrameCount, dispatch.dataSize);
+							break;
+						}
+
+						// Queue audio data directly to our audio streams
+						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+						// Update dispatch state
+						dispatch.currentOffset -= mixInFrameCount;
+						dispatch.audioRemaining += mixInFrameCount;
+
+						// Calculate how much feed time was consumed
+						if (dispatch.sampleRate > 0) {
+							int32 consumedFeed = mixInFrameCount * _player->_smushAudioSampleRate / dispatch.sampleRate;
+							tmpFeedSize -= consumedFeed;
+						} else {
+							tmpFeedSize -= mixInFrameCount;
+						}
+					}
+				}
+
+				// If currentOffset is depleted, process audio codes to get more
+				if (dispatch.currentOffset <= 0) {
+					// processAudioCodes returns true if there's more audio, false if done
+					if (!_player->processAudioCodes(i, tmpFeedSize, mixVolume)) {
+						break;
+					}
+					// If still no offset after processing codes, we're done
+					if (dispatch.currentOffset <= 0) {
+						break;
+					}
+				} else if (tmpFeedSize <= 0) {
+					break;
+				}
+			}
+		}
+
+		track.audioRemaining = dispatch.audioRemaining;
+		dispatch.state = track.state;
+	}
 }
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 5e7afaeadf7..ced8bb6e441 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -31,6 +31,14 @@
 #include "common/list.h"
 #include "common/rect.h"
 
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+
+namespace Audio {
+class QueuingAudioStream;
+class SoundHandle;
+}
+
 namespace Scumm {
 
 class InsaneRebel2 : public Insane {
@@ -214,6 +222,29 @@ public:
 	// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
 	int _targetLockTimer;
 
+	// ========== Audio Handling ==========
+	// Rebel Assault 2 doesn't use iMUSE - audio is handled directly here
+
+	static const int kRA2MaxAudioTracks = 4;
+
+	Audio::QueuingAudioStream *_audioStreams[kRA2MaxAudioTracks];
+	Audio::SoundHandle _audioHandles[kRA2MaxAudioTracks];
+	bool _audioTrackActive[kRA2MaxAudioTracks];
+	int _audioSampleRate;
+
+	// Initialize audio system for RA2
+	void initAudio(int sampleRate);
+
+	// Terminate audio system
+	void terminateAudio();
+
+	// Process audio dispatches - called from SmushPlayer when iMUSE is null
+	// This replaces the iMUSE audio path for RA2
+	void processAudioFrame(int16 feedSize);
+
+	// Queue audio data for playback on a specific track
+	void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
+
 };
 
 } // End of namespace Insane
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 4c6615fcc93..d29e0da93c1 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1807,9 +1807,10 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 		_useOriginalGUI = false;
 
 		_sound = new Sound(this, _mixer, false);
-		_musicEngine = _imuseDigital = new IMuseDigital(this, 11025, _mixer, &_resourceAccessMutex, false);
+		// Rebel Assault 2 doesn't use iMUSE for audio - audio is handled directly by INSANE
+		_musicEngine = _imuseDigital = nullptr;
 		_insane = new InsaneRebel2(this);
-		_splayer = new SmushPlayer(this, _imuseDigital, _insane);
+		_splayer = new SmushPlayer(this, nullptr, _insane);
 
 		// Initialize cursor
 		_macGui = nullptr; // Ensure this is null as we don't want MacGui behavior
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 59a1c332f24..a0c3c831bbb 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -43,6 +43,7 @@
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
+#include "scumm/insane/insane_rebel.h"
 
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
@@ -292,7 +293,13 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_smushAudioInitialized = false;
 	_smushAudioCallbackEnabled = false;
 
-	initAudio(_imuseDigital->getSampleRate(), 200000);
+	// Rebel Assault 2 doesn't use iMUSE for audio, so _imuseDigital may be null
+	if (_imuseDigital) {
+		initAudio(_imuseDigital->getSampleRate(), 200000);
+	} else {
+		// RA2 audio is 11025 Hz
+		initAudio(11025, 200000);
+	}
 }
 
 SmushPlayer::~SmushPlayer() {
@@ -1191,7 +1198,15 @@ void SmushPlayer::parseNextFrame() {
 	if (_insanity)
 		_vm->_sound->processSound();
 
-	_vm->_imuseDigital->flushTracks();
+	if (_vm->_imuseDigital)
+		_vm->_imuseDigital->flushTracks();
+
+	// Rebel Assault 2 audio processing - call processDispatches directly since no iMUSE
+	if (!_imuseDigital && _vm->_game.id == GID_REBEL2) {
+		// Use a feed size based on frame rate (similar to iMUSE)
+		// 11025 Hz / 12 fps = ~918 samples per frame
+		processDispatches(_smushAudioSampleRate / 12);
+	}
 }
 
 void SmushPlayer::setPalette(const byte *palette) {
@@ -1330,10 +1345,13 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 	// This piece of code is used to ensure there are
 	// no audio hiccups while loading the SMUSH video;
 	// Each version of the engine does it in its own way.
-	if (_imuseDigital->isFTSoundEngine()) {
-		_imuseDigital->fillStreamsWhileMusicCritical(20);
-	} else {
-		_imuseDigital->floodMusicBuffer();
+	// Rebel Assault 2 doesn't use iMUSE for audio.
+	if (_imuseDigital) {
+		if (_imuseDigital->isFTSoundEngine()) {
+			_imuseDigital->fillStreamsWhileMusicCritical(20);
+		} else {
+			_imuseDigital->floodMusicBuffer();
+		}
 	}
 
 	int skipped = 0;
@@ -1440,7 +1458,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 			_IACTpos = 0;
 
 			resetAudioTracks(); // For DIG demo
-			_imuseDigital->stopSMUSHAudio(); // For DIG & COMI
+			if (_imuseDigital)
+				_imuseDigital->stopSMUSHAudio(); // For DIG & COMI
 			break;
 		}
 
@@ -1461,7 +1480,9 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
 	int32 maxSizes[SMUSH_MAX_TRACKS] = {100000, 100000, 100000, 400000};
 
-	_imuseDigital->setSmushPlayer(this);
+	// Rebel Assault 2 doesn't use iMUSE for audio
+	if (_imuseDigital)
+		_imuseDigital->setSmushPlayer(this);
 
 	// DIG demo uses this audio system but doesn't use INSANE
 	if (_insane)
@@ -1471,6 +1492,23 @@ void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
 
 	memset(_smushAudioTable, 0, sizeof(_smushAudioTable));
 
+	// Initialize dispatch structures to safe defaults
+	for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+		_smushDispatch[i].headerPtr = nullptr;
+		_smushDispatch[i].dataBuf = nullptr;
+		_smushDispatch[i].dataSize = 0;
+		_smushDispatch[i].audioRemaining = 0;
+		_smushDispatch[i].currentOffset = 0;
+		_smushDispatch[i].sampleRate = 0;
+		_smushDispatch[i].state = TRK_STATE_INACTIVE;
+		_smushDispatch[i].fadeSampleRate = 0;
+		_smushDispatch[i].fadeVolume = 0;
+		_smushDispatch[i].fadeRemaining = 0;
+		_smushDispatch[i].volumeStep = 0;
+		_smushDispatch[i].elapsedAudio = 0;
+		_smushDispatch[i].audioLength = 0;
+	}
+
 	for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
 		_smushTrackVols[i] = 127;
 		_smushTrackFlags[i] = 1;
@@ -1516,6 +1554,9 @@ int SmushPlayer::addAudioTrack(int32 trackBlockSize, int32 maxBlockSize) {
 	_smushTracks[id].groupId = GRP_MASTER;
 	_smushTracks[id].blockSize = trackBlockSize;
 	_smushTracks[id].parsedChunks = 0;
+	_smushTracks[id].dataBuf = nullptr;
+	_smushTracks[id].subChunkPtr = nullptr;
+	_smushTracks[id].dataSize = 0;
 
 	_smushTracks[id].fadeBuf = (uint8 *)malloc(SMUSH_FADE_SIZE);
 	if (!_smushTracks[id].fadeBuf)
@@ -1737,6 +1778,15 @@ void SmushPlayer::processDispatches(int16 feedSize) {
 	bool isPlayableTrack;
 	bool speechIsPlaying = false;
 
+	// Rebel Assault 2 doesn't use iMUSE for audio - use InsaneRebel2's audio handler
+	if (!_imuseDigital) {
+		if (_vm->_game.id == GID_REBEL2 && _insane) {
+			InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+			rebel2->processAudioFrame(feedSize);
+		}
+		return;
+	}
+
 	int engineBaseFeedSize = _imuseDigital->getFeedSize();
 
 	if (!_paused) {
@@ -2166,6 +2216,10 @@ bool SmushPlayer::processAudioCodes(int idx, int32 &tmpFeedSize, int &mixVolume)
 }
 
 void SmushPlayer::sendAudioToDiMUSE(uint8 *mixBuf, int32 mixStartingPoint, int32 mixFeedSize, int32 mixInFrameCount, int volume, int pan) {
+	// Rebel Assault 2 doesn't use iMUSE for audio
+	if (!_imuseDigital)
+		return;
+
 	int clampedVol, clampedPan;
 	bool is11025Hz = false;
 


Commit: f3bd96abae7ec9418b38a66136f53cabaed67d03
    https://github.com/scummvm/scummvm/commit/f3bd96abae7ec9418b38a66136f53cabaed67d03
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:52+02:00

Commit Message:
SCUMM: RA2: Implement main menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/nut_renderer.h
    engines/scumm/scumm.cpp
    engines/scumm/scumm_v7.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 05e05f882d1..b6e76998ece 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -25,6 +25,8 @@
 #include "common/system.h"
 #include "common/memstream.h"
 
+#include "graphics/cursorman.h"
+
 #include "scumm/actor.h"
 #include "scumm/file.h"
 #include "scumm/resource.h"
@@ -77,6 +79,24 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
 	_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
 
+	// Load menu system fonts (from info.md - FUN_403BD0 lines 302-348)
+	// In low resolution mode, fonts are loaded as a linked list:
+	//   Font 0 (^f00): TALKFONT.NUT - Default menu font (DAT_00485058)
+	//   Font 1 (^f01): SMALFONT.NUT - Small font for format code switching
+	//   Font 2 (^f02): TITLFONT.NUT - Title font
+	//   Font 3 (^f03): POVFONT.NUT - POV font (not loaded here)
+	_smush_talkfontNut = new NutRenderer(_vm, "SYSTM/TALKFONT.NUT");
+	_smush_smalfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
+	_smush_titlefontNut = new NutRenderer(_vm, "SYSTM/TITLFONT.NUT");
+
+	// SmushFont for menu text rendering - uses SMALFONT with proper drawString support
+	_menuFont = new SmushFont(_vm, "SYSTM/SMALFONT.NUT", true);
+
+	// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
+	// This is loaded into DAT_0047aba8 and used as a background overlay via FUN_004236e0
+	// The original game uses the standard Windows arrow cursor (IDC_ARROW via LoadCursorA)
+	_smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
+
 	_enemies.clear();
 	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -225,13 +245,32 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_audioStreams[i] = nullptr;
 		_audioTrackActive[i] = false;
 	}
+
+	// Initialize menu system
+	_gameState = kStateMainMenu;  // Start at main menu
+	_menuSelection = 0;           // First item selected
+	// Main menu has 6 selectable items (0-5): START, OPTIONS, CONTINUE INTRO, TOP PILOTS, CREDITS, QUIT
+	// Note: The coordinate formula uses param_3 = 7 (includes title) for Y position calculation
+	// but _menuItemCount is the number of SELECTABLE items for bounds checking
+	_menuItemCount = 6;
+	_menuInactivityTimer = 0;
+	_lastMenuVariant = -1;        // No previous menu video
+	_menuRepeatDelay = 0;
+	for (i = 0; i < 16; i++) {
+		_levelUnlocked[i] = (i == 0);  // Only level 1 unlocked initially
+	}
 }
 
 
 InsaneRebel2::~InsaneRebel2() {
 	terminateAudio();
 	delete _rebelMsgFont;
+	delete _menuFont;
 	delete _smush_dispfontNut;
+	delete _smush_talkfontNut;
+	delete _smush_smalfontNut;
+	delete _smush_titlefontNut;
+	delete _smush_mouseoverNut;
 }
 
 // Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
@@ -506,9 +545,68 @@ void InsaneRebel2::clearBit(int n) {
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) {
+	// Debug: Log all IACT opcodes
+	debug("Rebel2 IACT: opcode=%d par2=%d par3=%d par4=%d gameState=%d sceneId=%d",
+		par1, par2, par3, par4, _gameState, _currSceneId);
+
 	if (_keyboardDisable)
 		return;
 
+	// Handle menu IACT - menu videos have embedded ANIM data in IACT chunks
+	// Menu IACTs have par1=8 (code), par2=46 (flags), par4>=1000 (userId)
+	// The embedded ANIM contains the full menu frame
+	if (_gameState == kStateMainMenu && par1 == 8 && par4 >= 1000) {
+		debug("Rebel2 IACT: Menu mode - processing embedded ANIM (userId=%d)", par4);
+
+		// Scan for embedded ANIM tag in the IACT data
+		int64 startPos = b.pos();
+		int64 totalSize = b.size();
+		debug("Rebel2 IACT: stream pos=%d, size=%d, remaining=%d",
+			(int)startPos, (int)totalSize, (int)(totalSize - startPos));
+
+		if (totalSize > startPos) {
+			int64 remaining = totalSize - startPos;
+			int scanSize = (int)MIN<int64>(remaining, 65536);
+			byte *scanBuf = (byte *)malloc(scanSize);
+			if (scanBuf) {
+				int bytesRead = b.read(scanBuf, scanSize);
+				debug("Rebel2 IACT: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+					bytesRead, scanBuf[0], scanBuf[1], scanBuf[2], scanBuf[3],
+					scanBuf[4], scanBuf[5], scanBuf[6], scanBuf[7],
+					scanBuf[8], scanBuf[9], scanBuf[10], scanBuf[11],
+					scanBuf[12], scanBuf[13], scanBuf[14], scanBuf[15]);
+
+				// Look for ANIM tag (embedded SAN containing menu frame)
+				for (int i = 0; i + 8 <= bytesRead; ++i) {
+					if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+						int64 animStreamPos = startPos + i;
+						uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+						debug("Rebel2 IACT: Found embedded ANIM at offset %d, size %d", (int)i, (int)animReportedSize);
+						if (toCopy > 0) {
+							byte *animData = (byte *)malloc(toCopy);
+							if (animData) {
+								b.seek(animStreamPos);
+								b.read(animData, toCopy);
+								// Use userId as the HUD slot (1000 -> slot 0 for menu background)
+								loadEmbeddedSan(0, animData, toCopy, renderBitmap);
+								free(animData);
+							}
+						}
+						b.seek(startPos);
+						free(scanBuf);
+						return;
+					}
+				}
+
+				debug("Rebel2 IACT: No ANIM tag found in menu IACT data");
+				b.seek(startPos);
+				free(scanBuf);
+			}
+		}
+		return;
+	}
+
 	if (_currSceneId == 1)
 		iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
 }
@@ -1202,8 +1300,8 @@ extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int wi
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
-	// Validate userId (1-4 for HUD slots)
-	if (userId < 1 || userId > 4 || !animData || size < 32) {
+	// Validate userId (0 for menu background, 1-4 for HUD slots)
+	if (userId < 0 || userId > 4 || !animData || size < 32) {
 		debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
 		return;
 	}
@@ -1748,10 +1846,65 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
 
 	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
-	// The 0x20 flag indicates a non-interactive cutscene/intro sequence
+	// The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
 	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
 
-	// During intro sequences:
+	// Check if we're in menu mode (menu state + intro flag)
+	bool menuMode = (introPlaying && _gameState == kStateMainMenu);
+
+	// Handle menu input and rendering if in menu mode
+	if (menuMode) {
+		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
+		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
+		// MSTOVER.NUT is a background overlay, NOT a cursor
+		static const byte arrowCursor[11 * 16] = {
+			1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+			1, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+			1, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0,
+			1, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0,
+			1, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0,
+			1, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0,
+			1, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0,
+			1, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0,
+			1, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0,
+			1, 255, 255, 255, 255, 255, 1, 1, 1, 1, 1,
+			1, 255, 255, 1, 255, 255, 1, 0, 0, 0, 0,
+			1, 255, 1, 0, 1, 255, 255, 1, 0, 0, 0,
+			1, 1, 0, 0, 1, 255, 255, 1, 0, 0, 0,
+			1, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
+			0, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
+			0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
+		};
+		CursorMan.replaceCursor(arrowCursor, 11, 16, 0, 0, 0);
+		CursorMan.showMouse(true);
+
+		// Process menu input during each frame
+		int selection = processMenuInput();
+
+		// Update inactivity timer
+		_menuInactivityTimer++;
+
+		// Check for inactivity timeout (300 frames = ~10 sec at 30fps, or ~25 sec at 12fps)
+		if (_menuInactivityTimer > 300) {
+			debug("Rebel2: Menu inactivity timeout");
+			// Reset timer but don't take action yet
+			_menuInactivityTimer = 0;
+		}
+
+		// Draw menu selection overlay
+		drawMenuOverlay(renderBitmap, pitch, width, height);
+
+		// If a selection was confirmed, signal video to stop
+		if (selection >= 0) {
+			debug("Rebel2: Menu selection confirmed: %d", selection);
+			_vm->_smushVideoShouldFinish = true;
+		}
+
+		// Skip normal HUD rendering in menu mode
+		return;
+	}
+
+	// During intro sequences (non-menu):
 	// - Hide the crosshair/cursor (handled by not drawing it)
 	// - Skip all HUD/status bar rendering
 	// - Skip mouse input processing
@@ -2414,4 +2567,410 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
 	}
 }
 
+// ======================= Menu System Implementation =======================
+// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
+
+void InsaneRebel2::resetMenu() {
+	_menuSelection = 0;
+	_menuInactivityTimer = 0;
+	_menuRepeatDelay = 0;
+}
+
+Common::String InsaneRebel2::getRandomMenuVideo() {
+	// Emulates FUN_0041FDC8 - selects random menu video variant
+	//
+	// NOTE: The original game plays O_MENU.SAN when no progress flags are set,
+	// but that file contains ONLY audio (no FOBJ video frames). The O_MENU_X.SAN
+	// variants (A through O) contain actual 320x200 background images in Frame 0.
+	//
+	// We ALWAYS use a random variant to ensure a proper background is displayed.
+	// The original behavior of showing O_MENU.SAN (audio-only) would result in
+	// a black/undefined background which doesn't match the intended experience.
+
+	// Select random variant (0-14 maps to A-O), ensuring different from last
+	int variant;
+	do {
+		variant = _vm->_rnd.getRandomNumber(14);  // 0-14
+	} while (variant == _lastMenuVariant && _lastMenuVariant >= 0);
+	_lastMenuVariant = variant;
+
+	// Map 0-14 to A-O (case 0/default = A, 1 = B, etc.)
+	char letter = 'A' + variant;
+	debug("Rebel2: Selected menu variant %c", letter);
+	return Common::String::format("OPEN/O_MENU_%c.SAN", letter);
+}
+
+int InsaneRebel2::processMenuInput() {
+	// Emulates FUN_0041f5ae menu input handling
+	// Returns: -1 = no action, 0-6 = menu item selected
+
+	Common::Event event;
+	int result = -1;
+
+	// Check for key/mouse events
+	Common::EventManager *eventMan = _vm->_system->getEventManager();
+
+	// Get current key state from _vm's VAR system or direct polling
+	// Key codes: 0x148 = Up, 0x150 = Down, 0x1B = ESC, 0x0D = Enter
+
+	// Poll for keyboard input
+	while (eventMan->pollEvent(event)) {
+		switch (event.type) {
+		case Common::EVENT_KEYDOWN:
+			_menuInactivityTimer = 0;  // Reset inactivity timer on any input
+
+			switch (event.kbd.keycode) {
+			case Common::KEYCODE_UP:
+				// Navigate up (wrap around)
+				if (_menuRepeatDelay == 0) {
+					_menuSelection--;
+					if (_menuSelection < 0) {
+						_menuSelection = _menuItemCount - 1;
+					}
+					_menuRepeatDelay = 3;  // Delay before repeat
+					debug("Menu: Selection changed to %d (UP)", _menuSelection);
+				}
+				break;
+
+			case Common::KEYCODE_DOWN:
+				// Navigate down (wrap around)
+				if (_menuRepeatDelay == 0) {
+					_menuSelection++;
+					if (_menuSelection >= _menuItemCount) {
+						_menuSelection = 0;
+					}
+					_menuRepeatDelay = 3;
+					debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
+				}
+				break;
+
+			case Common::KEYCODE_RETURN:
+			case Common::KEYCODE_KP_ENTER:
+				// Confirm selection
+				if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
+					result = _menuSelection;
+					debug("Menu: Item %d selected (ENTER)", _menuSelection);
+				}
+				break;
+
+			case Common::KEYCODE_ESCAPE:
+				// ESC - Exit/Quit (return special value)
+				result = 6;  // Quit option
+				debug("Menu: ESC pressed - quit");
+				break;
+
+			default:
+				break;
+			}
+			break;
+
+		case Common::EVENT_LBUTTONDOWN:
+			// Mouse click - check if over a menu item
+			// Menu items are vertically stacked, centered at X=160
+			// Y positions from FUN_41F5AE: param_3 * -5 + local_c * 10 + 0x68
+			// With param_3 = 7: base Y = 69, items at Y = 69, 79, 89, 99, 109, 119
+			_menuInactivityTimer = 0;
+			{
+				int mouseY = event.mouse.y;
+				int baseY = 69;  // First item Y position (low-res)
+				int itemHeight = 10;
+
+				// Check which item was clicked
+				for (int i = 0; i < _menuItemCount; i++) {
+					int itemY = baseY + i * itemHeight;
+					if (mouseY >= itemY - 5 && mouseY <= itemY + 5) {
+						_menuSelection = i;
+						result = i;
+						debug("Menu: Item %d clicked at Y=%d", i, mouseY);
+						break;
+					}
+				}
+			}
+			break;
+
+		default:
+			break;
+		}
+	}
+
+	// Handle key repeat delay
+	if (_menuRepeatDelay > 0) {
+		_menuRepeatDelay--;
+	}
+
+	return result;
+}
+
+void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
+	// Emulates FUN_0041f5ae menu text overlay rendering
+	//
+	// IMPORTANT: The menu background comes from the O_MENU_X.SAN video file, NOT from MSTOVER.NUT.
+	// The O_MENU_X.SAN files (A through O) each contain a full 320x200 FOBJ frame in Frame 0
+	// which is decoded by SmushPlayer and stored in renderBitmap before this function is called.
+	// MSTOVER.NUT is only used in cheat mode (when DAT_0047aba4 != 0).
+	//
+	// From FUN_4147B2: param_3 = (DAT_0047a806 == 0) + 6 = 7 for keyboard mode
+	// Menu structure: 1 title + 6 selectable items = 7 total items
+	static const char *menuItems[] = {
+		"GAME MAIN MENU",   // Title (index 0)
+		"START GAME",       // Item 0 (index 1)
+		"OPTIONS",          // Item 1 (index 2)
+		"CONTINUE INTRO",   // Item 2 (index 3)
+		"SHOW TOP PILOTS",  // Item 3 (index 4)
+		"SHOW CREDITS",     // Item 4 (index 5)
+		"QUIT"              // Item 5 (index 6)
+	};
+
+	const int numItemsTotal = 7;  // Title + 6 menu options (used for Y calculations)
+	const int numSelectableItems = 6;  // Selectable menu options (0-5)
+
+	// The O_MENU_X.SAN video frame is already in renderBitmap from SmushPlayer.
+	// We only draw text and selection highlights on top.
+
+	// From assembly FUN_0041f5ae (low-res mode, DAT_0047a808 < 2):
+	// Center X = 160 for 320px, scale for actual width
+	// Title Y = 46, Item base Y = 69, spacing = 10
+	const int centerX = width / 2;  // Center in actual buffer
+	const int titleY = numItemsTotal * -5 + 0x51;  // 46
+	const int itemBaseY = numItemsTotal * -5 + 0x68;  // 69
+	const int itemSpacing = 10;
+
+	debug(5, "drawMenuOverlay: buffer %dx%d, centerX=%d", width, height, centerX);
+
+	// Use SMALFONT.NUT for menu text rendering
+	NutRenderer *font = _smush_smalfontNut;
+	if (!font) {
+		debug(1, "drawMenuOverlay: font is NULL!");
+		return;
+	}
+
+	// Get the number of characters in the font
+	int numFontChars = font->getNumChars();
+	debug(5, "drawMenuOverlay: font has %d chars", numFontChars);
+
+	// Helper: calculate string width with bounds checking
+	auto getStringWidth = [&](const char *str) -> int {
+		int w = 0;
+		while (*str) {
+			byte c = (byte)*str++;
+			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c < numFontChars) {
+				w += font->getCharWidth(c);
+			}
+		}
+		return w;
+	};
+
+	// Debug: Check if renderBitmap has video content (non-zero pixels)
+	static int debugFrameCount = 0;
+	if (debugFrameCount < 5) {
+		int nonZeroCount = 0;
+		for (int i = 0; i < MIN(width * height, 1000); i++) {
+			if (renderBitmap[i] != 0) nonZeroCount++;
+		}
+		debug(1, "drawMenuOverlay: frame %d, buffer sample has %d non-zero pixels in first 1000",
+		      debugFrameCount, nonZeroCount);
+		debugFrameCount++;
+	}
+
+	// Set up clipRect for the entire rendering area
+	// Use screen dimensions (320x200) for the clip rect, not the video frame dimensions.
+	// The menu text is designed for a 320x200 screen, and the underlying renderBitmap
+	// (from SmushPlayer's virtual screen buffer) is always screen-sized. The width/height
+	// parameters come from _player->_width/_height which may be smaller if the video
+	// frame doesn't cover the full screen, but that would incorrectly clip the menu text.
+	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+
+	// Use screen width as the actual pitch for rendering. During SMUSH playback,
+	// SmushPlayer sets vs->pitch = vs->w = _screenWidth (see smush_player.cpp init()).
+	// The 'pitch' parameter comes from _player->_width which may differ from the actual
+	// buffer pitch if the video frame has different dimensions. Using the wrong pitch
+	// would cause character rows to be written at incorrect offsets in the buffer.
+	int actualPitch = _vm->_screenWidth;
+
+	auto drawString = [&](const char *str, int x, int y) {
+		while (*str) {
+			byte c = (byte)*str++;
+			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+
+			// Skip characters outside font range
+			if (c >= numFontChars) {
+				debug(5, "drawMenuOverlay: char %d out of range (max %d)", c, numFontChars);
+				continue;
+			}
+
+			int charW = font->getCharWidth(c);
+
+			// Use NutRenderer's drawCharV7 which properly handles character rendering
+			// col=-1 means use original font colors, hardcodedColors=true, smushColorMode=true
+			if (x >= 0 && y >= 0 && charW > 0) {
+				font->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
+				                 kStyleAlignLeft, c, true, true);
+			}
+			x += charW;
+		}
+	};
+
+	// Draw title centered at Y = 46
+	{
+		int titleWidth = getStringWidth(menuItems[0]);
+		int titleX = centerX - titleWidth / 2;
+		drawString(menuItems[0], titleX, titleY);
+	}
+
+	// Draw menu items starting at Y = 69
+	for (int i = 0; i < numSelectableItems; i++) {
+		int itemY = itemBaseY + i * itemSpacing;
+		const char *text = menuItems[i + 1];
+
+		int textWidth = getStringWidth(text);
+		int textX = centerX - textWidth / 2;
+		drawString(text, textX, itemY);
+
+		// Draw selection highlight box around selected item
+		if (i == _menuSelection) {
+			int bracketWidth = textWidth + 12;
+			int bracketHeight = 10;
+			byte highlightColor = 255;  // White/bright color for visibility
+
+			int leftX = centerX - bracketWidth / 2;
+			int rightX = centerX + bracketWidth / 2;
+			int topY = itemY - 1;
+			int bottomY = itemY + bracketHeight - 1;
+
+			// Clamp to screen bounds (use screen dimensions, not video frame dimensions)
+			int screenW = _vm->_screenWidth;
+			int screenH = _vm->_screenHeight;
+			if (leftX < 0) leftX = 0;
+			if (rightX >= screenW) rightX = screenW - 1;
+			if (topY < 0) topY = 0;
+			if (bottomY >= screenH) bottomY = screenH - 1;
+
+			// Draw selection rectangle using actualPitch (screen width)
+			for (int x = leftX; x <= rightX && x < screenW; x++) {
+				if (topY >= 0 && topY < screenH)
+					renderBitmap[topY * actualPitch + x] = highlightColor;
+				if (bottomY >= 0 && bottomY < screenH)
+					renderBitmap[bottomY * actualPitch + x] = highlightColor;
+			}
+			for (int py = topY; py <= bottomY && py < screenH; py++) {
+				if (leftX >= 0 && leftX < screenW)
+					renderBitmap[py * actualPitch + leftX] = highlightColor;
+				if (rightX >= 0 && rightX < screenW)
+					renderBitmap[py * actualPitch + rightX] = highlightColor;
+			}
+		}
+	}
+}
+
+int InsaneRebel2::runMainMenu() {
+	// Main menu loop - emulates FUN_004147B2
+	// Returns:
+	//   kMenuNewGame (2) = Start new game
+	//   kMenuContinue (4) = Continue game (level select)
+	//   kMenuCredits (1) = Show credits then return to menu
+	//   0 = Quit game
+
+	debug("Rebel2: Entering main menu");
+
+	resetMenu();
+	_gameState = kStateMainMenu;
+
+	// Get the SmushPlayer from ScummEngine_v7
+	// Note: _player isn't set until SmushPlayer::initAudio() is called during playback
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Main menu loop
+	while (!_vm->shouldQuit()) {
+		// Reset video finish flag before playing menu
+		_vm->_smushVideoShouldFinish = false;
+
+		// Select and play a random menu video
+		Common::String menuVideo = getRandomMenuVideo();
+		debug("Rebel2: Playing menu video: %s", menuVideo.c_str());
+
+		// Set video flags for menu (0x20 = intro/menu flag)
+		// This tells procPostRendering we're in menu mode
+		splayer->setCurVideoFlags(0x20);
+
+		// Play the menu video
+		// Input is processed in procPostRendering during playback
+		// When user confirms selection, _vm->_smushVideoShouldFinish is set
+		splayer->play(menuVideo.c_str(), 12);
+
+		// Check for quit
+		if (_vm->shouldQuit()) {
+			return 0;
+		}
+
+		// If video ended naturally (not by selection), loop back
+		if (!_vm->_smushVideoShouldFinish) {
+			// Video ended without selection (reached end or ESC during video)
+			// Continue looping menu videos
+			continue;
+		}
+
+		// Clear the flag
+		_vm->_smushVideoShouldFinish = false;
+
+		// A selection was made - process it
+		debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
+
+		// Process the menu result based on current selection
+		// Menu items (from FUN_004147B2 disassembly):
+		// case 0: return 2 (New Game)
+		// case 1: return 4 (Continue)
+		// case 2: Options menu (stays in loop)
+		// case 3: return 0 (Exit)
+		// case 4: Unknown function
+		// case 5: Credits (play O_CREDIT.SAN, then return 1)
+		// case 6: Quit (stop video, exit)
+		switch (_menuSelection) {
+		case 0:  // New Game
+			debug("Rebel2: New Game selected");
+			_gameState = kStateGameplay;
+			return kMenuNewGame;
+
+		case 1:  // Continue
+			debug("Rebel2: Continue selected");
+			_gameState = kStateLevelSelect;
+			return kMenuContinue;
+
+		case 2:  // Options
+			debug("Rebel2: Options selected");
+			// TODO: Show options menu (FUN_00406ed2)
+			// For now, just continue menu loop
+			break;
+
+		case 3:  // Exit (back to title/intro)
+			debug("Rebel2: Exit selected");
+			// Return to menu loop
+			break;
+
+		case 4:  // Unknown function (FUN_00420116)
+			debug("Rebel2: Unknown menu item 4 selected");
+			break;
+
+		case 5:  // Credits
+			debug("Rebel2: Credits selected");
+			_gameState = kStateCredits;
+			splayer->setCurVideoFlags(0x20);
+			splayer->play("OPEN/O_CREDIT.SAN", 12);
+			_gameState = kStateMainMenu;
+			// After credits, return to menu
+			break;
+
+		case 6:  // Quit
+			debug("Rebel2: Quit selected");
+			return 0;
+
+		default:
+			debug("Rebel2: Unknown menu selection %d", _menuSelection);
+			break;
+		}
+	}
+
+	return 0;
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ced8bb6e441..bf4ffa094cb 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -47,6 +47,54 @@ public:
 	InsaneRebel2(ScummEngine_v7 *scumm);
 	~InsaneRebel2();
 
+	// ======================= Menu System =======================
+	// Main game states (emulates retail state machine from FUN_004142BD)
+	enum GameState {
+		kStateIntro = 0,      // Stage 0: Intro/Credits sequence
+		kStateMainMenu = 1,   // Stage 1: Main menu (FUN_004147B2)
+		kStateLevelSelect = 2,// Stage 2: Level selection (FUN_00414A41)
+		kStateBriefing = 3,   // Stage 3: Mission briefing (FUN_00415CF8)
+		kStateGameplay = 4,   // Stage 4: Gameplay (FUN_00416787)
+		kStateCredits = 5,    // Credits sequence
+		kStateQuit = 6        // Exit game
+	};
+
+	// Menu selection results (return values from FUN_004147B2)
+	enum MenuResult {
+		kMenuNewGame = 2,     // case 0: New Game
+		kMenuContinue = 4,    // case 1: Continue
+		kMenuOptions = 0,     // case 2: Options (stays in menu)
+		kMenuExit = 0,        // case 3: Exit to title
+		kMenuUnknown = 0,     // case 4: Unknown function
+		kMenuCredits = 1,     // case 5: Show credits
+		kMenuQuit = 0         // case 6: Quit game
+	};
+
+	GameState _gameState;           // Current game state
+	int _menuSelection;             // Current menu item (0-6), mirrors DAT_00459988
+	int _menuItemCount;             // Number of menu items (7 for main menu)
+	int _menuInactivityTimer;       // Timeout counter (300 frames = ~10 sec)
+	int _lastMenuVariant;           // Last random menu video shown (DAT_00482400)
+	int _menuRepeatDelay;           // Delay for key repeat (DAT_00459ce0)
+	bool _levelUnlocked[16];        // Which levels are available (progress flags)
+
+	// Run the main menu loop - returns when game should start or quit
+	// This is the main entry point called from ScummEngine::go()
+	int runMainMenu();
+
+	// Process menu input (keyboard/mouse) - returns selected item or -1
+	int processMenuInput();
+
+	// Draw menu overlay (selection highlight) on current frame
+	void drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+	// Get random menu video filename (emulates FUN_0041FDC8)
+	Common::String getRandomMenuVideo();
+
+	// Reset menu state for fresh start
+	void resetMenu();
+	// =============================================================
+
 	NutRenderer *_smush_cockpitNut;
 
 	// Font used for HUD score/lives/damage display (SMALFONT.NUT)
@@ -55,6 +103,24 @@ public:
 
 	// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
 	SmushFont *_rebelMsgFont;
+
+	// Menu system fonts (from info.md - FUN_403BD0 font loading)
+	// Low resolution mode font list (stored in DAT_00485058 linked list):
+	//   Font 0 (^f00): TALKFONT.NUT - Default menu font
+	//   Font 1 (^f01): SMALFONT.NUT - Small font (for format code switching)
+	//   Font 2 (^f02): TITLFONT.NUT - Title font
+	//   Font 3 (^f03): POVFONT.NUT - POV font
+	NutRenderer *_smush_talkfontNut;   // Font 0 - primary menu font (DAT_00485058)
+	NutRenderer *_smush_smalfontNut;   // Font 1 - small font for ^f01 switching
+	NutRenderer *_smush_titlefontNut;  // Font 2 - title font
+
+	// SmushFont for menu text rendering (uses SMALFONT.NUT with proper string drawing)
+	SmushFont *_menuFont;
+
+	// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
+	// Loaded into DAT_0047aba8 and rendered via FUN_004236e0 as background
+	NutRenderer *_smush_mouseoverNut;
+
 	bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
 	
 
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index 8077816ca35..d88d0b1f572 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -75,6 +75,8 @@ public:
 	int getCharWidth(byte c) const;
 	int getCharHeight(byte c) const;
 	const byte *getCharData(byte c);
+	byte getCharTransparency(byte c) const { return _chars[c].transparency; }
+	byte getBpp() const { return _bpp; }
 
 	int getFontHeight() const { return _fontHeight; }
 };
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index d29e0da93c1..9f868c127cf 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2675,21 +2675,45 @@ int ScummEngine::getTalkSpeed() {
 Common::Error ScummEngine::go() {
 #ifdef ENABLE_SCUMM_7_8
 	if (_game.id == GID_REBEL2) {
+		ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
+		InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
+
 		// Use 12 FPS as default, same as The Dig. FT uses 10.
 		// Since we don't have the standard scripts initializing this, we pass it here.
 		if (_game.features & GF_DEMO) {
-			((ScummEngine_v7 *)this)->_splayer->play("OPEN/O_DEMO.SAN", 12);
-		} else {
-			// Play Level 1 intro cinematic (01BEG.SAN)
-			// Set video flags to 0x20 to mark this as a non-interactive intro sequence
-			// This flag tells procPostRendering to skip HUD/crosshair rendering
-			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0x20);
-			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01BEG.SAN", 12);
-
-			// After intro completes, start the gameplay mission video (01P01.SAN)
-			// Clear the intro flag so HUD renders during gameplay
-			((ScummEngine_v7 *)this)->_splayer->setCurVideoFlags(0);
-			((ScummEngine_v7 *)this)->_splayer->play("LEV01/01P01.SAN", 12);
+			vm7->_splayer->play("OPEN/O_DEMO.SAN", 12);
+			return Common::kNoError;
+		}
+
+		// Full game: Run the main menu loop
+		// This emulates the retail game flow from FUN_004142BD
+		while (!shouldQuit()) {
+			// Run main menu and get result
+			int menuResult = rebel->runMainMenu();
+
+			if (menuResult == 0 || shouldQuit()) {
+				// Quit selected
+				break;
+			}
+
+			if (menuResult == InsaneRebel2::kMenuNewGame ||
+			    menuResult == InsaneRebel2::kMenuContinue) {
+				// Start gameplay
+				// Play Level 1 intro cinematic (01BEG.SAN)
+				// Set video flags to 0x20 to mark this as a non-interactive intro sequence
+				vm7->_splayer->setCurVideoFlags(0x20);
+				vm7->_splayer->play("LEV01/01BEG.SAN", 12);
+
+				if (shouldQuit()) break;
+
+				// After intro completes, start the gameplay mission video (01P01.SAN)
+				// Clear the intro flag so HUD renders during gameplay
+				vm7->_splayer->setCurVideoFlags(0);
+				vm7->_splayer->play("LEV01/01P01.SAN", 12);
+
+				// After gameplay, return to menu
+				// TODO: Handle level progression, game over, etc.
+			}
 		}
 		return Common::kNoError;
 	}
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index 101e58a9611..4c76060c16b 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -105,6 +105,7 @@ public:
 	void displayDialog() override;
 	bool isSmushActive() override { return _smushActive; }
 	bool isInsaneActive() override { return _insane ? _insane->isInsaneActive() : false; }
+	Insane *getInsane() { return _insane; }
 	void removeBlastTexts() override;
 	void restoreBlastTextsRects();
 
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index a0c3c831bbb..df67aaff161 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -401,7 +401,18 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 	int unknown = b.readSint16LE();
 	int userId = b.readUint16LE();
 
-	if ((code != 8) && (flags != 46)) {
+	debug("SmushPlayer::handleIACT: code=%d flags=%d unknown=%d userId=%d subSize=%d",
+		code, flags, unknown, userId, subSize);
+
+	// Route to procIACT for:
+	// 1. Non-audio IACT (code != 8 or flags != 46)
+	// 2. RA2 menu/graphics IACT (userId >= 1000 indicates graphics data, not audio)
+	bool isAudioIACT = (code == 8) && (flags == 46);
+	bool isRA2GraphicsIACT = (_vm->_game.id == GID_REBEL2) && (userId >= 1000);
+
+	if (!isAudioIACT || isRA2GraphicsIACT) {
+		debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2GraphicsIACT=%d)",
+			isAudioIACT, isRA2GraphicsIACT);
 		_vm->_insane->procIACT(_dst, 0, 0, 0, b, 0, 0, code, flags, unknown, userId);
 		return;
 	}
@@ -1003,8 +1014,15 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
 	}
 
+	// Debug: Check if updateScreen is being called
+	if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
+		debug("SmushPlayer: frame=%d _width=%d _height=%d _dst=%p", _frame, _width, _height, (void*)_dst);
+	}
+
 	if (_width != 0 && _height != 0) {
 		updateScreen();
+	} else if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
+		debug("SmushPlayer: SKIPPING updateScreen (width=%d height=%d)", _width, _height);
 	}
 
 	_frame++;
@@ -1012,6 +1030,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 
 void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleAnimHeader()");
+	debug("SmushPlayer::handleAnimHeader: subSize=%d", subSize);
 	assert(subSize >= 0x300 + 6);
 	byte *headerContent = (byte *)malloc(subSize * sizeof(byte));
 
@@ -1046,6 +1065,17 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 
 		_width = READ_LE_UINT16(&headerContent[4]);
 		_height = READ_LE_UINT16(&headerContent[6]);
+		debug("SmushPlayer::handleAnimHeader: nbframes=%d width=%d height=%d", _nbframes, _width, _height);
+
+		// RA2 menu videos (O_MENU*.SAN) report width/height=0 in AHDR, but they DO have
+		// FOBJ frames with full 320x200 images. The FOBJ chunks contain the correct dimensions.
+		// We set default screen dimensions here so updateScreen() gets called and the
+		// _frameBuffer allocation in handleStore/handleFetch uses correct size.
+		if (_vm->_game.id == GID_REBEL2 && _width == 0 && _height == 0) {
+			_width = _vm->_screenWidth;   // 320
+			_height = _vm->_screenHeight; // 200
+			debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+		}
 
 		free(headerContent);
 	}


Commit: f91918ff34466f60423d8e46e0d1ba8f9b5cf8e7
    https://github.com/scummvm/scummvm/commit/f91918ff34466f60423d8e46e0d1ba8f9b5cf8e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:52+02:00

Commit Message:
SCUMM: RA2: Implement additional menus

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index b6e76998ece..73c49d9c4d4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -24,8 +24,10 @@
 #include "engines/engine.h"
 #include "common/system.h"
 #include "common/memstream.h"
+#include "common/events.h"
 
 #include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
 
 #include "scumm/actor.h"
 #include "scumm/file.h"
@@ -259,10 +261,24 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	for (i = 0; i < 16; i++) {
 		_levelUnlocked[i] = (i == 0);  // Only level 1 unlocked initially
 	}
+
+	// Initialize level selection system
+	_levelSelection = 0;          // First level selected
+	_levelItemCount = 2;          // Level 1 + MAIN MENU (will grow as more levels implemented)
+	_selectedLevel = 1;           // Default selected level
+
+	// Initialize menu input capture system
+	_menuInputActive = false;
+
+	// Register as EventObserver to capture input events before ScummEngine consumes them
+	_vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
 }
 
 
 InsaneRebel2::~InsaneRebel2() {
+	// Unregister EventObserver
+	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
+
 	terminateAudio();
 	delete _rebelMsgFont;
 	delete _menuFont;
@@ -273,6 +289,31 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _smush_mouseoverNut;
 }
 
+bool InsaneRebel2::notifyEvent(const Common::Event &event) {
+	// Capture menu-related input events when menu input is active.
+	// This is called before ScummEngine::parseEvents() consumes events,
+	// so we can reliably capture keyboard/mouse input for menu navigation.
+	if (!_menuInputActive)
+		return false;  // Not capturing, let normal processing occur
+
+	switch (event.type) {
+	case Common::EVENT_KEYDOWN:
+	case Common::EVENT_LBUTTONDOWN:
+	case Common::EVENT_MOUSEMOVE:
+	case Common::EVENT_QUIT:
+	case Common::EVENT_RETURN_TO_LAUNCHER:
+		// Queue these events for processing in processMenuInput()
+		_menuEventQueue.push(event);
+		break;
+	default:
+		break;
+	}
+
+	// Return false to allow ScummEngine to also process the event
+	// (needed for quit handling, etc.)
+	return false;
+}
+
 // Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
 // These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
 // For simplicity, we use fixed values based on difficulty level
@@ -1224,13 +1265,73 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
 	// The original uses colors from the palette, commonly white or yellow for subtitles
 	int16 textColor = 255;
 
-	// Draw the text string
+	// RA2 fonts (like DIHIFONT.NUT) have only 58 characters starting at ASCII 32 (space).
+	// We need to convert ASCII codes to font indices by subtracting 32.
+	// Character mapping: font index = ASCII code - 32
+	// So 'D' (68) becomes index 36, 'A' (65) becomes index 33, etc.
+	// IMPORTANT: Skip format codes (^f00, ^c255, ^l) which TextRenderer parses as raw ASCII.
+	char convertedText[512];
+	int srcLen = strlen(textStr);
+	int dstIdx = 0;
+	int numChars = _rebelMsgFont->getNumChars();
+
+	for (int i = 0; i < srcLen && dstIdx < (int)sizeof(convertedText) - 1; i++) {
+		byte ch = (byte)textStr[i];
+
+		// Check for format codes (^f, ^c, ^l) - keep them as raw ASCII
+		if (ch == '^' && i + 1 < srcLen) {
+			byte next = (byte)textStr[i + 1];
+			if (next == 'f' && i + 3 < srcLen) {
+				// ^fXX - font switch (4 chars total)
+				convertedText[dstIdx++] = textStr[i++];  // ^
+				convertedText[dstIdx++] = textStr[i++];  // f
+				convertedText[dstIdx++] = textStr[i++];  // X
+				convertedText[dstIdx++] = textStr[i];    // X
+				continue;
+			} else if (next == 'c' && i + 4 < srcLen) {
+				// ^cXXX - color switch (5 chars total)
+				convertedText[dstIdx++] = textStr[i++];  // ^
+				convertedText[dstIdx++] = textStr[i++];  // c
+				convertedText[dstIdx++] = textStr[i++];  // X
+				convertedText[dstIdx++] = textStr[i++];  // X
+				convertedText[dstIdx++] = textStr[i];    // X
+				continue;
+			} else if (next == 'l') {
+				// ^l - line break marker (2 chars)
+				convertedText[dstIdx++] = textStr[i++];  // ^
+				convertedText[dstIdx++] = textStr[i];    // l
+				continue;
+			} else if (next == '^') {
+				// ^^ - escaped caret (becomes single ^)
+				i++;  // Skip first ^
+				// Fall through to convert second ^ as normal char
+				ch = '^';
+			}
+		}
+
+		// Convert regular characters from ASCII to font index
+		// First convert lowercase to uppercase (the font likely only has uppercase)
+		if (ch >= 'a' && ch <= 'z') {
+			ch = ch - 'a' + 'A';  // Convert to uppercase
+		}
+
+		if (ch >= 32 && ch < (byte)(32 + numChars)) {
+			convertedText[dstIdx++] = ch - 32;  // Convert ASCII to font index
+		} else if (ch == '\n' || ch == '\r') {
+			convertedText[dstIdx++] = ch;  // Keep control characters as-is
+		} else {
+			convertedText[dstIdx++] = 0;  // Replace invalid characters with space (index 0)
+		}
+	}
+	convertedText[dstIdx] = '\0';
+
+	// Draw the text string (with converted character indices)
 	if (textFlags & 0x04) {
 		// Word-wrapped text
-		_rebelMsgFont->drawStringWrap(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+		_rebelMsgFont->drawStringWrap(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
 	} else {
 		// Single-line text
-		_rebelMsgFont->drawString(textStr, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
+		_rebelMsgFont->drawString(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
 	}
 
 	debug("Rebel2 Opcode 9: Rendered subtitle at (%d,%d) flags=0x%x clip=(%d,%d,%d,%d)",
@@ -1851,31 +1952,40 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	// Check if we're in menu mode (menu state + intro flag)
 	bool menuMode = (introPlaying && _gameState == kStateMainMenu);
+	bool levelSelectMode = (introPlaying && _gameState == kStateLevelSelect);
+
+	// Handle level selection input and rendering
+	if (levelSelectMode) {
+		// Show the standard Windows arrow cursor (same as menu)
+		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+		CursorMan.replaceCursor(cursor);
+		delete cursor;
+		CursorMan.showMouse(true);
+
+		// Process level selection input
+		int selection = processLevelSelectInput();
+
+		// Draw level selection overlay
+		drawLevelSelectOverlay(renderBitmap, pitch, width, height);
+
+		// If a selection was confirmed, signal video to stop
+		if (selection >= 0) {
+			debug("Rebel2: Level selection confirmed: %d", selection);
+			_vm->_smushVideoShouldFinish = true;
+		}
+
+		// Skip normal HUD rendering in level select mode
+		return;
+	}
 
 	// Handle menu input and rendering if in menu mode
 	if (menuMode) {
 		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
 		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
 		// MSTOVER.NUT is a background overlay, NOT a cursor
-		static const byte arrowCursor[11 * 16] = {
-			1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-			1, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0,
-			1, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0,
-			1, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0,
-			1, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0,
-			1, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0,
-			1, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0,
-			1, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0,
-			1, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0,
-			1, 255, 255, 255, 255, 255, 1, 1, 1, 1, 1,
-			1, 255, 255, 1, 255, 255, 1, 0, 0, 0, 0,
-			1, 255, 1, 0, 1, 255, 255, 1, 0, 0, 0,
-			1, 1, 0, 0, 1, 255, 255, 1, 0, 0, 0,
-			1, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
-			0, 0, 0, 0, 0, 1, 255, 255, 1, 0, 0,
-			0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
-		};
-		CursorMan.replaceCursor(arrowCursor, 11, 16, 0, 0, 0);
+		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+		CursorMan.replaceCursor(cursor);
+		delete cursor;
 		CursorMan.showMouse(true);
 
 		// Process menu input during each frame
@@ -2603,18 +2713,22 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
 int InsaneRebel2::processMenuInput() {
 	// Emulates FUN_0041f5ae menu input handling
 	// Returns: -1 = no action, 0-6 = menu item selected
+	//
+	// Events are captured by notifyEvent() (EventObserver) which runs before
+	// ScummEngine::parseEvents() consumes them. This ensures we don't miss
+	// any input events even though we only process them on video frames.
 
-	Common::Event event;
 	int result = -1;
 
-	// Check for key/mouse events
-	Common::EventManager *eventMan = _vm->_system->getEventManager();
-
-	// Get current key state from _vm's VAR system or direct polling
-	// Key codes: 0x148 = Up, 0x150 = Down, 0x1B = ESC, 0x0D = Enter
+	// Menu item Y positions (low-res 320x200 mode):
+	// From assembly: baseY = numItems * -5 + 0x68 = 7 * -5 + 104 = 69
+	// Items at Y = 69, 79, 89, 99, 109, 119 with spacing of 10
+	const int baseY = 69;
+	const int itemHeight = 10;
 
-	// Poll for keyboard input
-	while (eventMan->pollEvent(event)) {
+	// Process events from the queue (populated by notifyEvent)
+	while (!_menuEventQueue.empty()) {
+		Common::Event event = _menuEventQueue.pop();
 		switch (event.type) {
 		case Common::EVENT_KEYDOWN:
 			_menuInactivityTimer = 0;  // Reset inactivity timer on any input
@@ -2622,26 +2736,20 @@ int InsaneRebel2::processMenuInput() {
 			switch (event.kbd.keycode) {
 			case Common::KEYCODE_UP:
 				// Navigate up (wrap around)
-				if (_menuRepeatDelay == 0) {
-					_menuSelection--;
-					if (_menuSelection < 0) {
-						_menuSelection = _menuItemCount - 1;
-					}
-					_menuRepeatDelay = 3;  // Delay before repeat
-					debug("Menu: Selection changed to %d (UP)", _menuSelection);
+				_menuSelection--;
+				if (_menuSelection < 0) {
+					_menuSelection = _menuItemCount - 1;
 				}
+				debug("Menu: Selection changed to %d (UP)", _menuSelection);
 				break;
 
 			case Common::KEYCODE_DOWN:
 				// Navigate down (wrap around)
-				if (_menuRepeatDelay == 0) {
-					_menuSelection++;
-					if (_menuSelection >= _menuItemCount) {
-						_menuSelection = 0;
-					}
-					_menuRepeatDelay = 3;
-					debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
+				_menuSelection++;
+				if (_menuSelection >= _menuItemCount) {
+					_menuSelection = 0;
 				}
+				debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
 				break;
 
 			case Common::KEYCODE_RETURN:
@@ -2655,7 +2763,7 @@ int InsaneRebel2::processMenuInput() {
 
 			case Common::KEYCODE_ESCAPE:
 				// ESC - Exit/Quit (return special value)
-				result = 6;  // Quit option
+				result = 5;  // Quit option (index 5)
 				debug("Menu: ESC pressed - quit");
 				break;
 
@@ -2665,39 +2773,46 @@ int InsaneRebel2::processMenuInput() {
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
-			// Mouse click - check if over a menu item
-			// Menu items are vertically stacked, centered at X=160
-			// Y positions from FUN_41F5AE: param_3 * -5 + local_c * 10 + 0x68
-			// With param_3 = 7: base Y = 69, items at Y = 69, 79, 89, 99, 109, 119
 			_menuInactivityTimer = 0;
+
 			{
+				// Get mouse position from the event
+				int mouseX = event.mouse.x;
 				int mouseY = event.mouse.y;
-				int baseY = 69;  // First item Y position (low-res)
-				int itemHeight = 10;
 
-				// Check which item was clicked
+				debug("Menu: Click detected at (%d, %d)", mouseX, mouseY);
+
+				// Check which item was clicked (larger hit area for better usability)
 				for (int i = 0; i < _menuItemCount; i++) {
 					int itemY = baseY + i * itemHeight;
-					if (mouseY >= itemY - 5 && mouseY <= itemY + 5) {
+					// Use a larger vertical hit area (full item height)
+					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
 						_menuSelection = i;
 						result = i;
-						debug("Menu: Item %d clicked at Y=%d", i, mouseY);
+						debug("Menu: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
 						break;
 					}
 				}
 			}
 			break;
 
+		case Common::EVENT_MOUSEMOVE:
+			// Update mouse position for hover effects (optional)
+			_vm->_mouse.x = event.mouse.x;
+			_vm->_mouse.y = event.mouse.y;
+			break;
+
+		case Common::EVENT_QUIT:
+		case Common::EVENT_RETURN_TO_LAUNCHER:
+			// Handle quit request
+			result = 5;  // Quit option
+			break;
+
 		default:
 			break;
 		}
 	}
 
-	// Handle key repeat delay
-	if (_menuRepeatDelay > 0) {
-		_menuRepeatDelay--;
-	}
-
 	return result;
 }
 
@@ -2876,6 +2991,10 @@ int InsaneRebel2::runMainMenu() {
 	resetMenu();
 	_gameState = kStateMainMenu;
 
+	// Enable menu input capture via EventObserver
+	_menuInputActive = true;
+	while (!_menuEventQueue.empty()) _menuEventQueue.pop();  // Clear any stale events
+
 	// Get the SmushPlayer from ScummEngine_v7
 	// Note: _player isn't set until SmushPlayer::initAudio() is called during playback
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -2900,6 +3019,7 @@ int InsaneRebel2::runMainMenu() {
 
 		// Check for quit
 		if (_vm->shouldQuit()) {
+			_menuInputActive = false;
 			return 0;
 		}
 
@@ -2926,14 +3046,16 @@ int InsaneRebel2::runMainMenu() {
 		// case 5: Credits (play O_CREDIT.SAN, then return 1)
 		// case 6: Quit (stop video, exit)
 		switch (_menuSelection) {
-		case 0:  // New Game
-			debug("Rebel2: New Game selected");
-			_gameState = kStateGameplay;
-			return kMenuNewGame;
+		case 0:  // Start Game -> Level Selection
+			debug("Rebel2: Start Game selected - going to level selection");
+			_gameState = kStateLevelSelect;
+			_menuInputActive = false;
+			return kMenuContinue;  // Go to level selection
 
-		case 1:  // Continue
-			debug("Rebel2: Continue selected");
+		case 1:  // Continue (same as Start Game for now)
+			debug("Rebel2: Continue selected - going to level selection");
 			_gameState = kStateLevelSelect;
+			_menuInputActive = false;
 			return kMenuContinue;
 
 		case 2:  // Options
@@ -2962,6 +3084,7 @@ int InsaneRebel2::runMainMenu() {
 
 		case 6:  // Quit
 			debug("Rebel2: Quit selected");
+			_menuInputActive = false;
 			return 0;
 
 		default:
@@ -2970,7 +3093,330 @@ int InsaneRebel2::runMainMenu() {
 		}
 	}
 
+	_menuInputActive = false;
 	return 0;
 }
 
+// ==================== Level Selection Menu ====================
+// Emulates FUN_00414A41 - Level selection menu
+// For now, only Level 1 is available. This will be expanded later.
+
+int InsaneRebel2::runLevelSelect() {
+	// Level selection menu loop - emulates FUN_00414A41
+	// Returns:
+	//   kLevelSelectPlay (1) = Play selected level
+	//   kLevelSelectBack (0) = Return to main menu
+	//   kLevelSelectQuit (2) = Quit game
+
+	debug("Rebel2: Entering level selection");
+
+	// Enable menu input capture via EventObserver and clear any stale events
+	_menuInputActive = true;
+	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+
+	// Initialize level selection state
+	_levelSelection = 0;
+	_levelItemCount = 2;  // Selectable items: Level 1, MAIN MENU
+	_selectedLevel = 1;   // Default to level 1
+	_menuRepeatDelay = 0;
+	_gameState = kStateLevelSelect;
+
+	// Get the SmushPlayer
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Level selection loop - we'll reuse the menu video as background
+	// In the original, this uses the same O_MENU_X.SAN videos
+	while (!_vm->shouldQuit()) {
+		_vm->_smushVideoShouldFinish = false;
+
+		// Use a menu video as background for level selection
+		Common::String menuVideo = getRandomMenuVideo();
+		debug("Rebel2: Playing level select background: %s", menuVideo.c_str());
+
+		// Set video flags for menu mode
+		splayer->setCurVideoFlags(0x20);
+
+		// Play the menu video - input is processed in procPostRendering
+		splayer->play(menuVideo.c_str(), 12);
+
+		if (_vm->shouldQuit()) {
+			_menuInputActive = false;
+			return kLevelSelectQuit;
+		}
+
+		// If video ended without selection, continue looping
+		if (!_vm->_smushVideoShouldFinish) {
+			continue;
+		}
+
+		_vm->_smushVideoShouldFinish = false;
+
+		debug("Rebel2: Level selection made: %d", _levelSelection);
+
+		// Process level selection
+		// Menu items (based on FUN_414A41 analysis):
+		// 0: Level 1 (CHAPTER 1)
+		// 1: MAIN MENU (back)
+		// Future: More levels, NEW PILOT, DELETE PILOT, etc.
+		switch (_levelSelection) {
+		case 0:  // Level 1
+			debug("Rebel2: Level 1 selected");
+			_selectedLevel = 1;
+			_menuInputActive = false;
+			return kLevelSelectPlay;
+
+		case 1:  // Main Menu (back)
+			debug("Rebel2: Back to main menu selected");
+			_menuInputActive = false;
+			return kLevelSelectBack;
+
+		default:
+			// For now, any other selection defaults to Level 1
+			if (_levelSelection < _levelItemCount - 1) {
+				debug("Rebel2: Level %d selected (defaulting to 1)", _levelSelection + 1);
+				_selectedLevel = 1;  // Only level 1 available for now
+				_menuInputActive = false;
+				return kLevelSelectPlay;
+			}
+			break;
+		}
+	}
+
+	_menuInputActive = false;
+	return kLevelSelectQuit;
+}
+
+int InsaneRebel2::processLevelSelectInput() {
+	// Process input for level selection menu
+	// Similar to processMenuInput but for level selection
+	// Returns: -1 = no action, 0+ = item selected
+	//
+	// Events are captured by notifyEvent() - see processMenuInput for details.
+
+	int result = -1;
+
+	// Level menu Y positions (similar to main menu)
+	// Using same formula: base Y = numItemsTotal * -5 + 104
+	// numItemsTotal = 3 (title + 2 selectable items)
+	const int numItemsTotal = 3;
+	const int baseY = numItemsTotal * -5 + 104;  // 89
+	const int itemHeight = 10;
+
+	// Process events from the queue (populated by notifyEvent)
+	while (!_menuEventQueue.empty()) {
+		Common::Event event = _menuEventQueue.pop();
+		switch (event.type) {
+		case Common::EVENT_KEYDOWN:
+			switch (event.kbd.keycode) {
+			case Common::KEYCODE_UP:
+				_levelSelection--;
+				if (_levelSelection < 0) {
+					_levelSelection = _levelItemCount - 1;
+				}
+				debug("LevelSelect: Selection changed to %d (UP)", _levelSelection);
+				break;
+
+			case Common::KEYCODE_DOWN:
+				_levelSelection++;
+				if (_levelSelection >= _levelItemCount) {
+					_levelSelection = 0;
+				}
+				debug("LevelSelect: Selection changed to %d (DOWN)", _levelSelection);
+				break;
+
+			case Common::KEYCODE_RETURN:
+			case Common::KEYCODE_KP_ENTER:
+				if (_levelSelection >= 0 && _levelSelection < _levelItemCount) {
+					result = _levelSelection;
+					debug("LevelSelect: Item %d selected (ENTER)", _levelSelection);
+				}
+				break;
+
+			case Common::KEYCODE_ESCAPE:
+				// ESC - Back to main menu
+				result = _levelItemCount - 1;  // Last item is "MAIN MENU"
+				debug("LevelSelect: ESC pressed - back to menu");
+				break;
+
+			default:
+				break;
+			}
+			break;
+
+		case Common::EVENT_LBUTTONDOWN:
+			{
+				// Get mouse position from the event
+				int mouseX = event.mouse.x;
+				int mouseY = event.mouse.y;
+
+				debug("LevelSelect: Click detected at (%d, %d)", mouseX, mouseY);
+
+				for (int i = 0; i < _levelItemCount; i++) {
+					int itemY = baseY + i * itemHeight;
+					// Use a larger vertical hit area (full item height)
+					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+						_levelSelection = i;
+						result = i;
+						debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
+						break;
+					}
+				}
+			}
+			break;
+
+		case Common::EVENT_MOUSEMOVE:
+			// Update mouse position for hover effects
+			_vm->_mouse.x = event.mouse.x;
+			_vm->_mouse.y = event.mouse.y;
+			break;
+
+		case Common::EVENT_QUIT:
+		case Common::EVENT_RETURN_TO_LAUNCHER:
+			// Handle quit request - go back to main menu
+			result = _levelItemCount - 1;
+			break;
+
+		default:
+			break;
+		}
+	}
+
+	return result;
+}
+
+void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
+	// Draw level selection menu overlay
+	// Emulates FUN_0041f5ae for level selection mode
+	//
+	// From info.md - Low Resolution Coordinate Formulas:
+	// Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
+	// Item spacing = 10 pixels, Selection box height = 10 pixels
+
+	// Level menu items - for now just Level 1 and Main Menu
+	// In the original game, this would show all unlocked levels plus options
+	static const char *levelItems[] = {
+		"SELECT CHAPTER",    // Title (index 0)
+		"CHAPTER 1",         // Level 1 (index 1) - selectable
+		"MAIN MENU"          // Back to menu (index 2) - selectable
+	};
+
+	const int numItemsTotal = 3;     // Title + 2 selectable items
+	const int numSelectableItems = 2;
+
+	// Calculate positions (low-res 320x200 mode)
+	const int centerX = width / 2;
+	const int titleY = numItemsTotal * -5 + 81;      // 81 - 15 = 66
+	const int itemBaseY = numItemsTotal * -5 + 104;  // 104 - 15 = 89
+	const int itemSpacing = 10;
+
+	NutRenderer *font = _smush_smalfontNut;
+	if (!font) {
+		debug(1, "drawLevelSelectOverlay: font is NULL!");
+		return;
+	}
+
+	int numFontChars = font->getNumChars();
+	int actualPitch = pitch;
+	Common::Rect clipRect(0, 0, width, height);
+
+	// Helper function to draw centered text with optional highlight box
+	// We'll use a simple loop instead of lambda for compatibility
+	auto drawTextCentered = [&](const char *text, int y, bool highlight) {
+		// Calculate text width first
+		int textWidth = 0;
+		for (const char *c = text; *c; c++) {
+			int charIdx = (unsigned char)*c;
+			if (charIdx < numFontChars) {
+				textWidth += font->getCharWidth(charIdx);
+			}
+		}
+
+		int curX = centerX - textWidth / 2;
+
+		// Draw each character using drawCharV7
+		for (const char *c = text; *c; c++) {
+			int charIdx = (unsigned char)*c;
+			if (charIdx < numFontChars) {
+				int charWidth = font->getCharWidth(charIdx);
+				if (curX >= 0 && curX + charWidth <= width && y >= 0) {
+					// Use drawCharV7 with color -1 (original colors), hardcodedColors=true, smushColorMode=true
+					font->drawCharV7(renderBitmap, clipRect, curX, y, actualPitch, -1,
+					                 kStyleAlignLeft, charIdx, true, true);
+				}
+				curX += charWidth;
+			}
+		}
+
+		// If highlighted, draw selection box around text
+		if (highlight) {
+			int boxWidth = textWidth + 12;
+			int boxHeight = 10;
+			int boxX = centerX - boxWidth / 2;
+			int boxY = y - 1;
+
+			// Highlight color (bright color for visibility)
+			byte highlightColor = 0xF0;
+
+			// Draw box border (top, bottom, left, right edges)
+			if (boxY >= 0 && boxY < height && boxX >= 0 && boxX + boxWidth <= width) {
+				// Top edge
+				for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+					if (px >= 0) renderBitmap[boxY * actualPitch + px] = highlightColor;
+				}
+				// Bottom edge
+				int bottomY = boxY + boxHeight - 1;
+				if (bottomY < height) {
+					for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+						if (px >= 0) renderBitmap[bottomY * actualPitch + px] = highlightColor;
+					}
+				}
+				// Left edge
+				for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+					if (py >= 0 && boxX >= 0) renderBitmap[py * actualPitch + boxX] = highlightColor;
+				}
+				// Right edge
+				int rightX = boxX + boxWidth - 1;
+				if (rightX < width) {
+					for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+						if (py >= 0) renderBitmap[py * actualPitch + rightX] = highlightColor;
+					}
+				}
+			}
+		}
+	};
+
+	// Draw title (not selectable)
+	drawTextCentered(levelItems[0], titleY, false);
+
+	// Draw selectable items
+	for (int i = 0; i < numSelectableItems; i++) {
+		int itemY = itemBaseY + i * itemSpacing;
+		bool isSelected = (i == _levelSelection);
+		drawTextCentered(levelItems[i + 1], itemY, isSelected);
+	}
+
+	// Draw info text at bottom if a level is selected (not "MAIN MENU")
+	if (_levelSelection < numSelectableItems - 1) {
+		// Show difficulty or other info
+		// From info.md: Difficulty at X=30, Y=180
+		const char *difficultyText = "DIFFICULTY: EASY";
+		int infoY = 180;
+		int infoX = 30;
+
+		// Draw left-aligned text using drawCharV7
+		int curX = infoX;
+		for (const char *c = difficultyText; *c; c++) {
+			int charIdx = (unsigned char)*c;
+			if (charIdx < numFontChars) {
+				int charWidth = font->getCharWidth(charIdx);
+				if (curX >= 0 && curX + charWidth <= width && infoY >= 0) {
+					font->drawCharV7(renderBitmap, clipRect, curX, infoY, actualPitch, -1,
+					                 kStyleAlignLeft, charIdx, true, true);
+				}
+				curX += charWidth;
+			}
+		}
+	}
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bf4ffa094cb..9434645d484 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -28,8 +28,11 @@
 
 #include "scumm/insane/insane.h"
 
+#include "common/keyboard.h"
 #include "common/list.h"
 #include "common/rect.h"
+#include "common/events.h"
+#include "common/queue.h"
 
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
@@ -41,12 +44,19 @@ class SoundHandle;
 
 namespace Scumm {
 
-class InsaneRebel2 : public Insane {
+class InsaneRebel2 : public Insane, public Common::EventObserver {
 
 public:
 	InsaneRebel2(ScummEngine_v7 *scumm);
 	~InsaneRebel2();
 
+	// EventObserver interface - captures events before ScummEngine consumes them
+	bool notifyEvent(const Common::Event &event) override;
+
+	// Menu input event queue - events are captured by notifyEvent() and processed by processMenuInput()
+	Common::Queue<Common::Event> _menuEventQueue;
+	bool _menuInputActive;  // True when we're capturing menu input events
+
 	// ======================= Menu System =======================
 	// Main game states (emulates retail state machine from FUN_004142BD)
 	enum GameState {
@@ -93,6 +103,27 @@ public:
 
 	// Reset menu state for fresh start
 	void resetMenu();
+
+	// ================= Level Selection Menu ====================
+	// Level selection results
+	enum LevelSelectResult {
+		kLevelSelectBack = 0,     // Return to main menu
+		kLevelSelectPlay = 1,     // Play selected level
+		kLevelSelectQuit = 2      // Quit game
+	};
+
+	int _levelSelection;          // Current level selection (0-based)
+	int _levelItemCount;          // Number of level items (levels + options)
+	int _selectedLevel;           // Final selected level ID (1-15)
+
+	// Run level selection menu - returns LevelSelectResult
+	int runLevelSelect();
+
+	// Draw level selection overlay
+	void drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+	// Process level select input - returns -1 (no action) or action code
+	int processLevelSelectInput();
 	// =============================================================
 
 	NutRenderer *_smush_cockpitNut;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 9f868c127cf..61ec421a515 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2696,23 +2696,33 @@ Common::Error ScummEngine::go() {
 				break;
 			}
 
-			if (menuResult == InsaneRebel2::kMenuNewGame ||
-			    menuResult == InsaneRebel2::kMenuContinue) {
-				// Start gameplay
-				// Play Level 1 intro cinematic (01BEG.SAN)
-				// Set video flags to 0x20 to mark this as a non-interactive intro sequence
-				vm7->_splayer->setCurVideoFlags(0x20);
-				vm7->_splayer->play("LEV01/01BEG.SAN", 12);
-
-				if (shouldQuit()) break;
-
-				// After intro completes, start the gameplay mission video (01P01.SAN)
-				// Clear the intro flag so HUD renders during gameplay
-				vm7->_splayer->setCurVideoFlags(0);
-				vm7->_splayer->play("LEV01/01P01.SAN", 12);
-
-				// After gameplay, return to menu
-				// TODO: Handle level progression, game over, etc.
+			if (menuResult == InsaneRebel2::kMenuContinue) {
+				// Continue: Show level selection menu
+				int levelResult = rebel->runLevelSelect();
+
+				if (levelResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
+					break;
+				}
+
+				if (levelResult == InsaneRebel2::kLevelSelectPlay) {
+					// Play the selected level (currently only level 1 is supported)
+					int selectedLevel = rebel->_selectedLevel;
+					debug("ScummEngine: Starting level %d", selectedLevel);
+
+					// For now, only level 1 is implemented
+					// Play Level 1 intro cinematic
+					vm7->_splayer->setCurVideoFlags(0x20);
+					vm7->_splayer->play("LEV01/01BEG.SAN", 12);
+
+					if (shouldQuit()) break;
+
+					// Start gameplay
+					vm7->_splayer->setCurVideoFlags(0);
+					vm7->_splayer->play("LEV01/01P01.SAN", 12);
+
+					// After gameplay, return to menu
+				}
+				// If kLevelSelectBack, loop back to main menu
 			}
 		}
 		return Common::kNoError;


Commit: 14884b6d742f2f542ff0b9d1bc86d004efa08a59
    https://github.com/scummvm/scummvm/commit/14884b6d742f2f542ff0b9d1bc86d004efa08a59
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00

Commit Message:
SCUMM: RA2: Implement level loading

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 73c49d9c4d4..48f4988d902 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -270,6 +270,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Initialize menu input capture system
 	_menuInputActive = false;
 
+	// Initialize level state tracking for multi-phase levels
+	_currentPhase = 1;
+	_deathFrame = 0;
+	_phaseScore = 0;
+	_phaseMisses = 0;
+
 	// Register as EventObserver to capture input events before ScummEngine consumes them
 	_vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
 }
@@ -3116,7 +3122,7 @@ int InsaneRebel2::runLevelSelect() {
 
 	// Initialize level selection state
 	_levelSelection = 0;
-	_levelItemCount = 2;  // Selectable items: Level 1, MAIN MENU
+	_levelItemCount = 7;  // Selectable items: 6 levels + MAIN MENU
 	_selectedLevel = 1;   // Default to level 1
 	_menuRepeatDelay = 0;
 	_gameState = kStateLevelSelect;
@@ -3154,31 +3160,20 @@ int InsaneRebel2::runLevelSelect() {
 		debug("Rebel2: Level selection made: %d", _levelSelection);
 
 		// Process level selection
-		// Menu items (based on FUN_414A41 analysis):
-		// 0: Level 1 (CHAPTER 1)
-		// 1: MAIN MENU (back)
-		// Future: More levels, NEW PILOT, DELETE PILOT, etc.
-		switch (_levelSelection) {
-		case 0:  // Level 1
-			debug("Rebel2: Level 1 selected");
-			_selectedLevel = 1;
+		// Menu items:
+		// 0-5: Levels 1-6 (CHAPTER 1-6)
+		// 6: MAIN MENU (back)
+		if (_levelSelection >= 0 && _levelSelection <= 5) {
+			// Level selected (0 = Level 1, 5 = Level 6)
+			_selectedLevel = _levelSelection + 1;
+			debug("Rebel2: Level %d selected", _selectedLevel);
 			_menuInputActive = false;
 			return kLevelSelectPlay;
-
-		case 1:  // Main Menu (back)
+		} else if (_levelSelection == 6) {
+			// Main Menu (back)
 			debug("Rebel2: Back to main menu selected");
 			_menuInputActive = false;
 			return kLevelSelectBack;
-
-		default:
-			// For now, any other selection defaults to Level 1
-			if (_levelSelection < _levelItemCount - 1) {
-				debug("Rebel2: Level %d selected (defaulting to 1)", _levelSelection + 1);
-				_selectedLevel = 1;  // Only level 1 available for now
-				_menuInputActive = false;
-				return kLevelSelectPlay;
-			}
-			break;
 		}
 	}
 
@@ -3292,16 +3287,21 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	// Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
 	// Item spacing = 10 pixels, Selection box height = 10 pixels
 
-	// Level menu items - for now just Level 1 and Main Menu
+	// Level menu items - all 6 levels plus Main Menu
 	// In the original game, this would show all unlocked levels plus options
 	static const char *levelItems[] = {
 		"SELECT CHAPTER",    // Title (index 0)
 		"CHAPTER 1",         // Level 1 (index 1) - selectable
-		"MAIN MENU"          // Back to menu (index 2) - selectable
+		"CHAPTER 2",         // Level 2 (index 2) - selectable
+		"CHAPTER 3",         // Level 3 (index 3) - selectable
+		"CHAPTER 4",         // Level 4 (index 4) - selectable
+		"CHAPTER 5",         // Level 5 (index 5) - selectable
+		"CHAPTER 6",         // Level 6 (index 6) - selectable
+		"MAIN MENU"          // Back to menu (index 7) - selectable
 	};
 
-	const int numItemsTotal = 3;     // Title + 2 selectable items
-	const int numSelectableItems = 2;
+	const int numItemsTotal = 8;     // Title + 7 selectable items
+	const int numSelectableItems = 7;
 
 	// Calculate positions (low-res 320x200 mode)
 	const int centerX = width / 2;
@@ -3419,4 +3419,987 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	}
 }
 
+// ======================= Level Loading System =======================
+// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8
+// Based on disassembly analysis of the retail Rebel Assault 2 executable.
+
+Common::String InsaneRebel2::getLevelDir(int levelId) {
+	// Returns directory name like "LEV01" for level 1
+	return Common::String::format("LEV%02d", levelId);
+}
+
+Common::String InsaneRebel2::getLevelPrefix(int levelId) {
+	// Returns file prefix like "01" for level 1
+	return Common::String::format("%02d", levelId);
+}
+
+void InsaneRebel2::playIntroSequence() {
+	// Emulates case 0 in FUN_004142BD
+	// Plays the game intro sequence:
+	// 1. CREDITS/O_OPEN_C.SAN - Fox logo (if certain conditions)
+	// 2. CREDITS/O_OPEN_D.SAN - LucasArts logo (if certain conditions)
+	// 3. OPEN/O_OPEN_A.SAN - Main intro
+	// 4. OPEN/O_OPEN_B.SAN - Additional intro (if conditions)
+
+	debug("Rebel2: Playing intro sequence");
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Set intro flags (non-interactive)
+	splayer->setCurVideoFlags(0x20);
+
+	// Play Fox logo (CREDITS/O_OPEN_C.SAN)
+	// In retail, this checks if 'f', 'o', 'x' keys are held (easter egg)
+	// We'll play it unconditionally for now
+	debug("Rebel2: Playing Fox logo");
+	splayer->play("CREDITS/O_OPEN_C.SAN", 12);
+
+	if (_vm->shouldQuit()) return;
+
+	// Play LucasArts logo (CREDITS/O_OPEN_D.SAN)
+	// In retail, this checks if 'b', 'o', 't' keys are held
+	debug("Rebel2: Playing LucasArts logo");
+	splayer->play("CREDITS/O_OPEN_D.SAN", 12);
+
+	if (_vm->shouldQuit()) return;
+
+	// Play main intro (OPEN/O_OPEN_A.SAN)
+	debug("Rebel2: Playing main intro");
+	splayer->play("OPEN/O_OPEN_A.SAN", 12);
+
+	if (_vm->shouldQuit()) return;
+
+	// Play additional intro (OPEN/O_OPEN_B.SAN)
+	// In retail, this plays if DAT_0047ab45 or DAT_0047ab47 != 0
+	debug("Rebel2: Playing additional intro");
+	splayer->play("OPEN/O_OPEN_B.SAN", 12);
+}
+
+void InsaneRebel2::playMissionBriefing() {
+	// Emulates FUN_00415CF8 (partial - just the video)
+	// Plays OPEN/O_LEVEL.SAN which shows the mission briefing screen
+
+	debug("Rebel2: Playing mission briefing");
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x08);  // Briefing mode flag
+	splayer->play("OPEN/O_LEVEL.SAN", 12);
+}
+
+void InsaneRebel2::playLevelBegin(int levelId) {
+	// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
+	// Emulates FUN_004171c5 call in each level handler
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
+
+	debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);  // Cinematic mode
+	splayer->play(filename.c_str(), 12);
+}
+
+bool InsaneRebel2::playLevelGameplay(int levelId) {
+	// Play the main gameplay video(s) for a level
+	// Returns true if level completed (damage < 0xff), false if died
+	//
+	// Different levels have different gameplay structures:
+	// - Level 1, 4, 5: Single gameplay SAN (XXPXX.SAN or XXPLAY.SAN)
+	// - Level 2: Multiple parts with subdirectories (P1/, P2/, P3/)
+	// - Level 3, 6: Two gameplay phases (XXPLAY1.SAN, XXPLAY2.SAN)
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String filename;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Set gameplay flags (interactive with HUD)
+	splayer->setCurVideoFlags(0x28);
+
+	// Reset damage/shield for this level
+	_playerShield = 255;
+	_rebelHandler = 0;
+
+	debug("Rebel2: Starting gameplay for level %d", levelId);
+
+	switch (levelId) {
+	case 1:
+		// Level 1: Single gameplay file (01P01.SAN)
+		filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+
+	case 2:
+		// Level 2: Has cutscene first, then multiple parts
+		// First play the cutscene
+		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing cutscene %s", filename.c_str());
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Part 1 (multiple variations - play A for now)
+		splayer->setCurVideoFlags(0x28);
+		filename = Common::String::format("%s/P1/%sP01_A.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Post segment 1
+		filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Part 2
+		splayer->setCurVideoFlags(0x28);
+		filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Post segment 2
+		filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Part 3
+		splayer->setCurVideoFlags(0x28);
+		filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+
+	case 3:
+		// Level 3: Two gameplay phases
+		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Post segment
+		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Phase 2
+		splayer->setCurVideoFlags(0x28);
+		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+
+	case 4:
+		// Level 4: Has cutscene, then single gameplay
+		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing cutscene %s", filename.c_str());
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit()) return false;
+
+		splayer->setCurVideoFlags(0x28);
+		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+
+	case 5:
+		// Level 5: Single gameplay file
+		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+
+	case 6:
+		// Level 6: Two gameplay phases
+		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Post segment
+		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(filename.c_str(), 12);
+
+		if (_vm->shouldQuit() || _playerShield == 0) return false;
+
+		// Phase 2
+		splayer->setCurVideoFlags(0x28);
+		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Playing %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+
+	default:
+		// For levels 7-15 (not in demo), try common patterns
+		// First try XXPLAY.SAN
+		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
+		debug("Rebel2: Trying %s", filename.c_str());
+		splayer->play(filename.c_str(), 12);
+		break;
+	}
+
+	// Return true if player survived (shield > 0), false if died
+	return (_playerShield > 0);
+}
+
+void InsaneRebel2::playLevelEnd(int levelId) {
+	// Play level completion video (LEVXX/XXEND.SAN)
+	// Emulates FUN_00417327 call
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
+
+	debug("Rebel2: Playing level %d end: %s", levelId, filename.c_str());
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);  // Cinematic mode
+	splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelDeath(int levelId) {
+	// Play death video (LEVXX/XXDIE_X.SAN)
+	// The variant depends on the frame where player died
+	// For simplicity, we'll play the A variant
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+
+	// Most levels have DIE_A, some have just DIE
+	Common::String filename;
+	if (levelId == 2 || levelId == 4) {
+		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
+	} else {
+		filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
+	}
+
+	debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);
+	splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelRetry(int levelId) {
+	// Play retry prompt video (LEVXX/XXRETRY.SAN)
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
+
+	debug("Rebel2: Playing level %d retry: %s", levelId, filename.c_str());
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);
+	splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelGameOver(int levelId) {
+	// Play game over video (LEVXX/XXOVER.SAN)
+	// Emulates FUN_00417ab2 call
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String filename = Common::String::format("%s/%sOVER.SAN", dir.c_str(), prefix.c_str());
+
+	debug("Rebel2: Playing level %d game over: %s", levelId, filename.c_str());
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);
+	splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playCreditsSequence() {
+	// Play the end credits (OPEN/O_CREDIT.SAN)
+	// Individual credits are in CREDITS/CRED_XX.SAN
+
+	debug("Rebel2: Playing credits");
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);
+	splayer->play("OPEN/O_CREDIT.SAN", 12);
+}
+
+int InsaneRebel2::runLevel(int levelId) {
+	// Main level dispatcher - calls per-level handlers
+	// Each level handler emulates its retail counterpart (FUN_00417E53 etc.)
+
+	debug("Rebel2: Starting level %d", levelId);
+
+	// Validate level ID
+	if (levelId < 1 || levelId > 15) {
+		warning("Rebel2: Invalid level ID %d", levelId);
+		return kLevelReturnToMenu;
+	}
+
+	// Set the current level
+	_selectedLevel = levelId;
+
+	// Initialize common player state
+	_playerLives = 3;
+	_playerShield = 255;
+	_playerScore = 0;
+	_playerDamage = 0;
+	_currentPhase = 1;
+	_phaseScore = 0;
+	_phaseMisses = 0;
+
+	// Dispatch to per-level handler
+	switch (levelId) {
+	case 1:
+		return runLevel1();
+	case 2:
+		return runLevel2();
+	case 3:
+		return runLevel3();
+	case 4:
+		return runLevel4();
+	case 5:
+		return runLevel5();
+	case 6:
+		return runLevel6();
+	default:
+		// Levels 7-15: Use generic handler (similar to Level 1)
+		return runLevel1();
+	}
+}
+
+// =============================================================================
+// Helper functions
+// =============================================================================
+
+int InsaneRebel2::getRandomVariant(int max) {
+	// Emulates FUN_004233a0 - returns random number 0 to max-1
+	return _vm->_rnd.getRandomNumber(max - 1);
+}
+
+Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int frame) {
+	// Select death video variant based on level, phase, and death frame
+	// Emulates the frame-based death video selection in retail level handlers
+	//
+	// Returns variant suffix: "A", "B", "C", etc.
+
+	switch (levelId) {
+	case 1:
+		// Level 1: Random between A and B
+		return (getRandomVariant(2) == 0) ? "A" : "B";
+
+	case 2:
+		// Level 2: Just "DIE" (no variants)
+		return "";
+
+	case 3:
+		// Level 3: Based on death frame and phase
+		if (phase == 1) {
+			// Phase 1 death video selection (from FUN_0041885F lines 80-96)
+			if (frame < 0x10c) return "B";       // < 268
+			if (frame < 0x1a9) return "A";       // < 425
+			if (frame < 0x247) return "C";       // < 583
+			if (frame < 700) return "A";
+			if (frame < 900) return "B";
+			return "A";
+		} else {
+			// Phase 2 death video selection (from FUN_0041885F lines 53-67)
+			if (frame < 0x2f1) return "A";       // < 753
+			if (frame < 0x347) return "B";       // < 839
+			if (frame < 0x3b1) return "C";       // < 945
+			if (frame < 0x405) return "A";       // < 1029
+			return "C";
+		}
+
+	case 4:
+		// Level 4: Just "DIE" (no variants)
+		return "";
+
+	case 5:
+		// Level 5: Random between A and B (like Level 1)
+		return (getRandomVariant(2) == 0) ? "A" : "B";
+
+	case 6:
+		// Level 6: Similar to Level 3 (two phases with frame-based selection)
+		if (phase == 1) {
+			if (frame < 300) return "A";
+			if (frame < 600) return "B";
+			return "C";
+		} else {
+			if (frame < 400) return "A";
+			if (frame < 800) return "B";
+			return "C";
+		}
+
+	default:
+		return "A";
+	}
+}
+
+void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
+	// Play death video with proper variant selection
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String variant = selectDeathVideoVariant(levelId, phase, frame);
+	Common::String filename;
+
+	if (variant.empty()) {
+		// No variant suffix (Level 2, 4)
+		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
+	} else {
+		filename = Common::String::format("%s/%sDIE_%s.SAN", dir.c_str(), prefix.c_str(), variant.c_str());
+	}
+
+	debug("Rebel2: Playing death video: %s (phase=%d, frame=%d)", filename.c_str(), phase, frame);
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);
+	splayer->play(filename.c_str(), 12);
+}
+
+void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
+	// Play retry video - phase-specific for multi-phase levels
+
+	Common::String dir = getLevelDir(levelId);
+	Common::String prefix = getLevelPrefix(levelId);
+	Common::String filename;
+
+	if (levelId == 3 && phase == 2) {
+		// Level 3 phase 2 has its own retry video: 03RETRYB.SAN
+		filename = Common::String::format("%s/%sRETRYB.SAN", dir.c_str(), prefix.c_str());
+	} else {
+		// Standard retry video
+		filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
+	}
+
+	debug("Rebel2: Playing retry video: %s (phase=%d)", filename.c_str(), phase);
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);
+	splayer->play(filename.c_str(), 12);
+}
+
+// =============================================================================
+// Level 1 Handler - FUN_00417E53
+// Single gameplay phase (01P01.SAN)
+// =============================================================================
+
+int InsaneRebel2::runLevel1() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Play level beginning cinematic (01BEG.SAN)
+	playLevelBegin(1);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Main gameplay retry loop
+	while (!_vm->shouldQuit()) {
+		// Reset shield for this attempt
+		_playerShield = 255;
+		_playerDamage = 0;
+		_deathFrame = 0;
+
+		// Play gameplay (01P01.SAN with 0x28 flags)
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV01/01P01.SAN", 12);
+
+		// Store death frame for video selection
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			// Level completed!
+			debug("Rebel2: Level 1 completed!");
+			playLevelEnd(1);
+			_levelUnlocked[1] = true;  // Unlock level 2
+			return kLevelNextLevel;
+		}
+
+		// Player died - play death video with random A/B variant
+		debug("Rebel2: Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
+		playLevelDeathVariant(1, 1, _deathFrame);
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(1);
+			return kLevelGameOver;
+		}
+
+		// Play retry prompt and loop
+		playLevelRetry(1);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 2 Handler - FUN_00418063
+// Multiple parts with P1/P2/P3 subdirectories
+// Random animation variants for each part
+// =============================================================================
+
+int InsaneRebel2::runLevel2() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	int bonusCount = 0;  // Tracks special events (local_1c in assembly)
+
+	// Play cutscene (02CUT.SAN)
+	splayer->setCurVideoFlags(0x20);
+	splayer->play("LEV02/02CUT.SAN", 12);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (02BEG.SAN)
+	playLevelBegin(2);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Main gameplay retry loop (restarts from beginning on death)
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+		bonusCount = 0;
+
+		// ===== PHASE 1: P1/02P01_X.SAN =====
+		// Select random variant (B, C, or D - not A in main loop)
+		{
+			int variant = getRandomVariant(3);
+			const char *variants[] = {"B", "C", "D"};
+			Common::String filename = Common::String::format("LEV02/P1/02P01_%s.SAN", variants[variant]);
+
+			debug("Rebel2: Level 2 Phase 1 - playing %s", filename.c_str());
+			splayer->setCurVideoFlags(0x28);
+			splayer->play(filename.c_str(), 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield == 0) {
+			// Died in phase 1
+			debug("Rebel2: Level 2 Phase 1 death");
+			playLevelDeathVariant(2, 1, _deathFrame);
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			_playerLives--;
+			if (_playerLives <= 0) {
+				playLevelGameOver(2);
+				return kLevelGameOver;
+			}
+			playLevelRetry(2);
+			if (_vm->shouldQuit()) return kLevelQuit;
+			continue;  // Restart from beginning
+		}
+
+		// Post segment 1 (02PST1.SAN)
+		splayer->setCurVideoFlags(0x20);
+		splayer->play("LEV02/02PST1.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_currentPhase = 2;
+
+		// ===== PHASE 2: P2/02P02_X.SAN =====
+		// Variant selection based on switch (more complex in original)
+		{
+			int variant = getRandomVariant(6);
+			const char *variants[] = {"A", "B", "C", "D", "E", "F"};
+			Common::String filename = Common::String::format("LEV02/P2/02P02_%s.SAN", variants[variant]);
+
+			debug("Rebel2: Level 2 Phase 2 - playing %s", filename.c_str());
+			splayer->setCurVideoFlags(0x28);
+			splayer->play(filename.c_str(), 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield == 0) {
+			// Died in phase 2
+			debug("Rebel2: Level 2 Phase 2 death");
+			playLevelDeathVariant(2, 2, _deathFrame);
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			_playerLives--;
+			if (_playerLives <= 0) {
+				playLevelGameOver(2);
+				return kLevelGameOver;
+			}
+			playLevelRetry(2);
+			if (_vm->shouldQuit()) return kLevelQuit;
+			continue;  // Restart from beginning
+		}
+
+		// Post segment 2 (02PST2.SAN)
+		splayer->setCurVideoFlags(0x20);
+		splayer->play("LEV02/02PST2.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_currentPhase = 3;
+
+		// ===== PHASE 3: P3/02P03_X.SAN =====
+		// Variant selection with more options (A through I)
+		{
+			int variant = getRandomVariant(9);
+			const char *variants[] = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
+			Common::String filename = Common::String::format("LEV02/P3/02P03_%s.SAN", variants[variant]);
+
+			debug("Rebel2: Level 2 Phase 3 - playing %s", filename.c_str());
+			splayer->setCurVideoFlags(0x28);
+			splayer->play(filename.c_str(), 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield == 0) {
+			// Died in phase 3
+			debug("Rebel2: Level 2 Phase 3 death");
+			playLevelDeathVariant(2, 3, _deathFrame);
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			_playerLives--;
+			if (_playerLives <= 0) {
+				playLevelGameOver(2);
+				return kLevelGameOver;
+			}
+			playLevelRetry(2);
+			if (_vm->shouldQuit()) return kLevelQuit;
+			continue;  // Restart from beginning
+		}
+
+		// Level completed!
+		debug("Rebel2: Level 2 completed! bonusCount=%d", bonusCount);
+		playLevelEnd(2);
+		_levelUnlocked[2] = true;  // Unlock level 3
+		return kLevelNextLevel;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 3 Handler - FUN_0041885F
+// Two phases with per-phase retry handling
+// Phase 1: 03PLAY1.SAN, Phase 2: 03PLAY2.SAN
+// =============================================================================
+
+int InsaneRebel2::runLevel3() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	int phase1Score = 0;  // Score preserved across phase 2 retries
+
+	// Play level beginning cinematic (03BEG.SAN)
+	playLevelBegin(3);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// ===== PHASE 1 retry loop =====
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+
+		// Play phase 1 gameplay (03PLAY1.SAN)
+		debug("Rebel2: Level 3 Phase 1");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV03/03PLAY1.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			// Phase 1 completed - save score and proceed to phase 2
+			phase1Score = _playerScore;
+			break;
+		}
+
+		// Died in phase 1 - frame-based death video
+		debug("Rebel2: Level 3 Phase 1 death at frame %d", _deathFrame);
+		playLevelDeathVariant(3, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(3);
+			return kLevelGameOver;
+		}
+
+		// Phase 1 retry (03RETRY.SAN)
+		playLevelRetryVariant(3, 1);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Post segment 1 (03POST1.SAN)
+	splayer->setCurVideoFlags(0x20);
+	splayer->play("LEV03/03POST1.SAN", 12);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// ===== PHASE 2 retry loop (preserves phase 1 score) =====
+	_currentPhase = 2;
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+
+		// Play phase 2 gameplay (03PLAY2.SAN)
+		debug("Rebel2: Level 3 Phase 2");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV03/03PLAY2.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			// Level completed!
+			debug("Rebel2: Level 3 completed!");
+			playLevelEnd(3);
+			_levelUnlocked[3] = true;  // Unlock level 4
+			return kLevelNextLevel;
+		}
+
+		// Died in phase 2 - frame-based death video
+		debug("Rebel2: Level 3 Phase 2 death at frame %d", _deathFrame);
+		playLevelDeathVariant(3, 2, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			// Use phase 2 specific game over (03OVER.SAN, same file but at different point)
+			playLevelGameOver(3);
+			return kLevelGameOver;
+		}
+
+		// Phase 2 retry (03RETRYB.SAN)
+		playLevelRetryVariant(3, 2);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 4 Handler
+// Cutscene + single gameplay phase
+// =============================================================================
+
+int InsaneRebel2::runLevel4() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Play cutscene (04CUT.SAN)
+	splayer->setCurVideoFlags(0x20);
+	splayer->play("LEV04/04CUT.SAN", 12);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (04BEG.SAN)
+	playLevelBegin(4);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Main gameplay retry loop
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+
+		// Play gameplay (04PLAY.SAN)
+		debug("Rebel2: Level 4 gameplay");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV04/04PLAY.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			// Level completed!
+			debug("Rebel2: Level 4 completed!");
+			playLevelEnd(4);
+			_levelUnlocked[4] = true;  // Unlock level 5
+			return kLevelNextLevel;
+		}
+
+		// Died - play death video (04DIE.SAN, no variants)
+		debug("Rebel2: Level 4 death");
+		playLevelDeathVariant(4, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(4);
+			return kLevelGameOver;
+		}
+
+		playLevelRetry(4);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 5 Handler - FUN_00418EC6
+// Single gameplay phase (05PLAY.SAN)
+// Random A/B death video like Level 1
+// =============================================================================
+
+int InsaneRebel2::runLevel5() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Play level beginning cinematic (05BEG.SAN)
+	playLevelBegin(5);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Main gameplay retry loop
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+
+		// Play gameplay (05PLAY.SAN)
+		debug("Rebel2: Level 5 gameplay");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV05/05PLAY.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			// Level completed!
+			debug("Rebel2: Level 5 completed!");
+			playLevelEnd(5);
+			_levelUnlocked[5] = true;  // Unlock level 6
+			return kLevelNextLevel;
+		}
+
+		// Died - play death video with random A/B variant
+		debug("Rebel2: Level 5 death at frame %d", _deathFrame);
+		playLevelDeathVariant(5, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(5);
+			return kLevelGameOver;
+		}
+
+		playLevelRetry(5);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 6 Handler - FUN_00419317
+// Two phases with per-phase retry (like Level 3)
+// Phase 1: 06PLAY1.SAN, Phase 2: 06PLAY2.SAN
+// =============================================================================
+
+int InsaneRebel2::runLevel6() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	int phase1Score = 0;
+
+	// Play level beginning cinematic (06BEG.SAN)
+	playLevelBegin(6);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// ===== PHASE 1 retry loop =====
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+
+		// Play phase 1 gameplay (06PLAY1.SAN)
+		debug("Rebel2: Level 6 Phase 1");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV06/06PLAY1.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			phase1Score = _playerScore;
+			break;
+		}
+
+		// Died in phase 1
+		debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
+		playLevelDeathVariant(6, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(6);
+			return kLevelGameOver;
+		}
+
+		playLevelRetryVariant(6, 1);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Post segment 1 (06POST1.SAN)
+	splayer->setCurVideoFlags(0x20);
+	splayer->play("LEV06/06POST1.SAN", 12);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// ===== PHASE 2 retry loop =====
+	_currentPhase = 2;
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+
+		// Play phase 2 gameplay (06PLAY2.SAN)
+		debug("Rebel2: Level 6 Phase 2");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV06/06PLAY2.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			// Level completed!
+			debug("Rebel2: Level 6 completed!");
+			playLevelEnd(6);
+			_levelUnlocked[6] = true;  // Unlock level 7
+			return kLevelNextLevel;
+		}
+
+		// Died in phase 2
+		debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
+		playLevelDeathVariant(6, 2, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(6);
+			return kLevelGameOver;
+		}
+
+		playLevelRetryVariant(6, 2);
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 9434645d484..bc73cb4882b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -124,6 +124,90 @@ public:
 
 	// Process level select input - returns -1 (no action) or action code
 	int processLevelSelectInput();
+
+	// ======================= Level Loading System =======================
+	// Emulates the level handler functions FUN_00417E53 through FUN_0041BBE8
+	// Each level has: BEG (intro), gameplay SANs, END (completion), DIE variants,
+	// RETRY, and OVER (game over) videos.
+
+	// Level playback result (returned by runLevel)
+	enum LevelResult {
+		kLevelCompleted = 0,      // Level completed successfully
+		kLevelNextLevel = 1,      // Proceed to next level
+		kLevelGameOver = 2,       // No lives remaining
+		kLevelQuit = 3,           // Player quit
+		kLevelReturnToMenu = 4    // Return to main menu
+	};
+
+	// Play the intro sequence (CREDITS/O_OPEN_C, O_OPEN_D, OPEN/O_OPEN_A, O_OPEN_B)
+	// Emulates case 0 in FUN_004142BD
+	void playIntroSequence();
+
+	// Play the mission briefing video (OPEN/O_LEVEL.SAN)
+	// Emulates FUN_00415CF8
+	void playMissionBriefing();
+
+	// Main level runner - plays a complete level by ID (1-15)
+	// Handles all videos: BEG, gameplay, END/DIE/RETRY/OVER
+	// Returns LevelResult
+	int runLevel(int levelId);
+
+	// Play level beginning cinematic (LEVXX/XXBEG.SAN)
+	void playLevelBegin(int levelId);
+
+	// Play main gameplay video(s) for a level
+	// Returns true if level completed, false if player died
+	bool playLevelGameplay(int levelId);
+
+	// Play level completion video (LEVXX/XXEND.SAN)
+	void playLevelEnd(int levelId);
+
+	// Play death video (LEVXX/XXDIE_X.SAN) - variant based on frame/location
+	void playLevelDeath(int levelId);
+
+	// Play retry prompt video (LEVXX/XXRETRY.SAN)
+	void playLevelRetry(int levelId);
+
+	// Play game over video (LEVXX/XXOVER.SAN)
+	void playLevelGameOver(int levelId);
+
+	// Play the ending/credits sequence
+	void playCreditsSequence();
+
+	// Get the directory name for a level (e.g., "LEV01" for level 1)
+	Common::String getLevelDir(int levelId);
+
+	// Get the file prefix for a level (e.g., "01" for level 1)
+	Common::String getLevelPrefix(int levelId);
+
+	// Per-level handlers (emulate FUN_00417E53 through FUN_0041BBE8)
+	// These implement the complete level logic including retry handling
+	int runLevel1();   // FUN_00417E53 - Single gameplay phase
+	int runLevel2();   // FUN_00418063 - Multiple parts with P1/P2/P3 subdirs
+	int runLevel3();   // FUN_0041885F - Two phases with per-phase retry
+	int runLevel4();   // Cutscene + single gameplay
+	int runLevel5();   // FUN_00418EC6 - Single gameplay phase
+	int runLevel6();   // FUN_00419317 - Two phases with per-phase retry
+
+	// Random number helper (emulates FUN_004233a0)
+	int getRandomVariant(int max);
+
+	// Select death video variant based on level, phase, and frame
+	// Returns suffix like "A", "B", "C" for DIE_X.SAN
+	Common::String selectDeathVideoVariant(int levelId, int phase, int frame);
+
+	// Play death video with proper variant selection
+	void playLevelDeathVariant(int levelId, int phase, int frame);
+
+	// Play retry video (phase-specific for multi-phase levels)
+	void playLevelRetryVariant(int levelId, int phase);
+
+	// Level state tracking for multi-phase levels
+	int _currentPhase;        // Current gameplay phase (1, 2, 3 for Level 2; 1, 2 for Level 3/6)
+	int _deathFrame;          // Frame number where player died (for death video selection)
+	int _phaseScore;          // Accumulated score from previous phases (preserved on phase retry)
+	int _phaseMisses;         // Accumulated misses from previous phases
+
 	// =============================================================
 
 	NutRenderer *_smush_cockpitNut;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 61ec421a515..7c51ecef7de 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2705,22 +2705,19 @@ Common::Error ScummEngine::go() {
 				}
 
 				if (levelResult == InsaneRebel2::kLevelSelectPlay) {
-					// Play the selected level (currently only level 1 is supported)
+					// Play the selected level using the level loading system
 					int selectedLevel = rebel->_selectedLevel;
 					debug("ScummEngine: Starting level %d", selectedLevel);
 
-					// For now, only level 1 is implemented
-					// Play Level 1 intro cinematic
-					vm7->_splayer->setCurVideoFlags(0x20);
-					vm7->_splayer->play("LEV01/01BEG.SAN", 12);
+					// Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
+					int result = rebel->runLevel(selectedLevel);
 
-					if (shouldQuit()) break;
-
-					// Start gameplay
-					vm7->_splayer->setCurVideoFlags(0);
-					vm7->_splayer->play("LEV01/01P01.SAN", 12);
+					if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
+						break;
+					}
 
-					// After gameplay, return to menu
+					// After level completion or game over, return to menu
+					// Could also handle kLevelNextLevel to auto-start next level
 				}
 				// If kLevelSelectBack, loop back to main menu
 			}


Commit: ead8b2a3edec1ce1d6a22b61fab082f546e6ecf0
    https://github.com/scummvm/scummvm/commit/ead8b2a3edec1ce1d6a22b61fab082f546e6ecf0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00

Commit Message:
SCUMM: RA2: Clear screen before missions

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 48f4988d902..163bb7c10de 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3753,6 +3753,15 @@ int InsaneRebel2::runLevel(int levelId) {
 		return kLevelReturnToMenu;
 	}
 
+	// Switch to gameplay state to stop menu overlay rendering
+	_gameState = kStateGameplay;
+	_menuInputActive = false;
+
+	// Clear the screen to remove any leftover menu pixels
+	VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+	memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);
+	_vm->markRectAsDirty(kMainVirtScreen, 0, vs->w, 0, vs->h);
+
 	// Set the current level
 	_selectedLevel = levelId;
 


Commit: 2878546d81f65931c7c1f164baf622090cc5875a
    https://github.com/scummvm/scummvm/commit/2878546d81f65931c7c1f164baf622090cc5875a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00

Commit Message:
SCUMM: RA2: Implement pause and skip controls

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 163bb7c10de..5ec985f0f88 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -296,6 +296,44 @@ InsaneRebel2::~InsaneRebel2() {
 }
 
 bool InsaneRebel2::notifyEvent(const Common::Event &event) {
+	// Handle global key events (ESC to skip, SPACE to pause)
+	// These work regardless of menu state
+	if (event.type == Common::EVENT_KEYDOWN) {
+		SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_ESCAPE:
+			// ESC skips cutscenes (videos with 0x20 flag = non-interactive)
+			// Emulates FUN_0041f537 behavior: if key == 0x1b, skip video
+			if (splayer && (splayer->_curVideoFlags & 0x20) != 0) {
+				debug("Rebel2: ESC pressed - skipping cutscene");
+				_vm->_smushVideoShouldFinish = true;
+				return true;  // Consume the event
+			}
+			break;
+
+		case Common::KEYCODE_SPACE:
+			// SPACE toggles pause (emulates FUN_405A21 pause handling)
+			// Only allow pausing during gameplay, not in menus
+			if (splayer && _gameState == kStateGameplay) {
+				if (splayer->_paused) {
+					debug("Rebel2: SPACE pressed - unpausing");
+					splayer->unpause();
+				} else {
+					debug("Rebel2: SPACE pressed - pausing");
+					splayer->pause();
+					// Show the pause overlay with dimming effect and "PAUSED" text
+					showPauseOverlay();
+				}
+				return true;  // Consume the event
+			}
+			break;
+
+		default:
+			break;
+		}
+	}
+
 	// Capture menu-related input events when menu input is active.
 	// This is called before ScummEngine::parseEvents() consumes events,
 	// so we can reliably capture keyboard/mouse input for menu navigation.
@@ -2984,6 +3022,165 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 	}
 }
 
+// ======================= Pause Overlay =======================
+// Emulates FUN_405A21 pause rendering (lines 242-305)
+// Creates a dimmed overlay effect and displays "PAUSED" text
+void InsaneRebel2::showPauseOverlay() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer) {
+		debug("showPauseOverlay: No SmushPlayer active");
+		return;
+	}
+
+	// Get frame buffer and palette from SmushPlayer
+	// _dst points to the virtual screen pixels (the actual rendering destination)
+	// _frameBuffer is only used for store/fetch operations, not general rendering
+	byte *frameBuffer = splayer->_dst;
+	byte *palette = splayer->_pal;
+	int width = splayer->_width;
+	int height = splayer->_height;
+
+	if (!frameBuffer || !palette || width <= 0 || height <= 0) {
+		debug("showPauseOverlay: No frame buffer (%p), palette (%p), or invalid dimensions (%dx%d)",
+		      (void*)frameBuffer, (void*)palette, width, height);
+		return;
+	}
+
+	debug("showPauseOverlay: Applying dimming effect to %dx%d buffer", width, height);
+
+	// Apply dimming effect (emulates FUN_405A21 lines 242-251)
+	// Original algorithm:
+	//   For each pixel, take the green component of its palette entry
+	//   and the green component of the previous pixel's palette entry,
+	//   add them, divide by 8, add 16.
+	// This creates a dark dimmed effect.
+	int bufferSize = width * height;
+	byte prevPixel = 0;
+
+	for (int i = 0; i < bufferSize; i++) {
+		byte curPixel = frameBuffer[i];
+
+		// Get green components from palette (offset +1 in RGB triplets)
+		int greenCur = palette[curPixel * 3 + 1];
+		int greenPrev = palette[prevPixel * 3 + 1];
+
+		// Apply dimming formula: (green1 + green2) >> 3 + 0x10
+		byte dimmedValue = ((greenCur + greenPrev) >> 3) + 0x10;
+
+		frameBuffer[i] = dimmedValue;
+		prevPixel = curPixel;
+	}
+
+	// Draw border decorations (simplified version of FUN_405A21 lines 261-283)
+	// Draw horizontal lines at top and bottom of a centered box
+	int boxLeft = 12;
+	int boxRight = width - 12;
+	int boxTop = 23;   // 0x17
+	int boxBottom = height - 23;  // ~175 for 200 height
+
+	byte borderColor = 0x50;  // Gray border color
+
+	// Top and bottom borders
+	for (int x = boxLeft; x < boxRight; x++) {
+		if (boxTop >= 0 && boxTop < height)
+			frameBuffer[boxTop * width + x] = borderColor;
+		if (boxBottom >= 0 && boxBottom < height)
+			frameBuffer[boxBottom * width + x] = borderColor;
+	}
+
+	// Left and right borders
+	for (int y = boxTop; y < boxBottom; y++) {
+		if (boxLeft >= 0 && boxLeft < width)
+			frameBuffer[y * width + boxLeft] = borderColor;
+		if (boxRight >= 0 && boxRight < width)
+			frameBuffer[y * width + boxRight] = borderColor;
+	}
+
+	// Draw corner decorations (simplified)
+	byte cornerColor = 0x51;  // Slightly brighter for corners
+	for (int i = 0; i < 5; i++) {
+		// Top-left corner
+		if (boxTop + i < height && boxLeft + 5 < width)
+			frameBuffer[(boxTop + i) * width + boxLeft + 5] = cornerColor;
+		if (boxTop + 5 < height && boxLeft + i < width)
+			frameBuffer[(boxTop + 5) * width + boxLeft + i] = cornerColor;
+
+		// Top-right corner
+		if (boxTop + i < height && boxRight - 5 >= 0)
+			frameBuffer[(boxTop + i) * width + boxRight - 5] = cornerColor;
+		if (boxTop + 5 < height && boxRight - i >= 0)
+			frameBuffer[(boxTop + 5) * width + boxRight - i] = cornerColor;
+
+		// Bottom-left corner
+		if (boxBottom - i >= 0 && boxLeft + 5 < width)
+			frameBuffer[(boxBottom - i) * width + boxLeft + 5] = cornerColor;
+		if (boxBottom - 5 >= 0 && boxLeft + i < width)
+			frameBuffer[(boxBottom - 5) * width + boxLeft + i] = cornerColor;
+
+		// Bottom-right corner
+		if (boxBottom - i >= 0 && boxRight - 5 >= 0)
+			frameBuffer[(boxBottom - i) * width + boxRight - 5] = cornerColor;
+		if (boxBottom - 5 >= 0 && boxRight - i >= 0)
+			frameBuffer[(boxBottom - 5) * width + boxRight - i] = cornerColor;
+	}
+
+	// Draw "PAUSED" text centered
+	// Use hardcoded "PAUSED" string (TRS string 0x79 is "Quit Game" in RA2)
+	const char *pauseText = "PAUSED";
+
+	// Draw text using SmushFont if available
+	if (_menuFont) {
+		Common::Rect clipRect(0, 0, width, height);
+
+		// Calculate centered position
+		// Text should be centered horizontally and vertically in the box
+		int textX = width / 2;  // SmushFont handles centering with kStyleAlignCenter
+		int textY = height / 2 - 4;  // Slightly above center
+
+		// Draw with color 4 and background 0x10 (matching original parameters)
+		// FUN_00434cb0 params: x=10, y=10 or 20, color=4, bg=0x10
+		_menuFont->drawString(pauseText, frameBuffer, clipRect, textX, textY, 0x10, kStyleAlignCenter);
+	} else if (_smush_smalfontNut) {
+		// Fallback: draw using NutRenderer directly
+		NutRenderer *font = _smush_smalfontNut;
+		int numFontChars = font->getNumChars();
+		Common::Rect clipRect(0, 0, width, height);
+
+		// Calculate text width
+		int textWidth = 0;
+		const char *p = pauseText;
+		while (*p) {
+			byte c = (byte)*p++;
+			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c < numFontChars) {
+				textWidth += font->getCharWidth(c);
+			}
+		}
+
+		// Draw centered
+		int textX = (width - textWidth) / 2;
+		int textY = height / 2 - 4;
+
+		p = pauseText;
+		while (*p) {
+			byte c = (byte)*p++;
+			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c < numFontChars && textX >= 0 && textY >= 0) {
+				font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
+				                 kStyleAlignLeft, c, true, true);
+				textX += font->getCharWidth(c);
+			}
+		}
+	}
+
+	// Update the screen to show the pause overlay
+	// SmushPlayer uses copyRectToScreen to transfer the buffer to the display backend
+	_vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, width, height);
+	_vm->_system->updateScreen();
+
+	debug("showPauseOverlay: Overlay displayed");
+}
+
 int InsaneRebel2::runMainMenu() {
 	// Main menu loop - emulates FUN_004147B2
 	// Returns:
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bc73cb4882b..bc3e01364ff 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -400,6 +400,11 @@ public:
 	// Render score text to HUD (called from procPostRendering)
 	void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
 
+	// ======================= Pause Overlay =======================
+	// Show pause overlay with dimming effect and "PAUSED" text
+	// Emulates FUN_405A21 pause rendering (lines 242-305)
+	void showPauseOverlay();
+
 	// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
 	int _targetLockTimer;
 


Commit: 1fa930ed850195e4957687b09ffc1d084853b292
    https://github.com/scummvm/scummvm/commit/1fa930ed850195e4957687b09ffc1d084853b292
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:53+02:00

Commit Message:
SCUMM: RA2: Load directional ship sprites

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/nut_renderer.cpp
    engines/scumm/nut_renderer.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5ec985f0f88..aaabb44372f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -100,7 +100,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
 
 	_enemies.clear();
-	_rebelHandler = 8;  // Default to Handler 8 (ground vehicle) for Level 1
+	_rebelHandler = 8;  // Default to Handler 8 (third-person vehicle) for Level 1
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
 	_introCursorPushed = false;  // Cursor state tracking for intro sequences
 
@@ -232,7 +232,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_shots[i].counter = 0;
 	}
 
-	for (i = 0; i < 5; i++) {
+	for (i = 0; i < 16; i++) {
 		_rebelEmbeddedHud[i].pixels = nullptr;
 		_rebelEmbeddedHud[i].width = 0;
 		_rebelEmbeddedHud[i].height = 0;
@@ -241,6 +241,29 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_rebelEmbeddedHud[i].valid = false;
 	}
 
+	// Initialize Handler 8 ship system
+	_shipSprite = nullptr;
+	_shipSprite2 = nullptr;
+	_shipOverlay1 = nullptr;
+	_shipOverlay2 = nullptr;
+	_shipPosX = 0xa0;      // Start centered (160 in hex)
+	_shipPosY = 0x28;      // Start at vertical center (40)
+	_shipTargetX = 0xa0;
+	_shipTargetY = 0x28;
+	_shipLevelMode = 0;
+	_shipFiring = false;
+	_shipDirectionH = 2;   // Start centered horizontally (0-4 range)
+	_shipDirectionV = 3;   // Start centered vertically (0-6 range)
+	_shipDirectionIndex = 2 * 7 + 3;  // Center = 17
+
+	// Initialize Handler 7 FLY ship system
+	_flyShipSprite = nullptr;    // FLY001 - 35 direction frames
+	_flyLaserSprite = nullptr;   // FLY002 - laser sprites
+	_flyTargetSprite = nullptr;  // FLY003 - targeting overlay
+	_flyHiResSprite = nullptr;   // FLY004 - high-res alternative
+	_flyShipScreenX = 0xd4;      // Start at center (212) - matches DAT_00443708 default
+	_flyShipScreenY = 0x82;      // Start at center (130) - matches DAT_0044370a default
+
 	// Initialize audio system for RA2 (since we don't use iMUSE)
 	_audioSampleRate = 11025;  // RA2 audio is 11025 Hz, not 22050 Hz
 	for (i = 0; i < kRA2MaxAudioTracks; i++) {
@@ -293,6 +316,24 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _smush_smalfontNut;
 	delete _smush_titlefontNut;
 	delete _smush_mouseoverNut;
+
+	// Clean up Handler 8 ship sprites
+	delete _shipSprite;
+	delete _shipSprite2;
+	delete _shipOverlay1;
+	delete _shipOverlay2;
+
+	// Clean up Handler 7 FLY ship sprites
+	delete _flyShipSprite;
+	delete _flyLaserSprite;
+	delete _flyTargetSprite;
+	delete _flyHiResSprite;
+
+	// Clean up embedded HUD overlays
+	for (int i = 0; i < 16; i++) {
+		free(_rebelEmbeddedHud[i].pixels);
+		_rebelEmbeddedHud[i].pixels = nullptr;
+	}
 }
 
 bool InsaneRebel2::notifyEvent(const Common::Event &event) {
@@ -737,13 +778,51 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		// Opcode 5: Special handling based on par2 value
 		// Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
 		debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
-		
+
+	} else if (par1 == 7) {
+		// Opcode 7: Sprite/HUD control for Handler 7 (space flight levels like Level 3)
+		// par2 = control type (41 = sprite selection?)
+		// par3 = usually 0
+		// par4 = sprite/slot ID (0 or 5 seen in Level 3)
+		//
+		// This opcode may control which ship direction sprite to display
+		// or reference embedded graphics loaded elsewhere
+		debug("Rebel2 IACT Opcode 7: par2=%d par3=%d par4=%d handler=%d",
+			par2, par3, par4, _rebelHandler);
+
+		// Read remaining IACT data to understand structure
+		int64 startPos = b.pos();
+		int64 remaining = b.size() - startPos;
+		if (remaining > 0 && remaining <= 64) {
+			byte payload[64];
+			int bytesRead = b.read(payload, MIN((int64)64, remaining));
+			debug("Rebel2 Opcode 7: payload (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X",
+				bytesRead,
+				bytesRead > 0 ? payload[0] : 0, bytesRead > 1 ? payload[1] : 0,
+				bytesRead > 2 ? payload[2] : 0, bytesRead > 3 ? payload[3] : 0,
+				bytesRead > 4 ? payload[4] : 0, bytesRead > 5 ? payload[5] : 0,
+				bytesRead > 6 ? payload[6] : 0, bytesRead > 7 ? payload[7] : 0);
+			b.seek(startPos);
+		}
+
+		// par2 == 41 (0x29) seems to be a common value
+		// This might be a "show sprite" command referencing par4 as the slot
+		if (par2 == 41) {
+			// par4 could be a HUD slot or sprite index
+			// For Handler 7, set which embedded HUD frame to display
+			if (_rebelHandler == 7 && par4 >= 0 && par4 < 16) {
+				// Mark this slot as the active one for direction-based rendering
+				// This will be used in post-rendering to know which frame to show
+				debug("Rebel2 Opcode 7: Activating HUD slot %d for Handler 7", par4);
+			}
+		}
+
 	} else if (par1 == 6) {
 		// Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
 		iactRebel2Opcode6(renderBitmap, b, par2, par3, par4);
 	} else if (par1 == 8) {
 		// Opcode 8: HUD resource loading (FUN_41CADB case 6)
-		iactRebel2Opcode8(renderBitmap, b, par2, par3, par4);
+		iactRebel2Opcode8(renderBitmap, b, size, par2, par3, par4);
 	} else if (par1 == 9) {
 		// Opcode 9: Text/subtitle display
 		iactRebel2Opcode9(renderBitmap, b, par2, par3, par4);
@@ -901,12 +980,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	// Opcode 6: Level setup / mode switch
 	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
 	//
-	// Assembly behavior:
-	// 1. If par4 == 1: Draw status bar sprite 5, clear link tables, reset state
-	// 2. Set level type (DAT_00457900 = par3)
-	// 3. Handle autopilot/control mode logic
-	// 4. Update damage level counter
-	// 5. Calculate view offsets based on level type
+	// For Handler 8 (third-person vehicle) - FUN_00401234 case 4:
+	// - par3 sets ship level mode (DAT_0043e000)
+	// - par4 == 1 triggers status bar display and state reset
+	// - Updates ship position based on mouse input
+	//
+	// For Handler 0x26/0x19 (turret/space):
+	// - Same par4 == 1 behavior
+	// - Different view offset calculations
 
 	debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
 
@@ -916,6 +997,218 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		debug("Rebel2 Opcode 6: Setting handler=%d", par2);
 	}
 
+	// Handler 8 specific logic (third-person vehicle) - FUN_00401234 case 4
+	if (_rebelHandler == 8) {
+		// Set ship level mode (DAT_0043e000 = par3)
+		_shipLevelMode = par3;
+
+		// If par4 == 1, enable status bar and reset state
+		if (par4 == 1) {
+			_rebelStatusBarSprite = 5;  // Status bar sprite for Handler 8
+			// Reset link tables
+			for (int i = 0; i < 512; i++) {
+				_rebelLinks[i][0] = 0;
+				_rebelLinks[i][1] = 0;
+				_rebelLinks[i][2] = 0;
+			}
+			debug("Rebel2 Opcode 6 (Handler 8): Status bar enabled, state reset");
+		}
+
+		// Skip position calculation for special modes 4 and 5
+		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+			// Calculate target position from mouse input
+			// Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
+			// Based on FUN_00401234 lines 151-166:
+			// local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
+			// local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
+
+			// Map mouse position (-127 to 127 range) to ship target
+			// Mouse is 0-320, center is 160. Map to -127 to 127 range
+			int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
+			int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
+
+			// Clamp to valid range
+			if (mouseOffsetX > 127) mouseOffsetX = 127;
+			if (mouseOffsetX < -127) mouseOffsetX = -127;
+			if (mouseOffsetY > 127) mouseOffsetY = 127;
+			if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+			// Calculate target positions using the original formula
+			_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
+			_shipTargetY = (int16)(-((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
+
+			// Smooth interpolation toward target (max 50 pixels per frame)
+			const int16 maxStep = 50;  // 0x32 in hex
+			if (_shipPosX < _shipTargetX) {
+				int16 newX = _shipPosX + maxStep;
+				_shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
+			} else if (_shipPosX > _shipTargetX) {
+				int16 newX = _shipPosX - maxStep;
+				_shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
+			}
+
+			if (_shipPosY < _shipTargetY) {
+				int16 newY = _shipPosY + maxStep;
+				_shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
+			} else if (_shipPosY > _shipTargetY) {
+				int16 newY = _shipPosY - maxStep;
+				_shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
+			}
+
+			// Calculate ship direction indices for sprite selection
+			// Map mouse position to 5x7 direction grid (like Handler 7)
+			int16 mouseX = _vm->_mouse.x;
+			int16 mouseY = _vm->_mouse.y;
+
+			// Scale mouse if video is larger than 320x200
+			if (_player && _player->_width > 320) {
+				mouseX = (mouseX * 320) / _player->_width;
+			}
+			if (_player && _player->_height > 200) {
+				mouseY = (mouseY * 200) / _player->_height;
+			}
+
+			// Horizontal: 5 zones (0=far left, 2=center, 4=far right)
+			if (mouseX < 64) _shipDirectionH = 0;
+			else if (mouseX < 128) _shipDirectionH = 1;
+			else if (mouseX < 192) _shipDirectionH = 2;
+			else if (mouseX < 256) _shipDirectionH = 3;
+			else _shipDirectionH = 4;
+
+			// Vertical: 7 zones (0=far up, 3=center, 6=far down)
+			if (mouseY < 28) _shipDirectionV = 0;
+			else if (mouseY < 57) _shipDirectionV = 1;
+			else if (mouseY < 86) _shipDirectionV = 2;
+			else if (mouseY < 114) _shipDirectionV = 3;
+			else if (mouseY < 143) _shipDirectionV = 4;
+			else if (mouseY < 171) _shipDirectionV = 5;
+			else _shipDirectionV = 6;
+
+			_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
+		}
+
+		// Update firing state from mouse button
+		_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+
+		debug("Rebel2 Opcode 6 (Handler 8): mode=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
+			_shipLevelMode, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
+			_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+
+		// Handler 8 doesn't use the same view offset logic as other handlers
+		// Skip the rest of the function for Handler 8
+		return;
+	}
+
+	// Handler 7 specific logic (space flight) - FUN_0040d836 / FUN_0040c3cc
+	// Used for Level 3 and similar space combat levels
+	if (_rebelHandler == 7) {
+		// Set level mode (same as Handler 8)
+		_shipLevelMode = par3;
+
+		// If par4 == 1, enable status bar
+		if (par4 == 1) {
+			_rebelStatusBarSprite = 5;  // Status bar sprite
+			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled");
+		}
+
+		// Update ship screen position from mouse
+		// Handler 7 uses DAT_0044370c (Y) and DAT_0044370e (X) for screen position
+		// Get raw mouse position
+		int16 rawMouseX = _vm->_mouse.x;
+		int16 rawMouseY = _vm->_mouse.y;
+
+		// Scale mouse to 320x200 logical space if video is larger
+		int16 mouseX = rawMouseX;
+		int16 mouseY = rawMouseY;
+		if (_player && _player->_width > 320) {
+			mouseX = (rawMouseX * 320) / _player->_width;
+		}
+		if (_player && _player->_height > 200) {
+			mouseY = (rawMouseY * 200) / _player->_height;
+		}
+
+		// Clamp to screen bounds (matching FUN_0040c3cc bounds)
+		if (mouseX < 0) mouseX = 0;
+		if (mouseX > 319) mouseX = 319;
+		if (mouseY < 0) mouseY = 0;
+		if (mouseY > 199) mouseY = 199;
+
+		// Update ship position with smooth interpolation
+		// FUN_0040c3cc uses complex smoothing, we use simpler immediate response
+		const int16 maxStep = 15;
+		if (_shipPosX < mouseX) {
+			_shipPosX = MIN((int16)(_shipPosX + maxStep), mouseX);
+		} else if (_shipPosX > mouseX) {
+			_shipPosX = MAX((int16)(_shipPosX - maxStep), mouseX);
+		}
+		if (_shipPosY < mouseY) {
+			_shipPosY = MIN((int16)(_shipPosY + maxStep), mouseY);
+		} else if (_shipPosY > mouseY) {
+			_shipPosY = MAX((int16)(_shipPosY - maxStep), mouseY);
+		}
+
+		// Update Handler 7 screen position (DAT_0044370c/e)
+		// These track the actual on-screen position for direction calculation
+		_flyShipScreenX = _shipPosX;
+		_flyShipScreenY = _shipPosY;
+
+		// Calculate ship direction from position (FUN_0040d836 lines 88-106)
+		// Formula from assembly:
+		//   hDir = (0xa0 - posX) >> 6  (with signed rounding)
+		//   vDir = (0x95 - posY) / 0x2b
+		//   dirIndex = hDir * 7 + vDir
+		//
+		// Note: The assembly formula gives:
+		//   hDir: 0-4 where 2 is center (0xa0=160, range is -96 to +96, >> 6 gives -1 to 1, but clamped to 0-4)
+		//   vDir: 0-6 where 3 is center (0x95=149, 0x2b=43, so 149/43 ≈ 3.5)
+		//
+		// Simplified direction calculation based on mouse position relative to center
+
+		// Horizontal direction (0-4, center=2)
+		// Formula: (160 - posX) >> 6, clamped to 0-4
+		int16 hDiff = 160 - _flyShipScreenX;
+		int16 hDir = (hDiff + 64) >> 6;  // Add 64 to shift range, divide by 64
+		if (hDir < 0) hDir = 0;
+		if (hDir > 4) hDir = 4;
+
+		// Vertical direction (0-6, center=3)
+		// Formula: (149 - posY) / 43, clamped to 0-6
+		int16 vDir = (149 - _flyShipScreenY) / 43;
+		if (vDir < 0) vDir = 0;
+		if (vDir > 6) vDir = 6;
+
+		// Additional adjustment from assembly (lines 90-105):
+		// If vDir==3 and abs(posY) > 10, adjust by +/-1
+		// If hDir==2 and abs(posX) > 15, adjust by +/-1
+		// This creates a "deadzone" at center to reduce flicker
+		if (vDir == 3 && ABS(_flyShipScreenY - 100) > 10) {
+			if (_flyShipScreenY < 100) vDir = 2;
+			else vDir = 4;
+		}
+		if (hDir == 2 && ABS(_flyShipScreenX - 160) > 15) {
+			if (_flyShipScreenX < 160) hDir = 3;
+			else hDir = 1;
+		}
+
+		_shipDirectionH = hDir;
+		_shipDirectionV = vDir;
+		_shipDirectionIndex = hDir * 7 + vDir;
+
+		// Clamp direction index to valid range (0-34)
+		if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
+		if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
+
+		// Update firing state
+		_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+
+		debug("Rebel2 Handler7: rawMouse=(%d,%d) scaled=(%d,%d) shipPos=(%d,%d) screenPos=(%d,%d) dir=(%d,%d) idx=%d flySprite=%p",
+			rawMouseX, rawMouseY, mouseX, mouseY, _shipPosX, _shipPosY,
+			_flyShipScreenX, _flyShipScreenY, _shipDirectionH, _shipDirectionV, _shipDirectionIndex,
+			(void*)_flyShipSprite);
+
+		return;
+	}
+
 	// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
 	if (par4 == 1) {
 		// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
@@ -1088,11 +1381,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	}
 }
 
-void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// Opcode 8: HUD resource loading
+void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
+	// Opcode 8: HUD/Ship resource loading
 	// Based on FUN_41CADB case 6 (switch on *local_14 - 2 == 6, meaning opcode 8)
 	//
-	// par3 determines which HUD slot to load:
+	// For Handler 0x26 (turret) - par3 determines HUD slot:
 	// case 1: DAT_00482240 - Primary HUD overlay (GRD001)
 	// case 2: DAT_00482238 - Secondary HUD graphics (GRD002)
 	// case 4: DAT_00482268 - Ship cockpit frame (GRD010)
@@ -1101,50 +1394,222 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	// case 7: DAT_00482248 - Damage indicator (GRD004)
 	// case 10: DAT_00482258 - Additional effects (GRD005)
 	// case 12/13: DAT_00482260 - High-res HUD alternative (GRD007)
-	// cases 21-27: Sound loading slot 0
-	// cases 31-37: Sound loading slot 1
-	// case 40: Sound loading slot 3
-	// cases 41-47: Sound loading slot 2
+	//
+	// For Handler 8 (third-person vehicle) - par3 determines ship sprite slot (FUN_00401234):
+	// case 1: DAT_0047e010 - Primary ship sprite (POV001)
+	// case 3: DAT_0047e028 - Secondary ship sprite (POV004)
+	// case 6: DAT_0047e020 - Ship overlay 1 (POV002)
+	// case 7: DAT_0047e018 - Ship overlay 2 (POV003)
+	//
+	// cases 21-47: Sound loading (both handlers)
 
-	debug("Rebel2 IACT Opcode 8: par2=%d par3=%d par4=%d (HUD loading)", par2, par3, par4);
+	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)", _rebelHandler, par2, par3, par4, _gameState);
 
-	// Read the data size from the stream (at offset +14, which is local_14[7])
-	// The actual NUT/resource data starts at offset +18 (param_5 + 9 in assembly)
 	int64 startPos = b.pos();
+	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
+
+	// Handler 7 FLY NUT loading - fixed offset format (FUN_0040c3cc case 6)
+	// IACT structure after par1-par4 (we're at offset +8):
+	//   +0-5 (6 bytes): additional header
+	//   +6-9 (4 bytes): NUT data size (little-endian)
+	//   +10+: NUT data
+	// Assembly: param_5[7] = size at offset 14, param_5+9 = data at offset 18
+	// Since we've read 8 bytes (par1-par4), that's offset +6 and +10 from current pos
+	//
+	// IMPORTANT: The assembly switches on param_5[3] which is the 4th short (bytes 6-7)
+	// In SMUSH handleIACT, this corresponds to userId (par4), NOT unknown (par3)!
+	// So we use par4 for the FLY slot selection.
+	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
+
+	if (isHandler7FLY && remaining >= 14) {
+		// Read additional header and size from fixed offset
+		// Based on FUN_0040c3cc assembly:
+		//   param_5[7] = size at offset 14 (we're at offset 8, so read 6+4 bytes)
+		//   param_5+9 = data at offset 18
+		byte header[10];
+		if (b.read(header, 10) == 10) {
+			// Debug: dump all header bytes
+			debug("Rebel2 Opcode 8 Handler7: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+				header[0], header[1], header[2], header[3], header[4],
+				header[5], header[6], header[7], header[8], header[9]);
+
+			// Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
+			// Try both endianness
+			uint32 nutSizeLE = READ_LE_UINT32(header + 6);
+			uint32 nutSizeBE = READ_BE_UINT32(header + 6);
+			debug("Rebel2 Opcode 8 Handler7: par4=%d sizesLE=%u sizeBE=%u remaining=%lld",
+				par4, nutSizeLE, nutSizeBE, (long long)remaining);
+
+			// The assembly uses direct memory read on x86 which is LE
+			uint32 nutSize = nutSizeLE;
+
+			if (nutSize > 0 && nutSize <= (uint32)(remaining - 10)) {
+				byte *nutData = (byte *)malloc(nutSize);
+				if (nutData) {
+					int bytesRead = b.read(nutData, nutSize);
+					debug("Rebel2 Opcode 8 Handler7: Read %d/%u bytes of NUT data, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+						bytesRead, nutSize,
+						bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
+						bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
+						bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
+						bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
+						bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
+						bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
+						bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
+						bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
+
+					// Verify we read the expected amount
+					if (bytesRead != (int)nutSize) {
+						warning("Rebel2 Opcode 8 Handler7: Short read! Got %d expected %u", bytesRead, nutSize);
+					}
 
-	// Skip to where embedded data would be and scan for ANIM tag
-	// The assembly shows data at param_5 + 9 (18 bytes from IACT header start)
-	// Since we're already past the header params, scan remaining data
+					// Verify ANIM header
+					if (bytesRead >= 8) {
+						uint32 animTag = READ_BE_UINT32(nutData);
+						uint32 animSize = READ_BE_UINT32(nutData + 4);
+						debug("Rebel2 Opcode 8 Handler7: ANIM tag=%08X size=%u (expected %08X, size should be ~%d)",
+							animTag, animSize, MKTAG('A','N','I','M'), bytesRead - 8);
+						if (animTag != MKTAG('A','N','I','M')) {
+							warning("Rebel2 Opcode 8 Handler7: No ANIM tag! Data may be corrupted");
+						}
+						if ((int32)animSize > bytesRead - 8) {
+							warning("Rebel2 Opcode 8 Handler7: ANIM size %u exceeds data %d", animSize, bytesRead - 8);
+						}
+					}
+
+					// Try loading as NUT
+					NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
+					if (newNut && newNut->getNumChars() > 0) {
+						debug("Rebel2 Opcode 8 Handler7: Loaded FLY NUT par4=%d with %d sprites",
+							par4, newNut->getNumChars());
+
+						// Switch on par4 (userId) - matches assembly param_5[3]
+						switch (par4) {
+						case 1:  // FLY001 - Ship direction sprites (35 frames)
+							delete _flyShipSprite;
+							_flyShipSprite = newNut;
+							debug("Rebel2 Opcode 8: _flyShipSprite set with %d sprites", newNut->getNumChars());
+							break;
+						case 2:  // FLY003 - Targeting overlay
+							delete _flyTargetSprite;
+							_flyTargetSprite = newNut;
+							break;
+						case 3:  // FLY002 - Laser fire sprites
+							delete _flyLaserSprite;
+							_flyLaserSprite = newNut;
+							break;
+						case 11: // FLY004 - High-res alternative
+							delete _flyHiResSprite;
+							_flyHiResSprite = newNut;
+							break;
+						default:
+							delete newNut;
+							break;
+						}
+					} else {
+						debug("Rebel2 Opcode 8 Handler7: NUT load failed for par4=%d", par4);
+						delete newNut;
+					}
+					free(nutData);
+				}
+			}
+		}
+		b.seek(startPos);
+		return;
+	}
 
-	int64 totalSize = b.size();
-	if (totalSize > startPos) {
-		int64 remaining = totalSize - startPos;
+	// For non-Handler7 or non-FLY cases, scan for ANIM tag
+	debug("Rebel2 Opcode 8: startPos=%lld chunkSize=%d remaining=%lld", (long long)startPos, chunkSize, (long long)remaining);
+	if (remaining > 0) {
 		int scanSize = (int)MIN<int64>(remaining, 65536);
 		byte *scanBuf = (byte *)malloc(scanSize);
 		if (scanBuf) {
 			int bytesRead = b.read(scanBuf, scanSize);
-
-			// Look for ANIM tag (embedded SAN)
+			debug("Rebel2 Opcode 8: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+				bytesRead,
+				bytesRead > 0 ? scanBuf[0] : 0, bytesRead > 1 ? scanBuf[1] : 0,
+				bytesRead > 2 ? scanBuf[2] : 0, bytesRead > 3 ? scanBuf[3] : 0,
+				bytesRead > 4 ? scanBuf[4] : 0, bytesRead > 5 ? scanBuf[5] : 0,
+				bytesRead > 6 ? scanBuf[6] : 0, bytesRead > 7 ? scanBuf[7] : 0,
+				bytesRead > 8 ? scanBuf[8] : 0, bytesRead > 9 ? scanBuf[9] : 0,
+				bytesRead > 10 ? scanBuf[10] : 0, bytesRead > 11 ? scanBuf[11] : 0,
+				bytesRead > 12 ? scanBuf[12] : 0, bytesRead > 13 ? scanBuf[13] : 0,
+				bytesRead > 14 ? scanBuf[14] : 0, bytesRead > 15 ? scanBuf[15] : 0);
+
+			// Look for ANIM tag (embedded SAN or NUT)
 			for (int i = 0; i + 8 <= bytesRead; ++i) {
 				if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+					debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
 					int64 animStreamPos = startPos + i;
 					uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-					int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+					// Limit toCopy to remaining data in this chunk
+					int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, remaining - i);
 					if (toCopy > 0) {
 						byte *animData = (byte *)malloc(toCopy);
 						if (animData) {
 							b.seek(animStreamPos);
 							b.read(animData, toCopy);
-							// Map par3 to userId for HUD slots
-							int userId = 0;
-							switch (par3) {
-							case 1: userId = 1; break;  // Primary HUD
-							case 2: userId = 2; break;  // Secondary HUD
-							case 4: userId = 3; break;  // Cockpit frame
-							case 6: userId = 4; break;  // Explosion overlay
-							default: userId = par3; break;
+
+							// Handler 8 (Third-Person Vehicle) - FUN_00401234 case 6:
+							// par3 == 1: POV001 -> DAT_0047e010 (primary ship)
+							// par3 == 3: POV004 -> DAT_0047e028 (secondary ship)
+							// par3 == 6: POV002 -> DAT_0047e020 (overlay 1)
+							// par3 == 7: POV003 -> DAT_0047e018 (overlay 2)
+							bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
+							bool loadedAsNut = false;
+
+							if (_rebelHandler == 8 && isHandler8Par3) {
+								// Try loading as NUT (ship sprites for Handler 8)
+								NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+								if (newNut && newNut->getNumChars() > 0) {
+									debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
+										par3, newNut->getNumChars(), _rebelHandler);
+									loadedAsNut = true;
+
+									switch (par3) {
+									case 1:  // POV001 - Primary ship sprite
+										delete _shipSprite;
+										_shipSprite = newNut;
+										debug("Rebel2 Opcode 8: _shipSprite set to %p", (void*)_shipSprite);
+										break;
+									case 3:  // POV004 - Secondary ship sprite
+										delete _shipSprite2;
+										_shipSprite2 = newNut;
+										break;
+									case 6:  // POV002 - Ship overlay 1
+										delete _shipOverlay1;
+										_shipOverlay1 = newNut;
+										break;
+									case 7:  // POV003 - Ship overlay 2
+										delete _shipOverlay2;
+										_shipOverlay2 = newNut;
+										break;
+									default:
+										delete newNut;
+										loadedAsNut = false;
+										break;
+									}
+								} else {
+									debug("Rebel2 Opcode 8: NUT load failed for par3=%d, trying as embedded SAN", par3);
+									delete newNut;
+								}
+							}
+
+							if (!loadedAsNut) {
+								// Load as embedded SAN (HUD overlays)
+								// The userId comes from par4 (not par3!)
+								// par4 values seen in Level 3:
+								//   1000 = audio track
+								//   1-11 = HUD overlay slots
+								// The embedded HUD system uses userId to track different overlay elements
+								int userId = par4;
+
+								// Audio tracks (userId >= 1000) are handled separately, skip them
+								if (userId < 1000) {
+									debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d", userId);
+									loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+								}
 							}
-							loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
 							free(animData);
 						}
 					}
@@ -1445,8 +1910,8 @@ extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int wi
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
-	// Validate userId (0 for menu background, 1-4 for HUD slots)
-	if (userId < 0 || userId > 4 || !animData || size < 32) {
+	// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
+	if (userId < 0 || userId > 15 || !animData || size < 32) {
 		debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
 		return;
 	}
@@ -1477,6 +1942,21 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 
 				if (subTag == MKTAG('F','O','B','J')) {
 					// Found FOBJ - Embedded HUD Frame
+					// Dump raw FOBJ bytes for analysis
+					int32 fobjStart = stream.pos();
+					byte rawHeader[20];
+					int headerBytesToRead = MIN((int)subSize, 20);
+					stream.read(rawHeader, headerBytesToRead);
+					stream.seek(fobjStart);  // Reset to read normally
+
+					debug("Rebel2: Raw FOBJ header (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+						headerBytesToRead,
+						rawHeader[0], rawHeader[1], rawHeader[2], rawHeader[3],
+						rawHeader[4], rawHeader[5], rawHeader[6], rawHeader[7],
+						rawHeader[8], rawHeader[9], rawHeader[10], rawHeader[11],
+						rawHeader[12], rawHeader[13], rawHeader[14], rawHeader[15],
+						rawHeader[16], rawHeader[17], rawHeader[18], rawHeader[19]);
+
 					// Read FOBJ header
 					int codec = stream.readUint16LE();
 					int left = stream.readUint16LE();
@@ -1485,8 +1965,8 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 					int height = stream.readUint16LE();
 					stream.readUint16LE();  // unknown
 					stream.readUint16LE();  // unknown
-					
-					debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d", 
+
+					debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
 						userId, width, height, left, top, codec);
 					
 					// Allocate storage for the decoded frame
@@ -1494,11 +1974,14 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 					
 					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
 						if (frame.width != width || frame.height != height || !frame.pixels) {
-							frame.pixels = (byte *)calloc(width * height, 1);
+							free(frame.pixels);
+							frame.pixels = (byte *)malloc(width * height);
 							frame.width = width;
 							frame.height = height;
 						}
-						
+						// Clear buffer before decode (important for delta codecs)
+						memset(frame.pixels, 0, width * height);
+
 						// Update render position from FOBJ header
 						frame.renderX = left;
 						frame.renderY = top;
@@ -1545,25 +2028,167 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
 							} else if (codec == 45) {
-								// Codec 45: Block delta (simple copy for now)
-								int copySize = MIN((int)dataSize, width * height);
-								memcpy(frame.pixels, fobjData, copySize);
+								// Codec 45: RA2-specific codec with BOMP-style RLE
+								// Header: 01 FE 00 00 01 00 (6 bytes)
+								//   Byte 0: sub-codec (01)
+								//   Byte 1: transparent color (FE = 254)
+								//   Bytes 2-5: unknown/padding
+								debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+									fobjData[0], fobjData[1], fobjData[2], fobjData[3],
+									fobjData[4], fobjData[5], fobjData[6], fobjData[7],
+									fobjData[8], fobjData[9], fobjData[10], fobjData[11],
+									fobjData[12], fobjData[13], fobjData[14], fobjData[15],
+									fobjData[16], fobjData[17], fobjData[18], fobjData[19]);
+
+								// Parse 6-byte sub-header
+								int headerSkip = 0;
+								if (dataSize > 6 && fobjData[0] == 0x01 && fobjData[1] == 0xFE) {
+									headerSkip = 6;
+									debug("Rebel2: Codec 45 header: sub-codec=%d, transparent=%d",
+										fobjData[0], fobjData[1]);
+								}
+
+								byte *srcPtr = fobjData + headerSkip;
+								byte *dataEnd = fobjData + dataSize;
+
+								// Try per-line RLE with 2-byte LE size headers
+								int firstVal = READ_LE_UINT16(srcPtr);
+								bool validPerLine = (firstVal > 0 && firstVal <= width * 2);
+
+								if (validPerLine) {
+									debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
+									for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+										int lineSize = READ_LE_UINT16(srcPtr);
+										srcPtr += 2;
+										if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
+
+										byte *lineEnd = srcPtr + lineSize;
+										byte *dst = frame.pixels + row * width;
+										int x = 0;
+
+										while (srcPtr < lineEnd && x < width) {
+											byte ctrl = *srcPtr++;
+											int count = (ctrl >> 1) + 1;
+											if (ctrl & 1) {
+												byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
+												for (int i = 0; i < count && x < width; i++) dst[x++] = color;
+											} else {
+												for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
+													dst[x++] = *srcPtr++;
+											}
+										}
+										srcPtr = lineEnd;
+									}
+								} else {
+									// Try continuous BOMP RLE (no per-line headers)
+									// Each line produces exactly 'width' pixels
+									debug("Rebel2: Codec 45 using continuous BOMP RLE");
+									for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+										byte *dst = frame.pixels + row * width;
+										int x = 0;
+
+										while (x < width && srcPtr < dataEnd) {
+											byte ctrl = *srcPtr++;
+											int count = (ctrl >> 1) + 1;
+
+											if (ctrl & 1) {
+												// RLE fill
+												byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
+												for (int i = 0; i < count && x < width; i++) {
+													dst[x++] = color;
+												}
+											} else {
+												// Literal copy
+												for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
+													dst[x++] = *srcPtr++;
+												}
+											}
+										}
+									}
+								}
 								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec 45/block): %dx%d", width, height);
+
+								// Count non-zero pixels
+								int nonZero = 0;
+								for (int i = 0; i < width * height; i++) {
+									if (frame.pixels[i] != 0) nonZero++;
+								}
+								debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
+									width, height, nonZero, (nonZero * 100) / (width * height));
+							} else if (codec == 23) {
+								// Codec 23: Skip/copy with RLE in copy runs
+								// Format: each line has pairs of (skip, copy_count, RLE_data)
+								byte *srcPtr = fobjData;
+								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
+									int lineDataSize = READ_LE_UINT16(srcPtr);
+									srcPtr += 2;
+									byte *lineEnd = srcPtr + lineDataSize;
+									byte *lineDst = frame.pixels + row * width;
+									int x = 0;
+
+									while (srcPtr < lineEnd && x < width) {
+										int skip = READ_LE_UINT16(srcPtr);
+										srcPtr += 2;
+										x += skip;
+										if (srcPtr >= lineEnd || x >= width) break;
+
+										int runSize = READ_LE_UINT16(srcPtr);
+										srcPtr += 2;
+
+										// Decode RLE within this run
+										byte *runEnd = srcPtr + runSize;
+										while (srcPtr < runEnd && x < width) {
+											byte code = *srcPtr++;
+											int num = (code >> 1) + 1;
+											if (num > width - x) num = width - x;
+
+											if (code & 1) {
+												// RLE run
+												byte color = (srcPtr < runEnd) ? *srcPtr++ : 0;
+												for (int i = 0; i < num && x < width; i++) {
+													lineDst[x++] = color;
+												}
+											} else {
+												// Literal run
+												for (int i = 0; i < num && x < width && srcPtr < runEnd; i++) {
+													lineDst[x++] = *srcPtr++;
+												}
+											}
+										}
+										srcPtr = runEnd;
+									}
+									srcPtr = lineEnd;
+								}
+								frame.valid = true;
+								debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
 							} else {
 								debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
 								frame.valid = false;
 							}
-							
+
+							// Count non-zero pixels to verify frame has content
+							if (frame.valid) {
+								int nonZeroPixels = 0;
+								for (int i = 0; i < width * height; i++) {
+									if (frame.pixels[i] != 0) nonZeroPixels++;
+								}
+								debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
+									userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
+							}
+
 							// Draw immediately to renderBitmap if valid
-							if (frame.valid && renderBitmap) {
+							// Skip immediate draw for Handler 7/8 - these are ship direction sprites
+							// that should be selected based on direction and drawn during post-rendering
+							bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8);
+
+							if (frame.valid && renderBitmap && !skipImmediateDraw) {
 								int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
 								int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-								
+
 								for (int y = 0; y < height && (frame.renderY + y) < bufHeight; y++) {
 									for (int x = 0; x < width && (frame.renderX + x) < pitch; x++) {
 										byte pixel = frame.pixels[y * width + x];
-										if (pixel != 0) {  // 0 = transparent
+										if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
 											int destX = frame.renderX + x;
 											int destY = frame.renderY + y;
 											if (destX >= 0 && destY >= 0) {
@@ -1573,6 +2198,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 									}
 								}
 								debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
+							} else if (skipImmediateDraw) {
+								debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
+									_rebelHandler, userId);
 							}
 							
 							free(fobjData);
@@ -2058,16 +2686,19 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		return;
 	}
 
-	// During intro sequences (non-menu):
+	// During intro sequences (non-menu, non-gameplay):
 	// - Hide the crosshair/cursor (handled by not drawing it)
 	// - Skip all HUD/status bar rendering
 	// - Skip mouse input processing
 	// The mouse cursor is already hidden by SmushPlayer during video playback
-	if (introPlaying) {
+	//
+	// IMPORTANT: During gameplay (_gameState == kStateGameplay), we MUST render HUD
+	// even if video flags have 0x20 set (which can happen for some level videos)
+	if (introPlaying && _gameState != kStateGameplay) {
 		// Track state transition for debugging
 		if (!_introCursorPushed) {
 			_introCursorPushed = true;
-			debug("Rebel2: Intro sequence detected (flags=0x%x) - HUD disabled", _player->_curVideoFlags);
+			debug("Rebel2: Intro sequence detected (flags=0x%x, state=%d) - HUD disabled", _player->_curVideoFlags, _gameState);
 		}
 		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
 		return;
@@ -2075,12 +2706,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// Gameplay mode - restore normal rendering
 		if (_introCursorPushed) {
 			_introCursorPushed = false;
-			debug("Rebel2: Gameplay started - HUD enabled");
+			debug("Rebel2: Gameplay started - HUD enabled (flags=0x%x, state=%d)", _player->_curVideoFlags, _gameState);
 		}
 	}
 
 	// From here on, we're in gameplay mode (not intro)
-	if (!introPlaying) {
+	// Render HUD if NOT in intro mode, OR if we ARE in gameplay state
+	if (!introPlaying || _gameState == kStateGameplay) {
 	// ============================================================
 	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
 	// ============================================================
@@ -2100,44 +2732,101 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 	
 	// ============================================================
-	// STEP 1: Draw embedded SAN cockpit overlay FIRST (from IACT chunks)
+	// STEP 1: Draw embedded SAN HUD overlays FIRST (from IACT chunks)
 	// ============================================================
-	// The cockpit overlay forms the decorative frame at the bottom
-	// userId 1: Left piece at X=0
-	// userId 2: Right piece at X=slot1Width
+	// For Handler 7 (Level 3): HUD elements are scattered across the screen
+	//   - Each frame has its own renderX/renderY position from FOBJ left/top
+	//   - userId 1-11 are different HUD elements
+	//   - userIds at the same position are direction variants (select one based on direction)
+	// For turret handlers: slots 1-2 form a two-part cockpit overlay
+	//   - userId 1: Left piece at X=0
+	//   - userId 2: Right piece at X=slot1Width
 	// These are drawn BEFORE the status bar so status bar appears on top
-	
-	int slot1Width = 0;
-	if (_rebelEmbeddedHud[1].valid && _rebelEmbeddedHud[1].width > 0) {
-		slot1Width = _rebelEmbeddedHud[1].width;
-	}
-	
-	for (int hudSlot = 1; hudSlot <= 2; hudSlot++) {
+
+	// For Handler 7 ship direction: identify ship frames at same position
+	// Level 3 has userId 1 and 2 at position (162, 105) - these are direction variants
+	// We select one based on the current direction to simulate ship turning
+
+	for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
 		EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
 		if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
 			int destX, destY;
-			
-			// Position the two HUD pieces horizontally adjacent
-			if (hudSlot == 1) {
-				destX = 0;
-			} else {
-				destX = slot1Width;
+
+			// For Handler 7: Check if this is a ship direction frame
+			// Ship frames are at the same position - skip all but the selected one
+			if (_rebelHandler == 7) {
+				// Collect all frame IDs in this position group (may not be consecutive)
+				int groupMembers[16];
+				int groupCount = 0;
+
+				for (int id = 1; id < 16; id++) {
+					EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
+					if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
+						g.width == frame.width && g.height == frame.height) {
+						groupMembers[groupCount++] = id;
+					}
+				}
+
+				// If there's more than one frame in this group, select based on direction
+				if (groupCount > 1) {
+					// Map direction index (0-34) to group index (0 to groupCount-1)
+					int selectedOffset = _shipDirectionIndex % groupCount;
+					int selectedId = groupMembers[selectedOffset];
+
+					// Check if selected frame has pixels, if not find one that does
+					EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
+					int nonZero = 0;
+					for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
+						if (selectedFrame.pixels[i] != 0) nonZero++;
+					}
+
+					if (nonZero == 0) {
+						// Selected frame is empty, find another with pixels
+						for (int i = 0; i < groupCount; i++) {
+							EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
+							int altNonZero = 0;
+							for (int j = 0; j < altFrame.width * altFrame.height; j++) {
+								if (altFrame.pixels[j] != 0) altNonZero++;
+							}
+							if (altNonZero > 0) {
+								selectedId = groupMembers[i];
+								break;
+							}
+						}
+					}
+
+					// Only render if this is the selected frame
+					if (hudSlot != selectedId) {
+						continue;  // Skip this frame, render the selected one instead
+					}
+				}
+			}
+
+			// Use the stored render position from the embedded ANIM data
+			// The renderX/renderY are set from FOBJ's left/top values
+			destX = frame.renderX;
+			destY = frame.renderY;
+
+			// For Handler 7: Apply position offset based on ship position
+			// This makes the ship sprite follow the mouse/crosshair
+			if (_rebelHandler == 7 && destX > 100 && destY > 50) {
+				// This appears to be a ship sprite (center of screen)
+				// Offset based on ship position relative to center
+				int16 offsetX = (_shipPosX - 160) / 8;  // Scale down movement
+				int16 offsetY = (_shipPosY - 100) / 8;
+				destX += offsetX;
+				destY += offsetY;
 			}
-			
-			// Position at bottom of 320x200 video content
-			// The cockpit overlay sits at Y = 200 - frameHeight
-			destY = statusBarY - frame.height;
-			if (destY < 0) destY = 0;
 
-			// Apply View Offset for static screen elements
+			// Apply View Offset for all HUD elements
 			destX += _viewX;
 			destY += _viewY;
-			
-			// Draw frame with transparency (pixel 0 = transparent)
+
+			// Draw frame with transparency (pixel 0 and 231 = transparent, matching NUT rendering)
 			for (int y = 0; y < frame.height && (destY + y) < height; y++) {
 				for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
 					byte pixel = frame.pixels[y * frame.width + x];
-					if (pixel != 0) {  // Skip transparent pixels
+					if (pixel != 0 && pixel != 231) {  // Skip transparent pixels (0 and 231/0xE7)
 						int fx = destX + x;
 						int fy = destY + y;
 						if (fx >= 0 && fy >= 0) {
@@ -2248,10 +2937,213 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
 		// Uses sprite 1 again with different clip rect
 		// TODO: Implement lives rendering
-		
+
 		// Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
 		// TODO: Implement score rendering
-	} 
+	}
+
+	// ============================================================
+	// HANDLER 8 SHIP RENDERING (FUN_00401ccf equivalent)
+	// ============================================================
+	// For third-person vehicle missions (Handler 8), draw the player's ship sprite
+	// The ship position is calculated from _shipPosX/_shipPosY
+	//
+	// From FUN_00401ccf disassembly (lines 87-95):
+	// - Ship is drawn when DAT_0047e010 != NULL AND DAT_0043e000 != 5
+	// - Position offset X: (DAT_0043e006 - 0xa0) >> 3 = (shipPosX - 160) >> 3
+	// - Position offset Y: (DAT_0043e008 - 0x28) >> 2 = (shipPosY - 40) >> 2
+	// - Sprite index: param_5 & 1 (0 = normal, 1 = firing)
+	//
+	// The crosshair is drawn at lines 128-135 with base position:
+	// - Low-res: X = offset + 0xa0 (160), Y = offset + 0x69 (105)
+	// - High-res: X = offset*2 + 0x140 (320), Y = offset*2 + 0xd2 (210)
+	//
+	// The ship sprite is drawn using the same offset calculation but passed directly
+	// to FUN_004236e0. The rendering function treats these as screen coordinates.
+	debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
+		_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
+		_shipSprite ? _shipSprite->getNumChars() : 0,
+		_flyShipSprite ? _flyShipSprite->getNumChars() : 0);
+
+	// Handler 7 Ship Rendering (Space Flight - FLY sprites)
+	// Handler 7 uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
+	// Different from Handler 8's POV sprites
+	if (_rebelHandler == 7 && _flyShipSprite && _shipLevelMode != 5) {
+		// Handler 7 position calculation from FUN_0040d836 lines 173-175
+		// Draw position: (transformedX - 0xd4, transformedY - 0x82)
+		// where transformedX/Y come from FUN_0041c720 transformation
+		// For simplicity, we calculate based on ship direction
+
+		// Base position calculation (simplified from assembly)
+		// The ship is drawn at a fixed position offset by direction
+		int baseX = 160;  // Screen center X
+		int baseY = 105;  // Screen center Y (0x69)
+
+		// Add small offset based on ship position for "flight feel"
+		int16 posOffsetX = (_flyShipScreenX - 160) / 10;
+		int16 posOffsetY = (_flyShipScreenY - 100) / 10;
+
+		int shipScreenX = baseX + posOffsetX;
+		int shipScreenY = baseY + posOffsetY;
+
+		int numSprites = _flyShipSprite->getNumChars();
+		int spriteIndex = _shipDirectionIndex;
+
+		// Validate sprite index
+		if (spriteIndex < 0) spriteIndex = 0;
+		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+
+		// Get sprite dimensions and center it
+		int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
+		int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
+		int drawX = shipScreenX - spriteW / 2 + _viewX;
+		int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+		// Draw the ship sprite (DAT_0047fee8 / FLY001)
+		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+
+		// Draw laser overlay if firing and laser sprite loaded (FLY002)
+		if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
+			// Laser sprite uses same direction index for frame animation
+			int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
+			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
+		}
+
+		// Draw targeting overlay if loaded (FLY003) - goes at crosshair position
+		if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
+			// Draw targeting reticle at ship position
+			int targetW = _flyTargetSprite->getCharWidth(0);
+			int targetH = _flyTargetSprite->getCharHeight(0);
+			int targetX = shipScreenX - targetW / 2 + _viewX;
+			int targetY = shipScreenY - targetH / 2 + _viewY;
+			renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
+		}
+
+		debug("Rebel2 Handler7: Ship drawn at (%d,%d) screenPos=(%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
+			drawX, drawY, shipScreenX, shipScreenY, spriteIndex, numSprites,
+			_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+	}
+
+	// Handler 8 Ship Rendering (Third-Person Vehicle - POV sprites)
+	// Handler 8 uses _shipSprite (POV001) with position-based offset
+	else if (_rebelHandler == 8 && _shipSprite && _shipLevelMode != 5) {
+		// Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
+		// The shift operations create a dampened movement effect
+		int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;  // (shipPosX - 160) >> 3
+		int16 displayOffsetY = (_shipPosY - 0x28) >> 2;  // (shipPosY - 40) >> 2
+
+		// Base screen position from crosshair calculation (FUN_00401ccf lines 128-129)
+		// Low-res mode: base X = 0xa0 (160), base Y = 0x69 (105)
+		int shipScreenX = 0xa0 + displayOffsetX;  // 160 + offset
+		int shipScreenY = 0x69 + displayOffsetY;  // 105 + offset
+
+		int numSprites = _shipSprite->getNumChars();
+		int spriteIndex = 0;
+
+		// Select sprite based on direction when multiple sprites are available
+		// The ship sprite sheet is organized as a grid of direction sprites:
+		// - 35 sprites (5x7): Full direction grid
+		// - 25 sprites (5x5): Reduced grid
+		// - 5 sprites: Horizontal direction only
+		// - 2 sprites: Normal (0) and firing (1)
+		// - 1 sprite: Static ship
+		if (numSprites >= 35) {
+			spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
+			if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+		} else if (numSprites >= 25) {
+			int vDir5 = (_shipDirectionV * 5) / 7;
+			spriteIndex = _shipDirectionH * 5 + vDir5;
+			if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+		} else if (numSprites >= 5) {
+			spriteIndex = _shipDirectionH;
+			if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+		} else if (numSprites == 2) {
+			spriteIndex = _shipFiring ? 1 : 0;
+		}
+
+		// Get sprite dimensions and center it at the calculated position
+		int spriteW = _shipSprite->getCharWidth(spriteIndex);
+		int spriteH = _shipSprite->getCharHeight(spriteIndex);
+		int drawX = shipScreenX - spriteW / 2 + _viewX;
+		int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+		// Draw the primary ship sprite (DAT_0047e010)
+		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
+
+		// Draw secondary ship sprite if available (DAT_0047e028)
+		if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
+			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
+		}
+
+		debug("Rebel2 Handler8: Ship drawn at screen(%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
+			drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
+			spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
+	}
+
+	// Fallback: Use embedded HUD frame as ship sprite
+	else if ((_rebelHandler == 7 || _rebelHandler == 8) && _shipLevelMode != 5) {
+		// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
+		// userId=11 contains the ship sprite strip
+		EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
+		if (shipFrame.valid && shipFrame.pixels && shipFrame.width > 0 && shipFrame.height > 0) {
+			// Calculate display offset from raw ship position
+			int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
+			int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
+			int shipScreenX = 0xa0 + displayOffsetX;
+			int shipScreenY = 0x69 + displayOffsetY;
+
+			// Check if this is a sprite strip (multiple directions in one image)
+			// 205 width / 5 directions = 41 pixels per direction
+			int spriteW = shipFrame.width;
+			int spriteH = shipFrame.height;
+			int srcX = 0;
+			int srcY = 0;
+			int numHorizontal = 1;
+			int numVertical = 1;
+
+			// Detect sprite strip layout - look for common patterns
+			if (spriteW >= 200 && spriteW % 5 == 0) {
+				// 5 horizontal directions (like 205 = 41 * 5)
+				numHorizontal = 5;
+				spriteW = shipFrame.width / 5;
+			}
+			if (spriteH >= 350 && spriteH % 7 == 0) {
+				// 7 vertical directions
+				numVertical = 7;
+				spriteH = shipFrame.height / 7;
+			}
+
+			// Select sprite from strip based on direction
+			int hDir = _shipDirectionH;
+			int vDir = _shipDirectionV;
+			if (hDir >= numHorizontal) hDir = numHorizontal - 1;
+			if (vDir >= numVertical) vDir = numVertical - 1;
+			srcX = hDir * spriteW;
+			srcY = vDir * spriteH;
+
+			// Draw position (centered)
+			int drawX = shipScreenX - spriteW / 2 + _viewX;
+			int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+			// Blit from embedded HUD to render buffer
+			for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
+				if (drawY + y < 0) continue;
+				for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
+					if (drawX + x < 0) continue;
+					int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
+					byte pixel = shipFrame.pixels[srcIdx];
+					// Skip transparent pixels (0 and 231)
+					if (pixel != 0 && pixel != 231) {
+						int dstIdx = (drawY + y) * pitch + (drawX + x);
+						renderBitmap[dstIdx] = pixel;
+					}
+				}
+			}
+
+			debug("Rebel2: Ship (embedded HUD) at screen(%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
+				drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
+		}
+	}
 
 	Common::List<enemy>::iterator it;
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -2414,7 +3306,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	// Draw Crosshair/Reticle cursor
 	// Sprite indices based on handler type (from original game disassembly FUN_004089ab, FUN_0040d836, etc):
-	// - Handler 8 (ground vehicle): Index 0x2E (46)
+	// - Handler 8 (third-person vehicle): Index 0x2E (46)
 	// - Handler 7 (space flight): Index 0x2F (47)
 	// - Handler 0x19 (mixed/turret view): Index 0x2F (47)
 	// - Handler 0x26 (full turret): Index varies by levelType and animation
@@ -3732,6 +4624,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 
 	case 2:
 		// Level 2: Has cutscene first, then multiple parts
+		// Level 2 uses Handler 8 (third-person vehicle mode) - set before gameplay
+		_rebelHandler = 8;
 		// First play the cutscene
 		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing cutscene %s", filename.c_str());
@@ -3780,7 +4674,9 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		break;
 
 	case 3:
-		// Level 3: Two gameplay phases
+		// Level 3: Two gameplay phases (space flight)
+		// Level 3 uses Handler 7 (space flight mode) - FUN_0040d836/FUN_0040c3cc
+		_rebelHandler = 7;
 		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
 		splayer->play(filename.c_str(), 12);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bc3e01364ff..290b3ad9af5 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -251,7 +251,7 @@ public:
 	void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
-	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -288,7 +288,7 @@ public:
 	// Current handler type for Rebel Assault 2 (determines crosshair sprite)
 	// Handler 0: Background only
 	// Handler 7: Space flight - uses crosshair sprite 0x2F (47)
-	// Handler 8: Ground vehicle - uses crosshair sprite 0x2E (46)  
+	// Handler 8: Third-person vehicle - uses crosshair sprite 0x2E (46)  
 	// Handler 0x19: Mixed/turret view - uses crosshair sprite 0x2F (47)
 	// Handler 0x26: Full turret - crosshair varies by level type
 	int _rebelHandler;
@@ -315,7 +315,7 @@ public:
 		bool valid;        // True if this slot has valid data
 	};
 	
-	EmbeddedSanFrame _rebelEmbeddedHud[5];  // Index 0 unused, 1-4 for userId slots
+	EmbeddedSanFrame _rebelEmbeddedHud[16];  // HUD overlay slots (userId 0-15)
 	
 	// Check if a partial frame update should be skipped (overlaps with destroyed enemy)
 	bool shouldSkipFrameUpdate(int left, int top, int width, int height) override;
@@ -381,6 +381,75 @@ public:
 	Shot _shots[2];
 	void spawnShot(int x, int y);
 
+	// ======================= Handler 8 Ship System =======================
+	// For third-person vehicle missions (Levels 2, 3), the player controls a ship
+	// that can turn in different directions. The ship sprite comes from
+	// NUT files loaded via IACT opcode 8.
+	//
+	// Based on FUN_00401234 and FUN_00401ccf disassembly:
+	// - DAT_0047e010: Primary ship sprite (POV001, subcase 1)
+	// - DAT_0047e028: Secondary ship sprite (POV004, subcase 3)
+	// - DAT_0047e020: Additional overlay (POV002, subcase 6)
+	// - DAT_0047e018: Additional overlay (POV003, subcase 7)
+	// - DAT_0043e006: Ship X position (raw, needs conversion for display)
+	// - DAT_0043e008: Ship Y position (raw, needs conversion for display)
+	// - DAT_0043e000: Level mode from opcode 6 par3
+
+	NutRenderer *_shipSprite;        // DAT_0047e010 - Primary ship NUT
+	NutRenderer *_shipSprite2;       // DAT_0047e028 - Secondary ship NUT
+	NutRenderer *_shipOverlay1;      // DAT_0047e020 - Additional overlay
+	NutRenderer *_shipOverlay2;      // DAT_0047e018 - Additional overlay
+
+	// Ship position tracking (matches DAT_0043e006/008)
+	// These are "raw" positions that get converted for display
+	int16 _shipPosX;                 // DAT_0043e006
+	int16 _shipPosY;                 // DAT_0043e008
+
+	// Ship target positions (where ship is trying to move to)
+	// Set from mouse/joystick input in opcode 6 processing
+	int16 _shipTargetX;              // DAT_0043e002 - Target X
+	int16 _shipTargetY;              // DAT_0043e004 - Target Y
+
+	// Level mode for handler 8 (different from _rebelLevelType)
+	// Set by opcode 6 par3, affects ship rendering behavior
+	int16 _shipLevelMode;            // DAT_0043e000
+
+	// Ship firing state (from mouse button)
+	bool _shipFiring;
+
+	// Ship direction index for sprite selection (Handler 7)
+	// Calculated from ship position: horizontal * 7 + vertical
+	// horizontal: 0-4 (left to right), vertical: 0-6 (up to down)
+	// Used to select which embedded HUD userId to render
+	int16 _shipDirectionIndex;
+	int16 _shipDirectionH;           // Horizontal direction (0-4, center=2)
+	int16 _shipDirectionV;           // Vertical direction (0-6, center=3)
+
+	// Helper to load a NUT file from IACT chunk data
+	NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
+
+	// ======================= Handler 7 FLY Ship System =======================
+	// For space flight missions (Level 3, etc.), Handler 7 uses a 35-frame
+	// direction-based ship sprite system. The ship visually banks and turns
+	// based on player position using a 5x7 grid of sprites.
+	//
+	// Based on FUN_0040c3cc and FUN_0040d836 disassembly:
+	// - DAT_0047fee8: Ship direction sprites (FLY001, par3=1, 35 frames)
+	// - DAT_0047fef0: Laser fire sprites (FLY002, par3=3)
+	// - DAT_0047fef8: Targeting overlay (FLY003, par3=2)
+	// - DAT_0047ff00: High-res alternative (FLY004, par3=11)
+	// - DAT_0044370c: Ship Y screen position
+	// - DAT_0044370e: Ship X screen position
+
+	NutRenderer *_flyShipSprite;     // DAT_0047fee8 - FLY001 (35 direction frames)
+	NutRenderer *_flyLaserSprite;    // DAT_0047fef0 - FLY002
+	NutRenderer *_flyTargetSprite;   // DAT_0047fef8 - FLY003
+	NutRenderer *_flyHiResSprite;    // DAT_0047ff00 - FLY004
+
+	// Handler 7 screen position (different from Handler 8's raw positions)
+	int16 _flyShipScreenX;           // DAT_0044370e - Ship X screen position
+	int16 _flyShipScreenY;           // DAT_0044370c - Ship Y screen position
+
 	/* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
 	int _difficulty;
 	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 2213db9947d..1b040f6294b 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -50,6 +50,29 @@ NutRenderer::NutRenderer(ScummEngine *vm, const char *filename) :
 		loadFont(filename);
 }
 
+NutRenderer::NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize) :
+	_vm(vm),
+	_numChars(0),
+	_fontHeight(0),
+	_decodedData(0),
+	_2byteColorTable(0),
+	_2byteShadowXOffsetTable(0),
+	_2byteShadowYOffsetTable(0),
+	_2byteMainColor(0),
+	_spacing(vm->_useCJKMode && vm->_language != Common::JA_JPN ? 1 : 0),
+	_2byteSteps(vm->_game.version == 8 ? 4 : 2),
+	_direction(vm->_language == Common::HE_ISR ? -1 : 1) {
+		static const int8 cjkShadowOffsetsX[4] = { -1, 0, 1, 0 };
+		static const int8 cjkShadowOffsetsY[4] = { 0, 1, 0, 0 };
+		_2byteShadowXOffsetTable = &cjkShadowOffsetsX[ARRAYSIZE(cjkShadowOffsetsX) - _2byteSteps];
+		_2byteShadowYOffsetTable = &cjkShadowOffsetsY[ARRAYSIZE(cjkShadowOffsetsY) - _2byteSteps];
+		_2byteColorTable = new uint8[_2byteSteps];
+		memset(_2byteColorTable, 0, _2byteSteps);
+		_2byteMainColor = &_2byteColorTable[_2byteSteps - 1];
+		memset(_chars, 0, sizeof(_chars));
+		loadFontFromData(data, dataSize);
+}
+
 NutRenderer::~NutRenderer() {
 	delete[] _decodedData;
 	delete[] _2byteColorTable;
@@ -187,6 +210,110 @@ void NutRenderer::loadFont(const char *filename) {
 	delete[] dataSrc;
 }
 
+void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
+	if (!data || dataSize < 8) {
+		warning("NutRenderer::loadFontFromData: data too small (%d bytes)", dataSize);
+		return;
+	}
+
+	uint32 tag = READ_BE_UINT32(data);
+	if (tag != MKTAG('A','N','I','M')) {
+		warning("NutRenderer::loadFontFromData: no ANIM chunk (got %08x)", tag);
+		return;
+	}
+
+	uint32 length = READ_BE_UINT32(data + 4);
+	if (length > (uint32)(dataSize - 8)) {
+		warning("NutRenderer::loadFontFromData: ANIM size (%d) exceeds data size (%d)", length, dataSize);
+		length = dataSize - 8;
+	}
+
+	const byte *dataSrc = data + 8;
+
+	if (READ_BE_UINT32(dataSrc) != MKTAG('A','H','D','R')) {
+		warning("NutRenderer::loadFontFromData: no AHDR chunk in font data");
+		return;
+	}
+
+	// Parse the font data (same logic as loadFont)
+	_numChars = READ_LE_UINT16(dataSrc + 10);
+	if (_numChars > ARRAYSIZE(_chars)) {
+		warning("NutRenderer::loadFontFromData: numChars (%d) exceeds max", _numChars);
+		_numChars = ARRAYSIZE(_chars);
+	}
+
+	delete[] _decodedData;
+	_decodedData = nullptr;
+	memset(_chars, 0, sizeof(_chars));
+
+	uint32 offset = 0;
+	uint32 decodedLength = 0;
+	int l;
+
+	for (l = 0; l < _numChars; l++) {
+		if (offset + 8 > length)
+			break;
+		offset += READ_BE_UINT32(dataSrc + offset + 4) + 16;
+		if (offset + 18 > length)
+			break;
+		int width = READ_LE_UINT16(dataSrc + offset + 14);
+		_fontHeight = READ_LE_UINT16(dataSrc + offset + 16);
+		decodedLength += width * _fontHeight;
+	}
+
+	debug(1, "NutRenderer::loadFontFromData() - numChars=%d decodedLength=%d", _numChars, decodedLength);
+
+	_decodedData = new byte[decodedLength];
+	byte *decodedPtr = _decodedData;
+
+	offset = 0;
+	for (l = 0; l < _numChars; l++) {
+		if (offset + 8 > length)
+			break;
+		offset += READ_BE_UINT32(dataSrc + offset + 4) + 8;
+		if (offset + 8 > length)
+			break;
+		if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','R','M','E')) {
+			warning("NutRenderer::loadFontFromData: no FRME chunk %d (offset %x)", l, offset);
+			break;
+		}
+		offset += 8;
+		if (offset + 22 > length)
+			break;
+		if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','O','B','J')) {
+			warning("NutRenderer::loadFontFromData: no FOBJ chunk in FRME chunk %d (offset %x)", l, offset);
+			break;
+		}
+		int codec = READ_LE_UINT16(dataSrc + offset + 8);
+		_chars[l].width = READ_LE_UINT16(dataSrc + offset + 14);
+		_chars[l].height = READ_LE_UINT16(dataSrc + offset + 16);
+		_chars[l].src = decodedPtr;
+
+		decodedPtr += (_chars[l].width * _chars[l].height);
+
+		if (codec == 44) {
+			memset(_chars[l].src, kSmush44TransparentColor, _chars[l].width * _chars[l].height);
+			_chars[l].transparency = kSmush44TransparentColor;
+		} else {
+			memset(_chars[l].src, kDefaultTransparentColor, _chars[l].width * _chars[l].height);
+			_chars[l].transparency = kDefaultTransparentColor;
+		}
+
+		const uint8 *fobjptr = dataSrc + offset + 22;
+		switch (codec) {
+		case 1:
+			codec1(_chars[l].src, fobjptr, _chars[l].width, _chars[l].height, _chars[l].width);
+			break;
+		case 21:
+		case 44:
+			codec21(_chars[l].src, fobjptr, _chars[l].width, _chars[l].height, _chars[l].width);
+			break;
+		default:
+			warning("NutRenderer::loadFontFromData: unknown codec: %d", codec);
+		}
+	}
+}
+
 int NutRenderer::getCharWidth(byte c) const {
 	if (c >= 0x80 && _vm->_useCJKMode)
 		return _vm->_2byteWidth + _spacing;
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index d88d0b1f572..92abdfb246f 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -62,9 +62,11 @@ protected:
 	void codec21(byte *dst, const byte *src, int width, int height, int pitch);
 
 	void loadFont(const char *filename);
+	void loadFontFromData(const byte *data, int32 dataSize);
 
 public:
 	NutRenderer(ScummEngine *vm, const char *filename);
+	NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize);
 	virtual ~NutRenderer();
 	int getNumChars() const { return _numChars; }
 
@@ -76,7 +78,7 @@ public:
 	int getCharHeight(byte c) const;
 	const byte *getCharData(byte c);
 	byte getCharTransparency(byte c) const { return _chars[c].transparency; }
-	byte getBpp() const { return _bpp; }
+	byte getBpp() const { return 8; }
 
 	int getFontHeight() const { return _fontHeight; }
 };
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index df67aaff161..f88322f6846 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -405,15 +405,17 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 		code, flags, unknown, userId, subSize);
 
 	// Route to procIACT for:
-	// 1. Non-audio IACT (code != 8 or flags != 46)
-	// 2. RA2 menu/graphics IACT (userId >= 1000 indicates graphics data, not audio)
+	// 1. Non-audio IACT (code != 8 or flags != 46) - Full Throttle uses code=8, flags=46 for audio
+	// 2. ALL Rebel Assault 2 IACTs - RA2 uses a different IACT format where code=opcode, flags=par2
+	//    RA2 audio is handled through PSAD chunks, not IACT, so all RA2 IACTs go to procIACT
 	bool isAudioIACT = (code == 8) && (flags == 46);
-	bool isRA2GraphicsIACT = (_vm->_game.id == GID_REBEL2) && (userId >= 1000);
+	bool isRA2 = (_vm->_game.id == GID_REBEL2);
 
-	if (!isAudioIACT || isRA2GraphicsIACT) {
-		debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2GraphicsIACT=%d)",
-			isAudioIACT, isRA2GraphicsIACT);
-		_vm->_insane->procIACT(_dst, 0, 0, 0, b, 0, 0, code, flags, unknown, userId);
+	if (!isAudioIACT || isRA2) {
+		debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2=%d)",
+			isAudioIACT, isRA2);
+		// Pass subSize - 8 as the remaining payload size (after 8-byte header)
+		_vm->_insane->procIACT(_dst, 0, 0, 0, b, subSize - 8, 0, code, flags, unknown, userId);
 		return;
 	}
 


Commit: db7b16c04410cd6346970ba9ff8bee0d4c137d97
    https://github.com/scummvm/scummvm/commit/db7b16c04410cd6346970ba9ff8bee0d4c137d97
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:54+02:00

Commit Message:
SCUMM: RA2: Fix level 1 loading regression

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index aaabb44372f..57ab02fc9c1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1597,16 +1597,40 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 
 							if (!loadedAsNut) {
 								// Load as embedded SAN (HUD overlays)
-								// The userId comes from par4 (not par3!)
+								// The userId parameter varies by handler type:
+								//
+								// Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
+								//   par3=1: GRD001 (Primary HUD overlay)
+								//   par3=2: GRD002 (Secondary HUD graphics)
+								//   par3=4: GRD010 (Ship cockpit frame)
+								//   etc.
+								//
+								// Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
+								// Other handlers: Use par4 for userId
+								//
 								// par4 values seen in Level 3:
 								//   1000 = audio track
 								//   1-11 = HUD overlay slots
-								// The embedded HUD system uses userId to track different overlay elements
-								int userId = par4;
+								//
+								// Note: Handler may not be set yet if opcode 8 arrives before opcode 6
+								// Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
+								// or invalid, prefer par3
+								int userId;
+								bool usePar3 = (_rebelHandler == 0x26 || _rebelHandler == 0x19);
+
+								// Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
+								// out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
+								if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
+									usePar3 = true;
+									debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
+								}
+
+								userId = usePar3 ? par3 : par4;
 
 								// Audio tracks (userId >= 1000) are handled separately, skip them
-								if (userId < 1000) {
-									debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d", userId);
+								if (userId > 0 && userId < 1000) {
+									debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
+										userId, _rebelHandler, par3, par4);
 									loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
 								}
 							}
@@ -2807,6 +2831,24 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			destX = frame.renderX;
 			destY = frame.renderY;
 
+			// For Handler 0x26 (turret) and 0x19 (mixed): Cockpit overlays positioned at bottom
+			// The cockpit pieces (slots 1-2) should be rendered just above the status bar (Y=180)
+			// The FOBJ top value might be 0, so we override it for cockpit pieces
+			if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
+				// Cockpit overlay position: just above status bar
+				// Status bar is at Y=180, cockpit sits right above it
+				// For slot 1 (left piece): X = 0
+				// For slot 2 (right piece): X = width of slot 1
+				if (hudSlot == 1) {
+					destX = 0;
+				} else if (hudSlot == 2 && _rebelEmbeddedHud[1].valid) {
+					// Position slot 2 right after slot 1
+					destX = _rebelEmbeddedHud[1].width;
+				}
+				// Y position: status bar (180) minus cockpit height
+				destY = 180 - frame.height;
+			}
+
 			// For Handler 7: Apply position offset based on ship position
 			// This makes the ship sprite follow the mouse/crosshair
 			if (_rebelHandler == 7 && destX > 100 && destY > 50) {
@@ -4617,6 +4659,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 	switch (levelId) {
 	case 1:
 		// Level 1: Single gameplay file (01P01.SAN)
+		// Level 1 uses Handler 0x26 (turret mode) - set before gameplay
+		_rebelHandler = 0x26;
 		filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
 		splayer->play(filename.c_str(), 12);
@@ -4699,6 +4743,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		break;
 
 	case 4:
+		_rebelHandler = 0x26;
 		// Level 4: Has cutscene, then single gameplay
 		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing cutscene %s", filename.c_str());


Commit: ac86b2729c880933de819320ffa571be05252ceb
    https://github.com/scummvm/scummvm/commit/ac86b2729c880933de819320ffa571be05252ceb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:54+02:00

Commit Message:
SCUMM: RA2: Implement collision zones

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 57ab02fc9c1..453f44f1744 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -227,6 +227,19 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_explosions[i].counter = 0;
 	}
 
+	// Initialize collision zone system (for Level 3 pilot ship obstacle avoidance)
+	_primaryZoneCount = 0;
+	_secondaryZoneCount = 0;
+	for (i = 0; i < kMaxCollisionZones; i++) {
+		_primaryZones[i].active = false;
+		_secondaryZones[i].active = false;
+	}
+	_corridorLeftX = 0;
+	_corridorTopY = 0;
+	_corridorRightX = 320;
+	_corridorBottomY = 200;
+	_hitCooldown = 0;
+
 	for (i = 0; i < 2; i++) {
 		_shots[i].active = false;
 		_shots[i].counter = 0;
@@ -251,6 +264,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_shipTargetX = 0xa0;
 	_shipTargetY = 0x28;
 	_shipLevelMode = 0;
+	_flyControlMode = 0;   // DAT_004437c0 - Start in flight-only mode (no shooting)
 	_shipFiring = false;
 	_shipDirectionH = 2;   // Start centered horizontally (0-4 range)
 	_shipDirectionV = 3;   // Start centered vertically (0-6 range)
@@ -775,10 +789,17 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		iactRebel2Opcode3(b, par2, par3, par4);
 	}
 	else if (par1 == 5) {
-		// Opcode 5: Special handling based on par2 value
-		// Disassembly shows sub-opcodes 0xD (13) and 0xE (14)
+		// Opcode 5: Collision Zone Registration (FUN_004033cf case 5)
+		// Sub-opcode 0x0D (13) = Primary collision zones (obstacles)
+		// Sub-opcode 0x0E (14) = Secondary collision zones (boundaries)
+		// par2 is the sub-opcode that determines which zone table to use
 		debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
 
+		if (par2 == 0x0D || par2 == 0x0E) {
+			// Register the collision zone from the remaining IACT data
+			registerCollisionZone(b, par2);
+		}
+
 	} else if (par1 == 7) {
 		// Opcode 7: Sprite/HUD control for Handler 7 (space flight levels like Level 3)
 		// par2 = control type (41 = sprite selection?)
@@ -1102,8 +1123,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	// Handler 7 specific logic (space flight) - FUN_0040d836 / FUN_0040c3cc
 	// Used for Level 3 and similar space combat levels
 	if (_rebelHandler == 7) {
-		// Set level mode (same as Handler 8)
-		_shipLevelMode = par3;
+		// Set control mode (DAT_004437c0 = par3 in FUN_40C3CC case 4)
+		// This determines shooting capability:
+		//   Mode 0: Flight/avoid mode - no shooting
+		//   Mode 1: Alternate flight mode - no shooting
+		//   Mode 2: Combat mode - shooting ENABLED
+		_flyControlMode = par3;
+		debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
+			par3, (par3 == 2) ? "ENABLED" : "DISABLED");
 
 		// If par4 == 1, enable status bar
 		if (par4 == 1) {
@@ -2548,6 +2575,152 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
 	drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
 }
 
+// ============================================================
+// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
+// ============================================================
+// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
+// Zones are quadrilaterals registered via IACT opcode 5
+
+void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode) {
+	// IACT Opcode 5 data layout (from info.md):
+	//   +0x00: opcode (5) - already read by caller
+	//   +0x02: sub-opcode (0x0D or 0x0E) - passed as parameter
+	//   +0x04: par3 (flags)
+	//   +0x06: zoneType
+	//   +0x08: frameStart
+	//   +0x0A: frameEnd
+	//   +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
+	//
+	// The stream position is currently at offset +0x04 (after opcode and sub-opcode)
+
+	int16 par3 = b.readSint16LE();       // +0x04 (flags - unused for now)
+	(void)par3;  // Suppress unused variable warning
+	int16 zoneType = b.readSint16LE();   // +0x06
+	int16 frameStart = b.readSint16LE(); // +0x08
+	int16 frameEnd = b.readSint16LE();   // +0x0A
+	int16 x1 = b.readSint16LE();         // +0x0C
+	int16 y1 = b.readSint16LE();         // +0x0E
+	int16 x2 = b.readSint16LE();         // +0x10
+	int16 y2 = b.readSint16LE();         // +0x12
+	int16 x3 = b.readSint16LE();         // +0x14
+	int16 y3 = b.readSint16LE();         // +0x16
+	int16 x4 = b.readSint16LE();         // +0x18
+	int16 y4 = b.readSint16LE();         // +0x1A
+
+	CollisionZone zone;
+	zone.x1 = x1;
+	zone.y1 = y1;
+	zone.x2 = x2;
+	zone.y2 = y2;
+	zone.x3 = x3;
+	zone.y3 = y3;
+	zone.x4 = x4;
+	zone.y4 = y4;
+	zone.frameStart = frameStart;
+	zone.frameEnd = frameEnd;
+	zone.zoneType = zoneType;
+	zone.subOpcode = subOpcode;
+	zone.active = true;
+
+	// Register zone into appropriate table based on sub-opcode
+	if (subOpcode == 0x0D && _primaryZoneCount < kMaxCollisionZones) {
+		// Primary collision zones (obstacles)
+		_primaryZones[_primaryZoneCount++] = zone;
+		debug("Rebel2: Registered PRIMARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+			_primaryZoneCount - 1, zoneType, frameStart, frameEnd,
+			x1, y1, x2, y2, x3, y3, x4, y4);
+	} else if (subOpcode == 0x0E && _secondaryZoneCount < kMaxCollisionZones) {
+		// Secondary collision zones (boundaries)
+		_secondaryZones[_secondaryZoneCount++] = zone;
+		debug("Rebel2: Registered SECONDARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+			_secondaryZoneCount - 1, zoneType, frameStart, frameEnd,
+			x1, y1, x2, y2, x3, y3, x4, y4);
+	} else {
+		debug("Rebel2: WARNING - Could not register zone (subOpcode=%d, primary=%d, secondary=%d)",
+			subOpcode, _primaryZoneCount, _secondaryZoneCount);
+	}
+}
+
+void InsaneRebel2::resetCollisionZones() {
+	// Reset zone counters at end of frame (FUN_403240 equivalent)
+	// This clears the zone tables so they can be rebuilt from the next frame's IACT chunks
+	_primaryZoneCount = 0;
+	_secondaryZoneCount = 0;
+}
+
+void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
+                            int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
+	// Draw a quadrilateral by connecting its 4 vertices with lines
+	// Vertex order: top-left (1), top-right (2), bottom-right (3), bottom-left (4)
+	drawLine(dst, pitch, width, height, x1, y1, x2, y2, color);  // Top edge
+	drawLine(dst, pitch, width, height, x2, y2, x3, y3, color);  // Right edge
+	drawLine(dst, pitch, width, height, x3, y3, x4, y4, color);  // Bottom edge
+	drawLine(dst, pitch, width, height, x4, y4, x1, y1, color);  // Left edge
+}
+
+void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int height, byte color) {
+	// Draw all active collision zones as wireframe quadrilaterals for debugging
+	// Uses different colors for primary vs secondary zones
+
+	const byte primaryColor = 44;    // Bright red for primary (obstacle) zones
+	const byte secondaryColor = 47;  // Yellow for secondary (boundary) zones
+
+	// Draw primary zones (sub-opcode 0x0D - obstacles)
+	for (int i = 0; i < _primaryZoneCount; i++) {
+		CollisionZone &zone = _primaryZones[i];
+		if (!zone.active) continue;
+
+		// Apply view offset to convert from video coords to screen coords
+		int x1 = zone.x1 + _viewX;
+		int y1 = zone.y1 + _viewY;
+		int x2 = zone.x2 + _viewX;
+		int y2 = zone.y2 + _viewY;
+		int x3 = zone.x3 + _viewX;
+		int y3 = zone.y3 + _viewY;
+		int x4 = zone.x4 + _viewX;
+		int y4 = zone.y4 + _viewY;
+
+		drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, primaryColor);
+	}
+
+	// Draw secondary zones (sub-opcode 0x0E - boundaries)
+	for (int i = 0; i < _secondaryZoneCount; i++) {
+		CollisionZone &zone = _secondaryZones[i];
+		if (!zone.active) continue;
+
+		// Apply view offset
+		int x1 = zone.x1 + _viewX;
+		int y1 = zone.y1 + _viewY;
+		int x2 = zone.x2 + _viewX;
+		int y2 = zone.y2 + _viewY;
+		int x3 = zone.x3 + _viewX;
+		int y3 = zone.y3 + _viewY;
+		int x4 = zone.x4 + _viewX;
+		int y4 = zone.y4 + _viewY;
+
+		drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, secondaryColor);
+	}
+
+	// Draw corridor boundaries as a rectangle (from IACT opcode 7)
+	if (_corridorLeftX != 0 || _corridorRightX != 320) {
+		const byte corridorColor = 45;  // Cyan for corridor boundaries
+		// Draw vertical lines for left/right boundaries
+		drawLine(dst, pitch, width, height,
+			_corridorLeftX + _viewX, _corridorTopY + _viewY,
+			_corridorLeftX + _viewX, _corridorBottomY + _viewY, corridorColor);
+		drawLine(dst, pitch, width, height,
+			_corridorRightX + _viewX, _corridorTopY + _viewY,
+			_corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
+		// Draw horizontal lines for top/bottom boundaries
+		drawLine(dst, pitch, width, height,
+			_corridorLeftX + _viewX, _corridorTopY + _viewY,
+			_corridorRightX + _viewX, _corridorTopY + _viewY, corridorColor);
+		drawLine(dst, pitch, width, height,
+			_corridorLeftX + _viewX, _corridorBottomY + _viewY,
+			_corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
+	}
+}
+
 void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
 	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
 
@@ -2622,8 +2795,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	
 	_player->setScrollOffset(_viewX, _viewY);
 
-	processMouse();
-
 	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
 	// Based on FUN_004089ab:
 	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
@@ -2710,33 +2881,42 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		return;
 	}
 
-	// During intro sequences (non-menu, non-gameplay):
-	// - Hide the crosshair/cursor (handled by not drawing it)
-	// - Skip all HUD/status bar rendering
-	// - Skip mouse input processing
-	// The mouse cursor is already hidden by SmushPlayer during video playback
+	// During intro/cinematic sequences:
+	// - Hide the mouse cursor (original: ShowCursor(0) at startup in FUN_00420c70)
+	// - Skip all HUD/status bar/crosshair rendering
+	// - Skip mouse input processing (no shooting during intros)
 	//
-	// IMPORTANT: During gameplay (_gameState == kStateGameplay), we MUST render HUD
-	// even if video flags have 0x20 set (which can happen for some level videos)
-	if (introPlaying && _gameState != kStateGameplay) {
+	// Original behavior from FUN_00403240:
+	// - if (DAT_0047a814 == 0) { switch(DAT_0047ee84) { ... } }
+	// - DAT_0047ee84 (handler) is only set by IACT opcode 6 during gameplay videos
+	// - Cinematics/intros don't have opcode 6, so handler stays 0
+	// - We use _rebelHandler == 0 as the primary indicator for intro/cinematic mode
+	if (_rebelHandler == 0) {
+		// Hide mouse cursor during intro - no crosshair, no clicking
+		CursorMan.showMouse(false);
+
 		// Track state transition for debugging
 		if (!_introCursorPushed) {
 			_introCursorPushed = true;
-			debug("Rebel2: Intro sequence detected (flags=0x%x, state=%d) - HUD disabled", _player->_curVideoFlags, _gameState);
+			debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
+				  _player->_curVideoFlags, _gameState);
 		}
 		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
 		return;
 	} else {
-		// Gameplay mode - restore normal rendering
+		// Gameplay mode - handler was set by IACT opcode 6
 		if (_introCursorPushed) {
 			_introCursorPushed = false;
-			debug("Rebel2: Gameplay started - HUD enabled (flags=0x%x, state=%d)", _player->_curVideoFlags, _gameState);
+			debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
+				  _rebelHandler, _player->_curVideoFlags, _gameState);
 		}
 	}
 
-	// From here on, we're in gameplay mode (not intro)
-	// Render HUD if NOT in intro mode, OR if we ARE in gameplay state
-	if (!introPlaying || _gameState == kStateGameplay) {
+	// From here on, we're in gameplay mode (_rebelHandler != 0)
+	// Process mouse input for shooting
+	// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
+	processMouse();
+
 	// ============================================================
 	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
 	// ============================================================
@@ -3321,6 +3501,16 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
+	// ============================================================
+	// COLLISION ZONE VISUALIZATION (for debugging Level 3 pilot mode)
+	// ============================================================
+	// Draw collision zones as wireframe quadrilaterals when in Handler 7 (space flight)
+	// or Handler 8 (ground vehicle) modes. This helps verify parsing is correct.
+	// The zones are drawn BEFORE the crosshair so they appear under the UI.
+	if (_rebelHandler == 7 || _rebelHandler == 8) {
+		drawCollisionZones(renderBitmap, pitch, width, height, 0);
+	}
+
 	// ============================================================
 	// TARGET LOCK DETECTION (DAT_00443676 equivalent)
 	// ============================================================
@@ -3413,8 +3603,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// This is called after all other rendering so it appears on top
 	renderScoreHUD(renderBitmap, pitch, width, height, 0);
 
-	} // End of if (!introPlaying) block
-
 	// ============================================================
 	// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
 	// ============================================================
@@ -3430,6 +3618,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			resetIt->active = false;
 		}
 	}
+
+	// Reset collision zone counters for next frame (DAT_0047ee74/DAT_0047ee78 = 0)
+	// The zones are rebuilt from IACT opcode 5 chunks each frame
+	resetCollisionZones();
 }
 
 // ========== Audio Handling for Rebel Assault 2 ==========
@@ -4617,6 +4809,21 @@ void InsaneRebel2::playMissionBriefing() {
 	splayer->play("OPEN/O_LEVEL.SAN", 12);
 }
 
+void InsaneRebel2::playCinematic(const char *filename) {
+	// Play a cinematic/cutscene video with proper intro mode setup
+	// This helper ensures:
+	// 1. Handler is reset to 0 (no HUD, no shooting)
+	// 2. Video flags are set to 0x20 (cinematic mode)
+	//
+	// Original: DAT_0047ee84 is only set by IACT opcode 6 during gameplay videos
+	// Cinematics don't have opcode 6, so handler stays 0
+	_rebelHandler = 0;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x20);  // Cinematic mode
+	splayer->play(filename, 12);
+}
+
 void InsaneRebel2::playLevelBegin(int levelId) {
 	// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
 	// Emulates FUN_004171c5 call in each level handler
@@ -4626,10 +4833,7 @@ void InsaneRebel2::playLevelBegin(int levelId) {
 	Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
 
 	debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);  // Cinematic mode
-	splayer->play(filename.c_str(), 12);
+	playCinematic(filename.c_str());
 }
 
 bool InsaneRebel2::playLevelGameplay(int levelId) {
@@ -5111,8 +5315,7 @@ int InsaneRebel2::runLevel2() {
 	int bonusCount = 0;  // Tracks special events (local_1c in assembly)
 
 	// Play cutscene (02CUT.SAN)
-	splayer->setCurVideoFlags(0x20);
-	splayer->play("LEV02/02CUT.SAN", 12);
+	playCinematic("LEV02/02CUT.SAN");
 	if (_vm->shouldQuit()) return kLevelQuit;
 
 	// Play level beginning cinematic (02BEG.SAN)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 290b3ad9af5..bd48a8b016d 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -196,6 +196,9 @@ public:
 	// Returns suffix like "A", "B", "C" for DIE_X.SAN
 	Common::String selectDeathVideoVariant(int levelId, int phase, int frame);
 
+	// Play cinematic video by filename
+	void playCinematic(const char *filename);
+
 	// Play death video with proper variant selection
 	void playLevelDeathVariant(int levelId, int phase, int frame);
 
@@ -340,6 +343,62 @@ public:
 	Explosion _explosions[5];
 	void spawnExplosion(int x, int y, int objectHalfWidth);
 
+	// ======================= Collision Zone System =======================
+	// For Level 3 "pilot" ship obstacle avoidance (FUN_40E35E, FUN_40C3CC)
+	// Collision zones are quadrilaterals defined by IACT Opcode 5
+	// The player's ship position is tested against these zones each frame
+	//
+	// Zone Data Layout from IACT chunk:
+	//   +0x00: opcode (5)
+	//   +0x02: sub-opcode (0x0D = primary, 0x0E = secondary)
+	//   +0x04: par3 (flags)
+	//   +0x06: zoneType (e.g., 5 for damage zones)
+	//   +0x08: frameStart
+	//   +0x0A: frameEnd
+	//   +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
+
+	struct CollisionZone {
+		int16 x1, y1;  // Top-left
+		int16 x2, y2;  // Top-right
+		int16 x3, y3;  // Bottom-right
+		int16 x4, y4;  // Bottom-left
+		int16 frameStart;
+		int16 frameEnd;
+		int16 zoneType;
+		int16 subOpcode;  // 0x0D = primary, 0x0E = secondary
+		bool active;
+	};
+
+	// Two zone tables matching retail DAT_0043fb00 (primary) and DAT_0043f9c8 (secondary)
+	static const int kMaxCollisionZones = 5;
+	CollisionZone _primaryZones[kMaxCollisionZones];    // Sub-opcode 0x0D zones
+	CollisionZone _secondaryZones[kMaxCollisionZones];  // Sub-opcode 0x0E zones
+	int _primaryZoneCount;
+	int _secondaryZoneCount;
+
+	// Corridor boundaries from IACT opcode 7 sub-opcodes 1 and 2
+	int16 _corridorLeftX;    // DAT_00443b0a - Left X boundary
+	int16 _corridorTopY;     // DAT_00443b0c - Top Y boundary
+	int16 _corridorRightX;   // DAT_00443b0e - Right X boundary
+	int16 _corridorBottomY;  // DAT_00443b10 - Bottom Y boundary
+
+	// Hit cooldown timer (DAT_0044374c) - prevents rapid damage stacking
+	int16 _hitCooldown;
+
+	// Draw collision zone quadrilaterals for visualization/debugging
+	// Called from procPostRendering when collision zones should be visible
+	void drawCollisionZones(byte *dst, int pitch, int width, int height, byte color);
+
+	// Draw a single quadrilateral (4 edges)
+	void drawQuad(byte *dst, int pitch, int width, int height, 
+	              int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color);
+
+	// Register a collision zone from IACT opcode 5 data
+	void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode);
+
+	// Reset collision zone counters (called at end of frame)
+	void resetCollisionZones();
+
 	int16 _playerDamage;  // Legacy damage counter (kept for compatibility/telemetry)
 	int16 _playerShield;  // Shields: 0..255 where 255 = full
 	int16 _playerLives;
@@ -414,6 +473,16 @@ public:
 	// Set by opcode 6 par3, affects ship rendering behavior
 	int16 _shipLevelMode;            // DAT_0043e000
 
+	// Control mode for Handler 7 (space flight) - DAT_004437c0
+	// Set by IACT opcode 6 par3 when handler is 7
+	// Determines shooting capability and collision zone type:
+	//   Mode 0: Flight/avoid mode - no shooting, uses secondary zones (sub-opcode 0x0E)
+	//   Mode 1: Alternate flight mode - no shooting, uses primary zones (sub-opcode 0x0D)
+	//   Mode 2: Combat mode - shooting ENABLED, crosshair shown, uses secondary zones
+	// In Level 3's first sequence, par3=0 (no shooting - pure obstacle avoidance)
+	// In combat sequences, par3=2 (shooting enabled)
+	int16 _flyControlMode;           // DAT_004437c0
+
 	// Ship firing state (from mouse button)
 	bool _shipFiring;
 


Commit: a4bbf9032e8b31d2acad272aaf05aa17a75c46da
    https://github.com/scummvm/scummvm/commit/a4bbf9032e8b31d2acad272aaf05aa17a75c46da
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:54+02:00

Commit Message:
SCUMM: RA2: Load low-resolution cockpit HUD

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 453f44f1744..89be33ba7e8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -100,8 +100,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
 
 	_enemies.clear();
-	_rebelHandler = 8;  // Default to Handler 8 (third-person vehicle) for Level 1
+	_rebelHandler = 0;  // Not set yet - will be set by IACT opcode 6
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
+	_rebelStatusBarSprite = 0;  // 0 = disabled, 5 or 53 = enabled (set by IACT opcode 6 par4==1)
 	_introCursorPushed = false;  // Cursor state tracking for intro sequences
 
 	_playerDamage = 0;
@@ -278,6 +279,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_flyShipScreenX = 0xd4;      // Start at center (212) - matches DAT_00443708 default
 	_flyShipScreenY = 0x82;      // Start at center (130) - matches DAT_0044370a default
 
+	// Initialize Handler 0x26 turret HUD overlay system
+	_hudOverlayNut = nullptr;    // DAT_0047fe78 - Primary HUD overlay (GRD files, animated)
+	_hudOverlay2Nut = nullptr;   // DAT_0047fe80 - Secondary HUD overlay
+
 	// Initialize audio system for RA2 (since we don't use iMUSE)
 	_audioSampleRate = 11025;  // RA2 audio is 11025 Hz, not 22050 Hz
 	for (i = 0; i < kRA2MaxAudioTracks; i++) {
@@ -343,6 +348,10 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _flyTargetSprite;
 	delete _flyHiResSprite;
 
+	// Clean up Handler 0x26 turret HUD overlays
+	delete _hudOverlayNut;
+	delete _hudOverlay2Nut;
+
 	// Clean up embedded HUD overlays
 	for (int i = 0; i < 16; i++) {
 		free(_rebelEmbeddedHud[i].pixels);
@@ -1577,88 +1586,142 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 							b.seek(animStreamPos);
 							b.read(animData, toCopy);
 
-							// Handler 8 (Third-Person Vehicle) - FUN_00401234 case 6:
-							// par3 == 1: POV001 -> DAT_0047e010 (primary ship)
-							// par3 == 3: POV004 -> DAT_0047e028 (secondary ship)
-							// par3 == 6: POV002 -> DAT_0047e020 (overlay 1)
-							// par3 == 7: POV003 -> DAT_0047e018 (overlay 2)
-							bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
+							// Handler-dependent NUT loading for opcode 8:
+							//
+							// Handler 0x26/0x19 (turret) - FUN_00407fcb:
+							//   par3 == 1: Low-res primary HUD (GRD001) -> DAT_0047fe78
+							//   par3 == 2: High-res primary HUD (skip in low-res mode)
+							//   par3 == 3: Low-res secondary HUD (GRD010) -> DAT_0047fe80
+							//   par3 == 4: High-res secondary HUD (skip in low-res mode)
+							//
+							// Handler 8 (vehicle) - FUN_00401234:
+							//   par3 == 1: POV001 - Primary ship sprite -> DAT_0047e010
+							//   par3 == 3: POV004 - Secondary ship sprite -> DAT_0047e028
+							//   par3 == 6: POV002 - Ship overlay 1 -> DAT_0047e020
+							//   par3 == 7: POV003 - Ship overlay 2 -> DAT_0047e018
+							//
+							// ScummVM runs at 320x200 (low-res), so turret HUD uses par3 == 1/3 only.
 							bool loadedAsNut = false;
 
-							if (_rebelHandler == 8 && isHandler8Par3) {
-								// Try loading as NUT (ship sprites for Handler 8)
-								NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
-								if (newNut && newNut->getNumChars() > 0) {
-									debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
-										par3, newNut->getNumChars(), _rebelHandler);
+							// Handler 0x26/0x19 (turret modes): Load HUD overlays
+							if (_rebelHandler == 0x26 || _rebelHandler == 0x19) {
+								if (par3 == 1) {
+									// Low-res primary HUD overlay
+									NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+									if (newNut && newNut->getNumChars() > 0) {
+										debug("Rebel2 Opcode 8: Loaded turret HUD NUT par3=%d with %d sprites (handler=0x%x, low-res)",
+											par3, newNut->getNumChars(), _rebelHandler);
+										delete _hudOverlayNut;
+										_hudOverlayNut = newNut;
+										loadedAsNut = true;
+									} else {
+										debug("Rebel2 Opcode 8: Turret HUD NUT load failed for par3=%d", par3);
+										delete newNut;
+									}
+								} else if (par3 == 3) {
+									// Low-res secondary HUD overlay
+									NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+									if (newNut && newNut->getNumChars() > 0) {
+										debug("Rebel2 Opcode 8: Loaded turret HUD2 NUT par3=%d with %d sprites (handler=0x%x, low-res)",
+											par3, newNut->getNumChars(), _rebelHandler);
+										delete _hudOverlay2Nut;
+										_hudOverlay2Nut = newNut;
+										loadedAsNut = true;
+									} else {
+										debug("Rebel2 Opcode 8: Turret HUD2 NUT load failed for par3=%d", par3);
+										delete newNut;
+									}
+								} else if (par3 == 2 || par3 == 4) {
+									// High-res versions - skip in low-res mode
+									debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
 									loadedAsNut = true;
+								}
+							}
 
-									switch (par3) {
-									case 1:  // POV001 - Primary ship sprite
-										delete _shipSprite;
-										_shipSprite = newNut;
-										debug("Rebel2 Opcode 8: _shipSprite set to %p", (void*)_shipSprite);
-										break;
-									case 3:  // POV004 - Secondary ship sprite
-										delete _shipSprite2;
-										_shipSprite2 = newNut;
-										break;
-									case 6:  // POV002 - Ship overlay 1
-										delete _shipOverlay1;
-										_shipOverlay1 = newNut;
-										break;
-									case 7:  // POV003 - Ship overlay 2
-										delete _shipOverlay2;
-										_shipOverlay2 = newNut;
-										break;
-									default:
+							// Handler 8 (Third-Person Vehicle): Load ship sprites
+							if (!loadedAsNut && _rebelHandler == 8) {
+								bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
+								if (isHandler8Par3) {
+									NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
+									if (newNut && newNut->getNumChars() > 0) {
+										debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
+											par3, newNut->getNumChars(), _rebelHandler);
+										loadedAsNut = true;
+
+										switch (par3) {
+										case 1:  // POV001 - Primary ship sprite
+											delete _shipSprite;
+											_shipSprite = newNut;
+											break;
+										case 3:  // POV004 - Secondary ship sprite
+											delete _shipSprite2;
+											_shipSprite2 = newNut;
+											break;
+										case 6:  // POV002 - Ship overlay 1
+											delete _shipOverlay1;
+											_shipOverlay1 = newNut;
+											break;
+										case 7:  // POV003 - Ship overlay 2
+											delete _shipOverlay2;
+											_shipOverlay2 = newNut;
+											break;
+										default:
+											delete newNut;
+											loadedAsNut = false;
+											break;
+										}
+									} else {
+										debug("Rebel2 Opcode 8: Ship NUT load failed for par3=%d", par3);
 										delete newNut;
-										loadedAsNut = false;
-										break;
 									}
-								} else {
-									debug("Rebel2 Opcode 8: NUT load failed for par3=%d, trying as embedded SAN", par3);
-									delete newNut;
 								}
 							}
 
 							if (!loadedAsNut) {
-								// Load as embedded SAN (HUD overlays)
-								// The userId parameter varies by handler type:
-								//
-								// Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
-								//   par3=1: GRD001 (Primary HUD overlay)
-								//   par3=2: GRD002 (Secondary HUD graphics)
-								//   par3=4: GRD010 (Ship cockpit frame)
-								//   etc.
-								//
-								// Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
-								// Other handlers: Use par4 for userId
-								//
-								// par4 values seen in Level 3:
-								//   1000 = audio track
-								//   1-11 = HUD overlay slots
-								//
-								// Note: Handler may not be set yet if opcode 8 arrives before opcode 6
-								// Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
-								// or invalid, prefer par3
-								int userId;
-								bool usePar3 = (_rebelHandler == 0x26 || _rebelHandler == 0x19);
-
-								// Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
-								// out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
-								if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
-									usePar3 = true;
-									debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
-								}
+								// Skip high-res turret HUD data (par3 == 2, 4) regardless of handler
+								// ScummVM runs at 320x200, so we only want low-res data (par3 == 1, 3)
+								debug("Rebel2 Opcode 8: Fallback path - handler=%d par3=%d par4=%d animSize=%d",
+									_rebelHandler, par3, par4, toCopy);
+								if (par3 == 2 || par3 == 4) {
+									debug("Rebel2 Opcode 8: SKIPPING high-res HUD par3=%d (low-res mode)", par3);
+									// Don't load anything - skip this data entirely
+								} else {
+									// Load as embedded SAN (HUD overlays)
+									// The userId parameter varies by handler type:
+									//
+									// Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
+									//   par3=1: GRD001 (Primary HUD overlay)
+									//   par3=3: GRD010 (Secondary HUD)
+									//
+									// Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
+									// Other handlers: Use par4 for userId
+									//
+									// par4 values seen in Level 3:
+									//   1000 = audio track
+									//   1-11 = HUD overlay slots
+									//
+									// Note: Handler may not be set yet if opcode 8 arrives before opcode 6
+									// Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
+									// or invalid, prefer par3
+									int userId;
+									// Handler 0x19 uses par3; Handler 0x26 and others use par4
+									bool usePar3 = (_rebelHandler == 0x19);
+
+									// Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
+									// out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
+									if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
+										usePar3 = true;
+										debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
+									}
 
-								userId = usePar3 ? par3 : par4;
+									userId = usePar3 ? par3 : par4;
 
-								// Audio tracks (userId >= 1000) are handled separately, skip them
-								if (userId > 0 && userId < 1000) {
-									debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
-										userId, _rebelHandler, par3, par4);
-									loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+									// Audio tracks (userId >= 1000) are handled separately, skip them
+									if (userId > 0 && userId < 1000) {
+										debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
+											userId, _rebelHandler, par3, par4);
+										loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
+									}
 								}
 							}
 							free(animData);
@@ -2019,10 +2082,19 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 
 					debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
 						userId, width, height, left, top, codec);
-					
+
+					// Skip high-resolution frames - ScummVM runs at 320x200
+					// If frame dimensions exceed low-res screen size, it's high-res data
+					if (width > 400 || height > 250) {
+						debug("Rebel2: SKIPPING high-res embedded frame: userId=%d, %dx%d (exceeds 400x250)",
+							userId, width, height);
+						stream.seek(nextSubPos);
+						continue;
+					}
+
 					// Allocate storage for the decoded frame
 					EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
-					
+
 					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
 						if (frame.width != width || frame.height != height || !frame.pixels) {
 							free(frame.pixels);
@@ -2080,10 +2152,8 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
 							} else if (codec == 45) {
 								// Codec 45: RA2-specific codec with BOMP-style RLE
-								// Header: 01 FE 00 00 01 00 (6 bytes)
-								//   Byte 0: sub-codec (01)
-								//   Byte 1: transparent color (FE = 254)
-								//   Bytes 2-5: unknown/padding
+								// May have a variable-length sub-header before RLE data
+								// Common patterns: "01 FE 00 00 01 00" (6 bytes) or others
 								debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
 									fobjData[0], fobjData[1], fobjData[2], fobjData[3],
 									fobjData[4], fobjData[5], fobjData[6], fobjData[7],
@@ -2091,12 +2161,56 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 									fobjData[12], fobjData[13], fobjData[14], fobjData[15],
 									fobjData[16], fobjData[17], fobjData[18], fobjData[19]);
 
-								// Parse 6-byte sub-header
+								// Probe multiple header offsets to find where valid RLE data starts
+								// RLE data has 2-byte line sizes that should be reasonable values
 								int headerSkip = 0;
+								bool foundValidOffset = false;
+
+								// Check common header patterns
 								if (dataSize > 6 && fobjData[0] == 0x01 && fobjData[1] == 0xFE) {
+									// Known 6-byte header: 01 FE XX XX XX XX
 									headerSkip = 6;
-									debug("Rebel2: Codec 45 header: sub-codec=%d, transparent=%d",
-										fobjData[0], fobjData[1]);
+									debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
+									foundValidOffset = true;
+								}
+
+								// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
+								if (!foundValidOffset) {
+									for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+										int testLineSize = READ_LE_UINT16(fobjData + testOffset);
+										// A valid first line size should be: > 0, <= width*2 (reasonable for RLE)
+										// Also check that the total data is enough for at least height lines
+										if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+											// Further validation: sum line sizes should roughly match data size
+											int sumTest = 0;
+											int linesTest = 0;
+											byte *testPtr = fobjData + testOffset;
+											bool validSum = true;
+
+											while (linesTest < height && testPtr + 2 <= fobjData + dataSize) {
+												int ls = READ_LE_UINT16(testPtr);
+												if (ls <= 0 || ls > width * 2) {
+													validSum = false;
+													break;
+												}
+												sumTest += ls + 2;
+												testPtr += ls + 2;
+												linesTest++;
+											}
+
+											// Accept if we got close to expected number of lines
+											if (validSum && linesTest >= height - 1) {
+												headerSkip = testOffset;
+												foundValidOffset = true;
+												debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
+												break;
+											}
+										}
+									}
+								}
+
+								if (!foundValidOffset) {
+									debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
 								}
 
 								byte *srcPtr = fobjData + headerSkip;
@@ -2228,9 +2342,11 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 							}
 
 							// Draw immediately to renderBitmap if valid
-							// Skip immediate draw for Handler 7/8 - these are ship direction sprites
-							// that should be selected based on direction and drawn during post-rendering
-							bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8);
+							// Skip immediate draw for handlers that render HUD during post-processing:
+							// - Handler 7/8: Ship direction sprites selected based on direction
+							// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+							bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
+							                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
 
 							if (frame.valid && renderBitmap && !skipImmediateDraw) {
 								int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
@@ -2923,6 +3039,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Clear the status bar area at Y=180-199 with background color
 	// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
 	// This fills width=320, height=20 starting at Y=180 with color index 4
+	// Status bar is ALWAYS shown during gameplay (_rebelHandler != 0)
+	// Hidden during cinematics/intros when _rebelHandler == 0 (handled by early return above)
 	const byte statusBarBgColor = 4;
 
 	for (int y = statusBarY; y < videoHeight; y++) {
@@ -2936,7 +3054,96 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 	
 	// ============================================================
-	// STEP 1: Draw embedded SAN HUD overlays FIRST (from IACT chunks)
+	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
+	// ============================================================
+	// For turret handlers, the HUD overlay is loaded as a NUT file via IACT opcode 8
+	// and contains animated frames for blinking cockpit lights.
+	//
+	// From FUN_004089ab disassembly (lines 195-226):
+	// - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
+	// - Position formula (low-res):
+	//   X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
+	//   Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
+	// - Animation: spriteIndex = (frameCounter / 2) % 6
+	//
+	// The mouse offset creates a subtle parallax effect as the view pans.
+	if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlayNut && _hudOverlayNut->getNumChars() > 0) {
+		// Calculate mouse offset (clamped to -127..127)
+		// These match DAT_0047a7e0/DAT_0047a7e2 in the original
+		int mouseOffsetX = (_vm->_mouse.x - 160);  // Relative to screen center
+		int mouseOffsetY = (_vm->_mouse.y - 100);  // Relative to screen center
+		if (mouseOffsetX > 127) mouseOffsetX = 127;
+		if (mouseOffsetX < -127) mouseOffsetX = -127;
+		if (mouseOffsetY > 127) mouseOffsetY = 127;
+		if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+		// Animation frame cycling: (frameCounter / 2) % 6
+		// This creates a 12-frame cycle (each sprite shown for 2 video frames)
+		int numSprites = _hudOverlayNut->getNumChars();
+		int animFrameCount = MIN(numSprites, 6);  // Use up to 6 frames for animation
+		int animFrame = 0;
+		if (animFrameCount > 0) {
+			animFrame = (curFrame / 2) % animFrameCount;
+		}
+
+		// Get sprite dimensions and offset
+		// NUT format stores sprite offsets that affect positioning
+		int spriteW = _hudOverlayNut->getCharWidth(animFrame);
+		int spriteH = _hudOverlayNut->getCharHeight(animFrame);
+
+		// Position calculation from assembly (low-res mode):
+		// X = 0xa0 (160) + (mouseOffsetX >> 4) - (width / 2) - offsetX
+		// Y = 0xb6 (182) - (mouseOffsetY >> 4) - height - offsetY
+		// Note: The sprite offset is embedded in the NUT data; for simplicity we use 0
+		int spriteOffsetX = 0;  // Could be extracted from NUT if needed
+		int spriteOffsetY = 0;
+
+		int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
+		int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
+
+		// Apply view offset for scrolling background
+		hudX += _viewX;
+		hudY += _viewY;
+
+		// Draw the HUD overlay:
+		// From FUN_004089ab: Sprite 0 is ALWAYS drawn first (base cockpit)
+		// Then if animation frame != 0, draw the animation frame on top (blinking lights)
+		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
+
+		// Draw animation overlay frame if not frame 0
+		// Animation frames 1-5 contain the blinking light states
+		if (animFrame != 0 && animFrame < numSprites) {
+			renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
+		}
+
+		debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
+			  animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
+	}
+
+	// Draw secondary HUD overlay if present (DAT_0047fe80)
+	if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
+		// Similar positioning to primary overlay
+		int mouseOffsetX = (_vm->_mouse.x - 160);
+		int mouseOffsetY = (_vm->_mouse.y - 100);
+		if (mouseOffsetX > 127) mouseOffsetX = 127;
+		if (mouseOffsetX < -127) mouseOffsetX = -127;
+		if (mouseOffsetY > 127) mouseOffsetY = 127;
+		if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+		int spriteW = _hudOverlay2Nut->getCharWidth(0);
+		int spriteH = _hudOverlay2Nut->getCharHeight(0);
+
+		int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2);
+		int hudY = 182 - (mouseOffsetY >> 4) - spriteH;
+
+		hudX += _viewX;
+		hudY += _viewY;
+
+		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlay2Nut, 0);
+	}
+
+	// ============================================================
+	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
 	// ============================================================
 	// For Handler 7 (Level 3): HUD elements are scattered across the screen
 	//   - Each frame has its own renderX/renderY position from FOBJ left/top
@@ -3011,22 +3218,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			destX = frame.renderX;
 			destY = frame.renderY;
 
-			// For Handler 0x26 (turret) and 0x19 (mixed): Cockpit overlays positioned at bottom
-			// The cockpit pieces (slots 1-2) should be rendered just above the status bar (Y=180)
-			// The FOBJ top value might be 0, so we override it for cockpit pieces
+			// For Handler 0x26 (turret) and 0x19 (mixed): HUD overlay positioning
+			// From FUN_004089ab lines 203-222:
+			// X = 160 + (mouseOffsetX >> 4) - width/2 - spriteOffsetX
+			// Y = 182 - ((mouseOffsetY - 128) >> 4) - height - spriteOffsetY
+			// When mouse at center: X = 160 - width/2 - offsetX, Y = 190 - height - offsetY
 			if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
-				// Cockpit overlay position: just above status bar
-				// Status bar is at Y=180, cockpit sits right above it
-				// For slot 1 (left piece): X = 0
-				// For slot 2 (right piece): X = width of slot 1
-				if (hudSlot == 1) {
-					destX = 0;
-				} else if (hudSlot == 2 && _rebelEmbeddedHud[1].valid) {
-					// Position slot 2 right after slot 1
-					destX = _rebelEmbeddedHud[1].width;
-				}
-				// Y position: status bar (180) minus cockpit height
-				destY = 180 - frame.height;
+				// Position based on assembly formula (static center position)
+				// X: centered horizontally, adjusted by sprite offset
+				destX = 160 - frame.width / 2 - frame.renderX;
+				// Y: 190 - height - offsetY (just above status bar at Y=180)
+				destY = 190 - frame.height - frame.renderY;
 			}
 
 			// For Handler 7: Apply position offset based on ship position
@@ -3044,6 +3246,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			destX += _viewX;
 			destY += _viewY;
 
+			// Debug: log embedded frame render dimensions
+			debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d) screen=%dx%d",
+				hudSlot, frame.width, frame.height, destX, destY, pitch, height);
+
 			// Draw frame with transparency (pixel 0 and 231 = transparent, matching NUT rendering)
 			for (int y = 0; y < frame.height && (destY + y) < height; y++) {
 				for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
@@ -3069,6 +3275,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	//   - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
 	//   - Sprite 6: Damage bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
 	//   - Sprite 7: Damage alert (flashing red when damage critical)
+	// Status bar is ALWAYS drawn during gameplay (_rebelHandler != 0)
 	if (_smush_cockpitNut) {
 		// Draw status bar background frame (sprite 1) at (0, statusBarY)
 		// This sprite is the full-width status bar background
@@ -4818,6 +5025,7 @@ void InsaneRebel2::playCinematic(const char *filename) {
 	// Original: DAT_0047ee84 is only set by IACT opcode 6 during gameplay videos
 	// Cinematics don't have opcode 6, so handler stays 0
 	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // No status bar during cinematics
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	splayer->setCurVideoFlags(0x20);  // Cinematic mode
@@ -4857,6 +5065,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 	// Reset damage/shield for this level
 	_playerShield = 255;
 	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // Will be set by IACT opcode 6 if par4==1
 
 	debug("Rebel2: Starting gameplay for level %d", levelId);
 
@@ -5009,6 +5218,9 @@ void InsaneRebel2::playLevelEnd(int levelId) {
 	// Play level completion video (LEVXX/XXEND.SAN)
 	// Emulates FUN_00417327 call
 
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // No status bar during end cinematic
+
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
@@ -5025,6 +5237,9 @@ void InsaneRebel2::playLevelDeath(int levelId) {
 	// The variant depends on the frame where player died
 	// For simplicity, we'll play the A variant
 
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
+
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 
@@ -5045,6 +5260,10 @@ void InsaneRebel2::playLevelDeath(int levelId) {
 
 void InsaneRebel2::playLevelRetry(int levelId) {
 	// Play retry prompt video (LEVXX/XXRETRY.SAN)
+	// Reset handler state for the retry cinematic
+
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
 
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
@@ -5061,6 +5280,9 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
 	// Play game over video (LEVXX/XXOVER.SAN)
 	// Emulates FUN_00417ab2 call
 
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // No status bar during game over cinematic
+
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String filename = Common::String::format("%s/%sOVER.SAN", dir.c_str(), prefix.c_str());
@@ -5207,6 +5429,9 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 	// Play death video with proper variant selection
 
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
+
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String variant = selectDeathVideoVariant(levelId, phase, frame);
@@ -5229,6 +5454,9 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
 	// Play retry video - phase-specific for multi-phase levels
 
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
+
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String filename;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bd48a8b016d..913f9bd2814 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -519,6 +519,24 @@ public:
 	int16 _flyShipScreenX;           // DAT_0044370e - Ship X screen position
 	int16 _flyShipScreenY;           // DAT_0044370c - Ship Y screen position
 
+	// ======================= Handler 0x26 Turret HUD Overlays =======================
+	// For turret missions (Level 1, etc.), Handler 0x26 uses NUT-based HUD overlays
+	// loaded via IACT opcode 8. These contain animated cockpit panel elements.
+	//
+	// Based on FUN_00407fcb and FUN_004089ab disassembly:
+	// - DAT_0047fe78: Primary HUD overlay (GRD001/002, par3=1 or 2, 6 animation frames)
+	// - DAT_0047fe80: Secondary HUD overlay (GRD010, par3=3 or 4, static or animated)
+	//
+	// Animation: The HUD overlay cycles through 6 sprite frames for blinking lights
+	// Formula: spriteIndex = (frameCounter / 2) % 6
+	//
+	// Position formula (from FUN_004089ab lines 203-222):
+	// X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
+	// Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
+
+	NutRenderer *_hudOverlayNut;     // DAT_0047fe78 - Primary HUD overlay (animated)
+	NutRenderer *_hudOverlay2Nut;    // DAT_0047fe80 - Secondary HUD overlay
+
 	/* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
 	int _difficulty;
 	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);


Commit: 547360550093ac6ce37b135bed2997d3a56b71b0
    https://github.com/scummvm/scummvm/commit/547360550093ac6ce37b135bed2997d3a56b71b0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:55+02:00

Commit Message:
SCUMM: RA2: Fix low-resolution cockpit HUD loading

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 89be33ba7e8..2f956385176 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3161,6 +3161,15 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
 		EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
 		if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
+			// Skip frames at position (0,0) with small dimensions - these are likely animation patches
+			// that need special handling (userId=3: 11x26, userId=4: 17x53 in Level 1)
+			// TODO: Investigate how these are used in the original assembly
+			if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
+				debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
+					hudSlot, frame.width, frame.height);
+				continue;
+			}
+
 			int destX, destY;
 
 			// For Handler 7: Check if this is a ship direction frame
@@ -3219,24 +3228,30 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			destY = frame.renderY;
 
 			// For Handler 0x26 (turret) and 0x19 (mixed): HUD overlay positioning
-			// From FUN_004089ab lines 203-222:
+			// From FUN_004089ab assembly lines 560-582:
 			// X = 160 + (mouseOffsetX >> 4) - width/2 - spriteOffsetX
-			// Y = 182 - ((mouseOffsetY - 128) >> 4) - height - spriteOffsetY
-			// When mouse at center: X = 160 - width/2 - offsetX, Y = 190 - height - offsetY
+			// Y = 0xb6 (182) - (mouseY >> 4) - height - spriteOffsetY (low-res mode)
+			// When mouse centered (mouseY=0), local_38=-128, offset=-8, so Y = 190 - height - offsetY
+			// For embedded cockpit frames at fixed position, use screen bottom (200) as base
 			if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
 				// Position based on assembly formula (static center position)
 				// X: centered horizontally, adjusted by sprite offset
 				destX = 160 - frame.width / 2 - frame.renderX;
-				// Y: 190 - height - offsetY (just above status bar at Y=180)
-				destY = 190 - frame.height - frame.renderY;
+				// Y: screen bottom (200) - height - offsetY for bottom-aligned cockpit
+				destY = 200 - frame.height - frame.renderY;
 			}
 
-			// For Handler 7: Apply position offset based on ship position
-			// This makes the ship sprite follow the mouse/crosshair
-			if (_rebelHandler == 7 && destX > 100 && destY > 50) {
-				// This appears to be a ship sprite (center of screen)
-				// Offset based on ship position relative to center
-				int16 offsetX = (_shipPosX - 160) / 8;  // Scale down movement
+			// For Handler 7 (space flight): HUD cockpit overlays use FOBJ position directly
+			// From FUN_0040d836: The clip region is 320x170 (0x140 x 0xaa)
+			// Large cockpit frames (hudSlot 1/2) need centering like turret handler
+			if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
+				// Large cockpit frame - center horizontally, position at bottom
+				destX = 160 - frame.width / 2 - frame.renderX;
+				// Position above status bar area (170 is render height for Handler 7)
+				destY = 170 - frame.height - frame.renderY;
+			} else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
+				// Ship sprite in center of screen - apply position offset
+				int16 offsetX = (_shipPosX - 160) / 8;
 				int16 offsetY = (_shipPosY - 100) / 8;
 				destX += offsetX;
 				destY += offsetY;


Commit: 8718764edfdf42144a75bcfa1c8a5925acbfce12
    https://github.com/scummvm/scummvm/commit/8718764edfdf42144a75bcfa1c8a5925acbfce12
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:55+02:00

Commit Message:
SCUMM: RA2: Add level 2 video decoders

Changed paths:
    engines/scumm/insane/insane.h
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_scenes.cpp
    engines/scumm/smush/codec1.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index 19382f85576..d4d124184c5 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -61,13 +61,13 @@ public:
 	void setSmushPlayer(SmushPlayer *player);
 	void runScene(int arraynum);
 
-	void procPreRendering();
+	virtual void procPreRendering(byte *renderBitmap);
 	void virtual procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 						   int32 setupsan13, int32 curFrame, int32 maxFrame);
 	void virtual procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags, int16 par1,
 				  int16 par2, int16 par3, int16 par4);
-	void procSKIP(int32 subSize, Common::SeekableReadStream &b);
+	virtual void procSKIP(int32 subSize, Common::SeekableReadStream &b);
 	void escapeKeyHandler();
 
 	bool isInsaneActive() { return _insaneIsRunning; }
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2f956385176..5e9ca23ecc5 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -48,6 +48,10 @@
 
 namespace Scumm {
 
+// External codec functions from codec1.cpp
+extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
 InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_vm = scumm;
 
@@ -260,6 +264,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_shipSprite2 = nullptr;
 	_shipOverlay1 = nullptr;
 	_shipOverlay2 = nullptr;
+	_level2Background = nullptr;
+	_level2BackgroundLoaded = false;
 	_shipPosX = 0xa0;      // Start centered (160 in hex)
 	_shipPosY = 0x28;      // Start at vertical center (40)
 	_shipTargetX = 0xa0;
@@ -341,6 +347,8 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _shipSprite2;
 	delete _shipOverlay1;
 	delete _shipOverlay2;
+	free(_level2Background);
+	_level2Background = nullptr;
 
 	// Clean up Handler 7 FLY ship sprites
 	delete _flyShipSprite;
@@ -367,10 +375,9 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_ESCAPE:
-			// ESC skips cutscenes (videos with 0x20 flag = non-interactive)
-			// Emulates FUN_0041f537 behavior: if key == 0x1b, skip video
-			if (splayer && (splayer->_curVideoFlags & 0x20) != 0) {
-				debug("Rebel2: ESC pressed - skipping cutscene");
+			// ESC skips videos
+			if (splayer) {
+				debug("Rebel2: ESC pressed - skipping video");
 				_vm->_smushVideoShouldFinish = true;
 				return true;  // Consume the event
 			}
@@ -691,6 +698,37 @@ void InsaneRebel2::clearBit(int n) {
 	_iactBits[n] = 0;
 }
 
+void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
+	// Rebel Assault 2 does NOT use Full Throttle's conditional frame skip mechanism.
+	// The base Insane::procSKIP() uses _iactBits to decide whether to skip frame objects,
+	// but RA2 handles conditional content differently through IACT opcodes 2/3/4.
+	//
+	// By overriding this to do nothing, we prevent random frame objects from being
+	// skipped due to uninitialized _iactBits state.
+	//
+	// If RA2 SKIP chunks need to be handled, implement RA2-specific logic here.
+	// For now, just consume the data without setting _skipNext.
+	(void)subSize;
+	(void)b;
+}
+
+void InsaneRebel2::procPreRendering(byte *renderBitmap) {
+	// Call base class implementation first (handles Full Throttle state machine)
+	Insane::procPreRendering(renderBitmap);
+
+	// For Level 2 gameplay (Handler 8 or 25), restore the background BEFORE FOBJ decoding.
+	// The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
+	// clear old ones. By restoring the full background each frame, we ensure old sprite
+	// positions are erased before new ones are drawn.
+	//
+	// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
+	if ((_rebelHandler == 8 || _rebelHandler == 25) && _level2BackgroundLoaded && _level2Background && renderBitmap) {
+		for (int y = 0; y < 200; y++) {
+			memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
+		}
+	}
+}
+
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) {
@@ -849,7 +887,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 
 	} else if (par1 == 6) {
 		// Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
-		iactRebel2Opcode6(renderBitmap, b, par2, par3, par4);
+		iactRebel2Opcode6(renderBitmap, b, size, par2, par3, par4);
 	} else if (par1 == 8) {
 		// Opcode 8: HUD resource loading (FUN_41CADB case 6)
 		iactRebel2Opcode8(renderBitmap, b, size, par2, par3, par4);
@@ -1006,7 +1044,7 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 	// other subcases not implemented yet
 }
 
-void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
 	// Opcode 6: Level setup / mode switch
 	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
 	//
@@ -1023,6 +1061,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 	// Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
 	if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
+		// Reset Level 2 background flag when transitioning away from Handler 8
+		if (_rebelHandler == 8 && par2 != 8) {
+			_level2BackgroundLoaded = false;
+		}
 		_rebelHandler = par2;
 		debug("Rebel2 Opcode 6: Setting handler=%d", par2);
 	}
@@ -1382,11 +1424,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		_rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
 
 	// Detect and load embedded ANIM (SAN) within the remaining IACT payload
+	// Note: chunkSize is the remaining IACT payload size after par1-par4 header
 	{
 		int64 startPos = b.pos();
-		int64 totalSize = b.size();
-		if (totalSize >= 0 && totalSize > startPos) {
-			int64 remaining = totalSize - startPos;
+		// Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream)
+		int64 remaining = chunkSize;
+		if (remaining > 0) {
 			int scanSize = (int)MIN<int64>(remaining, 65536);
 			byte *scanBuf = (byte *)malloc(scanSize);
 			if (scanBuf) {
@@ -1395,7 +1438,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 					if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
 						int64 animStreamPos = startPos + i;
 						uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
+						// Limit to remaining IACT payload (chunkSize - offset into payload)
+						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
 						if (toCopy > 0) {
 							byte *animData = (byte *)malloc(toCopy);
 							if (animData) {
@@ -1677,6 +1721,99 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 								}
 							}
 
+							// Handler 8/25 (Level 2): Load background
+							// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
+							// FUN_00401234 case 6 switch case 5: Creates 320x200 buffer (DAT_0047e030)
+							// Handler 8 = third-person vehicle, Handler 25 (0x19) = turret/mixed view
+							// Level 2 uses handler 25 but background loading is the same mechanism
+							if (!loadedAsNut && (_rebelHandler == 8 || _rebelHandler == 25) && par4 == 5) {
+								debug("Rebel2 Opcode 8: Loading Level 2 background (par4=5, animSize=%d)", toCopy);
+
+								// Allocate background buffer if needed (320x200 = 64000 bytes)
+								if (_level2Background == nullptr) {
+									_level2Background = (byte *)malloc(320 * 200);
+									if (_level2Background) {
+										memset(_level2Background, 0, 320 * 200);
+									}
+								}
+
+								if (_level2Background) {
+									// Parse embedded ANIM to find FOBJ
+									// Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
+									int animOffset = 0;
+									if (toCopy >= 8 && READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
+										uint32 animSize = READ_BE_UINT32(animData + 4);
+										debug("Rebel2 Opcode 8: Found ANIM tag, size=%u", animSize);
+
+										// Skip ANIM header (8 bytes) + AHDR chunk
+										// AHDR starts at offset 8, size is at offset 12 (big endian)
+										if (toCopy >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
+											uint32 ahdrSize = READ_BE_UINT32(animData + 12);
+											animOffset = 8 + 8 + ahdrSize;  // After ANIM tag + AHDR
+											debug("Rebel2 Opcode 8: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
+										}
+									}
+
+									// Look for FRME containing FOBJ
+									bool foundBackground = false;
+									for (int scanPos = animOffset; scanPos + 16 < toCopy && !foundBackground; scanPos++) {
+										if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
+											// Found FRME, look for FOBJ inside
+											int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
+											debug("Rebel2 Opcode 8: Found FRME at %d, size=%d", scanPos, frmeSize);
+
+											for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < toCopy; fobjPos++) {
+												if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
+													uint32 fobjSize = READ_BE_UINT32(animData + fobjPos + 4);
+													byte *fobjData = animData + fobjPos + 8;
+
+													// FOBJ header: codec(2), x(2), y(2), w(2), h(2)
+													int16 codec = READ_LE_INT16(fobjData);
+													int16 fobjX = READ_LE_INT16(fobjData + 2);
+													int16 fobjY = READ_LE_INT16(fobjData + 4);
+													int16 fobjW = READ_LE_INT16(fobjData + 6);
+													int16 fobjH = READ_LE_INT16(fobjData + 8);
+
+													debug("Rebel2 Opcode 8: Found FOBJ at %d: codec=%d pos=(%d,%d) size=%dx%d dataSize=%u",
+														fobjPos, codec, fobjX, fobjY, fobjW, fobjH, fobjSize);
+
+													// Decode codec 3 (RLE) into background buffer
+													// Codec 3 uses bomp RLE format with line-by-line structure
+													// FOBJ header is 14 bytes: codec(2), x(2), y(2), w(2), h(2), unk(2), unk(2)
+													if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
+														byte *rleData = fobjData + 14;  // Skip full 14-byte FOBJ header
+
+														// Use the existing smushDecodeRLE function from codec1.cpp
+														// This properly decodes BOMP RLE with line size headers
+														smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+
+														debug("Rebel2 Opcode 8: Decoded Level 2 background (%dx%d at %d,%d)",
+															fobjW, fobjH, fobjX, fobjY);
+														_level2BackgroundLoaded = true;
+														foundBackground = true;
+
+														// Draw background to render bitmap immediately
+														if (renderBitmap) {
+															for (int by = 0; by < 200; by++) {
+																memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
+															}
+															debug("Rebel2 Opcode 8: Copied Level 2 background to renderBitmap");
+														}
+													}
+													break;  // Found FOBJ, stop searching
+												}
+											}
+											break;  // Found FRME, stop searching
+										}
+									}
+
+									if (!foundBackground) {
+										debug("Rebel2 Opcode 8: Failed to find/decode background FOBJ");
+									}
+								}
+								loadedAsNut = true;  // Mark as handled even if decode failed
+							}
+
 							if (!loadedAsNut) {
 								// Skip high-res turret HUD data (par3 == 2, 4) regardless of handler
 								// ScummVM runs at 320x200, so we only want low-res data (par3 == 1, 3)
@@ -2019,10 +2156,6 @@ void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h,
 	_enemies.push_back(e);
 }
 
-// External helpers from smush_player.cpp but we are already in Scumm namespace
-extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 	// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
 	if (userId < 0 || userId > 15 || !animData || size < 32) {
@@ -3033,6 +3166,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
 	processMouse();
 
+	// NOTE: Level 2 background is drawn ONCE during IACT opcode 8 par4=5 processing
+	// (in procIACT when the background ANIM is first loaded). The 0x08 video flag
+	// (preserve background) prevents the frame buffer from being cleared, so the
+	// background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
+	// We do NOT redraw the background here as that would overwrite FOBJ content.
+
 	// ============================================================
 	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
 	// ============================================================
@@ -3227,17 +3366,15 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			destX = frame.renderX;
 			destY = frame.renderY;
 
-			// For Handler 0x26 (turret) and 0x19 (mixed): HUD overlay positioning
-			// From FUN_004089ab assembly lines 560-582:
-			// X = 160 + (mouseOffsetX >> 4) - width/2 - spriteOffsetX
-			// Y = 0xb6 (182) - (mouseY >> 4) - height - spriteOffsetY (low-res mode)
-			// When mouse centered (mouseY=0), local_38=-128, offset=-8, so Y = 190 - height - offsetY
-			// For embedded cockpit frames at fixed position, use screen bottom (200) as base
+			// For Handler 0x26 (turret) and 0x19 (mixed): Embedded ANIM frame positioning
+			// From FUN_00407fcb assembly lines 389-391: screen dimensions are 320x200 (0x140 x 0xc8)
+			// Unlike NUT overlays (which use 0xb6=182 base with mouse parallax), embedded ANIM
+			// frames use screen height (200) as the base for bottom-aligned positioning.
+			// Formula: X = 160 - width/2 - fobj_left, Y = 200 - height - fobj_top
 			if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
-				// Position based on assembly formula (static center position)
-				// X: centered horizontally, adjusted by sprite offset
+				// X: centered horizontally, adjusted by FOBJ left offset
 				destX = 160 - frame.width / 2 - frame.renderX;
-				// Y: screen bottom (200) - height - offsetY for bottom-aligned cockpit
+				// Y: screen height (0xc8=200) - height - fobj_top for bottom-aligned cockpit
 				destY = 200 - frame.height - frame.renderY;
 			}
 
@@ -5573,13 +5710,24 @@ int InsaneRebel2::runLevel2() {
 		bonusCount = 0;
 
 		// ===== PHASE 1: P1/02P01_X.SAN =====
-		// Select random variant (B, C, or D - not A in main loop)
+		// First play variant A which contains the background IACT (opcode 8, par4=5)
+		// The background is loaded during this video and persists for B/C/D variants
 		{
+			debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background loader)");
+			splayer->setCurVideoFlags(0x28);
+			splayer->play("LEV02/P1/02P01_A.SAN", 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// Now select random variant (B, C, or D) for actual gameplay
+		if (_playerShield > 0) {
 			int variant = getRandomVariant(3);
 			const char *variants[] = {"B", "C", "D"};
 			Common::String filename = Common::String::format("LEV02/P1/02P01_%s.SAN", variants[variant]);
 
-			debug("Rebel2: Level 2 Phase 1 - playing %s", filename.c_str());
+			debug("Rebel2: Level 2 Phase 1 - playing %s (gameplay)", filename.c_str());
 			splayer->setCurVideoFlags(0x28);
 			splayer->play(filename.c_str(), 12);
 			_deathFrame = splayer->_frame;
@@ -5611,13 +5759,23 @@ int InsaneRebel2::runLevel2() {
 		_currentPhase = 2;
 
 		// ===== PHASE 2: P2/02P02_X.SAN =====
-		// Variant selection based on switch (more complex in original)
+		// First play variant A which contains the background IACT for this phase
 		{
-			int variant = getRandomVariant(6);
-			const char *variants[] = {"A", "B", "C", "D", "E", "F"};
+			debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background loader)");
+			splayer->setCurVideoFlags(0x28);
+			splayer->play("LEV02/P2/02P02_A.SAN", 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// Now select gameplay variant (B through F)
+		if (_playerShield > 0) {
+			int variant = getRandomVariant(5);
+			const char *variants[] = {"B", "C", "D", "E", "F"};
 			Common::String filename = Common::String::format("LEV02/P2/02P02_%s.SAN", variants[variant]);
 
-			debug("Rebel2: Level 2 Phase 2 - playing %s", filename.c_str());
+			debug("Rebel2: Level 2 Phase 2 - playing %s (gameplay)", filename.c_str());
 			splayer->setCurVideoFlags(0x28);
 			splayer->play(filename.c_str(), 12);
 			_deathFrame = splayer->_frame;
@@ -5649,13 +5807,23 @@ int InsaneRebel2::runLevel2() {
 		_currentPhase = 3;
 
 		// ===== PHASE 3: P3/02P03_X.SAN =====
-		// Variant selection with more options (A through I)
+		// First play variant A which contains the background IACT for this phase
 		{
-			int variant = getRandomVariant(9);
-			const char *variants[] = {"A", "B", "C", "D", "E", "F", "G", "H", "I"};
+			debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background loader)");
+			splayer->setCurVideoFlags(0x28);
+			splayer->play("LEV02/P3/02P03_A.SAN", 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// Now select gameplay variant (B through I)
+		if (_playerShield > 0) {
+			int variant = getRandomVariant(8);
+			const char *variants[] = {"B", "C", "D", "E", "F", "G", "H", "I"};
 			Common::String filename = Common::String::format("LEV02/P3/02P03_%s.SAN", variants[variant]);
 
-			debug("Rebel2: Level 2 Phase 3 - playing %s", filename.c_str());
+			debug("Rebel2: Level 2 Phase 3 - playing %s (gameplay)", filename.c_str());
 			splayer->setCurVideoFlags(0x28);
 			splayer->play(filename.c_str(), 12);
 			_deathFrame = splayer->_frame;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 913f9bd2814..b47bd1f3098 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -253,7 +253,7 @@ public:
 	// Handle IACT opcode subcases
 	void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
-	void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+	void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
@@ -264,6 +264,14 @@ public:
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
+	// Override procSKIP to disable Full Throttle's conditional frame skip mechanism
+	// RA2 uses a different system for conditional frames via IACT opcodes
+	void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
+
+	// Override procPreRendering to restore Level 2 background before FOBJ decoding
+	// This is called at the start of each frame, before FOBJ sprites are decoded
+	void procPreRendering(byte *renderBitmap) override;
+
 	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
 	// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
 	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
@@ -459,6 +467,12 @@ public:
 	NutRenderer *_shipOverlay1;      // DAT_0047e020 - Additional overlay
 	NutRenderer *_shipOverlay2;      // DAT_0047e018 - Additional overlay
 
+	// Level 2 background buffer (DAT_0047e030)
+	// Loaded from IACT opcode 8, par4=5 - contains 320x200 background image
+	// decoded from embedded ANIM in gameplay video frame 0
+	byte *_level2Background;
+	bool _level2BackgroundLoaded;
+
 	// Ship position tracking (matches DAT_0043e006/008)
 	// These are "raw" positions that get converted for display
 	int16 _shipPosX;                 // DAT_0043e006
diff --git a/engines/scumm/insane/insane_scenes.cpp b/engines/scumm/insane/insane_scenes.cpp
index 3f12701cb63..2b2d5d91bb8 100644
--- a/engines/scumm/insane/insane_scenes.cpp
+++ b/engines/scumm/insane/insane_scenes.cpp
@@ -812,7 +812,8 @@ void Insane::setEnemyCostumes() {
 	smush_warpMouse(160, 100, -1);
 }
 
-void Insane::procPreRendering() {
+void Insane::procPreRendering(byte *renderBitmap) {
+	(void)renderBitmap;  // Base class doesn't use this
 	_smush_isSanFileSetup = 0; // FIXME: This shouldn't be here
 
 	switchSceneIfNeeded();
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 087e4d53aaf..5fa4dae5791 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,4 +36,227 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
 	} while (--height);
 }
 
+/**
+ * Codec 21/44: Line Update codec
+ * Used for fonts (NUT files) and some embedded HUD frames.
+ * Format: Each line has a 2-byte size header, then 2-byte skip, 2-byte count pairs with literal pixels.
+ * The count value needs +1 to get the actual number of pixels to copy.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch + left;
+
+	while (height--) {
+		byte *dstPtrNext = dst + pitch;
+		const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
+		src += 2;  // Skip line size header
+		int len = width;
+		byte *lineDst = dst;
+
+		while (len > 0) {
+			// Read 2-byte LE skip value
+			int skip = READ_LE_UINT16(src);
+			src += 2;
+			lineDst += skip;
+			len -= skip;
+			if (len <= 0)
+				break;
+
+			// Read 2-byte LE copy count (+1 for actual count)
+			int count = READ_LE_UINT16(src) + 1;
+			src += 2;
+			if (count > len)
+				count = len;
+			len -= count;
+
+			// Copy literal pixels
+			memcpy(lineDst, src, count);
+			lineDst += count;
+			src += count;
+		}
+		dst = dstPtrNext;
+		src = srcPtrNext;
+	}
+}
+
+/**
+ * Codec 23: Skip/Copy with embedded RLE
+ * Used for video frames with skip regions.
+ * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch + left;
+
+	for (int row = 0; row < height; row++) {
+		int lineDataSize = READ_LE_UINT16(src);
+		src += 2;
+		const byte *lineEnd = src + lineDataSize;
+		byte *lineDst = dst;
+		int x = 0;
+
+		while (src < lineEnd && x < width) {
+			int skip = READ_LE_UINT16(src);
+			src += 2;
+			x += skip;  // Skip preserves previous frame content
+			if (src >= lineEnd || x >= width)
+				break;
+
+			int runSize = READ_LE_UINT16(src);
+			src += 2;
+			const byte *runEnd = src + runSize;
+
+			// Decode RLE within this run - write ALL colors including 0
+			while (src < runEnd && x < width) {
+				byte code = *src++;
+				int num = (code >> 1) + 1;
+				if (num > width - x)
+					num = width - x;
+
+				if (code & 1) {
+					// RLE run - repeat color
+					byte color = (src < runEnd) ? *src++ : 0;
+					memset(lineDst + x, color, num);
+					x += num;
+				} else {
+					// Literal run - copy bytes
+					int toCopy = num;
+					if (toCopy > (int)(runEnd - src))
+						toCopy = (int)(runEnd - src);
+					memcpy(lineDst + x, src, toCopy);
+					src += toCopy;
+					x += toCopy;
+				}
+			}
+			src = runEnd;
+		}
+		src = lineEnd;
+		dst += pitch;
+	}
+}
+
+/**
+ * Codec 45: RA2-specific BOMP RLE with variable header
+ * Used for embedded ANIM frames, particularly small animation elements.
+ * Has a variable-length header (commonly 6 bytes starting with 01 FE).
+ * Note: For overlay sprites, color 0 is treated as transparent.
+ */
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	dst += top * pitch + left;
+
+	// Detect header pattern and find RLE data start
+	int headerSkip = 0;
+
+	// Check for common 6-byte header pattern: 01 FE XX XX XX XX
+	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
+		headerSkip = 6;
+	} else {
+		// Probe offsets to find valid RLE start
+		// Valid start should have reasonable line size values
+		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+			int testLineSize = READ_LE_UINT16(src + testOffset);
+			// A valid line size should be positive and reasonable for the width
+			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+				// Further validation: try to count valid line sizes
+				int linesTest = 0;
+				const byte *testPtr = src + testOffset;
+				bool validSum = true;
+
+				while (linesTest < height && testPtr + 2 <= src + dataSize) {
+					int ls = READ_LE_UINT16(testPtr);
+					if (ls <= 0 || ls > width * 2) {
+						validSum = false;
+						break;
+					}
+					testPtr += ls + 2;
+					linesTest++;
+				}
+
+				if (validSum && linesTest >= height - 1) {
+					headerSkip = testOffset;
+					break;
+				}
+			}
+		}
+	}
+
+	src += headerSkip;
+	const byte *dataEnd = src + (dataSize - headerSkip);
+
+	// Check first value to determine per-line vs continuous mode
+	int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
+	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+
+	if (perLineMode) {
+		// Per-line RLE with 2-byte size headers
+		for (int row = 0; row < height && src < dataEnd; row++) {
+			int lineSize = READ_LE_UINT16(src);
+			src += 2;
+			if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
+				break;
+
+			const byte *lineEnd = src + lineSize;
+			byte *rowDst = dst + row * pitch;
+			int x = 0;
+
+			while (src < lineEnd && x < width) {
+				byte ctrl = *src++;
+				int count = (ctrl >> 1) + 1;
+
+				if (ctrl & 1) {
+					// RLE fill - color 0 is transparent for overlay sprites
+					byte color = (src < lineEnd) ? *src++ : 0;
+					if (color != 0) {
+						int num = (count > width - x) ? width - x : count;
+						memset(rowDst + x, color, num);
+					}
+					x += count;
+					if (x > width)
+						x = width;
+				} else {
+					// Literal copy - color 0 is transparent for overlay sprites
+					for (int i = 0; i < count && x < width && src < lineEnd; i++) {
+						byte color = *src++;
+						if (color != 0)
+							rowDst[x] = color;
+						x++;
+					}
+				}
+			}
+			src = lineEnd;
+		}
+	} else {
+		// Continuous BOMP RLE (no per-line headers)
+		for (int row = 0; row < height && src < dataEnd; row++) {
+			byte *rowDst = dst + row * pitch;
+			int x = 0;
+
+			while (x < width && src < dataEnd) {
+				byte ctrl = *src++;
+				int count = (ctrl >> 1) + 1;
+
+				if (ctrl & 1) {
+					// RLE fill - color 0 is transparent for overlay sprites
+					byte color = (src < dataEnd) ? *src++ : 0;
+					if (color != 0) {
+						int num = (count > width - x) ? width - x : count;
+						memset(rowDst + x, color, num);
+					}
+					x += count;
+					if (x > width)
+						x = width;
+				} else {
+					// Literal copy - color 0 is transparent for overlay sprites
+					for (int i = 0; i < count && x < width && src < dataEnd; i++) {
+						byte color = *src++;
+						if (color != 0)
+							rowDst[x] = color;
+						x++;
+					}
+				}
+			}
+		}
+	}
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index f88322f6846..7bd1b78ccfb 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -258,11 +258,20 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_seekPos = -1;
 
 	_skipNext = false;
+	_ra2FastForwarding = false;
 	_dst = nullptr;
 	_storeFrame = false;
 	_compressedFileMode = false;
 	_width = 0;
 	_height = 0;
+
+	// LOAD chunk streaming buffer (RA2)
+	_loadBuffer = nullptr;
+	_loadBufferSize = 0;
+	_loadBufferOffset = 0;
+	_loadReadOffset = 8;  // Original starts reading at offset 8 (skips header)
+	_lastLoadChunkIdx = -1;
+	_totalLoadChunks = 0;
 	_scrollX = 0;
 	_scrollY = 0;
 	_IACTpos = 0;
@@ -306,6 +315,14 @@ SmushPlayer::~SmushPlayer() {
 	delete _IACTchannel;
 	delete _compressedFileSoundHandle;
 	terminateAudio();
+
+	// Free any preserved frame buffer (RA2 preserves this across videos)
+	free(_frameBuffer);
+	_frameBuffer = nullptr;
+	free(_specialBuffer);
+	_specialBuffer = nullptr;
+	free(_loadBuffer);
+	_loadBuffer = nullptr;
 }
 
 void SmushPlayer::init(int32 speed) {
@@ -321,6 +338,31 @@ void SmushPlayer::init(int32 speed) {
 	_vm->setDirtyColors(0, 255);
 	_dst = vs->getPixels(0, 0);
 
+	// For Rebel Assault 2, handle background preservation between videos:
+	// - Cinematic videos (flags 0x20) clear the buffer for a fresh start
+	// - Gameplay videos (flags 0x28) preserve the existing screen content
+	//
+	// The virtual screen (_dst = vs->getPixels) persists between videos, so
+	// when a gameplay video starts, _dst already contains the last frame of
+	// the previous cinematic video - which is exactly what we want.
+	// We do NOT restore from _frameBuffer because STOR captures frame 0
+	// (often a black initialization frame), not the last frame.
+	if (_vm->_game.id == GID_REBEL2 && _dst != nullptr) {
+		if ((_curVideoFlags & 0x08) == 0) {
+			// Cinematic mode (flags 0x20) - clear buffer for fresh video
+			memset(_dst, 0, vs->w * vs->h);
+		} else {
+			// Gameplay mode (flags 0x28) - do nothing, preserve existing screen content
+			// Count non-zero pixels to verify there's actual content
+			int nonZero = 0;
+			for (int i = 0; i < vs->w * vs->h; i++) {
+				if (_dst[i] != 0) nonZero++;
+			}
+			debug("SmushPlayer::init: Preserving screen for gameplay video (%dx%d, %d%% non-zero)",
+				vs->w, vs->h, (nonZero * 100) / (vs->w * vs->h));
+		}
+	}
+
 	// HACK HACK HACK: This is an *evil* trick, beware!
 	// We do this to fix bug #1792. A proper solution would change all the
 	// drawing code to use the pitch value specified by the virtual screen.
@@ -354,8 +396,14 @@ void SmushPlayer::release() {
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
 
-	free(_frameBuffer);
-	_frameBuffer = nullptr;
+	// For Rebel Assault 2, preserve _frameBuffer across videos so that
+	// gameplay videos (which have no background FOBJ) can use the stored
+	// background from the previous BEG cinematic video.
+	// The _frameBuffer will be freed in the destructor or reused by the next video.
+	if (_vm->_game.id != GID_REBEL2) {
+		free(_frameBuffer);
+		_frameBuffer = nullptr;
+	}
 
 	_IACTstream = nullptr;
 
@@ -388,6 +436,97 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	}
 }
 
+/**
+ * Handle LOAD chunk for Rebel Assault 2
+ *
+ * LOAD chunks stream embedded resource data (likely audio for streaming playback)
+ * across multiple frames. The data is accumulated in a buffer and consumed by
+ * the audio system.
+ *
+ * Chunk format (from FUN_00424450 in original):
+ *   Offset 0 (2 bytes): totalChunks - Total number of LOAD chunks in sequence
+ *   Offset 2 (2 bytes): chunkIndex - Current chunk index (0-based)
+ *   Offset 4 (6 bytes): unknown/padding
+ *   Offset 10+: Actual data payload
+ *
+ * Processing:
+ *   - First chunk (index 0) resets the buffer
+ *   - Chunks must arrive sequentially (lastIndex + 1 == currentIndex)
+ *   - Data is accumulated until all chunks are received
+ *   - Consumer reads from buffer via getLoadData() (DAT_00482c18 read offset)
+ */
+void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
+
+	if (subSize < 10) {
+		warning("SmushPlayer::handleLoad: chunk too small (%d bytes)", subSize);
+		return;
+	}
+
+	// Read LOAD header
+	int16 totalChunks = b.readUint16LE();
+	int16 chunkIndex = b.readUint16LE();
+	b.skip(6);  // Unknown/padding
+
+	int32 dataSize = subSize - 10;
+
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: chunk %d/%d, dataSize=%d, bufferOffset=%d",
+		chunkIndex, totalChunks, dataSize, _loadBufferOffset);
+
+	// First chunk in sequence - reset buffer state
+	if (chunkIndex == 0) {
+		_loadBufferOffset = 0;
+		_loadReadOffset = 8;  // Original skips 8 bytes at start (header in accumulated data?)
+		_lastLoadChunkIdx = -1;
+		_totalLoadChunks = totalChunks;
+
+		// Allocate/reallocate buffer if needed
+		// Estimate buffer size: typical LOAD data per chunk is ~350 bytes
+		// Total expected: totalChunks * 400 bytes (with margin)
+		int32 estimatedSize = totalChunks * 400;
+		if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
+			free(_loadBuffer);
+			_loadBufferSize = estimatedSize;
+			_loadBuffer = (byte *)malloc(_loadBufferSize);
+			if (_loadBuffer == nullptr) {
+				warning("SmushPlayer::handleLoad: Failed to allocate %d bytes for LOAD buffer",
+					_loadBufferSize);
+				_loadBufferSize = 0;
+				return;
+			}
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Allocated %d bytes for LOAD buffer",
+				_loadBufferSize);
+		}
+	}
+
+	// Check sequential order (original: DAT_00482c3c - sVar1 == -1)
+	if (_lastLoadChunkIdx + 1 != chunkIndex) {
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Non-sequential chunk %d (expected %d), skipping",
+			chunkIndex, _lastLoadChunkIdx + 1);
+		return;
+	}
+
+	// Check buffer capacity (original: DAT_00482c14 + param_2 < DAT_00482c10)
+	if (_loadBuffer == nullptr || _loadBufferOffset + dataSize >= _loadBufferSize) {
+		warning("SmushPlayer::handleLoad: Buffer overflow - offset=%d size=%d limit=%d",
+			_loadBufferOffset, dataSize, _loadBufferSize);
+		return;
+	}
+
+	// Copy data to buffer
+	b.read(_loadBuffer + _loadBufferOffset, dataSize);
+	_loadBufferOffset += dataSize;
+	_lastLoadChunkIdx = chunkIndex;
+
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Accumulated %d bytes total", _loadBufferOffset);
+
+	// Check if sequence is complete
+	if (chunkIndex == totalChunks - 1) {
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Sequence complete - %d chunks, %d bytes total",
+			totalChunks, _loadBufferOffset);
+	}
+}
+
 void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
 	assert(subSize >= 8);
@@ -807,8 +946,11 @@ byte *SmushPlayer::getVideoPalette() {
 
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 
-void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height) {
+void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
 	if ((height == 242) && (width == 384)) {
 		if (_specialBuffer == 0)
 			_specialBuffer = (byte *)malloc(242 * 384);
@@ -819,13 +961,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
 			return;  // Skip this frame update
 		}
-		
-		if (_specialBuffer == 0) {
-			_specialBuffer = (byte *)malloc(width * height);
-			_width = width;
-			_height = height;
-		}
-		_dst = _specialBuffer;
+		// Rebel2 partial frames decode directly to main video buffer at (left, top)
+		// using screen pitch - don't use _specialBuffer
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;
 	// FT Insane uses smaller frames to draw overlays with moving objects
@@ -870,7 +1007,26 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		// Used by Full Throttle Classic (from Remastered)
 		smushDecodeUncompressed(_dst, src, left, top, width, height, _vm->_screenWidth);
 		break;
+	case SMUSH_CODEC_LINE_UPDATE:
+	case SMUSH_CODEC_LINE_UPDATE2:
+		// RA2: Skip/copy with literal pixels (used for fonts and HUD overlays)
+		smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
+		break;
+	case SMUSH_CODEC_SKIP_RLE:
+		// RA2: Skip/copy with embedded RLE (used for HUD frames with transparency)
+		smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
+		break;
+	case SMUSH_CODEC_RA2_BOMP:
+		// RA2: BOMP RLE with variable header (used for small animation elements)
+		smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
+		break;
 	default:
+		if (_vm->_game.id == GID_REBEL2) {
+			// Rebel Assault 2 may have other unknown codecs
+			debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
+				codec, left, top, width, height);
+			break;
+		}
 		error("Invalid codec for frame object : %d", codec);
 	}
 
@@ -907,7 +1063,8 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
 	int width = READ_LE_UINT16(ptr); ptr += 2;
 	int height = READ_LE_UINT16(ptr); ptr += 2;
 
-	decodeFrameObject(codec, fobjBuffer + 14, left, top, width, height);
+	int fobjDataSize = (int)decompressedSize - 14;
+	decodeFrameObject(codec, fobjBuffer + 14, left, top, width, height, fobjDataSize);
 
 	free(fobjBuffer);
 }
@@ -915,6 +1072,7 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
 void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
 	assert(subSize >= 14);
 	if (_skipNext) {
+		debug("SmushPlayer::handleFrameObject: SKIPPING due to _skipNext, frame=%d", _frame);
 		_skipNext = false;
 		return;
 	}
@@ -928,12 +1086,15 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	b.readUint16LE();
 	b.readUint16LE();
 
+	debug("SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
+		_frame, codec, left, top, width, height, subSize - 14);
+
 	int32 chunk_size = subSize - 14;
 	byte *chunk_buffer = (byte *)malloc(chunk_size);
 	assert(chunk_buffer);
 	b.read(chunk_buffer, chunk_size);
 
-	decodeFrameObject(codec, chunk_buffer, left, top, width, height);
+	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
 
 	free(chunk_buffer);
 }
@@ -944,13 +1105,19 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	_skipNext = false;
 
 	if (_insanity) {
-		_vm->_insane->procPreRendering();
+		_vm->_insane->procPreRendering(_dst);
 	}
 
 	while (frameSize > 0) {
 		const uint32 subType = b.readUint32BE();
 		const int32 subSize = b.readUint32BE();
 		const int32 subOffset = b.pos();
+
+		// Debug: Log all chunk types for first few frames
+		if (_vm->_game.id == GID_REBEL2 && _frame < 5) {
+			debug("SmushPlayer::handleFrame: frame=%d chunk=%s size=%d", _frame, tag2str(subType), subSize);
+		}
+
 		switch (subType) {
 		case MKTAG('N','P','A','L'):
 			handleNewPalette(subSize, b);
@@ -1000,6 +1167,9 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		case MKTAG('T','E','X','T'):
 			handleTextResource(subType, subSize, b);
 			break;
+		case MKTAG('L','O','A','D'):
+			handleLoad(subSize, b);
+			break;
 		default:
 			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 		}
@@ -1484,6 +1654,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 		if (_endOfFile)
 			break;
+
 		if (_vm->shouldQuit() || _vm->_saveLoadFlag || _vm->_smushVideoShouldFinish) {
 			_vm->_mixer->stopHandle(*_compressedFileSoundHandle);
 			_vm->_mixer->stopHandle(*_IACTchannel);
@@ -1492,6 +1663,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 			resetAudioTracks(); // For DIG demo
 			if (_imuseDigital)
 				_imuseDigital->stopSMUSHAudio(); // For DIG & COMI
+
 			break;
 		}
 
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index b4d236abf09..494ed2d8372 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -78,7 +78,11 @@ namespace Scumm {
 #define SMUSH_CODEC_RLE          1
 #define SMUSH_CODEC_RLE_ALT      3
 #define SMUSH_CODEC_UNCOMPRESSED 20
+#define SMUSH_CODEC_LINE_UPDATE  21   // RA2: Skip/copy with literal pixels
+#define SMUSH_CODEC_SKIP_RLE     23   // RA2: Skip/copy with embedded RLE
 #define SMUSH_CODEC_DELTA_BLOCKS 37
+#define SMUSH_CODEC_LINE_UPDATE2 44   // RA2: Variant of codec 21
+#define SMUSH_CODEC_RA2_BOMP     45   // RA2: BOMP RLE with variable header
 #define SMUSH_CODEC_DELTA_GLYPHS 47
 
 class ScummEngine_v7;
@@ -151,6 +155,7 @@ private:
 	uint32 _seekFrame;
 
 	bool _skipNext;
+	bool _ra2FastForwarding;  // Fast-forwarding RA2 BEG video to establish background
 	uint32 _frame;
 
 	Audio::SoundHandle *_IACTchannel;
@@ -249,7 +254,7 @@ private:
 	void tryCmpFile(const char *filename);
 
 	bool readString(const char *file);
-	void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height);
+	void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0);
 	void handleAnimHeader(int32 subSize, Common::SeekableReadStream &);
 	void handleFrame(int32 frameSize, Common::SeekableReadStream &);
 	void handleNewPalette(int32 subSize, Common::SeekableReadStream &);
@@ -261,8 +266,17 @@ private:
 	void handleIACT(int32 subSize, Common::SeekableReadStream &);
 	void handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &);
 	void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
+	void handleLoad(int32 subSize, Common::SeekableReadStream &);
 	void readPalette(byte *, Common::SeekableReadStream &);
 
+	// LOAD chunk streaming buffer (RA2 - embedded resource data)
+	byte *_loadBuffer;        // Accumulated LOAD data
+	int32 _loadBufferSize;    // Allocated buffer size
+	int32 _loadBufferOffset;  // Current write position (how much data accumulated)
+	int32 _loadReadOffset;    // Current read position (for streaming consumption)
+	int16 _lastLoadChunkIdx;  // Last processed chunk index (-1 = none)
+	int16 _totalLoadChunks;   // Total chunks expected in current sequence
+
 	void initAudio(int samplerate, int32 maxChunkSize);
 	void terminateAudio();
 	int isChanActive(int flagId);


Commit: 0a05f1f519d47b377294345188891a45ae4d8da6
    https://github.com/scummvm/scummvm/commit/0a05f1f519d47b377294345188891a45ae4d8da6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:55+02:00

Commit Message:
SCUMM: RA2: Fix missing handler 1 crash

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 7bd1b78ccfb..efb7d20abc3 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -254,6 +254,7 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_base = nullptr;
 	_frameBuffer = nullptr;
 	_specialBuffer = nullptr;
+	_specialBufferSize = 0;
 
 	_seekPos = -1;
 
@@ -952,8 +953,12 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
 
 void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
 	if ((height == 242) && (width == 384)) {
-		if (_specialBuffer == 0)
-			_specialBuffer = (byte *)malloc(242 * 384);
+		int bufSize = 242 * 384;
+		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+			free(_specialBuffer);
+			_specialBuffer = (byte *)malloc(bufSize);
+			_specialBufferSize = bufSize;
+		}
 		_dst = _specialBuffer;
 	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
 		// For Rebel2, check if this partial update overlaps with a destroyed enemy
@@ -961,8 +966,18 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
 			return;  // Skip this frame update
 		}
-		// Rebel2 partial frames decode directly to main video buffer at (left, top)
-		// using screen pitch - don't use _specialBuffer
+		// Rebel2 videos use frames larger than the screen (e.g., 424x260 vs 320x200).
+		// Allocate a special buffer to hold the full frame - this is needed because
+		// codecs like 37/47 write the full frame size regardless of screen size.
+		int bufSize = width * height;
+		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+			free(_specialBuffer);
+			_specialBuffer = (byte *)malloc(bufSize);
+			_specialBufferSize = bufSize;
+			_width = width;
+			_height = height;
+		}
+		_dst = _specialBuffer;
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;
 	// FT Insane uses smaller frames to draw overlays with moving objects
@@ -975,8 +990,9 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		_width = width;
 		_height = height;
 	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		// Do not update _width/_height here to preserve original video size
-		// if frames are partial updates
+		// Do not update _width/_height here - preserve original video size set during
+		// buffer allocation. Small overlay sprites (e.g., 8x7) need to use the background
+		// frame's pitch (424) to be drawn at the correct position in the buffer.
 	} else {
 		_width = _vm->_screenWidth;
 		_height = _vm->_screenHeight;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 494ed2d8372..2ab2759cc18 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -147,6 +147,7 @@ private:
 	uint32 _baseSize;
 	byte *_frameBuffer;
 	byte *_specialBuffer;
+	int _specialBufferSize;
 
 	Common::String _seekFile;
 	uint32 _startFrame;


Commit: 54610c886d3658318dffbfcd0a63616132ff11bc
    https://github.com/scummvm/scummvm/commit/54610c886d3658318dffbfcd0a63616132ff11bc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:56+02:00

Commit Message:
SCUMM: RA2: Refactor level object handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5e9ca23ecc5..cf98a73f119 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3172,14 +3172,65 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
 	// We do NOT redraw the background here as that would overwrite FOBJ content.
 
-	// ============================================================
-	// STEP 0: Fill status bar background (FUN_004288c0 equivalent)
-	// ============================================================
-	// Clear the status bar area at Y=180-199 with background color
+	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
+	// 1. FUN_004288c0: Fill status bar background at Y=0xb4 (180)
+	// 2. FUN_004089ab: Draw turret overlays, targeting reticle, crosshair
+	// 3. FUN_0041c012: Draw status bar sprites LAST (on top)
+
+	// STEP 0: Fill status bar background (FUN_004288c0)
+	renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
+
+	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
+	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
+
+	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
+	renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
+
+	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
+	renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
+
+	// Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
+	debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
+		_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
+		_shipSprite ? _shipSprite->getNumChars() : 0,
+		_flyShipSprite ? _flyShipSprite->getNumChars() : 0);
+
+	renderHandler7Ship(renderBitmap, pitch, width, height);
+	renderHandler8Ship(renderBitmap, pitch, width, height);
+	renderFallbackShip(renderBitmap, pitch, width, height);
+
+	// Enemy indicators and destroyed enemy area erase
+	renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
+
+	// Explosion animations (FUN_409FBC)
+	renderExplosions(renderBitmap, pitch, width, height);
+
+	// Laser shot beams and impacts
+	renderLaserShots(renderBitmap, pitch, width, height);
+
+	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
+	if (_rebelHandler == 7 || _rebelHandler == 8) {
+		drawCollisionZones(renderBitmap, pitch, width, height, 0);
+	}
+
+	// Crosshair/reticle (FUN_004089ab, FUN_0040d836)
+	renderCrosshair(renderBitmap, pitch, width, height);
+
+	// HUD score/lives rendering (FUN_0041c012)
+	renderScoreHUD(renderBitmap, pitch, width, height, 0);
+
+	// Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
+	frameEndCleanup();
+}
+
+// ======================= Rendering Helper Functions =======================
+// These are extracted from procPostRendering for better readability
+
+void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
+											 int videoWidth, int videoHeight, int statusBarY) {
+	// Fill status bar background (FUN_004288c0 equivalent)
 	// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
 	// This fills width=320, height=20 starting at Y=180 with color index 4
-	// Status bar is ALWAYS shown during gameplay (_rebelHandler != 0)
-	// Hidden during cinematics/intros when _rebelHandler == 0 (handled by early return above)
 	const byte statusBarBgColor = 4;
 
 	for (int y = statusBarY; y < videoHeight; y++) {
@@ -3191,795 +3242,607 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			renderBitmap[destY * pitch + destX] = statusBarBgColor;
 		}
 	}
-	
-	// ============================================================
-	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
-	// ============================================================
-	// For turret handlers, the HUD overlay is loaded as a NUT file via IACT opcode 8
-	// and contains animated frames for blinking cockpit lights.
-	//
+}
+
+void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
+	// Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
 	// From FUN_004089ab disassembly (lines 195-226):
 	// - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
 	// - Position formula (low-res):
 	//   X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
 	//   Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
 	// - Animation: spriteIndex = (frameCounter / 2) % 6
-	//
-	// The mouse offset creates a subtle parallax effect as the view pans.
-	if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlayNut && _hudOverlayNut->getNumChars() > 0) {
-		// Calculate mouse offset (clamped to -127..127)
-		// These match DAT_0047a7e0/DAT_0047a7e2 in the original
-		int mouseOffsetX = (_vm->_mouse.x - 160);  // Relative to screen center
-		int mouseOffsetY = (_vm->_mouse.y - 100);  // Relative to screen center
-		if (mouseOffsetX > 127) mouseOffsetX = 127;
-		if (mouseOffsetX < -127) mouseOffsetX = -127;
-		if (mouseOffsetY > 127) mouseOffsetY = 127;
-		if (mouseOffsetY < -127) mouseOffsetY = -127;
-
-		// Animation frame cycling: (frameCounter / 2) % 6
-		// This creates a 12-frame cycle (each sprite shown for 2 video frames)
-		int numSprites = _hudOverlayNut->getNumChars();
-		int animFrameCount = MIN(numSprites, 6);  // Use up to 6 frames for animation
-		int animFrame = 0;
-		if (animFrameCount > 0) {
-			animFrame = (curFrame / 2) % animFrameCount;
-		}
-
-		// Get sprite dimensions and offset
-		// NUT format stores sprite offsets that affect positioning
-		int spriteW = _hudOverlayNut->getCharWidth(animFrame);
-		int spriteH = _hudOverlayNut->getCharHeight(animFrame);
-
-		// Position calculation from assembly (low-res mode):
-		// X = 0xa0 (160) + (mouseOffsetX >> 4) - (width / 2) - offsetX
-		// Y = 0xb6 (182) - (mouseOffsetY >> 4) - height - offsetY
-		// Note: The sprite offset is embedded in the NUT data; for simplicity we use 0
-		int spriteOffsetX = 0;  // Could be extracted from NUT if needed
-		int spriteOffsetY = 0;
-
-		int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
-		int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
-
-		// Apply view offset for scrolling background
-		hudX += _viewX;
-		hudY += _viewY;
-
-		// Draw the HUD overlay:
-		// From FUN_004089ab: Sprite 0 is ALWAYS drawn first (base cockpit)
-		// Then if animation frame != 0, draw the animation frame on top (blinking lights)
-		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
-
-		// Draw animation overlay frame if not frame 0
-		// Animation frames 1-5 contain the blinking light states
-		if (animFrame != 0 && animFrame < numSprites) {
-			renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
-		}
-
-		debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
-			  animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
+
+	if ((_rebelHandler != 0x26 && _rebelHandler != 0x19) || !_hudOverlayNut || _hudOverlayNut->getNumChars() <= 0)
+		return;
+
+	// Calculate mouse offset (clamped to -127..127)
+	int mouseOffsetX = (_vm->_mouse.x - 160);
+	int mouseOffsetY = (_vm->_mouse.y - 100);
+	if (mouseOffsetX > 127) mouseOffsetX = 127;
+	if (mouseOffsetX < -127) mouseOffsetX = -127;
+	if (mouseOffsetY > 127) mouseOffsetY = 127;
+	if (mouseOffsetY < -127) mouseOffsetY = -127;
+
+	// Animation frame cycling: (frameCounter / 2) % 6
+	int numSprites = _hudOverlayNut->getNumChars();
+	int animFrameCount = MIN(numSprites, 6);
+	int animFrame = 0;
+	if (animFrameCount > 0) {
+		animFrame = (curFrame / 2) % animFrameCount;
 	}
 
-	// Draw secondary HUD overlay if present (DAT_0047fe80)
-	if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && _hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
-		// Similar positioning to primary overlay
-		int mouseOffsetX = (_vm->_mouse.x - 160);
-		int mouseOffsetY = (_vm->_mouse.y - 100);
-		if (mouseOffsetX > 127) mouseOffsetX = 127;
-		if (mouseOffsetX < -127) mouseOffsetX = -127;
-		if (mouseOffsetY > 127) mouseOffsetY = 127;
-		if (mouseOffsetY < -127) mouseOffsetY = -127;
+	// Get sprite dimensions
+	int spriteW = _hudOverlayNut->getCharWidth(animFrame);
+	int spriteH = _hudOverlayNut->getCharHeight(animFrame);
 
-		int spriteW = _hudOverlay2Nut->getCharWidth(0);
-		int spriteH = _hudOverlay2Nut->getCharHeight(0);
+	// Position calculation from assembly (low-res mode)
+	int spriteOffsetX = 0;
+	int spriteOffsetY = 0;
+	int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
+	int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
 
-		int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2);
-		int hudY = 182 - (mouseOffsetY >> 4) - spriteH;
+	// Apply view offset for scrolling background
+	hudX += _viewX;
+	hudY += _viewY;
 
-		hudX += _viewX;
-		hudY += _viewY;
+	// Draw base cockpit (sprite 0 always drawn first)
+	renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
 
-		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlay2Nut, 0);
+	// Draw animation overlay frame if not frame 0
+	if (animFrame != 0 && animFrame < numSprites) {
+		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
 	}
 
-	// ============================================================
-	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
-	// ============================================================
+	debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
+		  animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
+
+	// Draw secondary HUD overlay if present (DAT_0047fe80)
+	if (_hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
+		int spr2W = _hudOverlay2Nut->getCharWidth(0);
+		int spr2H = _hudOverlay2Nut->getCharHeight(0);
+		int hud2X = 160 + (mouseOffsetX >> 4) - (spr2W / 2) + _viewX;
+		int hud2Y = 182 - (mouseOffsetY >> 4) - spr2H + _viewY;
+		renderNutSprite(renderBitmap, pitch, width, height, hud2X, hud2Y, _hudOverlay2Nut, 0);
+	}
+}
+
+void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height) {
+	// Draw embedded SAN HUD overlays (from IACT chunks)
 	// For Handler 7 (Level 3): HUD elements are scattered across the screen
-	//   - Each frame has its own renderX/renderY position from FOBJ left/top
-	//   - userId 1-11 are different HUD elements
-	//   - userIds at the same position are direction variants (select one based on direction)
 	// For turret handlers: slots 1-2 form a two-part cockpit overlay
-	//   - userId 1: Left piece at X=0
-	//   - userId 2: Right piece at X=slot1Width
-	// These are drawn BEFORE the status bar so status bar appears on top
-
-	// For Handler 7 ship direction: identify ship frames at same position
-	// Level 3 has userId 1 and 2 at position (162, 105) - these are direction variants
-	// We select one based on the current direction to simulate ship turning
 
 	for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
 		EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
-		if (frame.valid && frame.pixels && frame.width > 0 && frame.height > 0) {
-			// Skip frames at position (0,0) with small dimensions - these are likely animation patches
-			// that need special handling (userId=3: 11x26, userId=4: 17x53 in Level 1)
-			// TODO: Investigate how these are used in the original assembly
-			if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
-				debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
-					hudSlot, frame.width, frame.height);
-				continue;
-			}
+		if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
+			continue;
 
-			int destX, destY;
+		// Skip small frames at (0,0) - likely animation patches
+		if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
+			debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
+				hudSlot, frame.width, frame.height);
+			continue;
+		}
 
-			// For Handler 7: Check if this is a ship direction frame
-			// Ship frames are at the same position - skip all but the selected one
-			if (_rebelHandler == 7) {
-				// Collect all frame IDs in this position group (may not be consecutive)
-				int groupMembers[16];
-				int groupCount = 0;
+		// For Handler 7: handle direction-based frame selection
+		if (_rebelHandler == 7) {
+			int groupMembers[16];
+			int groupCount = 0;
 
-				for (int id = 1; id < 16; id++) {
-					EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
-					if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
-						g.width == frame.width && g.height == frame.height) {
-						groupMembers[groupCount++] = id;
-					}
+			for (int id = 1; id < 16; id++) {
+				EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
+				if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
+					g.width == frame.width && g.height == frame.height) {
+					groupMembers[groupCount++] = id;
 				}
+			}
 
-				// If there's more than one frame in this group, select based on direction
-				if (groupCount > 1) {
-					// Map direction index (0-34) to group index (0 to groupCount-1)
-					int selectedOffset = _shipDirectionIndex % groupCount;
-					int selectedId = groupMembers[selectedOffset];
-
-					// Check if selected frame has pixels, if not find one that does
-					EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
-					int nonZero = 0;
-					for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
-						if (selectedFrame.pixels[i] != 0) nonZero++;
-					}
+			if (groupCount > 1) {
+				int selectedOffset = _shipDirectionIndex % groupCount;
+				int selectedId = groupMembers[selectedOffset];
 
-					if (nonZero == 0) {
-						// Selected frame is empty, find another with pixels
-						for (int i = 0; i < groupCount; i++) {
-							EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
-							int altNonZero = 0;
-							for (int j = 0; j < altFrame.width * altFrame.height; j++) {
-								if (altFrame.pixels[j] != 0) altNonZero++;
-							}
-							if (altNonZero > 0) {
-								selectedId = groupMembers[i];
-								break;
-							}
-						}
-					}
+				// Verify selected frame has pixels
+				EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
+				int nonZero = 0;
+				for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
+					if (selectedFrame.pixels[i] != 0) nonZero++;
+				}
 
-					// Only render if this is the selected frame
-					if (hudSlot != selectedId) {
-						continue;  // Skip this frame, render the selected one instead
+				if (nonZero == 0) {
+					for (int i = 0; i < groupCount; i++) {
+						EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
+						int altNonZero = 0;
+						for (int j = 0; j < altFrame.width * altFrame.height; j++) {
+							if (altFrame.pixels[j] != 0) altNonZero++;
+						}
+						if (altNonZero > 0) {
+							selectedId = groupMembers[i];
+							break;
+						}
 					}
 				}
-			}
 
-			// Use the stored render position from the embedded ANIM data
-			// The renderX/renderY are set from FOBJ's left/top values
-			destX = frame.renderX;
-			destY = frame.renderY;
-
-			// For Handler 0x26 (turret) and 0x19 (mixed): Embedded ANIM frame positioning
-			// From FUN_00407fcb assembly lines 389-391: screen dimensions are 320x200 (0x140 x 0xc8)
-			// Unlike NUT overlays (which use 0xb6=182 base with mouse parallax), embedded ANIM
-			// frames use screen height (200) as the base for bottom-aligned positioning.
-			// Formula: X = 160 - width/2 - fobj_left, Y = 200 - height - fobj_top
-			if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
-				// X: centered horizontally, adjusted by FOBJ left offset
-				destX = 160 - frame.width / 2 - frame.renderX;
-				// Y: screen height (0xc8=200) - height - fobj_top for bottom-aligned cockpit
-				destY = 200 - frame.height - frame.renderY;
+				if (hudSlot != selectedId)
+					continue;
 			}
+		}
 
-			// For Handler 7 (space flight): HUD cockpit overlays use FOBJ position directly
-			// From FUN_0040d836: The clip region is 320x170 (0x140 x 0xaa)
-			// Large cockpit frames (hudSlot 1/2) need centering like turret handler
-			if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
-				// Large cockpit frame - center horizontally, position at bottom
-				destX = 160 - frame.width / 2 - frame.renderX;
-				// Position above status bar area (170 is render height for Handler 7)
-				destY = 170 - frame.height - frame.renderY;
-			} else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
-				// Ship sprite in center of screen - apply position offset
-				int16 offsetX = (_shipPosX - 160) / 8;
-				int16 offsetY = (_shipPosY - 100) / 8;
-				destX += offsetX;
-				destY += offsetY;
-			}
+		// Calculate destination position
+		int destX = frame.renderX;
+		int destY = frame.renderY;
 
-			// Apply View Offset for all HUD elements
-			destX += _viewX;
-			destY += _viewY;
-
-			// Debug: log embedded frame render dimensions
-			debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d) screen=%dx%d",
-				hudSlot, frame.width, frame.height, destX, destY, pitch, height);
-
-			// Draw frame with transparency (pixel 0 and 231 = transparent, matching NUT rendering)
-			for (int y = 0; y < frame.height && (destY + y) < height; y++) {
-				for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
-					byte pixel = frame.pixels[y * frame.width + x];
-					if (pixel != 0 && pixel != 231) {  // Skip transparent pixels (0 and 231/0xE7)
-						int fx = destX + x;
-						int fy = destY + y;
-						if (fx >= 0 && fy >= 0) {
-							renderBitmap[fy * pitch + fx] = pixel;
-						}
+		// Handler 0x26/0x19 turret positioning
+		if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
+			destX = 160 - frame.width / 2 - frame.renderX;
+			destY = 200 - frame.height - frame.renderY;
+		}
+
+		// Handler 7 large cockpit frame positioning
+		if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
+			destX = 160 - frame.width / 2 - frame.renderX;
+			destY = 170 - frame.height - frame.renderY;
+		} else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
+			int16 offsetX = (_shipPosX - 160) / 8;
+			int16 offsetY = (_shipPosY - 100) / 8;
+			destX += offsetX;
+			destY += offsetY;
+		}
+
+		destX += _viewX;
+		destY += _viewY;
+
+		debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d)",
+			hudSlot, frame.width, frame.height, destX, destY);
+
+		// Draw frame with transparency (pixel 0 and 231 = transparent)
+		for (int y = 0; y < frame.height && (destY + y) < height; y++) {
+			for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
+				byte pixel = frame.pixels[y * frame.width + x];
+				if (pixel != 0 && pixel != 231) {
+					int fx = destX + x;
+					int fy = destY + y;
+					if (fx >= 0 && fy >= 0) {
+						renderBitmap[fy * pitch + fx] = pixel;
 					}
 				}
 			}
 		}
 	}
-	
-	// ============================================================
-	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
-	// ============================================================
-	// DISPFONT.NUT contains status bar elements - drawn ON TOP of cockpit overlay
-	// From assembly FUN_0041c012:
-	//   - Sprite 1: Status bar background frame (full width, drawn at 0,0)
-	//   - Sprites 2-5: Difficulty stars (1-4 stars, drawn at 0,0)
-	//   - Sprite 6: Damage bar fill (drawn with clip rect X=0x3f, Y=9, W=64, H=6)
-	//   - Sprite 7: Damage alert (flashing red when damage critical)
-	// Status bar is ALWAYS drawn during gameplay (_rebelHandler != 0)
-	if (_smush_cockpitNut) {
-		// Draw status bar background frame (sprite 1) at (0, statusBarY)
-		// This sprite is the full-width status bar background
-		if (_smush_cockpitNut->getNumChars() > 1) {
-			renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
-		}
-		
-		// Draw difficulty indicator (sprites 2-5 based on difficulty level 0-3)
-		// Sprite index = difficulty + 2; capped at 4 max difficulty (sprite 5)
-		// Assembly draws at (0,0) in buffer - same position as sprite 1
-		int difficulty = 0;  // TODO: Read from game state (DAT_0047a7fa)
-		if (difficulty > 3) difficulty = 3;
-		int difficultySprite = difficulty + 2;  // sprites 2, 3, 4, or 5
-		if (_smush_cockpitNut->getNumChars() > difficultySprite) {
-			renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
-		}
-		
-// Draw damage bar (sprite 6) 
-			// Assembly uses clip rect: X=0x3f(63), Y=0x9(9), W=0x40(64), H=0x6(6)
-			// The width is scaled based on accumulated damage value (0..255)
-			// For now, draw at position (0, statusBarY) - sprite has internal positioning
-			if (_smush_cockpitNut->getNumChars() > 6) {
-				// Calculate width based on damage value.
-				// Damage range: 0..255 where 255 = full (Width 64)
-				int drawWidth = (64 * _playerDamage) / 255;
-			if (drawWidth < 0) drawWidth = 0;
-			if (drawWidth > 64) drawWidth = 64;
-			
-			// We need to draw a partial sprite or use a clip rect.
-			// smlayer_drawSomething supports scaling/clip?
-			// The current implementation of smlayer_drawSomething just draws the whole sprite.
-			// We can pass a "frame" or clip rect if we modify the function or use a lower level draw.
-			// For now, let's just draw the full sprite if damage < 255, to verify it appears.
-			// Ideally we should implement clipping.
-			
-			// NOTE: smlayer_drawSomething calls `_smush_cockpitNut->draw(...)`
-			// We can't easily clip without modifying NutRenderer or using a custom draw loop.
-			// Let's implement a custom draw loop for the damage bar here since it's simple.
-			
-			// Sprite 6 data (we need to copy a CLIP rect from within this sprite)
-			const byte *src = _smush_cockpitNut->getCharData(6);
-			int sw = _smush_cockpitNut->getCharWidth(6);
-			int sh = _smush_cockpitNut->getCharHeight(6);
-			// Clip rect inside the sprite (from assembly): X=63, Y=9, W=64, H=6
-			const int clipX = 63;
-			const int clipY = 9;
-			const int clipW = 64;
-			const int clipH = 6;
-			
-			// Draw clipped width
-			if (src && sw > 0 && sh > 0) {
-				// Clamp drawWidth to the clip width and ensure we don't read past sprite bounds
-				int maxClipW = sw - clipX;
-				if (maxClipW < 0) maxClipW = 0;
-				int drawW = drawWidth;
-				if (drawW > clipW) drawW = clipW;
-				if (drawW > maxClipW) drawW = maxClipW;
-				if (drawW > 0) {
-					int drawH = clipH;
-					if (drawH > (sh - clipY)) drawH = sh - clipY;
-					if (drawH < 0) drawH = 0;
-					for (int y = 0; y < drawH; y++) {
-						for (int x = 0; x < drawW; x++) {
-							// Render to (clipX + x + viewX, statusBarY + clipY + y + viewY)
-							int destX = clipX + x + _viewX;
-							int destY = statusBarY + clipY + y + _viewY;
-							if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
-								byte pixel = src[(clipY + y) * sw + (clipX + x)];
-								if (pixel != 0) {
-									renderBitmap[destY * pitch + destX] = pixel;
-								}
-							}
+}
+
+void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
+										  int statusBarY, int32 curFrame) {
+	// Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
+	// DISPFONT.NUT contains:
+	//   Sprite 1: Status bar background frame
+	//   Sprites 2-5: Difficulty stars (1-4)
+	//   Sprite 6: Damage bar fill (with clip rect X=63, Y=9, W=64, H=6)
+	//   Sprite 7: Damage alert (flashing red when critical)
+
+	if (!_smush_cockpitNut)
+		return;
+
+	// Sprite 1: Status bar background
+	if (_smush_cockpitNut->getNumChars() > 1) {
+		renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
+	}
+
+	// Difficulty indicator (sprites 2-5)
+	int difficulty = 0;  // TODO: Read from game state
+	if (difficulty > 3) difficulty = 3;
+	int difficultySprite = difficulty + 2;
+	if (_smush_cockpitNut->getNumChars() > difficultySprite) {
+		renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
+	}
+
+	// Damage bar (sprite 6) with clipped width
+	if (_smush_cockpitNut->getNumChars() > 6) {
+		int drawWidth = (64 * _playerDamage) / 255;
+		if (drawWidth < 0) drawWidth = 0;
+		if (drawWidth > 64) drawWidth = 64;
+
+		const byte *src = _smush_cockpitNut->getCharData(6);
+		int sw = _smush_cockpitNut->getCharWidth(6);
+		int sh = _smush_cockpitNut->getCharHeight(6);
+
+		// Clip rect inside sprite: X=63, Y=9, W=64, H=6
+		const int clipX = 63, clipY = 9, clipW = 64, clipH = 6;
+
+		if (src && sw > 0 && sh > 0) {
+			int maxClipW = sw - clipX;
+			if (maxClipW < 0) maxClipW = 0;
+			int drawW = MIN(drawWidth, MIN(clipW, maxClipW));
+			int drawH = MIN(clipH, sh - clipY);
+			if (drawH < 0) drawH = 0;
+
+			for (int y = 0; y < drawH; y++) {
+				for (int x = 0; x < drawW; x++) {
+					int destX = clipX + x + _viewX;
+					int destY = statusBarY + clipY + y + _viewY;
+					if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
+						byte pixel = src[(clipY + y) * sw + (clipX + x)];
+						if (pixel != 0) {
+							renderBitmap[destY * pitch + destX] = pixel;
 						}
 					}
 				}
 			}
 		}
-		
-		// Draw damage alert overlay (sprite 7) when damage is critical (> 0xAA = 170)
-		// Only draws when frame counter bit 3 is clear (every 8 frames)
-		if (_smush_cockpitNut->getNumChars() > 7) {
-			if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
-				// Draw overlay sprite 7 at same general region (sx, sy)
-				renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
-			}
-		}
-		
-		// Draw lives indicator - assembly shows at X=0xa8 (168), Y=7
-		// Uses sprite 1 again with different clip rect
-		// TODO: Implement lives rendering
-
-		// Draw score - uses FUN_00434cb0 (text rendering) at X=0x101(257)
-		// TODO: Implement score rendering
 	}
 
-	// ============================================================
-	// HANDLER 8 SHIP RENDERING (FUN_00401ccf equivalent)
-	// ============================================================
-	// For third-person vehicle missions (Handler 8), draw the player's ship sprite
-	// The ship position is calculated from _shipPosX/_shipPosY
-	//
-	// From FUN_00401ccf disassembly (lines 87-95):
-	// - Ship is drawn when DAT_0047e010 != NULL AND DAT_0043e000 != 5
-	// - Position offset X: (DAT_0043e006 - 0xa0) >> 3 = (shipPosX - 160) >> 3
-	// - Position offset Y: (DAT_0043e008 - 0x28) >> 2 = (shipPosY - 40) >> 2
-	// - Sprite index: param_5 & 1 (0 = normal, 1 = firing)
-	//
-	// The crosshair is drawn at lines 128-135 with base position:
-	// - Low-res: X = offset + 0xa0 (160), Y = offset + 0x69 (105)
-	// - High-res: X = offset*2 + 0x140 (320), Y = offset*2 + 0xd2 (210)
-	//
-	// The ship sprite is drawn using the same offset calculation but passed directly
-	// to FUN_004236e0. The rendering function treats these as screen coordinates.
-	debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
-		_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
-		_shipSprite ? _shipSprite->getNumChars() : 0,
-		_flyShipSprite ? _flyShipSprite->getNumChars() : 0);
+	// Damage alert overlay (sprite 7) when damage > 170 and flashing
+	if (_smush_cockpitNut->getNumChars() > 7) {
+		if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
+			renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
+		}
+	}
+}
 
+void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 7 Ship Rendering (Space Flight - FLY sprites)
-	// Handler 7 uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
-	// Different from Handler 8's POV sprites
-	if (_rebelHandler == 7 && _flyShipSprite && _shipLevelMode != 5) {
-		// Handler 7 position calculation from FUN_0040d836 lines 173-175
-		// Draw position: (transformedX - 0xd4, transformedY - 0x82)
-		// where transformedX/Y come from FUN_0041c720 transformation
-		// For simplicity, we calculate based on ship direction
-
-		// Base position calculation (simplified from assembly)
-		// The ship is drawn at a fixed position offset by direction
-		int baseX = 160;  // Screen center X
-		int baseY = 105;  // Screen center Y (0x69)
-
-		// Add small offset based on ship position for "flight feel"
-		int16 posOffsetX = (_flyShipScreenX - 160) / 10;
-		int16 posOffsetY = (_flyShipScreenY - 100) / 10;
-
-		int shipScreenX = baseX + posOffsetX;
-		int shipScreenY = baseY + posOffsetY;
-
-		int numSprites = _flyShipSprite->getNumChars();
-		int spriteIndex = _shipDirectionIndex;
-
-		// Validate sprite index
-		if (spriteIndex < 0) spriteIndex = 0;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+	// Uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
 
-		// Get sprite dimensions and center it
-		int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
-		int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
-		int drawX = shipScreenX - spriteW / 2 + _viewX;
-		int drawY = shipScreenY - spriteH / 2 + _viewY;
+	if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
+		return;
 
-		// Draw the ship sprite (DAT_0047fee8 / FLY001)
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+	// Base position at screen center with direction offset
+	int baseX = 160;
+	int baseY = 105;
+	int16 posOffsetX = (_flyShipScreenX - 160) / 10;
+	int16 posOffsetY = (_flyShipScreenY - 100) / 10;
+	int shipScreenX = baseX + posOffsetX;
+	int shipScreenY = baseY + posOffsetY;
 
-		// Draw laser overlay if firing and laser sprite loaded (FLY002)
-		if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
-			// Laser sprite uses same direction index for frame animation
-			int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
-			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
-		}
+	int numSprites = _flyShipSprite->getNumChars();
+	int spriteIndex = _shipDirectionIndex;
+	if (spriteIndex < 0) spriteIndex = 0;
+	if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
 
-		// Draw targeting overlay if loaded (FLY003) - goes at crosshair position
-		if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
-			// Draw targeting reticle at ship position
-			int targetW = _flyTargetSprite->getCharWidth(0);
-			int targetH = _flyTargetSprite->getCharHeight(0);
-			int targetX = shipScreenX - targetW / 2 + _viewX;
-			int targetY = shipScreenY - targetH / 2 + _viewY;
-			renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
-		}
+	// Center sprite at position
+	int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
+	int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
+	int drawX = shipScreenX - spriteW / 2 + _viewX;
+	int drawY = shipScreenY - spriteH / 2 + _viewY;
 
-		debug("Rebel2 Handler7: Ship drawn at (%d,%d) screenPos=(%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
-			drawX, drawY, shipScreenX, shipScreenY, spriteIndex, numSprites,
-			_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
+
+	// Laser overlay if firing
+	if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
+		int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
+		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
+	}
+
+	// Targeting overlay
+	if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
+		int targetW = _flyTargetSprite->getCharWidth(0);
+		int targetH = _flyTargetSprite->getCharHeight(0);
+		int targetX = shipScreenX - targetW / 2 + _viewX;
+		int targetY = shipScreenY - targetH / 2 + _viewY;
+		renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
 	}
 
+	debug("Rebel2 Handler7: Ship at (%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
+		drawX, drawY, spriteIndex, numSprites, _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+}
+
+void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 8 Ship Rendering (Third-Person Vehicle - POV sprites)
-	// Handler 8 uses _shipSprite (POV001) with position-based offset
-	else if (_rebelHandler == 8 && _shipSprite && _shipLevelMode != 5) {
-		// Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
-		// The shift operations create a dampened movement effect
-		int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;  // (shipPosX - 160) >> 3
-		int16 displayOffsetY = (_shipPosY - 0x28) >> 2;  // (shipPosY - 40) >> 2
-
-		// Base screen position from crosshair calculation (FUN_00401ccf lines 128-129)
-		// Low-res mode: base X = 0xa0 (160), base Y = 0x69 (105)
-		int shipScreenX = 0xa0 + displayOffsetX;  // 160 + offset
-		int shipScreenY = 0x69 + displayOffsetY;  // 105 + offset
-
-		int numSprites = _shipSprite->getNumChars();
-		int spriteIndex = 0;
-
-		// Select sprite based on direction when multiple sprites are available
-		// The ship sprite sheet is organized as a grid of direction sprites:
-		// - 35 sprites (5x7): Full direction grid
-		// - 25 sprites (5x5): Reduced grid
-		// - 5 sprites: Horizontal direction only
-		// - 2 sprites: Normal (0) and firing (1)
-		// - 1 sprite: Static ship
-		if (numSprites >= 35) {
-			spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
-			if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-		} else if (numSprites >= 25) {
-			int vDir5 = (_shipDirectionV * 5) / 7;
-			spriteIndex = _shipDirectionH * 5 + vDir5;
-			if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-		} else if (numSprites >= 5) {
-			spriteIndex = _shipDirectionH;
-			if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-		} else if (numSprites == 2) {
-			spriteIndex = _shipFiring ? 1 : 0;
-		}
-
-		// Get sprite dimensions and center it at the calculated position
-		int spriteW = _shipSprite->getCharWidth(spriteIndex);
-		int spriteH = _shipSprite->getCharHeight(spriteIndex);
-		int drawX = shipScreenX - spriteW / 2 + _viewX;
-		int drawY = shipScreenY - spriteH / 2 + _viewY;
-
-		// Draw the primary ship sprite (DAT_0047e010)
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
-
-		// Draw secondary ship sprite if available (DAT_0047e028)
-		if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
-			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
-		}
-
-		debug("Rebel2 Handler8: Ship drawn at screen(%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
-			drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
-			spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
-	}
-
-	// Fallback: Use embedded HUD frame as ship sprite
-	else if ((_rebelHandler == 7 || _rebelHandler == 8) && _shipLevelMode != 5) {
-		// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
-		// userId=11 contains the ship sprite strip
-		EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
-		if (shipFrame.valid && shipFrame.pixels && shipFrame.width > 0 && shipFrame.height > 0) {
-			// Calculate display offset from raw ship position
-			int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
-			int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
-			int shipScreenX = 0xa0 + displayOffsetX;
-			int shipScreenY = 0x69 + displayOffsetY;
-
-			// Check if this is a sprite strip (multiple directions in one image)
-			// 205 width / 5 directions = 41 pixels per direction
-			int spriteW = shipFrame.width;
-			int spriteH = shipFrame.height;
-			int srcX = 0;
-			int srcY = 0;
-			int numHorizontal = 1;
-			int numVertical = 1;
-
-			// Detect sprite strip layout - look for common patterns
-			if (spriteW >= 200 && spriteW % 5 == 0) {
-				// 5 horizontal directions (like 205 = 41 * 5)
-				numHorizontal = 5;
-				spriteW = shipFrame.width / 5;
-			}
-			if (spriteH >= 350 && spriteH % 7 == 0) {
-				// 7 vertical directions
-				numVertical = 7;
-				spriteH = shipFrame.height / 7;
+	// Uses _shipSprite (POV001) with position-based offset
+
+	if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
+		return;
+
+	// Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
+	int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
+	int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
+
+	// Base screen position (low-res: X=160, Y=105)
+	int shipScreenX = 0xa0 + displayOffsetX;
+	int shipScreenY = 0x69 + displayOffsetY;
+
+	int numSprites = _shipSprite->getNumChars();
+	int spriteIndex = 0;
+
+	// Select sprite based on direction and sprite count
+	if (numSprites >= 35) {
+		spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
+		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+	} else if (numSprites >= 25) {
+		int vDir5 = (_shipDirectionV * 5) / 7;
+		spriteIndex = _shipDirectionH * 5 + vDir5;
+		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+	} else if (numSprites >= 5) {
+		spriteIndex = _shipDirectionH;
+		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+	} else if (numSprites == 2) {
+		spriteIndex = _shipFiring ? 1 : 0;
+	}
+
+	// Center sprite at position
+	int spriteW = _shipSprite->getCharWidth(spriteIndex);
+	int spriteH = _shipSprite->getCharHeight(spriteIndex);
+	int drawX = shipScreenX - spriteW / 2 + _viewX;
+	int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
+
+	// Secondary ship sprite
+	if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
+		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
+	}
+
+	debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
+		drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
+		spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
+}
+
+void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
+	// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
+	// userId=11 contains the ship sprite strip
+
+	if ((_rebelHandler != 7 && _rebelHandler != 8) || _shipLevelMode == 5)
+		return;
+
+	// Skip if we have proper sprites
+	if (_rebelHandler == 7 && _flyShipSprite)
+		return;
+	if (_rebelHandler == 8 && _shipSprite)
+		return;
+
+	EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
+	if (!shipFrame.valid || !shipFrame.pixels || shipFrame.width <= 0 || shipFrame.height <= 0)
+		return;
+
+	// Calculate display offset
+	int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
+	int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
+	int shipScreenX = 0xa0 + displayOffsetX;
+	int shipScreenY = 0x69 + displayOffsetY;
+
+	// Detect sprite strip layout
+	int spriteW = shipFrame.width;
+	int spriteH = shipFrame.height;
+	int srcX = 0, srcY = 0;
+	int numHorizontal = 1, numVertical = 1;
+
+	if (spriteW >= 200 && spriteW % 5 == 0) {
+		numHorizontal = 5;
+		spriteW = shipFrame.width / 5;
+	}
+	if (spriteH >= 350 && spriteH % 7 == 0) {
+		numVertical = 7;
+		spriteH = shipFrame.height / 7;
+	}
+
+	int hDir = MIN((int)_shipDirectionH, numHorizontal - 1);
+	int vDir = MIN((int)_shipDirectionV, numVertical - 1);
+	srcX = hDir * spriteW;
+	srcY = vDir * spriteH;
+
+	int drawX = shipScreenX - spriteW / 2 + _viewX;
+	int drawY = shipScreenY - spriteH / 2 + _viewY;
+
+	// Blit from embedded HUD
+	for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
+		if (drawY + y < 0) continue;
+		for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
+			if (drawX + x < 0) continue;
+			int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
+			byte pixel = shipFrame.pixels[srcIdx];
+			if (pixel != 0 && pixel != 231) {
+				int dstIdx = (drawY + y) * pitch + (drawX + x);
+				renderBitmap[dstIdx] = pixel;
 			}
+		}
+	}
 
-			// Select sprite from strip based on direction
-			int hDir = _shipDirectionH;
-			int vDir = _shipDirectionV;
-			if (hDir >= numHorizontal) hDir = numHorizontal - 1;
-			if (vDir >= numVertical) vDir = numVertical - 1;
-			srcX = hDir * spriteW;
-			srcY = vDir * spriteH;
-
-			// Draw position (centered)
-			int drawX = shipScreenX - spriteW / 2 + _viewX;
-			int drawY = shipScreenY - spriteH / 2 + _viewY;
-
-			// Blit from embedded HUD to render buffer
-			for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
-				if (drawY + y < 0) continue;
-				for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
-					if (drawX + x < 0) continue;
-					int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
-					byte pixel = shipFrame.pixels[srcIdx];
-					// Skip transparent pixels (0 and 231)
-					if (pixel != 0 && pixel != 231) {
-						int dstIdx = (drawY + y) * pitch + (drawX + x);
-						renderBitmap[dstIdx] = pixel;
-					}
+	debug("Rebel2: Ship (fallback) at (%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
+		drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
+}
+
+void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
+	// Draw enemy indicator brackets and erase destroyed enemy areas
+
+	// Erase destroyed enemies' areas (fill with black)
+	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+		if (it->destroyed) {
+			Common::Rect r = it->rect;
+			if (r.left < 0) r.left = 0;
+			if (r.top < 0) r.top = 0;
+			if (r.right > width) r.right = width;
+			if (r.bottom > height) r.bottom = height;
+
+			for (int y = r.top; y < r.bottom; y++) {
+				for (int x = r.left; x < r.right; x++) {
+					renderBitmap[y * pitch + x] = 0;
 				}
 			}
-
-			debug("Rebel2: Ship (embedded HUD) at screen(%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
-				drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
 		}
 	}
 
-	Common::List<enemy>::iterator it;
-	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
-		// Skip destroyed enemies (explosion handled by 5-slot system)
-		if (it->destroyed) continue;
+	// Draw green brackets for active enemies (Easy/Medium difficulty only)
+	if (_difficulty >= 2)
+		return;
+
+	Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
 
-		// Skip inactive enemies or those disabled by IACT bit
-		if (!it->active || isBitSet(it->id)) continue;
+	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+		if (it->destroyed || !it->active || isBitSet(it->id))
+			continue;
 
 		Common::Rect r = it->rect;
+		if (r.right <= viewRect.left || r.left >= viewRect.right ||
+		    r.bottom <= viewRect.top || r.top >= viewRect.bottom)
+			continue;
 
-		// Check if enemy rect is visible within the current view area
-		// (matching FUN_00428b90 clip logic: reject lines entirely outside clip rect)
-		// The visible area in video coordinates is the current scroll viewport
-		Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + videoHeight);
+		const byte color = 5;  // Green
+		drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
+	}
+}
 
-		// If enemy rect doesn't intersect the view area at all, skip drawing
-		if (r.right <= viewRect.left || r.left >= viewRect.right ||
-		    r.bottom <= viewRect.top || r.top >= viewRect.bottom) {
+void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
+	// Draw explosion animations from 5-slot system
+
+	if (!_smush_iconsNut)
+		return;
+
+	for (int i = 0; i < 5; i++) {
+		if (!_explosions[i].active)
 			continue;
-		}
 
-		// Draw Green Indicators (Corner Brackets) for Easy (0) and Medium (1) difficulty
-		// Hard (2) mode does not show indicators (matching FUN_00425d30(4) check)
-		if (_difficulty < 2) {
-			const byte color = 5; // Green color index for brackets
-			drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
+		if (_explosions[i].counter <= 0) {
+			_explosions[i].active = false;
+			continue;
 		}
-	}
 
-	// Draw 5-slot Explosion System
-	if (_smush_iconsNut) {
-		for (int i = 0; i < 5; i++) {
-			if (_explosions[i].active) {
-				if (_explosions[i].counter <= 0) {
-					_explosions[i].active = false;
-					continue;
-				}
+		// Determine base sprite index based on scale (FUN_409FBC logic)
+		int baseIndex;
+		if (_explosions[i].scale < 11) {
+			baseIndex = 9;   // Small/Medium
+		} else if (_explosions[i].scale < 21) {
+			baseIndex = 19;  // Medium/Large
+		} else {
+			baseIndex = 29;  // Large/XL
+		}
 
-				// Determine base sprite index based on scale (FUN_409FBC logic)
-				int baseIndex;
-				if (_explosions[i].scale < 11) {
-					baseIndex = 9;  // Small/Medium transition
-				} else if (_explosions[i].scale < 21) {
-					baseIndex = 19; // Medium/Large transition
-				} else {
-					baseIndex = 29; // Large/XL transition
-				}
-				
-				// Formula: Base + (12 - Counter)
-				// Counter goes 10 -> 1.
-				// Frame goes Base+2 -> Base+11.
-				int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-				
-				if (_smush_iconsNut->getNumChars() > spriteIndex) {
-					int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-					int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-					int cx = _explosions[i].x - ew / 2;
-					int cy = _explosions[i].y - eh / 2;
-					
-					// Draw explosion
-					renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
-				}
+		// Formula: Base + (12 - Counter)
+		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
 
-				_explosions[i].counter--;
-			}
+		if (_smush_iconsNut->getNumChars() > spriteIndex) {
+			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+			int cx = _explosions[i].x - ew / 2;
+			int cy = _explosions[i].y - eh / 2;
+			renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
 		}
+
+		_explosions[i].counter--;
 	}
+}
+
+void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+	// Draw laser shot beams and impacts
+
+	if (!_smush_iconsNut || _smush_iconsNut->getNumChars() <= 0)
+		return;
 
-	// Draw Laser Shots
-	// Gun Positions (Approximate for Turret Mode / Default):
-	// Left: (10, 190), Right: (310, 190) - Adjusted for low-res
-	const int GUN_LEFT_X = 10;
-	const int GUN_LEFT_Y = 190;
-	const int GUN_RIGHT_X = 310;
-	const int GUN_RIGHT_Y = 190;
+	// Gun positions (approximate for turret mode)
+	const int GUN_LEFT_X = 10, GUN_LEFT_Y = 190;
+	const int GUN_RIGHT_X = 310, GUN_RIGHT_Y = 190;
 
 	for (int i = 0; i < 2; i++) {
-		if (_shots[i].active) {
-			if (_shots[i].counter <= 0) {
-				_shots[i].active = false;
-				continue;
-			}
-			
-			// Use Sprite 0 from CPITIMAG.NUT as the laser texture (15x15 projectile)
-			// Confirmed by info.md: indexes 0-4 are laser/projectile
-			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 0) {
-				// Calculate progress
-				int maxProgress = 4; // Max duration from table (supposedly)
-				int progress = maxProgress - _shots[i].counter;
-
-				// Draw Beams depending on Level Type
-				// Scene 1 (LevelType 1) has 3 beams: Right, Middle, Left
-				if (_rebelLevelType <= 1) { // Default or Type 1
-					// Right Beam: Origin(310, 170), Thickness 8, LengthFac 12
-					drawLaserBeam(renderBitmap, pitch, width, height, 
-						310 + _viewX, 170 + _viewY, 
-						_shots[i].x, _shots[i].y, 
-						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-
-					// Middle Beam: Origin(160, 380), Thickness 5, LengthFac 8
-					// Note: 380 is virtual origin below screen
-					drawLaserBeam(renderBitmap, pitch, width, height, 
-						160 + _viewX, 380 + _viewY, 
-						_shots[i].x, _shots[i].y, 
-						progress, maxProgress, 5, 8, _smush_iconsNut, 0);
-
-					// Left Beam: Origin(10, 170), Thickness 8, LengthFac 12
-					drawLaserBeam(renderBitmap, pitch, width, height, 
-						10 + _viewX, 170 + _viewY, 
-						_shots[i].x, _shots[i].y, 
-						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-						
-				} else {
-					// Fallback for other levels (2 beams)
-					drawLaserBeam(renderBitmap, pitch, width, height, 
-						GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, 
-						_shots[i].x, _shots[i].y, 
-						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-
-					drawLaserBeam(renderBitmap, pitch, width, height, 
-						GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, 
-						_shots[i].x, _shots[i].y, 
-						progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-				}
+		if (!_shots[i].active)
+			continue;
 
-				// Draw Projectile Impact
-				// Using Sprite 0 (small flash) or similar at impact point
-				renderNutSprite(renderBitmap, pitch, width, height, _shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
-			}
-			
-			_shots[i].counter--;
+		if (_shots[i].counter <= 0) {
+			_shots[i].active = false;
+			continue;
 		}
-	}
 
-	// ============================================================
-	// COLLISION ZONE VISUALIZATION (for debugging Level 3 pilot mode)
-	// ============================================================
-	// Draw collision zones as wireframe quadrilaterals when in Handler 7 (space flight)
-	// or Handler 8 (ground vehicle) modes. This helps verify parsing is correct.
-	// The zones are drawn BEFORE the crosshair so they appear under the UI.
-	if (_rebelHandler == 7 || _rebelHandler == 8) {
-		drawCollisionZones(renderBitmap, pitch, width, height, 0);
-	}
+		int maxProgress = 4;
+		int progress = maxProgress - _shots[i].counter;
 
-	// ============================================================
-	// TARGET LOCK DETECTION (DAT_00443676 equivalent)
-	// ============================================================
-	// In retail: When crosshair is over an active enemy, set target lock timer to 7
-	// This triggers the animated crosshair cycling for Handler 0x26 (turret)
-	// Check from FUN_40A2E0 lines 127-143
-	{
-		Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
-		bool targetLocked = false;
+		// Draw beams based on level type
+		if (_rebelLevelType <= 1) {
+			// Type 1: 3 beams (Right, Middle, Left)
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				310 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
+				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
 
-		for (Common::List<enemy>::iterator enemyIt = _enemies.begin(); enemyIt != _enemies.end(); ++enemyIt) {
-			if (enemyIt->active && !enemyIt->destroyed && enemyIt->rect.contains(worldMousePos)) {
-				targetLocked = true;
-				break;
-			}
-		}
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				160 + _viewX, 380 + _viewY, _shots[i].x, _shots[i].y,
+				progress, maxProgress, 5, 8, _smush_iconsNut, 0);
 
-		// Update target lock timer
-		if (targetLocked) {
-			_targetLockTimer = 7;  // Set to 7 when over target (retail behavior)
-		} else if (_targetLockTimer > 0) {
-			_targetLockTimer--;  // Count down when not over target
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				10 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
+				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+		} else {
+			// Other levels: 2 beams
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y,
+				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y,
+				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
 		}
+
+		// Impact flash
+		renderNutSprite(renderBitmap, pitch, width, height,
+			_shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
+
+		_shots[i].counter--;
 	}
+}
 
-	// Draw Crosshair/Reticle cursor
-	// Sprite indices based on handler type (from original game disassembly FUN_004089ab, FUN_0040d836, etc):
-	// - Handler 8 (third-person vehicle): Index 0x2E (46)
-	// - Handler 7 (space flight): Index 0x2F (47)
-	// - Handler 0x19 (mixed/turret view): Index 0x2F (47)
-	// - Handler 0x26 (full turret): Index varies by levelType and animation
-	//
-	// For Handler 0x26 (turret), the crosshair sprite formula from FUN_004089ab line 192-193:
-	//   (-(ushort)(DAT_004436de == 5) & 0x30) + local_28
-	// where local_28 is an animation offset (0-3) based on target lock timer (DAT_00443676)
-	//
-	// When DAT_00443676 == 0 (no target locked): local_28 = 0
-	// When DAT_00443676 != 0: local_28 = 3 - (DAT_0047fe98 & 3) -- animated cycling
-	if (_smush_iconsNut) {
-		int reticleIndex;
-		switch (_rebelHandler) {
-		case 7:   // Space flight - uses crosshair sprite 0x2F (47)
-			reticleIndex = 47;
-			break;
-		case 0x19: // Mixed/turret view - uses crosshair sprite 0x2F (47)
-			reticleIndex = 47;
-			break;
-		case 0x26: { // Full turret mode - animated crosshair
-			// Calculate animation offset based on target lock state
-			// In retail: DAT_00443676 is the "target lock timer" (set to 7 when targeting)
-			// DAT_0047fe98 is a per-frame counter incremented each frame
-			static int turretAnimCounter = 0;
-			turretAnimCounter++;
-
-			int animOffset;
-			// Use actual target lock timer (DAT_00443676 equivalent)
-			if (_targetLockTimer == 0) {
-				animOffset = 0;  // No target - use static crosshair
-			} else {
-				animOffset = 3 - (turretAnimCounter & 3);  // Cycles 3, 2, 1, 0
-			}
+void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
+	// Update target lock state and draw crosshair/reticle
 
-			// If levelType == 5, use sprites 0x30-0x33 (48-51)
-			// Otherwise use sprites 0-3 (basic targeting reticle)
-			if (_rebelLevelType == 5) {
-				reticleIndex = 0x30 + animOffset;  // 48-51
-			} else {
-				reticleIndex = animOffset;  // 0-3
-			}
-			break;
-		}
-		case 8:   // Ground vehicle - uses crosshair sprite 0x2E (46)
-		default:
-			reticleIndex = 46;
+	// Target lock detection (DAT_00443676 equivalent)
+	Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
+	bool targetLocked = false;
+
+	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+		if (it->active && !it->destroyed && it->rect.contains(worldMousePos)) {
+			targetLocked = true;
 			break;
 		}
+	}
+
+	if (targetLocked) {
+		_targetLockTimer = 7;
+	} else if (_targetLockTimer > 0) {
+		_targetLockTimer--;
+	}
+
+	// Draw crosshair
+	if (!_smush_iconsNut)
+		return;
+
+	int reticleIndex;
+	switch (_rebelHandler) {
+	case 7:    // Space flight
+	case 0x19: // Mixed/turret
+		reticleIndex = 47;
+		break;
+	case 0x26: { // Full turret - animated crosshair
+		static int turretAnimCounter = 0;
+		turretAnimCounter++;
+
+		int animOffset = (_targetLockTimer == 0) ? 0 : 3 - (turretAnimCounter & 3);
 
-		if (_smush_iconsNut->getNumChars() > reticleIndex) {
-			int cw = _smush_iconsNut->getCharWidth(reticleIndex);
-			int ch = _smush_iconsNut->getCharHeight(reticleIndex);
-			// Center the crosshair on mouse position (in world coordinates)
-			renderNutSprite(renderBitmap, pitch, width, height, _vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY, _smush_iconsNut, reticleIndex);
+		if (_rebelLevelType == 5) {
+			reticleIndex = 0x30 + animOffset;
+		} else {
+			reticleIndex = animOffset;
 		}
+		break;
+	}
+	case 8:    // Ground vehicle
+	default:
+		reticleIndex = 46;
+		break;
 	}
 
-	// ============================================================
-	// HUD SCORE/LIVES RENDERING (FUN_0041c012 equivalent)
-	// ============================================================
-	// Render score and lives counter on the status bar
-	// This is called after all other rendering so it appears on top
-	renderScoreHUD(renderBitmap, pitch, width, height, 0);
+	if (_smush_iconsNut->getNumChars() > reticleIndex) {
+		int cw = _smush_iconsNut->getCharWidth(reticleIndex);
+		int ch = _smush_iconsNut->getCharHeight(reticleIndex);
+		renderNutSprite(renderBitmap, pitch, width, height,
+			_vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY,
+			_smush_iconsNut, reticleIndex);
+	}
+}
 
-	// ============================================================
-	// FRAME END RESET: Clear active flags for all enemies (FUN_403240 equivalent)
-	// ============================================================
-	// The original game rebuilds the object list from scratch each frame:
-	// - FUN_4033CF (IACT processing) adds objects to DAT_0043fb00 list
-	// - FUN_4092D9 iterates only over objects in the current frame's list
-	// - FUN_403240 (called at frame end) resets DAT_0047ee74 = 0 (clears list counter)
-	//
-	// We achieve the same by marking all non-destroyed enemies inactive at frame end.
-	// The next frame's IACT opcode 4 (enemyUpdate) will re-activate them if present.
-	for (Common::List<enemy>::iterator resetIt = _enemies.begin(); resetIt != _enemies.end(); ++resetIt) {
-		if (!resetIt->destroyed) {
-			resetIt->active = false;
+void InsaneRebel2::frameEndCleanup() {
+	// Reset enemy active flags and collision zones at frame end
+	// The original game rebuilds lists from scratch each frame
+
+	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
+		if (!it->destroyed) {
+			it->active = false;
 		}
 	}
 
-	// Reset collision zone counters for next frame (DAT_0047ee74/DAT_0047ee78 = 0)
-	// The zones are rebuilt from IACT opcode 5 chunks each frame
 	resetCollisionZones();
 }
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index b47bd1f3098..a1e7fe81d19 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -264,6 +264,74 @@ public:
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
+	// ======================= Rendering Helper Functions =======================
+	// These are extracted from procPostRendering for better readability
+
+	// Fill status bar background area (FUN_004288c0 equivalent)
+	void renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
+								   int videoWidth, int videoHeight, int statusBarY);
+
+	// Draw NUT-based HUD overlays for Handler 0x26/0x19 turret modes
+	void renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+
+	// Draw embedded SAN HUD overlays from IACT chunks
+	void renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
+	void renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
+								int statusBarY, int32 curFrame);
+
+	// Draw Handler 7 ship sprite (space flight - FLY sprites)
+	void renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw Handler 8 ship sprite (third-person vehicle - POV sprites)
+	void renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw fallback ship using embedded HUD frame
+	void renderFallbackShip(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw enemy indicator brackets and erase destroyed enemy areas
+	void renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth);
+
+	// Draw explosion animations from 5-slot system
+	void renderExplosions(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw laser shot beams and impacts
+	void renderLaserShots(byte *renderBitmap, int pitch, int width, int height);
+
+	// Update target lock state and draw crosshair/reticle
+	void renderCrosshair(byte *renderBitmap, int pitch, int width, int height);
+
+	// Reset enemy active flags and collision zones at frame end
+	void frameEndCleanup();
+
+	// ======================= Opcode 6 Helper Functions =======================
+	// Handler-specific setup extracted from iactRebel2Opcode6
+
+	// Handler 8 (third-person vehicle) setup - FUN_00401234 case 4
+	void opcode6Handler8Setup(int16 par3, int16 par4);
+
+	// Handler 7 (space flight) setup - FUN_0040c3cc case 4
+	void opcode6Handler7Setup(int16 par3, int16 par4);
+
+	// Calculate view offsets based on level type (lines 182-213)
+	void opcode6CalcViewOffsets();
+
+	// ======================= Opcode 8 Helper Functions =======================
+	// Resource loading extracted from iactRebel2Opcode8
+
+	// Load Handler 7 FLY NUT sprites from IACT data
+	bool loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4);
+
+	// Load turret HUD overlay NUT from ANIM data
+	bool loadTurretHudOverlay(byte *animData, int32 size, int16 par3);
+
+	// Load Handler 8 ship POV NUT sprites from ANIM data
+	bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par3);
+
+	// Load Level 2 background from embedded ANIM
+	bool loadLevel2Background(byte *animData, int32 size, byte *renderBitmap);
+
 	// Override procSKIP to disable Full Throttle's conditional frame skip mechanism
 	// RA2 uses a different system for conditional frames via IACT opcodes
 	void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index efb7d20abc3..13a839e80f2 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -961,23 +961,32 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		}
 		_dst = _specialBuffer;
 	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		// For Rebel2, check if this partial update overlaps with a destroyed enemy
-		// If so, skip the update entirely to prevent showing the enemy sprite
+		// Skip frame updates for destroyed enemies. The enemy area is erased
+		// in procPostRendering (eraseDestroyedEnemies) before explosions are drawn.
 		if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
 			return;  // Skip this frame update
 		}
-		// Rebel2 videos use frames larger than the screen (e.g., 424x260 vs 320x200).
-		// Allocate a special buffer to hold the full frame - this is needed because
-		// codecs like 37/47 write the full frame size regardless of screen size.
+		// Rebel2 uses a special buffer for all non-matching frames.
+		// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
+		// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null
+		//          because first frames are small and don't need oversized buffer)
+		// Only allocate/expand buffer for frames LARGER than current buffer or screen
 		int bufSize = width * height;
-		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
-			free(_specialBuffer);
-			_specialBuffer = (byte *)malloc(bufSize);
-			_specialBufferSize = bufSize;
-			_width = width;
-			_height = height;
+		if (bufSize > _vm->_screenWidth * _vm->_screenHeight) {
+			// Frame is larger than screen - need special buffer
+			if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+				free(_specialBuffer);
+				_specialBuffer = (byte *)malloc(bufSize);
+				_specialBufferSize = bufSize;
+				_width = width;
+				_height = height;
+			}
+		}
+		// Use special buffer if allocated (for oversized videos like Level 1)
+		// Otherwise use virtual screen (for Level 2 small sprites)
+		if (_specialBuffer != nullptr && _specialBufferSize >= _vm->_screenWidth * _vm->_screenHeight) {
+			_dst = _specialBuffer;
 		}
-		_dst = _specialBuffer;
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;
 	// FT Insane uses smaller frames to draw overlays with moving objects


Commit: 0642d63e3865e736ccca7461e6207fe7c64b59e3
    https://github.com/scummvm/scummvm/commit/0642d63e3865e736ccca7461e6207fe7c64b59e3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:56+02:00

Commit Message:
SCUMM: RA2: Refactor level state handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cf98a73f119..9977e1444ac 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -2156,6 +2156,241 @@ void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h,
 	_enemies.push_back(e);
 }
 
+// ======================= Embedded Frame Codec Decoders =======================
+// These implement the retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0
+
+void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
+	// Codec 21/44: Line Update codec (FUN_0042BD60)
+	// Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
+	for (int row = 0; row < height; row++) {
+		int lineDataSize = READ_LE_UINT16(src);
+		src += 2;
+		const byte *lineEnd = src + lineDataSize;
+		byte *lineDst = dst + row * width;
+		int x = 0;
+
+		while (src < lineEnd && x < width) {
+			int skip = READ_LE_UINT16(src);
+			src += 2;
+			x += skip;
+			if (src >= lineEnd) break;
+
+			int count = READ_LE_UINT16(src) + 1;
+			src += 2;
+			while (count-- > 0 && x < width && src < lineEnd) {
+				lineDst[x++] = *src++;
+			}
+		}
+		src = lineEnd;
+	}
+}
+
+void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
+	// Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
+	// Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
+	const byte *dataEnd = src + dataSize;
+
+	for (int row = 0; row < height && src < dataEnd; row++) {
+		int lineDataSize = READ_LE_UINT16(src);
+		src += 2;
+		const byte *lineEnd = src + lineDataSize;
+		byte *lineDst = dst + row * width;
+		int x = 0;
+
+		while (src < lineEnd && x < width) {
+			int skip = READ_LE_UINT16(src);
+			src += 2;
+			x += skip;
+			if (src >= lineEnd || x >= width) break;
+
+			int runSize = READ_LE_UINT16(src);
+			src += 2;
+
+			// Decode RLE within this run
+			const byte *runEnd = src + runSize;
+			while (src < runEnd && x < width) {
+				byte code = *src++;
+				int num = (code >> 1) + 1;
+				if (num > width - x) num = width - x;
+
+				if (code & 1) {
+					// RLE run
+					byte color = (src < runEnd) ? *src++ : 0;
+					for (int i = 0; i < num && x < width; i++) {
+						lineDst[x++] = color;
+					}
+				} else {
+					// Literal run
+					for (int i = 0; i < num && x < width && src < runEnd; i++) {
+						lineDst[x++] = *src++;
+					}
+				}
+			}
+			src = runEnd;
+		}
+		src = lineEnd;
+	}
+}
+
+void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
+	// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
+	// May have a 6-byte sub-header starting with "01 FE"
+
+	debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+		src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7],
+		src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
+		src[16], src[17], src[18], src[19]);
+
+	// Probe for header offset
+	int headerSkip = 0;
+	bool foundValidOffset = false;
+
+	// Check for known 6-byte header pattern: 01 FE XX XX XX XX
+	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
+		headerSkip = 6;
+		debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
+		foundValidOffset = true;
+	}
+
+	// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
+	if (!foundValidOffset) {
+		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+			int testLineSize = READ_LE_UINT16(src + testOffset);
+			// A valid first line size should be: > 0, <= width*2
+			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+				// Validate by summing line sizes
+				int sumTest = 0;
+				int linesTest = 0;
+				const byte *testPtr = src + testOffset;
+				bool validSum = true;
+
+				while (linesTest < height && testPtr + 2 <= src + dataSize) {
+					int ls = READ_LE_UINT16(testPtr);
+					if (ls <= 0 || ls > width * 2) {
+						validSum = false;
+						break;
+					}
+					sumTest += ls + 2;
+					testPtr += ls + 2;
+					linesTest++;
+				}
+
+				// Accept if we got close to expected number of lines
+				if (validSum && linesTest >= height - 1) {
+					headerSkip = testOffset;
+					foundValidOffset = true;
+					debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
+					break;
+				}
+			}
+		}
+	}
+
+	if (!foundValidOffset) {
+		debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
+	}
+
+	const byte *srcPtr = src + headerSkip;
+	const byte *dataEnd = src + dataSize;
+
+	// Check if this is per-line RLE or continuous RLE
+	int firstVal = READ_LE_UINT16(srcPtr);
+	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+
+	if (perLineMode) {
+		debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
+		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+			int lineSize = READ_LE_UINT16(srcPtr);
+			srcPtr += 2;
+			if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
+
+			const byte *lineEnd = srcPtr + lineSize;
+			byte *rowDst = dst + row * width;
+			int x = 0;
+
+			while (srcPtr < lineEnd && x < width) {
+				byte ctrl = *srcPtr++;
+				int count = (ctrl >> 1) + 1;
+				if (ctrl & 1) {
+					byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
+					for (int i = 0; i < count && x < width; i++) rowDst[x++] = color;
+				} else {
+					for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
+						rowDst[x++] = *srcPtr++;
+				}
+			}
+			srcPtr = lineEnd;
+		}
+	} else {
+		// Continuous BOMP RLE (no per-line headers)
+		debug("Rebel2: Codec 45 using continuous BOMP RLE");
+		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
+			byte *rowDst = dst + row * width;
+			int x = 0;
+
+			while (x < width && srcPtr < dataEnd) {
+				byte ctrl = *srcPtr++;
+				int count = (ctrl >> 1) + 1;
+
+				if (ctrl & 1) {
+					// RLE fill
+					byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
+					for (int i = 0; i < count && x < width; i++) {
+						rowDst[x++] = color;
+					}
+				} else {
+					// Literal copy
+					for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
+						rowDst[x++] = *srcPtr++;
+					}
+				}
+			}
+		}
+	}
+
+	// Count non-zero pixels for debug
+	int nonZero = 0;
+	for (int i = 0; i < width * height; i++) {
+		if (dst[i] != 0) nonZero++;
+	}
+	debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
+		width, height, nonZero, (nonZero * 100) / (width * height));
+}
+
+void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
+	// Render the decoded embedded frame to the video buffer
+	// Skip immediate draw for handlers that render HUD during post-processing:
+	// - Handler 7/8: Ship direction sprites selected based on direction
+	// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+	bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
+	                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
+
+	if (!frame.valid || !renderBitmap || skipImmediateDraw) {
+		if (skipImmediateDraw && frame.valid) {
+			debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
+				_rebelHandler, userId);
+		}
+		return;
+	}
+
+	int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+	int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+	for (int y = 0; y < frame.height && (frame.renderY + y) < bufHeight; y++) {
+		for (int x = 0; x < frame.width && (frame.renderX + x) < pitch; x++) {
+			byte pixel = frame.pixels[y * frame.width + x];
+			if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
+				int destX = frame.renderX + x;
+				int destY = frame.renderY + y;
+				if (destX >= 0 && destY >= 0) {
+					renderBitmap[destY * pitch + destX] = pixel;
+				}
+			}
+		}
+	}
+	debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
+}
+
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 	// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
 	if (userId < 0 || userId > 15 || !animData || size < 32) {
@@ -2247,216 +2482,30 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 						if (dataSize > 0) {
 							byte *fobjData = (byte *)malloc(dataSize);
 							stream.read(fobjData, dataSize);
-							
-							// Decode based on codec
+
+							// Decode based on codec - use extracted helper functions (FUN_0042BD60, etc.)
 							if (codec == 1 || codec == 3) {
-								// RLE use existing decoder
+								// Codec 1/3: RLE - use existing decoder (FUN_0042C590)
 								smushDecodeRLE(frame.pixels, fobjData, 0, 0, width, height, width);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec %d/RLE): %dx%d", codec, width, height);
 							} else if (codec == 20) {
-								// Uncompressed
+								// Codec 20: Uncompressed (FUN_0042C400)
 								smushDecodeUncompressed(frame.pixels, fobjData, 0, 0, width, height, width);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
 							} else if (codec == 21 || codec == 44) {
-								// Codec 21/44: Line update codec
-								byte *srcPtr = fobjData;
-								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
-									int lineDataSize = READ_LE_UINT16(srcPtr);
-									srcPtr += 2;
-									byte *lineEnd = srcPtr + lineDataSize;
-									byte *lineDst = frame.pixels + row * width;
-									int x = 0;
-									while (srcPtr < lineEnd && x < width) {
-										int skip = READ_LE_UINT16(srcPtr);
-										srcPtr += 2;
-										x += skip;
-										if (srcPtr >= lineEnd) break;
-										int count = READ_LE_UINT16(srcPtr) + 1;
-										srcPtr += 2;
-										while (count-- > 0 && x < width && srcPtr < lineEnd) {
-											lineDst[x++] = *srcPtr++;
-										}
-									}
-									srcPtr = lineEnd;
-								}
+								// Codec 21/44: Line update (FUN_0042BD60)
+								decodeCodec21(frame.pixels, fobjData, width, height);
 								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec 21/line update): %dx%d", width, height);
+								debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
 							} else if (codec == 45) {
-								// Codec 45: RA2-specific codec with BOMP-style RLE
-								// May have a variable-length sub-header before RLE data
-								// Common patterns: "01 FE 00 00 01 00" (6 bytes) or others
-								debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-									fobjData[0], fobjData[1], fobjData[2], fobjData[3],
-									fobjData[4], fobjData[5], fobjData[6], fobjData[7],
-									fobjData[8], fobjData[9], fobjData[10], fobjData[11],
-									fobjData[12], fobjData[13], fobjData[14], fobjData[15],
-									fobjData[16], fobjData[17], fobjData[18], fobjData[19]);
-
-								// Probe multiple header offsets to find where valid RLE data starts
-								// RLE data has 2-byte line sizes that should be reasonable values
-								int headerSkip = 0;
-								bool foundValidOffset = false;
-
-								// Check common header patterns
-								if (dataSize > 6 && fobjData[0] == 0x01 && fobjData[1] == 0xFE) {
-									// Known 6-byte header: 01 FE XX XX XX XX
-									headerSkip = 6;
-									debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
-									foundValidOffset = true;
-								}
-
-								// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
-								if (!foundValidOffset) {
-									for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-										int testLineSize = READ_LE_UINT16(fobjData + testOffset);
-										// A valid first line size should be: > 0, <= width*2 (reasonable for RLE)
-										// Also check that the total data is enough for at least height lines
-										if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
-											// Further validation: sum line sizes should roughly match data size
-											int sumTest = 0;
-											int linesTest = 0;
-											byte *testPtr = fobjData + testOffset;
-											bool validSum = true;
-
-											while (linesTest < height && testPtr + 2 <= fobjData + dataSize) {
-												int ls = READ_LE_UINT16(testPtr);
-												if (ls <= 0 || ls > width * 2) {
-													validSum = false;
-													break;
-												}
-												sumTest += ls + 2;
-												testPtr += ls + 2;
-												linesTest++;
-											}
-
-											// Accept if we got close to expected number of lines
-											if (validSum && linesTest >= height - 1) {
-												headerSkip = testOffset;
-												foundValidOffset = true;
-												debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
-												break;
-											}
-										}
-									}
-								}
-
-								if (!foundValidOffset) {
-									debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
-								}
-
-								byte *srcPtr = fobjData + headerSkip;
-								byte *dataEnd = fobjData + dataSize;
-
-								// Try per-line RLE with 2-byte LE size headers
-								int firstVal = READ_LE_UINT16(srcPtr);
-								bool validPerLine = (firstVal > 0 && firstVal <= width * 2);
-
-								if (validPerLine) {
-									debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
-									for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-										int lineSize = READ_LE_UINT16(srcPtr);
-										srcPtr += 2;
-										if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
-
-										byte *lineEnd = srcPtr + lineSize;
-										byte *dst = frame.pixels + row * width;
-										int x = 0;
-
-										while (srcPtr < lineEnd && x < width) {
-											byte ctrl = *srcPtr++;
-											int count = (ctrl >> 1) + 1;
-											if (ctrl & 1) {
-												byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
-												for (int i = 0; i < count && x < width; i++) dst[x++] = color;
-											} else {
-												for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
-													dst[x++] = *srcPtr++;
-											}
-										}
-										srcPtr = lineEnd;
-									}
-								} else {
-									// Try continuous BOMP RLE (no per-line headers)
-									// Each line produces exactly 'width' pixels
-									debug("Rebel2: Codec 45 using continuous BOMP RLE");
-									for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-										byte *dst = frame.pixels + row * width;
-										int x = 0;
-
-										while (x < width && srcPtr < dataEnd) {
-											byte ctrl = *srcPtr++;
-											int count = (ctrl >> 1) + 1;
-
-											if (ctrl & 1) {
-												// RLE fill
-												byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
-												for (int i = 0; i < count && x < width; i++) {
-													dst[x++] = color;
-												}
-											} else {
-												// Literal copy
-												for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
-													dst[x++] = *srcPtr++;
-												}
-											}
-										}
-									}
-								}
+								// Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
+								decodeCodec45(frame.pixels, fobjData, width, height, dataSize);
 								frame.valid = true;
-
-								// Count non-zero pixels
-								int nonZero = 0;
-								for (int i = 0; i < width * height; i++) {
-									if (frame.pixels[i] != 0) nonZero++;
-								}
-								debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
-									width, height, nonZero, (nonZero * 100) / (width * height));
 							} else if (codec == 23) {
-								// Codec 23: Skip/copy with RLE in copy runs
-								// Format: each line has pairs of (skip, copy_count, RLE_data)
-								byte *srcPtr = fobjData;
-								for (int row = 0; row < height && srcPtr < fobjData + dataSize; row++) {
-									int lineDataSize = READ_LE_UINT16(srcPtr);
-									srcPtr += 2;
-									byte *lineEnd = srcPtr + lineDataSize;
-									byte *lineDst = frame.pixels + row * width;
-									int x = 0;
-
-									while (srcPtr < lineEnd && x < width) {
-										int skip = READ_LE_UINT16(srcPtr);
-										srcPtr += 2;
-										x += skip;
-										if (srcPtr >= lineEnd || x >= width) break;
-
-										int runSize = READ_LE_UINT16(srcPtr);
-										srcPtr += 2;
-
-										// Decode RLE within this run
-										byte *runEnd = srcPtr + runSize;
-										while (srcPtr < runEnd && x < width) {
-											byte code = *srcPtr++;
-											int num = (code >> 1) + 1;
-											if (num > width - x) num = width - x;
-
-											if (code & 1) {
-												// RLE run
-												byte color = (srcPtr < runEnd) ? *srcPtr++ : 0;
-												for (int i = 0; i < num && x < width; i++) {
-													lineDst[x++] = color;
-												}
-											} else {
-												// Literal run
-												for (int i = 0; i < num && x < width && srcPtr < runEnd; i++) {
-													lineDst[x++] = *srcPtr++;
-												}
-											}
-										}
-										srcPtr = runEnd;
-									}
-									srcPtr = lineEnd;
-								}
+								// Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
+								decodeCodec23(frame.pixels, fobjData, width, height, dataSize);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
 							} else {
@@ -2474,35 +2523,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 									userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
 							}
 
-							// Draw immediately to renderBitmap if valid
-							// Skip immediate draw for handlers that render HUD during post-processing:
-							// - Handler 7/8: Ship direction sprites selected based on direction
-							// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
-							bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
-							                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
-
-							if (frame.valid && renderBitmap && !skipImmediateDraw) {
-								int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
-								int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
-								for (int y = 0; y < height && (frame.renderY + y) < bufHeight; y++) {
-									for (int x = 0; x < width && (frame.renderX + x) < pitch; x++) {
-										byte pixel = frame.pixels[y * width + x];
-										if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
-											int destX = frame.renderX + x;
-											int destY = frame.renderY + y;
-											if (destX >= 0 && destY >= 0) {
-												renderBitmap[destY * pitch + destX] = pixel;
-											}
-										}
-									}
-								}
-								debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
-							} else if (skipImmediateDraw) {
-								debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
-									_rebelHandler, userId);
-							}
-							
+							// Render the decoded frame to the video buffer
+							renderEmbeddedFrame(renderBitmap, frame, userId);
+
 							free(fobjData);
 						}
 					}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index a1e7fe81d19..393a6ad7146 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -403,6 +403,26 @@ public:
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
 	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
 
+	// ======================= Embedded Frame Codec Decoders =======================
+	// These functions decode different codec formats used in embedded ANIM/FOBJ data
+	// Based on retail FUN_0042C590 (codec 1), FUN_0042BD60 (codec 21), etc.
+
+	// Decode codec 21/44 (Line Update) - skip/copy pairs per line
+	// Used for fonts and some HUD frames (FUN_0042BD60)
+	void decodeCodec21(byte *dst, const byte *src, int width, int height);
+
+	// Decode codec 23 (Skip/Copy with embedded RLE) - hybrid format
+	// Used for embedded HUD frames with transparency (FUN_0042BBF0)
+	void decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize);
+
+	// Decode codec 45 (RA2-specific BOMP RLE) - variable header format
+	// Used for small animation elements and HUD pieces (FUN_0042B5F0)
+	void decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize);
+
+	// Render a decoded embedded frame to the video buffer
+	// Handles transparency (color 0 and 231) and boundary checks
+	void renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId);
+
 
 
 	int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)


Commit: 6da03acf19d77b61ba22c7617e7c00ece8503ab6
    https://github.com/scummvm/scummvm/commit/6da03acf19d77b61ba22c7617e7c00ece8503ab6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:56+02:00

Commit Message:
SCUMM: RA2: Refactor gameplay handlers

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 9977e1444ac..86ab08d07a9 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1463,423 +1463,456 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
 	// Opcode 8: HUD/Ship resource loading
-	// Based on FUN_41CADB case 6 (switch on *local_14 - 2 == 6, meaning opcode 8)
+	// Dispatches to handler-specific loading functions based on current handler and parameters.
 	//
-	// For Handler 0x26 (turret) - par3 determines HUD slot:
-	// case 1: DAT_00482240 - Primary HUD overlay (GRD001)
-	// case 2: DAT_00482238 - Secondary HUD graphics (GRD002)
-	// case 4: DAT_00482268 - Ship cockpit frame (GRD010)
-	// case 5: DAT_0048226c - Mask buffer (GRD011)
-	// case 6: DAT_00482250 - Explosion overlay (GRD003)
-	// case 7: DAT_00482248 - Damage indicator (GRD004)
-	// case 10: DAT_00482258 - Additional effects (GRD005)
-	// case 12/13: DAT_00482260 - High-res HUD alternative (GRD007)
+	// Handler-specific routing (based on retail disassembly):
+	//   Handler 7 (FUN_0040c3cc):  FLY NUT sprites via par4 (1, 2, 3, 11)
+	//   Handler 8 (FUN_00401234):  POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
+	//   Handler 0x26 (FUN_00407fcb): Turret HUD NUT via par3 (1-4)
+	//   Handler 0x19: Mixed turret mode, similar to 0x26
 	//
-	// For Handler 8 (third-person vehicle) - par3 determines ship sprite slot (FUN_00401234):
-	// case 1: DAT_0047e010 - Primary ship sprite (POV001)
-	// case 3: DAT_0047e028 - Secondary ship sprite (POV004)
-	// case 6: DAT_0047e020 - Ship overlay 1 (POV002)
-	// case 7: DAT_0047e018 - Ship overlay 2 (POV003)
-	//
-	// cases 21-47: Sound loading (both handlers)
+	// Sound loading: par3 in range 21-47
 
-	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)", _rebelHandler, par2, par3, par4, _gameState);
+	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
+		_rebelHandler, par2, par3, par4, _gameState);
 
 	int64 startPos = b.pos();
 	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
 
-	// Handler 7 FLY NUT loading - fixed offset format (FUN_0040c3cc case 6)
+	// ===== Handler 7: FLY NUT Loading (Space Flight) =====
+	// FUN_0040c3cc case 6: par4 determines FLY sprite slot
+	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
+	if (isHandler7FLY && remaining >= 14) {
+		if (loadHandler7FlySprites(b, remaining, par4)) {
+			b.seek(startPos);
+			return;
+		}
+		b.seek(startPos);
+	}
+
+	// ===== Sound Loading (par3 21-47) =====
+	if (par3 >= 21 && par3 <= 47) {
+		debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
+		// TODO: Implement sound loading via FUN_004118df equivalent
+		return;
+	}
+
+	// ===== Scan for embedded ANIM data =====
+	// Remaining handlers require finding ANIM tag in the stream
+	debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
+		(long long)startPos, (long long)remaining);
+
+	if (remaining <= 0) {
+		return;
+	}
+
+	int scanSize = (int)MIN<int64>(remaining, 65536);
+	byte *scanBuf = (byte *)malloc(scanSize);
+	if (!scanBuf) {
+		return;
+	}
+
+	int bytesRead = b.read(scanBuf, scanSize);
+	debug("Rebel2 Opcode 8: Read %d bytes for ANIM scan", bytesRead);
+
+	// Find ANIM tag
+	int animOffset = -1;
+	for (int i = 0; i + 8 <= bytesRead; ++i) {
+		if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+			animOffset = i;
+			debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
+			break;
+		}
+	}
+
+	if (animOffset < 0) {
+		debug("Rebel2 Opcode 8: No ANIM tag found");
+		free(scanBuf);
+		b.seek(startPos);
+		return;
+	}
+
+	// Extract ANIM data
+	uint32 animReportedSize = READ_BE_UINT32(scanBuf + animOffset + 4);
+	int32 animDataSize = (int)MIN<int64>((int64)animReportedSize + 8, remaining - animOffset);
+	if (animDataSize <= 0) {
+		free(scanBuf);
+		b.seek(startPos);
+		return;
+	}
+
+	byte *animData = (byte *)malloc(animDataSize);
+	if (!animData) {
+		free(scanBuf);
+		b.seek(startPos);
+		return;
+	}
+
+	b.seek(startPos + animOffset);
+	b.read(animData, animDataSize);
+
+	bool handled = false;
+
+	// ===== Handler 0x26/0x19: Turret HUD Overlays =====
+	// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
+	if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
+		if (par3 >= 1 && par3 <= 4) {
+			handled = loadTurretHudOverlay(animData, animDataSize, par3);
+		}
+	}
+
+	// ===== Handler 8: POV Ship Sprites or Background =====
+	// FUN_00401234 case 6: par3 for POV NUTs, par4=5 for background
+	if (!handled && _rebelHandler == 8) {
+		// Check for background loading first (par4=5)
+		if (par4 == 5) {
+			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+		}
+		// Check for POV NUT sprites
+		else if (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7) {
+			handled = loadHandler8ShipSprites(animData, animDataSize, par3);
+		}
+	}
+
+	// ===== Handler 25 (0x19): Also supports Level 2 background =====
+	if (!handled && _rebelHandler == 25 && par4 == 5) {
+		handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+	}
+
+	// ===== Fallback: Embedded SAN HUD overlays =====
+	// For other cases, load as embedded SAN frame to HUD overlay slots
+	if (!handled) {
+		// Skip high-res data (par3 == 2, 4)
+		if (par3 == 2 || par3 == 4) {
+			debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
+			handled = true;
+		} else {
+			// Determine userId: Handler 0x19 uses par3, others use par4
+			// Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3
+			int userId;
+			bool usePar3 = (_rebelHandler == 0x19);
+			if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
+				usePar3 = true;
+			}
+			userId = usePar3 ? par3 : par4;
+
+			// Skip audio tracks (userId >= 1000)
+			if (userId > 0 && userId < 1000) {
+				debug("Rebel2 Opcode 8: Loading embedded SAN HUD userId=%d (handler=%d par3=%d par4=%d)",
+					userId, _rebelHandler, par3, par4);
+				loadEmbeddedSan(userId, animData, animDataSize, renderBitmap);
+				handled = true;
+			}
+		}
+	}
+
+	if (!handled) {
+		debug("Rebel2 Opcode 8: Unhandled case - handler=%d par3=%d par4=%d", _rebelHandler, par3, par4);
+	}
+
+	free(animData);
+	free(scanBuf);
+	b.seek(startPos);
+}
+
+// ======================= Opcode 8 Helper Functions =======================
+// These helper functions are extracted from the original monolithic iactRebel2Opcode8
+// to improve code readability and match the retail FUN_* function structure.
+
+bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4) {
+	// Handler 7 FLY NUT loading - FUN_0040c3cc case 6 (opcode 8)
 	// IACT structure after par1-par4 (we're at offset +8):
 	//   +0-5 (6 bytes): additional header
 	//   +6-9 (4 bytes): NUT data size (little-endian)
 	//   +10+: NUT data
-	// Assembly: param_5[7] = size at offset 14, param_5+9 = data at offset 18
-	// Since we've read 8 bytes (par1-par4), that's offset +6 and +10 from current pos
 	//
-	// IMPORTANT: The assembly switches on param_5[3] which is the 4th short (bytes 6-7)
-	// In SMUSH handleIACT, this corresponds to userId (par4), NOT unknown (par3)!
-	// So we use par4 for the FLY slot selection.
-	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
+	// par4 values (param_5[3] - 1 in assembly):
+	//   1 -> case 0: FLY001 - Ship direction sprites (DAT_0047fee8)
+	//   2 -> case 1: FLY003 - Targeting overlay (DAT_0047fef8)
+	//   3 -> case 2: FLY002 - Laser fire sprites (DAT_0047fef0)
+	//  11 -> case 10: FLY004 - High-res alternative (DAT_0047ff00)
 
-	if (isHandler7FLY && remaining >= 14) {
-		// Read additional header and size from fixed offset
-		// Based on FUN_0040c3cc assembly:
-		//   param_5[7] = size at offset 14 (we're at offset 8, so read 6+4 bytes)
-		//   param_5+9 = data at offset 18
-		byte header[10];
-		if (b.read(header, 10) == 10) {
-			// Debug: dump all header bytes
-			debug("Rebel2 Opcode 8 Handler7: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-				header[0], header[1], header[2], header[3], header[4],
-				header[5], header[6], header[7], header[8], header[9]);
-
-			// Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
-			// Try both endianness
-			uint32 nutSizeLE = READ_LE_UINT32(header + 6);
-			uint32 nutSizeBE = READ_BE_UINT32(header + 6);
-			debug("Rebel2 Opcode 8 Handler7: par4=%d sizesLE=%u sizeBE=%u remaining=%lld",
-				par4, nutSizeLE, nutSizeBE, (long long)remaining);
-
-			// The assembly uses direct memory read on x86 which is LE
-			uint32 nutSize = nutSizeLE;
-
-			if (nutSize > 0 && nutSize <= (uint32)(remaining - 10)) {
-				byte *nutData = (byte *)malloc(nutSize);
-				if (nutData) {
-					int bytesRead = b.read(nutData, nutSize);
-					debug("Rebel2 Opcode 8 Handler7: Read %d/%u bytes of NUT data, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-						bytesRead, nutSize,
-						bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
-						bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
-						bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
-						bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
-						bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
-						bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
-						bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
-						bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
-
-					// Verify we read the expected amount
-					if (bytesRead != (int)nutSize) {
-						warning("Rebel2 Opcode 8 Handler7: Short read! Got %d expected %u", bytesRead, nutSize);
-					}
+	if (remaining < 14) {
+		return false;
+	}
 
-					// Verify ANIM header
-					if (bytesRead >= 8) {
-						uint32 animTag = READ_BE_UINT32(nutData);
-						uint32 animSize = READ_BE_UINT32(nutData + 4);
-						debug("Rebel2 Opcode 8 Handler7: ANIM tag=%08X size=%u (expected %08X, size should be ~%d)",
-							animTag, animSize, MKTAG('A','N','I','M'), bytesRead - 8);
-						if (animTag != MKTAG('A','N','I','M')) {
-							warning("Rebel2 Opcode 8 Handler7: No ANIM tag! Data may be corrupted");
-						}
-						if ((int32)animSize > bytesRead - 8) {
-							warning("Rebel2 Opcode 8 Handler7: ANIM size %u exceeds data %d", animSize, bytesRead - 8);
-						}
-					}
+	// Read additional header and size from fixed offset
+	byte header[10];
+	if (b.read(header, 10) != 10) {
+		return false;
+	}
 
-					// Try loading as NUT
-					NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
-					if (newNut && newNut->getNumChars() > 0) {
-						debug("Rebel2 Opcode 8 Handler7: Loaded FLY NUT par4=%d with %d sprites",
-							par4, newNut->getNumChars());
-
-						// Switch on par4 (userId) - matches assembly param_5[3]
-						switch (par4) {
-						case 1:  // FLY001 - Ship direction sprites (35 frames)
-							delete _flyShipSprite;
-							_flyShipSprite = newNut;
-							debug("Rebel2 Opcode 8: _flyShipSprite set with %d sprites", newNut->getNumChars());
-							break;
-						case 2:  // FLY003 - Targeting overlay
-							delete _flyTargetSprite;
-							_flyTargetSprite = newNut;
-							break;
-						case 3:  // FLY002 - Laser fire sprites
-							delete _flyLaserSprite;
-							_flyLaserSprite = newNut;
-							break;
-						case 11: // FLY004 - High-res alternative
-							delete _flyHiResSprite;
-							_flyHiResSprite = newNut;
-							break;
-						default:
-							delete newNut;
-							break;
-						}
-					} else {
-						debug("Rebel2 Opcode 8 Handler7: NUT load failed for par4=%d", par4);
-						delete newNut;
-					}
-					free(nutData);
-				}
-			}
+	debug("Rebel2 loadHandler7FlySprites: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+		header[0], header[1], header[2], header[3], header[4],
+		header[5], header[6], header[7], header[8], header[9]);
+
+	// Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
+	uint32 nutSize = READ_LE_UINT32(header + 6);
+	debug("Rebel2 loadHandler7FlySprites: par4=%d nutSize=%u remaining=%lld",
+		par4, nutSize, (long long)remaining);
+
+	if (nutSize == 0 || nutSize > (uint32)(remaining - 10)) {
+		return false;
+	}
+
+	byte *nutData = (byte *)malloc(nutSize);
+	if (!nutData) {
+		return false;
+	}
+
+	int bytesRead = b.read(nutData, nutSize);
+	debug("Rebel2 loadHandler7FlySprites: Read %d/%u bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
+		bytesRead, nutSize,
+		bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
+		bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
+		bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
+		bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
+		bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
+		bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
+		bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
+		bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
+
+	if (bytesRead != (int)nutSize) {
+		warning("Rebel2 loadHandler7FlySprites: Short read! Got %d expected %u", bytesRead, nutSize);
+		free(nutData);
+		return false;
+	}
+
+	// Verify ANIM header
+	if (bytesRead >= 8) {
+		uint32 animTag = READ_BE_UINT32(nutData);
+		if (animTag != MKTAG('A','N','I','M')) {
+			warning("Rebel2 loadHandler7FlySprites: No ANIM tag! Data may be corrupted");
+			free(nutData);
+			return false;
 		}
-		b.seek(startPos);
-		return;
 	}
 
-	// For non-Handler7 or non-FLY cases, scan for ANIM tag
-	debug("Rebel2 Opcode 8: startPos=%lld chunkSize=%d remaining=%lld", (long long)startPos, chunkSize, (long long)remaining);
-	if (remaining > 0) {
-		int scanSize = (int)MIN<int64>(remaining, 65536);
-		byte *scanBuf = (byte *)malloc(scanSize);
-		if (scanBuf) {
-			int bytesRead = b.read(scanBuf, scanSize);
-			debug("Rebel2 Opcode 8: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-				bytesRead,
-				bytesRead > 0 ? scanBuf[0] : 0, bytesRead > 1 ? scanBuf[1] : 0,
-				bytesRead > 2 ? scanBuf[2] : 0, bytesRead > 3 ? scanBuf[3] : 0,
-				bytesRead > 4 ? scanBuf[4] : 0, bytesRead > 5 ? scanBuf[5] : 0,
-				bytesRead > 6 ? scanBuf[6] : 0, bytesRead > 7 ? scanBuf[7] : 0,
-				bytesRead > 8 ? scanBuf[8] : 0, bytesRead > 9 ? scanBuf[9] : 0,
-				bytesRead > 10 ? scanBuf[10] : 0, bytesRead > 11 ? scanBuf[11] : 0,
-				bytesRead > 12 ? scanBuf[12] : 0, bytesRead > 13 ? scanBuf[13] : 0,
-				bytesRead > 14 ? scanBuf[14] : 0, bytesRead > 15 ? scanBuf[15] : 0);
-
-			// Look for ANIM tag (embedded SAN or NUT)
-			for (int i = 0; i + 8 <= bytesRead; ++i) {
-				if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
-					debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
-					int64 animStreamPos = startPos + i;
-					uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-					// Limit toCopy to remaining data in this chunk
-					int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, remaining - i);
-					if (toCopy > 0) {
-						byte *animData = (byte *)malloc(toCopy);
-						if (animData) {
-							b.seek(animStreamPos);
-							b.read(animData, toCopy);
-
-							// Handler-dependent NUT loading for opcode 8:
-							//
-							// Handler 0x26/0x19 (turret) - FUN_00407fcb:
-							//   par3 == 1: Low-res primary HUD (GRD001) -> DAT_0047fe78
-							//   par3 == 2: High-res primary HUD (skip in low-res mode)
-							//   par3 == 3: Low-res secondary HUD (GRD010) -> DAT_0047fe80
-							//   par3 == 4: High-res secondary HUD (skip in low-res mode)
-							//
-							// Handler 8 (vehicle) - FUN_00401234:
-							//   par3 == 1: POV001 - Primary ship sprite -> DAT_0047e010
-							//   par3 == 3: POV004 - Secondary ship sprite -> DAT_0047e028
-							//   par3 == 6: POV002 - Ship overlay 1 -> DAT_0047e020
-							//   par3 == 7: POV003 - Ship overlay 2 -> DAT_0047e018
-							//
-							// ScummVM runs at 320x200 (low-res), so turret HUD uses par3 == 1/3 only.
-							bool loadedAsNut = false;
-
-							// Handler 0x26/0x19 (turret modes): Load HUD overlays
-							if (_rebelHandler == 0x26 || _rebelHandler == 0x19) {
-								if (par3 == 1) {
-									// Low-res primary HUD overlay
-									NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
-									if (newNut && newNut->getNumChars() > 0) {
-										debug("Rebel2 Opcode 8: Loaded turret HUD NUT par3=%d with %d sprites (handler=0x%x, low-res)",
-											par3, newNut->getNumChars(), _rebelHandler);
-										delete _hudOverlayNut;
-										_hudOverlayNut = newNut;
-										loadedAsNut = true;
-									} else {
-										debug("Rebel2 Opcode 8: Turret HUD NUT load failed for par3=%d", par3);
-										delete newNut;
-									}
-								} else if (par3 == 3) {
-									// Low-res secondary HUD overlay
-									NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
-									if (newNut && newNut->getNumChars() > 0) {
-										debug("Rebel2 Opcode 8: Loaded turret HUD2 NUT par3=%d with %d sprites (handler=0x%x, low-res)",
-											par3, newNut->getNumChars(), _rebelHandler);
-										delete _hudOverlay2Nut;
-										_hudOverlay2Nut = newNut;
-										loadedAsNut = true;
-									} else {
-										debug("Rebel2 Opcode 8: Turret HUD2 NUT load failed for par3=%d", par3);
-										delete newNut;
-									}
-								} else if (par3 == 2 || par3 == 4) {
-									// High-res versions - skip in low-res mode
-									debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
-									loadedAsNut = true;
-								}
-							}
+	// Load as NUT
+	NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
+	if (!newNut || newNut->getNumChars() <= 0) {
+		debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
+		delete newNut;
+		free(nutData);
+		return false;
+	}
 
-							// Handler 8 (Third-Person Vehicle): Load ship sprites
-							if (!loadedAsNut && _rebelHandler == 8) {
-								bool isHandler8Par3 = (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7);
-								if (isHandler8Par3) {
-									NutRenderer *newNut = new NutRenderer(_vm, animData, toCopy);
-									if (newNut && newNut->getNumChars() > 0) {
-										debug("Rebel2 Opcode 8: Loaded ship NUT par3=%d with %d sprites (handler=%d)",
-											par3, newNut->getNumChars(), _rebelHandler);
-										loadedAsNut = true;
-
-										switch (par3) {
-										case 1:  // POV001 - Primary ship sprite
-											delete _shipSprite;
-											_shipSprite = newNut;
-											break;
-										case 3:  // POV004 - Secondary ship sprite
-											delete _shipSprite2;
-											_shipSprite2 = newNut;
-											break;
-										case 6:  // POV002 - Ship overlay 1
-											delete _shipOverlay1;
-											_shipOverlay1 = newNut;
-											break;
-										case 7:  // POV003 - Ship overlay 2
-											delete _shipOverlay2;
-											_shipOverlay2 = newNut;
-											break;
-										default:
-											delete newNut;
-											loadedAsNut = false;
-											break;
-										}
-									} else {
-										debug("Rebel2 Opcode 8: Ship NUT load failed for par3=%d", par3);
-										delete newNut;
-									}
-								}
-							}
+	debug("Rebel2 loadHandler7FlySprites: Loaded FLY NUT par4=%d with %d sprites",
+		par4, newNut->getNumChars());
 
-							// Handler 8/25 (Level 2): Load background
-							// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
-							// FUN_00401234 case 6 switch case 5: Creates 320x200 buffer (DAT_0047e030)
-							// Handler 8 = third-person vehicle, Handler 25 (0x19) = turret/mixed view
-							// Level 2 uses handler 25 but background loading is the same mechanism
-							if (!loadedAsNut && (_rebelHandler == 8 || _rebelHandler == 25) && par4 == 5) {
-								debug("Rebel2 Opcode 8: Loading Level 2 background (par4=5, animSize=%d)", toCopy);
-
-								// Allocate background buffer if needed (320x200 = 64000 bytes)
-								if (_level2Background == nullptr) {
-									_level2Background = (byte *)malloc(320 * 200);
-									if (_level2Background) {
-										memset(_level2Background, 0, 320 * 200);
-									}
-								}
+	// Assign to appropriate slot based on par4 (matches FUN_0040c3cc case 6 switch)
+	bool assigned = true;
+	switch (par4) {
+	case 1:  // FLY001 - Ship direction sprites (35 frames)
+		delete _flyShipSprite;
+		_flyShipSprite = newNut;
+		debug("Rebel2: _flyShipSprite set with %d sprites", newNut->getNumChars());
+		break;
+	case 2:  // FLY003 - Targeting overlay
+		delete _flyTargetSprite;
+		_flyTargetSprite = newNut;
+		break;
+	case 3:  // FLY002 - Laser fire sprites
+		delete _flyLaserSprite;
+		_flyLaserSprite = newNut;
+		break;
+	case 11: // FLY004 - High-res alternative
+		delete _flyHiResSprite;
+		_flyHiResSprite = newNut;
+		break;
+	default:
+		delete newNut;
+		assigned = false;
+		break;
+	}
 
-								if (_level2Background) {
-									// Parse embedded ANIM to find FOBJ
-									// Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
-									int animOffset = 0;
-									if (toCopy >= 8 && READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
-										uint32 animSize = READ_BE_UINT32(animData + 4);
-										debug("Rebel2 Opcode 8: Found ANIM tag, size=%u", animSize);
-
-										// Skip ANIM header (8 bytes) + AHDR chunk
-										// AHDR starts at offset 8, size is at offset 12 (big endian)
-										if (toCopy >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
-											uint32 ahdrSize = READ_BE_UINT32(animData + 12);
-											animOffset = 8 + 8 + ahdrSize;  // After ANIM tag + AHDR
-											debug("Rebel2 Opcode 8: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
-										}
-									}
-
-									// Look for FRME containing FOBJ
-									bool foundBackground = false;
-									for (int scanPos = animOffset; scanPos + 16 < toCopy && !foundBackground; scanPos++) {
-										if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
-											// Found FRME, look for FOBJ inside
-											int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
-											debug("Rebel2 Opcode 8: Found FRME at %d, size=%d", scanPos, frmeSize);
-
-											for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < toCopy; fobjPos++) {
-												if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
-													uint32 fobjSize = READ_BE_UINT32(animData + fobjPos + 4);
-													byte *fobjData = animData + fobjPos + 8;
-
-													// FOBJ header: codec(2), x(2), y(2), w(2), h(2)
-													int16 codec = READ_LE_INT16(fobjData);
-													int16 fobjX = READ_LE_INT16(fobjData + 2);
-													int16 fobjY = READ_LE_INT16(fobjData + 4);
-													int16 fobjW = READ_LE_INT16(fobjData + 6);
-													int16 fobjH = READ_LE_INT16(fobjData + 8);
-
-													debug("Rebel2 Opcode 8: Found FOBJ at %d: codec=%d pos=(%d,%d) size=%dx%d dataSize=%u",
-														fobjPos, codec, fobjX, fobjY, fobjW, fobjH, fobjSize);
-
-													// Decode codec 3 (RLE) into background buffer
-													// Codec 3 uses bomp RLE format with line-by-line structure
-													// FOBJ header is 14 bytes: codec(2), x(2), y(2), w(2), h(2), unk(2), unk(2)
-													if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
-														byte *rleData = fobjData + 14;  // Skip full 14-byte FOBJ header
-
-														// Use the existing smushDecodeRLE function from codec1.cpp
-														// This properly decodes BOMP RLE with line size headers
-														smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
-
-														debug("Rebel2 Opcode 8: Decoded Level 2 background (%dx%d at %d,%d)",
-															fobjW, fobjH, fobjX, fobjY);
-														_level2BackgroundLoaded = true;
-														foundBackground = true;
-
-														// Draw background to render bitmap immediately
-														if (renderBitmap) {
-															for (int by = 0; by < 200; by++) {
-																memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
-															}
-															debug("Rebel2 Opcode 8: Copied Level 2 background to renderBitmap");
-														}
-													}
-													break;  // Found FOBJ, stop searching
-												}
-											}
-											break;  // Found FRME, stop searching
-										}
-									}
-
-									if (!foundBackground) {
-										debug("Rebel2 Opcode 8: Failed to find/decode background FOBJ");
-									}
-								}
-								loadedAsNut = true;  // Mark as handled even if decode failed
-							}
+	free(nutData);
+	return assigned;
+}
 
-							if (!loadedAsNut) {
-								// Skip high-res turret HUD data (par3 == 2, 4) regardless of handler
-								// ScummVM runs at 320x200, so we only want low-res data (par3 == 1, 3)
-								debug("Rebel2 Opcode 8: Fallback path - handler=%d par3=%d par4=%d animSize=%d",
-									_rebelHandler, par3, par4, toCopy);
-								if (par3 == 2 || par3 == 4) {
-									debug("Rebel2 Opcode 8: SKIPPING high-res HUD par3=%d (low-res mode)", par3);
-									// Don't load anything - skip this data entirely
-								} else {
-									// Load as embedded SAN (HUD overlays)
-									// The userId parameter varies by handler type:
-									//
-									// Handler 0x26 (turret): Uses par3 for HUD slot (GRD files)
-									//   par3=1: GRD001 (Primary HUD overlay)
-									//   par3=3: GRD010 (Secondary HUD)
-									//
-									// Handler 7 (space flight): Uses par4 for userId (FLY files handled above)
-									// Other handlers: Use par4 for userId
-									//
-									// par4 values seen in Level 3:
-									//   1000 = audio track
-									//   1-11 = HUD overlay slots
-									//
-									// Note: Handler may not be set yet if opcode 8 arrives before opcode 6
-									// Use heuristics: if par3 is in valid GRD range (1-13) and par4 is >= 1000
-									// or invalid, prefer par3
-									int userId;
-									// Handler 0x19 uses par3; Handler 0x26 and others use par4
-									bool usePar3 = (_rebelHandler == 0x19);
-
-									// Heuristic: if par3 is in typical GRD slot range (1-13) and par4 is
-									// out of range (0 or >= 1000), use par3 as it's likely a turret/GRD case
-									if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
-										usePar3 = true;
-										debug("Rebel2 Opcode 8: Using par3 heuristic (par3=%d, par4=%d)", par3, par4);
-									}
-
-									userId = usePar3 ? par3 : par4;
-
-									// Audio tracks (userId >= 1000) are handled separately, skip them
-									if (userId > 0 && userId < 1000) {
-										debug("Rebel2 Opcode 8: Loading embedded SAN as HUD userId=%d (handler=%d, par3=%d, par4=%d)",
-											userId, _rebelHandler, par3, par4);
-										loadEmbeddedSan(userId, animData, toCopy, renderBitmap);
-									}
-								}
+bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
+	// Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
+	// Resolution-dependent loading:
+	//   par3 == 1: Low-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
+	//   par3 == 2: High-res primary HUD (skip in 320x200 mode)
+	//   par3 == 3: Low-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
+	//   par3 == 4: High-res secondary HUD (skip in 320x200 mode)
+
+	if (!animData || size <= 0) {
+		return false;
+	}
+
+	// ScummVM runs at 320x200 (low-res), skip high-res data
+	if (par3 == 2 || par3 == 4) {
+		debug("Rebel2 loadTurretHudOverlay: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
+		return true;  // Successfully "handled" by skipping
+	}
+
+	if (par3 != 1 && par3 != 3) {
+		return false;  // Not a turret HUD slot
+	}
+
+	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+	if (!newNut || newNut->getNumChars() <= 0) {
+		debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
+		delete newNut;
+		return false;
+	}
+
+	debug("Rebel2 loadTurretHudOverlay: Loaded turret HUD NUT par3=%d with %d sprites",
+		par3, newNut->getNumChars());
+
+	if (par3 == 1) {
+		// Low-res primary HUD overlay
+		delete _hudOverlayNut;
+		_hudOverlayNut = newNut;
+	} else {  // par3 == 3
+		// Low-res secondary HUD overlay
+		delete _hudOverlay2Nut;
+		_hudOverlay2Nut = newNut;
+	}
+
+	return true;
+}
+
+bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par3) {
+	// Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
+	// par3 values:
+	//   1: POV001 - Primary ship sprite (DAT_0047e010 / _shipSprite)
+	//   3: POV004 - Secondary ship sprite (DAT_0047e028 / _shipSprite2)
+	//   6: POV002 - Ship overlay 1 (DAT_0047e020 / _shipOverlay1)
+	//   7: POV003 - Ship overlay 2 (DAT_0047e018 / _shipOverlay2)
+
+	if (!animData || size <= 0) {
+		return false;
+	}
+
+	// Only handle valid POV sprite slots
+	if (par3 != 1 && par3 != 3 && par3 != 6 && par3 != 7) {
+		return false;
+	}
+
+	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+	if (!newNut || newNut->getNumChars() <= 0) {
+		debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par3=%d", par3);
+		delete newNut;
+		return false;
+	}
+
+	debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par3=%d with %d sprites",
+		par3, newNut->getNumChars());
+
+	switch (par3) {
+	case 1:  // POV001 - Primary ship sprite
+		delete _shipSprite;
+		_shipSprite = newNut;
+		break;
+	case 3:  // POV004 - Secondary ship sprite
+		delete _shipSprite2;
+		_shipSprite2 = newNut;
+		break;
+	case 6:  // POV002 - Ship overlay 1
+		delete _shipOverlay1;
+		_shipOverlay1 = newNut;
+		break;
+	case 7:  // POV003 - Ship overlay 2
+		delete _shipOverlay2;
+		_shipOverlay2 = newNut;
+		break;
+	default:
+		delete newNut;
+		return false;
+	}
+
+	return true;
+}
+
+bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
+	// Level 2 background loading from embedded ANIM - FUN_00401234 case 5
+	// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
+	// Creates 320x200 buffer (DAT_0047e030 / _level2Background)
+
+	if (!animData || size < 8) {
+		return false;
+	}
+
+	debug("Rebel2 loadLevel2Background: Loading Level 2 background (animSize=%d)", size);
+
+	// Allocate background buffer if needed (320x200 = 64000 bytes)
+	if (_level2Background == nullptr) {
+		_level2Background = (byte *)malloc(320 * 200);
+		if (!_level2Background) {
+			return false;
+		}
+		memset(_level2Background, 0, 320 * 200);
+	}
+
+	// Parse embedded ANIM to find FOBJ
+	// Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
+	int animOffset = 0;
+	if (READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
+		uint32 animSize = READ_BE_UINT32(animData + 4);
+		debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
+
+		// Skip ANIM header (8 bytes) + AHDR chunk
+		if (size >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
+			uint32 ahdrSize = READ_BE_UINT32(animData + 12);
+			animOffset = 8 + 8 + ahdrSize;  // After ANIM tag + AHDR
+			debug("Rebel2 loadLevel2Background: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
+		}
+	}
+
+	// Look for FRME containing FOBJ
+	bool foundBackground = false;
+	for (int scanPos = animOffset; scanPos + 16 < size && !foundBackground; scanPos++) {
+		if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
+			int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
+			debug("Rebel2 loadLevel2Background: Found FRME at %d, size=%d", scanPos, frmeSize);
+
+			for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < size; fobjPos++) {
+				if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
+					byte *fobjData = animData + fobjPos + 8;
+
+					// FOBJ header: codec(2), x(2), y(2), w(2), h(2)
+					int16 codec = READ_LE_INT16(fobjData);
+					int16 fobjX = READ_LE_INT16(fobjData + 2);
+					int16 fobjY = READ_LE_INT16(fobjData + 4);
+					int16 fobjW = READ_LE_INT16(fobjData + 6);
+					int16 fobjH = READ_LE_INT16(fobjData + 8);
+
+					debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
+						codec, fobjX, fobjY, fobjW, fobjH);
+
+					// Decode codec 3 (RLE) into background buffer
+					if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
+						byte *rleData = fobjData + 14;  // Skip full 14-byte FOBJ header
+						smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+
+						debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
+							fobjW, fobjH, fobjX, fobjY);
+						_level2BackgroundLoaded = true;
+						foundBackground = true;
+
+						// Copy to render bitmap immediately if provided
+						if (renderBitmap) {
+							for (int by = 0; by < 200; by++) {
+								memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
 							}
-							free(animData);
+							debug("Rebel2 loadLevel2Background: Copied to renderBitmap");
 						}
 					}
-					b.seek(startPos);
-					free(scanBuf);
-					return;
+					break;
 				}
 			}
-
-			b.seek(startPos);
-			free(scanBuf);
+			break;
 		}
 	}
 
-	// Handle sound loading cases (par3 21-47)
-	if (par3 >= 21 && par3 <= 47) {
-		debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
-		// TODO: Implement sound loading when needed
+	if (!foundBackground) {
+		debug("Rebel2 loadLevel2Background: Failed to find/decode background FOBJ");
 	}
+
+	return foundBackground;
 }
 
 void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {


Commit: 5c0b23415fb3736ac5a2d6ee0ecd5e5138835aeb
    https://github.com/scummvm/scummvm/commit/5c0b23415fb3736ac5a2d6ee0ecd5e5138835aeb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:57+02:00

Commit Message:
SCUMM: RA2: Reduce shared SMUSH dependencies

Changed paths:
    engines/scumm/insane/insane.h
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index d4d124184c5..85647829a36 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -460,10 +460,6 @@ public:
 
  public:
 
-	bool virtual shouldSkipFrameUpdate(int left, int top, int width, int height) { 
-		return false; 
-	};
-
 	void virtual loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 		// Nothing by default
 	};
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 86ab08d07a9..66afeef366a 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -642,44 +642,6 @@ int32 InsaneRebel2::processMouse() {
 	return buttons;
 }
 
-bool InsaneRebel2::shouldSkipFrameUpdate(int left, int top, int width, int height) {
-	// Only check for Rebel2
-	if (_vm->_game.id != GID_REBEL2) {
-		return false;
-	}
-	
-	Common::Rect updateRect(left, top, left + width, top + height);
-	int updateArea = width * height;
-	
-	// Check if this update region significantly overlaps with any destroyed enemy
-	Common::List<enemy>::iterator it;
-	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (it->destroyed) {
-			// Calculate the intersection of the update rect and enemy rect
-			Common::Rect enemyRect = it->rect;
-			
-			if (updateRect.intersects(enemyRect)) {
-				// Calculate the intersection area
-				int intLeft = MAX(updateRect.left, enemyRect.left);
-				int intTop = MAX(updateRect.top, enemyRect.top);
-				int intRight = MIN(updateRect.right, enemyRect.right);
-				int intBottom = MIN(updateRect.bottom, enemyRect.bottom);
-				int intArea = (intRight - intLeft) * (intBottom - intTop);
-				
-				// Require at least 70% overlap to skip the update
-				// This prevents unrelated frame updates from being incorrectly skipped
-				if (intArea * 100 >= updateArea * 70) {
-					debug("Rebel2: Skipping frame update (%d,%d %dx%d) - %d%% overlap with destroyed enemy ID=%d",
-						left, top, width, height, (intArea * 100) / updateArea, it->id);
-					return true;
-				}
-			}
-		}
-	}
-	
-	return false;
-}
-
 bool InsaneRebel2::isBitSet(int n) {
 	assert (n < 0x200);
 
@@ -699,17 +661,38 @@ void InsaneRebel2::clearBit(int n) {
 }
 
 void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
-	// Rebel Assault 2 does NOT use Full Throttle's conditional frame skip mechanism.
-	// The base Insane::procSKIP() uses _iactBits to decide whether to skip frame objects,
-	// but RA2 handles conditional content differently through IACT opcodes 2/3/4.
+	// Rebel Assault 2 uses SKIP chunks to conditionally skip the next FOBJ/PSAD chunk.
+	// The SKIP chunk contains one or two object IDs. If the bit for the object is set
+	// (i.e., the object is disabled/destroyed), skip the next chunk.
 	//
-	// By overriding this to do nothing, we prevent random frame objects from being
-	// skipped due to uninitialized _iactBits state.
+	// This is the same mechanism as Full Throttle, but RA2 uses it for enemy objects:
+	// - When an enemy is destroyed, setBit(enemy_id) is called
+	// - SKIP chunks in the video contain the enemy ID
+	// - If the bit is set, the next FOBJ (enemy sprite) is skipped
+	// - This prevents destroyed enemy sprites from being rendered
 	//
-	// If RA2 SKIP chunks need to be handled, implement RA2-specific logic here.
-	// For now, just consume the data without setting _skipNext.
-	(void)subSize;
-	(void)b;
+	// The original game's FUN_00423A50 chunk reader uses this mechanism.
+
+	int16 par1, par2;
+	_player->_skipNext = false;
+
+	assert(subSize >= 4);
+	par1 = b.readUint16LE();
+	par2 = b.readUint16LE();
+
+	if (!par2) {
+		// Single ID mode: skip next chunk if this object's bit is set (disabled)
+		if (isBitSet(par1)) {
+			_player->_skipNext = true;
+			debug("Rebel2 SKIP: ID=%d bit is set, skipping next chunk", par1);
+		}
+	} else {
+		// Dual ID mode: skip if bits are different (XOR logic)
+		if (isBitSet(par1) != isBitSet(par2)) {
+			_player->_skipNext = true;
+			debug("Rebel2 SKIP: ID=%d and ID=%d bits differ, skipping next chunk", par1, par2);
+		}
+	}
 }
 
 void InsaneRebel2::procPreRendering(byte *renderBitmap) {
@@ -3697,24 +3680,15 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
 }
 
 void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
-	// Draw enemy indicator brackets and erase destroyed enemy areas
-
-	// Erase destroyed enemies' areas (fill with black)
-	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (it->destroyed) {
-			Common::Rect r = it->rect;
-			if (r.left < 0) r.left = 0;
-			if (r.top < 0) r.top = 0;
-			if (r.right > width) r.right = width;
-			if (r.bottom > height) r.bottom = height;
-
-			for (int y = r.top; y < r.bottom; y++) {
-				for (int x = r.left; x < r.right; x++) {
-					renderBitmap[y * pitch + x] = 0;
-				}
-			}
-		}
-	}
+	// Draw enemy indicator brackets for active enemies
+	//
+	// NOTE: Do NOT fill destroyed enemy areas with black. The original game does not do this.
+	// When an enemy is destroyed:
+	// 1. setBit(enemy_id) disables the enemy in the bit table
+	// 2. clearBit(dependency_id) enables dependent objects (explosion animations)
+	// 3. SKIP chunks in the video cause enemy FOBJ sprites to be skipped (via procSKIP)
+	// 4. renderExplosions() draws the explosion animation from the 5-slot system
+	// 5. The background video shows through where the enemy was
 
 	// Draw green brackets for active enemies (Easy/Medium difficulty only)
 	if (_difficulty >= 2)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 393a6ad7146..fd7a18baafa 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -395,10 +395,7 @@ public:
 	};
 	
 	EmbeddedSanFrame _rebelEmbeddedHud[16];  // HUD overlay slots (userId 0-15)
-	
-	// Check if a partial frame update should be skipped (overlaps with destroyed enemy)
-	bool shouldSkipFrameUpdate(int left, int top, int width, int height) override;
-	
+
 	// Load and decode an embedded SAN animation from IACT chunk data
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
 	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 13a839e80f2..aa12d0d6f36 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -961,11 +961,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		}
 		_dst = _specialBuffer;
 	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		// Skip frame updates for destroyed enemies. The enemy area is erased
-		// in procPostRendering (eraseDestroyedEnemies) before explosions are drawn.
-		if (_insane && _insane->shouldSkipFrameUpdate(left, top, width, height)) {
-			return;  // Skip this frame update
-		}
+		// Rebel2 uses SKIP chunks to conditionally skip FOBJ frames for destroyed enemies.
+		// The SKIP chunk mechanism (via procSKIP -> _skipNext) is checked at the START
+		// of handleFrameObject(), so destroyed enemy sprites are already skipped before
+		// reaching this point. No additional skip logic needed here.
+		//
 		// Rebel2 uses a special buffer for all non-matching frames.
 		// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
 		// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null


Commit: 04aef6e22952ad7ca15c17cd0263592e11d7b5e0
    https://github.com/scummvm/scummvm/commit/04aef6e22952ad7ca15c17cd0263592e11d7b5e0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:57+02:00

Commit Message:
SCUMM: RA2: Read level 2 backgrounds correctly

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/smush/codec1.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 66afeef366a..a6d48da8da1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -50,6 +50,7 @@ namespace Scumm {
 
 // External codec functions from codec1.cpp
 extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
 InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
@@ -1557,9 +1558,23 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// ===== Handler 25 (0x19): Also supports Level 2 background =====
-	if (!handled && _rebelHandler == 25 && par4 == 5) {
-		handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+	// ===== Handler 25 (0x19): Level 2 Background and Overlays =====
+	// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
+	//   par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
+	//   par4=5: 320x200 background -> DAT_0048226c
+	//   par4=6: Overlay -> DAT_00482250, draws immediately
+	//   par4=7: Overlay -> DAT_00482248, draws immediately
+	if (!handled && _rebelHandler == 25) {
+		if (par4 == 5) {
+			// Background (320x200) - stored for per-frame restoration
+			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
+		} else if (par4 == 4 || par4 == 6 || par4 == 7) {
+			// Overlays - draw immediately to renderBitmap
+			// These complete the visual scene along with the background
+			debug("Rebel2 Opcode 8: Handler 25 overlay par4=%d - drawing to screen", par4);
+			loadEmbeddedSan(par4, animData, animDataSize, renderBitmap);
+			handled = true;
+		}
 	}
 
 	// ===== Fallback: Embedded SAN HUD overlays =====
@@ -1867,9 +1882,12 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
 						codec, fobjX, fobjY, fobjW, fobjH);
 
 					// Decode codec 3 (RLE) into background buffer
+					// Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
+					// The standard smushDecodeRLE treats color 0 as transparent, which causes
+					// the background to appear as a "sketch" with black pixels missing.
 					if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
 						byte *rleData = fobjData + 14;  // Skip full 14-byte FOBJ header
-						smushDecodeRLE(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+						smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
 
 						debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
 							fobjW, fobjH, fobjX, fobjY);
@@ -2378,9 +2396,18 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
 	// Skip immediate draw for handlers that render HUD during post-processing:
 	// - Handler 7/8: Ship direction sprites selected based on direction
 	// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
+	//
+	// Exception: Handler 25 (0x19) background overlays (par4/userId=4, 6, 7) should draw immediately.
+	// These complete the visual scene and are NOT positioned by mouse/crosshair.
 	bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
 	                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
 
+	// Handler 25 background overlays should draw immediately
+	if (_rebelHandler == 0x19 && (userId == 4 || userId == 6 || userId == 7)) {
+		skipImmediateDraw = false;
+		debug("Rebel2: Handler 25 background overlay userId=%d - forcing immediate draw", userId);
+	}
+
 	if (!frame.valid || !renderBitmap || skipImmediateDraw) {
 		if (skipImmediateDraw && frame.valid) {
 			debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 5fa4dae5791..0020c51d8cc 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,6 +36,24 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
 	} while (--height);
 }
 
+/**
+ * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
+ * Use this for background images where color 0 should NOT be treated as transparent.
+ * The standard smushDecodeRLE() treats color 0 as transparent, which is correct
+ * for overlay sprites but wrong for background images.
+ *
+ * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
+ */
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch;
+	do {
+		dst += left;
+		bompDecodeLine(dst, src + 2, width, true);  // setZero = TRUE to write all colors
+		src += READ_LE_UINT16(src) + 2;
+		dst += pitch - left;
+	} while (--height);
+}
+
 /**
  * Codec 21/44: Line Update codec
  * Used for fonts (NUT files) and some embedded HUD frames.


Commit: 3abc8c5bc6655c2c32754d51a8a47f917ea4bdd6
    https://github.com/scummvm/scummvm/commit/3abc8c5bc6655c2c32754d51a8a47f917ea4bdd6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:57+02:00

Commit Message:
SCUMM: RA2: Improve level 2 backgrounds

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index a6d48da8da1..09dae96b0ec 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -272,6 +272,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_shipTargetX = 0xa0;
 	_shipTargetY = 0x28;
 	_shipLevelMode = 0;
+	_movementRangeLimit = 127;  // DAT_0047e034 - Start at full range (shooting state)
 	_flyControlMode = 0;   // DAT_004437c0 - Start in flight-only mode (no shooting)
 	_shipFiring = false;
 	_shipDirectionH = 2;   // Start centered horizontally (0-4 range)
@@ -700,13 +701,20 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// Call base class implementation first (handles Full Throttle state machine)
 	Insane::procPreRendering(renderBitmap);
 
-	// For Level 2 gameplay (Handler 8 or 25), restore the background BEFORE FOBJ decoding.
+	// For Level 2 gameplay (Handler 8 only), restore the background BEFORE FOBJ decoding.
 	// The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
 	// clear old ones. By restoring the full background each frame, we ensure old sprite
 	// positions are erased before new ones are drawn.
 	//
+	// NOTE: Handler 25 does NOT restore the background here because:
+	// - Handler 25 uses par4=4,6,7 overlays drawn on top of par4=5 base background
+	// - These overlays are only drawn once in frame 0
+	// - _level2Background only contains the par4=5 base, not the composited result
+	// - The video uses flag 0x08 (preserve background) to keep frame buffer intact
+	// - Enemies draw on top of the existing composited frame buffer
+	//
 	// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
-	if ((_rebelHandler == 8 || _rebelHandler == 25) && _level2BackgroundLoaded && _level2Background && renderBitmap) {
+	if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
 		for (int y = 0; y < 200; y++) {
 			memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
 		}
@@ -1072,6 +1080,29 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 		// Skip position calculation for special modes 4 and 5
 		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+			// ===== Movement Range Transition (Covered vs Shooting) =====
+			// Based on FUN_00401234 lines 85-120:
+			// Mode 2 = "Covered" state - contract movement range to 41 (0x29)
+			// Other modes = "Shooting" state - expand movement range to 127 (0x7f)
+			// Transition happens gradually at ±10 per frame for smooth animation
+			if (_shipLevelMode == 2) {
+				// Covered state - contract movement range
+				if (_movementRangeLimit > 41) {
+					_movementRangeLimit -= 10;
+				}
+				if (_movementRangeLimit < 41) {
+					_movementRangeLimit = 41;
+				}
+			} else {
+				// Shooting state - expand movement range
+				if (_movementRangeLimit < 127) {
+					_movementRangeLimit += 10;
+				}
+				if (_movementRangeLimit > 127) {
+					_movementRangeLimit = 127;
+				}
+			}
+
 			// Calculate target position from mouse input
 			// Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
 			// Based on FUN_00401234 lines 151-166:
@@ -1083,9 +1114,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
 			int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
 
-			// Clamp to valid range
-			if (mouseOffsetX > 127) mouseOffsetX = 127;
-			if (mouseOffsetX < -127) mouseOffsetX = -127;
+			// Clamp X offset to movement range limit (covered/shooting state)
+			// Based on FUN_00401234 lines 119-136
+			if (mouseOffsetX > _movementRangeLimit) mouseOffsetX = _movementRangeLimit;
+			if (mouseOffsetX < -_movementRangeLimit) mouseOffsetX = -_movementRangeLimit;
+			// Y offset always uses full range (±127)
 			if (mouseOffsetY > 127) mouseOffsetY = 127;
 			if (mouseOffsetY < -127) mouseOffsetY = -127;
 
@@ -1144,10 +1177,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 
 		// Update firing state from mouse button
-		_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+		// Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
+		if (_shipLevelMode == 4) {
+			_shipFiring = false;
+		} else {
+			_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+		}
 
-		debug("Rebel2 Opcode 6 (Handler 8): mode=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
-			_shipLevelMode, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
+		debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
+			_shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
 			_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
 
 		// Handler 8 doesn't use the same view offset logic as other handlers
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index fd7a18baafa..4dc42b6160d 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -246,6 +246,9 @@ public:
 	bool isBitSet(int n) override;
 	void setBit(int n) override;
 
+	// Get current handler ID (8, 25, 38 etc.) for SMUSH player to query
+	int getHandler() const { return _rebelHandler; }
+
 	void iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 				  int16 par1, int16 par2, int16 par3, int16 par4);
@@ -570,8 +573,17 @@ public:
 
 	// Level mode for handler 8 (different from _rebelLevelType)
 	// Set by opcode 6 par3, affects ship rendering behavior
+	// Mode 0/1/3: "Shooting" - full movement range (127)
+	// Mode 2: "Covered" - restricted movement (41) - behind cover
+	// Mode 4: "Autopilot" - no shooting, scripted movement
+	// Mode 5: "Cutscene" - ship not rendered
 	int16 _shipLevelMode;            // DAT_0043e000
 
+	// Movement range limiter for Handler 8 (Level 2 covered/shooting states)
+	// Controls horizontal movement range: 127 for shooting, 41 for covered
+	// Gradually transitions by ±10 per frame for smooth animation
+	int16 _movementRangeLimit;       // DAT_0047e034
+
 	// Control mode for Handler 7 (space flight) - DAT_004437c0
 	// Set by IACT opcode 6 par3 when handler is 7
 	// Determines shooting capability and collision zone type:
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index aa12d0d6f36..51b6eec3b4b 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -432,6 +432,18 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
 	assert(subSize >= 6);
 
+	// For RA2 Handler 25, skip FTCH because the frame buffer only contains the
+	// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
+	// immediately in frame 0. Restoring would erase those overlays and make
+	// enemies invisible since they draw on top of the erased areas.
+	if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
+		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+		if (rebel2->getHandler() == 25) {
+			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
+			return;
+		}
+	}
+
 	if (_frameBuffer != nullptr) {
 		memcpy(_dst, _frameBuffer, _width * _height);
 	}


Commit: b621b9cc6e97caaeb9101af3681e9aa81e82b133
    https://github.com/scummvm/scummvm/commit/b621b9cc6e97caaeb9101af3681e9aa81e82b133
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:58+02:00

Commit Message:
SCUMM: RA2: Improve level 2 player sprites

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 09dae96b0ec..2334a645346 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -275,6 +275,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_movementRangeLimit = 127;  // DAT_0047e034 - Start at full range (shooting state)
 	_flyControlMode = 0;   // DAT_004437c0 - Start in flight-only mode (no shooting)
 	_shipFiring = false;
+	_prevMouseButtons = 0; // For edge detection in mouse button handling
 	_shipDirectionH = 2;   // Start centered horizontally (0-4 range)
 	_shipDirectionV = 3;   // Start centered vertically (0-6 range)
 	_shipDirectionIndex = 2 * 7 + 3;  // Center = 17
@@ -287,6 +288,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_flyShipScreenX = 0xd4;      // Start at center (212) - matches DAT_00443708 default
 	_flyShipScreenY = 0x82;      // Start at center (130) - matches DAT_0044370a default
 
+	// Initialize Handler 25 GRD ship system
+	_grd001Sprite = nullptr;     // DAT_00482240 - GRD001 primary ship
+	_grd002Sprite = nullptr;     // DAT_00482238 - GRD002 secondary ship
+	_grdSpriteMode = 0;          // DAT_00457900 - sprite mode (1,2,3,4)
+
 	// Initialize Handler 0x26 turret HUD overlay system
 	_hudOverlayNut = nullptr;    // DAT_0047fe78 - Primary HUD overlay (GRD files, animated)
 	_hudOverlay2Nut = nullptr;   // DAT_0047fe80 - Secondary HUD overlay
@@ -358,6 +364,10 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _flyTargetSprite;
 	delete _flyHiResSprite;
 
+	// Clean up Handler 25 GRD ship sprites
+	delete _grd001Sprite;
+	delete _grd002Sprite;
+
 	// Clean up Handler 0x26 turret HUD overlays
 	delete _hudOverlayNut;
 	delete _hudOverlay2Nut;
@@ -564,11 +574,34 @@ int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
 
 	// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
-	bool wasPressed = false;
-	bool isPressed = (_vm->_system->getEventManager()->getButtonState() & 1) != 0;
-	
-	// Edge detection: only trigger on button press (not hold)
-	if (isPressed && !wasPressed) {
+	// Bit 0 = left button, Bit 1 = right button, Bit 2 = middle button
+	uint32 currentButtons = _vm->_system->getEventManager()->getButtonState();
+
+	// Edge detection for buttons
+	bool leftPressed = (currentButtons & 1) != 0;
+	bool leftWasPressed = (_prevMouseButtons & 1) != 0;
+	bool rightPressed = (currentButtons & 2) != 0;
+	bool rightWasPressed = (_prevMouseButtons & 2) != 0;
+
+	// Store current state for next frame's edge detection
+	_prevMouseButtons = currentButtons;
+
+	// Update _rebelControlMode (DAT_0047a7e4) for Handler 25 covered/uncovered toggle:
+	// The original uses input throttling (every 5 frames) to prevent oscillation.
+	// We use edge detection instead - only set the control mode flag on button DOWN.
+	// - Bit 1 (value 2): Right mouse button just pressed - triggers cover entry
+	// - Bit 0 (value 1): Left mouse button just pressed - triggers cover exit
+	// When covered, any NEW button press exits cover. When uncovered, right button enters cover.
+	_rebelControlMode = 0;
+	if (rightPressed && !rightWasPressed) {
+		_rebelControlMode |= 2;  // Right button just pressed
+	}
+	if (leftPressed && !leftWasPressed) {
+		_rebelControlMode |= 1;  // Left button just pressed
+	}
+
+	// Left button: Trigger shot on button press (not hold)
+	if (leftPressed && !leftWasPressed) {
 		Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
 		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d", 
 			mousePos.x, mousePos.y, _enemies.size());
@@ -640,7 +673,6 @@ int32 InsaneRebel2::processMouse() {
 			}
 		}
 	}
-	wasPressed = isPressed;
 	return buttons;
 }
 
@@ -706,19 +738,41 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// clear old ones. By restoring the full background each frame, we ensure old sprite
 	// positions are erased before new ones are drawn.
 	//
-	// NOTE: Handler 25 does NOT restore the background here because:
-	// - Handler 25 uses par4=4,6,7 overlays drawn on top of par4=5 base background
-	// - These overlays are only drawn once in frame 0
-	// - _level2Background only contains the par4=5 base, not the composited result
-	// - The video uses flag 0x08 (preserve background) to keep frame buffer intact
-	// - Enemies draw on top of the existing composited frame buffer
-	//
 	// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
 	if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
 		for (int y = 0; y < 200; y++) {
 			memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
 		}
 	}
+
+	// For Handler 25 (Level 2 speeder bike), draw the corridor overlay BEFORE FOBJ decoding.
+	// The corridor overlay (par4=4, _rebelEmbeddedHud[4]) shows the covered/uncovered state
+	// and must be drawn BEFORE enemy FOBJ frames so enemies appear ON TOP of the corridor.
+	// Position is (_rebelViewOffsetX, _rebelViewOffsetY) from FUN_0041cadb line 216.
+	if (_rebelHandler == 25 && renderBitmap) {
+		EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
+		if (corridorOverlay.valid && corridorOverlay.pixels) {
+			int overlayX = _rebelViewOffsetX;
+			int overlayY = _rebelViewOffsetY;
+			int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+			int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+			for (int y = 0; y < corridorOverlay.height && (overlayY + y) < bufHeight; y++) {
+				for (int x = 0; x < corridorOverlay.width && (overlayX + x) < pitch; x++) {
+					byte pixel = corridorOverlay.pixels[y * corridorOverlay.width + x];
+					if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
+						int destX = overlayX + x;
+						int destY = overlayY + y;
+						if (destX >= 0 && destY >= 0) {
+							renderBitmap[destY * pitch + destX] = pixel;
+						}
+					}
+				}
+			}
+			debug("Rebel2 procPreRendering: Corridor overlay at (%d,%d) size(%d,%d)",
+				overlayX, overlayY, corridorOverlay.width, corridorOverlay.height);
+		}
+	}
 }
 
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -1309,6 +1363,135 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
+	// Handler 25 (0x19) specific logic (mixed mode - speeder bike)
+	// Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
+	if (_rebelHandler == 25) {
+		// If par4 == 1, enable status bar and reset state (lines 114-121)
+		if (par4 == 1) {
+			_rebelStatusBarSprite = 5;
+			// Reset link tables (DAT_0045797c through DAT_0045917c)
+			for (int i = 0; i < 512; i++) {
+				_rebelLinks[i][0] = 0;
+				_rebelLinks[i][1] = 0;
+				_rebelLinks[i][2] = 0;
+			}
+			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset");
+		}
+
+		// Set sprite mode (DAT_00457900 = par4) - controls which GRD sprite to render
+		// NOTE: Original code uses local_14[3] which is par4, NOT par3!
+		// Mode 1: Uncovered, shooting position - sprite on left
+		// Mode 2: Covered, vertical shift
+		// Mode 3: Transition between covered/uncovered - sprite position depends on direction
+		// Mode 4: Alternative uncovered position - sprite on right
+		_grdSpriteMode = par4;
+
+		// Autopilot logic (lines 123-146)
+		if (!_rebelInvulnerable) {
+			if (_rebelAutopilot == 0) {
+				if ((_rebelControlMode & 2) != 0) {
+					_rebelAutopilot = 1;
+				}
+			} else {
+				if (_rebelControlMode != 0) {
+					_rebelAutopilot = 0;
+				}
+			}
+		} else {
+			// Invulnerable mode: random autopilot changes
+			if (_rebelAutopilot == 0) {
+				if (_vm->_rnd.getRandomNumber(100) == 0) {
+					_rebelAutopilot = 1;
+				}
+			} else {
+				if (_vm->_rnd.getRandomNumber(15) == 0) {
+					_rebelAutopilot = 0;
+					_rebelFlightDir = _vm->_rnd.getRandomNumber(2);
+				}
+			}
+		}
+
+		// Update damage level counter (lines 147-154)
+		if (_rebelAutopilot == 0) {
+			if (_rebelDamageLevel > 0) {
+				_rebelDamageLevel--;
+			}
+		} else {
+			if (_rebelDamageLevel < 5) {
+				_rebelDamageLevel++;
+			}
+		}
+
+		// Flight direction logic for mode 3 (lines 155-177)
+		if (_grdSpriteMode == 3) {
+			if (_rebelDamageLevel == 5) {
+				// At max damage, check for direction change input
+				// For now, use mouse X position to determine direction
+				int16 mouseX = _vm->_mouse.x;
+				if (_player && _player->_width > 320) {
+					mouseX = (mouseX * 320) / _player->_width;
+				}
+				if (mouseX > 235) {  // 0x4b + 160 = 235
+					_rebelFlightDir = 1;
+				}
+				if (mouseX < 85) {   // 160 - 0x4b = 85
+					_rebelFlightDir = 0;
+				}
+			}
+		} else {
+			_rebelFlightDir = 0;
+		}
+
+		// Calculate sprite and view offset positions based on mode (lines 182-213)
+		// DAT_0045790c = view offset X (for corridor overlay)
+		// DAT_0045790e = view offset Y (for corridor overlay)
+		// DAT_00457910 = sprite position X (relative to center)
+		// DAT_00457912 = sprite position Y (relative to center)
+		if (_grdSpriteMode == 1) {
+			// Mode 1: Uncovered, shooting - sprite shifts left as damage increases
+			_rebelViewMode1 = 0x0e;
+			_rebelViewMode2 = 0;
+			_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;   // DAT_0045790c
+			_rebelViewOffset2X = _rebelDamageLevel * -22;       // DAT_00457910
+			_rebelViewOffsetY = 0;                              // DAT_0045790e
+			_rebelViewOffset2Y = 0;                             // DAT_00457912
+		} else if (_grdSpriteMode == 4) {
+			// Mode 4: Alternative uncovered - sprite shifts right
+			_rebelViewMode1 = 0x22;
+			_rebelViewMode2 = 0;
+			_rebelViewOffsetX = _rebelDamageLevel * 10 + -16;   // DAT_0045790c
+			_rebelViewOffset2X = _rebelDamageLevel * 17 + -85;  // DAT_00457910 (0x11 = 17, -0x55 = -85)
+			_rebelViewOffsetY = 0;
+			_rebelViewOffset2Y = 0;
+		} else if (_grdSpriteMode == 2) {
+			// Mode 2: Covered - vertical shift
+			_rebelViewMode1 = 0;
+			_rebelViewMode2 = 0x0e;
+			_rebelViewOffsetY = _rebelDamageLevel * -5 + -14;   // DAT_0045790e
+			_rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60;  // DAT_00457912 (0xf = 15, -0x3c = -60)
+			_rebelViewOffsetX = 0;
+			_rebelViewOffset2X = 0;
+		} else if (_grdSpriteMode == 3) {
+			// Mode 3: Transition - direction-dependent horizontal shift
+			_rebelViewMode1 = 0x0f;
+			_rebelViewMode2 = 0;
+			// (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
+			int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
+			// (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
+			int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
+			_rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15;  // DAT_0045790c
+			_rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel);      // DAT_00457910
+			_rebelViewOffsetY = 0;
+			_rebelViewOffset2Y = 0;
+		}
+
+		debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d viewOff=(%d,%d) spritePos=(%d,%d)",
+			_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir,
+			_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+
+		return;
+	}
+
 	// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
 	if (par4 == 1) {
 		// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
@@ -1584,26 +1767,32 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	}
 
 	// ===== Handler 8: POV Ship Sprites or Background =====
-	// FUN_00401234 case 6: par3 for POV NUTs, par4=5 for background
+	// FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
+	// NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
 	if (!handled && _rebelHandler == 8) {
 		// Check for background loading first (par4=5)
 		if (par4 == 5) {
 			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
 		}
-		// Check for POV NUT sprites
-		else if (par3 == 1 || par3 == 3 || par3 == 6 || par3 == 7) {
-			handled = loadHandler8ShipSprites(animData, animDataSize, par3);
+		// Check for POV NUT sprites (par4=1,3,6,7)
+		else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
+			handled = loadHandler8ShipSprites(animData, animDataSize, par4);
 		}
 	}
 
-	// ===== Handler 25 (0x19): Level 2 Background and Overlays =====
+	// ===== Handler 25 (0x19): Level 2 GRD Ship Sprites and Background =====
 	// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
+	//   par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
+	//   par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
 	//   par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
 	//   par4=5: 320x200 background -> DAT_0048226c
 	//   par4=6: Overlay -> DAT_00482250, draws immediately
 	//   par4=7: Overlay -> DAT_00482248, draws immediately
 	if (!handled && _rebelHandler == 25) {
-		if (par4 == 5) {
+		if (par4 == 1 || par4 == 2) {
+			// GRD ship sprites - load into NutRenderer for per-frame rendering
+			handled = loadHandler25GrdSprites(animData, animDataSize, par4);
+		} else if (par4 == 5) {
 			// Background (320x200) - stored for per-frame restoration
 			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
 		} else if (par4 == 4 || par4 == 6 || par4 == 7) {
@@ -1811,9 +2000,9 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
 	return true;
 }
 
-bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par3) {
+bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par4) {
 	// Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
-	// par3 values:
+	// par4 values (from IACT data offset +6, NOT par3 which is always 0):
 	//   1: POV001 - Primary ship sprite (DAT_0047e010 / _shipSprite)
 	//   3: POV004 - Secondary ship sprite (DAT_0047e028 / _shipSprite2)
 	//   6: POV002 - Ship overlay 1 (DAT_0047e020 / _shipOverlay1)
@@ -1824,21 +2013,21 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
 	}
 
 	// Only handle valid POV sprite slots
-	if (par3 != 1 && par3 != 3 && par3 != 6 && par3 != 7) {
+	if (par4 != 1 && par4 != 3 && par4 != 6 && par4 != 7) {
 		return false;
 	}
 
 	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
-		debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par3=%d", par3);
+		debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
 		delete newNut;
 		return false;
 	}
 
-	debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par3=%d with %d sprites",
-		par3, newNut->getNumChars());
+	debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par4=%d with %d sprites",
+		par4, newNut->getNumChars());
 
-	switch (par3) {
+	switch (par4) {
 	case 1:  // POV001 - Primary ship sprite
 		delete _shipSprite;
 		_shipSprite = newNut;
@@ -1863,6 +2052,50 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
 	return true;
 }
 
+bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par4) {
+	// Handler 25 GRD ship NUT loading - FUN_0041cadb case 6 (opcode 8)
+	// par4 values (from IACT data offset +6):
+	//   1: GRD001 - Primary ship sprite (DAT_00482240 / _grd001Sprite)
+	//   2: GRD002 - Secondary ship sprite (DAT_00482238 / _grd002Sprite)
+
+	if (!animData || size <= 0) {
+		return false;
+	}
+
+	// Only handle valid GRD sprite slots
+	if (par4 != 1 && par4 != 2) {
+		return false;
+	}
+
+	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+	if (!newNut || newNut->getNumChars() <= 0) {
+		debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
+		delete newNut;
+		return false;
+	}
+
+	debug("Rebel2 loadHandler25GrdSprites: Loaded GRD NUT par4=%d with %d sprites",
+		par4, newNut->getNumChars());
+
+	switch (par4) {
+	case 1:  // GRD001 - Primary ship sprite
+		delete _grd001Sprite;
+		_grd001Sprite = newNut;
+		debug("Rebel2: _grd001Sprite set with %d sprites", newNut->getNumChars());
+		break;
+	case 2:  // GRD002 - Secondary ship sprite
+		delete _grd002Sprite;
+		_grd002Sprite = newNut;
+		debug("Rebel2: _grd002Sprite set with %d sprites", newNut->getNumChars());
+		break;
+	default:
+		delete newNut;
+		return false;
+	}
+
+	return true;
+}
+
 bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
 	// Level 2 background loading from embedded ANIM - FUN_00401234 case 5
 	// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
@@ -2440,11 +2673,14 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
 	bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
 	                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
 
-	// Handler 25 background overlays should draw immediately
-	if (_rebelHandler == 0x19 && (userId == 4 || userId == 6 || userId == 7)) {
+	// Handler 25 overlays:
+	// - userId 4 (corridor overlay): Draw during procPostRendering at view offset, NOT immediately
+	// - userId 6, 7 (static overlays): Draw immediately (they don't move)
+	if (_rebelHandler == 0x19 && (userId == 6 || userId == 7)) {
 		skipImmediateDraw = false;
-		debug("Rebel2: Handler 25 background overlay userId=%d - forcing immediate draw", userId);
+		debug("Rebel2: Handler 25 static overlay userId=%d - forcing immediate draw", userId);
 	}
+	// userId 4 should NOT draw immediately - it will be drawn at view offset each frame
 
 	if (!frame.valid || !renderBitmap || skipImmediateDraw) {
 		if (skipImmediateDraw && frame.valid) {
@@ -3301,6 +3537,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	renderHandler7Ship(renderBitmap, pitch, width, height);
 	renderHandler8Ship(renderBitmap, pitch, width, height);
+	renderHandler25Ship(renderBitmap, pitch, width, height);
 	renderFallbackShip(renderBitmap, pitch, width, height);
 
 	// Enemy indicators and destroyed enemy area erase
@@ -3680,6 +3917,144 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 		spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
 }
 
+void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
+	// Handler 25 Ship Rendering (Mixed Mode - GRD sprites)
+	// Based on FUN_0041db5e disassembly (lines 202-248)
+	// Uses _grd001Sprite (GRD001) and _grd002Sprite (GRD002)
+	//
+	// The GRD sprite is drawn at position (DAT_00457910, DAT_00457912) which are
+	// offsets calculated from sprite mode and damage level, relative to screen center.
+	// In the original, these appear to be direct pixel coordinates where:
+	//   - Mode 1: sprite shifts left as damage increases (X = damage * -22)
+	//   - Mode 4: sprite shifts right as damage increases (X = damage * 17 - 85)
+	//   - Mode 2: sprite shifts vertically
+	//   - Mode 3: depends on flight direction
+	//
+	// NOTE: The corridor overlay (par4=4) is drawn in procPreRendering BEFORE enemies,
+	// not here, so that enemies appear ON TOP of the corridor.
+
+	if (_rebelHandler != 25)
+		return;
+
+	// Need at least one GRD sprite to render
+	if (!_grd001Sprite && !_grd002Sprite)
+		return;
+
+	// Base position calculation based on FUN_0041db5e disassembly:
+	// The original reads sprite internal offsets from the NUT data at +0x12 (X) and +0x14 (Y).
+	// Since NutRenderer doesn't expose these, we use the standard positioning formula:
+	// - X: center (160) + mode-based offset
+	// - Y: positioned above status bar (status bar starts at ~Y=180)
+	//
+	// From the HUD positioning formula (lines 3583-3584):
+	//   Y = 182 - (mouseOffset >> 4) - height - spriteOffsetY
+	// This means sprites are bottom-aligned relative to Y=182.
+	//
+	// For Handler 25, the sprite position offsets (DAT_00457910, DAT_00457912) are added.
+	const int baseX = 160;
+	// Base Y is calculated to keep sprite above status bar (Y=180)
+	// We'll position the center around Y=90 (middle of playable area)
+	const int baseY = 90;
+
+	// Calculate actual ship position from base + offset
+	// _rebelViewOffset2X = DAT_00457910 (sprite X offset from mode/damage)
+	// _rebelViewOffset2Y = DAT_00457912 (sprite Y offset from mode/damage)
+	int shipX = baseX + _rebelViewOffset2X;
+	int shipY = baseY + _rebelViewOffset2Y;
+
+	// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
+	// Mode 1, 2, 3, 4 all potentially draw _grd001Sprite
+	if (_grd001Sprite && _grd001Sprite->getNumChars() > 0) {
+		bool shouldDraw = false;
+
+		// Mode 1: Always draw (uncovered/shooting position)
+		if (_grdSpriteMode == 1) {
+			shouldDraw = true;
+		}
+		// Mode 2: Only draw when damaged (DAT_0045790a != 0) - covered position
+		else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
+			shouldDraw = true;
+		}
+		// Mode 3: Always draw (transition between covered/uncovered)
+		else if (_grdSpriteMode == 3) {
+			shouldDraw = true;
+		}
+		// Mode 4: Always draw (alternative uncovered position)
+		else if (_grdSpriteMode == 4) {
+			shouldDraw = true;
+		}
+
+		if (shouldDraw) {
+			// Use sprite 0 (primary frame)
+			int spriteW = _grd001Sprite->getCharWidth(0);
+			int spriteH = _grd001Sprite->getCharHeight(0);
+			// Center the sprite at the calculated position
+			int drawX = shipX - spriteW / 2;
+			int drawY = shipY - spriteH / 2;
+
+			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd001Sprite, 0);
+
+			debug("Rebel2 Handler25: GRD001 at (%d,%d) base(%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
+				drawX, drawY, baseX, baseY, _rebelViewOffset2X, _rebelViewOffset2Y,
+				spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
+		}
+	}
+
+	// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
+	// The sprite index is calculated based on damage level and aiming position
+	// From FUN_0041db5e lines 160-168:
+	//   If damage == 0: index = yZone * 5 + xZone + 5 (aiming-based, 5-14)
+	//   If damage != 0:
+	//     If direction == 0: index = 5 - damage (0-5, covered left)
+	//     If direction != 0: index = 25 - damage (20-25, covered right)
+	if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
+		// Calculate sprite index based on damage level and direction
+		int spriteIdx;
+		int numSprites = _grd002Sprite->getNumChars();
+
+		if (_rebelDamageLevel == 0) {
+			// Uncovered state: use aiming-based sprite selection (5-14)
+			// For now, use center position (xZone=2, yZone=0) -> 0*5+2+5 = 7
+			// TODO: Calculate actual zone from crosshair position
+			int xZone = 2;  // Center horizontal (0-4)
+			int yZone = 0;  // Top vertical (0-1)
+
+			// Direction-based mirroring (line 161-162)
+			if (_rebelFlightDir == (yZone & 1)) {
+				xZone = 4 - xZone;
+			}
+
+			spriteIdx = yZone * 5 + xZone + 5;
+		} else {
+			// Transitioning/covered state: use direction-based sprite
+			if (_rebelFlightDir == 0) {
+				// Direction 0: sprites 0-5 (covered left)
+				spriteIdx = 5 - _rebelDamageLevel;
+			} else {
+				// Direction 1: sprites 20-25 (covered right)
+				spriteIdx = 25 - _rebelDamageLevel;
+			}
+		}
+
+		// Clamp to valid range
+		if (spriteIdx < 0) spriteIdx = 0;
+		if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+
+		int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
+		int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
+
+		// Position offset from GRD002 sprite header (lines 238-243 in disasm)
+		// This adds an offset from the sprite's internal positioning data
+		int drawX = shipX - spriteW / 2;
+		int drawY = shipY - spriteH / 2;
+
+		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd002Sprite, spriteIdx);
+
+		debug("Rebel2 Handler25: GRD002 at (%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d",
+			drawX, drawY, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir);
+	}
+}
+
 void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
 	// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
 	// userId=11 contains the ship sprite strip
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 4dc42b6160d..0b8a32d2974 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -329,8 +329,11 @@ public:
 	// Load turret HUD overlay NUT from ANIM data
 	bool loadTurretHudOverlay(byte *animData, int32 size, int16 par3);
 
-	// Load Handler 8 ship POV NUT sprites from ANIM data
-	bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par3);
+	// Load Handler 8 ship POV NUT sprites from ANIM data (par4 = sprite type: 1,3,6,7)
+	bool loadHandler8ShipSprites(byte *animData, int32 size, int16 par4);
+
+	// Load Handler 25 GRD NUT sprites from ANIM data (par4 = sprite type: 1,2)
+	bool loadHandler25GrdSprites(byte *animData, int32 size, int16 par4);
 
 	// Load Level 2 background from embedded ANIM
 	bool loadLevel2Background(byte *animData, int32 size, byte *renderBitmap);
@@ -597,6 +600,9 @@ public:
 	// Ship firing state (from mouse button)
 	bool _shipFiring;
 
+	// Previous mouse button state for edge detection (bit 0=left, bit 1=right, bit 2=middle)
+	uint32 _prevMouseButtons;
+
 	// Ship direction index for sprite selection (Handler 7)
 	// Calculated from ship position: horizontal * 7 + vertical
 	// horizontal: 0-4 (left to right), vertical: 0-6 (up to down)
@@ -630,6 +636,33 @@ public:
 	int16 _flyShipScreenX;           // DAT_0044370e - Ship X screen position
 	int16 _flyShipScreenY;           // DAT_0044370c - Ship Y screen position
 
+	// ======================= Handler 25 (0x19) GRD Ship System =======================
+	// For mixed mode missions (Level 2 speeder bike, etc.), Handler 25 uses GRD NUT
+	// sprites loaded via IACT opcode 8. The ship is rendered based on DAT_00457900 mode.
+	//
+	// Based on FUN_0041cadb case 6 and FUN_0041db5e disassembly:
+	// - DAT_00482240: Primary ship sprite (GRD001, par4=1)
+	// - DAT_00482238: Secondary ship sprite (GRD002, par4=2)
+	// - DAT_00457900: Sprite mode (1,2,3,4) controls which sprite to draw
+	// - DAT_00457910: Ship X screen position
+	// - DAT_00457912: Ship Y screen position
+	// - DAT_00457902: Flight direction (affects GRD002 mirroring)
+	// - DAT_0045790a: Damage level (affects rendering conditions)
+
+	NutRenderer *_grd001Sprite;      // DAT_00482240 - GRD001 primary ship NUT
+	NutRenderer *_grd002Sprite;      // DAT_00482238 - GRD002 secondary ship NUT
+
+	// Handler 25 sprite mode (DAT_00457900) - set by opcode 6 par3
+	// Controls which sprite variant to draw:
+	//   1: Draw _grd001Sprite normally
+	//   2: Draw _grd001Sprite only when damaged (DAT_0045790a != 0)
+	//   3: Draw _grd001Sprite and GRD005 (DAT_00482258) overlay
+	//   4: Draw _grd001Sprite with buffer offset
+	int16 _grdSpriteMode;            // DAT_00457900
+
+	// Render Handler 25 ship sprites (called from procPostRendering)
+	void renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height);
+
 	// ======================= Handler 0x26 Turret HUD Overlays =======================
 	// For turret missions (Level 1, etc.), Handler 0x26 uses NUT-based HUD overlays
 	// loaded via IACT opcode 8. These contain animated cockpit panel elements.


Commit: 9ce2e159b1c50140c07c06a741cb2117e141e33a
    https://github.com/scummvm/scummvm/commit/9ce2e159b1c50140c07c06a741cb2117e141e33a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:58+02:00

Commit Message:
SCUMM: RA2: Implement level selection screen

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2334a645346..35563e955ad 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -318,9 +318,25 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_levelUnlocked[i] = (i == 0);  // Only level 1 unlocked initially
 	}
 
-	// Initialize level selection system
+	// Initialize chapter selection system (FUN_00415CF8)
+	// 17 items: 16 chapters + BACK option
+	_chapterSelection = 0;        // First chapter selected
+	_chapterItemCount = 17;       // 16 chapters + BACK
+	_selectedChapter = 0;         // Default selected chapter
+	_passwordInput = "";          // No password input
+	for (i = 0; i < 16; i++) {
+		_chapterUnlocked[i] = (i == 0);  // Only chapter 1 unlocked initially
+	}
+
+	// Initialize preview offset for chapter selection (FUN_00425170)
+	// X offset: -90 (0xffa6), Y offset: selection * -50 + 75
+	_previewOffsetX = -90;
+	_previewOffsetY = 75;  // Chapter 0: 0 * -50 + 75 = 75
+
+	// Initialize pilot selection system (FUN_00414A41)
+	// Menu structure: 6 levels + 4 options (NEW PILOT, DELETE PILOT, COPY PILOT, MAIN MENU)
 	_levelSelection = 0;          // First level selected
-	_levelItemCount = 2;          // Level 1 + MAIN MENU (will grow as more levels implemented)
+	_levelItemCount = 10;         // 6 levels + 4 options
 	_selectedLevel = 1;           // Default selected level
 
 	// Initialize menu input capture system
@@ -3408,29 +3424,63 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	// Check if we're in menu mode (menu state + intro flag)
 	bool menuMode = (introPlaying && _gameState == kStateMainMenu);
-	bool levelSelectMode = (introPlaying && _gameState == kStateLevelSelect);
+	bool pilotSelectMode = (introPlaying && _gameState == kStatePilotSelect);
+	bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
 
-	// Handle level selection input and rendering
-	if (levelSelectMode) {
-		// Show the standard Windows arrow cursor (same as menu)
+	// Handle pilot selection input and rendering (FUN_00414A41)
+	// This is the pilot/save slot selection screen with centered menu
+	if (pilotSelectMode) {
+		// Show the standard Windows arrow cursor
 		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
 		CursorMan.replaceCursor(cursor);
 		delete cursor;
 		CursorMan.showMouse(true);
 
-		// Process level selection input
+		// Process pilot selection input - emulates FUN_00414A41 input handling
 		int selection = processLevelSelectInput();
 
-		// Draw level selection overlay
+		// Draw pilot selection overlay - centered menu like main menu
 		drawLevelSelectOverlay(renderBitmap, pitch, width, height);
 
 		// If a selection was confirmed, signal video to stop
 		if (selection >= 0) {
-			debug("Rebel2: Level selection confirmed: %d", selection);
+			debug("Rebel2: Pilot selection confirmed: %d", selection);
 			_vm->_smushVideoShouldFinish = true;
 		}
 
-		// Skip normal HUD rendering in level select mode
+		// Skip normal HUD rendering in pilot select mode
+		return;
+	}
+
+	// Handle chapter selection input and rendering (FUN_00415CF8)
+	// This is the actual level/chapter selection screen with preview and password
+	if (chapterSelectMode) {
+		// Show the standard Windows arrow cursor (same as menu)
+		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+		CursorMan.replaceCursor(cursor);
+		delete cursor;
+		CursorMan.showMouse(true);
+
+		// Fill screen with BLACK background
+		// The original uses O_LEVEL.SAN (640x400) which has a black background.
+		// For 320x200 mode, we just fill with black (color index 0).
+		for (int y = 0; y < height; y++) {
+			memset(renderBitmap + y * pitch, 0, width);
+		}
+
+		// Process chapter selection input - emulates FUN_00415CF8 input handling
+		int selection = processChapterSelectInput();
+
+		// Draw chapter selection overlay - emulates FUN_00415CF8 rendering
+		drawChapterSelectOverlay(renderBitmap, pitch, width, height);
+
+		// If a selection was confirmed, signal video to stop
+		if (selection >= 0) {
+			debug("Rebel2: Chapter selection confirmed: %d", selection);
+			_vm->_smushVideoShouldFinish = true;
+		}
+
+		// Skip normal HUD rendering in chapter select mode
 		return;
 	}
 
@@ -5067,15 +5117,15 @@ int InsaneRebel2::runMainMenu() {
 		// case 5: Credits (play O_CREDIT.SAN, then return 1)
 		// case 6: Quit (stop video, exit)
 		switch (_menuSelection) {
-		case 0:  // Start Game -> Level Selection
-			debug("Rebel2: Start Game selected - going to level selection");
-			_gameState = kStateLevelSelect;
+		case 0:  // Start Game -> Pilot Selection
+			debug("Rebel2: Start Game selected - going to pilot selection");
+			_gameState = kStatePilotSelect;
 			_menuInputActive = false;
-			return kMenuContinue;  // Go to level selection
+			return kMenuContinue;  // Go to pilot selection
 
 		case 1:  // Continue (same as Start Game for now)
-			debug("Rebel2: Continue selected - going to level selection");
-			_gameState = kStateLevelSelect;
+			debug("Rebel2: Continue selected - going to pilot selection");
+			_gameState = kStatePilotSelect;
 			_menuInputActive = false;
 			return kMenuContinue;
 
@@ -5118,46 +5168,798 @@ int InsaneRebel2::runMainMenu() {
 	return 0;
 }
 
-// ==================== Level Selection Menu ====================
-// Emulates FUN_00414A41 - Level selection menu
-// For now, only Level 1 is available. This will be expanded later.
+// ==================== Chapter Selection Screen ====================
+// Emulates FUN_00415CF8 - Chapter selection with preview and password input
+// This is the actual level/chapter selection that players see after pilot select
+
+int InsaneRebel2::runChapterSelect() {
+	// Chapter selection screen loop - emulates FUN_00415CF8
+	// Returns:
+	//   kChapterSelectPlay (5) = Play selected chapter
+	//   kChapterSelectBack (2) = Return to main menu (ESC or BACK)
+	//   kChapterSelectQuit (0) = Quit game
+
+	debug("Rebel2: Entering chapter selection (FUN_00415CF8)");
+
+	// Enable menu input capture
+	_menuInputActive = true;
+	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+
+	// Initialize chapter selection state
+	_chapterSelection = 0;
+	_chapterItemCount = 17;  // 16 chapters + BACK
+	_selectedChapter = 0;
+	_passwordInput = "";
+	_menuRepeatDelay = 0;
+	_gameState = kStateChapterSelect;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Chapter selection background - emulates FUN_00415CF8 line 57
+	// Original uses O_LEVEL.SAN (640x400). We use menu video for 320x200 mode.
+	while (!_vm->shouldQuit()) {
+		_vm->_smushVideoShouldFinish = false;
+
+		Common::String menuVideo = getRandomMenuVideo();
+		debug("Rebel2: Playing chapter select background: %s", menuVideo.c_str());
+
+		// Set video flags for menu mode
+		splayer->setCurVideoFlags(0x20);
+
+		// Play the menu video as chapter selection background
+		splayer->play(menuVideo.c_str(), 12);
+
+		if (_vm->shouldQuit()) {
+			_menuInputActive = false;
+			return kChapterSelectQuit;
+		}
+
+		// If video ended without selection, continue looping
+		if (!_vm->_smushVideoShouldFinish) {
+			continue;
+		}
+
+		_vm->_smushVideoShouldFinish = false;
+
+		debug("Rebel2: Chapter selection made: %d", _chapterSelection);
+
+		// Process chapter selection (lines 134-236 of FUN_00415CF8)
+		if (_chapterSelection == 16) {
+			// BACK selected (index 16 = 17th item)
+			debug("Rebel2: BACK to main menu selected");
+			_menuInputActive = false;
+			return kChapterSelectBack;
+		}
+
+		if (_chapterSelection >= 0 && _chapterSelection < 16) {
+			// Chapter selected
+			if (_chapterUnlocked[_chapterSelection]) {
+				// Chapter is unlocked - start it
+				_selectedChapter = _chapterSelection;
+				debug("Rebel2: Chapter %d selected (unlocked)", _selectedChapter + 1);
+				_menuInputActive = false;
+				return kChapterSelectPlay;
+			} else {
+				// Chapter is locked - check password (lines 239-257 of FUN_00415CF8)
+				// For now, just play error sound and continue
+				debug("Rebel2: Chapter %d is locked", _chapterSelection + 1);
+				continue;
+			}
+		}
+	}
+
+	_menuInputActive = false;
+	return kChapterSelectQuit;
+}
+
+int InsaneRebel2::processChapterSelectInput() {
+	// Process input for chapter selection screen
+	// Emulates input handling in FUN_00415CF8 (lines 95-133)
+	// Returns: -1 = no action, 0+ = item selected
+
+	int result = -1;
+
+	while (!_menuEventQueue.empty()) {
+		Common::Event event = _menuEventQueue.front();
+		_menuEventQueue.pop();
+
+		switch (event.type) {
+		case Common::EVENT_KEYDOWN:
+			switch (event.kbd.keycode) {
+			case Common::KEYCODE_UP:
+				// Move selection up, wrap to bottom
+				_chapterSelection--;
+				if (_chapterSelection < 0) {
+					_chapterSelection = _chapterItemCount - 1;
+				}
+				debug("ChapterSelect: Selection changed to %d (UP)", _chapterSelection);
+				break;
+
+			case Common::KEYCODE_DOWN:
+				// Move selection down, wrap to top
+				_chapterSelection++;
+				if (_chapterSelection >= _chapterItemCount) {
+					_chapterSelection = 0;
+				}
+				debug("ChapterSelect: Selection changed to %d (DOWN)", _chapterSelection);
+				break;
+
+			case Common::KEYCODE_RETURN:
+			case Common::KEYCODE_KP_ENTER:
+				if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
+					result = _chapterSelection;
+					debug("ChapterSelect: Item %d selected (ENTER)", _chapterSelection);
+				}
+				break;
+
+			case Common::KEYCODE_ESCAPE:
+				// ESC = Back to main menu (same as selecting BACK)
+				result = 16;  // BACK index
+				debug("ChapterSelect: ESC pressed - back to menu");
+				break;
+
+			case Common::KEYCODE_BACKSPACE:
+				// Backspace for password input (line 107-112 of FUN_00415CF8)
+				if (!_passwordInput.empty()) {
+					_passwordInput.deleteLastChar();
+					debug("ChapterSelect: Password backspace, now: %s", _passwordInput.c_str());
+				}
+				break;
+
+			default:
+				// Printable character for password input (lines 114-121 of FUN_00415CF8)
+				if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7E) {
+					if (_passwordInput.size() < 8) {
+						_passwordInput += (char)event.kbd.ascii;
+						debug("ChapterSelect: Password input: %s", _passwordInput.c_str());
+					}
+				}
+				break;
+			}
+			break;
+
+		case Common::EVENT_LBUTTONDOWN:
+			{
+				// Mouse click - check if clicking on a menu item
+				// From FUN_0041F5AE assembly (low-res 320x200):
+				// Item Y = 17 * -5 + i * 10 + 104 = 19 + i * 10
+				// Chapters 0-14 at Y = 19 + i*10
+				// FINALE (15) at Y = 19 + 15*10 = 169
+				// RETURN TO PILOTS (16) at Y = 19 + 16*10 = 179
+				int baseY = 19;
+				int itemHeight = 10;
+				int mouseY = event.mouse.y;
+
+				// Check chapters 0-14
+				for (int i = 0; i < 15; i++) {
+					int itemY = baseY + i * itemHeight;
+					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+						_chapterSelection = i;
+						result = i;
+						debug("ChapterSelect: Chapter %d clicked at Y=%d", i + 1, mouseY);
+						break;
+					}
+				}
+
+				// Check FINALE (index 15) at Y=169
+				int finaleY = baseY + 15 * itemHeight;
+				if (mouseY >= finaleY - 4 && mouseY < finaleY + 6) {
+					_chapterSelection = 15;
+					result = 15;
+					debug("ChapterSelect: FINALE clicked");
+				}
+
+				// Check RETURN TO PILOTS (index 16) at Y=179
+				int returnY = baseY + 16 * itemHeight;
+				if (mouseY >= returnY - 4 && mouseY < returnY + 6) {
+					_chapterSelection = 16;
+					result = 16;
+					debug("ChapterSelect: RETURN TO PILOTS clicked");
+				}
+			}
+			break;
+
+		default:
+			break;
+		}
+	}
+
+	return result;
+}
+
+// Draw preview box border - emulates FUN_004292D0 calls at lines 128-133 of FUN_00415CF8
+void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int height) {
+	// Low-res (320x200) coordinates from FUN_00415CF8:
+	// Outer box: X=0xe4 (228), Y=0x49 (73), W=0x54 (84), H=0x36 (54), color=0xF8
+	// Inner box: X=0xe5 (229), Y=0x4a (74), W=0x52 (82), H=0x34 (52), color=4
+
+	// Outer border (bright)
+	int outerX = 228, outerY = 73, outerW = 84, outerH = 54;
+	byte outerColor = 0xF8;
+
+	// Draw outer box edges
+	// Top edge
+	for (int px = outerX; px < outerX + outerW && px < width; px++) {
+		if (outerY >= 0 && outerY < height && px >= 0)
+			renderBitmap[outerY * pitch + px] = outerColor;
+	}
+	// Bottom edge
+	int bottomY = outerY + outerH - 1;
+	if (bottomY < height) {
+		for (int px = outerX; px < outerX + outerW && px < width; px++) {
+			if (px >= 0)
+				renderBitmap[bottomY * pitch + px] = outerColor;
+		}
+	}
+	// Left edge
+	for (int py = outerY; py < outerY + outerH && py < height; py++) {
+		if (py >= 0 && outerX >= 0 && outerX < width)
+			renderBitmap[py * pitch + outerX] = outerColor;
+	}
+	// Right edge
+	int rightX = outerX + outerW - 1;
+	if (rightX < width) {
+		for (int py = outerY; py < outerY + outerH && py < height; py++) {
+			if (py >= 0)
+				renderBitmap[py * pitch + rightX] = outerColor;
+		}
+	}
+
+	// Inner border (dark)
+	int innerX = 229, innerY = 74, innerW = 82, innerH = 52;
+	byte innerColor = 4;
+
+	// Top edge
+	for (int px = innerX; px < innerX + innerW && px < width; px++) {
+		if (innerY >= 0 && innerY < height && px >= 0)
+			renderBitmap[innerY * pitch + px] = innerColor;
+	}
+	// Bottom edge
+	bottomY = innerY + innerH - 1;
+	if (bottomY < height) {
+		for (int px = innerX; px < innerX + innerW && px < width; px++) {
+			if (px >= 0)
+				renderBitmap[bottomY * pitch + px] = innerColor;
+		}
+	}
+	// Left edge
+	for (int py = innerY; py < innerY + innerH && py < height; py++) {
+		if (py >= 0 && innerX >= 0 && innerX < width)
+			renderBitmap[py * pitch + innerX] = innerColor;
+	}
+	// Right edge
+	rightX = innerX + innerW - 1;
+	if (rightX < width) {
+		for (int py = innerY; py < innerY + innerH && py < height; py++) {
+			if (py >= 0)
+				renderBitmap[py * pitch + rightX] = innerColor;
+		}
+	}
+}
+
+// Draw preview thumbnail content - emulates FUN_00429b40 scaled preview draw
+// Shows chapter number and unlock status inside the preview box
+void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
+	// Preview area coordinates (inside the inner border)
+	// Inner box: X=230 (229+1), Y=75 (74+1), W=80 (82-2), H=50 (52-2)
+	int previewX = 230;
+	int previewY = 75;
+	int previewW = 80;
+	int previewH = 50;
+
+	// Update preview offset based on chapter selection (FUN_00425170)
+	// Y offset = chapter * -50 + 75
+	_previewOffsetY = chapter * -50 + 75;
+
+	// Determine fill color based on chapter state
+	// Unlocked: dark blue (0x10), Locked: dark gray (0x08)
+	byte fillColor = (_chapterUnlocked[chapter]) ? 0x10 : 0x08;
+
+	// Fill the preview area with background color
+	for (int py = previewY; py < previewY + previewH && py < height; py++) {
+		if (py >= 0) {
+			for (int px = previewX; px < previewX + previewW && px < width; px++) {
+				if (px >= 0)
+					renderBitmap[py * pitch + px] = fillColor;
+			}
+		}
+	}
+
+	// Draw chapter number in the center of the preview area
+	NutRenderer *font = _smush_smalfontNut;
+	if (!font) return;
+
+	char chapterStr[16];
+	if (chapter < 15) {
+		snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
+	} else {
+		snprintf(chapterStr, sizeof(chapterStr), "FIN");  // FINALE
+	}
+
+	// Calculate text width for centering
+	int textWidth = 0;
+	int numChars = font->getNumChars();
+	for (const char *c = chapterStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numChars) {
+			textWidth += font->getCharWidth(charIdx);
+		}
+	}
+
+	// Center the text in the preview area
+	int textX = previewX + (previewW - textWidth) / 2;
+	int textY = previewY + previewH / 2 - 4;  // Center vertically (approx)
+
+	Common::Rect clipRect(0, 0, width, height);
+
+	// Draw the chapter text
+	int curX = textX;
+	for (const char *c = chapterStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numChars) {
+			int charWidth = font->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && textY >= 0 && textY < height) {
+				font->drawCharV7(renderBitmap, clipRect, curX, textY, pitch, -1,
+				                 kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+
+	// Draw lock icon for locked chapters (simple "X" pattern)
+	if (!_chapterUnlocked[chapter]) {
+		byte lockColor = 0xF8;  // Bright red/orange
+		int lockX = previewX + previewW - 15;
+		int lockY = previewY + 5;
+
+		// Draw small X to indicate locked
+		for (int i = 0; i < 8; i++) {
+			if (lockX + i < width && lockY + i < height)
+				renderBitmap[(lockY + i) * pitch + lockX + i] = lockColor;
+			if (lockX + 7 - i < width && lockY + i < height)
+				renderBitmap[(lockY + i) * pitch + lockX + 7 - i] = lockColor;
+		}
+	}
+}
+
+// Draw left-aligned menu item - emulates FUN_0041F5AE with param_4=1
+void InsaneRebel2::drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
+                                           const char *text, int x, int y, bool selected) {
+	NutRenderer *font = _smush_smalfontNut;
+	if (!font) return;
+
+	int numFontChars = font->getNumChars();
+	Common::Rect clipRect(0, 0, width, height);
+
+	// Calculate text width for selection box
+	int textWidth = 0;
+	for (const char *c = text; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			textWidth += font->getCharWidth(charIdx);
+		}
+	}
+
+	// Draw selection box if selected
+	// Box dimensions: width = textWidth + 6, height = 10 (low-res)
+	if (selected) {
+		static int frameCounter = 0;
+		frameCounter++;
+
+		int boxWidth = textWidth + 6;
+		int boxHeight = 10;
+		int boxX = x - 3;  // Left-aligned, so box starts 3 pixels before text
+		int boxY = y - 1;
+
+		// Flashing color (emulates (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10)
+		byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+		// Draw box border
+		if (boxY >= 0 && boxY < height && boxX >= 0) {
+			// Top edge
+			for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+				if (px >= 0) renderBitmap[boxY * pitch + px] = highlightColor;
+			}
+			// Bottom edge
+			int bottomY = boxY + boxHeight - 1;
+			if (bottomY < height) {
+				for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
+					if (px >= 0) renderBitmap[bottomY * pitch + px] = highlightColor;
+				}
+			}
+			// Left edge
+			for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+				if (py >= 0 && boxX >= 0) renderBitmap[py * pitch + boxX] = highlightColor;
+			}
+			// Right edge
+			int rightX = boxX + boxWidth - 1;
+			if (rightX < width) {
+				for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
+					if (py >= 0) renderBitmap[py * pitch + rightX] = highlightColor;
+				}
+			}
+		}
+	}
+
+	// Draw text characters
+	int curX = x;
+	for (const char *c = text; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			int charWidth = font->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && y >= 0 && y < height) {
+				font->drawCharV7(renderBitmap, clipRect, curX, y, pitch, -1,
+				                 kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+}
+
+// Draw password input field - emulates lines 106-125 of FUN_00415CF8
+void InsaneRebel2::drawPasswordInput(byte *renderBitmap, int pitch, int width, int height) {
+	// Password display position for low-res: X=30 (0x1e), Y=190 (0xbe)
+	int infoX = 30;
+	int infoY = 190;
+
+	// Build display string with cursor
+	static int frameCounter = 0;
+	frameCounter++;
+	char cursor = (frameCounter & 2) ? '_' : ' ';  // Blinking cursor
+
+	char displayText[32];
+	snprintf(displayText, sizeof(displayText), "ACCESS CODE: %s%c", _passwordInput.c_str(), cursor);
+
+	NutRenderer *font = _smush_smalfontNut;
+	if (!font) return;
+
+	int numFontChars = font->getNumChars();
+	Common::Rect clipRect(0, 0, width, height);
+
+	int curX = infoX;
+	for (const char *c = displayText; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			int charWidth = font->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
+				font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
+				                 kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+}
+
+// Draw score/time display - emulates lines 99-104 of FUN_00415CF8
+void InsaneRebel2::drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter) {
+	// Score display position for low-res: X=25 (0x19), Y=190 (0xbe)
+	int infoX = 25;
+	int infoY = 190;
+
+	// For now, just display a placeholder score
+	char displayText[32];
+	snprintf(displayText, sizeof(displayText), "SCORE: %d", 0);
+
+	NutRenderer *font = _smush_smalfontNut;
+	if (!font) return;
+
+	int numFontChars = font->getNumChars();
+	Common::Rect clipRect(0, 0, width, height);
+
+	int curX = infoX;
+	for (const char *c = displayText; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			int charWidth = font->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
+				font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
+				                 kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+}
+
+// Draw chapter selection overlay - called during O_LEVEL.SAN playback
+// FUN_00415CF8 - Chapter selection screen with preview thumbnail
+void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
+	// Draw chapter selection screen overlay
+	// Emulates rendering in FUN_00415CF8
+	//
+	// Layout (320x200 mode):
+	// - Title "Chapters" at top-left using TITLFONT
+	// - 15 chapters + FINALE + RETURN TO PILOTS (17 items total)
+	// - Three preview boxes on right side
+	// - Status bar at bottom: "PILOTS: X  SCORE: Y  RANK:"
+
+	// Chapter names from original game (DAT_00457820)
+	// Format: "CHAPTER X - NAME" for unlocked, "CHAPTER X -" for locked
+	static const char *chapterNames[] = {
+		"THE DREIGHTON TRIANGLE",   // Chapter 1
+		"ASTEROID PURSUIT",         // Chapter 2
+		"ABOARD THE TERROR",        // Chapter 3
+		"TIE FIGHTER ATTACK",       // Chapter 4
+		"DREIGHTON NEBULA",         // Chapter 5
+		"CORELLIA",                 // Chapter 6
+		"SPEEDER PURSUIT",          // Chapter 7
+		"CANYON CHASE",             // Chapter 8
+		"DEATH STAR APPROACH",      // Chapter 9
+		"THE ASTEROID FIELD",       // Chapter 10
+		"INSIDE THE DEATH STAR",    // Chapter 11
+		"TRENCH RUN",               // Chapter 12
+		"THE MAIN REACTOR",         // Chapter 13
+		"ESCAPE FROM YAVIN",        // Chapter 14
+		"FINALE",                   // Chapter 15 is actually FINALE
+	};
+
+	// Frame counter for flashing selection box
+	static int frameCounter = 0;
+	frameCounter++;
+
+	NutRenderer *menuFont = _smush_smalfontNut;
+	NutRenderer *titleFont = _smush_titlefontNut;
+	if (!menuFont) return;
+
+	Common::Rect clipRect(0, 0, width, height);
+	int numFontChars = menuFont->getNumChars();
+
+	// === Draw title "Chapters" using TITLFONT ===
+	// From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
+	// Title X = 0x28 = 40, Title Y = param_3 * -5 + 0x56 = 17 * -5 + 86 = 1
+	int titleX = 40;
+	int titleY = 1;
+
+	if (titleFont) {
+		const char *titleText = "Chapters";
+		int curX = titleX;
+		int numTitleChars = titleFont->getNumChars();
+		for (const char *c = titleText; *c; c++) {
+			int charIdx = (unsigned char)*c;
+			if (charIdx < numTitleChars) {
+				int charWidth = titleFont->getCharWidth(charIdx);
+				if (curX >= 0 && curX + charWidth <= width && titleY >= 0) {
+					titleFont->drawCharV7(renderBitmap, clipRect, curX, titleY, pitch, -1,
+					                      kStyleAlignLeft, charIdx, true, true);
+				}
+				curX += charWidth;
+			}
+		}
+	}
+
+	// === Draw chapter list (left-aligned) ===
+	// From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
+	// Item X = 0x17 = 23
+	// Item Y = param_3 * -5 + local_c * 10 + 0x68 = 17 * -5 + i * 10 + 104 = 19 + i * 10
+	// Selection Box X = 0x14 = 20, Y = 18 + i * 10 (item Y - 1)
+	int itemX = 23;       // From assembly: 0x17 = 23
+	int itemBaseY = 19;   // From assembly: 17 * -5 + 104 = 19
+	int itemSpacing = 10; // From assembly: 10 pixels between items
+	int selBoxX = 20;     // From assembly: 0x14 = 20
+
+	// Draw 15 chapters (indices 0-14)
+	for (int i = 0; i < 15; i++) {
+		int itemY = itemBaseY + i * itemSpacing;
+		bool isSelected = (i == _chapterSelection);
+
+		// Build chapter string: "CHAPTER X - NAME" or "CHAPTER X -"
+		char chapterStr[64];
+		snprintf(chapterStr, sizeof(chapterStr), "CHAPTER %d - %s", i + 1,
+		         _chapterUnlocked[i] ? chapterNames[i] : "");
+
+		// Calculate text width for selection box
+		int textWidth = 0;
+		for (const char *c = chapterStr; *c; c++) {
+			int charIdx = (unsigned char)*c;
+			if (charIdx < numFontChars) {
+				textWidth += menuFont->getCharWidth(charIdx);
+			}
+		}
+
+		// Draw selection box if selected (red border)
+		// From assembly: box starts at X=0x14=20, Y = itemY - 1
+		if (isSelected) {
+			int boxWidth = textWidth + 6;  // From assembly: textWidth + 6
+			int boxHeight = 10;            // From assembly: 10 pixels
+			int boxX = selBoxX;            // From assembly: 0x14 = 20
+			int boxY = itemY - 1;          // From assembly: itemY - 1
+
+			// Flashing color from assembly: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
+			// This gives -16 or -8, which are palette-relative negative offsets
+			byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+			// Draw box border
+			if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
+				for (int px = boxX; px < boxX + boxWidth; px++) {
+					renderBitmap[boxY * pitch + px] = boxColor;
+					renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
+				}
+				for (int py = boxY; py < boxY + boxHeight; py++) {
+					renderBitmap[py * pitch + boxX] = boxColor;
+					renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
+				}
+			}
+		}
+
+		// Draw chapter text
+		int curX = itemX;
+		for (const char *c = chapterStr; *c; c++) {
+			int charIdx = (unsigned char)*c;
+			if (charIdx < numFontChars) {
+				int charWidth = menuFont->getCharWidth(charIdx);
+				if (curX >= 0 && curX + charWidth <= width && itemY >= 0 && itemY < height) {
+					menuFont->drawCharV7(renderBitmap, clipRect, curX, itemY, pitch, -1,
+					                     kStyleAlignLeft, charIdx, true, true);
+				}
+				curX += charWidth;
+			}
+		}
+	}
+
+	// Draw FINALE (chapter 16 = index 15)
+	// Position: Y = 19 + 15*10 = 169
+	int finaleY = itemBaseY + 15 * itemSpacing;
+	bool finaleSelected = (_chapterSelection == 15);
+	const char *finaleStr = "FINALE     -";
+
+	int finaleWidth = 0;
+	for (const char *c = finaleStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) finaleWidth += menuFont->getCharWidth(charIdx);
+	}
+
+	if (finaleSelected) {
+		int boxWidth = finaleWidth + 6;
+		int boxHeight = 10;
+		int boxX = selBoxX;  // Use assembly value: 20
+		int boxY = finaleY - 1;
+		byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+		if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
+			for (int px = boxX; px < boxX + boxWidth; px++) {
+				renderBitmap[boxY * pitch + px] = boxColor;
+				renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
+			}
+			for (int py = boxY; py < boxY + boxHeight; py++) {
+				renderBitmap[py * pitch + boxX] = boxColor;
+				renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
+			}
+		}
+	}
+
+	int curX = itemX;
+	for (const char *c = finaleStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			int charWidth = menuFont->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && finaleY >= 0) {
+				menuFont->drawCharV7(renderBitmap, clipRect, curX, finaleY, pitch, -1,
+				                     kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+
+	// Draw "RETURN TO PILOTS" (index 16)
+	// Position: Y = 19 + 16*10 = 179
+	int returnY = itemBaseY + 16 * itemSpacing;
+	bool returnSelected = (_chapterSelection == 16);
+	const char *returnStr = "RETURN TO PILOTS";
+
+	int returnWidth = 0;
+	for (const char *c = returnStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) returnWidth += menuFont->getCharWidth(charIdx);
+	}
+
+	if (returnSelected) {
+		int boxWidth = returnWidth + 6;
+		int boxHeight = 10;
+		int boxX = selBoxX;  // Use assembly value: 20
+		int boxY = returnY - 1;
+		byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+
+		if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
+			for (int px = boxX; px < boxX + boxWidth; px++) {
+				renderBitmap[boxY * pitch + px] = boxColor;
+				renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
+			}
+			for (int py = boxY; py < boxY + boxHeight; py++) {
+				renderBitmap[py * pitch + boxX] = boxColor;
+				renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
+			}
+		}
+	}
+
+	curX = itemX;
+	for (const char *c = returnStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			int charWidth = menuFont->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && returnY >= 0) {
+				menuFont->drawCharV7(renderBitmap, clipRect, curX, returnY, pitch, -1,
+				                     kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+
+	// === Draw preview box on the right side ===
+	// From FUN_00415CF8 lines 128-133:
+	// Outer: X=228 (0xe4), Y=73 (0x49), W=84 (0x54), H=54 (0x36), color=0xF8
+	// Inner: X=229 (0xe5), Y=74 (0x4a), W=82 (0x52), H=52 (0x34), color=4
+	drawPreviewBox(renderBitmap, pitch, width, height);
+
+	// === Draw preview thumbnail inside the box ===
+	// Shows chapter number and unlock status for currently selected chapter
+	// Only draw for chapters 0-15 (not RETURN TO PILOTS at index 16)
+	if (_chapterSelection >= 0 && _chapterSelection < 16) {
+		drawPreviewThumbnail(renderBitmap, pitch, width, height, _chapterSelection);
+	}
+
+	// === Draw status bar at bottom ===
+	// From FUN_00415CF8 lines 101-103 (unlocked chapter score display):
+	// X = 0x19 + 0x19 = 50 (but we use 23 to align with menu items)
+	// Y = 0xbe = 190
+	int statusY = 190;
+	int statusX = 23;  // Align with menu items
+
+	char statusStr[64];
+	snprintf(statusStr, sizeof(statusStr), "PILOTS: %d  SCORE: %d  RANK:", 4, _playerScore);
+
+	curX = statusX;
+	for (const char *c = statusStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numFontChars) {
+			int charWidth = menuFont->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && statusY >= 0 && statusY < height) {
+				menuFont->drawCharV7(renderBitmap, clipRect, curX, statusY, pitch, -1,
+				                     kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+}
+
+// ==================== Pilot Selection Menu (FUN_00414A41) ====================
+// Emulates FUN_00414A41 - Pilot/save selection menu
+// This appears before chapter selection. All options go to chapter selection except MAIN MENU.
 
 int InsaneRebel2::runLevelSelect() {
-	// Level selection menu loop - emulates FUN_00414A41
+	// Pilot selection menu loop - emulates FUN_00414A41
 	// Returns:
-	//   kLevelSelectPlay (1) = Play selected level
+	//   kLevelSelectPlay (1) = Go to chapter selection
 	//   kLevelSelectBack (0) = Return to main menu
 	//   kLevelSelectQuit (2) = Quit game
 
-	debug("Rebel2: Entering level selection");
+	debug("Rebel2: Entering pilot selection (FUN_00414A41)");
 
 	// Enable menu input capture via EventObserver and clear any stale events
 	_menuInputActive = true;
 	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
 
-	// Initialize level selection state
+	// Initialize pilot selection state
+	// Menu structure from FUN_00414A41:
+	// Items 0-5: Pilot slots (PILOT 1-6)
+	// Item 6: NEW PILOT
+	// Item 7: DELETE PILOT
+	// Item 8: COPY PILOT
+	// Item 9: MAIN MENU
 	_levelSelection = 0;
-	_levelItemCount = 7;  // Selectable items: 6 levels + MAIN MENU
-	_selectedLevel = 1;   // Default to level 1
+	_levelItemCount = 10;  // Selectable items: 6 pilots + 4 options
+	_selectedLevel = 1;
 	_menuRepeatDelay = 0;
-	_gameState = kStateLevelSelect;
+	_gameState = kStatePilotSelect;
 
-	// Get the SmushPlayer
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
-	// Level selection loop - we'll reuse the menu video as background
-	// In the original, this uses the same O_MENU_X.SAN videos
+	// Pilot selection uses menu video as background (320x200 mode)
 	while (!_vm->shouldQuit()) {
 		_vm->_smushVideoShouldFinish = false;
 
-		// Use a menu video as background for level selection
 		Common::String menuVideo = getRandomMenuVideo();
-		debug("Rebel2: Playing level select background: %s", menuVideo.c_str());
+		debug("Rebel2: Playing pilot select background: %s", menuVideo.c_str());
 
-		// Set video flags for menu mode
 		splayer->setCurVideoFlags(0x20);
-
-		// Play the menu video - input is processed in procPostRendering
 		splayer->play(menuVideo.c_str(), 12);
 
 		if (_vm->shouldQuit()) {
@@ -5165,26 +5967,43 @@ int InsaneRebel2::runLevelSelect() {
 			return kLevelSelectQuit;
 		}
 
-		// If video ended without selection, continue looping
 		if (!_vm->_smushVideoShouldFinish) {
 			continue;
 		}
 
 		_vm->_smushVideoShouldFinish = false;
 
-		debug("Rebel2: Level selection made: %d", _levelSelection);
+		debug("Rebel2: Pilot selection made: %d", _levelSelection);
 
-		// Process level selection
-		// Menu items:
-		// 0-5: Levels 1-6 (CHAPTER 1-6)
-		// 6: MAIN MENU (back)
+		// Process pilot selection - all options go to chapter selection except MAIN MENU
+		// Menu items (from FUN_00414A41):
+		// 0-5: Pilot slots
+		// 6: NEW PILOT
+		// 7: DELETE PILOT
+		// 8: COPY PILOT
+		// 9: MAIN MENU (back)
 		if (_levelSelection >= 0 && _levelSelection <= 5) {
-			// Level selected (0 = Level 1, 5 = Level 6)
+			// Pilot selected - go to chapter selection
 			_selectedLevel = _levelSelection + 1;
-			debug("Rebel2: Level %d selected", _selectedLevel);
+			debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
 			_menuInputActive = false;
 			return kLevelSelectPlay;
 		} else if (_levelSelection == 6) {
+			// NEW PILOT - go to chapter selection
+			debug("Rebel2: NEW PILOT selected - going to chapter selection");
+			_menuInputActive = false;
+			return kLevelSelectPlay;
+		} else if (_levelSelection == 7) {
+			// DELETE PILOT - go to chapter selection
+			debug("Rebel2: DELETE PILOT selected - going to chapter selection");
+			_menuInputActive = false;
+			return kLevelSelectPlay;
+		} else if (_levelSelection == 8) {
+			// COPY PILOT - go to chapter selection
+			debug("Rebel2: COPY PILOT selected - going to chapter selection");
+			_menuInputActive = false;
+			return kLevelSelectPlay;
+		} else if (_levelSelection == 9) {
 			// Main Menu (back)
 			debug("Rebel2: Back to main menu selected");
 			_menuInputActive = false;
@@ -5298,30 +6117,42 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	// Draw level selection menu overlay
 	// Emulates FUN_0041f5ae for level selection mode
 	//
-	// From info.md - Low Resolution Coordinate Formulas:
+	// From info.md - Low Resolution Coordinate Formulas (320x200 mode):
 	// Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
-	// Item spacing = 10 pixels, Selection box height = 10 pixels
-
-	// Level menu items - all 6 levels plus Main Menu
-	// In the original game, this would show all unlocked levels plus options
-	static const char *levelItems[] = {
-		"SELECT CHAPTER",    // Title (index 0)
-		"CHAPTER 1",         // Level 1 (index 1) - selectable
-		"CHAPTER 2",         // Level 2 (index 2) - selectable
-		"CHAPTER 3",         // Level 3 (index 3) - selectable
-		"CHAPTER 4",         // Level 4 (index 4) - selectable
-		"CHAPTER 5",         // Level 5 (index 5) - selectable
-		"CHAPTER 6",         // Level 6 (index 6) - selectable
-		"MAIN MENU"          // Back to menu (index 7) - selectable
+	// Item spacing = 10 pixels, Selection box: width = textWidth + 6, height = 10
+
+	// Frame counter for flashing selection box (emulates DAT_0047a7e4)
+	static int frameCounter = 0;
+	frameCounter++;
+
+	// Pilot selection menu items - matches original structure from FUN_00414A41:
+	// Items 0-5: Pilot save slots (PILOT 1 through PILOT 6)
+	// Item 6: NEW PILOT
+	// Item 7: DELETE PILOT
+	// Item 8: COPY PILOT
+	// Item 9: MAIN MENU
+	static const char *pilotItems[] = {
+		"SELECT PILOT",      // Title (index 0) - not selectable
+		"PILOT 1",           // Pilot slot 1 (index 1) - selectable
+		"PILOT 2",           // Pilot slot 2 (index 2) - selectable
+		"PILOT 3",           // Pilot slot 3 (index 3) - selectable
+		"PILOT 4",           // Pilot slot 4 (index 4) - selectable
+		"PILOT 5",           // Pilot slot 5 (index 5) - selectable
+		"PILOT 6",           // Pilot slot 6 (index 6) - selectable
+		"NEW PILOT",         // Create new pilot (index 7) - selectable
+		"DELETE PILOT",      // Delete pilot (index 8) - selectable
+		"COPY PILOT",        // Copy pilot (index 9) - selectable
+		"MAIN MENU"          // Back to menu (index 10) - selectable
 	};
 
-	const int numItemsTotal = 8;     // Title + 7 selectable items
-	const int numSelectableItems = 7;
+	const int numItemsTotal = 11;     // Title + 10 selectable items
+	const int numSelectableItems = 10;
 
 	// Calculate positions (low-res 320x200 mode)
-	const int centerX = width / 2;
-	const int titleY = numItemsTotal * -5 + 81;      // 81 - 15 = 66
-	const int itemBaseY = numItemsTotal * -5 + 104;  // 104 - 15 = 89
+	// Formula from FUN_0041F5AE: titleY = numItems * -5 + 81, itemBaseY = numItems * -5 + 104
+	const int centerX = width / 2;  // 160 for 320 width
+	const int titleY = numSelectableItems * -5 + 81;      // 10 * -5 + 81 = 31
+	const int itemBaseY = numSelectableItems * -5 + 104;  // 10 * -5 + 104 = 54
 	const int itemSpacing = 10;
 
 	NutRenderer *font = _smush_smalfontNut;
@@ -5364,13 +6195,17 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 
 		// If highlighted, draw selection box around text
 		if (highlight) {
-			int boxWidth = textWidth + 12;
+			// Box dimensions from FUN_0041F5AE:
+			// Width = textWidth + 6 (low-res), Height = 10
+			int boxWidth = textWidth + 6;
 			int boxHeight = 10;
 			int boxX = centerX - boxWidth / 2;
-			int boxY = y - 1;
+			int boxY = y - 1;  // 1 pixel above text (itemY - 1 = numItems * -5 + idx * 10 + 103)
 
-			// Highlight color (bright color for visibility)
-			byte highlightColor = 0xF0;
+			// Flashing highlight color (emulates original behavior)
+			// Original uses palette-relative colors -8 and -16 alternating on frameCounter & 1
+			// We use bright colors that approximate the visual effect
+			byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
 
 			// Draw box border (top, bottom, left, right edges)
 			if (boxY >= 0 && boxY < height && boxX >= 0 && boxX + boxWidth <= width) {
@@ -5401,26 +6236,27 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	};
 
 	// Draw title (not selectable)
-	drawTextCentered(levelItems[0], titleY, false);
+	drawTextCentered(pilotItems[0], titleY, false);
 
 	// Draw selectable items
 	for (int i = 0; i < numSelectableItems; i++) {
 		int itemY = itemBaseY + i * itemSpacing;
 		bool isSelected = (i == _levelSelection);
-		drawTextCentered(levelItems[i + 1], itemY, isSelected);
+		drawTextCentered(pilotItems[i + 1], itemY, isSelected);
 	}
 
-	// Draw info text at bottom if a level is selected (not "MAIN MENU")
-	if (_levelSelection < numSelectableItems - 1) {
-		// Show difficulty or other info
-		// From info.md: Difficulty at X=30, Y=180
-		const char *difficultyText = "DIFFICULTY: EASY";
+	// Draw info text at bottom if a pilot slot is selected (items 0-5)
+	// From FUN_00414A41: info is shown when local_18 < local_10 (selection < num_pilots)
+	if (_levelSelection >= 0 && _levelSelection <= 5) {
+		// Show difficulty or score info for selected pilot
+		// From info.md: Info displayed at X=30, Y=180
+		const char *pilotInfoText = "DIFFICULTY: EASY";
 		int infoY = 180;
 		int infoX = 30;
 
 		// Draw left-aligned text using drawCharV7
 		int curX = infoX;
-		for (const char *c = difficultyText; *c; c++) {
+		for (const char *c = pilotInfoText; *c; c++) {
 			int charIdx = (unsigned char)*c;
 			if (charIdx < numFontChars) {
 				int charWidth = font->getCharWidth(charIdx);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 0b8a32d2974..dca510ff89e 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -60,13 +60,13 @@ public:
 	// ======================= Menu System =======================
 	// Main game states (emulates retail state machine from FUN_004142BD)
 	enum GameState {
-		kStateIntro = 0,      // Stage 0: Intro/Credits sequence
-		kStateMainMenu = 1,   // Stage 1: Main menu (FUN_004147B2)
-		kStateLevelSelect = 2,// Stage 2: Level selection (FUN_00414A41)
-		kStateBriefing = 3,   // Stage 3: Mission briefing (FUN_00415CF8)
-		kStateGameplay = 4,   // Stage 4: Gameplay (FUN_00416787)
-		kStateCredits = 5,    // Credits sequence
-		kStateQuit = 6        // Exit game
+		kStateIntro = 0,        // Stage 0: Intro/Credits sequence
+		kStateMainMenu = 1,     // Stage 1: Main menu (FUN_004147B2)
+		kStatePilotSelect = 2,  // Stage 2: Pilot selection (FUN_00414A41)
+		kStateChapterSelect = 3,// Stage 3: Chapter selection (FUN_00415CF8)
+		kStateGameplay = 4,     // Stage 4: Gameplay (FUN_00416787)
+		kStateCredits = 5,      // Credits sequence
+		kStateQuit = 6          // Exit game
 	};
 
 	// Menu selection results (return values from FUN_004147B2)
@@ -104,8 +104,54 @@ public:
 	// Reset menu state for fresh start
 	void resetMenu();
 
-	// ================= Level Selection Menu ====================
-	// Level selection results
+	// ================= Chapter Selection Screen (FUN_00415CF8) ====================
+	// This is the actual level/chapter selection screen with preview thumbnail
+	// Distinct from pilot selection (FUN_00414A41)
+
+	enum ChapterSelectResult {
+		kChapterSelectBack = 2,   // Return to main menu (ESC or BACK selected)
+		kChapterSelectPlay = 5,   // Play selected chapter
+		kChapterSelectQuit = 0    // Quit game
+	};
+
+	int _chapterSelection;        // Current chapter selection (0-15, or 16 for BACK)
+	int _chapterItemCount;        // Number of chapter items (17: 16 chapters + BACK)
+	int _selectedChapter;         // Final selected chapter ID (0-15)
+	Common::String _passwordInput; // Current password input string (max 8 chars)
+	bool _chapterUnlocked[16];    // Which chapters are unlocked
+
+	// Run chapter selection screen - emulates FUN_00415CF8
+	int runChapterSelect();
+
+	// Draw chapter selection overlay - called during O_LEVEL.SAN playback
+	void drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+	// Process chapter select input - returns -1 (no action) or action code
+	int processChapterSelectInput();
+
+	// Draw the preview thumbnail box - emulates FUN_004292D0 calls in FUN_00415CF8
+	void drawPreviewBox(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw the preview thumbnail content - shows chapter number/status in preview area
+	void drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter);
+
+	// View offset for chapter preview scrolling (DAT_0047abe2/DAT_0047abe4)
+	int16 _previewOffsetX;   // X offset = -90 for chapter select
+	int16 _previewOffsetY;   // Y offset = chapter * -50 + 75
+
+	// Draw left-aligned menu text - emulates FUN_0041F5AE with param_4=1
+	void drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
+	                             const char *text, int x, int y, bool selected);
+
+	// Draw password input field - emulates lines 106-125 of FUN_00415CF8
+	void drawPasswordInput(byte *renderBitmap, int pitch, int width, int height);
+
+	// Draw score/time display - emulates lines 99-104 of FUN_00415CF8
+	void drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter);
+
+	// ================= Pilot Selection Menu (FUN_00414A41) ====================
+	// This is the pilot/save selection menu (separate from chapter selection)
+
 	enum LevelSelectResult {
 		kLevelSelectBack = 0,     // Return to main menu
 		kLevelSelectPlay = 1,     // Play selected level
@@ -116,13 +162,13 @@ public:
 	int _levelItemCount;          // Number of level items (levels + options)
 	int _selectedLevel;           // Final selected level ID (1-15)
 
-	// Run level selection menu - returns LevelSelectResult
+	// Run pilot selection menu - emulates FUN_00414A41
 	int runLevelSelect();
 
-	// Draw level selection overlay
+	// Draw pilot selection overlay
 	void drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height);
 
-	// Process level select input - returns -1 (no action) or action code
+	// Process pilot select input - returns -1 (no action) or action code
 	int processLevelSelectInput();
 
 	// ======================= Level Loading System =======================
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 7c51ecef7de..b87601c85b5 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2697,17 +2697,30 @@ Common::Error ScummEngine::go() {
 			}
 
 			if (menuResult == InsaneRebel2::kMenuContinue) {
-				// Continue: Show level selection menu
-				int levelResult = rebel->runLevelSelect();
+				// Continue: Show pilot selection screen (FUN_00414A41)
+				int pilotResult = rebel->runLevelSelect();
 
-				if (levelResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
+				if (pilotResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
 					break;
 				}
 
-				if (levelResult == InsaneRebel2::kLevelSelectPlay) {
-					// Play the selected level using the level loading system
-					int selectedLevel = rebel->_selectedLevel;
-					debug("ScummEngine: Starting level %d", selectedLevel);
+				if (pilotResult == InsaneRebel2::kLevelSelectBack) {
+					// Back to main menu
+					continue;
+				}
+
+				// Pilot selected: Show chapter selection screen (FUN_00415CF8)
+				int chapterResult = rebel->runChapterSelect();
+
+				if (chapterResult == InsaneRebel2::kChapterSelectQuit || shouldQuit()) {
+					break;
+				}
+
+				if (chapterResult == InsaneRebel2::kChapterSelectPlay) {
+					// Play the selected chapter using the level loading system
+					// Note: _selectedChapter is 0-based, runLevel expects 1-based
+					int selectedLevel = rebel->_selectedChapter + 1;
+					debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
 
 					// Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
 					int result = rebel->runLevel(selectedLevel);
@@ -2719,7 +2732,7 @@ Common::Error ScummEngine::go() {
 					// After level completion or game over, return to menu
 					// Could also handle kLevelNextLevel to auto-start next level
 				}
-				// If kLevelSelectBack, loop back to main menu
+				// If kChapterSelectBack, loop back to main menu
 			}
 		}
 		return Common::kNoError;


Commit: bb9375faec17a63d319dfc6bea8797877fb2680b
    https://github.com/scummvm/scummvm/commit/bb9375faec17a63d319dfc6bea8797877fb2680b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:58+02:00

Commit Message:
SCUMM: RA2: Improve font rendering

Changed paths:
  A engines/scumm/smush/smush_multi_font.cpp
  A engines/scumm/smush/smush_multi_font.h
    engines/scumm/module.mk
    engines/scumm/nut_renderer.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 318e1a99231..24650499866 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -138,6 +138,7 @@ MODULE_OBJS += \
 	smush/codec20.o \
 	smush/codec37.o \
 	smush/codec47.o \
+	smush/smush_multi_font.o \
 	smush/smush_player.o
 
 ifdef USE_ARM_SMUSH_ASM
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 1b040f6294b..fa24c7fb049 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -318,8 +318,11 @@ int NutRenderer::getCharWidth(byte c) const {
 	if (c >= 0x80 && _vm->_useCJKMode)
 		return _vm->_2byteWidth + _spacing;
 
-	if (c >= _numChars)
-		error("invalid character in NutRenderer::getCharWidth : %d (%d)", c, _numChars);
+	if (c >= _numChars) {
+		// Character not in font - return 0 width (skip it)
+		// This can happen with SMUSH fonts that have limited character sets
+		return 0;
+	}
 
 	return _chars[c].width;
 }
@@ -328,8 +331,11 @@ int NutRenderer::getCharHeight(byte c) const {
 	if (c >= 0x80 && _vm->_useCJKMode)
 		return _vm->_2byteHeight;
 
-	if (c >= _numChars)
-		error("invalid character in NutRenderer::getCharHeight : %d (%d)", c, _numChars);
+	if (c >= _numChars) {
+		// Character not in font - return default font height
+		// This can happen with SMUSH fonts that have limited character sets
+		return _fontHeight;
+	}
 
 	return _chars[c].height;
 }
@@ -377,6 +383,11 @@ void NutRenderer::drawFrame(byte *dst, int c, int x, int y, int pitch) {
 }
 
 int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr, bool hardcodedColors, bool smushColorMode) {
+	// Character not in font - skip drawing
+	// This can happen with SMUSH fonts that have limited character sets
+	if (chr >= _numChars)
+		return 0;
+
 	if (_direction < 0)
 		x -= _chars[chr].width;
 
diff --git a/engines/scumm/smush/smush_multi_font.cpp b/engines/scumm/smush/smush_multi_font.cpp
new file mode 100644
index 00000000000..7f6bbd603a7
--- /dev/null
+++ b/engines/scumm/smush/smush_multi_font.cpp
@@ -0,0 +1,124 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/scumm.h"
+
+namespace Scumm {
+
+SmushMultiFont::SmushMultiFont(ScummEngine *vm, SmushPlayer *player, bool useOriginalColors)
+	: _vm(vm), _player(player), _currentFont(0), _defaultFont(0), _hardcodedFontColors(useOriginalColors) {
+	_textRenderer = new TextRenderer_v7(vm, this);
+}
+
+SmushMultiFont::~SmushMultiFont() {
+	delete _textRenderer;
+}
+
+NutRenderer *SmushMultiFont::getFont(int id) {
+	// Delegate to SmushPlayer to get the font
+	// SmushPlayer::getFont() handles font loading and caching
+	return _player->getFont(id);
+}
+
+NutRenderer *SmushMultiFont::getCurrentFont() const {
+	// We need a const version that doesn't trigger loading
+	// For const access, use _player's cached fonts directly
+	return const_cast<SmushMultiFont*>(this)->getFont(_currentFont);
+}
+
+void SmushMultiFont::drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags) {
+	// Reset to default font before drawing
+	_currentFont = _defaultFont;
+	_textRenderer->drawString(str, buffer, clipRect, x, y, _vm->_screenWidth, col, flags);
+}
+
+void SmushMultiFont::drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags) {
+	// Reset to default font before drawing
+	_currentFont = _defaultFont;
+	_textRenderer->drawStringWrap(str, buffer, clipRect, x, y, _vm->_screenWidth, col, flags);
+}
+
+int SmushMultiFont::draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr) {
+	NutRenderer *font = getCurrentFont();
+	if (!font)
+		return 0;
+
+	// Adjust color for CMI compatibility
+	int16 adjCol = col;
+	if (_vm->_game.id == GID_CMI)
+		adjCol = 255;
+	else if (_vm->_game.id == GID_DIG && col == -1)
+		adjCol = 1;
+
+	return font->draw2byte(buffer, clipRect, x, y, pitch, adjCol, chr);
+}
+
+int SmushMultiFont::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr) {
+	NutRenderer *font = getCurrentFont();
+	if (!font)
+		return 0;
+
+	return font->drawCharV7(buffer, clipRect, x, y, pitch, col, flags, chr, _hardcodedFontColors, true);
+}
+
+int SmushMultiFont::getCharWidth(uint16 chr) const {
+	NutRenderer *font = getCurrentFont();
+	if (!font)
+		return 0;
+
+	return font->getCharWidth(chr & 0xFF);
+}
+
+int SmushMultiFont::getCharHeight(uint16 chr) const {
+	NutRenderer *font = getCurrentFont();
+	if (!font)
+		return 0;
+
+	return font->getCharHeight(chr & 0xFF);
+}
+
+int SmushMultiFont::getFontHeight() const {
+	NutRenderer *font = getCurrentFont();
+	if (!font)
+		return 0;
+
+	return font->getFontHeight();
+}
+
+int SmushMultiFont::setFont(int id) {
+	// This is called by TextRenderer_v7 when it encounters ^fXX escape codes
+	// Actually switch the current font
+	int oldFont = _currentFont;
+
+	if (id >= 0 && id < MAX_FONTS) {
+		_currentFont = id;
+		debugC(DEBUG_SMUSH, "SmushMultiFont::setFont: switching from font %d to font %d", oldFont, id);
+	} else {
+		debugC(DEBUG_SMUSH, "SmushMultiFont::setFont: invalid font id %d, keeping font %d", id, _currentFont);
+	}
+
+	return oldFont;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_multi_font.h b/engines/scumm/smush/smush_multi_font.h
new file mode 100644
index 00000000000..1dbb4c6e2e1
--- /dev/null
+++ b/engines/scumm/smush/smush_multi_font.h
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_MULTI_FONT_H
+#define SCUMM_SMUSH_MULTI_FONT_H
+
+#include "common/scummsys.h"
+#include "scumm/nut_renderer.h"
+#include "scumm/scumm.h"
+#include "scumm/string_v7.h"
+
+namespace Scumm {
+
+class SmushPlayer;
+
+/**
+ * SmushMultiFont - Multi-font renderer for SMUSH videos
+ *
+ * This class implements the GlyphRenderer_v7 interface and supports
+ * switching between multiple fonts during text rendering via the ^fXX
+ * escape sequence.
+ *
+ * The original Rebel Assault 2 (and other SCUMM v7 games) used a linked
+ * list of font structures that could be traversed when ^f escape codes
+ * were encountered. This class provides equivalent functionality by
+ * holding an array of NutRenderer pointers and switching between them.
+ */
+class SmushMultiFont : public GlyphRenderer_v7 {
+public:
+	static const int MAX_FONTS = 5;
+
+	SmushMultiFont(ScummEngine *vm, SmushPlayer *player, bool useOriginalColors);
+	~SmushMultiFont() override;
+
+	// String drawing methods
+	void drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
+	void drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
+
+	// GlyphRenderer_v7 interface
+	int draw2byte(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, uint16 chr) override;
+	int drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags, byte chr) override;
+	int getCharWidth(uint16 chr) const override;
+	int getCharHeight(uint16 chr) const override;
+	int getFontHeight() const override;
+	int setFont(int id) override;
+	bool newStyleWrapping() const override { return true; }
+
+	// Set the initial/default font
+	void setDefaultFont(int id) { _defaultFont = id; _currentFont = id; }
+
+private:
+	NutRenderer *getFont(int id);
+	NutRenderer *getCurrentFont() const;
+
+	ScummEngine *_vm;
+	SmushPlayer *_player;
+	TextRenderer_v7 *_textRenderer;
+
+	int _currentFont;
+	int _defaultFont;
+	bool _hardcodedFontColors;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 51b6eec3b4b..9468f75aa6a 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,6 +40,7 @@
 #include "scumm/smush/codec37.h"
 #include "scumm/smush/codec47.h"
 #include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
@@ -251,6 +252,7 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_sf[2] = nullptr;
 	_sf[3] = nullptr;
 	_sf[4] = nullptr;
+	_multiFont = nullptr;
 	_base = nullptr;
 	_frameBuffer = nullptr;
 	_specialBuffer = nullptr;
@@ -315,6 +317,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 SmushPlayer::~SmushPlayer() {
 	delete _IACTchannel;
 	delete _compressedFileSoundHandle;
+	delete _multiFont;
+	_multiFont = nullptr;
 	terminateAudio();
 
 	// Free any preserved frame buffer (RA2 preserves this across videos)
@@ -836,14 +840,6 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 		color = 255;
 	}
 
-	SmushFont *sf = getFont(fontId);
-	assert(sf != nullptr);
-
-	// The hack that used to be here to prevent bug #2220 is no longer necessary and
-	// has been removed. The font renderer can handle all ^codes it encounters (font
-	// changes on the fly will be ignored for Smush texts, since our code design does
-	// not permit it and the feature isn't used anyway).
-
 	if (_vm->_language == Common::HE_ISR && !(flags & kStyleAlignCenter)) {
 		flags |= kStyleAlignRight;
 		pos_x = _width - 1 - pos_x;
@@ -861,23 +857,51 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 	// bit 7 - skip ^ codes (COMI)     0x80        (should be irrelevant for Smush, we strip these commands anyway)
 	// bit 8 - no vertical fix (COMI)  0x100       (COMI handles this in the printing method, but I haven't seen a case where it is used)
 
-	if (flg & kStyleWordWrap) {
-		// COMI has to do it all a bit different, of course. SCUMM7 games immediately render the text from here and actually use the clipping data
-		// provided by the text resource. COMI does not render directly, but enqueues a blast string (which is then drawn through the usual main
-		// loop routines). During that process the rect data will get dumped and replaced with the following default values. It's hard to tell
-		// whether this is on purpose or not (the text looks not necessarily better or worse, just different), so we follow the original...
-		if (_vm->_game.id == GID_CMI) {
-			left = top = 10;
-			width = _width - 20;
-			height = _height - 20;
+	// For Rebel Assault 2, use SmushMultiFont to support inline font switching via ^fXX codes.
+	// The original game uses a linked list of fonts and switches between them mid-string.
+	// Other games (FT, DIG, CMI) only use ^f codes at the start of strings.
+	if (_vm->_game.id == GID_REBEL2) {
+		// Create multi-font renderer on first use
+		if (!_multiFont) {
+			_multiFont = new SmushMultiFont(_vm, this, true);
+		}
+		_multiFont->setDefaultFont(fontId);
+
+		if (flg & kStyleWordWrap) {
+			Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
+			_multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
+		} else {
+			Common::Rect clipRect(0, 0, _width, _height);
+			_multiFont->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
 		}
-		Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
-		sf->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
 	} else {
-		// Similar to the wrapped text, COMI will pass on rect coords here, which will later be lost. Unlike with the wrapped text, it will
-		// finally use the full screen dimenstions. SCUMM7 renders directly from here (see comment above), but also with the full screen.
-		Common::Rect clipRect(0, 0, _width, _height);
-		sf->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
+		// For other games, use single font (original behavior)
+		SmushFont *sf = getFont(fontId);
+		assert(sf != nullptr);
+
+		// The hack that used to be here to prevent bug #2220 is no longer necessary and
+		// has been removed. The font renderer can handle all ^codes it encounters (font
+		// changes on the fly will be ignored for Smush texts, since our code design does
+		// not permit it and the feature isn't used anyway).
+
+		if (flg & kStyleWordWrap) {
+			// COMI has to do it all a bit different, of course. SCUMM7 games immediately render the text from here and actually use the clipping data
+			// provided by the text resource. COMI does not render directly, but enqueues a blast string (which is then drawn through the usual main
+			// loop routines). During that process the rect data will get dumped and replaced with the following default values. It's hard to tell
+			// whether this is on purpose or not (the text looks not necessarily better or worse, just different), so we follow the original...
+			if (_vm->_game.id == GID_CMI) {
+				left = top = 10;
+				width = _width - 20;
+				height = _height - 20;
+			}
+			Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
+			sf->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
+		} else {
+			// Similar to the wrapped text, COMI will pass on rect coords here, which will later be lost. Unlike with the wrapped text, it will
+			// finally use the full screen dimenstions. SCUMM7 renders directly from here (see comment above), but also with the full screen.
+			Common::Rect clipRect(0, 0, _width, _height);
+			sf->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
+		}
 	}
 
 	free(string);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 2ab2759cc18..62b0e0c1500 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -87,6 +87,7 @@ namespace Scumm {
 
 class ScummEngine_v7;
 class SmushFont;
+class SmushMultiFont;
 class SmushMixer;
 class StringResource;
 class SmushDeltaBlocksDecoder;
@@ -97,6 +98,7 @@ class Insane;
 class SmushPlayer {
 	friend class Insane;
 	friend class InsaneRebel2;
+	friend class SmushMultiFont;
 private:
 	struct SmushAudioDispatch {
 		uint8 *headerPtr;
@@ -140,6 +142,7 @@ private:
 	int32 _shiftedDeltaPal[0x300];
 	byte _pal[0x300];
 	SmushFont *_sf[5];
+	SmushMultiFont *_multiFont;  // Multi-font renderer for inline font switching
 	StringResource *_strings;
 	SmushDeltaBlocksDecoder *_deltaBlocksCodec;
 	SmushDeltaGlyphsDecoder *_deltaGlyphsCodec;


Commit: 6239b89fbb8c306a19415629db6491809ea9bc1f
    https://github.com/scummvm/scummvm/commit/6239b89fbb8c306a19415629db6491809ea9bc1f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:59+02:00

Commit Message:
SCUMM: RA2: Improve shot rendering and terminology

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 35563e955ad..047f531e170 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -25,6 +25,7 @@
 #include "common/system.h"
 #include "common/memstream.h"
 #include "common/events.h"
+#include "common/util.h"
 
 #include "graphics/cursorman.h"
 #include "graphics/wincursor.h"
@@ -77,6 +78,16 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// For now, use CPITIMAG since the game runs at 320x200
 	_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
 	_smush_icons2Nut = nullptr;  // Not used for Rebel2
+
+	// Initialize laser texture buffer (DAT_0047fee4) from sprite 5 of CPITIMAG.NUT
+	// This is done by FUN_0040BAB0/FUN_0040BB87 in the original with sprite index 5
+	// Sprite 5 is 136x13 pixels - a wide, thin texture perfect for laser beams
+	_laserTexture.pixels = nullptr;
+	_laserTexture.width = 0;
+	_laserTexture.height = 0;
+	if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+		initLaserTexture(_smush_iconsNut, 5);
+	}
 	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
 
 	// Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
@@ -246,11 +257,41 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_corridorBottomY = 200;
 	_hitCooldown = 0;
 
+	// Initialize legacy shot system (backwards compatibility)
 	for (i = 0; i < 2; i++) {
 		_shots[i].active = false;
 		_shots[i].counter = 0;
 	}
 
+	// Initialize Handler 0x26 Turret shot system (FUN_40AD63)
+	for (i = 0; i < 2; i++) {
+		_turretShots[i].counter = 0;
+		_turretShots[i].targetX = 0;
+		_turretShots[i].targetY = 0;
+		_turretShots[i].seqNum = 0;
+	}
+	_turretShotSeqCounter = 0;
+
+	// Initialize Handler 8 Vehicle shot system (FUN_402ED0)
+	for (i = 0; i < 2; i++) {
+		_vehicleShots[i].counter = 0;
+		_vehicleShots[i].targetX = 0;
+		_vehicleShots[i].targetY = 0;
+	}
+
+	// Initialize Handler 7 Space shot system (FUN_40FADF)
+	for (i = 0; i < 2; i++) {
+		_spaceShots[i].counter = 0;
+		_spaceShots[i].targetX = 0;
+		_spaceShots[i].targetY = 0;
+		_spaceShots[i].leftGunX = 0;
+		_spaceShots[i].leftGunY = 0;
+		_spaceShots[i].rightGunX = 0;
+		_spaceShots[i].rightGunY = 0;
+		_spaceShots[i].variant = 0;
+	}
+	_spaceShotDirection = 0;
+
 	for (i = 0; i < 16; i++) {
 		_rebelEmbeddedHud[i].pixels = nullptr;
 		_rebelEmbeddedHud[i].width = 0;
@@ -388,6 +429,9 @@ InsaneRebel2::~InsaneRebel2() {
 	delete _hudOverlayNut;
 	delete _hudOverlay2Nut;
 
+	// Clean up laser texture buffer (DAT_0047fee4)
+	freeLaserTexture();
+
 	// Clean up embedded HUD overlays
 	for (int i = 0; i < 16; i++) {
 		free(_rebelEmbeddedHud[i].pixels);
@@ -910,7 +954,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		}
 
 	} else if (par1 == 7) {
-		// Opcode 7: Sprite/HUD control for Handler 7 (space flight levels like Level 3)
+		// Opcode 7: Sprite/HUD control for Handler 7 (third-person ship levels like Level 3)
 		// par2 = control type (41 = sprite selection?)
 		// par3 = usually 0
 		// par4 = sprite/slot ID (0 or 5 seen in Level 3)
@@ -1110,12 +1154,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	// Opcode 6: Level setup / mode switch
 	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
 	//
-	// For Handler 8 (third-person vehicle) - FUN_00401234 case 4:
+	// For Handler 8 (third-person on foot) - FUN_00401234 case 4:
 	// - par3 sets ship level mode (DAT_0043e000)
 	// - par4 == 1 triggers status bar display and state reset
 	// - Updates ship position based on mouse input
 	//
-	// For Handler 0x26/0x19 (turret/space):
+	// For Handler 0x26/0x19 (turret/FPS):
 	// - Same par4 == 1 behavior
 	// - Different view offset calculations
 
@@ -1131,7 +1175,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		debug("Rebel2 Opcode 6: Setting handler=%d", par2);
 	}
 
-	// Handler 8 specific logic (third-person vehicle) - FUN_00401234 case 4
+	// Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4
 	if (_rebelHandler == 8) {
 		// Set ship level mode (DAT_0043e000 = par3)
 		_shipLevelMode = par3;
@@ -1263,7 +1307,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
-	// Handler 7 specific logic (space flight) - FUN_0040d836 / FUN_0040c3cc
+	// Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
 	// Used for Level 3 and similar space combat levels
 	if (_rebelHandler == 7) {
 		// Set control mode (DAT_004437c0 = par3 in FUN_40C3CC case 4)
@@ -1700,7 +1744,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	int64 startPos = b.pos();
 	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
 
-	// ===== Handler 7: FLY NUT Loading (Space Flight) =====
+	// ===== Handler 7: FLY NUT Loading (Third-Person Ship) =====
 	// FUN_0040c3cc case 6: par4 determines FLY sprite slot
 	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
 	if (isHandler7FLY && remaining >= 14) {
@@ -2899,15 +2943,97 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 	}
 }
 
+// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
+// For now, use a reasonable default based on observed values
+int16 InsaneRebel2::getShotMaxDuration() {
+	// The original uses: DAT_0047e0f0 + DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
+	// Typical values observed: 2-5 frames
+	return 4;
+}
+
+// Dispatcher - calls appropriate spawn function based on current handler
 void InsaneRebel2::spawnShot(int x, int y) {
-	// Find free shot slot (2 slots total)
+	switch (_rebelHandler) {
+	case 0x26:  // Turret
+		spawnTurretShot(x, y);
+		break;
+	case 8:     // Vehicle
+		spawnVehicleShot(x, y);
+		break;
+	case 7:     // Space combat
+		spawnSpaceShot(x, y);
+		break;
+	default:
+		// Legacy fallback
+		for (int i = 0; i < 2; i++) {
+			if (!_shots[i].active) {
+				_shots[i].active = true;
+				_shots[i].counter = getShotMaxDuration();
+				_shots[i].x = x + _viewX;
+				_shots[i].y = y + _viewY;
+				break;
+			}
+		}
+		break;
+	}
+}
+
+// Handler 0x26 Turret shot spawn (based on FUN_4089AB lines 127-140)
+void InsaneRebel2::spawnTurretShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
-		if (!_shots[i].active) {
-			_shots[i].active = true;
-			_shots[i].counter = 4; // Lasts 4 frames
-			_shots[i].x = x + _viewX;
-			_shots[i].y = y + _viewY;
+		if (_turretShots[i].counter == 0) {
+			// Play sound based on level type
+			// FUN_0041189e(-(ushort)(_rebelLevelType == 5) & 7, i + 1, 0x7f, 0, 0)
+			// Sound ID: 0 for type 5, 7 for others
+			// TODO: Play laser sound via audio system
+
+			_turretShots[i].counter = getShotMaxDuration();
+			_turretShots[i].seqNum = _turretShotSeqCounter;
+			_turretShotSeqCounter++;
+			_turretShots[i].targetX = x + _viewX;  // DAT_0044366e in original
+			_turretShots[i].targetY = y + _viewY;  // DAT_00443670 in original
+			break;
+		}
+	}
+}
+
+// Handler 8 Vehicle shot spawn (based on FUN_401CCF)
+void InsaneRebel2::spawnVehicleShot(int x, int y) {
+	for (int i = 0; i < 2; i++) {
+		if (_vehicleShots[i].counter == 0) {
 			// TODO: Play laser sound
+			_vehicleShots[i].counter = getShotMaxDuration();
+			_vehicleShots[i].targetX = x + _viewX;
+			_vehicleShots[i].targetY = y + _viewY;
+			break;
+		}
+	}
+}
+
+// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
+void InsaneRebel2::spawnSpaceShot(int x, int y) {
+	for (int i = 0; i < 2; i++) {
+		if (_spaceShots[i].counter == 0) {
+			// Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
+			// TODO: Play laser sound
+
+			_spaceShots[i].counter = getShotMaxDuration();
+			_spaceShots[i].targetX = x;  // Screen coords
+			_spaceShots[i].targetY = y;
+
+			// Calculate gun positions from direction-based lookup tables
+			// In the original, these come from tables indexed by _shipDirectionIndex
+			// DAT_004437c2/DAT_00443808 for left gun, DAT_0044384e/DAT_00443894 for right gun
+			// For now, use simplified positions relative to ship
+			int shipScreenX = 160 + ((_shipPosX - 160) >> 3);
+			int shipScreenY = 105 + ((_shipPosY - 40) >> 2);
+
+			// Gun offsets (approximate from disassembly)
+			_spaceShots[i].leftGunX = shipScreenX - 28;
+			_spaceShots[i].leftGunY = shipScreenY + 10;
+			_spaceShots[i].rightGunX = shipScreenX + 28;
+			_spaceShots[i].rightGunY = shipScreenY + 10;
+			_spaceShots[i].variant = _spaceShotDirection;
 			break;
 		}
 	}
@@ -3077,61 +3203,148 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 }
 
 
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx) {
-	if (!nut || spriteIdx >= nut->getNumChars()) return;
+// Initialize laser texture buffer from NUT sprite (FUN_0040BAB0)
+// This pre-renders a sprite into a buffer that drawLaserBeam uses
+void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
+	if (!nut || spriteIdx >= nut->getNumChars())
+		return;
 
-	// Follow original FUN_0040BBF6 math precisely
-	int texW = nut->getCharWidth(spriteIdx);
-	int texH = nut->getCharHeight(spriteIdx);
-	int param_11 = (_rebelLevelType <= 1) ? 12 : 25;
+	// Get sprite dimensions (FUN_0040BAB0 lines 13-14)
+	int16 texWidth = nut->getCharWidth(spriteIdx);
+	int16 texHeight = nut->getCharHeight(spriteIdx);
 
-	if (maxProgress == 0) maxProgress = 1;
-	int sVar7 = (param_11 * progress * 16) / maxProgress;
+	// Clamp height to max 15 pixels (FUN_0040BAB0 lines 15-17)
+	if (texHeight > 15) {
+		texHeight = 15;
+	}
 
-	int dx = x1 - x0;
-	int dy = y1 - y0;
-	int sVar6 = ((dx) * (param_11 + 1)) / param_11;
-	int sVar1 = ((dy) * (param_11 + 1)) / param_11;
+	// Free existing texture if any (FUN_0040BAB0 lines 18-20)
+	freeLaserTexture();
 
-	int sVar4 = (sVar6 + x0) - (sVar6 * 16) / (sVar7 + 16);
-	int sVar5 = (sVar1 + y0) - (sVar1 * 16) / (sVar7 + 16);
-	int sVar6_end = (sVar6 + x0) - (sVar6 * 16) / (param_9 + sVar7 + 16);
-	int sVar7_end = (sVar1 + y0) - (sVar1 * 16) / (param_9 + sVar7 + 16);
+	// Allocate new buffer (FUN_0040BAB0 line 21)
+	_laserTexture.width = texWidth;
+	_laserTexture.height = texHeight;
+	_laserTexture.pixels = (byte *)calloc(texWidth * texHeight, 1);
 
-	const byte *srcBase = nut->getCharData(spriteIdx);
-	if (!srcBase || texW <= 0 || texH <= 0) return;
+	if (!_laserTexture.pixels)
+		return;
 
-	int iVar2 = abs(sVar5 - sVar7_end);
-	int iVar3 = abs(sVar4 - sVar6_end);
+	// Render sprite into buffer (FUN_0040BAB0 lines 23-24)
+	// We copy the sprite data directly since it's already in the right format
+	const byte *srcData = nut->getCharData(spriteIdx);
+	if (srcData) {
+		int srcHeight = nut->getCharHeight(spriteIdx);
+		int copyHeight = MIN(texHeight, (int16)srcHeight);
+		memcpy(_laserTexture.pixels, srcData, texWidth * copyHeight);
+	}
+
+	debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
+}
 
+// Free laser texture buffer (FUN_0040BBD1)
+void InsaneRebel2::freeLaserTexture() {
+	free(_laserTexture.pixels);
+	_laserTexture.pixels = nullptr;
+	_laserTexture.width = 0;
+	_laserTexture.height = 0;
+}
+
+// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
+// This is a direct port of the assembly function
+//
+// Parameters (matching FUN_0040bbf6):
+//   dst, pitch, width, height: destination buffer info
+//   gunX, gunY (param_3, param_4): gun/start position
+//   targetX, targetY (param_5, param_6): target/end position
+//   animFrame (param_7): current animation frame (shot counter)
+//   maxFrames (param_8): max animation frames (shot duration)
+//   widthScale (param_9): width scaling factor for perspective
+//   heightScale (param_10): height/thickness multiplier
+//   thickness (param_11): base line thickness
+void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
+                                  int16 gunX, int16 gunY, int16 targetX, int16 targetY,
+                                  int16 animFrame, int16 maxFrames,
+                                  int16 widthScale, int16 heightScale, int16 thickness) {
+	// Check if laser texture is initialized
+	if (!_laserTexture.pixels || _laserTexture.width <= 0 || _laserTexture.height <= 0)
+		return;
+
+	int16 texW = _laserTexture.width;
+	int16 texH = _laserTexture.height;
+	byte *texPixels = _laserTexture.pixels;
+
+	// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
+	if (maxFrames == 0) maxFrames = 1;
+	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
+
+	// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
+	int16 dx = targetX - gunX;
+	int16 dy = targetY - gunY;
+	int16 sVar6 = (int16)(((int)dx * (thickness + 1)) / (int)thickness);
+	int16 sVar1 = (int16)(((int)dy * (thickness + 1)) / (int)thickness);
+
+	// FUN_0040BBF6 lines 26-29: Calculate adjusted start and end points
+	// Start point (closer to gun, adjusted by animation progress)
+	int16 startX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (sVar7 + 16));
+	int16 startY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (sVar7 + 16));
+	// End point (closer to target)
+	int16 endX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (widthScale + sVar7 + 16));
+	int16 endY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (widthScale + sVar7 + 16));
+
+	// FUN_0040BBF6 line 30: Get texture pixel pointer
+	byte *local_28 = texPixels;
+
+	// FUN_0040BBF6 lines 31-32: Calculate abs differences (FUN_004356e4 = abs)
+	int iVar2 = abs(startY - endY);  // |dy| of beam
+	int iVar3 = abs(startX - endX);  // |dx| of beam
+
+	// FUN_0040BBF6 line 33: Choose rendering path based on beam orientation
 	if (iVar2 < iVar3) {
-		// Column major case (wide)
-		iVar2 = abs(sVar4 - sVar6_end);
-		long long temp = (long long)iVar2 * (long long)texH * (long long)thickness;
-		// sVar1calc = (temp >> 3) / texW + 2
-		int sVar1calc = (int)((temp >> 3) / texW) + 2;
-		int local_24 = -sVar1calc;
-		int sVar8 = sVar1calc >> 1;
-		const byte *local_28 = srcBase;
-		for (int local_2c = 0; local_2c < sVar1calc; local_2c++) {
-			drawTexturedSegment(dst, pitch, width, height, sVar4, (sVar5 - sVar8) + local_2c,
-						 sVar6_end, (sVar7_end - sVar8) + local_2c, texW, local_28);
-			for (local_24 = texH + local_24; local_24 > 0; local_24 -= sVar1calc) {
+		// Mostly horizontal beam - draw vertical scanlines
+		// FUN_0040BBF6 lines 34-37
+		iVar2 = abs(startX - endX);
+		int temp = iVar2 * texH * heightScale;
+		int16 numLines = (int16)((temp >> 3) / texW) + 2;
+		int16 local_24 = -numLines;
+		int16 halfLines = numLines >> 1;
+
+		// FUN_0040BBF6 lines 39-46: Draw parallel lines
+		for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
+			// Draw one textured segment (vertical offset for this scanline)
+			drawTexturedSegment(dst, pitch, width, height,
+			                    startX, (startY - halfLines) + lineIdx,
+			                    endX, (endY - halfLines) + lineIdx,
+			                    texW, local_28);
+
+			// Advance texture pointer (step through texture rows)
+			for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
 				local_28 += texW;
 			}
 		}
 	} else {
-		// Row major case (tall)
-		iVar2 = abs(sVar5 - sVar7_end);
-		int local_30 = (int)(((long long)iVar2 * (long long)texH) / texW) + 2;
-		if (texH < local_30) local_30 = texH;
-		int local_24 = -local_30;
-		const byte *local_28 = srcBase;
-		int sVar1_half = local_30 >> 1;
-		for (int local_2c = 0; local_2c < local_30; local_2c++) {
-			drawTexturedSegment(dst, pitch, width, height, (sVar4 - sVar1_half) + local_2c, sVar5,
-						 (sVar6_end - sVar1_half) + local_2c, sVar7_end, texW, local_28);
-			for (local_24 = texH + local_24; local_24 > 0; local_24 -= local_30) {
+		// Mostly vertical beam - draw horizontal scanlines
+		// FUN_0040BBF6 lines 54-56
+		iVar2 = abs(startY - endY);
+		int16 numLines = (int16)((iVar2 * texH) / texW) + 2;
+		int16 local_24 = -numLines;
+
+		// FUN_0040BBF6 lines 58-60: Clamp to texture height
+		if (texH < numLines) {
+			numLines = texH;
+		}
+
+		int16 halfLines = numLines >> 1;
+
+		// FUN_0040BBF6 lines 61-68: Draw parallel lines
+		for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
+			// Draw one textured segment (horizontal offset for this scanline)
+			drawTexturedSegment(dst, pitch, width, height,
+			                    (startX - halfLines) + lineIdx, startY,
+			                    (endX - halfLines) + lineIdx, endY,
+			                    texW, local_28);
+
+			// Advance texture pointer
+			for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
 				local_28 += texW;
 			}
 		}
@@ -3870,7 +4083,7 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 }
 
 void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 7 Ship Rendering (Space Flight - FLY sprites)
+	// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
 	// Uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
 
 	if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
@@ -3917,7 +4130,7 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 }
 
 void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 8 Ship Rendering (Third-Person Vehicle - POV sprites)
+	// Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
 	// Uses _shipSprite (POV001) with position-based offset
 
 	if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
@@ -4240,58 +4453,200 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
 	}
 }
 
+// Dispatcher - calls appropriate render function based on current handler
 void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
-	// Draw laser shot beams and impacts
+	switch (_rebelHandler) {
+	case 0x26:  // Turret - FUN_40AD63
+		renderTurretLaserShots(renderBitmap, pitch, width, height);
+		break;
+	case 8:     // Vehicle - FUN_402ED0
+		renderVehicleLaserShots(renderBitmap, pitch, width, height);
+		break;
+	case 7:     // Space combat - FUN_40FADF
+		renderSpaceLaserShots(renderBitmap, pitch, width, height);
+		break;
+	default:
+		// No laser rendering for other handlers
+		break;
+	}
+}
 
-	if (!_smush_iconsNut || _smush_iconsNut->getNumChars() <= 0)
-		return;
+// Handler 0x26 Turret laser rendering (FUN_40AD63)
+// Gun positions depend on _rebelLevelType (DAT_004436de)
+void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+	// Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
 
-	// Gun positions (approximate for turret mode)
-	const int GUN_LEFT_X = 10, GUN_LEFT_Y = 190;
-	const int GUN_RIGHT_X = 310, GUN_RIGHT_Y = 190;
+	int16 maxDuration = getShotMaxDuration();
 
 	for (int i = 0; i < 2; i++) {
-		if (!_shots[i].active)
+		if (_turretShots[i].counter <= 0)
 			continue;
 
-		if (_shots[i].counter <= 0) {
-			_shots[i].active = false;
-			continue;
-		}
+		// Calculate sound panning from target X position (FUN_004262f0 call)
+		// sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
+		int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - _viewX - 160)) / 2;
+		pan = CLIP<int16>(pan, -127, 127);
+		// TODO: Apply panning to sound channel i+1
+
+		int16 targetX = _turretShots[i].targetX;
+		int16 targetY = _turretShots[i].targetY;
+		int16 progress = maxDuration - _turretShots[i].counter;
+
+		// Gun positions based on level type (from FUN_40AD63 switch statement)
+		// Parameters from assembly: widthScale=0xC(12), heightScale=8, thickness=0xC(12)
+		switch (_rebelLevelType) {
+		case 1:
+			// Type 1: 3 guns (triple cannon configuration)
+			// Gun 1: (0x136, 0xaa) = (310, 170)
+			// Gun 2: (0xa0, 0x17c) = (160, 380)
+			// Gun 3: (0x0a, 0xaa) = (10, 170)
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				310 + _viewX, 170 + _viewY, targetX, targetY,
+				progress, maxDuration, 12, 8, 12);
 
-		int maxProgress = 4;
-		int progress = maxProgress - _shots[i].counter;
+			drawLaserBeam(renderBitmap, pitch, width, height,
+				160 + _viewX, 380 + _viewY, targetX, targetY,
+				progress, maxDuration, 12, 8, 12);
 
-		// Draw beams based on level type
-		if (_rebelLevelType <= 1) {
-			// Type 1: 3 beams (Right, Middle, Left)
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				310 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
-				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+				10 + _viewX, 170 + _viewY, targetX, targetY,
+				progress, maxDuration, 12, 8, 12);
+			break;
 
+		case 2:
+		case 5:
+			// Type 2/5: 2 guns (wing cannons)
+			// Left: (0x6e, 0xe6) = (110, 230)
+			// Right: (0xd2, 0xe6) = (210, 230)
+			// Assembly uses widthScale=0x19(25) for these types
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				160 + _viewX, 380 + _viewY, _shots[i].x, _shots[i].y,
-				progress, maxProgress, 5, 8, _smush_iconsNut, 0);
+				110 + _viewX, 230 + _viewY, targetX, targetY,
+				progress, maxDuration, 25, 8, 25);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				10 + _viewX, 170 + _viewY, _shots[i].x, _shots[i].y,
-				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
-		} else {
-			// Other levels: 2 beams
+				210 + _viewX, 230 + _viewY, targetX, targetY,
+				progress, maxDuration, 25, 8, 25);
+			break;
+
+		case 6:
+			// Type 6: 2 guns (offscreen - cinematic effect)
+			// Gun 1: (-100, 0)
+			// Gun 2: (0, 0)
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				GUN_LEFT_X + _viewX, GUN_LEFT_Y + _viewY, _shots[i].x, _shots[i].y,
-				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+				-100 + _viewX, 0 + _viewY, targetX, targetY,
+				progress, maxDuration, 25, 8, 25);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
-				GUN_RIGHT_X + _viewX, GUN_RIGHT_Y + _viewY, _shots[i].x, _shots[i].y,
-				progress, maxProgress, 8, 12, _smush_iconsNut, 0);
+				0 + _viewX, 0 + _viewY, targetX, targetY,
+				progress, maxDuration, 25, 8, 25);
+			break;
+
+		default:
+			// Default: 2 guns with alternating pattern based on shot sequence
+			// When seqNum & 1 == 0: Left (10, 50), Right (310, 130)
+			// When seqNum & 1 == 1: Left (310, 50), Right (10, 130)
+			if ((_turretShots[i].seqNum & 1) == 0) {
+				drawLaserBeam(renderBitmap, pitch, width, height,
+					10 + _viewX, 50 + _viewY, targetX, targetY,
+					progress, maxDuration, 25, 8, 25);
+
+				drawLaserBeam(renderBitmap, pitch, width, height,
+					310 + _viewX, 130 + _viewY, targetX, targetY,
+					progress, maxDuration, 25, 8, 25);
+			} else {
+				drawLaserBeam(renderBitmap, pitch, width, height,
+					310 + _viewX, 50 + _viewY, targetX, targetY,
+					progress, maxDuration, 25, 8, 25);
+
+				drawLaserBeam(renderBitmap, pitch, width, height,
+					10 + _viewX, 130 + _viewY, targetX, targetY,
+					progress, maxDuration, 25, 8, 25);
+			}
+			break;
 		}
 
-		// Impact flash
-		renderNutSprite(renderBitmap, pitch, width, height,
-			_shots[i].x - 7, _shots[i].y - 7, _smush_iconsNut, 0);
+		_turretShots[i].counter--;
+	}
+}
+
+// Handler 8 Vehicle laser rendering (FUN_402ED0)
+// Gun position derived from ship position
+void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+	// No NUT check needed - uses pre-initialized _laserTexture
+
+	int16 maxDuration = getShotMaxDuration();
+
+	for (int i = 0; i < 2; i++) {
+		if (_vehicleShots[i].counter <= 0)
+			continue;
+
+		// Calculate sound panning
+		int16 pan = ((2 - _vehicleShots[i].counter) * (_vehicleShots[i].targetX - _viewX - 160)) / 2;
+		pan = CLIP<int16>(pan, -127, 127);
+		// TODO: Apply panning
+
+		// Calculate gun position from ship position (FUN_402ED0 lines 26-36)
+		// Low-res formula:
+		// shipScreenY = ((shipPosY - 0x28) >> 2) + 0x69 = ((shipPosY - 40) >> 2) + 105
+		// shipScreenX = ((shipPosX - 0xa0) >> 3) + 0xa0 = ((shipPosX - 160) >> 3) + 160
+		// gunY = ((shipPosY - 0x28) >> 2) + 0x82 = shipScreenY + 25
+		// gunX = ((shipPosX - 0xa0) >> 3) + 0xa7 = shipScreenX + 7
+		int16 shipScreenX = ((_shipPosX - 160) >> 3) + 160;
+		int16 shipScreenY = ((_shipPosY - 40) >> 2) + 105;
+		int16 gunX = shipScreenX + 7;
+		int16 gunY = shipScreenY + 25;
+
+		int16 targetX = _vehicleShots[i].targetX;
+		int16 targetY = _vehicleShots[i].targetY;
+		int16 progress = maxDuration - _vehicleShots[i].counter;
+
+		// Single beam from gun to target
+		// From FUN_402ED0: widthScale=0x14(20), heightScale=8, thickness=4
+		drawLaserBeam(renderBitmap, pitch, width, height,
+			gunX + _viewX, gunY + _viewY, targetX, targetY,
+			progress, maxDuration, 20, 8, 4);
+
+		_vehicleShots[i].counter--;
+	}
+}
+
+// Handler 7 Space combat laser rendering (FUN_40FADF)
+// Dual beams from left and right gun positions
+void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height) {
+	// No NUT check needed - uses pre-initialized _laserTexture
+
+	int16 maxDuration = getShotMaxDuration();
+
+	for (int i = 0; i < 2; i++) {
+		if (_spaceShots[i].counter <= 0)
+			continue;
 
-		_shots[i].counter--;
+		// Calculate sound panning
+		int16 pan = ((_spaceShots[i].targetX - 160) * (2 - _spaceShots[i].counter)) / 2;
+		pan = CLIP<int16>(pan, -127, 127);
+		// TODO: Apply panning
+
+		int16 targetX = _spaceShots[i].targetX;
+		int16 targetY = _spaceShots[i].targetY;
+		int16 leftGunX = _spaceShots[i].leftGunX;
+		int16 leftGunY = _spaceShots[i].leftGunY;
+		int16 rightGunX = _spaceShots[i].rightGunX;
+		int16 rightGunY = _spaceShots[i].rightGunY;
+		int16 progress = maxDuration - _spaceShots[i].counter;
+
+		// Draw dual beams
+		// From FUN_40FADF: widthScale=0xC(12), heightScale=4, thickness=6
+		// Left gun beam
+		drawLaserBeam(renderBitmap, pitch, width, height,
+			leftGunX, leftGunY, targetX, targetY,
+			progress, maxDuration, 12, 4, 6);
+
+		// Right gun beam
+		drawLaserBeam(renderBitmap, pitch, width, height,
+			rightGunX, rightGunY, targetX, targetY,
+			progress, maxDuration, 12, 4, 6);
+
+		_spaceShots[i].counter--;
 	}
 }
 
@@ -4321,11 +4676,11 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 
 	int reticleIndex;
 	switch (_rebelHandler) {
-	case 7:    // Space flight
-	case 0x19: // Mixed/turret
+	case 7:    // Third-Person Ship
+	case 0x19: // FPS/Mixed
 		reticleIndex = 47;
 		break;
-	case 0x26: { // Full turret - animated crosshair
+	case 0x26: { // Turret/Cockpit - animated crosshair
 		static int turretAnimCounter = 0;
 		turretAnimCounter++;
 
@@ -4338,7 +4693,7 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		}
 		break;
 	}
-	case 8:    // Ground vehicle
+	case 8:    // Third-Person On Foot
 	default:
 		reticleIndex = 46;
 		break;
@@ -6402,7 +6757,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 
 	case 2:
 		// Level 2: Has cutscene first, then multiple parts
-		// Level 2 uses Handler 8 (third-person vehicle mode) - set before gameplay
+		// Level 2 uses Handler 8 (third-person on foot mode) - set before gameplay
 		_rebelHandler = 8;
 		// First play the cutscene
 		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
@@ -6452,8 +6807,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		break;
 
 	case 3:
-		// Level 3: Two gameplay phases (space flight)
-		// Level 3 uses Handler 7 (space flight mode) - FUN_0040d836/FUN_0040c3cc
+		// Level 3: Two gameplay phases (third-person ship)
+		// Level 3 uses Handler 7 (third-person ship mode) - FUN_0040d836/FUN_0040c3cc
 		_rebelHandler = 7;
 		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index dca510ff89e..708141e2db7 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -330,10 +330,10 @@ public:
 	void renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
 								int statusBarY, int32 curFrame);
 
-	// Draw Handler 7 ship sprite (space flight - FLY sprites)
+	// Draw Handler 7 ship sprite (third-person ship - FLY sprites)
 	void renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height);
 
-	// Draw Handler 8 ship sprite (third-person vehicle - POV sprites)
+	// Draw Handler 8 ship sprite (third-person on foot - POV sprites)
 	void renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height);
 
 	// Draw fallback ship using embedded HUD frame
@@ -357,10 +357,10 @@ public:
 	// ======================= Opcode 6 Helper Functions =======================
 	// Handler-specific setup extracted from iactRebel2Opcode6
 
-	// Handler 8 (third-person vehicle) setup - FUN_00401234 case 4
+	// Handler 8 (third-person on foot) setup - FUN_00401234 case 4
 	void opcode6Handler8Setup(int16 par3, int16 par4);
 
-	// Handler 7 (space flight) setup - FUN_0040c3cc case 4
+	// Handler 7 (third-person ship) setup - FUN_0040c3cc case 4
 	void opcode6Handler7Setup(int16 par3, int16 par4);
 
 	// Calculate view offsets based on level type (lines 182-213)
@@ -396,7 +396,26 @@ public:
 	// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
 	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
 
-	void drawLaserBeam(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, int progress, int maxProgress, int thickness, int param_9, NutRenderer *nut, int spriteIdx);
+	// ======================= Laser Texture Buffer (DAT_0047fee4) =======================
+	// Pre-rendered laser texture used by FUN_0040BBF6
+	// Initialized from CPITIMAG.NUT sprite 0 via initLaserTexture() (FUN_0040BAB0)
+	struct LaserTexture {
+		byte *pixels;      // Pixel data (rendered from NUT sprite)
+		int16 width;       // Texture width
+		int16 height;      // Texture height (clamped to max 15)
+	};
+	LaserTexture _laserTexture;  // DAT_0047fee4
+
+	// Initialize laser texture from NUT sprite (FUN_0040BAB0)
+	void initLaserTexture(NutRenderer *nut, int spriteIdx);
+	void freeLaserTexture();
+
+	// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
+	// Parameters match the original assembly function
+	void drawLaserBeam(byte *dst, int pitch, int width, int height,
+	                   int16 gunX, int16 gunY, int16 targetX, int16 targetY,
+	                   int16 animFrame, int16 maxFrames,
+	                   int16 widthScale, int16 heightScale, int16 thickness);
 	void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
 
 	struct enemy {
@@ -418,10 +437,10 @@ public:
 	
 	// Current handler type for Rebel Assault 2 (determines crosshair sprite)
 	// Handler 0: Background only
-	// Handler 7: Space flight - uses crosshair sprite 0x2F (47)
-	// Handler 8: Third-person vehicle - uses crosshair sprite 0x2E (46)  
-	// Handler 0x19: Mixed/turret view - uses crosshair sprite 0x2F (47)
-	// Handler 0x26: Full turret - crosshair varies by level type
+	// Handler 7: Third-Person Ship - uses crosshair sprite 0x2F (47)
+	// Handler 8: Third-Person On Foot - uses crosshair sprite 0x2E (46)  
+	// Handler 0x19: FPS/Mixed view - uses crosshair sprite 0x2F (47)
+	// Handler 0x26: Turret/Cockpit - crosshair varies by level type
 	int _rebelHandler;
 	
 	// Level type from IACT opcode 6 par3 (corresponds to DAT_004436de)
@@ -577,16 +596,85 @@ public:
 	int _rebelLastCounter;         // Mirrors DAT_0047ab90 (last updated counter)
 
 
+	// ======================= Handler 0x26 Turret Shot System =======================
+	// Based on FUN_40AD63 disassembly - Turret laser rendering
+	// DAT_0044367a[2]: Shot duration counter (0=inactive)
+	// DAT_0044367e[2]: Target X position
+	// DAT_00443682[2]: Target Y position
+	// DAT_0044368a[2]: Shot sequence number (for alternating gun pattern)
+	// DAT_004436de: Level type (determines gun positions) - already have as _rebelLevelType
+
+	struct TurretShot {
+		int16 counter;     // DAT_0044367a[i] - duration counter, 0=inactive
+		int16 targetX;     // DAT_0044367e[i] - target X position
+		int16 targetY;     // DAT_00443682[i] - target Y position
+		int16 seqNum;      // DAT_0044368a[i] - shot sequence (for alternating)
+	};
+	TurretShot _turretShots[2];
+	int16 _turretShotSeqCounter;  // DAT_0047fe94 - global sequence counter
+
+	// ======================= Handler 8 Vehicle Shot System =======================
+	// Based on FUN_402ED0 disassembly - Vehicle laser rendering
+	// DAT_0043e00a[2]: Shot duration counter
+	// DAT_0043e00e[2]: Target X position
+	// DAT_0043e012[2]: Target Y position
+	// Gun position derived from ship position (_shipPosX, _shipPosY)
+
+	struct VehicleShot {
+		int16 counter;     // DAT_0043e00a[i] - duration counter, 0=inactive
+		int16 targetX;     // DAT_0043e00e[i] - target X position
+		int16 targetY;     // DAT_0043e012[i] - target Y position
+	};
+	VehicleShot _vehicleShots[2];
+
+	// ======================= Handler 7 Third-Person Ship Shot System =======================
+	// Based on FUN_40FADF disassembly - Third-Person Ship laser rendering
+	// DAT_00443750[2]: Shot duration counter
+	// DAT_00443754[2]: Target X position
+	// DAT_00443758[2]: Target Y position
+	// DAT_0044375c[2]: Left gun X position
+	// DAT_00443760[2]: Left gun Y position
+	// DAT_00443764[2]: Right gun X position
+	// DAT_00443768[2]: Right gun Y position
+	// DAT_0044376c[2]: Shot variant
+
+	struct SpaceShot {
+		int16 counter;     // DAT_00443750[i] - duration counter, 0=inactive
+		int16 targetX;     // DAT_00443754[i] - target X position
+		int16 targetY;     // DAT_00443758[i] - target Y position
+		int16 leftGunX;    // DAT_0044375c[i] - left gun X
+		int16 leftGunY;    // DAT_00443760[i] - left gun Y
+		int16 rightGunX;   // DAT_00443764[i] - right gun X
+		int16 rightGunY;   // DAT_00443768[i] - right gun Y
+		int16 variant;     // DAT_0044376c[i] - shot variant
+	};
+	SpaceShot _spaceShots[2];
+	int16 _spaceShotDirection;  // DAT_0044374e - ship direction for gun lookup
+
+	// Legacy struct for backwards compatibility
 	struct Shot {
 		bool active;
 		int counter;
 		int x, y;       // Target position
 	};
 	Shot _shots[2];
-	void spawnShot(int x, int y);
+
+	// Handler-specific shot spawning
+	void spawnTurretShot(int x, int y);    // Handler 0x26
+	void spawnVehicleShot(int x, int y);   // Handler 8
+	void spawnSpaceShot(int x, int y);     // Handler 7
+	void spawnShot(int x, int y);          // Dispatcher based on current handler
+
+	// Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF)
+	void renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height);
+	void renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height);
+	void renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height);
+
+	// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
+	int16 getShotMaxDuration();
 
 	// ======================= Handler 8 Ship System =======================
-	// For third-person vehicle missions (Levels 2, 3), the player controls a ship
+	// For third-person on foot missions (Level 2, 11), the player controls Rookie One
 	// that can turn in different directions. The ship sprite comes from
 	// NUT files loaded via IACT opcode 8.
 	//
@@ -633,7 +721,7 @@ public:
 	// Gradually transitions by ±10 per frame for smooth animation
 	int16 _movementRangeLimit;       // DAT_0047e034
 
-	// Control mode for Handler 7 (space flight) - DAT_004437c0
+	// Control mode for Handler 7 (third-person ship) - DAT_004437c0
 	// Set by IACT opcode 6 par3 when handler is 7
 	// Determines shooting capability and collision zone type:
 	//   Mode 0: Flight/avoid mode - no shooting, uses secondary zones (sub-opcode 0x0E)
@@ -661,7 +749,7 @@ public:
 	NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
 
 	// ======================= Handler 7 FLY Ship System =======================
-	// For space flight missions (Level 3, etc.), Handler 7 uses a 35-frame
+	// For third-person ship missions (Level 3, etc.), Handler 7 uses a 35-frame
 	// direction-based ship sprite system. The ship visually banks and turns
 	// based on player position using a 5x7 grid of sprites.
 	//


Commit: 0dad1b0270bc409eb70e5c88b428e7b2e14b7c46
    https://github.com/scummvm/scummvm/commit/0dad1b0270bc409eb70e5c88b428e7b2e14b7c46
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:59+02:00

Commit Message:
SCUMM: RA2: Improve level selection screen

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 047f531e170..45e9a0a4ef3 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -365,8 +365,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_chapterItemCount = 17;       // 16 chapters + BACK
 	_selectedChapter = 0;         // Default selected chapter
 	_passwordInput = "";          // No password input
+
+	// Debug flag to unlock all chapters for testing
+	// Based on original debug mode (DAT_0047ab34 == 'd') from FUN_00415CF8
+	// Set to true to bypass normal unlock progression
+	_debugUnlockAll = true;  // TODO: Set to false for release, or read from ScummVM config
+
 	for (i = 0; i < 16; i++) {
-		_chapterUnlocked[i] = (i == 0);  // Only chapter 1 unlocked initially
+		// If debug unlock is enabled, unlock all chapters
+		// Otherwise only chapter 1 (index 0) is unlocked initially
+		// Original: pilotData->scores[chapter] < 0xFF means unlocked
+		_chapterUnlocked[i] = _debugUnlockAll || (i == 0);
 	}
 
 	// Initialize preview offset for chapter selection (FUN_00425170)
@@ -3675,8 +3684,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		CursorMan.showMouse(true);
 
 		// Fill screen with BLACK background
-		// The original uses O_LEVEL.SAN (640x400) which has a black background.
-		// For 320x200 mode, we just fill with black (color index 0).
+		// The original game plays O_LEVEL.SAN (640x400) which has a dark/black background
+		// with chapter preview thumbnails embedded. For 320x200 mode, we fill with black
+		// to match the visual appearance and overlay the UI elements.
+		// Note: O_MENU_X.SAN (320x200) doesn't contain chapter-specific preview images,
+		// those are only in O_LEVEL.SAN (640x400). We use a styled placeholder instead.
+		// From FUN_00415CF8 line 57: FUN_0041f4d0(s_OPEN_O_LEVEL_SAN_004806e0,8,0xffff,0xffff,0);
 		for (int y = 0; y < height; y++) {
 			memset(renderBitmap + y * pitch, 0, width);
 		}
@@ -4953,6 +4966,18 @@ void InsaneRebel2::resetMenu() {
 	_menuRepeatDelay = 0;
 }
 
+// Unlock all chapters for testing
+// Emulates the debug mode from original FUN_00415CF8 (lines 60-71)
+// where DAT_0047ab34 == 'd' enables level unlock via special codes
+void InsaneRebel2::unlockAllChapters() {
+	debug("Rebel2: Unlocking all chapters for testing");
+	_debugUnlockAll = true;
+	for (int i = 0; i < 16; i++) {
+		_chapterUnlocked[i] = true;
+		_levelUnlocked[i] = true;
+	}
+}
+
 Common::String InsaneRebel2::getRandomMenuVideo() {
 	// Emulates FUN_0041FDC8 - selects random menu video variant
 	//
@@ -5792,35 +5817,42 @@ void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int
 	}
 }
 
-// Draw preview thumbnail content - emulates FUN_00429b40 scaled preview draw
-// Shows chapter number and unlock status inside the preview box
+// Draw preview thumbnail content - emulates FUN_00428a10 + FUN_00429b40
+// Based on FUN_00415CF8 assembly analysis:
+//
+// The original uses O_LEVEL.SAN (640x400) with chapter previews stacked vertically.
+// Video offset (FUN_00425170) shifts which preview is visible:
+//   X offset = -90 (0xffa6)
+//   Y offset = chapter * -50 + 75
+//
+// For 320x200 mode, O_MENU_X.SAN doesn't contain chapter-specific preview images.
+// Those are only in O_LEVEL.SAN (640x400). We display a styled placeholder instead
+// with the chapter number and visual styling to match the original UI appearance.
 void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
-	// Preview area coordinates (inside the inner border)
-	// Inner box: X=230 (229+1), Y=75 (74+1), W=80 (82-2), H=50 (52-2)
-	int previewX = 230;
-	int previewY = 75;
-	int previewW = 80;
-	int previewH = 50;
-
-	// Update preview offset based on chapter selection (FUN_00425170)
-	// Y offset = chapter * -50 + 75
-	_previewOffsetY = chapter * -50 + 75;
-
-	// Determine fill color based on chapter state
-	// Unlocked: dark blue (0x10), Locked: dark gray (0x08)
-	byte fillColor = (_chapterUnlocked[chapter]) ? 0x10 : 0x08;
-
-	// Fill the preview area with background color
-	for (int py = previewY; py < previewY + previewH && py < height; py++) {
-		if (py >= 0) {
-			for (int px = previewX; px < previewX + previewW && px < width; px++) {
-				if (px >= 0)
-					renderBitmap[py * pitch + px] = fillColor;
-			}
+	// Preview destination area coordinates (inside the inner border)
+	// From assembly: Inner box at X=230, Y=75, W=80, H=50
+	const int destX = 230;
+	const int destY = 75;
+	const int thumbW = 80;  // 0x50
+	const int thumbH = 50;  // 0x32
+
+	// Fill preview area with a dark blue gradient background
+	// This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
+	for (int py = 0; py < thumbH; py++) {
+		int dy = destY + py;
+		if (dy < 0 || dy >= height) continue;
+
+		// Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
+		byte bgColor = 0x10 + (py * 8 / thumbH);
+
+		for (int px = 0; px < thumbW; px++) {
+			int dx = destX + px;
+			if (dx < 0 || dx >= width) continue;
+			renderBitmap[dy * pitch + dx] = bgColor;
 		}
 	}
 
-	// Draw chapter number in the center of the preview area
+	// Draw chapter number overlay in the center of the preview
 	NutRenderer *font = _smush_smalfontNut;
 	if (!font) return;
 
@@ -5828,7 +5860,7 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 	if (chapter < 15) {
 		snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
 	} else {
-		snprintf(chapterStr, sizeof(chapterStr), "FIN");  // FINALE
+		snprintf(chapterStr, sizeof(chapterStr), "FINALE");
 	}
 
 	// Calculate text width for centering
@@ -5842,13 +5874,27 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 	}
 
 	// Center the text in the preview area
-	int textX = previewX + (previewW - textWidth) / 2;
-	int textY = previewY + previewH / 2 - 4;  // Center vertically (approx)
+	int textX = destX + (thumbW - textWidth) / 2;
+	int textY = destY + thumbH / 2 - 4;
 
 	Common::Rect clipRect(0, 0, width, height);
 
-	// Draw the chapter text
-	int curX = textX;
+	// Draw text shadow (offset by 1,1)
+	int curX = textX + 1;
+	for (const char *c = chapterStr; *c; c++) {
+		int charIdx = (unsigned char)*c;
+		if (charIdx < numChars) {
+			int charWidth = font->getCharWidth(charIdx);
+			if (curX >= 0 && curX + charWidth <= width && textY + 1 >= 0 && textY + 1 < height) {
+				font->drawCharV7(renderBitmap, clipRect, curX, textY + 1, pitch, 0,
+				                 kStyleAlignLeft, charIdx, true, true);
+			}
+			curX += charWidth;
+		}
+	}
+
+	// Draw main text (bright)
+	curX = textX;
 	for (const char *c = chapterStr; *c; c++) {
 		int charIdx = (unsigned char)*c;
 		if (charIdx < numChars) {
@@ -5861,18 +5907,28 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 		}
 	}
 
-	// Draw lock icon for locked chapters (simple "X" pattern)
+	// Draw lock icon for locked chapters
 	if (!_chapterUnlocked[chapter]) {
-		byte lockColor = 0xF8;  // Bright red/orange
-		int lockX = previewX + previewW - 15;
-		int lockY = previewY + 5;
-
-		// Draw small X to indicate locked
-		for (int i = 0; i < 8; i++) {
-			if (lockX + i < width && lockY + i < height)
-				renderBitmap[(lockY + i) * pitch + lockX + i] = lockColor;
-			if (lockX + 7 - i < width && lockY + i < height)
-				renderBitmap[(lockY + i) * pitch + lockX + 7 - i] = lockColor;
+		byte lockColor = 0xF8;
+		int lockX = destX + thumbW - 15;
+		int lockY = destY + 5;
+
+		// Draw padlock shape
+		for (int i = 2; i < 6; i++) {
+			if (lockX + i < width && lockY < height && lockY >= 0)
+				renderBitmap[lockY * pitch + lockX + i] = lockColor;
+		}
+		for (int i = 1; i < 4; i++) {
+			if (lockX + 2 < width && lockY + i < height && lockY + i >= 0)
+				renderBitmap[(lockY + i) * pitch + lockX + 2] = lockColor;
+			if (lockX + 5 < width && lockY + i < height && lockY + i >= 0)
+				renderBitmap[(lockY + i) * pitch + lockX + 5] = lockColor;
+		}
+		for (int y = 0; y < 4; y++) {
+			for (int x = 1; x < 7; x++) {
+				if (lockX + x < width && lockY + 4 + y < height && lockY + 4 + y >= 0)
+					renderBitmap[(lockY + 4 + y) * pitch + lockX + x] = lockColor;
+			}
 		}
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 708141e2db7..b094063852a 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -119,6 +119,11 @@ public:
 	int _selectedChapter;         // Final selected chapter ID (0-15)
 	Common::String _passwordInput; // Current password input string (max 8 chars)
 	bool _chapterUnlocked[16];    // Which chapters are unlocked
+	bool _debugUnlockAll;         // Debug flag to unlock all chapters for testing
+
+	// Unlock all chapters for testing (debug mode)
+	// Call this to enable access to all chapters without passwords
+	void unlockAllChapters();
 
 	// Run chapter selection screen - emulates FUN_00415CF8
 	int runChapterSelect();


Commit: 7e80992c722615733fbe3b42d989fb150cb69d44
    https://github.com/scummvm/scummvm/commit/7e80992c722615733fbe3b42d989fb150cb69d44
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:08:59+02:00

Commit Message:
SCUMM: RA2: Check whether the player can shoot

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 45e9a0a4ef3..792a1171225 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -670,9 +670,11 @@ int32 InsaneRebel2::processMouse() {
 	}
 
 	// Left button: Trigger shot on button press (not hold)
-	if (leftPressed && !leftWasPressed) {
+	// From FUN_0040d836 (Handler 7) line 141: shots only spawn when DAT_004437c0 == 2
+	// From FUN_00401CCF (Handler 8) line 82-84: mode 4 disables shooting
+	if (leftPressed && !leftWasPressed && isShootingAllowed()) {
 		Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
-		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d", 
+		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
 			mousePos.x, mousePos.y, _enemies.size());
 
 		// Spawn visual shot immediately
@@ -763,6 +765,27 @@ void InsaneRebel2::clearBit(int n) {
 	_iactBits[n] = 0;
 }
 
+// Check if shooting is allowed based on current handler and control mode
+// From FUN_0040d836 (Handler 7): shooting only allowed when DAT_004437c0 == 2
+// From FUN_00401CCF (Handler 8): mode 4/5 disable shooting
+bool InsaneRebel2::isShootingAllowed() {
+	// Handler 7 (Third-Person Ship): Only mode 2 allows shooting
+	// FUN_0040d836 line 141: if (DAT_004437c0 == 2) { /* spawn shots, draw crosshair */ }
+	if (_rebelHandler == 7) {
+		return (_flyControlMode == 2);
+	}
+
+	// Handler 8 (Third-Person On Foot): Modes 4/5 disable shooting
+	// FUN_00401CCF line 82-84: if (DAT_0043e000 == 4) { param_5 = 0; }
+	// Mode 5: Ship not rendered (cutscene)
+	if (_rebelHandler == 8) {
+		return (_shipLevelMode != 4 && _shipLevelMode != 5);
+	}
+
+	// Handler 0x26 (Turret) and 0x19 (FPS): Always allow shooting when active
+	return (_rebelHandler != 0);
+}
+
 void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 	// Rebel Assault 2 uses SKIP chunks to conditionally skip the next FOBJ/PSAD chunk.
 	// The SKIP chunk contains one or two object IDs. If the bit for the object is set
@@ -4664,6 +4687,12 @@ void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int widt
 }
 
 void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
+	// From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
+	// Don't draw crosshair when shooting is disabled (flight-only segments)
+	if (!isShootingAllowed()) {
+		return;
+	}
+
 	// Update target lock state and draw crosshair/reticle
 
 	// Target lock detection (DAT_00443676 equivalent)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index b094063852a..04aa51d7ab2 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -500,6 +500,7 @@ public:
 
 	int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
 	void clearBit(int n);
+	bool isShootingAllowed();  // FUN_0040d836/FUN_00401CCF: Check control mode before spawning shots
 
 	struct Explosion {
 		int x, y;


Commit: 07f891177c39f012c9ee3fa28662c3293fae9f37
    https://github.com/scummvm/scummvm/commit/07f891177c39f012c9ee3fa28662c3293fae9f37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:00+02:00

Commit Message:
SCUMM: RA2: Animate level 2 player correctly

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/nut_renderer.cpp
    engines/scumm/nut_renderer.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 792a1171225..16d00b0dbd9 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -138,6 +138,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelDamageLevel = 0;
 	_rebelFlightDir = 0;
 	_rebelControlMode = 0;
+	_rebelInputThrottle = 0;
 	_rebelViewOffsetX = 0;
 	_rebelViewOffsetY = 0;
 	_rebelViewOffset2X = 0;
@@ -656,17 +657,28 @@ int32 InsaneRebel2::processMouse() {
 	_prevMouseButtons = currentButtons;
 
 	// Update _rebelControlMode (DAT_0047a7e4) for Handler 25 covered/uncovered toggle:
-	// The original uses input throttling (every 5 frames) to prevent oscillation.
-	// We use edge detection instead - only set the control mode flag on button DOWN.
-	// - Bit 1 (value 2): Right mouse button just pressed - triggers cover entry
-	// - Bit 0 (value 1): Left mouse button just pressed - triggers cover exit
-	// When covered, any NEW button press exits cover. When uncovered, right button enters cover.
-	_rebelControlMode = 0;
-	if (rightPressed && !rightWasPressed) {
-		_rebelControlMode |= 2;  // Right button just pressed
-	}
-	if (leftPressed && !leftWasPressed) {
-		_rebelControlMode |= 1;  // Left button just pressed
+	// Use "sticky" flags - set on button press, cleared by IACT handler after consumption.
+	// This ensures button presses aren't missed due to timing.
+	//
+	// For Handler 25: use edge detection with sticky flags
+	if (_rebelHandler == 25) {
+		// Only SET flags on button press (edge), don't clear them here
+		// The IACT handler will clear them after processing
+		if (rightPressed && !rightWasPressed) {
+			_rebelControlMode |= 2;  // Right button pressed - sticky
+		}
+		if (leftPressed && !leftWasPressed) {
+			_rebelControlMode |= 1;  // Left button pressed - sticky
+		}
+	} else {
+		// Other handlers: use simple hold state
+		_rebelControlMode = 0;
+		if (rightPressed) {
+			_rebelControlMode |= 2;
+		}
+		if (leftPressed) {
+			_rebelControlMode |= 1;
+		}
 	}
 
 	// Left button: Trigger shot on button press (not hold)
@@ -768,6 +780,7 @@ void InsaneRebel2::clearBit(int n) {
 // Check if shooting is allowed based on current handler and control mode
 // From FUN_0040d836 (Handler 7): shooting only allowed when DAT_004437c0 == 2
 // From FUN_00401CCF (Handler 8): mode 4/5 disable shooting
+// From FUN_41DB5E (Handler 25): only shoot when fully uncovered (DAT_0045790a == 0)
 bool InsaneRebel2::isShootingAllowed() {
 	// Handler 7 (Third-Person Ship): Only mode 2 allows shooting
 	// FUN_0040d836 line 141: if (DAT_004437c0 == 2) { /* spawn shots, draw crosshair */ }
@@ -782,7 +795,14 @@ bool InsaneRebel2::isShootingAllowed() {
 		return (_shipLevelMode != 4 && _shipLevelMode != 5);
 	}
 
-	// Handler 0x26 (Turret) and 0x19 (FPS): Always allow shooting when active
+	// Handler 25 (0x19): Only allow shooting when fully uncovered
+	// FUN_41DB5E lines 170-171: if (((param_5 & 1) != 0) && (DAT_0045790a == 0))
+	// _rebelDamageLevel = DAT_0045790a (cover transition counter, 0 = uncovered, 5 = covered)
+	if (_rebelHandler == 25) {
+		return (_rebelDamageLevel == 0);
+	}
+
+	// Handler 0x26 (Turret): Always allow shooting when active
 	return (_rebelHandler != 0);
 }
 
@@ -838,31 +858,80 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	}
 
 	// For Handler 25 (Level 2 speeder bike), draw the corridor overlay BEFORE FOBJ decoding.
-	// The corridor overlay (par4=4, _rebelEmbeddedHud[4]) shows the covered/uncovered state
-	// and must be drawn BEFORE enemy FOBJ frames so enemies appear ON TOP of the corridor.
-	// Position is (_rebelViewOffsetX, _rebelViewOffsetY) from FUN_0041cadb line 216.
+	// The corridor overlay (par3=4 -> _rebelEmbeddedHud[4]) is DAT_00482268, a 350x230 buffer.
+	// From FUN_0041cadb line 216: FUN_00428a10(param_1,0,DAT_0045790c,DAT_0045790e,DAT_00482268)
+	// It's drawn at (DAT_0045790c, DAT_0045790e) which are _rebelViewOffsetX/Y.
+	//
+	// For Mode 1: DAT_0045790c = damageLevel * -5 - 14, range -39 (covered) to -14 (uncovered)
+	//
+	// From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
+	// This creates a "scrolling window" effect as the character enters/exits cover.
 	if (_rebelHandler == 25 && renderBitmap) {
 		EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
 		if (corridorOverlay.valid && corridorOverlay.pixels) {
-			int overlayX = _rebelViewOffsetX;
-			int overlayY = _rebelViewOffsetY;
 			int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
 			int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
 
-			for (int y = 0; y < corridorOverlay.height && (overlayY + y) < bufHeight; y++) {
-				for (int x = 0; x < corridorOverlay.width && (overlayX + x) < pitch; x++) {
-					byte pixel = corridorOverlay.pixels[y * corridorOverlay.width + x];
-					if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
-						int destX = overlayX + x;
-						int destY = overlayY + y;
-						if (destX >= 0 && destY >= 0) {
-							renderBitmap[destY * pitch + destX] = pixel;
+			// Calculate source offset and destination position (FUN_00428a10 lines 31-43)
+			// If position is negative, skip source pixels and draw at screen edge
+			// Use _rebelViewOffsetX which is DAT_0045790c (damageLevel * -5 - 14)
+			int srcOffsetX = 0;
+			int srcOffsetY = 0;
+			int destX = _rebelViewOffsetX;   // DAT_0045790c
+			int destY = _rebelViewOffsetY;   // DAT_0045790e
+			int drawWidth = corridorOverlay.width;
+			int drawHeight = corridorOverlay.height;
+
+			// Handle negative X position: skip source pixels, draw at X=0
+			if (destX < 0) {
+				srcOffsetX = -destX;
+				drawWidth -= srcOffsetX;
+				destX = 0;
+			}
+			// Handle negative Y position: skip source pixels, draw at Y=0
+			if (destY < 0) {
+				srcOffsetY = -destY;
+				drawHeight -= srcOffsetY;
+				destY = 0;
+			}
+
+			// Clip to screen bounds
+			if (destX + drawWidth > pitch) {
+				drawWidth = pitch - destX;
+			}
+			if (destY + drawHeight > bufHeight) {
+				drawHeight = bufHeight - destY;
+			}
+
+			// Bounds check: ensure srcOffsetX doesn't exceed image width
+			if (srcOffsetX >= corridorOverlay.width) {
+				debug("Rebel2 procPreRendering: srcOffsetX (%d) >= image width (%d), skipping", srcOffsetX, corridorOverlay.width);
+			}
+			// Bounds check: ensure we have valid draw dimensions
+			else if (drawWidth > 0 && drawHeight > 0) {
+				// Additional safety: clamp to source image bounds
+				int maxDrawWidth = corridorOverlay.width - srcOffsetX;
+				int maxDrawHeight = corridorOverlay.height - srcOffsetY;
+				if (drawWidth > maxDrawWidth) drawWidth = maxDrawWidth;
+				if (drawHeight > maxDrawHeight) drawHeight = maxDrawHeight;
+
+				if (drawWidth > 0 && drawHeight > 0) {
+					for (int y = 0; y < drawHeight; y++) {
+						for (int x = 0; x < drawWidth; x++) {
+							int srcIdx = (srcOffsetY + y) * corridorOverlay.width + (srcOffsetX + x);
+							byte pixel = corridorOverlay.pixels[srcIdx];
+							if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
+								renderBitmap[(destY + y) * pitch + (destX + x)] = pixel;
+							}
 						}
 					}
 				}
 			}
-			debug("Rebel2 procPreRendering: Corridor overlay at (%d,%d) size(%d,%d)",
-				overlayX, overlayY, corridorOverlay.width, corridorOverlay.height);
+
+			debug("Rebel2 procPreRendering: Corridor overlay viewOff=(%d,%d) damageLevel=%d autopilot=%d srcOff=(%d,%d) dest=(%d,%d) draw=(%d,%d) imgSize=(%d,%d)",
+				_rebelViewOffsetX, _rebelViewOffsetY, _rebelDamageLevel, _rebelAutopilot,
+				srcOffsetX, srcOffsetY, destX, destY, drawWidth, drawHeight,
+				corridorOverlay.width, corridorOverlay.height);
 		}
 	}
 }
@@ -1458,8 +1527,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	// Handler 25 (0x19) specific logic (mixed mode - speeder bike)
 	// Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
 	if (_rebelHandler == 25) {
-		// If par4 == 1, enable status bar and reset state (lines 114-121)
-		if (par4 == 1) {
+		// Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code)
+		// The stream position should be at offset 8 after par4 was read
+		// From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
+		int16 par5 = 0;
+		if (b.pos() + 2 <= b.size()) {
+			int64 savedPos = b.pos();
+			par5 = b.readSint16LE();
+			b.seek(savedPos);  // Don't consume the stream
+		}
+
+		// If par5 == 1, enable status bar and reset state (lines 114-121)
+		// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
+		if (par5 == 1) {
 			_rebelStatusBarSprite = 5;
 			// Reset link tables (DAT_0045797c through DAT_0045917c)
 			for (int i = 0; i < 512; i++) {
@@ -1467,28 +1547,44 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 				_rebelLinks[i][1] = 0;
 				_rebelLinks[i][2] = 0;
 			}
-			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset");
+			// Initialize to covered state - player starts behind cover
+			// This ensures the corridor overlay shows the "covered" position initially
+			_rebelAutopilot = 1;    // DAT_00457904 = 1 (covered)
+			_rebelDamageLevel = 5;  // DAT_0045790a = 5 (fully in cover)
+			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED");
 		}
 
-		// Set sprite mode (DAT_00457900 = par4) - controls which GRD sprite to render
-		// NOTE: Original code uses local_14[3] which is par4, NOT par3!
+		// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
+		// From FUN_0041cadb line 122: DAT_00457900 = local_14[3];
+		// In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4
 		// Mode 1: Uncovered, shooting position - sprite on left
 		// Mode 2: Covered, vertical shift
 		// Mode 3: Transition between covered/uncovered - sprite position depends on direction
 		// Mode 4: Alternative uncovered position - sprite on right
-		_grdSpriteMode = par4;
+		_grdSpriteMode = par4;  // local_14[3] maps to par4 (offset 6-7)
+
+		debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
+			par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
 
 		// Autopilot logic (lines 123-146)
+		// From original FUN_0041cadb - NO damageLevel check, toggle happens immediately
+		// The damage level counter provides the smooth visual transition
 		if (!_rebelInvulnerable) {
 			if (_rebelAutopilot == 0) {
+				// Uncovered: RIGHT button enters cover
 				if ((_rebelControlMode & 2) != 0) {
 					_rebelAutopilot = 1;
+					debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
 				}
 			} else {
+				// Covered: ANY button exits cover
 				if (_rebelControlMode != 0) {
 					_rebelAutopilot = 0;
+					debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
 				}
 			}
+			// Clear control mode after processing (sticky flags consumed)
+			_rebelControlMode = 0;
 		} else {
 			// Invulnerable mode: random autopilot changes
 			if (_rebelAutopilot == 0) {
@@ -1504,15 +1600,23 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 
 		// Update damage level counter (lines 147-154)
+		// This provides the smooth transition animation between covered/uncovered states
+		int prevDamageLevel = _rebelDamageLevel;
 		if (_rebelAutopilot == 0) {
+			// Uncovered: decrement damage level towards 0
 			if (_rebelDamageLevel > 0) {
 				_rebelDamageLevel--;
 			}
 		} else {
+			// Covered: increment damage level towards 5
 			if (_rebelDamageLevel < 5) {
 				_rebelDamageLevel++;
 			}
 		}
+		if (_rebelDamageLevel != prevDamageLevel) {
+			debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
+				prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
+		}
 
 		// Flight direction logic for mode 3 (lines 155-177)
 		if (_grdSpriteMode == 3) {
@@ -1575,10 +1679,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			_rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel);      // DAT_00457910
 			_rebelViewOffsetY = 0;
 			_rebelViewOffset2Y = 0;
+		} else {
+			// Mode 0 or unknown: use Mode 1 defaults as fallback
+			_rebelViewMode1 = 0x0e;
+			_rebelViewMode2 = 0;
+			_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
+			_rebelViewOffset2X = _rebelDamageLevel * -22;
+			_rebelViewOffsetY = 0;
+			_rebelViewOffset2Y = 0;
+			debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
 		}
 
-		debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d viewOff=(%d,%d) spritePos=(%d,%d)",
-			_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir,
+		debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
+			_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
 			_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
 
 		return;
@@ -3572,6 +3685,12 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
 }
 
 void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
+	renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
+}
+
+// Render NUT sprite with optional horizontal mirroring
+// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
+void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
 	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
 
 	int w = nut->getCharWidth(spriteIdx);
@@ -3606,12 +3725,19 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
 
 	if (drawW <= 0 || drawH <= 0) return;
 
-	// Draw loop
+	// Draw loop - with optional horizontal mirroring
 	for (int iy = 0; iy < drawH; iy++) {
-		const byte *s = src + (srcOffsetY + iy) * w + srcOffsetX;
+		const byte *s = src + (srcOffsetY + iy) * w;
 		byte *d = dst + (drawY + iy) * pitch + drawX;
 		for (int ix = 0; ix < drawW; ix++) {
-			byte px = s[ix];
+			int srcX;
+			if (mirror) {
+				// When mirrored, read from the opposite side of the sprite
+				srcX = (w - 1) - (srcOffsetX + ix);
+			} else {
+				srcX = srcOffsetX + ix;
+			}
+			byte px = s[srcX];
 			if (px != 231 && px != 0) { // Check both 0 and 231 (0xE7) for transparency
 				d[ix] = px;
 			}
@@ -4239,27 +4365,26 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 	if (!_grd001Sprite && !_grd002Sprite)
 		return;
 
+	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
+	// From FUN_0041db5e line 260: _DAT_00482236 = 0xb4 during gameplay
+	// The status bar occupies Y=180-200, and sprites must not render there
+	const int clipHeight = 180;  // Status bar boundary (0xb4)
+	int renderHeight = MIN(height, clipHeight);
+
 	// Base position calculation based on FUN_0041db5e disassembly:
 	// The original reads sprite internal offsets from the NUT data at +0x12 (X) and +0x14 (Y).
-	// Since NutRenderer doesn't expose these, we use the standard positioning formula:
-	// - X: center (160) + mode-based offset
-	// - Y: positioned above status bar (status bar starts at ~Y=180)
+	// Position calculation from FUN_0041db5e assembly:
+	//
+	// GRD001 (line 206): FUN_004236e0(..., DAT_00457910, DAT_00457912, ...)
+	//   - Raw offsets passed directly, render function applies sprite internal offsets
 	//
-	// From the HUD positioning formula (lines 3583-3584):
-	//   Y = 182 - (mouseOffset >> 4) - height - spriteOffsetY
-	// This means sprites are bottom-aligned relative to Y=182.
+	// GRD002 (lines 238-247): Position uses sprite header offsets:
+	//   - X = DAT_00457910 + sprite_internal_x_offset (NUT header +0x12)
+	//   - Y = sprite_internal_y_offset (NUT header +0x14) + DAT_00457912
 	//
-	// For Handler 25, the sprite position offsets (DAT_00457910, DAT_00457912) are added.
-	const int baseX = 160;
-	// Base Y is calculated to keep sprite above status bar (Y=180)
-	// We'll position the center around Y=90 (middle of playable area)
-	const int baseY = 90;
-
-	// Calculate actual ship position from base + offset
-	// _rebelViewOffset2X = DAT_00457910 (sprite X offset from mode/damage)
-	// _rebelViewOffset2Y = DAT_00457912 (sprite Y offset from mode/damage)
-	int shipX = baseX + _rebelViewOffset2X;
-	int shipY = baseY + _rebelViewOffset2Y;
+	// Since NutRenderer doesn't expose internal offsets, we simulate them:
+	// - GRD001: Draw at raw offset position (the sprite likely has built-in positioning)
+	// - GRD002: Add base position to simulate internal offsets for character sprite
 
 	// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
 	// Mode 1, 2, 3, 4 all potentially draw _grd001Sprite
@@ -4284,17 +4409,21 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		}
 
 		if (shouldDraw) {
+			// GRD001 is drawn at raw (DAT_00457910, DAT_00457912) per assembly line 206
+			// The render function applies the sprite's internal positioning
 			// Use sprite 0 (primary frame)
 			int spriteW = _grd001Sprite->getCharWidth(0);
 			int spriteH = _grd001Sprite->getCharHeight(0);
-			// Center the sprite at the calculated position
-			int drawX = shipX - spriteW / 2;
-			int drawY = shipY - spriteH / 2;
 
-			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd001Sprite, 0);
+			// Draw at raw offset position - the sprite is designed to be positioned
+			// at these coordinates (the NUT's internal offsets handle actual placement)
+			int drawX = _rebelViewOffset2X;
+			int drawY = _rebelViewOffset2Y;
 
-			debug("Rebel2 Handler25: GRD001 at (%d,%d) base(%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
-				drawX, drawY, baseX, baseY, _rebelViewOffset2X, _rebelViewOffset2Y,
+			renderNutSprite(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd001Sprite, 0);
+
+			debug("Rebel2 Handler25: GRD001 at (%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
+				drawX, drawY, _rebelViewOffset2X, _rebelViewOffset2Y,
 				spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
 		}
 	}
@@ -4311,14 +4440,49 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		int spriteIdx;
 		int numSprites = _grd002Sprite->getNumChars();
 
+		// Determine if we should mirror the sprite (from FUN_41DB5E lines 231-235)
+		// Mirror when: direction != 0 AND damage == 0 (fully uncovered, facing right)
+		bool shouldMirror = (_rebelFlightDir != 0) && (_rebelDamageLevel == 0);
+
 		if (_rebelDamageLevel == 0) {
 			// Uncovered state: use aiming-based sprite selection (5-14)
-			// For now, use center position (xZone=2, yZone=0) -> 0*5+2+5 = 7
-			// TODO: Calculate actual zone from crosshair position
-			int xZone = 2;  // Center horizontal (0-4)
-			int yZone = 0;  // Top vertical (0-1)
+			// Calculate zones from crosshair position relative to playable area
+			// From FUN_41DB5E lines 155-164
+			//
+			// The playable area bounds are defined by corridor boundaries.
+			// xZone = 0-4 (left to right), yZone = 0-1 (top to bottom)
+			// Default to center if bounds not set
+			int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
+			int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
+			int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
+			int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
+
+			// Get crosshair position (using mouse position scaled to game coords)
+			int16 crosshairX = _vm->_mouse.x;
+			int16 crosshairY = _vm->_mouse.y;
+			if (_player && _player->_width > 320) {
+				crosshairX = (crosshairX * 320) / _player->_width;
+				crosshairY = (crosshairY * 200) / _player->_height;
+			}
+
+			// Calculate zone widths
+			int areaWidth = areaRight - areaLeft;
+			int areaHeight = areaBottom - areaTop;
+			int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80;  // Divide into ~4 zones
+			int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;     // Divide into 2 zones
+
+			// Calculate xZone (0-4) and yZone (0-1) from crosshair position
+			int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (crosshairX - areaLeft)) / zoneWidth : 2;
+			int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (crosshairY - areaTop)) / zoneHeight : 0;
+
+			// Clamp to valid ranges
+			if (xZone < 0) xZone = 0;
+			if (xZone > 4) xZone = 4;
+			if (yZone < 0) yZone = 0;
+			if (yZone > 1) yZone = 1;
 
-			// Direction-based mirroring (line 161-162)
+			// Direction-based sprite flip logic (line 161-162 in decompiled)
+			// if (DAT_00457902 == (uVar7 & 1)) { local_58 = 4 - local_58; }
 			if (_rebelFlightDir == (yZone & 1)) {
 				xZone = 4 - xZone;
 			}
@@ -4326,11 +4490,15 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 			spriteIdx = yZone * 5 + xZone + 5;
 		} else {
 			// Transitioning/covered state: use direction-based sprite
+			// From FUN_41DB5E lines 166-168:
+			// sVar8 = ((-(ushort)(DAT_00457902 == 0) & 0xffec) + 0x19) - DAT_0045790a
+			// direction == 0: 5 - damage
+			// direction != 0: 25 - damage
 			if (_rebelFlightDir == 0) {
-				// Direction 0: sprites 0-5 (covered left)
+				// Direction 0: sprites 0-5 (transition left)
 				spriteIdx = 5 - _rebelDamageLevel;
 			} else {
-				// Direction 1: sprites 20-25 (covered right)
+				// Direction 1: sprites 20-25 (transition right)
 				spriteIdx = 25 - _rebelDamageLevel;
 			}
 		}
@@ -4342,15 +4510,43 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
 		int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
 
-		// Position offset from GRD002 sprite header (lines 238-243 in disasm)
-		// This adds an offset from the sprite's internal positioning data
-		int drawX = shipX - spriteW / 2;
-		int drawY = shipY - spriteH / 2;
+		// Position calculation from FUN_41DB5E lines 237-247:
+		// GRD002 explicitly adds sprite internal offsets from NUT header:
+		//
+		// Normal case (direction==0 OR damage!=0):
+		//   local_60 = sprite_internal_x_offset (from NUT header +0x12)
+		//   X = DAT_00457910 + local_60
+		//   Y = sprite_internal_y_offset (from NUT header +0x14) + DAT_00457912
+		//
+		// Mirrored case (direction!=0 AND damage==0):
+		//   local_60 = 320 - sprite_width - sprite_internal_x_offset
+		//   X = DAT_00457910 + local_60
+		//   Y = sprite_internal_y_offset + DAT_00457912
+		//
+		// Now using actual NUT sprite offsets from NutRenderer!
+		int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
+		int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
+
+		int drawX, drawY;
+
+		if (shouldMirror) {
+			// Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
+			// From assembly lines 240-243
+			drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+		} else {
+			// Normal position: X = DAT_00457910 + sprite_internal_x_offset
+			// From assembly line 238
+			drawX = _rebelViewOffset2X + spriteXOffset;
+		}
+
+		// Y = sprite_internal_y_offset + DAT_00457912
+		// From assembly line 246
+		drawY = spriteYOffset + _rebelViewOffset2Y;
 
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _grd002Sprite, spriteIdx);
+		renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
 
-		debug("Rebel2 Handler25: GRD002 at (%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d",
-			drawX, drawY, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir);
+		debug("Rebel2 Handler25: GRD002 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d mirror=%d",
+			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir, shouldMirror ? 1 : 0);
 	}
 }
 
@@ -4693,6 +4889,12 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		return;
 	}
 
+	// Handler 25 (0x19): From FUN_41DB5E lines 195-197, crosshair only drawn when
+	// DAT_0045790a == 0 (fully uncovered). Hide crosshair during cover transition.
+	if (_rebelHandler == 25 && _rebelDamageLevel != 0) {
+		return;
+	}
+
 	// Update target lock state and draw crosshair/reticle
 
 	// Target lock detection (DAT_00443676 equivalent)
@@ -4719,8 +4921,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 	int reticleIndex;
 	switch (_rebelHandler) {
 	case 7:    // Third-Person Ship
-	case 0x19: // FPS/Mixed
-		reticleIndex = 47;
+	case 0x19: // FPS/Mixed (Handler 25)
+		reticleIndex = 47;  // 0x2F
 		break;
 	case 0x26: { // Turret/Cockpit - animated crosshair
 		static int turretAnimCounter = 0;
@@ -4744,8 +4946,20 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 	if (_smush_iconsNut->getNumChars() > reticleIndex) {
 		int cw = _smush_iconsNut->getCharWidth(reticleIndex);
 		int ch = _smush_iconsNut->getCharHeight(reticleIndex);
+
+		// Calculate crosshair position
+		int crosshairX = _vm->_mouse.x - cw / 2 + _viewX;
+		int crosshairY = _vm->_mouse.y - ch / 2 + _viewY;
+
+		// Handler 25 (0x19): Add view offset to crosshair position
+		// From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
+		if (_rebelHandler == 25) {
+			crosshairX += _rebelViewOffsetX;
+			crosshairY += _rebelViewOffsetY;
+		}
+
 		renderNutSprite(renderBitmap, pitch, width, height,
-			_vm->_mouse.x - cw / 2 + _viewX, _vm->_mouse.y - ch / 2 + _viewY,
+			crosshairX, crosshairY,
 			_smush_iconsNut, reticleIndex);
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 04aa51d7ab2..cdef385b033 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -422,6 +422,7 @@ public:
 	                   int16 animFrame, int16 maxFrames,
 	                   int16 widthScale, int16 heightScale, int16 thickness);
 	void renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx);
+	void renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror);
 
 	struct enemy {
 		int id;
@@ -587,6 +588,7 @@ public:
 	int _rebelDamageLevel;   // DAT_0045790a - damage level (0-5)
 	int _rebelFlightDir;     // DAT_00457902 - flight direction (0 or 1)
 	int _rebelControlMode;   // DAT_0047a7e4 - control mode flags
+	int _rebelInputThrottle; // DAT_00482278 - frame counter for input throttling (every 5 frames)
 
 	// View offset variables (calculated from level type)
 	int _rebelViewOffsetX;   // DAT_0045790c
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index fa24c7fb049..3448672be24 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -174,8 +174,8 @@ void NutRenderer::loadFont(const char *filename) {
 			break;
 		}
 		int codec = READ_LE_UINT16(dataSrc + offset + 8);
-		// _chars[l].xoffs = READ_LE_UINT16(dataSrc + offset + 10);
-		// _chars[l].yoffs = READ_LE_UINT16(dataSrc + offset + 12);
+		_chars[l].xoffs = READ_LE_INT16(dataSrc + offset + 10);
+		_chars[l].yoffs = READ_LE_INT16(dataSrc + offset + 12);
 		_chars[l].width = READ_LE_UINT16(dataSrc + offset + 14);
 		_chars[l].height = READ_LE_UINT16(dataSrc + offset + 16);
 		_chars[l].src = decodedPtr;
@@ -285,6 +285,8 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
 			break;
 		}
 		int codec = READ_LE_UINT16(dataSrc + offset + 8);
+		_chars[l].xoffs = READ_LE_INT16(dataSrc + offset + 10);
+		_chars[l].yoffs = READ_LE_INT16(dataSrc + offset + 12);
 		_chars[l].width = READ_LE_UINT16(dataSrc + offset + 14);
 		_chars[l].height = READ_LE_UINT16(dataSrc + offset + 16);
 		_chars[l].src = decodedPtr;
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index 92abdfb246f..c83ac830400 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -54,6 +54,8 @@ protected:
 	struct {
 		uint16 width;
 		uint16 height;
+		int16 xoffs;   // X offset from NUT frame header (for sprite positioning)
+		int16 yoffs;   // Y offset from NUT frame header (for sprite positioning)
 		byte *src;
 		byte transparency;
 	} _chars[256];
@@ -76,6 +78,8 @@ public:
 
 	int getCharWidth(byte c) const;
 	int getCharHeight(byte c) const;
+	int16 getCharXOffset(byte c) const { return _chars[c].xoffs; }
+	int16 getCharYOffset(byte c) const { return _chars[c].yoffs; }
 	const byte *getCharData(byte c);
 	byte getCharTransparency(byte c) const { return _chars[c].transparency; }
 	byte getBpp() const { return 8; }


Commit: 05b0dc540ae6da78fc6b57824dc11b9e690d9bba
    https://github.com/scummvm/scummvm/commit/05b0dc540ae6da78fc6b57824dc11b9e690d9bba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:00+02:00

Commit Message:
SCUMM: RA2: Improve bit handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 16d00b0dbd9..e86a059644b 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -760,20 +760,38 @@ int32 InsaneRebel2::processMouse() {
 }
 
 bool InsaneRebel2::isBitSet(int n) {
+	// FUN_00423970: When param_1 < 1 (0 or negative), the bounds check fails and returns false.
+	// This means ID 0 or negative IDs are always treated as "enabled" (not skipped).
+	if (n < 1) {
+		return false;
+	}
 	assert (n < 0x200);
 
 	return (_iactBits[n] != 0);
 }
 
 void InsaneRebel2::setBit(int n) {
+	// FUN_004239b0: When n < 1 (i.e., n == 0 or negative), set ALL bits to 1 (disable all objects)
+	// This is used to disable all enemies/objects at once
+	if (n < 1) {
+		for (int i = 0; i < 0x200; i++)
+			_iactBits[i] = 1;
+		return;
+	}
 	assert (n < 0x200);
-
 	_iactBits[n] = 1;
 }
 
 void InsaneRebel2::clearBit(int n) {
+	// FUN_00423a00: When n < 1 (i.e., n == 0 or negative), clear ALL bits to 0 (enable all objects)
+	// This is called by FUN_00423880 at the start of video playback to reset the bit table,
+	// ensuring all enemies are visible when a new level/segment starts.
+	if (n < 1) {
+		for (int i = 0; i < 0x200; i++)
+			_iactBits[i] = 0;
+		return;
+	}
 	assert (n < 0x200);
-
 	_iactBits[n] = 0;
 }
 
@@ -1116,13 +1134,16 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 	if (par3 == 4) {
 		int16 childId = b.readSint16LE(); // Offset +8
 		int16 parentId = b.readSint16LE(); // Offset +10
-		
-		if (parentId >= 0 && parentId < 512) {
+
+		// Validate BOTH parentId AND childId to avoid triggering "set/clear ALL bits" behavior
+		// when childId <= 0. The original game's setBit(0)/clearBit(0) affects ALL bits,
+		// which would disable/enable all enemies at once - not the intended linking behavior.
+		if (parentId >= 1 && parentId < 512 && childId >= 1 && childId < 512) {
 			// Shift links
 			_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
 			_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
 			_rebelLinks[parentId][0] = childId;
-			
+
 			// Apply initial state based on parent state
 			if (!isBitSet(parentId)) {
 				setBit(childId);
@@ -1131,13 +1152,17 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 				clearBit(childId);
 				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
 			}
+		} else {
+			debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
 		}
 		return;
 	} else if (par3 == 1) { // Probabilistic / counter cases: par3 == 1
 		int16 value = par4; // sVar6
 		int16 targetId = b.readSint16LE(); // Offset +8 (sVar7)
-		
-		if (targetId < 0 || targetId >= 0x200) 
+
+		// Validate targetId >= 1 to avoid triggering "set/clear ALL bits" behavior
+		// The original game's setBit(0)/clearBit(0) affects ALL bits, not intended here
+		if (targetId < 1 || targetId >= 0x200)
 			return;
 		
 		if (value > 1 && value < 10) { // 1 < value < 10: random disable
@@ -4409,21 +4434,25 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		}
 
 		if (shouldDraw) {
-			// GRD001 is drawn at raw (DAT_00457910, DAT_00457912) per assembly line 206
-			// The render function applies the sprite's internal positioning
+			// GRD001 is drawn at (DAT_00457910, DAT_00457912) per assembly line 206
+			// FUN_004236e0 internally applies the sprite's internal offsets from NUT header
 			// Use sprite 0 (primary frame)
 			int spriteW = _grd001Sprite->getCharWidth(0);
 			int spriteH = _grd001Sprite->getCharHeight(0);
 
-			// Draw at raw offset position - the sprite is designed to be positioned
-			// at these coordinates (the NUT's internal offsets handle actual placement)
-			int drawX = _rebelViewOffset2X;
-			int drawY = _rebelViewOffset2Y;
+			// Get sprite internal offsets from NUT header (like FUN_004236e0 does internally)
+			int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
+			int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
+
+			// Position = view offset + sprite internal offset
+			// This matches how FUN_004236e0 positions sprites using NUT header offsets
+			int drawX = _rebelViewOffset2X + spriteXOffset;
+			int drawY = _rebelViewOffset2Y + spriteYOffset;
 
 			renderNutSprite(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd001Sprite, 0);
 
-			debug("Rebel2 Handler25: GRD001 at (%d,%d) offset(%d,%d) size(%d,%d) mode=%d damage=%d",
-				drawX, drawY, _rebelViewOffset2X, _rebelViewOffset2Y,
+			debug("Rebel2 Handler25: GRD001 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) mode=%d damage=%d",
+				drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
 				spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
 		}
 	}
@@ -7470,6 +7499,10 @@ int InsaneRebel2::runLevel1() {
 		_playerDamage = 0;
 		_deathFrame = 0;
 
+		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
+		// This ensures all enemies are visible (not skipped by SKIP chunks)
+		clearBit(0);
+
 		// Play gameplay (01P01.SAN with 0x28 flags)
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV01/01P01.SAN", 12);
@@ -7532,6 +7565,17 @@ int InsaneRebel2::runLevel2() {
 		_currentPhase = 1;
 		bonusCount = 0;
 
+		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
+		// This ensures all enemies are visible (not skipped by SKIP chunks)
+		clearBit(0);
+
+		// Also reset link tables for enemy dependencies
+		for (int i = 0; i < 512; i++) {
+			_rebelLinks[i][0] = 0;
+			_rebelLinks[i][1] = 0;
+			_rebelLinks[i][2] = 0;
+		}
+
 		// ===== PHASE 1: P1/02P01_X.SAN =====
 		// First play variant A which contains the background IACT (opcode 8, par4=5)
 		// The background is loaded during this video and persists for B/C/D variants
@@ -7581,6 +7625,9 @@ int InsaneRebel2::runLevel2() {
 
 		_currentPhase = 2;
 
+		// Reset bit table before Phase 2 gameplay starts
+		clearBit(0);
+
 		// ===== PHASE 2: P2/02P02_X.SAN =====
 		// First play variant A which contains the background IACT for this phase
 		{
@@ -7629,6 +7676,9 @@ int InsaneRebel2::runLevel2() {
 
 		_currentPhase = 3;
 
+		// Reset bit table before Phase 3 gameplay starts
+		clearBit(0);
+
 		// ===== PHASE 3: P3/02P03_X.SAN =====
 		// First play variant A which contains the background IACT for this phase
 		{
@@ -7700,6 +7750,9 @@ int InsaneRebel2::runLevel3() {
 		_playerDamage = 0;
 		_currentPhase = 1;
 
+		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
+		clearBit(0);
+
 		// Play phase 1 gameplay (03PLAY1.SAN)
 		debug("Rebel2: Level 3 Phase 1");
 		splayer->setCurVideoFlags(0x28);
@@ -7744,6 +7797,9 @@ int InsaneRebel2::runLevel3() {
 		_playerShield = 255;
 		_playerDamage = 0;
 
+		// Reset bit table before gameplay starts
+		clearBit(0);
+
 		// Play phase 2 gameplay (03PLAY2.SAN)
 		debug("Rebel2: Level 3 Phase 2");
 		splayer->setCurVideoFlags(0x28);
@@ -7803,6 +7859,9 @@ int InsaneRebel2::runLevel4() {
 		_playerDamage = 0;
 		_currentPhase = 1;
 
+		// Reset bit table before gameplay starts
+		clearBit(0);
+
 		// Play gameplay (04PLAY.SAN)
 		debug("Rebel2: Level 4 gameplay");
 		splayer->setCurVideoFlags(0x28);
@@ -7856,6 +7915,9 @@ int InsaneRebel2::runLevel5() {
 		_playerDamage = 0;
 		_currentPhase = 1;
 
+		// Reset bit table before gameplay starts
+		clearBit(0);
+
 		// Play gameplay (05PLAY.SAN)
 		debug("Rebel2: Level 5 gameplay");
 		splayer->setCurVideoFlags(0x28);
@@ -7910,6 +7972,9 @@ int InsaneRebel2::runLevel6() {
 		_playerDamage = 0;
 		_currentPhase = 1;
 
+		// Reset bit table before gameplay starts
+		clearBit(0);
+
 		// Play phase 1 gameplay (06PLAY1.SAN)
 		debug("Rebel2: Level 6 Phase 1");
 		splayer->setCurVideoFlags(0x28);
@@ -7952,6 +8017,9 @@ int InsaneRebel2::runLevel6() {
 		_playerShield = 255;
 		_playerDamage = 0;
 
+		// Reset bit table before gameplay starts
+		clearBit(0);
+
 		// Play phase 2 gameplay (06PLAY2.SAN)
 		debug("Rebel2: Level 6 Phase 2");
 		splayer->setCurVideoFlags(0x28);


Commit: 30795feeb52874254620fe82bbbe50985a93b69a
    https://github.com/scummvm/scummvm/commit/30795feeb52874254620fe82bbbe50985a93b69a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:00+02:00

Commit Message:
SCUMM: RA2: Improve level 2 rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index e86a059644b..80bd3ca95f8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -885,10 +885,26 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
 	// This creates a "scrolling window" effect as the character enters/exits cover.
 	if (_rebelHandler == 25 && renderBitmap) {
+		// Calculate pitch and buffer height for both corridor overlay and GRD001 rendering
+		int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+		int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+		// FIRST: Restore Level 2 background (par4=5) as the base layer
+		// This is the scene background that enemies (FOBJ) draw on top of.
+		// Without this, enemies are drawn onto black/empty buffer and appear invisible.
+		// The corridor overlay (par4=4) is a HUD frame drawn ON TOP of this background.
+		if (_level2BackgroundLoaded && _level2Background) {
+			for (int y = 0; y < MIN(200, bufHeight); y++) {
+				memcpy(renderBitmap + y * pitch, _level2Background + y * 320, MIN(320, pitch));
+			}
+			debug("Rebel2 Handler25 PRE: Restored _level2Background to renderBitmap");
+		} else {
+			debug("Rebel2 Handler25 PRE: WARNING - _level2Background NOT restored (loaded=%d ptr=%p)",
+				_level2BackgroundLoaded, (void*)_level2Background);
+		}
+
 		EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
 		if (corridorOverlay.valid && corridorOverlay.pixels) {
-			int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
-			int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
 
 			// Calculate source offset and destination position (FUN_00428a10 lines 31-43)
 			// If position is negative, skip source pixels and draw at screen edge
@@ -951,6 +967,11 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 				srcOffsetX, srcOffsetY, destX, destY, drawWidth, drawHeight,
 				corridorOverlay.width, corridorOverlay.height);
 		}
+
+		// Draw GRD001 (wall/cockpit overlay) AFTER corridor but BEFORE FOBJ enemies.
+		// This ensures enemies from FOBJ chunks draw ON TOP of the cockpit overlay.
+		// Uses width-halving logic from FUN_0041db5e lines 202-221.
+		renderHandler25ShipPre(renderBitmap, pitch, pitch, bufHeight);
 	}
 }
 
@@ -1144,14 +1165,13 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 			_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
 			_rebelLinks[parentId][0] = childId;
 
-			// Apply initial state based on parent state
-			if (!isBitSet(parentId)) {
-				setBit(childId);
-				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Alive -> Child Disabled.", childId, parentId);
-			} else {
-				clearBit(childId);
-				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0). Parent Dead -> Child Enabled.", childId, parentId);
-			}
+			// DO NOT modify bits here! All bits are reset to CLEAR by clearBit(0) at level start.
+			// SKIP chunks use XOR logic: skip when bits DIFFER.
+			// - Both bits CLEAR (alive) -> same -> don't skip -> enemy visible
+			// - Parent SET (dead), child SET (disabled) -> same -> skip -> enemy hidden
+			// The bits are only modified when enemies are actually destroyed (setBit/clearBit
+			// calls in processMouse() enemy destruction handling).
+			debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0)", childId, parentId);
 		} else {
 			debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
 		}
@@ -3133,6 +3153,9 @@ void InsaneRebel2::spawnShot(int x, int y) {
 	case 7:     // Space combat
 		spawnSpaceShot(x, y);
 		break;
+	case 25:    // Speeder bike - uses turret shot array with different gun position
+		spawnHandler25Shot(x, y);
+		break;
 	default:
 		// Legacy fallback
 		for (int i = 0; i < 2; i++) {
@@ -3180,6 +3203,34 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
 	}
 }
 
+// Handler 25 Speeder bike shot spawn (based on FUN_0041db5e lines 170-190)
+// Similar to turret but with character-based gun position
+void InsaneRebel2::spawnHandler25Shot(int x, int y) {
+	// Handler 25 can only shoot when uncovered (damage == 0)
+	if (_rebelDamageLevel != 0) {
+		return;  // Can't shoot while taking cover
+	}
+
+	for (int i = 0; i < 2; i++) {
+		if (_turretShots[i].counter == 0) {
+			// Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
+			// TODO: Play laser sound
+
+			_turretShots[i].counter = getShotMaxDuration();
+			_turretShots[i].seqNum = _turretShotSeqCounter;
+			_turretShotSeqCounter++;
+
+			// Target position is where player clicked (screen coords)
+			_turretShots[i].targetX = x;
+			_turretShots[i].targetY = y;
+
+			debug("Rebel2 Handler25: Spawned shot %d target (%d,%d)",
+				i, _turretShots[i].targetX, _turretShots[i].targetY);
+			break;
+		}
+	}
+}
+
 // Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
 void InsaneRebel2::spawnSpaceShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
@@ -4108,6 +4159,13 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 		if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
 			continue;
 
+		// For Handler 25: Skip slot 4 (corridor overlay) here - it's already drawn
+		// in procPreRendering BEFORE FOBJ enemies. Drawing it again here (after FOBJs)
+		// would cover the enemies and make them invisible.
+		if (_rebelHandler == 25 && hudSlot == 4) {
+			continue;
+		}
+
 		// Skip small frames at (0,0) - likely animation patches
 		if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
 			debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
@@ -4367,95 +4425,98 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 		spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
 }
 
-void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 25 Ship Rendering (Mixed Mode - GRD sprites)
-	// Based on FUN_0041db5e disassembly (lines 202-248)
-	// Uses _grd001Sprite (GRD001) and _grd002Sprite (GRD002)
-	//
-	// The GRD sprite is drawn at position (DAT_00457910, DAT_00457912) which are
-	// offsets calculated from sprite mode and damage level, relative to screen center.
-	// In the original, these appear to be direct pixel coordinates where:
-	//   - Mode 1: sprite shifts left as damage increases (X = damage * -22)
-	//   - Mode 4: sprite shifts right as damage increases (X = damage * 17 - 85)
-	//   - Mode 2: sprite shifts vertically
-	//   - Mode 3: depends on flight direction
-	//
-	// NOTE: The corridor overlay (par4=4) is drawn in procPreRendering BEFORE enemies,
-	// not here, so that enemies appear ON TOP of the corridor.
-
+// Handler 25 PRE-rendering: Draw GRD001 BEFORE FOBJ decoding
+// This is called from procPreRendering so enemies draw ON TOP of GRD001
+//
+// From FUN_0041db5e disassembly (lines 202-221):
+// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
+// - Mode 4 with damage==0: Width halved AND buffer offset (right half only, pixels 160-319)
+// - All other cases: Full width (320 pixels)
+void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
 	if (_rebelHandler != 25)
 		return;
 
-	// Need at least one GRD sprite to render
-	if (!_grd001Sprite && !_grd002Sprite)
+	if (!_grd001Sprite || _grd001Sprite->getNumChars() <= 0)
 		return;
 
 	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
-	// From FUN_0041db5e line 260: _DAT_00482236 = 0xb4 during gameplay
-	// The status bar occupies Y=180-200, and sprites must not render there
-	const int clipHeight = 180;  // Status bar boundary (0xb4)
+	const int clipHeight = 180;
 	int renderHeight = MIN(height, clipHeight);
 
-	// Base position calculation based on FUN_0041db5e disassembly:
-	// The original reads sprite internal offsets from the NUT data at +0x12 (X) and +0x14 (Y).
-	// Position calculation from FUN_0041db5e assembly:
-	//
-	// GRD001 (line 206): FUN_004236e0(..., DAT_00457910, DAT_00457912, ...)
-	//   - Raw offsets passed directly, render function applies sprite internal offsets
-	//
-	// GRD002 (lines 238-247): Position uses sprite header offsets:
-	//   - X = DAT_00457910 + sprite_internal_x_offset (NUT header +0x12)
-	//   - Y = sprite_internal_y_offset (NUT header +0x14) + DAT_00457912
-	//
-	// Since NutRenderer doesn't expose internal offsets, we simulate them:
-	// - GRD001: Draw at raw offset position (the sprite likely has built-in positioning)
-	// - GRD002: Add base position to simulate internal offsets for character sprite
-
 	// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
-	// Mode 1, 2, 3, 4 all potentially draw _grd001Sprite
-	if (_grd001Sprite && _grd001Sprite->getNumChars() > 0) {
-		bool shouldDraw = false;
-
-		// Mode 1: Always draw (uncovered/shooting position)
-		if (_grdSpriteMode == 1) {
-			shouldDraw = true;
-		}
-		// Mode 2: Only draw when damaged (DAT_0045790a != 0) - covered position
-		else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
-			shouldDraw = true;
-		}
-		// Mode 3: Always draw (transition between covered/uncovered)
-		else if (_grdSpriteMode == 3) {
-			shouldDraw = true;
-		}
-		// Mode 4: Always draw (alternative uncovered position)
-		else if (_grdSpriteMode == 4) {
-			shouldDraw = true;
+	// Each mode has specific conditions from FUN_0041db5e:
+	bool shouldDraw = false;
+	bool useHalfWidth = false;
+	bool useRightHalf = false;
+
+	// Mode 1 (lines 202-210): Draw with width halving when uncovered
+	if (_grdSpriteMode == 1) {
+		shouldDraw = true;
+		useHalfWidth = (_rebelDamageLevel == 0);  // Half width when uncovered
+	}
+	// Mode 2 (lines 222-224): Only draw when damaged (covered)
+	else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
+		shouldDraw = true;
+	}
+	// Mode 3 (lines 225-228): Always draw full width
+	else if (_grdSpriteMode == 3) {
+		shouldDraw = true;
+	}
+	// Mode 4 (lines 211-221): Draw to right half when uncovered
+	else if (_grdSpriteMode == 4) {
+		shouldDraw = true;
+		useHalfWidth = (_rebelDamageLevel == 0);
+		useRightHalf = (_rebelDamageLevel == 0);
+	}
+
+	if (shouldDraw) {
+		int spriteW = _grd001Sprite->getCharWidth(0);
+		int spriteH = _grd001Sprite->getCharHeight(0);
+		int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
+		int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
+
+		int drawX = _rebelViewOffset2X + spriteXOffset;
+		int drawY = _rebelViewOffset2Y + spriteYOffset;
+
+		// Apply width-halving logic from original assembly:
+		// When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
+		// This clips the sprite to only half the screen.
+		int renderWidth = width;
+		byte *dstBitmap = renderBitmap;
+
+		if (useHalfWidth) {
+			renderWidth = width / 2;  // Clip to half width (160 pixels)
+
+			if (useRightHalf) {
+				// Mode 4: Draw to right half by offsetting the destination buffer
+				// Original: DAT_00482230 += DAT_00482234 (adds 160 to buffer start)
+				// This makes drawing appear on the right half (pixels 160-319)
+				dstBitmap = renderBitmap + (width / 2);
+			}
 		}
 
-		if (shouldDraw) {
-			// GRD001 is drawn at (DAT_00457910, DAT_00457912) per assembly line 206
-			// FUN_004236e0 internally applies the sprite's internal offsets from NUT header
-			// Use sprite 0 (primary frame)
-			int spriteW = _grd001Sprite->getCharWidth(0);
-			int spriteH = _grd001Sprite->getCharHeight(0);
+		renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
 
-			// Get sprite internal offsets from NUT header (like FUN_004236e0 does internally)
-			int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
-			int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
+		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
+			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
+			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, renderWidth);
+	}
+}
 
-			// Position = view offset + sprite internal offset
-			// This matches how FUN_004236e0 positions sprites using NUT header offsets
-			int drawX = _rebelViewOffset2X + spriteXOffset;
-			int drawY = _rebelViewOffset2Y + spriteYOffset;
+void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
+	// Handler 25 POST-rendering: Draw ONLY GRD002 (character sprite)
+	// GRD001 (wall/cockpit) is now drawn in renderHandler25ShipPre() during procPreRendering
+	// so that FOBJ enemies can draw ON TOP of it.
+	//
+	// From FUN_0041db5e disassembly (lines 230-248):
+	// GRD002 is drawn LAST (after enemies) so the character appears in front.
 
-			renderNutSprite(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd001Sprite, 0);
+	if (_rebelHandler != 25)
+		return;
 
-			debug("Rebel2 Handler25: GRD001 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) mode=%d damage=%d",
-				drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
-				spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel);
-		}
-	}
+	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
+	const int clipHeight = 180;
+	int renderHeight = MIN(height, clipHeight);
 
 	// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
 	// The sprite index is calculated based on damage level and aiming position
@@ -4726,6 +4787,9 @@ void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, in
 	case 7:     // Space combat - FUN_40FADF
 		renderSpaceLaserShots(renderBitmap, pitch, width, height);
 		break;
+	case 25:    // Speeder bike - FUN_0041f004
+		renderHandler25LaserShots(renderBitmap, pitch, width, height);
+		break;
 	default:
 		// No laser rendering for other handlers
 		break;
@@ -4831,7 +4895,8 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 }
 
 // Handler 8 Vehicle laser rendering (FUN_402ED0)
-// Gun position derived from ship position
+// In the original, the laser is a short muzzle flash from gun barrel toward ship center,
+// NOT a projectile traveling across the screen. The "hit" effect is handled separately.
 void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	// No NUT check needed - uses pre-initialized _laserTexture
 
@@ -4841,13 +4906,17 @@ void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int wi
 		if (_vehicleShots[i].counter <= 0)
 			continue;
 
-		// Calculate sound panning
+		// Calculate sound panning from STORED target position (FUN_402ED0 lines 24-51)
+		// pan = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
 		int16 pan = ((2 - _vehicleShots[i].counter) * (_vehicleShots[i].targetX - _viewX - 160)) / 2;
 		pan = CLIP<int16>(pan, -127, 127);
 		// TODO: Apply panning
 
-		// Calculate gun position from ship position (FUN_402ED0 lines 26-36)
-		// Low-res formula:
+		// Calculate positions from CURRENT ship position (FUN_402ED0 lines 53-122)
+		// The original game draws the laser from gun position toward ship center,
+		// creating a short muzzle flash effect (7 pixels horizontal, 25 pixels vertical).
+		//
+		// Low-res formula (DAT_0047a808 < 2):
 		// shipScreenY = ((shipPosY - 0x28) >> 2) + 0x69 = ((shipPosY - 40) >> 2) + 105
 		// shipScreenX = ((shipPosX - 0xa0) >> 3) + 0xa0 = ((shipPosX - 160) >> 3) + 160
 		// gunY = ((shipPosY - 0x28) >> 2) + 0x82 = shipScreenY + 25
@@ -4857,14 +4926,14 @@ void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int wi
 		int16 gunX = shipScreenX + 7;
 		int16 gunY = shipScreenY + 25;
 
-		int16 targetX = _vehicleShots[i].targetX;
-		int16 targetY = _vehicleShots[i].targetY;
 		int16 progress = maxDuration - _vehicleShots[i].counter;
 
-		// Single beam from gun to target
+		// Draw beam from gun toward ship center (muzzle flash effect)
 		// From FUN_402ED0: widthScale=0x14(20), heightScale=8, thickness=4
+		// Parameters: gunX, gunY -> shipScreenX, shipScreenY (NOT the stored target!)
 		drawLaserBeam(renderBitmap, pitch, width, height,
-			gunX + _viewX, gunY + _viewY, targetX, targetY,
+			gunX + _viewX, gunY + _viewY,
+			shipScreenX + _viewX, shipScreenY + _viewY,
 			progress, maxDuration, 20, 8, 4);
 
 		_vehicleShots[i].counter--;
@@ -4911,6 +4980,57 @@ void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int widt
 	}
 }
 
+// Handler 25 laser rendering (FUN_0041f004)
+// Speeder bike laser shots - draws beam from gun position to target
+void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height) {
+	// FUN_0041f004 uses turret-style shot slots with view offset adjustment
+	// Only render when player is uncovered (damage == 0)
+	if (_rebelDamageLevel != 0) {
+		return;  // Can't shoot while taking cover
+	}
+
+	int16 maxDuration = getShotMaxDuration();
+
+	for (int i = 0; i < 2; i++) {
+		if (_turretShots[i].counter <= 0)
+			continue;
+
+		// Calculate sound panning from target X position (FUN_004262f0)
+		// sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
+		int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - 160)) / 2;
+		pan = CLIP<int16>(pan, -127, 127);
+		// TODO: Apply panning to sound channel i+1
+
+		// Target position (where player clicked)
+		int16 targetX = _turretShots[i].targetX;
+		int16 targetY = _turretShots[i].targetY;
+
+		// Gun position: In FUN_0041f004, the gun is at a fixed offset based on character sprite
+		// From FUN_0041db5e lines 178-187, the gun position is:
+		// gunX = (spriteOffset + DAT_00457910) - viewOffsetX
+		// gunY = (spriteOffset + DAT_00457912) - viewOffsetY
+		//
+		// For simplicity, use a position near the bottom-center of the screen
+		// where the character's gun would be in the speeder bike cockpit.
+		// The character is typically around (160, 150) in the cockpit view.
+		int16 gunX = 160;  // Center of screen
+		int16 gunY = 170;  // Near bottom where character sits
+
+		int16 progress = maxDuration - _turretShots[i].counter;
+
+		// From FUN_0041f004 parameters for FUN_0040bbf6:
+		// widthScale=0xC(12), heightScale=4, thickness=6
+		drawLaserBeam(renderBitmap, pitch, width, height,
+			gunX, gunY, targetX, targetY,
+			progress, maxDuration, 12, 4, 6);
+
+		_turretShots[i].counter--;
+
+		debug("Rebel2 Handler25: Laser shot %d from (%d,%d) to (%d,%d) progress=%d/%d",
+			i, gunX, gunY, targetX, targetY, progress, maxDuration);
+	}
+}
+
 void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
 	// From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
 	// Don't draw crosshair when shooting is disabled (flight-only segments)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index cdef385b033..0a32966f5ab 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -671,12 +671,14 @@ public:
 	void spawnTurretShot(int x, int y);    // Handler 0x26
 	void spawnVehicleShot(int x, int y);   // Handler 8
 	void spawnSpaceShot(int x, int y);     // Handler 7
+	void spawnHandler25Shot(int x, int y); // Handler 25 (speeder bike)
 	void spawnShot(int x, int y);          // Dispatcher based on current handler
 
-	// Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF)
+	// Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF, FUN_0041f004)
 	void renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height);
 	void renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height);
 	void renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height);
+	void renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height);
 
 	// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
 	int16 getShotMaxDuration();
@@ -802,7 +804,10 @@ public:
 	//   4: Draw _grd001Sprite with buffer offset
 	int16 _grdSpriteMode;            // DAT_00457900
 
-	// Render Handler 25 ship sprites (called from procPostRendering)
+	// Render Handler 25 ship sprites
+	// renderHandler25ShipPre: Draw GRD001 BEFORE FOBJ (in procPreRendering)
+	// renderHandler25Ship: Draw GRD002 and other overlays AFTER FOBJ (in procPostRendering)
+	void renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height);
 	void renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height);
 
 	// ======================= Handler 0x26 Turret HUD Overlays =======================
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 9468f75aa6a..bed00ea3049 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -436,14 +436,19 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
 	assert(subSize >= 6);
 
-	// For RA2 Handler 25, skip FTCH because the frame buffer only contains the
+	// For RA2 Handlers 8 and 25, skip FTCH because the frame buffer only contains the
 	// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
 	// immediately in frame 0. Restoring would erase those overlays and make
 	// enemies invisible since they draw on top of the erased areas.
+	//
+	// Handler 8 (Level 2 on-foot): Background is drawn in procPreRendering from
+	// _level2Background. FTCH would overwrite FOBJ-drawn enemies.
+	// Handler 25 (Level 2 speeder bike): Same issue with corridor overlays.
 	if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
 		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
-		if (rebel2->getHandler() == 25) {
-			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
+		int handler = rebel2->getHandler();
+		if (handler == 8 || handler == 25) {
+			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler %d - preserving overlays", handler);
 			return;
 		}
 	}
@@ -1018,10 +1023,39 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 				_height = height;
 			}
 		}
-		// Use special buffer if allocated (for oversized videos like Level 1)
-		// Otherwise use virtual screen (for Level 2 small sprites)
-		if (_specialBuffer != nullptr && _specialBufferSize >= _vm->_screenWidth * _vm->_screenHeight) {
+		// Use special buffer ONLY for oversized frames that need it.
+		// Small enemy sprites (like Level 2's 9x38 stormtroopers) should draw
+		// directly to the virtual screen, not to _specialBuffer.
+		// The special buffer is only needed when the CURRENT frame is larger than screen.
+		if (bufSize > _vm->_screenWidth * _vm->_screenHeight &&
+		    _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
 			_dst = _specialBuffer;
+			debug("SmushPlayer: Using _specialBuffer for oversized FOBJ %dx%d", width, height);
+		} else {
+			// For small RA2 sprites, check if we should use _specialBuffer or virtual screen.
+			//
+			// If _specialBuffer was allocated in this video (by a larger frame like Level 1's
+			// 424x260 background), small sprites should use it too so everything composites
+			// in the same buffer.
+			//
+			// If _specialBuffer is null (no large frames in this video, like Level 2), small
+			// sprites should use the virtual screen directly.
+			//
+			// This is important because release() frees _specialBuffer at the end of each video,
+			// so a new video starts with _specialBuffer = nullptr. Without this check, small
+			// sprites in Level 2 could incorrectly use a stale _specialBuffer pointer (though
+			// release() should have freed it, init() might not reset _dst if video flags are set).
+			if (_specialBuffer == nullptr) {
+				VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+				_dst = vs->getPixels(0, 0);
+				debug("SmushPlayer: Reset _dst to virtual screen for FOBJ %dx%d at (%d,%d) _dst=%p",
+					width, height, left, top, (void*)_dst);
+			} else {
+				// Large frame was in this video, use _specialBuffer for compositing
+				_dst = _specialBuffer;
+				debug("SmushPlayer: Using _specialBuffer for small FOBJ %dx%d (compositing with large frame)",
+					width, height);
+			}
 		}
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;


Commit: 0983d2c688de0317dc7483dd4786feff4b864756
    https://github.com/scummvm/scummvm/commit/0983d2c688de0317dc7483dd4786feff4b864756
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00

Commit Message:
SCUMM: RA2: Improve main menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 80bd3ca95f8..bf9fa303ca8 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -349,10 +349,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Initialize menu system
 	_gameState = kStateMainMenu;  // Start at main menu
 	_menuSelection = 0;           // First item selected
-	// Main menu has 6 selectable items (0-5): START, OPTIONS, CONTINUE INTRO, TOP PILOTS, CREDITS, QUIT
-	// Note: The coordinate formula uses param_3 = 7 (includes title) for Y position calculation
-	// but _menuItemCount is the number of SELECTABLE items for bounds checking
-	_menuItemCount = 6;
+	// Main menu has 7 selectable items (0-6) matching GAME.TRS indices 11-17:
+	//   0: Start Game, 1: Options, 2: Calibrate Joystick, 3: Continue Intro,
+	//   4: Show Top Pilots, 5: Show Credits, 6: Return to Launcher
+	// Note: The coordinate formula uses numItemsTotal = 8 (includes title) for Y position calculation
+	// Formula from FUN_0041f5ae: (DAT_0047a806 == 0) + 6 = 7 items for keyboard mode
+	_menuItemCount = 7;
 	_menuInactivityTimer = 0;
 	_lastMenuVariant = -1;        // No previous menu video
 	_menuRepeatDelay = 0;
@@ -457,9 +459,20 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_ESCAPE:
-			// ESC skips videos
+			// ESC handling depends on game state:
+			// - In menus: Select quit option and confirm
+			// - During gameplay/cutscenes: Skip video
 			if (splayer) {
-				debug("Rebel2: ESC pressed - skipping video");
+				if (_menuInputActive && (_gameState == kStateMainMenu ||
+				                          _gameState == kStatePilotSelect ||
+				                          _gameState == kStateChapterSelect)) {
+					// In menu mode: Select quit option and confirm selection
+					// This emulates the assembly behavior from FUN_0041f5ae
+					_menuSelection = _menuItemCount - 1;  // Select last item (quit/back)
+					debug("Rebel2: ESC pressed in menu - selecting quit (item %d)", _menuSelection);
+				} else {
+					debug("Rebel2: ESC pressed - skipping video");
+				}
 				_vm->_smushVideoShouldFinish = true;
 				return true;  // Consume the event
 			}
@@ -3936,6 +3949,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 
 	// Handle menu input and rendering if in menu mode
+	// Debug: Log menu mode detection on first few frames
+	static int menuDebugFrames = 0;
+	if (menuDebugFrames < 5 && _gameState == kStateMainMenu) {
+		debug("Rebel2: procPostRendering frame check - introPlaying=%d menuMode=%d _menuInputActive=%d flags=0x%x",
+		      introPlaying, menuMode, _menuInputActive, _player ? _player->_curVideoFlags : -1);
+		menuDebugFrames++;
+	}
+
 	if (menuMode) {
 		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
 		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
@@ -3948,14 +3969,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// Process menu input during each frame
 		int selection = processMenuInput();
 
-		// Update inactivity timer
+		// Update inactivity timer (only increments when no input is received)
+		// Input resets timer in processMenuInput()
 		_menuInactivityTimer++;
 
-		// Check for inactivity timeout (300 frames = ~10 sec at 30fps, or ~25 sec at 12fps)
+		// Check for inactivity timeout
+		// From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode)
+		// At 12fps video rate, 300 frames = ~25 seconds of inactivity
+		// The original checks: if (local_8 > 299) return 0;
 		if (_menuInactivityTimer > 300) {
-			debug("Rebel2: Menu inactivity timeout");
-			// Reset timer but don't take action yet
+			debug("Rebel2: Menu inactivity timeout - ending video to loop");
+			// Signal video to end so menu loop plays new video
+			// This emulates the attract mode behavior where a new random
+			// menu video is selected after inactivity
 			_menuInactivityTimer = 0;
+			// Don't set _smushVideoShouldFinish here - let video end naturally
+			// This will cause runMainMenu to loop and play a new random video
 		}
 
 		// Draw menu selection overlay
@@ -5396,19 +5425,26 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
 
 int InsaneRebel2::processMenuInput() {
 	// Emulates FUN_0041f5ae menu input handling
-	// Returns: -1 = no action, 0-6 = menu item selected
+	// Returns: -1 = no action, 0-4 = menu item selected
 	//
 	// Events are captured by notifyEvent() (EventObserver) which runs before
 	// ScummEngine::parseEvents() consumes them. This ensures we don't miss
 	// any input events even though we only process them on video frames.
+	//
+	// From FUN_0041f5ae disassembly:
+	// - Keyboard: Up/Down arrows navigate, Enter confirms
+	// - Mouse mode (DAT_0047a806 == 1): Y position maps to selection
+	// - Key codes: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b
 
 	int result = -1;
 
 	// Menu item Y positions (low-res 320x200 mode):
-	// From assembly: baseY = numItems * -5 + 0x68 = 7 * -5 + 104 = 69
-	// Items at Y = 69, 79, 89, 99, 109, 119 with spacing of 10
-	const int baseY = 69;
-	const int itemHeight = 10;
+	// From FUN_0041f5ae: baseY = numItems * -5 + 0x68
+	// With 8 total items (title + 7 options): 8 * -5 + 104 = 64
+	// Items at Y = 64, 74, 84, 94, 104, 114, 124 with spacing of 10
+	const int numItemsTotal = 8;  // Title + 7 selectable items (matching assembly)
+	const int baseY = numItemsTotal * -5 + 0x68;  // = 64
+	const int itemSpacing = 10;
 
 	// Process events from the queue (populated by notifyEvent)
 	while (!_menuEventQueue.empty()) {
@@ -5419,26 +5455,29 @@ int InsaneRebel2::processMenuInput() {
 
 			switch (event.kbd.keycode) {
 			case Common::KEYCODE_UP:
-				// Navigate up (wrap around)
+				// Navigate up (wrap around) - emulates key code 0x148
 				_menuSelection--;
 				if (_menuSelection < 0) {
 					_menuSelection = _menuItemCount - 1;
 				}
+				// Reset repeat delay counter (DAT_00459ce0)
+				_menuRepeatDelay = 3;
 				debug("Menu: Selection changed to %d (UP)", _menuSelection);
 				break;
 
 			case Common::KEYCODE_DOWN:
-				// Navigate down (wrap around)
+				// Navigate down (wrap around) - emulates key code 0x150
 				_menuSelection++;
 				if (_menuSelection >= _menuItemCount) {
 					_menuSelection = 0;
 				}
+				_menuRepeatDelay = 3;
 				debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
 				break;
 
 			case Common::KEYCODE_RETURN:
 			case Common::KEYCODE_KP_ENTER:
-				// Confirm selection
+				// Confirm selection - emulates key code 0x0d
 				if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
 					result = _menuSelection;
 					debug("Menu: Item %d selected (ENTER)", _menuSelection);
@@ -5446,9 +5485,9 @@ int InsaneRebel2::processMenuInput() {
 				break;
 
 			case Common::KEYCODE_ESCAPE:
-				// ESC - Exit/Quit (return special value)
-				result = 5;  // Quit option (index 5)
-				debug("Menu: ESC pressed - quit");
+				// ESC - Quit (index 4 = last item) - emulates key code 0x1b
+				result = _menuItemCount - 1;  // Select quit option
+				debug("Menu: ESC pressed - selecting quit (item %d)", result);
 				break;
 
 			default:
@@ -5458,22 +5497,22 @@ int InsaneRebel2::processMenuInput() {
 
 		case Common::EVENT_LBUTTONDOWN:
 			_menuInactivityTimer = 0;
-
 			{
 				// Get mouse position from the event
-				int mouseX = event.mouse.x;
 				int mouseY = event.mouse.y;
 
-				debug("Menu: Click detected at (%d, %d)", mouseX, mouseY);
+				debug("Menu: Left click at Y=%d", mouseY);
 
-				// Check which item was clicked (larger hit area for better usability)
+				// Check which item was clicked
+				// From FUN_0041f5ae mouse mode: selection = (mouseY + 100 - baseY) / 10
+				// But we use a simpler direct hit-test approach
 				for (int i = 0; i < _menuItemCount; i++) {
-					int itemY = baseY + i * itemHeight;
-					// Use a larger vertical hit area (full item height)
-					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+					int itemY = baseY + i * itemSpacing;
+					// Hit area: itemY - 2 to itemY + 8 (10 pixel height)
+					if (mouseY >= itemY - 2 && mouseY < itemY + 8) {
 						_menuSelection = i;
 						result = i;
-						debug("Menu: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
+						debug("Menu: Item %d clicked (itemY=%d)", i, itemY);
 						break;
 					}
 				}
@@ -5481,15 +5520,36 @@ int InsaneRebel2::processMenuInput() {
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
-			// Update mouse position for hover effects (optional)
+			// Update hover selection based on Y position
+			// This emulates FUN_0041f5ae mouse mode behavior (DAT_0047a806 == 1)
+			{
+				int mouseY = event.mouse.y;
+				// Calculate selection from mouse Y position
+				// From assembly: DAT_00459988 = ((mouseY + 100) - (param_3 * -5 + 0x67)) / 10
+				int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
+
+				// Clamp to valid range
+				if (newSelection < 0) newSelection = 0;
+				if (newSelection >= _menuItemCount) newSelection = _menuItemCount - 1;
+
+				// Only update if within menu area (not too far above/below)
+				int topY = baseY - 5;
+				int bottomY = baseY + (_menuItemCount - 1) * itemSpacing + 10;
+				if (mouseY >= topY && mouseY <= bottomY) {
+					if (newSelection != _menuSelection) {
+						_menuSelection = newSelection;
+						debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
+					}
+				}
+			}
 			_vm->_mouse.x = event.mouse.x;
 			_vm->_mouse.y = event.mouse.y;
 			break;
 
 		case Common::EVENT_QUIT:
 		case Common::EVENT_RETURN_TO_LAUNCHER:
-			// Handle quit request
-			result = 5;  // Quit option
+			// Handle quit request - select quit option
+			result = _menuItemCount - 1;
 			break;
 
 		default:
@@ -5497,147 +5557,331 @@ int InsaneRebel2::processMenuInput() {
 		}
 	}
 
+	// Decrement repeat delay counter (for smooth keyboard navigation)
+	if (_menuRepeatDelay > 0) {
+		_menuRepeatDelay--;
+	}
+
 	return result;
 }
 
 void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// Emulates FUN_0041f5ae menu text overlay rendering
+	// =====================================================================
+	// Emulates FUN_0041f5ae - Menu Text Overlay Renderer
+	// Address: 0x41F5AE
+	// =====================================================================
+	//
+	// Call chain from main menu:
+	//   FUN_004147b2 (Main Menu Handler) -> FUN_0041f5ae (this function)
 	//
 	// IMPORTANT: The menu background comes from the O_MENU_X.SAN video file, NOT from MSTOVER.NUT.
 	// The O_MENU_X.SAN files (A through O) each contain a full 320x200 FOBJ frame in Frame 0
 	// which is decoded by SmushPlayer and stored in renderBitmap before this function is called.
 	// MSTOVER.NUT is only used in cheat mode (when DAT_0047aba4 != 0).
 	//
-	// From FUN_4147B2: param_3 = (DAT_0047a806 == 0) + 6 = 7 for keyboard mode
-	// Menu structure: 1 title + 6 selectable items = 7 total items
-	static const char *menuItems[] = {
-		"GAME MAIN MENU",   // Title (index 0)
-		"START GAME",       // Item 0 (index 1)
-		"OPTIONS",          // Item 1 (index 2)
-		"CONTINUE INTRO",   // Item 2 (index 3)
-		"SHOW TOP PILOTS",  // Item 3 (index 4)
-		"SHOW CREDITS",     // Item 4 (index 5)
-		"QUIT"              // Item 5 (index 6)
+	// FUN_0041f5ae parameters:
+	//   param_1: 0 = render mode, 1 = init mode (reset selection)
+	//   param_2: Array of string pointers (menu items from TRS)
+	//   param_3: Number of selectable menu items
+	//   param_4: 0 = main menu (keyboard), 1 = level selection menu
+	//
+	// Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
+	//   TRS index 10: "^f02Game Main Menu"           -> Title (uses TITLFONT)
+	//   TRS index 11: "^f01^c005Start Game"          -> Item 0 (uses SMALFONT, color 5)
+	//   TRS index 12: "^f01^c009Options"             -> Item 1 (uses SMALFONT, color 9)
+	//   TRS index 13: "^f01^c009Calibrate Joystick"  -> Item 2
+	//   TRS index 14: "^f01^c009Continue Intro"      -> Item 3
+	//   TRS index 15: "^f01^c009Show Top Pilots"     -> Item 4
+	//   TRS index 16: "^f01^c009Show Credits"        -> Item 5
+	//   TRS index 17: "^f01^c240Return to Launcher"  -> Item 6 (color 240)
+
+	// Load menu strings from GAME.TRS via SmushPlayer
+	// TRS indices 10-17 correspond to main menu items (from FUN_00414073)
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	const char *menuItems[8];
+	static const char *fallbackItems[] = {
+		"GAME MAIN MENU", "START GAME", "OPTIONS", "CALIBRATE JOYSTICK",
+		"CONTINUE INTRO", "SHOW TOP PILOTS", "SHOW CREDITS", "RETURN TO LAUNCHER"
 	};
 
-	const int numItemsTotal = 7;  // Title + 6 menu options (used for Y calculations)
-	const int numSelectableItems = 6;  // Selectable menu options (0-5)
+	for (int i = 0; i < 8; i++) {
+		const char *trsStr = splayer ? splayer->getString(10 + i) : nullptr;
+		menuItems[i] = (trsStr && trsStr[0]) ? trsStr : fallbackItems[i];
+	}
 
-	// The O_MENU_X.SAN video frame is already in renderBitmap from SmushPlayer.
-	// We only draw text and selection highlights on top.
+	const int numItemsTotal = 8;  // Title + 7 menu options (matching assembly)
+	const int numSelectableItems = 7;  // Selectable menu options (0-6)
 
-	// From assembly FUN_0041f5ae (low-res mode, DAT_0047a808 < 2):
-	// Center X = 160 for 320px, scale for actual width
-	// Title Y = 46, Item base Y = 69, spacing = 10
-	const int centerX = width / 2;  // Center in actual buffer
-	const int titleY = numItemsTotal * -5 + 0x51;  // 46
-	const int itemBaseY = numItemsTotal * -5 + 0x68;  // 69
-	const int itemSpacing = 10;
+	// =====================================================================
+	// Coordinate calculations from FUN_0041f5ae (lines 18-32)
+	// =====================================================================
+	// Low-res mode (DAT_0047a808 < 2):
+	//   Title Y: param_3 * -5 + 0x51 = 8 * -5 + 81 = 41  (line 19)
+	//   Item base Y: param_3 * -5 + 0x68 = 8 * -5 + 104 = 64  (line 28)
+	//   Center X: ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160  (line 25)
+	//
+	// High-res mode (DAT_0047a808 >= 2):
+	//   Title Y: (param_3 * -5 + 0x5a) * 2 + -0x12  (line 22-23)
+	//   Item Y: (param_3 * -5 + 0x5a + i * 10) * 2 + 0x1c  (line 31)
+	//   Center X: 320
+	const int centerX = width / 2;  // 160 for 320px width
+	const int titleY = numItemsTotal * -5 + 0x51;  // 41
+	const int itemBaseY = numItemsTotal * -5 + 0x68;  // 64
+	const int itemSpacing = 10;  // Line 28: local_c * 10
 
 	debug(5, "drawMenuOverlay: buffer %dx%d, centerX=%d", width, height, centerX);
 
-	// Use SMALFONT.NUT for menu text rendering
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font) {
-		debug(1, "drawMenuOverlay: font is NULL!");
+	// =====================================================================
+	// Font system - Emulates linked list from FUN_00403bd0 (lines 302-348)
+	// =====================================================================
+	// Font linked list structure (DAT_00485058):
+	//   offset 0x00: pointer to previous font in chain
+	//   offset 0x04: pointer to next font in chain
+	//   offset 0x08: pointer to font data (NUT)
+	//   offset 0x0C: font index/ID
+	//   offset 0x0E: chain terminator flag (0 = end of chain)
+	//
+	// Fonts loaded in low-res mode:
+	//   Font 0 (^f00): TALKFONT.NUT - DAT_00485058 (root)
+	//   Font 1 (^f01): SMALFONT.NUT - linked via offset 0x04
+	//   Font 2 (^f02): TITLFONT.NUT - linked via offset 0x04
+	NutRenderer *fonts[3] = {
+		_smush_talkfontNut,   // Font 0 - TALKFONT.NUT (default)
+		_smush_smalfontNut,   // Font 1 - SMALFONT.NUT (menu items)
+		_smush_titlefontNut   // Font 2 - TITLFONT.NUT (title)
+	};
+
+	// FUN_004341a0 line 35-37: Default font when param_1 == -1
+	// if (param_1 == (int *)0xffffffff) param_1 = DAT_00485058;
+	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+	if (!defaultFont) {
+		debug(1, "drawMenuOverlay: no fonts available!");
 		return;
 	}
 
-	// Get the number of characters in the font
-	int numFontChars = font->getNumChars();
-	debug(5, "drawMenuOverlay: font has %d chars", numFontChars);
+	// Set up clipRect for the entire rendering area
+	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	int actualPitch = _vm->_screenWidth;
 
-	// Helper: calculate string width with bounds checking
+	// =====================================================================
+	// Format code parser - Emulates FUN_00434d10 and FUN_00433da0
+	// =====================================================================
+	// Format codes parsed by the original game:
+	//   0x5e 0x5e (^^)   : Literal ^ character (FUN_00434d10 line 34-36)
+	//   0x5e 0x66 (^f)   : Font switch ^fNN (FUN_00434d10 lines 41-66)
+	//   0x5e 0x63 (^c)   : Color code ^cNNN (FUN_00434d10 lines 70-84)
+	//   0x5e 0x6c (^l)   : Newline
+	//
+	// Font switching algorithm from FUN_00433da0 (lines 86-107):
+	//   sVar4 = cVar1 * 10 + (short)*pcVar5;  // Parse 2-digit font index
+	//   Font index 0x210 (528) = '0'*10 + '0' = font 0
+	//   Font index 0x211 (529) = '0'*10 + '1' = font 1
+	//   Font index 0x212 (530) = '0'*10 + '2' = font 2
+	//
+	// Color parsing from FUN_00434d10 (line 81):
+	//   color = (digit1 - '0') * 100 + (digit2 - '0') * 10 + (digit3 - '0')
+	//   e.g., ^c005 -> color = 5, ^c240 -> color = 240
+	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
+		if (*str != '^') return -1;  // 0x5e check
+
+		const char *p = str + 1;
+		if (*p == '^') {
+			// ^^ = literal ^ (FUN_00434d10 line 34-36)
+			str = p;
+			return -1;  // Process ^ as character
+		}
+		if (*p == 'f') {
+			// ^fNN = font switch (FUN_00434d10 lines 41-66)
+			// Parse 2-digit font index: sVar4 = cVar1 * 10 + (short)*pcVar5
+			p++;
+			int fontIdx = 0;
+			while (*p >= '0' && *p <= '9') {
+				fontIdx = fontIdx * 10 + (*p - '0');
+				p++;
+			}
+			str = p;
+			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
+		}
+		if (*p == 'c') {
+			// ^cNNN = color code (FUN_00434d10 lines 70-84)
+			// Parse 3-digit color: (d1-'0')*100 + (d2-'0')*10 + (d3-'0')
+			p++;
+			int color = 0;
+			while (*p >= '0' && *p <= '9') {
+				color = color * 10 + (*p - '0');
+				p++;
+			}
+			str = p;
+			outColor = color;
+			return -2;  // Color changed, no font change
+		}
+		if (*p == 'l') {
+			// ^l = newline
+			str = p + 1;
+			return -2;
+		}
+		// Unknown code, skip
+		return -1;
+	};
+
+	// =====================================================================
+	// String width calculation - Emulates FUN_00433da0
+	// Address: 0x433DA0
+	// =====================================================================
+	// Iterates through string, parsing format codes and summing character widths
+	// Character width lookup (FUN_00433da0 lines 129-133):
+	//   piVar2 = (int *)DAT_0046a5e4[2];  // Font data pointer
+	//   sVar8 = sVar8 + *(short *)(*piVar2 + 0x16 + iVar3);  // Add char width
 	auto getStringWidth = [&](const char *str) -> int {
 		int w = 0;
+		NutRenderer *curFont = defaultFont;
+		int curColor = -1;
+
 		while (*str) {
+			int fontChange = parseFormatCode(str, curColor);
+			if (fontChange >= 0) {
+				// Font switch via linked list traversal (FUN_00433da0 lines 94-106)
+				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
+				continue;
+			}
+			if (fontChange == -2) continue;  // Color or newline code
+
 			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-			if (c < numFontChars) {
-				w += font->getCharWidth(c);
+			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';  // Uppercase conversion
+			if (curFont && c < curFont->getNumChars()) {
+				w += curFont->getCharWidth(c);
 			}
 		}
 		return w;
 	};
 
-	// Debug: Check if renderBitmap has video content (non-zero pixels)
-	static int debugFrameCount = 0;
-	if (debugFrameCount < 5) {
-		int nonZeroCount = 0;
-		for (int i = 0; i < MIN(width * height, 1000); i++) {
-			if (renderBitmap[i] != 0) nonZeroCount++;
-		}
-		debug(1, "drawMenuOverlay: frame %d, buffer sample has %d non-zero pixels in first 1000",
-		      debugFrameCount, nonZeroCount);
-		debugFrameCount++;
-	}
-
-	// Set up clipRect for the entire rendering area
-	// Use screen dimensions (320x200) for the clip rect, not the video frame dimensions.
-	// The menu text is designed for a 320x200 screen, and the underlying renderBitmap
-	// (from SmushPlayer's virtual screen buffer) is always screen-sized. The width/height
-	// parameters come from _player->_width/_height which may be smaller if the video
-	// frame doesn't cover the full screen, but that would incorrectly clip the menu text.
-	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
-
-	// Use screen width as the actual pitch for rendering. During SMUSH playback,
-	// SmushPlayer sets vs->pitch = vs->w = _screenWidth (see smush_player.cpp init()).
-	// The 'pitch' parameter comes from _player->_width which may differ from the actual
-	// buffer pitch if the video frame has different dimensions. Using the wrong pitch
-	// would cause character rows to be written at incorrect offsets in the buffer.
-	int actualPitch = _vm->_screenWidth;
-
+	// =====================================================================
+	// String rendering - Emulates FUN_00434d10
+	// Address: 0x434D10
+	// =====================================================================
+	// Renders string character-by-character with format code support
+	// Calls FUN_004236e0 for each character glyph (FUN_00434d10 lines 96-97)
+	//
+	// Color handling analysis from assembly:
+	//   FUN_00434d10 parses ^cNNN and passes color to FUN_004236e0 as param_8
+	//   FUN_004236e0 passes it to FUN_0042cba0, which passes to codec functions
+	//   In the codecs (FUN_0042cc50, FUN_0042ce90), the color is stored in
+	//   _DAT_00483fd0 and used as a CLIPPING BOUNDARY, not for pixel coloring!
+	//
+	//   The actual pixel colors come from the NUT font's embedded palette data.
+	//   Each font (TALKFONT, SMALFONT, TITLFONT) has its own color scheme.
+	//   The ^cNNN codes in TRS strings are metadata, not used for NUT font
+	//   pixel rendering - the codecs write pixel data directly from the font.
+	//
+	// Therefore: Always use hardcodedColors=true to render fonts with their
+	// embedded palette colors, which matches the original behavior.
 	auto drawString = [&](const char *str, int x, int y) {
+		NutRenderer *curFont = defaultFont;
+		int curColor = -1;  // Parsed but not used for NUT font pixel rendering
+
 		while (*str) {
+			int fontChange = parseFormatCode(str, curColor);
+			if (fontChange >= 0) {
+				// Font switch (FUN_00434d10 lines 41-66)
+				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
+				continue;
+			}
+			if (fontChange == -2) continue;  // Color code parsed (not used for NUT)
+
 			byte c = (byte)*str++;
 			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
 
-			// Skip characters outside font range
-			if (c >= numFontChars) {
-				debug(5, "drawMenuOverlay: char %d out of range (max %d)", c, numFontChars);
-				continue;
-			}
+			if (!curFont) continue;
+			int numChars = curFont->getNumChars();
+			if (c >= numChars) continue;
 
-			int charW = font->getCharWidth(c);
+			int charW = curFont->getCharWidth(c);
 
-			// Use NutRenderer's drawCharV7 which properly handles character rendering
-			// col=-1 means use original font colors, hardcodedColors=true, smushColorMode=true
+			// FUN_004236e0 -> FUN_0042cba0 -> codec: Render character glyph
+			// NUT fonts contain embedded palette indices in their pixel data.
+			// The codec writes these values directly without color transformation.
+			// Use hardcodedColors=true and smushColorMode=true for proper rendering.
 			if (x >= 0 && y >= 0 && charW > 0) {
-				font->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
-				                 kStyleAlignLeft, c, true, true);
+				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
+				                    kStyleAlignLeft, c, true, true);
 			}
 			x += charW;
 		}
 	};
 
-	// Draw title centered at Y = 46
+	// =====================================================================
+	// Draw title - FUN_0041f5ae lines 24-25
+	// =====================================================================
+	// FUN_004341a0((int *)0xffffffff, DAT_0047a7d0, DAT_0047a7d8,
+	//              ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0, sVar2, 1, 0, 1, (char *)*param_2);
+	// X position: centerX (160 for low-res)
+	// Y position: sVar2 = param_3 * -5 + 0x51
 	{
 		int titleWidth = getStringWidth(menuItems[0]);
-		int titleX = centerX - titleWidth / 2;
+		int titleX = centerX - titleWidth / 2;  // Center alignment (param_8 & 1)
 		drawString(menuItems[0], titleX, titleY);
 	}
 
-	// Draw menu items starting at Y = 69
+	// =====================================================================
+	// Draw menu items - FUN_0041f5ae lines 26-48
+	// =====================================================================
+	// for (local_c = 0; local_c < param_3; local_c = local_c + 1) {
+	//     FUN_004341a0(..., (char *)param_2[local_c + 1]);
+	//     if (DAT_00459988 == local_c) {  // Selected item
+	//         sVar2 = FUN_00433da0(...);  // Get string width
+	//         FUN_004292d0(...);          // Draw selection box
+	//     }
+	// }
 	for (int i = 0; i < numSelectableItems; i++) {
+		// Item Y calculation (FUN_0041f5ae line 28):
+		// sVar2 = param_3 * -5 + local_c * 10 + 0x68
 		int itemY = itemBaseY + i * itemSpacing;
 		const char *text = menuItems[i + 1];
 
 		int textWidth = getStringWidth(text);
-		int textX = centerX - textWidth / 2;
+		int textX = centerX - textWidth / 2;  // Center alignment
 		drawString(text, textX, itemY);
 
-		// Draw selection highlight box around selected item
+		// =====================================================================
+		// Selection highlight box - FUN_0041f5ae lines 36-47
+		// Calls FUN_004292d0 (Rectangle Drawer) at 0x4292D0
+		// =====================================================================
+		// if (DAT_00459988 == local_c) {
+		//     sVar2 = FUN_00433da0((int *)0xffffffff, (byte *)param_2[local_c + 1]);
+		//     sVar2 = sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6;  // Width padding
+		//     FUN_004292d0(DAT_0047a7d0, DAT_0047a7d8,
+		//                  (((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0) - sVar2 / 2,  // X
+		//                  sVar1,  // Y = param_3 * -5 + local_c * 10 + 0x67
+		//                  sVar2,  // Width
+		//                  ((DAT_0047a808 < 2) - 1 & 10) + 10,  // Height = 10
+		//                  (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10);  // Color
+		// }
 		if (i == _menuSelection) {
-			int bracketWidth = textWidth + 12;
+			// Width: sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6
+			// For low-res: textWidth + (0 & 6) + 6 = textWidth + 6
+			int bracketWidth = textWidth + 6;
+
+			// Height: ((DAT_0047a808 < 2) - 1 & 10) + 10
+			// For low-res: (0 & 10) + 10 = 10
 			int bracketHeight = 10;
-			byte highlightColor = 255;  // White/bright color for visibility
 
+			// Flash color (FUN_0041f5ae line 47):
+			// (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
+			// When mouse button NOT pressed (bit 0 == 0): (-1 & 8) - 16 = 8 - 16 = -8 = 248
+			// When mouse button pressed (bit 0 == 1): (0 & 8) - 16 = -16 = 240
+			static int frameCounter = 0;
+			frameCounter++;
+			byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
+
+			// Box position (FUN_0041f5ae lines 40, 45-46):
+			// X: centerX - sVar2 / 2
+			// Y: sVar1 = param_3 * -5 + local_c * 10 + 0x67 (one pixel above text)
 			int leftX = centerX - bracketWidth / 2;
 			int rightX = centerX + bracketWidth / 2;
-			int topY = itemY - 1;
+			int topY = itemY - 1;  // 0x67 vs 0x68 = 1 pixel difference
 			int bottomY = itemY + bracketHeight - 1;
 
-			// Clamp to screen bounds (use screen dimensions, not video frame dimensions)
+			// Clamp to screen bounds
 			int screenW = _vm->_screenWidth;
 			int screenH = _vm->_screenHeight;
 			if (leftX < 0) leftX = 0;
@@ -5645,18 +5889,25 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 			if (topY < 0) topY = 0;
 			if (bottomY >= screenH) bottomY = screenH - 1;
 
-			// Draw selection rectangle using actualPitch (screen width)
+			// =====================================================================
+			// FUN_004292d0 - Rectangle Drawer (Address: 0x4292D0)
+			// =====================================================================
+			// Draws rectangle border as 4 lines:
+			//   FUN_004290d0(param_1, param_2, x, y, width, color);      // Top horizontal
+			//   FUN_004291d0(param_1, param_2, x, y, height, color);     // Left vertical
+			//   FUN_004291d0(param_1, param_2, x+w-1, y, height, color); // Right vertical
+			//   FUN_004290d0(param_1, param_2, x, y+h-1, width, color);  // Bottom horizontal
 			for (int x = leftX; x <= rightX && x < screenW; x++) {
 				if (topY >= 0 && topY < screenH)
-					renderBitmap[topY * actualPitch + x] = highlightColor;
+					renderBitmap[topY * actualPitch + x] = highlightColor;  // Top line
 				if (bottomY >= 0 && bottomY < screenH)
-					renderBitmap[bottomY * actualPitch + x] = highlightColor;
+					renderBitmap[bottomY * actualPitch + x] = highlightColor;  // Bottom line
 			}
 			for (int py = topY; py <= bottomY && py < screenH; py++) {
 				if (leftX >= 0 && leftX < screenW)
-					renderBitmap[py * actualPitch + leftX] = highlightColor;
+					renderBitmap[py * actualPitch + leftX] = highlightColor;  // Left line
 				if (rightX >= 0 && rightX < screenW)
-					renderBitmap[py * actualPitch + rightX] = highlightColor;
+					renderBitmap[py * actualPitch + rightX] = highlightColor;  // Right line
 			}
 		}
 	}
@@ -5880,55 +6131,62 @@ int InsaneRebel2::runMainMenu() {
 		debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
 
 		// Process the menu result based on current selection
-		// Menu items (from FUN_004147B2 disassembly):
-		// case 0: return 2 (New Game)
-		// case 1: return 4 (Continue)
-		// case 2: Options menu (stays in loop)
-		// case 3: return 0 (Exit)
-		// case 4: Unknown function
-		// case 5: Credits (play O_CREDIT.SAN, then return 1)
-		// case 6: Quit (stop video, exit)
+		// Menu items matching GAME.TRS indices 11-17 (FUN_004147B2):
+		//   case 0 (TRS 11): Start Game -> pilot selection, returns 2
+		//   case 1 (TRS 12): Options -> FUN_00416787 options screen
+		//   case 2 (TRS 13): Calibrate Joystick -> FUN_00425820
+		//   case 3 (TRS 14): Continue Intro -> replay O_OPEN videos
+		//   case 4 (TRS 15): Show Top Pilots -> FUN_00420116(-1)
+		//   case 5 (TRS 16): Show Credits -> play O_CREDIT.SAN, returns 1
+		//   case 6 (TRS 17): Return to Launcher -> quit, returns 0
 		switch (_menuSelection) {
-		case 0:  // Start Game -> Pilot Selection
+		case 0:  // Start Game -> go to pilot selection
 			debug("Rebel2: Start Game selected - going to pilot selection");
 			_gameState = kStatePilotSelect;
 			_menuInputActive = false;
-			return kMenuContinue;  // Go to pilot selection
+			return kMenuNewGame;  // Return 2 (kMenuNewGame)
 
-		case 1:  // Continue (same as Start Game for now)
-			debug("Rebel2: Continue selected - going to pilot selection");
-			_gameState = kStatePilotSelect;
-			_menuInputActive = false;
-			return kMenuContinue;
-
-		case 2:  // Options
+		case 1:  // Options -> show options menu
 			debug("Rebel2: Options selected");
-			// TODO: Show options menu (FUN_00406ed2)
-			// For now, just continue menu loop
+			// TODO: Implement options menu (FUN_004167a6)
+			// Options: Music, Sound, Voices, Auto Control, Indicators,
+			// Arrows, Difficulty (0-5), Music Volume, SFX Volume
 			break;
 
-		case 3:  // Exit (back to title/intro)
-			debug("Rebel2: Exit selected");
-			// Return to menu loop
+		case 2:  // Calibrate Joystick
+			debug("Rebel2: Calibrate Joystick selected");
+			// TODO: Implement joystick calibration (FUN_00425820)
+			// Plays O_CALIB.SAN with joystick calibration prompts
 			break;
 
-		case 4:  // Unknown function (FUN_00420116)
-			debug("Rebel2: Unknown menu item 4 selected");
+		case 3:  // Continue Intro -> replay intro videos
+			debug("Rebel2: Continue Intro selected - replaying intro");
+			// Play intro sequence again (O_OPEN_A/B)
+			splayer->setCurVideoFlags(0x20);
+			splayer->play("OPEN/O_OPEN_A.SAN", 12);
+			if (!_vm->shouldQuit()) {
+				splayer->play("OPEN/O_OPEN_B.SAN", 12);
+			}
+			break;
+
+		case 4:  // Show Top Pilots -> high score display
+			debug("Rebel2: Show Top Pilots selected");
+			// TODO: Implement high score display (FUN_00420116(-1))
 			break;
 
-		case 5:  // Credits
-			debug("Rebel2: Credits selected");
+		case 5:  // Show Credits -> play credits video
+			debug("Rebel2: Show Credits selected - playing O_CREDIT.SAN");
 			_gameState = kStateCredits;
 			splayer->setCurVideoFlags(0x20);
 			splayer->play("OPEN/O_CREDIT.SAN", 12);
 			_gameState = kStateMainMenu;
-			// After credits, return to menu
+			// Returns 1 in original -> stays at stage 1 (main menu)
 			break;
 
-		case 6:  // Quit
-			debug("Rebel2: Quit selected");
+		case 6:  // Return to Launcher -> quit game
+			debug("Rebel2: Return to Launcher selected");
 			_menuInputActive = false;
-			return 0;
+			return 0;  // Return 0 to exit
 
 		default:
 			debug("Rebel2: Unknown menu selection %d", _menuSelection);
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index b87601c85b5..32e0de1006d 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2696,8 +2696,8 @@ Common::Error ScummEngine::go() {
 				break;
 			}
 
-			if (menuResult == InsaneRebel2::kMenuContinue) {
-				// Continue: Show pilot selection screen (FUN_00414A41)
+			if (menuResult == InsaneRebel2::kMenuNewGame || menuResult == InsaneRebel2::kMenuContinue) {
+				// Start Game or Continue: Show pilot selection screen (FUN_00414A41)
 				int pilotResult = rebel->runLevelSelect();
 
 				if (pilotResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {


Commit: 4dd1e28b95fd6385befcd57b5da5b2b236b8706b
    https://github.com/scummvm/scummvm/commit/4dd1e28b95fd6385befcd57b5da5b2b236b8706b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00

Commit Message:
SCUMM: RA2: Play intro correctly from main menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bf9fa303ca8..95396a5c5bc 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -6161,12 +6161,19 @@ int InsaneRebel2::runMainMenu() {
 
 		case 3:  // Continue Intro -> replay intro videos
 			debug("Rebel2: Continue Intro selected - replaying intro");
+			// Temporarily switch to intro state to disable menu overlay
+			// This emulates FUN_004142BD case 0 behavior
+			_gameState = kStateIntro;
+			_menuInputActive = false;
 			// Play intro sequence again (O_OPEN_A/B)
 			splayer->setCurVideoFlags(0x20);
 			splayer->play("OPEN/O_OPEN_A.SAN", 12);
 			if (!_vm->shouldQuit()) {
 				splayer->play("OPEN/O_OPEN_B.SAN", 12);
 			}
+			// Restore menu state
+			_gameState = kStateMainMenu;
+			_menuInputActive = true;
 			break;
 
 		case 4:  // Show Top Pilots -> high score display
@@ -6177,9 +6184,11 @@ int InsaneRebel2::runMainMenu() {
 		case 5:  // Show Credits -> play credits video
 			debug("Rebel2: Show Credits selected - playing O_CREDIT.SAN");
 			_gameState = kStateCredits;
+			_menuInputActive = false;
 			splayer->setCurVideoFlags(0x20);
 			splayer->play("OPEN/O_CREDIT.SAN", 12);
 			_gameState = kStateMainMenu;
+			_menuInputActive = true;
 			// Returns 1 in original -> stays at stage 1 (main menu)
 			break;
 


Commit: 84cf81e8e64f3c4254219ea03229701bbf0dcbf4
    https://github.com/scummvm/scummvm/commit/84cf81e8e64f3c4254219ea03229701bbf0dcbf4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00

Commit Message:
SCUMM: RA2: Load menu strings from TRS files

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 95396a5c5bc..ccd5c017563 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3949,14 +3949,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 
 	// Handle menu input and rendering if in menu mode
-	// Debug: Log menu mode detection on first few frames
-	static int menuDebugFrames = 0;
-	if (menuDebugFrames < 5 && _gameState == kStateMainMenu) {
-		debug("Rebel2: procPostRendering frame check - introPlaying=%d menuMode=%d _menuInputActive=%d flags=0x%x",
-		      introPlaying, menuMode, _menuInputActive, _player ? _player->_curVideoFlags : -1);
-		menuDebugFrames++;
-	}
-
 	if (menuMode) {
 		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
 		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
@@ -5597,16 +5589,21 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 
 	// Load menu strings from GAME.TRS via SmushPlayer
 	// TRS indices 10-17 correspond to main menu items (from FUN_00414073)
+	// No fallback strings - all text must come from TRS for localization support
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	const char *menuItems[8];
-	static const char *fallbackItems[] = {
-		"GAME MAIN MENU", "START GAME", "OPTIONS", "CALIBRATE JOYSTICK",
-		"CONTINUE INTRO", "SHOW TOP PILOTS", "SHOW CREDITS", "RETURN TO LAUNCHER"
-	};
+
+	if (!splayer) {
+		debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
+		return;
+	}
 
 	for (int i = 0; i < 8; i++) {
-		const char *trsStr = splayer ? splayer->getString(10 + i) : nullptr;
-		menuItems[i] = (trsStr && trsStr[0]) ? trsStr : fallbackItems[i];
+		menuItems[i] = splayer->getString(10 + i);
+		if (!menuItems[i] || !menuItems[i][0]) {
+			debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
+			menuItems[i] = "";  // Empty string to avoid crashes
+		}
 	}
 
 	const int numItemsTotal = 8;  // Title + 7 menu options (matching assembly)
@@ -6016,8 +6013,14 @@ void InsaneRebel2::showPauseOverlay() {
 	}
 
 	// Draw "PAUSED" text centered
-	// Use hardcoded "PAUSED" string (TRS string 0x79 is "Quit Game" in RA2)
-	const char *pauseText = "PAUSED";
+	// Try to load from TRS - the exact index may vary by language version
+	// TRS index 80 (0x50) is likely "PAUSED" or equivalent (from DAT_004573f8)
+	// Note: splayer is already defined at the start of this function
+	const char *pauseText = splayer ? splayer->getString(80) : nullptr;
+	if (!pauseText || !pauseText[0]) {
+		// Fallback only if TRS string not available
+		pauseText = "PAUSED";
+	}
 
 	// Draw text using SmushFont if available
 	if (_menuFont) {
@@ -6742,25 +6745,37 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	// - Three preview boxes on right side
 	// - Status bar at bottom: "PILOTS: X  SCORE: Y  RANK:"
 
-	// Chapter names from original game (DAT_00457820)
-	// Format: "CHAPTER X - NAME" for unlocked, "CHAPTER X -" for locked
-	static const char *chapterNames[] = {
-		"THE DREIGHTON TRIANGLE",   // Chapter 1
-		"ASTEROID PURSUIT",         // Chapter 2
-		"ABOARD THE TERROR",        // Chapter 3
-		"TIE FIGHTER ATTACK",       // Chapter 4
-		"DREIGHTON NEBULA",         // Chapter 5
-		"CORELLIA",                 // Chapter 6
-		"SPEEDER PURSUIT",          // Chapter 7
-		"CANYON CHASE",             // Chapter 8
-		"DEATH STAR APPROACH",      // Chapter 9
-		"THE ASTEROID FIELD",       // Chapter 10
-		"INSIDE THE DEATH STAR",    // Chapter 11
-		"TRENCH RUN",               // Chapter 12
-		"THE MAIN REACTOR",         // Chapter 13
-		"ESCAPE FROM YAVIN",        // Chapter 14
-		"FINALE",                   // Chapter 15 is actually FINALE
-	};
+	// Chapter names from GAME.TRS (DAT_00457820 and DAT_00457868)
+	// From FUN_00414073:
+	//   TRS indices 0x28-0x39 (40-57): 18 unlocked chapter strings -> DAT_00457820
+	//   TRS indices 0x3c-0x4d (60-77): 18 locked chapter strings -> DAT_00457868
+	// These contain complete strings like "CHAPTER 1 - THE DREIGHTON TRIANGLE"
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	const char *chapterNamesUnlocked[18];
+	const char *chapterNamesLocked[18];
+
+	if (!splayer) {
+		debug(1, "drawChapterSelectOverlay: SmushPlayer not available for TRS strings!");
+		return;
+	}
+
+	// Load unlocked chapter strings (TRS indices 40-57)
+	for (int i = 0; i < 18; i++) {
+		chapterNamesUnlocked[i] = splayer->getString(40 + i);
+		if (!chapterNamesUnlocked[i] || !chapterNamesUnlocked[i][0]) {
+			debug(1, "drawChapterSelectOverlay: TRS unlocked string %d not found!", 40 + i);
+			chapterNamesUnlocked[i] = "";
+		}
+	}
+
+	// Load locked chapter strings (TRS indices 60-77)
+	for (int i = 0; i < 18; i++) {
+		chapterNamesLocked[i] = splayer->getString(60 + i);
+		if (!chapterNamesLocked[i] || !chapterNamesLocked[i][0]) {
+			debug(1, "drawChapterSelectOverlay: TRS locked string %d not found!", 60 + i);
+			chapterNamesLocked[i] = "";
+		}
+	}
 
 	// Frame counter for flashing selection box
 	static int frameCounter = 0;
@@ -6811,10 +6826,11 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 		int itemY = itemBaseY + i * itemSpacing;
 		bool isSelected = (i == _chapterSelection);
 
-		// Build chapter string: "CHAPTER X - NAME" or "CHAPTER X -"
-		char chapterStr[64];
-		snprintf(chapterStr, sizeof(chapterStr), "CHAPTER %d - %s", i + 1,
-		         _chapterUnlocked[i] ? chapterNames[i] : "");
+		// Get chapter string from TRS (already contains "CHAPTER X - NAME" or "CHAPTER X -")
+		const char *chapterStr = _chapterUnlocked[i] ? chapterNamesUnlocked[i] : chapterNamesLocked[i];
+		if (!chapterStr || !chapterStr[0]) {
+			continue;  // Skip if no string available
+		}
 
 		// Calculate text width for selection box
 		int textWidth = 0;
@@ -6867,9 +6883,13 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 
 	// Draw FINALE (chapter 16 = index 15)
 	// Position: Y = 19 + 15*10 = 169
+	// FINALE uses TRS index 55 (unlocked) or 75 (locked) - index 15 in the arrays
 	int finaleY = itemBaseY + 15 * itemSpacing;
 	bool finaleSelected = (_chapterSelection == 15);
-	const char *finaleStr = "FINALE     -";
+	const char *finaleStr = _chapterUnlocked[15] ? chapterNamesUnlocked[15] : chapterNamesLocked[15];
+	if (!finaleStr || !finaleStr[0]) {
+		finaleStr = "";  // Fallback to empty if TRS not available
+	}
 
 	int finaleWidth = 0;
 	for (const char *c = finaleStr; *c; c++) {
@@ -6911,9 +6931,15 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 
 	// Draw "RETURN TO PILOTS" (index 16)
 	// Position: Y = 19 + 16*10 = 179
+	// This string comes from TRS - likely in DAT_00457400 (indices 89-109)
+	// Using TRS index 89 for "RETURN TO PILOTS" or equivalent
 	int returnY = itemBaseY + 16 * itemSpacing;
 	bool returnSelected = (_chapterSelection == 16);
-	const char *returnStr = "RETURN TO PILOTS";
+	const char *returnStr = splayer->getString(89);
+	if (!returnStr || !returnStr[0]) {
+		debug(1, "drawChapterSelectOverlay: TRS string 89 (RETURN TO PILOTS) not found!");
+		returnStr = "";
+	}
 
 	int returnWidth = 0;
 	for (const char *c = returnStr; *c; c++) {
@@ -6970,11 +6996,22 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	// From FUN_00415CF8 lines 101-103 (unlocked chapter score display):
 	// X = 0x19 + 0x19 = 50 (but we use 23 to align with menu items)
 	// Y = 0xbe = 190
+	// TODO: The status bar format should use TRS strings from DAT_00457400 (indices 89-109)
+	// for proper localization. Currently using minimal display.
 	int statusY = 190;
 	int statusX = 23;  // Align with menu items
 
+	// Build status string using TRS format strings where available
+	// TRS indices 89-109 contain format strings for status display
 	char statusStr[64];
-	snprintf(statusStr, sizeof(statusStr), "PILOTS: %d  SCORE: %d  RANK:", 4, _playerScore);
+	const char *pilotsLabel = splayer->getString(90);  // "PILOTS:" or equivalent
+	const char *scoreLabel = splayer->getString(91);   // "SCORE:" or equivalent
+	if (pilotsLabel && pilotsLabel[0] && scoreLabel && scoreLabel[0]) {
+		snprintf(statusStr, sizeof(statusStr), "%s %d  %s %d", pilotsLabel, 4, scoreLabel, _playerScore);
+	} else {
+		// Minimal fallback if TRS strings not available
+		snprintf(statusStr, sizeof(statusStr), "%d  %d", 4, _playerScore);
+	}
 
 	curX = statusX;
 	for (const char *c = statusStr; *c; c++) {
@@ -7196,24 +7233,32 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	frameCounter++;
 
 	// Pilot selection menu items - matches original structure from FUN_00414A41:
-	// Items 0-5: Pilot save slots (PILOT 1 through PILOT 6)
-	// Item 6: NEW PILOT
+	// Load strings from GAME.TRS via SmushPlayer (indices from FUN_00414073)
+	// TRS indices 0x14-0x23 (20-35) are stored at DAT_004573b8
+	// Items 0-5: Pilot save slots (would come from save data, using TRS placeholder)
+	// Item 6: NEW PILOT (TRS index 20+6=26 or similar)
 	// Item 7: DELETE PILOT
 	// Item 8: COPY PILOT
 	// Item 9: MAIN MENU
-	static const char *pilotItems[] = {
-		"SELECT PILOT",      // Title (index 0) - not selectable
-		"PILOT 1",           // Pilot slot 1 (index 1) - selectable
-		"PILOT 2",           // Pilot slot 2 (index 2) - selectable
-		"PILOT 3",           // Pilot slot 3 (index 3) - selectable
-		"PILOT 4",           // Pilot slot 4 (index 4) - selectable
-		"PILOT 5",           // Pilot slot 5 (index 5) - selectable
-		"PILOT 6",           // Pilot slot 6 (index 6) - selectable
-		"NEW PILOT",         // Create new pilot (index 7) - selectable
-		"DELETE PILOT",      // Delete pilot (index 8) - selectable
-		"COPY PILOT",        // Copy pilot (index 9) - selectable
-		"MAIN MENU"          // Back to menu (index 10) - selectable
-	};
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	const char *pilotItems[11];
+
+	if (!splayer) {
+		debug(1, "drawLevelSelectOverlay: SmushPlayer not available for TRS strings!");
+		return;
+	}
+
+	// TRS index mapping from FUN_00414073:
+	// DAT_004573b8[0-15] = TRS indices 20-35
+	// Title: TRS 20, Pilot slots use save data or TRS template, Options: TRS 26-29 or similar
+	// Load from TRS indices 20-30 for the menu structure
+	for (int i = 0; i < 11; i++) {
+		pilotItems[i] = splayer->getString(20 + i);
+		if (!pilotItems[i] || !pilotItems[i][0]) {
+			debug(1, "drawLevelSelectOverlay: TRS string %d not found!", 20 + i);
+			pilotItems[i] = "";
+		}
+	}
 
 	const int numItemsTotal = 11;     // Title + 10 selectable items
 	const int numSelectableItems = 10;


Commit: 79010d66044dbf0ca9dabc216117c3bcaf24e9d8
    https://github.com/scummvm/scummvm/commit/79010d66044dbf0ca9dabc216117c3bcaf24e9d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:01+02:00

Commit Message:
SCUMM: RA2: Support colored fonts

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/nut_renderer.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ccd5c017563..3d16c682d2c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -5760,21 +5760,22 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 	// Calls FUN_004236e0 for each character glyph (FUN_00434d10 lines 96-97)
 	//
 	// Color handling analysis from assembly:
-	//   FUN_00434d10 parses ^cNNN and passes color to FUN_004236e0 as param_8
-	//   FUN_004236e0 passes it to FUN_0042cba0, which passes to codec functions
-	//   In the codecs (FUN_0042cc50, FUN_0042ce90), the color is stored in
-	//   _DAT_00483fd0 and used as a CLIPPING BOUNDARY, not for pixel coloring!
+	// Color handling for NUT fonts (from assembly analysis):
 	//
-	//   The actual pixel colors come from the NUT font's embedded palette data.
-	//   Each font (TALKFONT, SMALFONT, TITLFONT) has its own color scheme.
-	//   The ^cNNN codes in TRS strings are metadata, not used for NUT font
-	//   pixel rendering - the codecs write pixel data directly from the font.
+	// For codec 44 (used by RA2 fonts): The ^cNNN color IS used for pixel coloring!
+	//   - Font pixels with value 1 are replaced with the ^cNNN color
+	//   - Font pixels with value 255 are replaced with 0
+	//   - Other values are written directly
 	//
-	// Therefore: Always use hardcodedColors=true to render fonts with their
-	// embedded palette colors, which matches the original behavior.
+	// FUN_00434d10 parses ^cNNN and passes the color to FUN_004236e0 as param_8,
+	// which passes it through to the codec for byte 1 substitution.
+	//
+	// In drawCharV7's default mode (hardcodedColors=false, smushColorMode=false):
+	//   dst[i] = (value == 1) ? color : value;
+	// This implements the codec 44 color substitution.
 	auto drawString = [&](const char *str, int x, int y) {
 		NutRenderer *curFont = defaultFont;
-		int curColor = -1;  // Parsed but not used for NUT font pixel rendering
+		int curColor = 1;  // Default color if no ^cNNN specified (white/foreground)
 
 		while (*str) {
 			int fontChange = parseFormatCode(str, curColor);
@@ -5783,7 +5784,7 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
 				continue;
 			}
-			if (fontChange == -2) continue;  // Color code parsed (not used for NUT)
+			if (fontChange == -2) continue;  // Color code parsed, curColor updated
 
 			byte c = (byte)*str++;
 			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
@@ -5795,12 +5796,10 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 			int charW = curFont->getCharWidth(c);
 
 			// FUN_004236e0 -> FUN_0042cba0 -> codec: Render character glyph
-			// NUT fonts contain embedded palette indices in their pixel data.
-			// The codec writes these values directly without color transformation.
-			// Use hardcodedColors=true and smushColorMode=true for proper rendering.
+			// Use default mode for codec 44 color substitution: byte 1 -> curColor
 			if (x >= 0 && y >= 0 && charW > 0) {
-				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, -1,
-				                    kStyleAlignLeft, c, true, true);
+				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
+				                    kStyleAlignLeft, c, false, false);
 			}
 			x += charW;
 		}
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 3448672be24..583a2019d8f 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -439,7 +439,20 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
 			}
 		}
 	} else {
-		if (smushColorMode) {
+		if (hardcodedColors) {
+			// Direct pixel write for NUT fonts with embedded palette colors (e.g., RA2 menu fonts)
+			// This mirrors the version 7 hardcodedColors behavior
+			for (int j = minY; j < height; j++) {
+				for (int i = minX; i < width; i++) {
+					int8 value = *src++;
+					if (value != _chars[chr].transparency)
+						dst[i] = value;
+				}
+				src += clipWdth;
+				dst += pitch;
+			}
+		} else if (smushColorMode) {
+			// SMUSH subtitle color mode: remap specific values
 			for (int j = minY; j < height; j++) {
 				for (int i = minX; i < width; i++) {
 					int8 value = *src++;


Commit: 2bc1457374b257b43da66e35b94013f0d2851de2
    https://github.com/scummvm/scummvm/commit/2bc1457374b257b43da66e35b94013f0d2851de2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:02+02:00

Commit Message:
SCUMM: RA2: Read fonts correctly

Changed paths:
    engines/scumm/nut_renderer.cpp


diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 583a2019d8f..9612093b54d 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -185,7 +185,12 @@ void NutRenderer::loadFont(const char *filename) {
 		// If characters have transparency, then bytes just get skipped and
 		// so there may appear some garbage. That's why we have to fill it
 		// with a default color first.
-		if (codec == 44) {
+		//
+		// For codec 44: standard SCUMM v7/v8 fonts use value 2 as the
+		// transparent color. But RA2 codec 44 fonts use value 2 as an actual
+		// glyph color (medium body shade), so we must use 0 instead to avoid
+		// making those pixels invisible during rendering.
+		if (codec == 44 && _vm->_game.id != GID_REBEL2) {
 			memset(_chars[l].src, kSmush44TransparentColor, _chars[l].width * _chars[l].height);
 			_chars[l].transparency = kSmush44TransparentColor;
 		} else {
@@ -293,7 +298,9 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
 
 		decodedPtr += (_chars[l].width * _chars[l].height);
 
-		if (codec == 44) {
+		// Same transparency logic as loadFont: RA2 codec 44 fonts use
+		// value 2 as a glyph color, not as transparency.
+		if (codec == 44 && _vm->_game.id != GID_REBEL2) {
 			memset(_chars[l].src, kSmush44TransparentColor, _chars[l].width * _chars[l].height);
 			_chars[l].transparency = kSmush44TransparentColor;
 		} else {
@@ -414,7 +421,20 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
 	int clipWdth = (_chars[chr].width - width);
 	char color = (col != -1) ? col : 1;
 
-	if (_vm->_game.version == 7) {
+	if (_vm->_game.id == GID_REBEL2) {
+		// RA2 codec 44 fonts use pixel values 1-4 as body/gradient.
+		// The AHDR palette indices don't match the SMUSH video palette,
+		// so all non-transparent pixels get the caller's text color.
+		for (int j = minY; j < height; j++) {
+			for (int i = minX; i < width; i++) {
+				int8 value = *src++;
+				if (value != _chars[chr].transparency)
+					dst[i] = color;
+			}
+			src += clipWdth;
+			dst += pitch;
+		}
+	} else if (_vm->_game.version == 7) {
 		if (hardcodedColors) {
 			for (int j = minY; j < height; j++) {
 				for (int i = minX; i < width; i++) {
@@ -440,8 +460,7 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
 		}
 	} else {
 		if (hardcodedColors) {
-			// Direct pixel write for NUT fonts with embedded palette colors (e.g., RA2 menu fonts)
-			// This mirrors the version 7 hardcodedColors behavior
+			// Direct pixel write for NUT fonts with embedded palette colors
 			for (int j = minY; j < height; j++) {
 				for (int i = minX; i < width; i++) {
 					int8 value = *src++;


Commit: 8ea7634eeea92ebfdc891c94caf83c4df2cebe0e
    https://github.com/scummvm/scummvm/commit/8ea7634eeea92ebfdc891c94caf83c4df2cebe0e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:02+02:00

Commit Message:
SCUMM: RA2: Implement damage effect

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3d16c682d2c..782e83b356d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -128,9 +128,14 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_viewX = 0;
 	_viewY = 0;
 
+	// Damage visual effect counters (FUN_420515/420562/420754/42073B)
+	_damageFlashCounter = 0;
+	_damageHighFlashCounter = 0;
+	_damageShakeCounter = 0;
+	memset(_damageSavedPalette, 0, sizeof(_damageSavedPalette));
+
 	// Retail globals mapped: hit counter, cooldown, invulnerability flag
 	_rebelHitCounter = 0;
-	_rebelHitCooldown = 0;
 	_rebelInvulnerable = false;
 
 	// Opcode 6 state variables
@@ -1243,70 +1248,57 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 	}
 }
 void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// Handle IACT opcode 3 subcases (damage, counters, special 100 branch)
-	// Mirrors retail FUN_0041CADB case 1 behavior where possible.
-
-	// Very small cooldown counter decremented on each IACT to emulate DAT_0045790a behavior
-	if (_rebelHitCooldown > 0) _rebelHitCooldown--;
+	// FUN_00401234 case 1 (Handler 8): IACT opcode 3 — damage and hit counter.
+	//
+	// par3 == 5: Damage path — probability check, accumulate damage, trigger visual effect
+	//   Original reads probability from per-level table DAT_0047e0fc[levelIdx]
+	//   and damage amount from DAT_0047e0f8[levelIdx].
+	//   triggerDamageEffect (FUN_0042073B) is called OUTSIDE invulnerability check.
+	//
+	// par3 == 1: Hit counter increment (DAT_0047ab80)
 
-	// Subcase: par3 == 5 -> damage logic, expects extra param at +10 (source enemy ID)
 	if (par3 == 5) {
-		b.skip(2); // Offset +8
-		int16 srcId = b.readSint16LE(); // Offset +10 (Enemy ID)
+		b.skip(2); // Offset +8 (unused)
+		int16 srcId = b.readSint16LE(); // Offset +10: source enemy ID (local_14[5])
 
-		// Only proceed if source is active (bit clear)
-		if (!isBitSet(srcId)) {
-			if (_rebelHitCooldown < 2) {
-				int limit = 20 + _difficulty * 20; // heuristic mapping for probability table
-				if (limit < 5) limit = 5;
-				if (limit > 90) limit = 90;
-				if (_vm->_rnd.getRandomNumber(100) < limit) {
-					// Apply damage unless invulnerable flag set (DAT_0047ab64)
-					if (!_rebelInvulnerable) {
-						int damageAmount = 5 + (_difficulty * 2);
-						// Apply to shields first (do not end game on depletion during tests)
-	
-	
-						// Update the retail-like damage accumulator (DAT_0047a7ec equivalent)
-						_playerDamage += damageAmount;
-						if (_playerDamage > 255) _playerDamage = 255;
-						debug("Rebel2: Damage HIT by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
-						// TODO: call UI update / flash screen / play sound to match retail (FUN_00420515 / FUN_0041189e)
-					}
-					// Impose short cooldown to prevent immediate repeated damage
-					_rebelHitCooldown = 6;
-				}
-			}
-		}
-	}
-	// Subcase: par3 == 1 -> increment hit counter when source active and par4 != 4
-	else if (par3 == 1) {
-		b.skip(2); // read extra param (source id)
-		int16 srcId = b.readSint16LE();
-		if (!isBitSet(srcId) && par4 != 4) {
-			_rebelHitCounter++;
-			debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d (source=%d)", _rebelHitCounter, srcId);
-		}
-	}
-	// Special-case branch when par2 == 100 (retail: triggers damage/sound via different offsets)
-	else if (par2 == 100) {
-		b.skip(2);
-		int16 srcId = b.readSint16LE();
+		debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
+
+		// FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
 		if (!isBitSet(srcId)) {
-			int limit = 20 + _difficulty * 20;
-			if (_vm->_rnd.getRandomNumber(100) < limit) {
+			// Probability check: original reads from per-level table DAT_0047e0fc
+			// TODO: Use actual per-level probability table instead of hardcoded values
+			int probability = 20 + _difficulty * 20;
+			if (probability < 5) probability = 5;
+			if (probability > 90) probability = 90;
+
+			int roll = _vm->_rnd.getRandomNumber(99); // FUN_004233a0(100) returns [0,99]
+			debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
+
+			if (roll < probability) {
+				// Apply damage unless invulnerable (DAT_0047ab64 == 0)
 				if (!_rebelInvulnerable) {
+					// TODO: Read damage amount from per-level table DAT_0047e0f8
 					int damageAmount = 5 + (_difficulty * 2);
-					// Increment the retail-like damage accumulator (DAT_0047a7ec equivalent)
 					_playerDamage += damageAmount;
 					if (_playerDamage > 255) _playerDamage = 255;
-					debug("Rebel2: Damage HIT (special) by Enemy %d. Damage=%d (limit=%d)", srcId, _playerDamage, limit);
+					debug("Rebel2: Damage HIT by Enemy %d. Damage=%d", srcId, _playerDamage);
+				}
+				// Visual effect — called regardless of invulnerability.
+				// Handler 8: FUN_0042073B — palette flash + screen shake
+				// Other handlers: FUN_00420515 — palette flash only (no screen shake)
+				if (_rebelHandler == 8) {
+					triggerDamageEffect();
+				} else {
+					initDamageFlash();
 				}
-				_rebelHitCooldown = 6;
+				// TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) — play hit sound
 			}
 		}
+	} else if (par3 == 1) {
+		// Hit counter increment: unconditional in original (no isBitSet or par4 check)
+		_rebelHitCounter++;
+		debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d", _rebelHitCounter);
 	}
-	// other subcases not implemented yet
 }
 
 void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
@@ -4071,6 +4063,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Laser shot beams and impacts
 	renderLaserShots(renderBitmap, pitch, width, height);
 
+	// Damage visual effects — handler-specific per original architecture:
+	//   Handler 8:    FUN_401CCF line 119 → FUN_00420754 (palette flash + screen shake)
+	//   Handler 0x19: FUN_41DB5E line 192 → FUN_00420562 (palette flash only, every frame)
+	//   Handler 0x26: FUN_4092D9 lines 135/225/237 → FUN_00420515 trigger + palette flash
+	//   Handler 7:    No damage effects
+	if (_rebelHandler == 8) {
+		// Full damage effect: palette flash + screen shake
+		// Suppressed during autopilot (mode 4) and cutscene (mode 5)
+		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+			updateDamageEffect(renderBitmap, pitch, width, height);
+		}
+	} else if (_rebelHandler == 0x19 || _rebelHandler == 0x26) {
+		// Palette flash only — no screen shake for turret/FPS handlers
+		updateDamageFlashPalette();
+	}
+
 	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
 	if (_rebelHandler == 7 || _rebelHandler == 8) {
 		drawCollisionZones(renderBitmap, pitch, width, height, 0);
@@ -4086,6 +4094,143 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	frameEndCleanup();
 }
 
+// ======================= Damage Visual Effect Functions =======================
+// Palette flash + screen shake when the player takes damage.
+// Original retail functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501
+
+// FUN_00420501 - Reset palette flash counter.
+// Called at level start / scene transitions to clear any in-progress flash.
+void InsaneRebel2::resetDamageFlash() {
+	_damageFlashCounter = 0;
+}
+
+// FUN_00420515 - Save current palette and initiate a 5-frame flash.
+// If a flash is already in progress, just resets the counter to 5
+// (the palette was already saved on the first hit).
+void InsaneRebel2::initDamageFlash() {
+	if (_damageFlashCounter == 0) {
+		// Save current SMUSH palette before modifying it
+		memcpy(_damageSavedPalette, _player->_pal, 0x300);
+	}
+	_damageFlashCounter = 5;
+}
+
+// FUN_0042073B - Trigger both palette flash and screen shake.
+// Called from the damage hit handler when the player takes damage.
+void InsaneRebel2::triggerDamageEffect() {
+	initDamageFlash();
+	_damageShakeCounter = 10;
+}
+
+// FUN_00420562 - Per-frame palette modification.
+//
+// Two modes determined by _damageHighFlashCounter:
+//
+//   Normal hit flash (_damageHighFlashCounter == 0 or odd):
+//     Decrements _damageFlashCounter. On even counter values, all 768 palette
+//     bytes (RGB) are blended from inverted toward the saved original:
+//       output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
+//     Counter 5→4(apply)→3(skip)→2(apply)→1(skip)→0(apply=original). The
+//     alternating apply/skip creates a strobe-like flash effect.
+//
+//   High-damage red pulse (_playerDamage >= 0xFF, even counter):
+//     Only the R channel (every 3rd byte) is modified using the same formula
+//     with _damageHighFlashCounter. Creates a pulsing red tint overlay.
+void InsaneRebel2::updateDamageFlashPalette() {
+	// High-damage mode: persistent red pulsing when damage is maxed out
+	if (_playerDamage < 0xFF) {
+		_damageHighFlashCounter = 0;
+	} else {
+		if (_damageHighFlashCounter == 0) {
+			// Save palette on first frame of high-damage mode
+			memcpy(_damageSavedPalette, _player->_pal, 0x300);
+		}
+		if (_damageHighFlashCounter < 0x10) {
+			_damageHighFlashCounter++;
+		}
+	}
+
+	if (_damageHighFlashCounter == 0 || (_damageHighFlashCounter & 1) != 0) {
+		// Normal hit flash path: decrement counter, apply on even values.
+		// Original C: if ((counter != 0) && (counter--, (counter & 1) == 0))
+		if (_damageFlashCounter != 0) {
+			_damageFlashCounter--;
+			if ((_damageFlashCounter & 1) == 0) {
+				// Apply palette inversion on ALL RGB channels
+				byte modPal[0x300];
+				int blend = 0x10 - _damageFlashCounter;
+				for (int i = 0; i < 0x300; i++) {
+					modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
+				}
+				_player->setPalette(modPal);
+			}
+		}
+	} else {
+		// High-damage red-only flash (even _damageHighFlashCounter):
+		// Modify only R channel (stride 3), G and B stay unchanged.
+		byte modPal[0x300];
+		memcpy(modPal, _player->_pal, 0x300);
+		int blend = 0x10 - _damageHighFlashCounter;
+		for (int i = 0; i < 0x300; i += 3) {
+			modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
+		}
+		_player->setPalette(modPal);
+	}
+}
+
+// FUN_00420754 - Per-frame screen shake + palette flash.
+//
+// Screen shake randomly shifts scanlines left or right for visual distortion.
+// The number of affected scanlines decreases each frame (counter * 5),
+// creating a diminishing shake effect over 10 frames.
+//
+// Called every frame from procPostRendering when not in cutscene modes
+// (shipLevelMode != 4 and != 5, matching original: DAT_0043e000 != 4 && != 5).
+void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
+	if (_damageShakeCounter != 0) {
+		_damageShakeCounter--;
+		int numLines = _damageShakeCounter * 5;
+
+		// Temporary buffer for scanline rotation (case 1 in original)
+		byte tempLine[640];
+
+		for (int n = numLines; n > 0; n--) {
+			// Pick a random scanline within the gameplay area (0..179, not status bar)
+			int maxY = MIN(height, 180);
+			int scanline = _vm->_rnd.getRandomNumber(maxY - 1);
+
+			byte *linePtr = renderBitmap + pitch * scanline;
+			int offset = _vm->_rnd.getRandomNumber(4) + 1;  // 1..5 pixel shift
+			int direction = _vm->_rnd.getRandomNumber(4);    // 0..4
+
+			int copyLen = pitch - offset;
+			if (copyLen <= 0)
+				continue;
+
+			switch (direction) {
+			case 0:
+			case 3:
+				// Shift left: copy line[offset..] -> line[0..]
+				memmove(linePtr, linePtr + offset, copyLen);
+				break;
+			case 1:
+				// Shift right with wrap: save, then copy
+				memcpy(tempLine, linePtr, MIN(copyLen, (int)sizeof(tempLine)));
+				memmove(linePtr + offset, tempLine, MIN(copyLen, (int)sizeof(tempLine)));
+				break;
+			case 2:
+			case 4:
+				// Shift right: copy line[0..] -> line[offset..]
+				memmove(linePtr + offset, linePtr, copyLen);
+				break;
+			}
+		}
+	}
+
+	// Palette flash runs every frame (even without shake)
+	updateDamageFlashPalette();
+}
+
 // ======================= Rendering Helper Functions =======================
 // These are extracted from procPostRendering for better readability
 
@@ -4436,9 +4581,14 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 
 	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
 
-	// Secondary ship sprite
-	if (_shipSprite2 && _shipSprite2->getNumChars() > spriteIndex) {
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, spriteIndex);
+	// Shadow sprite (POV004 / DAT_0047e028): drawn at same position as primary ship.
+	// Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index
+	// for both primary and shadow, NOT the direction-based spriteIndex.
+	if (_shipSprite2) {
+		int shadowIndex = _shipFiring ? 1 : 0;
+		if (shadowIndex < _shipSprite2->getNumChars()) {
+			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, shadowIndex);
+		}
 	}
 
 	debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
@@ -7769,6 +7919,9 @@ int InsaneRebel2::runLevel(int levelId) {
 	_playerShield = 255;
 	_playerScore = 0;
 	_playerDamage = 0;
+	resetDamageFlash();
+	_damageHighFlashCounter = 0;
+	_damageShakeCounter = 0;
 	_currentPhase = 1;
 	_phaseScore = 0;
 	_phaseMisses = 0;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 0a32966f5ab..ba460e2fd8f 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -578,9 +578,32 @@ public:
 	int _viewX;
 	int _viewY;
 
+	// ======================= Damage Visual Effect System =======================
+	// Palette flash + screen shake on taking damage.
+	// Original functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B
+	//
+	// FUN_42073B (triggerDamageEffect): Called on damage hit. Initiates palette
+	//   flash via FUN_420515 and sets screen shake counter to 10.
+	// FUN_420515 (initDamageFlash): Saves current palette, sets 5-frame flash.
+	// FUN_420562 (updateDamageFlashPalette): Per-frame palette inversion.
+	//   Normal hit: all RGB channels inverted toward white, fades over 5 frames.
+	//   High damage (>=255): red channel pulsing on even frames.
+	// FUN_420754 (updateDamageEffect): Per-frame screen shake (random scanline
+	//   shifts) + calls FUN_420562. Called every frame from the render loop.
+
+	void triggerDamageEffect();          // FUN_0042073B
+	void initDamageFlash();              // FUN_00420515
+	void updateDamageFlashPalette();     // FUN_00420562
+	void updateDamageEffect(byte *renderBitmap, int pitch, int width, int height); // FUN_00420754
+	void resetDamageFlash();             // FUN_00420501
+
+	int16 _damageFlashCounter;           // DAT_00482404 - palette flash countdown (0..5)
+	int16 _damageHighFlashCounter;       // DAT_00482408 - high-damage red flash (0..16)
+	int16 _damageShakeCounter;           // DAT_0048240c - screen shake countdown (0..10)
+	byte _damageSavedPalette[0x300];     // DAT_00459990 - palette snapshot before flash
+
 	// Rebel per-level counters / flags mapped from retail globals
 	int _rebelHitCounter;    // DAT_0047ab80 - hit counter / state tracker
-	int _rebelHitCooldown;   // DAT_0045790a - cooldown / timing for damage checks
 	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
 
 	// Opcode 6 state variables (from FUN_41CADB case 4)


Commit: 1f53cb0d7ef911560d74d7803bfdaf51007e12f2
    https://github.com/scummvm/scummvm/commit/1f53cb0d7ef911560d74d7803bfdaf51007e12f2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:02+02:00

Commit Message:
SCUMM: RA2: Improve pilot menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 782e83b356d..769f0c56ab0 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -392,9 +392,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_previewOffsetY = 75;  // Chapter 0: 0 * -50 + 75 = 75
 
 	// Initialize pilot selection system (FUN_00414A41)
-	// Menu structure: 6 levels + 4 options (NEW PILOT, DELETE PILOT, COPY PILOT, MAIN MENU)
-	_levelSelection = 0;          // First level selected
-	_levelItemCount = 10;         // 6 levels + 4 options
+	// Menu structure: [saved pilots] + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
+	_levelSelection = 0;          // First item selected
+	_levelItemCount = 4;          // 0 saved pilots + 4 fixed options
 	_selectedLevel = 1;           // Default selected level
 
 	// Initialize menu input capture system
@@ -5707,139 +5707,63 @@ int InsaneRebel2::processMenuInput() {
 	return result;
 }
 
-void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
+void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
+                                  const char **items, int numItems, int selection) {
 	// =====================================================================
-	// Emulates FUN_0041f5ae - Menu Text Overlay Renderer
+	// Shared menu renderer - Emulates FUN_0041f5ae param_4==0 (centered mode)
 	// Address: 0x41F5AE
 	// =====================================================================
 	//
-	// Call chain from main menu:
-	//   FUN_004147b2 (Main Menu Handler) -> FUN_0041f5ae (this function)
-	//
-	// IMPORTANT: The menu background comes from the O_MENU_X.SAN video file, NOT from MSTOVER.NUT.
-	// The O_MENU_X.SAN files (A through O) each contain a full 320x200 FOBJ frame in Frame 0
-	// which is decoded by SmushPlayer and stored in renderBitmap before this function is called.
-	// MSTOVER.NUT is only used in cheat mode (when DAT_0047aba4 != 0).
-	//
-	// FUN_0041f5ae parameters:
-	//   param_1: 0 = render mode, 1 = init mode (reset selection)
-	//   param_2: Array of string pointers (menu items from TRS)
-	//   param_3: Number of selectable menu items
-	//   param_4: 0 = main menu (keyboard), 1 = level selection menu
+	// items[0] = title string, items[1..numItems] = selectable items
+	// numItems = number of selectable items (FUN_0041f5ae param_3)
+	// selection = currently highlighted item (0-based, maps to DAT_00459988)
 	//
-	// Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
-	//   TRS index 10: "^f02Game Main Menu"           -> Title (uses TITLFONT)
-	//   TRS index 11: "^f01^c005Start Game"          -> Item 0 (uses SMALFONT, color 5)
-	//   TRS index 12: "^f01^c009Options"             -> Item 1 (uses SMALFONT, color 9)
-	//   TRS index 13: "^f01^c009Calibrate Joystick"  -> Item 2
-	//   TRS index 14: "^f01^c009Continue Intro"      -> Item 3
-	//   TRS index 15: "^f01^c009Show Top Pilots"     -> Item 4
-	//   TRS index 16: "^f01^c009Show Credits"        -> Item 5
-	//   TRS index 17: "^f01^c240Return to Launcher"  -> Item 6 (color 240)
-
-	// Load menu strings from GAME.TRS via SmushPlayer
-	// TRS indices 10-17 correspond to main menu items (from FUN_00414073)
-	// No fallback strings - all text must come from TRS for localization support
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	const char *menuItems[8];
-
-	if (!splayer) {
-		debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
-		return;
-	}
-
-	for (int i = 0; i < 8; i++) {
-		menuItems[i] = splayer->getString(10 + i);
-		if (!menuItems[i] || !menuItems[i][0]) {
-			debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
-			menuItems[i] = "";  // Empty string to avoid crashes
-		}
-	}
-
-	const int numItemsTotal = 8;  // Title + 7 menu options (matching assembly)
-	const int numSelectableItems = 7;  // Selectable menu options (0-6)
-
-	// =====================================================================
-	// Coordinate calculations from FUN_0041f5ae (lines 18-32)
-	// =====================================================================
-	// Low-res mode (DAT_0047a808 < 2):
-	//   Title Y: param_3 * -5 + 0x51 = 8 * -5 + 81 = 41  (line 19)
-	//   Item base Y: param_3 * -5 + 0x68 = 8 * -5 + 104 = 64  (line 28)
-	//   Center X: ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160  (line 25)
-	//
-	// High-res mode (DAT_0047a808 >= 2):
-	//   Title Y: (param_3 * -5 + 0x5a) * 2 + -0x12  (line 22-23)
-	//   Item Y: (param_3 * -5 + 0x5a + i * 10) * 2 + 0x1c  (line 31)
-	//   Center X: 320
-	const int centerX = width / 2;  // 160 for 320px width
-	const int titleY = numItemsTotal * -5 + 0x51;  // 41
-	const int itemBaseY = numItemsTotal * -5 + 0x68;  // 64
-	const int itemSpacing = 10;  // Line 28: local_c * 10
-
-	debug(5, "drawMenuOverlay: buffer %dx%d, centerX=%d", width, height, centerX);
+	// Coordinate formulas from FUN_0041f5ae (low-res, DAT_0047a808 < 2):
+	//   Title Y:     param_3 * -5 + 0x51           (line 19)
+	//   Item base Y: param_3 * -5 + 0x68           (line 28)
+	//   Item Y:      param_3 * -5 + i * 10 + 0x68  (line 28)
+	//   Center X:    ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160
+	//   Box Y:       param_3 * -5 + i * 10 + 0x67  (1px above text)
+
+	const int centerX = width / 2;
+	const int titleY = numItems * -5 + 0x51;
+	const int itemBaseY = numItems * -5 + 0x68;
+	const int itemSpacing = 10;
 
 	// =====================================================================
-	// Font system - Emulates linked list from FUN_00403bd0 (lines 302-348)
+	// Font system - Emulates linked list from FUN_00403bd0
 	// =====================================================================
-	// Font linked list structure (DAT_00485058):
-	//   offset 0x00: pointer to previous font in chain
-	//   offset 0x04: pointer to next font in chain
-	//   offset 0x08: pointer to font data (NUT)
-	//   offset 0x0C: font index/ID
-	//   offset 0x0E: chain terminator flag (0 = end of chain)
-	//
-	// Fonts loaded in low-res mode:
-	//   Font 0 (^f00): TALKFONT.NUT - DAT_00485058 (root)
-	//   Font 1 (^f01): SMALFONT.NUT - linked via offset 0x04
-	//   Font 2 (^f02): TITLFONT.NUT - linked via offset 0x04
+	//   Font 0 (^f00): TALKFONT.NUT
+	//   Font 1 (^f01): SMALFONT.NUT (menu items)
+	//   Font 2 (^f02): TITLFONT.NUT (title)
 	NutRenderer *fonts[3] = {
-		_smush_talkfontNut,   // Font 0 - TALKFONT.NUT (default)
-		_smush_smalfontNut,   // Font 1 - SMALFONT.NUT (menu items)
-		_smush_titlefontNut   // Font 2 - TITLFONT.NUT (title)
+		_smush_talkfontNut,
+		_smush_smalfontNut,
+		_smush_titlefontNut
 	};
 
-	// FUN_004341a0 line 35-37: Default font when param_1 == -1
-	// if (param_1 == (int *)0xffffffff) param_1 = DAT_00485058;
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
 	if (!defaultFont) {
-		debug(1, "drawMenuOverlay: no fonts available!");
+		debug(1, "drawMenuItems: no fonts available!");
 		return;
 	}
 
-	// Set up clipRect for the entire rendering area
 	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
 	int actualPitch = _vm->_screenWidth;
 
 	// =====================================================================
-	// Format code parser - Emulates FUN_00434d10 and FUN_00433da0
+	// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
 	// =====================================================================
-	// Format codes parsed by the original game:
-	//   0x5e 0x5e (^^)   : Literal ^ character (FUN_00434d10 line 34-36)
-	//   0x5e 0x66 (^f)   : Font switch ^fNN (FUN_00434d10 lines 41-66)
-	//   0x5e 0x63 (^c)   : Color code ^cNNN (FUN_00434d10 lines 70-84)
-	//   0x5e 0x6c (^l)   : Newline
-	//
-	// Font switching algorithm from FUN_00433da0 (lines 86-107):
-	//   sVar4 = cVar1 * 10 + (short)*pcVar5;  // Parse 2-digit font index
-	//   Font index 0x210 (528) = '0'*10 + '0' = font 0
-	//   Font index 0x211 (529) = '0'*10 + '1' = font 1
-	//   Font index 0x212 (530) = '0'*10 + '2' = font 2
-	//
-	// Color parsing from FUN_00434d10 (line 81):
-	//   color = (digit1 - '0') * 100 + (digit2 - '0') * 10 + (digit3 - '0')
-	//   e.g., ^c005 -> color = 5, ^c240 -> color = 240
+	//   ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
 	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
-		if (*str != '^') return -1;  // 0x5e check
+		if (*str != '^') return -1;
 
 		const char *p = str + 1;
 		if (*p == '^') {
-			// ^^ = literal ^ (FUN_00434d10 line 34-36)
 			str = p;
-			return -1;  // Process ^ as character
+			return -1;
 		}
 		if (*p == 'f') {
-			// ^fNN = font switch (FUN_00434d10 lines 41-66)
-			// Parse 2-digit font index: sVar4 = cVar1 * 10 + (short)*pcVar5
 			p++;
 			int fontIdx = 0;
 			while (*p >= '0' && *p <= '9') {
@@ -5850,8 +5774,6 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
 		}
 		if (*p == 'c') {
-			// ^cNNN = color code (FUN_00434d10 lines 70-84)
-			// Parse 3-digit color: (d1-'0')*100 + (d2-'0')*10 + (d3-'0')
 			p++;
 			int color = 0;
 			while (*p >= '0' && *p <= '9') {
@@ -5860,25 +5782,16 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 			}
 			str = p;
 			outColor = color;
-			return -2;  // Color changed, no font change
+			return -2;
 		}
 		if (*p == 'l') {
-			// ^l = newline
 			str = p + 1;
 			return -2;
 		}
-		// Unknown code, skip
 		return -1;
 	};
 
-	// =====================================================================
 	// String width calculation - Emulates FUN_00433da0
-	// Address: 0x433DA0
-	// =====================================================================
-	// Iterates through string, parsing format codes and summing character widths
-	// Character width lookup (FUN_00433da0 lines 129-133):
-	//   piVar2 = (int *)DAT_0046a5e4[2];  // Font data pointer
-	//   sVar8 = sVar8 + *(short *)(*piVar2 + 0x16 + iVar3);  // Add char width
 	auto getStringWidth = [&](const char *str) -> int {
 		int w = 0;
 		NutRenderer *curFont = defaultFont;
@@ -5887,14 +5800,13 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 		while (*str) {
 			int fontChange = parseFormatCode(str, curColor);
 			if (fontChange >= 0) {
-				// Font switch via linked list traversal (FUN_00433da0 lines 94-106)
 				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
 				continue;
 			}
-			if (fontChange == -2) continue;  // Color or newline code
+			if (fontChange == -2) continue;
 
 			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';  // Uppercase conversion
+			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
 			if (curFont && c < curFont->getNumChars()) {
 				w += curFont->getCharWidth(c);
 			}
@@ -5902,39 +5814,19 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 		return w;
 	};
 
-	// =====================================================================
 	// String rendering - Emulates FUN_00434d10
-	// Address: 0x434D10
-	// =====================================================================
-	// Renders string character-by-character with format code support
-	// Calls FUN_004236e0 for each character glyph (FUN_00434d10 lines 96-97)
-	//
-	// Color handling analysis from assembly:
-	// Color handling for NUT fonts (from assembly analysis):
-	//
-	// For codec 44 (used by RA2 fonts): The ^cNNN color IS used for pixel coloring!
-	//   - Font pixels with value 1 are replaced with the ^cNNN color
-	//   - Font pixels with value 255 are replaced with 0
-	//   - Other values are written directly
-	//
-	// FUN_00434d10 parses ^cNNN and passes the color to FUN_004236e0 as param_8,
-	// which passes it through to the codec for byte 1 substitution.
-	//
-	// In drawCharV7's default mode (hardcodedColors=false, smushColorMode=false):
-	//   dst[i] = (value == 1) ? color : value;
-	// This implements the codec 44 color substitution.
+	// Codec 44 color substitution: font pixels with value 1 → ^cNNN color
 	auto drawString = [&](const char *str, int x, int y) {
 		NutRenderer *curFont = defaultFont;
-		int curColor = 1;  // Default color if no ^cNNN specified (white/foreground)
+		int curColor = 1;
 
 		while (*str) {
 			int fontChange = parseFormatCode(str, curColor);
 			if (fontChange >= 0) {
-				// Font switch (FUN_00434d10 lines 41-66)
 				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
 				continue;
 			}
-			if (fontChange == -2) continue;  // Color code parsed, curColor updated
+			if (fontChange == -2) continue;
 
 			byte c = (byte)*str++;
 			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
@@ -5945,8 +5837,6 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 
 			int charW = curFont->getCharWidth(c);
 
-			// FUN_004236e0 -> FUN_0042cba0 -> codec: Render character glyph
-			// Use default mode for codec 44 color substitution: byte 1 -> curColor
 			if (x >= 0 && y >= 0 && charW > 0) {
 				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
 				                    kStyleAlignLeft, c, false, false);
@@ -5956,78 +5846,44 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 	};
 
 	// =====================================================================
-	// Draw title - FUN_0041f5ae lines 24-25
+	// Draw title - items[0], centered at titleY
 	// =====================================================================
-	// FUN_004341a0((int *)0xffffffff, DAT_0047a7d0, DAT_0047a7d8,
-	//              ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0, sVar2, 1, 0, 1, (char *)*param_2);
-	// X position: centerX (160 for low-res)
-	// Y position: sVar2 = param_3 * -5 + 0x51
 	{
-		int titleWidth = getStringWidth(menuItems[0]);
-		int titleX = centerX - titleWidth / 2;  // Center alignment (param_8 & 1)
-		drawString(menuItems[0], titleX, titleY);
+		int titleWidth = getStringWidth(items[0]);
+		int titleX = centerX - titleWidth / 2;
+		drawString(items[0], titleX, titleY);
 	}
 
 	// =====================================================================
-	// Draw menu items - FUN_0041f5ae lines 26-48
+	// Draw selectable items with selection highlight box
 	// =====================================================================
-	// for (local_c = 0; local_c < param_3; local_c = local_c + 1) {
-	//     FUN_004341a0(..., (char *)param_2[local_c + 1]);
-	//     if (DAT_00459988 == local_c) {  // Selected item
-	//         sVar2 = FUN_00433da0(...);  // Get string width
-	//         FUN_004292d0(...);          // Draw selection box
-	//     }
-	// }
-	for (int i = 0; i < numSelectableItems; i++) {
-		// Item Y calculation (FUN_0041f5ae line 28):
-		// sVar2 = param_3 * -5 + local_c * 10 + 0x68
+	for (int i = 0; i < numItems; i++) {
 		int itemY = itemBaseY + i * itemSpacing;
-		const char *text = menuItems[i + 1];
+		const char *text = items[i + 1];
 
 		int textWidth = getStringWidth(text);
-		int textX = centerX - textWidth / 2;  // Center alignment
+		int textX = centerX - textWidth / 2;
 		drawString(text, textX, itemY);
 
-		// =====================================================================
-		// Selection highlight box - FUN_0041f5ae lines 36-47
-		// Calls FUN_004292d0 (Rectangle Drawer) at 0x4292D0
-		// =====================================================================
-		// if (DAT_00459988 == local_c) {
-		//     sVar2 = FUN_00433da0((int *)0xffffffff, (byte *)param_2[local_c + 1]);
-		//     sVar2 = sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6;  // Width padding
-		//     FUN_004292d0(DAT_0047a7d0, DAT_0047a7d8,
-		//                  (((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0) - sVar2 / 2,  // X
-		//                  sVar1,  // Y = param_3 * -5 + local_c * 10 + 0x67
-		//                  sVar2,  // Width
-		//                  ((DAT_0047a808 < 2) - 1 & 10) + 10,  // Height = 10
-		//                  (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10);  // Color
-		// }
-		if (i == _menuSelection) {
-			// Width: sVar2 + ((DAT_0047a808 < 2) - 1 & 6) + 6
-			// For low-res: textWidth + (0 & 6) + 6 = textWidth + 6
+		// Selection highlight box - FUN_004292d0
+		if (i == selection) {
+			// Width: textWidth + ((DAT_0047a808 < 2) - 1 & 6) + 6 = textWidth + 6
 			int bracketWidth = textWidth + 6;
-
-			// Height: ((DAT_0047a808 < 2) - 1 & 10) + 10
-			// For low-res: (0 & 10) + 10 = 10
+			// Height: ((DAT_0047a808 < 2) - 1 & 10) + 10 = 10
 			int bracketHeight = 10;
 
-			// Flash color (FUN_0041f5ae line 47):
-			// (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
-			// When mouse button NOT pressed (bit 0 == 0): (-1 & 8) - 16 = 8 - 16 = -8 = 248
-			// When mouse button pressed (bit 0 == 1): (0 & 8) - 16 = -16 = 240
+			// Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
+			// bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
 			static int frameCounter = 0;
 			frameCounter++;
 			byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
 
-			// Box position (FUN_0041f5ae lines 40, 45-46):
-			// X: centerX - sVar2 / 2
-			// Y: sVar1 = param_3 * -5 + local_c * 10 + 0x67 (one pixel above text)
+			// Box position: X centered, Y = itemY - 1 (0x67 vs 0x68)
 			int leftX = centerX - bracketWidth / 2;
 			int rightX = centerX + bracketWidth / 2;
-			int topY = itemY - 1;  // 0x67 vs 0x68 = 1 pixel difference
+			int topY = itemY - 1;
 			int bottomY = itemY + bracketHeight - 1;
 
-			// Clamp to screen bounds
 			int screenW = _vm->_screenWidth;
 			int screenH = _vm->_screenHeight;
 			if (leftX < 0) leftX = 0;
@@ -6035,30 +5891,59 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 			if (topY < 0) topY = 0;
 			if (bottomY >= screenH) bottomY = screenH - 1;
 
-			// =====================================================================
-			// FUN_004292d0 - Rectangle Drawer (Address: 0x4292D0)
-			// =====================================================================
-			// Draws rectangle border as 4 lines:
-			//   FUN_004290d0(param_1, param_2, x, y, width, color);      // Top horizontal
-			//   FUN_004291d0(param_1, param_2, x, y, height, color);     // Left vertical
-			//   FUN_004291d0(param_1, param_2, x+w-1, y, height, color); // Right vertical
-			//   FUN_004290d0(param_1, param_2, x, y+h-1, width, color);  // Bottom horizontal
+			// FUN_004292d0 - Draw rectangle border (4 lines)
 			for (int x = leftX; x <= rightX && x < screenW; x++) {
 				if (topY >= 0 && topY < screenH)
-					renderBitmap[topY * actualPitch + x] = highlightColor;  // Top line
+					renderBitmap[topY * actualPitch + x] = highlightColor;
 				if (bottomY >= 0 && bottomY < screenH)
-					renderBitmap[bottomY * actualPitch + x] = highlightColor;  // Bottom line
+					renderBitmap[bottomY * actualPitch + x] = highlightColor;
 			}
 			for (int py = topY; py <= bottomY && py < screenH; py++) {
 				if (leftX >= 0 && leftX < screenW)
-					renderBitmap[py * actualPitch + leftX] = highlightColor;  // Left line
+					renderBitmap[py * actualPitch + leftX] = highlightColor;
 				if (rightX >= 0 && rightX < screenW)
-					renderBitmap[py * actualPitch + rightX] = highlightColor;  // Right line
+					renderBitmap[py * actualPitch + rightX] = highlightColor;
 			}
 		}
 	}
 }
 
+void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
+	// =====================================================================
+	// Main menu renderer - calls shared drawMenuItems()
+	// Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
+	// =====================================================================
+	//
+	// Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
+	//   TRS index 10: "^f02Game Main Menu"           -> Title (uses TITLFONT)
+	//   TRS index 11: "^f01^c005Start Game"          -> Item 0 (uses SMALFONT, color 5)
+	//   TRS index 12: "^f01^c009Options"             -> Item 1
+	//   TRS index 13: "^f01^c009Calibrate Joystick"  -> Item 2
+	//   TRS index 14: "^f01^c009Continue Intro"      -> Item 3
+	//   TRS index 15: "^f01^c009Show Top Pilots"     -> Item 4
+	//   TRS index 16: "^f01^c009Show Credits"        -> Item 5
+	//   TRS index 17: "^f01^c240Return to Launcher"  -> Item 6 (color 240)
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer) {
+		debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
+		return;
+	}
+
+	// Load TRS strings 10-17 (title + 7 selectable items)
+	const char *menuItems[8];
+	for (int i = 0; i < 8; i++) {
+		menuItems[i] = splayer->getString(10 + i);
+		if (!menuItems[i] || !menuItems[i][0]) {
+			debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
+			menuItems[i] = "";
+		}
+	}
+
+	// FUN_004147b2 line 25: param_3 = (DAT_0047a806 == 0) + 6 = 7 (keyboard mode)
+	drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
+}
+
 // ======================= Pause Overlay =======================
 // Emulates FUN_405A21 pause rendering (lines 242-305)
 // Creates a dimmed overlay effect and displays "PAUSED" text
@@ -7201,7 +7086,7 @@ int InsaneRebel2::runLevelSelect() {
 	// Item 8: COPY PILOT
 	// Item 9: MAIN MENU
 	_levelSelection = 0;
-	_levelItemCount = 10;  // Selectable items: 6 pilots + 4 options
+	_levelItemCount = 4;  // Selectable items: 0 saved pilots + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
 	_selectedLevel = 1;
 	_menuRepeatDelay = 0;
 	_gameState = kStatePilotSelect;
@@ -7280,11 +7165,9 @@ int InsaneRebel2::processLevelSelectInput() {
 
 	int result = -1;
 
-	// Level menu Y positions (similar to main menu)
-	// Using same formula: base Y = numItemsTotal * -5 + 104
-	// numItemsTotal = 3 (title + 2 selectable items)
-	const int numItemsTotal = 3;
-	const int baseY = numItemsTotal * -5 + 104;  // 89
+	// Level menu Y positions — must match drawMenuItems() formula
+	// itemBaseY = numItems * -5 + 0x68
+	const int baseY = _levelItemCount * -5 + 0x68;
 	const int itemHeight = 10;
 
 	// Process events from the queue (populated by notifyEvent)
@@ -7370,168 +7253,63 @@ int InsaneRebel2::processLevelSelectInput() {
 }
 
 void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// Draw level selection menu overlay
-	// Emulates FUN_0041f5ae for level selection mode
+	// =====================================================================
+	// Pilot selection menu renderer - Emulates FUN_00414A41
+	// =====================================================================
 	//
-	// From info.md - Low Resolution Coordinate Formulas (320x200 mode):
-	// Center X = 160, Title Y = numItems * -5 + 81, Item Base Y = numItems * -5 + 104
-	// Item spacing = 10 pixels, Selection box: width = textWidth + 6, height = 10
-
-	// Frame counter for flashing selection box (emulates DAT_0047a7e4)
-	static int frameCounter = 0;
-	frameCounter++;
+	// FUN_00414A41 builds the menu dynamically:
+	//   items[0]          = DAT_004573b8[0] = TRS 20 (title "PILOTS")
+	//   items[1..N]       = saved pilot slot strings (dynamically formatted)
+	//   items[N+1]        = DAT_004573b8[1] = TRS 21 (ADD NEW PILOT)
+	//   items[N+2]        = DAT_004573b8[2] = TRS 22 (DUPE PILOT)
+	//   items[N+3]        = DAT_004573b8[3] = TRS 23 (DELETE PILOT)
+	//   items[N+4]        = DAT_004573b8[4] = TRS 24 (RETURN TO MAIN MENU)
+	//
+	// FUN_0041f5ae called with param_3 = N + 4 (selectable items)
+	//
+	// TRS 25+ are NOT menu items — they are info format strings
+	// ("DIFFICULTY: %S", "CHAPTER: %HO") rendered separately by FUN_00434cb0
+	// at fixed coordinates when a saved pilot is selected.
 
-	// Pilot selection menu items - matches original structure from FUN_00414A41:
-	// Load strings from GAME.TRS via SmushPlayer (indices from FUN_00414073)
-	// TRS indices 0x14-0x23 (20-35) are stored at DAT_004573b8
-	// Items 0-5: Pilot save slots (would come from save data, using TRS placeholder)
-	// Item 6: NEW PILOT (TRS index 20+6=26 or similar)
-	// Item 7: DELETE PILOT
-	// Item 8: COPY PILOT
-	// Item 9: MAIN MENU
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	const char *pilotItems[11];
-
 	if (!splayer) {
 		debug(1, "drawLevelSelectOverlay: SmushPlayer not available for TRS strings!");
 		return;
 	}
 
-	// TRS index mapping from FUN_00414073:
-	// DAT_004573b8[0-15] = TRS indices 20-35
-	// Title: TRS 20, Pilot slots use save data or TRS template, Options: TRS 26-29 or similar
-	// Load from TRS indices 20-30 for the menu structure
-	for (int i = 0; i < 11; i++) {
-		pilotItems[i] = splayer->getString(20 + i);
-		if (!pilotItems[i] || !pilotItems[i][0]) {
-			debug(1, "drawLevelSelectOverlay: TRS string %d not found!", 20 + i);
-			pilotItems[i] = "";
-		}
-	}
-
-	const int numItemsTotal = 11;     // Title + 10 selectable items
-	const int numSelectableItems = 10;
-
-	// Calculate positions (low-res 320x200 mode)
-	// Formula from FUN_0041F5AE: titleY = numItems * -5 + 81, itemBaseY = numItems * -5 + 104
-	const int centerX = width / 2;  // 160 for 320 width
-	const int titleY = numSelectableItems * -5 + 81;      // 10 * -5 + 81 = 31
-	const int itemBaseY = numSelectableItems * -5 + 104;  // 10 * -5 + 104 = 54
-	const int itemSpacing = 10;
-
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font) {
-		debug(1, "drawLevelSelectOverlay: font is NULL!");
-		return;
-	}
-
-	int numFontChars = font->getNumChars();
-	int actualPitch = pitch;
-	Common::Rect clipRect(0, 0, width, height);
-
-	// Helper function to draw centered text with optional highlight box
-	// We'll use a simple loop instead of lambda for compatibility
-	auto drawTextCentered = [&](const char *text, int y, bool highlight) {
-		// Calculate text width first
-		int textWidth = 0;
-		for (const char *c = text; *c; c++) {
-			int charIdx = (unsigned char)*c;
-			if (charIdx < numFontChars) {
-				textWidth += font->getCharWidth(charIdx);
-			}
-		}
+	// Number of saved pilots (TODO: implement save system, 0 for now)
+	int numPilots = 0;
 
-		int curX = centerX - textWidth / 2;
+	// Build menu item array: title + [pilot slots] + 4 fixed options
+	// Max: 1 title + 6 pilots + 4 options = 11
+	const char *pilotItems[11];
+	int idx = 0;
 
-		// Draw each character using drawCharV7
-		for (const char *c = text; *c; c++) {
-			int charIdx = (unsigned char)*c;
-			if (charIdx < numFontChars) {
-				int charWidth = font->getCharWidth(charIdx);
-				if (curX >= 0 && curX + charWidth <= width && y >= 0) {
-					// Use drawCharV7 with color -1 (original colors), hardcodedColors=true, smushColorMode=true
-					font->drawCharV7(renderBitmap, clipRect, curX, y, actualPitch, -1,
-					                 kStyleAlignLeft, charIdx, true, true);
-				}
-				curX += charWidth;
-			}
-		}
+	// Title: TRS 20
+	pilotItems[idx++] = splayer->getString(20);
 
-		// If highlighted, draw selection box around text
-		if (highlight) {
-			// Box dimensions from FUN_0041F5AE:
-			// Width = textWidth + 6 (low-res), Height = 10
-			int boxWidth = textWidth + 6;
-			int boxHeight = 10;
-			int boxX = centerX - boxWidth / 2;
-			int boxY = y - 1;  // 1 pixel above text (itemY - 1 = numItems * -5 + idx * 10 + 103)
+	// Saved pilot slots would be inserted here (items[1..numPilots])
+	// Each formatted as "^f01^c005<name>^f00" by the original
 
-			// Flashing highlight color (emulates original behavior)
-			// Original uses palette-relative colors -8 and -16 alternating on frameCounter & 1
-			// We use bright colors that approximate the visual effect
-			byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+	// Fixed options: TRS 21-24
+	for (int i = 0; i < 4; i++) {
+		pilotItems[idx++] = splayer->getString(21 + i);
+	}
 
-			// Draw box border (top, bottom, left, right edges)
-			if (boxY >= 0 && boxY < height && boxX >= 0 && boxX + boxWidth <= width) {
-				// Top edge
-				for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
-					if (px >= 0) renderBitmap[boxY * actualPitch + px] = highlightColor;
-				}
-				// Bottom edge
-				int bottomY = boxY + boxHeight - 1;
-				if (bottomY < height) {
-					for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
-						if (px >= 0) renderBitmap[bottomY * actualPitch + px] = highlightColor;
-					}
-				}
-				// Left edge
-				for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
-					if (py >= 0 && boxX >= 0) renderBitmap[py * actualPitch + boxX] = highlightColor;
-				}
-				// Right edge
-				int rightX = boxX + boxWidth - 1;
-				if (rightX < width) {
-					for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
-						if (py >= 0) renderBitmap[py * actualPitch + rightX] = highlightColor;
-					}
-				}
-			}
+	// Validate strings
+	for (int i = 0; i < idx; i++) {
+		if (!pilotItems[i] || !pilotItems[i][0]) {
+			pilotItems[i] = "";
 		}
-	};
+	}
 
-	// Draw title (not selectable)
-	drawTextCentered(pilotItems[0], titleY, false);
+	int numSelectableItems = numPilots + 4;
+	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numSelectableItems, _levelSelection);
 
-	// Draw selectable items
-	for (int i = 0; i < numSelectableItems; i++) {
-		int itemY = itemBaseY + i * itemSpacing;
-		bool isSelected = (i == _levelSelection);
-		drawTextCentered(pilotItems[i + 1], itemY, isSelected);
-	}
-
-	// Draw info text at bottom if a pilot slot is selected (items 0-5)
-	// From FUN_00414A41: info is shown when local_18 < local_10 (selection < num_pilots)
-	if (_levelSelection >= 0 && _levelSelection <= 5) {
-		// Show difficulty or score info for selected pilot
-		// From info.md: Info displayed at X=30, Y=180
-		const char *pilotInfoText = "DIFFICULTY: EASY";
-		int infoY = 180;
-		int infoX = 30;
-
-		// Draw left-aligned text using drawCharV7
-		int curX = infoX;
-		for (const char *c = pilotInfoText; *c; c++) {
-			int charIdx = (unsigned char)*c;
-			if (charIdx < numFontChars) {
-				int charWidth = font->getCharWidth(charIdx);
-				if (curX >= 0 && curX + charWidth <= width && infoY >= 0) {
-					font->drawCharV7(renderBitmap, clipRect, curX, infoY, actualPitch, -1,
-					                 kStyleAlignLeft, charIdx, true, true);
-				}
-				curX += charWidth;
-			}
-		}
-	}
+	// When a saved pilot is selected, show info at fixed coordinates
+	// FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
+	// DAT_004573cc = TRS 25 ("DIFFICULTY: %S"), DAT_004573d0 = TRS 26 ("CHAPTER: %HO")
+	// TODO: Implement pilot info display when save system is added
 }
 
 // ======================= Level Loading System =======================
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ba460e2fd8f..ac542e841da 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -95,6 +95,11 @@ public:
 	// Process menu input (keyboard/mouse) - returns selected item or -1
 	int processMenuInput();
 
+	// Shared menu item renderer - emulates FUN_0041F5AE param_4==0 (centered mode)
+	// items[0] = title, items[1..numItems] = selectable items, selection = highlighted item
+	void drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
+	                   const char **items, int numItems, int selection);
+
 	// Draw menu overlay (selection highlight) on current frame
 	void drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height);
 


Commit: 267ddb67fe32eb6cce59ae6b8edd78f49ba988e0
    https://github.com/scummvm/scummvm/commit/267ddb67fe32eb6cce59ae6b8edd78f49ba988e0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:03+02:00

Commit Message:
SCUMM: RA2: Implement difficulty selection menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 769f0c56ab0..f3b98fc56bd 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -396,6 +396,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_levelSelection = 0;          // First item selected
 	_levelItemCount = 4;          // 0 saved pilots + 4 fixed options
 	_selectedLevel = 1;           // Default selected level
+	_difficultySelection = 2;     // Default to 3rd difficulty (matching original init param_3=2)
 
 	// Initialize menu input capture system
 	_menuInputActive = false;
@@ -470,6 +471,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			if (splayer) {
 				if (_menuInputActive && (_gameState == kStateMainMenu ||
 				                          _gameState == kStatePilotSelect ||
+				                          _gameState == kStateDifficultySelect ||
 				                          _gameState == kStateChapterSelect)) {
 					// In menu mode: Select quit option and confirm selection
 					// This emulates the assembly behavior from FUN_0041f5ae
@@ -3876,7 +3878,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	// Check if we're in menu mode (menu state + intro flag)
 	bool menuMode = (introPlaying && _gameState == kStateMainMenu);
-	bool pilotSelectMode = (introPlaying && _gameState == kStatePilotSelect);
+	bool pilotSelectMode = (introPlaying && (_gameState == kStatePilotSelect || _gameState == kStateDifficultySelect));
 	bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
 
 	// Handle pilot selection input and rendering (FUN_00414A41)
@@ -7068,25 +7070,28 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 int InsaneRebel2::runLevelSelect() {
 	// Pilot selection menu loop - emulates FUN_00414A41
 	// Returns:
-	//   kLevelSelectPlay (1) = Go to chapter selection
-	//   kLevelSelectBack (0) = Return to main menu
+	//   kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
+	//   kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
 	//   kLevelSelectQuit (2) = Quit game
+	//
+	// Original action dispatch (FUN_00414A41):
+	//   sel < N        → saved pilot selected → return 3 (start game)
+	//   sel == N       → ADD NEW PILOT → difficulty submenu → loop back
+	//   sel == N+1     → COPY PILOT → source select (no-op if N==0)
+	//   sel == N+2     → DELETE PILOT → confirm select (no-op if N==0)
+	//   sel == N+3     → RETURN TO MAIN MENU → return 1
+	//   ESC            → return 1
 
 	debug("Rebel2: Entering pilot selection (FUN_00414A41)");
 
-	// Enable menu input capture via EventObserver and clear any stale events
 	_menuInputActive = true;
 	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
 
-	// Initialize pilot selection state
-	// Menu structure from FUN_00414A41:
-	// Items 0-5: Pilot slots (PILOT 1-6)
-	// Item 6: NEW PILOT
-	// Item 7: DELETE PILOT
-	// Item 8: COPY PILOT
-	// Item 9: MAIN MENU
+	// Number of saved pilots (TODO: implement save system)
+	int numPilots = 0;
+
 	_levelSelection = 0;
-	_levelItemCount = 4;  // Selectable items: 0 saved pilots + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
+	_levelItemCount = numPilots + 4;  // N pilots + NEW/COPY/DELETE/MAIN MENU
 	_selectedLevel = 1;
 	_menuRepeatDelay = 0;
 	_gameState = kStatePilotSelect;
@@ -7114,38 +7119,53 @@ int InsaneRebel2::runLevelSelect() {
 
 		_vm->_smushVideoShouldFinish = false;
 
-		debug("Rebel2: Pilot selection made: %d", _levelSelection);
-
-		// Process pilot selection - all options go to chapter selection except MAIN MENU
-		// Menu items (from FUN_00414A41):
-		// 0-5: Pilot slots
-		// 6: NEW PILOT
-		// 7: DELETE PILOT
-		// 8: COPY PILOT
-		// 9: MAIN MENU (back)
-		if (_levelSelection >= 0 && _levelSelection <= 5) {
-			// Pilot selected - go to chapter selection
-			_selectedLevel = _levelSelection + 1;
-			debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
-			_menuInputActive = false;
-			return kLevelSelectPlay;
-		} else if (_levelSelection == 6) {
-			// NEW PILOT - go to chapter selection
-			debug("Rebel2: NEW PILOT selected - going to chapter selection");
-			_menuInputActive = false;
-			return kLevelSelectPlay;
-		} else if (_levelSelection == 7) {
-			// DELETE PILOT - go to chapter selection
-			debug("Rebel2: DELETE PILOT selected - going to chapter selection");
+		// Dispatch based on current game state
+		if (_gameState == kStateDifficultySelect) {
+			// Difficulty submenu — selection made
+			debug("Rebel2: Difficulty %d selected", _difficultySelection);
+			// Original stores difficulty in pilot record and loops back.
+			// Since we have no save system, proceed to chapter select.
+			_gameState = kStatePilotSelect;
 			_menuInputActive = false;
 			return kLevelSelectPlay;
-		} else if (_levelSelection == 8) {
-			// COPY PILOT - go to chapter selection
-			debug("Rebel2: COPY PILOT selected - going to chapter selection");
+		}
+
+		// Pilot menu — process selection
+		debug("Rebel2: Pilot selection made: %d (numPilots=%d)", _levelSelection, numPilots);
+
+		if (_levelSelection < numPilots) {
+			// Saved pilot selected — go to chapter selection
+			_selectedLevel = _levelSelection + 1;
+			debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
 			_menuInputActive = false;
 			return kLevelSelectPlay;
-		} else if (_levelSelection == 9) {
-			// Main Menu (back)
+		} else if (_levelSelection == numPilots) {
+			// ADD NEW PILOT — show difficulty submenu
+			debug("Rebel2: ADD NEW PILOT - showing difficulty submenu");
+			_gameState = kStateDifficultySelect;
+			_difficultySelection = 2;  // Default to 3rd option (matching original init)
+			// Continue loop — next video frame will render difficulty menu
+			continue;
+		} else if (_levelSelection == numPilots + 1) {
+			// COPY PILOT — no-op when numPilots==0 (original checks local_10 != 0)
+			if (numPilots > 0) {
+				debug("Rebel2: COPY PILOT selected");
+				// TODO: implement copy pilot sub-flow
+			} else {
+				debug("Rebel2: COPY PILOT - no pilots to copy");
+			}
+			continue;
+		} else if (_levelSelection == numPilots + 2) {
+			// DELETE PILOT — no-op when numPilots==0 (original checks local_10 != 0)
+			if (numPilots > 0) {
+				debug("Rebel2: DELETE PILOT selected");
+				// TODO: implement delete pilot sub-flow
+			} else {
+				debug("Rebel2: DELETE PILOT - no pilots to delete");
+			}
+			continue;
+		} else if (_levelSelection == numPilots + 3) {
+			// RETURN TO MAIN MENU
 			debug("Rebel2: Back to main menu selected");
 			_menuInputActive = false;
 			return kLevelSelectBack;
@@ -7157,53 +7177,65 @@ int InsaneRebel2::runLevelSelect() {
 }
 
 int InsaneRebel2::processLevelSelectInput() {
-	// Process input for level selection menu
-	// Similar to processMenuInput but for level selection
+	// Process input for pilot selection and difficulty submenu
+	// Handles both kStatePilotSelect and kStateDifficultySelect modes
 	// Returns: -1 = no action, 0+ = item selected
-	//
-	// Events are captured by notifyEvent() - see processMenuInput for details.
 
 	int result = -1;
 
-	// Level menu Y positions — must match drawMenuItems() formula
-	// itemBaseY = numItems * -5 + 0x68
-	const int baseY = _levelItemCount * -5 + 0x68;
+	// Determine which menu mode we're in
+	bool isDifficultyMode = (_gameState == kStateDifficultySelect);
+	int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
+	int itemCount = isDifficultyMode ? 6 : _levelItemCount;
+
+	// Mouse hit Y positions — must match drawMenuItems() formula
+	const int baseY = itemCount * -5 + 0x68;
 	const int itemHeight = 10;
 
-	// Process events from the queue (populated by notifyEvent)
 	while (!_menuEventQueue.empty()) {
 		Common::Event event = _menuEventQueue.pop();
 		switch (event.type) {
 		case Common::EVENT_KEYDOWN:
 			switch (event.kbd.keycode) {
 			case Common::KEYCODE_UP:
-				_levelSelection--;
-				if (_levelSelection < 0) {
-					_levelSelection = _levelItemCount - 1;
+				selection--;
+				if (selection < 0) {
+					selection = itemCount - 1;
 				}
-				debug("LevelSelect: Selection changed to %d (UP)", _levelSelection);
+				debug("LevelSelect: Selection changed to %d (UP, %s)",
+				      selection, isDifficultyMode ? "difficulty" : "pilot");
 				break;
 
 			case Common::KEYCODE_DOWN:
-				_levelSelection++;
-				if (_levelSelection >= _levelItemCount) {
-					_levelSelection = 0;
+				selection++;
+				if (selection >= itemCount) {
+					selection = 0;
 				}
-				debug("LevelSelect: Selection changed to %d (DOWN)", _levelSelection);
+				debug("LevelSelect: Selection changed to %d (DOWN, %s)",
+				      selection, isDifficultyMode ? "difficulty" : "pilot");
 				break;
 
 			case Common::KEYCODE_RETURN:
 			case Common::KEYCODE_KP_ENTER:
-				if (_levelSelection >= 0 && _levelSelection < _levelItemCount) {
-					result = _levelSelection;
-					debug("LevelSelect: Item %d selected (ENTER)", _levelSelection);
+				if (selection >= 0 && selection < itemCount) {
+					result = selection;
+					debug("LevelSelect: Item %d selected (ENTER, %s)",
+					      selection, isDifficultyMode ? "difficulty" : "pilot");
 				}
 				break;
 
 			case Common::KEYCODE_ESCAPE:
-				// ESC - Back to main menu
-				result = _levelItemCount - 1;  // Last item is "MAIN MENU"
-				debug("LevelSelect: ESC pressed - back to menu");
+				if (isDifficultyMode) {
+					// ESC in difficulty submenu — return to pilot menu
+					// Note: original has no ESC in difficulty submenu, but we add it as a
+					// graceful fallback
+					_gameState = kStatePilotSelect;
+					debug("LevelSelect: ESC in difficulty - back to pilot menu");
+				} else {
+					// ESC in pilot menu — back to main menu (return 1 in original)
+					result = _levelItemCount - 1;  // Last item = MAIN MENU
+					debug("LevelSelect: ESC pressed - back to main menu");
+				}
 				break;
 
 			default:
@@ -7213,17 +7245,14 @@ int InsaneRebel2::processLevelSelectInput() {
 
 		case Common::EVENT_LBUTTONDOWN:
 			{
-				// Get mouse position from the event
-				int mouseX = event.mouse.x;
 				int mouseY = event.mouse.y;
+				debug("LevelSelect: Click at Y=%d (%s)", mouseY,
+				      isDifficultyMode ? "difficulty" : "pilot");
 
-				debug("LevelSelect: Click detected at (%d, %d)", mouseX, mouseY);
-
-				for (int i = 0; i < _levelItemCount; i++) {
+				for (int i = 0; i < itemCount; i++) {
 					int itemY = baseY + i * itemHeight;
-					// Use a larger vertical hit area (full item height)
 					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
-						_levelSelection = i;
+						selection = i;
 						result = i;
 						debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
 						break;
@@ -7233,15 +7262,17 @@ int InsaneRebel2::processLevelSelectInput() {
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
-			// Update mouse position for hover effects
 			_vm->_mouse.x = event.mouse.x;
 			_vm->_mouse.y = event.mouse.y;
 			break;
 
 		case Common::EVENT_QUIT:
 		case Common::EVENT_RETURN_TO_LAUNCHER:
-			// Handle quit request - go back to main menu
-			result = _levelItemCount - 1;
+			if (isDifficultyMode) {
+				_gameState = kStatePilotSelect;
+			} else {
+				result = _levelItemCount - 1;
+			}
 			break;
 
 		default:
@@ -7254,22 +7285,9 @@ int InsaneRebel2::processLevelSelectInput() {
 
 void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
 	// =====================================================================
-	// Pilot selection menu renderer - Emulates FUN_00414A41
+	// Pilot selection / difficulty submenu renderer
+	// Emulates FUN_00414A41 → FUN_0041f5ae
 	// =====================================================================
-	//
-	// FUN_00414A41 builds the menu dynamically:
-	//   items[0]          = DAT_004573b8[0] = TRS 20 (title "PILOTS")
-	//   items[1..N]       = saved pilot slot strings (dynamically formatted)
-	//   items[N+1]        = DAT_004573b8[1] = TRS 21 (ADD NEW PILOT)
-	//   items[N+2]        = DAT_004573b8[2] = TRS 22 (DUPE PILOT)
-	//   items[N+3]        = DAT_004573b8[3] = TRS 23 (DELETE PILOT)
-	//   items[N+4]        = DAT_004573b8[4] = TRS 24 (RETURN TO MAIN MENU)
-	//
-	// FUN_0041f5ae called with param_3 = N + 4 (selectable items)
-	//
-	// TRS 25+ are NOT menu items — they are info format strings
-	// ("DIFFICULTY: %S", "CHAPTER: %HO") rendered separately by FUN_00434cb0
-	// at fixed coordinates when a saved pilot is selected.
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	if (!splayer) {
@@ -7277,11 +7295,36 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 		return;
 	}
 
-	// Number of saved pilots (TODO: implement save system, 0 for now)
-	int numPilots = 0;
+	if (_gameState == kStateDifficultySelect) {
+		// =====================================================================
+		// Difficulty submenu - LAB_00414ff6
+		// FUN_0041f5ae(0, &DAT_00457458, 6, 0)
+		// DAT_00457458 = 7 entries loaded from TRS 110-116 (FUN_00414073 lines 47-50)
+		// param_3 = 6 → items[0]=title(TRS 110), items[1..6]=selectable(TRS 111-116)
+		// =====================================================================
+		const char *diffItems[7];
+		for (int i = 0; i < 7; i++) {
+			diffItems[i] = splayer->getString(110 + i);
+			if (!diffItems[i] || !diffItems[i][0]) {
+				diffItems[i] = "";
+			}
+		}
+		drawMenuItems(renderBitmap, pitch, width, height, diffItems, 6, _difficultySelection);
+		return;
+	}
+
+	// =====================================================================
+	// Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
+	// =====================================================================
+	// items[0]    = title (TRS 20)
+	// items[1..N] = saved pilots (formatted with ^f01^c005)
+	// items[N+1]  = TRS 21 (ADD NEW PILOT)
+	// items[N+2]  = TRS 22 (COPY PILOT)
+	// items[N+3]  = TRS 23 (DELETE PILOT)
+	// items[N+4]  = TRS 24 (RETURN TO MAIN MENU)
+
+	int numPilots = 0;  // TODO: implement save system
 
-	// Build menu item array: title + [pilot slots] + 4 fixed options
-	// Max: 1 title + 6 pilots + 4 options = 11
 	const char *pilotItems[11];
 	int idx = 0;
 
@@ -7296,20 +7339,17 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 		pilotItems[idx++] = splayer->getString(21 + i);
 	}
 
-	// Validate strings
 	for (int i = 0; i < idx; i++) {
 		if (!pilotItems[i] || !pilotItems[i][0]) {
 			pilotItems[i] = "";
 		}
 	}
 
-	int numSelectableItems = numPilots + 4;
-	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numSelectableItems, _levelSelection);
+	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numPilots + 4, _levelSelection);
 
-	// When a saved pilot is selected, show info at fixed coordinates
+	// Pilot info display at fixed coordinates when saved pilot selected
 	// FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
-	// DAT_004573cc = TRS 25 ("DIFFICULTY: %S"), DAT_004573d0 = TRS 26 ("CHAPTER: %HO")
-	// TODO: Implement pilot info display when save system is added
+	// TODO: Implement when save system is added
 }
 
 // ======================= Level Loading System =======================
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ac542e841da..e8ec2f4c3f6 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -66,7 +66,8 @@ public:
 		kStateChapterSelect = 3,// Stage 3: Chapter selection (FUN_00415CF8)
 		kStateGameplay = 4,     // Stage 4: Gameplay (FUN_00416787)
 		kStateCredits = 5,      // Credits sequence
-		kStateQuit = 6          // Exit game
+		kStateQuit = 6,         // Exit game
+		kStateDifficultySelect = 7 // Difficulty submenu within pilot select
 	};
 
 	// Menu selection results (return values from FUN_004147B2)
@@ -171,6 +172,7 @@ public:
 	int _levelSelection;          // Current level selection (0-based)
 	int _levelItemCount;          // Number of level items (levels + options)
 	int _selectedLevel;           // Final selected level ID (1-15)
+	int _difficultySelection;     // Current difficulty selection in submenu (0-based)
 
 	// Run pilot selection menu - emulates FUN_00414A41
 	int runLevelSelect();


Commit: 9b76b0247a38f481c2b45d2dc78fca0046f85868
    https://github.com/scummvm/scummvm/commit/9b76b0247a38f481c2b45d2dc78fca0046f85868
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:03+02:00

Commit Message:
SCUMM: RA2: Improve level menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index f3b98fc56bd..2577983d670 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -5710,25 +5710,36 @@ int InsaneRebel2::processMenuInput() {
 }
 
 void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
-                                  const char **items, int numItems, int selection) {
+                                  const char **items, int numItems, int selection,
+                                  bool leftAligned) {
 	// =====================================================================
-	// Shared menu renderer - Emulates FUN_0041f5ae param_4==0 (centered mode)
+	// Shared menu renderer - Emulates FUN_0041f5ae
 	// Address: 0x41F5AE
 	// =====================================================================
 	//
 	// items[0] = title string, items[1..numItems] = selectable items
 	// numItems = number of selectable items (FUN_0041f5ae param_3)
 	// selection = currently highlighted item (0-based, maps to DAT_00459988)
+	// leftAligned = false: param_4==0 (centered), true: param_4==1 (left-aligned)
 	//
 	// Coordinate formulas from FUN_0041f5ae (low-res, DAT_0047a808 < 2):
-	//   Title Y:     param_3 * -5 + 0x51           (line 19)
-	//   Item base Y: param_3 * -5 + 0x68           (line 28)
-	//   Item Y:      param_3 * -5 + i * 10 + 0x68  (line 28)
-	//   Center X:    ((DAT_0047a808 < 2) - 1 & 0xa0) + 0xa0 = 160
+	// Centered (param_4=0):
+	//   Title X:     center - titleWidth/2  (centerX = 160)
+	//   Title Y:     param_3 * -5 + 0x51
+	//   Item X:      center - textWidth/2
+	//   Box X:       center - bracketWidth/2
+	// Left-aligned (param_4=1):
+	//   Title X:     0x28 = 40
+	//   Title Y:     param_3 * -5 + 0x56
+	//   Item X:      0x17 = 23
+	//   Box X:       0x14 = 20
+	// Both modes:
+	//   Item base Y: param_3 * -5 + 0x68
+	//   Item Y:      param_3 * -5 + i * 10 + 0x68
 	//   Box Y:       param_3 * -5 + i * 10 + 0x67  (1px above text)
 
 	const int centerX = width / 2;
-	const int titleY = numItems * -5 + 0x51;
+	const int titleY = numItems * -5 + (leftAligned ? 0x56 : 0x51);
 	const int itemBaseY = numItems * -5 + 0x68;
 	const int itemSpacing = 10;
 
@@ -5848,23 +5859,27 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	};
 
 	// =====================================================================
-	// Draw title - items[0], centered at titleY
+	// Draw title - items[0]
+	// Centered: X = center - titleWidth/2
+	// Left-aligned: X = 40 (0x28)
 	// =====================================================================
 	{
 		int titleWidth = getStringWidth(items[0]);
-		int titleX = centerX - titleWidth / 2;
+		int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
 		drawString(items[0], titleX, titleY);
 	}
 
 	// =====================================================================
 	// Draw selectable items with selection highlight box
+	// Centered: item X = center - textWidth/2, box X = center - bracketWidth/2
+	// Left-aligned: item X = 23 (0x17), box X = 20 (0x14)
 	// =====================================================================
 	for (int i = 0; i < numItems; i++) {
 		int itemY = itemBaseY + i * itemSpacing;
 		const char *text = items[i + 1];
 
 		int textWidth = getStringWidth(text);
-		int textX = centerX - textWidth / 2;
+		int textX = leftAligned ? 23 : (centerX - textWidth / 2);
 		drawString(text, textX, itemY);
 
 		// Selection highlight box - FUN_004292d0
@@ -5880,9 +5895,9 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 			frameCounter++;
 			byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
 
-			// Box position: X centered, Y = itemY - 1 (0x67 vs 0x68)
-			int leftX = centerX - bracketWidth / 2;
-			int rightX = centerX + bracketWidth / 2;
+			// Box position: Y = itemY - 1 (0x67 vs 0x68)
+			int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
+			int rightX = leftX + bracketWidth;
 			int topY = itemY - 1;
 			int bottomY = itemY + bracketHeight - 1;
 
@@ -6264,8 +6279,13 @@ int InsaneRebel2::runChapterSelect() {
 	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
 
 	// Initialize chapter selection state
-	_chapterSelection = 0;
-	_chapterItemCount = 17;  // 16 chapters + BACK
+	// Original (lines 51-54): local_10 = 0xf; while (local_10 > 0 && locked) local_10--;
+	// Finds highest unlocked chapter. With debug unlock all = 15 (FINALE).
+	_chapterSelection = 15;
+	while (_chapterSelection > 0 && !_chapterUnlocked[_chapterSelection]) {
+		_chapterSelection--;
+	}
+	_chapterItemCount = 17;  // 16 chapters + RETURN TO PILOTS
 	_selectedChapter = 0;
 	_passwordInput = "";
 	_menuRepeatDelay = 0;
@@ -6399,41 +6419,20 @@ int InsaneRebel2::processChapterSelectInput() {
 		case Common::EVENT_LBUTTONDOWN:
 			{
 				// Mouse click - check if clicking on a menu item
-				// From FUN_0041F5AE assembly (low-res 320x200):
+				// From FUN_0041F5AE (low-res, left-aligned):
 				// Item Y = 17 * -5 + i * 10 + 104 = 19 + i * 10
-				// Chapters 0-14 at Y = 19 + i*10
-				// FINALE (15) at Y = 19 + 15*10 = 169
-				// RETURN TO PILOTS (16) at Y = 19 + 16*10 = 179
-				int baseY = 19;
-				int itemHeight = 10;
+				int baseY = _chapterItemCount * -5 + 0x68;  // = 19
 				int mouseY = event.mouse.y;
 
-				// Check chapters 0-14
-				for (int i = 0; i < 15; i++) {
-					int itemY = baseY + i * itemHeight;
+				for (int i = 0; i < _chapterItemCount; i++) {
+					int itemY = baseY + i * 10;
 					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
 						_chapterSelection = i;
 						result = i;
-						debug("ChapterSelect: Chapter %d clicked at Y=%d", i + 1, mouseY);
+						debug("ChapterSelect: Item %d clicked at Y=%d", i, mouseY);
 						break;
 					}
 				}
-
-				// Check FINALE (index 15) at Y=169
-				int finaleY = baseY + 15 * itemHeight;
-				if (mouseY >= finaleY - 4 && mouseY < finaleY + 6) {
-					_chapterSelection = 15;
-					result = 15;
-					debug("ChapterSelect: FINALE clicked");
-				}
-
-				// Check RETURN TO PILOTS (index 16) at Y=179
-				int returnY = baseY + 16 * itemHeight;
-				if (mouseY >= returnY - 4 && mouseY < returnY + 6) {
-					_chapterSelection = 16;
-					result = 16;
-					debug("ChapterSelect: RETURN TO PILOTS clicked");
-				}
 			}
 			break;
 
@@ -6631,140 +6630,39 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 	}
 }
 
-// Draw left-aligned menu item - emulates FUN_0041F5AE with param_4=1
-void InsaneRebel2::drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
-                                           const char *text, int x, int y, bool selected) {
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font) return;
-
-	int numFontChars = font->getNumChars();
-	Common::Rect clipRect(0, 0, width, height);
-
-	// Calculate text width for selection box
-	int textWidth = 0;
-	for (const char *c = text; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			textWidth += font->getCharWidth(charIdx);
-		}
-	}
-
-	// Draw selection box if selected
-	// Box dimensions: width = textWidth + 6, height = 10 (low-res)
-	if (selected) {
-		static int frameCounter = 0;
-		frameCounter++;
-
-		int boxWidth = textWidth + 6;
-		int boxHeight = 10;
-		int boxX = x - 3;  // Left-aligned, so box starts 3 pixels before text
-		int boxY = y - 1;
+// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
+// For unlocked chapters: score display using TRS 80 at (25, 190)
+// For locked chapters: password prompt at (30, 190)
+void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
+	if (_chapterSelection < 0 || _chapterSelection >= 16) return;
 
-		// Flashing color (emulates (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10)
-		byte highlightColor = (frameCounter & 1) ? 0xF8 : 0xF0;
-
-		// Draw box border
-		if (boxY >= 0 && boxY < height && boxX >= 0) {
-			// Top edge
-			for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
-				if (px >= 0) renderBitmap[boxY * pitch + px] = highlightColor;
-			}
-			// Bottom edge
-			int bottomY = boxY + boxHeight - 1;
-			if (bottomY < height) {
-				for (int px = boxX; px < boxX + boxWidth && px < width; px++) {
-					if (px >= 0) renderBitmap[bottomY * pitch + px] = highlightColor;
-				}
-			}
-			// Left edge
-			for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
-				if (py >= 0 && boxX >= 0) renderBitmap[py * pitch + boxX] = highlightColor;
-			}
-			// Right edge
-			int rightX = boxX + boxWidth - 1;
-			if (rightX < width) {
-				for (int py = boxY; py < boxY + boxHeight && py < height; py++) {
-					if (py >= 0) renderBitmap[py * pitch + rightX] = highlightColor;
-				}
-			}
-		}
-	}
-
-	// Draw text characters
-	int curX = x;
-	for (const char *c = text; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && y >= 0 && y < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, y, pitch, -1,
-				                 kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-}
-
-// Draw password input field - emulates lines 106-125 of FUN_00415CF8
-void InsaneRebel2::drawPasswordInput(byte *renderBitmap, int pitch, int width, int height) {
-	// Password display position for low-res: X=30 (0x1e), Y=190 (0xbe)
-	int infoX = 30;
-	int infoY = 190;
-
-	// Build display string with cursor
-	static int frameCounter = 0;
-	frameCounter++;
-	char cursor = (frameCounter & 2) ? '_' : ' ';  // Blinking cursor
-
-	char displayText[32];
-	snprintf(displayText, sizeof(displayText), "ACCESS CODE: %s%c", _passwordInput.c_str(), cursor);
-
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font) return;
-
-	int numFontChars = font->getNumChars();
-	Common::Rect clipRect(0, 0, width, height);
-
-	int curX = infoX;
-	for (const char *c = displayText; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
-				                 kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-}
-
-// Draw score/time display - emulates lines 99-104 of FUN_00415CF8
-void InsaneRebel2::drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter) {
-	// Score display position for low-res: X=25 (0x19), Y=190 (0xbe)
-	int infoX = 25;
-	int infoY = 190;
-
-	// For now, just display a placeholder score
-	char displayText[32];
-	snprintf(displayText, sizeof(displayText), "SCORE: %d", 0);
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer) return;
 
 	NutRenderer *font = _smush_smalfontNut;
 	if (!font) return;
 
-	int numFontChars = font->getNumChars();
-	Common::Rect clipRect(0, 0, width, height);
+	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	int actualPitch = _vm->_screenWidth;
 
-	int curX = infoX;
-	for (const char *c = displayText; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && infoY >= 0 && infoY < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, infoY, pitch, -1,
-				                 kStyleAlignLeft, charIdx, true, true);
+	if (_chapterUnlocked[_chapterSelection]) {
+		// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
+		const char *scoreStr = splayer->getString(80);
+		if (!scoreStr || !scoreStr[0]) return;
+
+		int curX = 25;
+		int numChars = font->getNumChars();
+		for (const char *c = scoreStr; *c; c++) {
+			byte ch = (byte)*c;
+			if (ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
+			if (ch < numChars) {
+				int charW = font->getCharWidth(ch);
+				if (curX >= 0 && curX + charW <= width && 190 < height) {
+					font->drawCharV7(renderBitmap, clipRect, curX, 190, actualPitch, 1,
+					                 kStyleAlignLeft, ch, false, false);
+				}
+				curX += charW;
 			}
-			curX += charWidth;
 		}
 	}
 }
@@ -6772,295 +6670,50 @@ void InsaneRebel2::drawScoreDisplay(byte *renderBitmap, int pitch, int width, in
 // Draw chapter selection overlay - called during O_LEVEL.SAN playback
 // FUN_00415CF8 - Chapter selection screen with preview thumbnail
 void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// Draw chapter selection screen overlay
-	// Emulates rendering in FUN_00415CF8
+	// Emulates FUN_00415CF8 rendering via shared drawMenuItems(leftAligned=true)
 	//
-	// Layout (320x200 mode):
-	// - Title "Chapters" at top-left using TITLFONT
-	// - 15 chapters + FINALE + RETURN TO PILOTS (17 items total)
-	// - Three preview boxes on right side
-	// - Status bar at bottom: "PILOTS: X  SCORE: Y  RANK:"
-
-	// Chapter names from GAME.TRS (DAT_00457820 and DAT_00457868)
-	// From FUN_00414073:
-	//   TRS indices 0x28-0x39 (40-57): 18 unlocked chapter strings -> DAT_00457820
-	//   TRS indices 0x3c-0x4d (60-77): 18 locked chapter strings -> DAT_00457868
-	// These contain complete strings like "CHAPTER 1 - THE DREIGHTON TRIANGLE"
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	const char *chapterNamesUnlocked[18];
-	const char *chapterNamesLocked[18];
-
-	if (!splayer) {
-		debug(1, "drawChapterSelectOverlay: SmushPlayer not available for TRS strings!");
-		return;
-	}
-
-	// Load unlocked chapter strings (TRS indices 40-57)
-	for (int i = 0; i < 18; i++) {
-		chapterNamesUnlocked[i] = splayer->getString(40 + i);
-		if (!chapterNamesUnlocked[i] || !chapterNamesUnlocked[i][0]) {
-			debug(1, "drawChapterSelectOverlay: TRS unlocked string %d not found!", 40 + i);
-			chapterNamesUnlocked[i] = "";
-		}
-	}
-
-	// Load locked chapter strings (TRS indices 60-77)
-	for (int i = 0; i < 18; i++) {
-		chapterNamesLocked[i] = splayer->getString(60 + i);
-		if (!chapterNamesLocked[i] || !chapterNamesLocked[i][0]) {
-			debug(1, "drawChapterSelectOverlay: TRS locked string %d not found!", 60 + i);
-			chapterNamesLocked[i] = "";
-		}
-	}
-
-	// Frame counter for flashing selection box
-	static int frameCounter = 0;
-	frameCounter++;
-
-	NutRenderer *menuFont = _smush_smalfontNut;
-	NutRenderer *titleFont = _smush_titlefontNut;
-	if (!menuFont) return;
-
-	Common::Rect clipRect(0, 0, width, height);
-	int numFontChars = menuFont->getNumChars();
-
-	// === Draw title "Chapters" using TITLFONT ===
-	// From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
-	// Title X = 0x28 = 40, Title Y = param_3 * -5 + 0x56 = 17 * -5 + 86 = 1
-	int titleX = 40;
-	int titleY = 1;
-
-	if (titleFont) {
-		const char *titleText = "Chapters";
-		int curX = titleX;
-		int numTitleChars = titleFont->getNumChars();
-		for (const char *c = titleText; *c; c++) {
-			int charIdx = (unsigned char)*c;
-			if (charIdx < numTitleChars) {
-				int charWidth = titleFont->getCharWidth(charIdx);
-				if (curX >= 0 && curX + charWidth <= width && titleY >= 0) {
-					titleFont->drawCharV7(renderBitmap, clipRect, curX, titleY, pitch, -1,
-					                      kStyleAlignLeft, charIdx, true, true);
-				}
-				curX += charWidth;
-			}
-		}
-	}
-
-	// === Draw chapter list (left-aligned) ===
-	// From FUN_0041F5AE param_4=1 (left-aligned mode), low-res (320x200):
-	// Item X = 0x17 = 23
-	// Item Y = param_3 * -5 + local_c * 10 + 0x68 = 17 * -5 + i * 10 + 104 = 19 + i * 10
-	// Selection Box X = 0x14 = 20, Y = 18 + i * 10 (item Y - 1)
-	int itemX = 23;       // From assembly: 0x17 = 23
-	int itemBaseY = 19;   // From assembly: 17 * -5 + 104 = 19
-	int itemSpacing = 10; // From assembly: 10 pixels between items
-	int selBoxX = 20;     // From assembly: 0x14 = 20
-
-	// Draw 15 chapters (indices 0-14)
-	for (int i = 0; i < 15; i++) {
-		int itemY = itemBaseY + i * itemSpacing;
-		bool isSelected = (i == _chapterSelection);
-
-		// Get chapter string from TRS (already contains "CHAPTER X - NAME" or "CHAPTER X -")
-		const char *chapterStr = _chapterUnlocked[i] ? chapterNamesUnlocked[i] : chapterNamesLocked[i];
-		if (!chapterStr || !chapterStr[0]) {
-			continue;  // Skip if no string available
-		}
-
-		// Calculate text width for selection box
-		int textWidth = 0;
-		for (const char *c = chapterStr; *c; c++) {
-			int charIdx = (unsigned char)*c;
-			if (charIdx < numFontChars) {
-				textWidth += menuFont->getCharWidth(charIdx);
-			}
-		}
-
-		// Draw selection box if selected (red border)
-		// From assembly: box starts at X=0x14=20, Y = itemY - 1
-		if (isSelected) {
-			int boxWidth = textWidth + 6;  // From assembly: textWidth + 6
-			int boxHeight = 10;            // From assembly: 10 pixels
-			int boxX = selBoxX;            // From assembly: 0x14 = 20
-			int boxY = itemY - 1;          // From assembly: itemY - 1
-
-			// Flashing color from assembly: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
-			// This gives -16 or -8, which are palette-relative negative offsets
-			byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
-
-			// Draw box border
-			if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
-				for (int px = boxX; px < boxX + boxWidth; px++) {
-					renderBitmap[boxY * pitch + px] = boxColor;
-					renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
-				}
-				for (int py = boxY; py < boxY + boxHeight; py++) {
-					renderBitmap[py * pitch + boxX] = boxColor;
-					renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
-				}
-			}
-		}
-
-		// Draw chapter text
-		int curX = itemX;
-		for (const char *c = chapterStr; *c; c++) {
-			int charIdx = (unsigned char)*c;
-			if (charIdx < numFontChars) {
-				int charWidth = menuFont->getCharWidth(charIdx);
-				if (curX >= 0 && curX + charWidth <= width && itemY >= 0 && itemY < height) {
-					menuFont->drawCharV7(renderBitmap, clipRect, curX, itemY, pitch, -1,
-					                     kStyleAlignLeft, charIdx, true, true);
-				}
-				curX += charWidth;
-			}
-		}
-	}
-
-	// Draw FINALE (chapter 16 = index 15)
-	// Position: Y = 19 + 15*10 = 169
-	// FINALE uses TRS index 55 (unlocked) or 75 (locked) - index 15 in the arrays
-	int finaleY = itemBaseY + 15 * itemSpacing;
-	bool finaleSelected = (_chapterSelection == 15);
-	const char *finaleStr = _chapterUnlocked[15] ? chapterNamesUnlocked[15] : chapterNamesLocked[15];
-	if (!finaleStr || !finaleStr[0]) {
-		finaleStr = "";  // Fallback to empty if TRS not available
-	}
-
-	int finaleWidth = 0;
-	for (const char *c = finaleStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) finaleWidth += menuFont->getCharWidth(charIdx);
-	}
+	// Menu structure (18 entries, 17 selectable):
+	//   items[0]     = title = TRS 60 (locked chapter 0 header, contains ^f02 for TITLFONT)
+	//   items[1..16] = chapters 1-16 = TRS 41-56 (unlocked) or TRS 61-76 (locked)
+	//   items[17]    = "RETURN TO PILOTS" = TRS 77
+	//
+	// FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
 
-	if (finaleSelected) {
-		int boxWidth = finaleWidth + 6;
-		int boxHeight = 10;
-		int boxX = selBoxX;  // Use assembly value: 20
-		int boxY = finaleY - 1;
-		byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer) return;
 
-		if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
-			for (int px = boxX; px < boxX + boxWidth; px++) {
-				renderBitmap[boxY * pitch + px] = boxColor;
-				renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
-			}
-			for (int py = boxY; py < boxY + boxHeight; py++) {
-				renderBitmap[py * pitch + boxX] = boxColor;
-				renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
-			}
-		}
-	}
+	// Build items array matching original DAT_004577a8 layout
+	const char *items[18];
 
-	int curX = itemX;
-	for (const char *c = finaleStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			int charWidth = menuFont->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && finaleY >= 0) {
-				menuFont->drawCharV7(renderBitmap, clipRect, curX, finaleY, pitch, -1,
-				                     kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
+	// items[0] = title (always locked version = TRS 60, contains "^f02CHAPTERS")
+	items[0] = splayer->getString(60);
+	if (!items[0] || !items[0][0]) items[0] = "^f02CHAPTERS";
 
-	// Draw "RETURN TO PILOTS" (index 16)
-	// Position: Y = 19 + 16*10 = 179
-	// This string comes from TRS - likely in DAT_00457400 (indices 89-109)
-	// Using TRS index 89 for "RETURN TO PILOTS" or equivalent
-	int returnY = itemBaseY + 16 * itemSpacing;
-	bool returnSelected = (_chapterSelection == 16);
-	const char *returnStr = splayer->getString(89);
-	if (!returnStr || !returnStr[0]) {
-		debug(1, "drawChapterSelectOverlay: TRS string 89 (RETURN TO PILOTS) not found!");
-		returnStr = "";
+	// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
+	for (int i = 1; i <= 16; i++) {
+		bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
+		int trsIdx = unlocked ? (40 + i) : (60 + i);
+		items[i] = splayer->getString(trsIdx);
+		if (!items[i] || !items[i][0]) items[i] = "";
 	}
 
-	int returnWidth = 0;
-	for (const char *c = returnStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) returnWidth += menuFont->getCharWidth(charIdx);
-	}
+	// items[17] = "RETURN TO PILOTS" (always locked version = TRS 77)
+	items[17] = splayer->getString(77);
+	if (!items[17] || !items[17][0]) items[17] = "RETURN TO PILOTS";
 
-	if (returnSelected) {
-		int boxWidth = returnWidth + 6;
-		int boxHeight = 10;
-		int boxX = selBoxX;  // Use assembly value: 20
-		int boxY = returnY - 1;
-		byte boxColor = (frameCounter & 1) ? 0xF8 : 0xF0;
+	// Render menu using shared renderer with left-aligned mode
+	drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
 
-		if (boxY >= 0 && boxY + boxHeight <= height && boxX >= 0 && boxX + boxWidth <= width) {
-			for (int px = boxX; px < boxX + boxWidth; px++) {
-				renderBitmap[boxY * pitch + px] = boxColor;
-				renderBitmap[(boxY + boxHeight - 1) * pitch + px] = boxColor;
-			}
-			for (int py = boxY; py < boxY + boxHeight; py++) {
-				renderBitmap[py * pitch + boxX] = boxColor;
-				renderBitmap[py * pitch + boxX + boxWidth - 1] = boxColor;
-			}
-		}
-	}
-
-	curX = itemX;
-	for (const char *c = returnStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			int charWidth = menuFont->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && returnY >= 0) {
-				menuFont->drawCharV7(renderBitmap, clipRect, curX, returnY, pitch, -1,
-				                     kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-
-	// === Draw preview box on the right side ===
-	// From FUN_00415CF8 lines 128-133:
-	// Outer: X=228 (0xe4), Y=73 (0x49), W=84 (0x54), H=54 (0x36), color=0xF8
-	// Inner: X=229 (0xe5), Y=74 (0x4a), W=82 (0x52), H=52 (0x34), color=4
+	// Draw preview box on the right side (FUN_004292d0 calls at lines 128-133)
 	drawPreviewBox(renderBitmap, pitch, width, height);
 
-	// === Draw preview thumbnail inside the box ===
-	// Shows chapter number and unlock status for currently selected chapter
-	// Only draw for chapters 0-15 (not RETURN TO PILOTS at index 16)
+	// Draw preview thumbnail for chapters 0-15 (not RETURN TO PILOTS)
 	if (_chapterSelection >= 0 && _chapterSelection < 16) {
 		drawPreviewThumbnail(renderBitmap, pitch, width, height, _chapterSelection);
 	}
 
-	// === Draw status bar at bottom ===
-	// From FUN_00415CF8 lines 101-103 (unlocked chapter score display):
-	// X = 0x19 + 0x19 = 50 (but we use 23 to align with menu items)
-	// Y = 0xbe = 190
-	// TODO: The status bar format should use TRS strings from DAT_00457400 (indices 89-109)
-	// for proper localization. Currently using minimal display.
-	int statusY = 190;
-	int statusX = 23;  // Align with menu items
-
-	// Build status string using TRS format strings where available
-	// TRS indices 89-109 contain format strings for status display
-	char statusStr[64];
-	const char *pilotsLabel = splayer->getString(90);  // "PILOTS:" or equivalent
-	const char *scoreLabel = splayer->getString(91);   // "SCORE:" or equivalent
-	if (pilotsLabel && pilotsLabel[0] && scoreLabel && scoreLabel[0]) {
-		snprintf(statusStr, sizeof(statusStr), "%s %d  %s %d", pilotsLabel, 4, scoreLabel, _playerScore);
-	} else {
-		// Minimal fallback if TRS strings not available
-		snprintf(statusStr, sizeof(statusStr), "%d  %d", 4, _playerScore);
-	}
-
-	curX = statusX;
-	for (const char *c = statusStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numFontChars) {
-			int charWidth = menuFont->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && statusY >= 0 && statusY < height) {
-				menuFont->drawCharV7(renderBitmap, clipRect, curX, statusY, pitch, -1,
-				                     kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
+	// Draw score/info line at bottom
+	drawChapterInfoLine(renderBitmap, pitch, width, height);
 }
 
 // ==================== Pilot Selection Menu (FUN_00414A41) ====================
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index e8ec2f4c3f6..c6e96de2d28 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -96,10 +96,13 @@ public:
 	// Process menu input (keyboard/mouse) - returns selected item or -1
 	int processMenuInput();
 
-	// Shared menu item renderer - emulates FUN_0041F5AE param_4==0 (centered mode)
+	// Shared menu item renderer - emulates FUN_0041F5AE
 	// items[0] = title, items[1..numItems] = selectable items, selection = highlighted item
+	// leftAligned=false: param_4==0 (centered, for main menu / pilot select)
+	// leftAligned=true:  param_4==1 (left-aligned, for chapter select)
 	void drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
-	                   const char **items, int numItems, int selection);
+	                   const char **items, int numItems, int selection,
+	                   bool leftAligned = false);
 
 	// Draw menu overlay (selection highlight) on current frame
 	void drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height);
@@ -150,15 +153,8 @@ public:
 	int16 _previewOffsetX;   // X offset = -90 for chapter select
 	int16 _previewOffsetY;   // Y offset = chapter * -50 + 75
 
-	// Draw left-aligned menu text - emulates FUN_0041F5AE with param_4=1
-	void drawLeftAlignedMenuItem(byte *renderBitmap, int pitch, int width, int height,
-	                             const char *text, int x, int y, bool selected);
-
-	// Draw password input field - emulates lines 106-125 of FUN_00415CF8
-	void drawPasswordInput(byte *renderBitmap, int pitch, int width, int height);
-
-	// Draw score/time display - emulates lines 99-104 of FUN_00415CF8
-	void drawScoreDisplay(byte *renderBitmap, int pitch, int width, int height, int chapter);
+	// Draw score/info display at bottom of chapter select - emulates FUN_00434cb0 calls
+	void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
 
 	// ================= Pilot Selection Menu (FUN_00414A41) ====================
 	// This is the pilot/save selection menu (separate from chapter selection)


Commit: 1ae852f6e98521338851e2b4d59febaf1b6705a2
    https://github.com/scummvm/scummvm/commit/1ae852f6e98521338851e2b4d59febaf1b6705a2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:03+02:00

Commit Message:
SCUMM: RA2: Improve level 2

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2577983d670..480bca2168f 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -136,7 +136,10 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	// Retail globals mapped: hit counter, cooldown, invulnerability flag
 	_rebelHitCounter = 0;
+	_rebelKillCounter = 0;
 	_rebelInvulnerable = false;
+	_rebelWaveState = 0;
+	_rebelPhaseState = 0;
 
 	// Opcode 6 state variables
 	_rebelAutopilot = 0;
@@ -727,19 +730,30 @@ int32 InsaneRebel2::processMouse() {
 				// Enemy hit!
 				it->active = false;
 				it->destroyed = true;  // Mark as destroyed so IACT won't re-activate
-				debug("Rebel2: HIT enemy ID=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)", 
-					it->id, mousePos.x, mousePos.y,
+				debug("Rebel2: HIT enemy ID=%d type=%d at (%d,%d) - Rect: (%d,%d)-(%d,%d)",
+					it->id, it->type, mousePos.x, mousePos.y,
 					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
 
 				// Spawn explosion using native system
 				// Use width / 2 as the scale parameter
-				spawnExplosion((it->rect.left + it->rect.right) / 2, 
-							   (it->rect.top + it->rect.bottom) / 2, 
+				spawnExplosion((it->rect.left + it->rect.right) / 2,
+							   (it->rect.top + it->rect.bottom) / 2,
 							   it->rect.width() / 2);
 
-				// Disable self
+				// Disable self (prevents sprite from rendering via SKIP chunks)
 				setBit(it->id);
 
+				// Set enemy type bit in wave state (FUN_004028c5 line 74)
+				// DAT_0047ab98 |= 1 << (enemyType & 0x1f)
+				// This tracks which enemy GROUPS have been killed in this wave
+				if (it->type > 0 && it->type < 32) {
+					_rebelWaveState |= (1 << it->type);
+					debug("Rebel2: Wave state updated: 0x%x (set bit %d)", _rebelWaveState, it->type);
+				}
+
+				// Increment kill counter (DAT_0047ab88)
+				_rebelKillCounter++;
+
 				// Handle dependencies
 				int id = it->id;
 				if (id >= 0 && id < 512) {
@@ -1204,7 +1218,19 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 		// The original game's setBit(0)/clearBit(0) affects ALL bits, not intended here
 		if (targetId < 1 || targetId >= 0x200)
 			return;
-		
+
+		// Handler 8 specific: FUN_401234 case 0, par3==1, par4!=0
+		// "If enemy type par4 has been killed (bit set in wave state), disable targetId"
+		// This conditionally hides entities based on which enemy groups have been destroyed
+		if (_rebelHandler == 8 && value != 0) {
+			int bitMask = 1 << (value & 0x1f);
+			if ((_rebelWaveState & bitMask) != 0) {
+				setBit(targetId);
+				debug("Rebel2 Opcode2 (H8): Disable target=%d (type %d killed, wave=0x%x)", targetId, value, _rebelWaveState);
+			}
+			return;
+		}
+
 		if (value > 1 && value < 10) { // 1 < value < 10: random disable
 			if (_vm->_rnd.getRandomNumber(value) == 0) {
 				setBit(targetId);
@@ -1333,16 +1359,22 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// Set ship level mode (DAT_0043e000 = par3)
 		_shipLevelMode = par3;
 
-		// If par4 == 1, enable status bar and reset state
+		// If par4 == 1, enable status bar
 		if (par4 == 1) {
 			_rebelStatusBarSprite = 5;  // Status bar sprite for Handler 8
-			// Reset link tables
+		}
+
+		// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
+		if (_shipLevelMode != 0 && par4 == 1) {
+			// Clear link tables
 			for (int i = 0; i < 512; i++) {
 				_rebelLinks[i][0] = 0;
 				_rebelLinks[i][1] = 0;
 				_rebelLinks[i][2] = 0;
 			}
-			debug("Rebel2 Opcode 6 (Handler 8): Status bar enabled, state reset");
+			// DAT_0047ab98 = DAT_0047ab9c: Reset wave state to accumulated phase state
+			_rebelWaveState = _rebelPhaseState;
+			debug("Rebel2 Opcode 6 (Handler 8): State reset, wave=0x%x", _rebelWaveState);
 		}
 
 		// Skip position calculation for special modes 4 and 5
@@ -1763,9 +1795,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			_rebelLinks[i][2] = 0;
 		}
 
-		// DAT_0047ab98 = DAT_0047ab9c (reset state flags)
-		// We don't have a direct equivalent, but we can reset relevant counters
+		// DAT_0047ab98 = DAT_0047ab9c: At the start of each wave video,
+		// reset wave state to accumulated phase state. Enemies killed in
+		// previous waves stay killed; new kills add during this wave.
+		_rebelWaveState = _rebelPhaseState;
 		_rebelHitCounter = 0;
+		debug("Rebel2 Opcode 6: Wave state reset to phase state 0x%x", _rebelWaveState);
 	}
 
 	// Step 2: Set level type (DAT_00457900 = par3)
@@ -2692,6 +2727,7 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 		if (it->id == enemyId) {
 			it->rect = Common::Rect(x, y, x + w, y + h);
+			it->type = par3;  // Update enemy type/group
 			// Only re-activate if not destroyed
 			if (!it->destroyed) {
 				it->active = true;
@@ -2701,13 +2737,14 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 		}
 	}
 	if (!found) {
-		init_enemyStruct(enemyId, x, y, w, h, true, false, -1);
+		init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par3);
 	}
 }
 
-void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame) {
+void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
 	enemy e;
 	e.id = id;
+	e.type = type;
 	e.rect = Common::Rect(x, y, x + w, y + h);
 	e.active = active;
 	e.destroyed = destroyed;
@@ -7602,8 +7639,17 @@ int InsaneRebel2::runLevel1() {
 // =============================================================================
 
 int InsaneRebel2::runLevel2() {
+	// FUN_00418063: Level 2 "Corellia Star" - Third-person on-foot shooting
+	// Three phases, each with looping enemy waves until all enemy types killed.
+	// Phase completion: (_rebelPhaseState & mask) == mask
+	// Phase 1 mask: 0x06 (enemy types 1,2)
+	// Phase 2 mask: 0x0e (enemy types 1,2,3)
+	// Phase 3 mask: 0x0e (enemy types 1,2,3)
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int bonusCount = 0;  // Tracks special events (local_1c in assembly)
+	int bonusCount = 0;     // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
+	int totalKills = 0;     // local_c: accumulated kill count across phases
+	int totalMisses = 0;    // Accumulated misses (sVar1 + sVar2 from hit counters)
+	int prevWaveState = 0;  // local_8: previous wave's state for Phase 3 randomization
 
 	// Play cutscene (02CUT.SAN)
 	playCinematic("LEV02/02CUT.SAN");
@@ -7619,12 +7665,12 @@ int InsaneRebel2::runLevel2() {
 		_playerDamage = 0;
 		_currentPhase = 1;
 		bonusCount = 0;
+		totalKills = 0;
+		totalMisses = 0;
 
-		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
-		// This ensures all enemies are visible (not skipped by SKIP chunks)
+		// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
 		clearBit(0);
-
-		// Also reset link tables for enemy dependencies
+		_enemies.clear();
 		for (int i = 0; i < 512; i++) {
 			_rebelLinks[i][0] = 0;
 			_rebelLinks[i][1] = 0;
@@ -7632,154 +7678,231 @@ int InsaneRebel2::runLevel2() {
 		}
 
 		// ===== PHASE 1: P1/02P01_X.SAN =====
-		// First play variant A which contains the background IACT (opcode 8, par4=5)
-		// The background is loaded during this video and persists for B/C/D variants
-		{
-			debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background loader)");
-			splayer->setCurVideoFlags(0x28);
-			splayer->play("LEV02/P1/02P01_A.SAN", 12);
-			_deathFrame = splayer->_frame;
-		}
+		// FUN_0041c7d0: Reset per-phase counters
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+
+		// Play A.SAN (background loader)
+		debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background)");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV02/P1/02P01_A.SAN", 12);
+		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
 
-		// Now select random variant (B, C, or D) for actual gameplay
-		if (_playerShield > 0) {
-			int variant = getRandomVariant(3);
-			const char *variants[] = {"B", "C", "D"};
-			Common::String filename = Common::String::format("LEV02/P1/02P01_%s.SAN", variants[variant]);
+		// Copy wave state to phase state after A.SAN
+		_rebelPhaseState = _rebelWaveState;
 
-			debug("Rebel2: Level 2 Phase 1 - playing %s (gameplay)", filename.c_str());
+		// Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
+		// Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
+		while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			// Random variant B, C, or D
+			int variant = _vm->_rnd.getRandomNumber(2);  // 0-2
+			const char *variants[] = {
+				"LEV02/P1/02P01_B.SAN",
+				"LEV02/P1/02P01_C.SAN",
+				"LEV02/P1/02P01_D.SAN"
+			};
+			debug("Rebel2: Phase 1 wave - playing %s (state=0x%x)", variants[variant], _rebelPhaseState);
 			splayer->setCurVideoFlags(0x28);
-			splayer->play(filename.c_str(), 12);
+			splayer->play(variants[variant], 12);
 			_deathFrame = splayer->_frame;
-		}
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+			// After wave: copy wave state to phase state (FUN_00417b61 line 33)
+			_rebelPhaseState = _rebelWaveState;
+			debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06)", _rebelPhaseState);
+		}
 
-		if (_playerShield == 0) {
-			// Died in phase 1
-			debug("Rebel2: Level 2 Phase 1 death");
-			playLevelDeathVariant(2, 1, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
+		// Check for bonus (bit 4 = 0x10)
+		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
 
-			_playerLives--;
-			if (_playerLives <= 0) {
-				playLevelGameOver(2);
-				return kLevelGameOver;
-			}
-			playLevelRetry(2);
-			if (_vm->shouldQuit()) return kLevelQuit;
-			continue;  // Restart from beginning
-		}
+		if (_playerDamage >= 255) goto level2_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
 
 		// Post segment 1 (02PST1.SAN)
 		splayer->setCurVideoFlags(0x20);
 		splayer->play("LEV02/02PST1.SAN", 12);
 		if (_vm->shouldQuit()) return kLevelQuit;
 
-		_currentPhase = 2;
-
-		// Reset bit table before Phase 2 gameplay starts
-		clearBit(0);
+		totalKills += _rebelKillCounter;
+		totalMisses += _rebelHitCounter;
 
 		// ===== PHASE 2: P2/02P02_X.SAN =====
-		// First play variant A which contains the background IACT for this phase
-		{
-			debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background loader)");
-			splayer->setCurVideoFlags(0x28);
-			splayer->play("LEV02/P2/02P02_A.SAN", 12);
-			_deathFrame = splayer->_frame;
-		}
+		_currentPhase = 2;
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+		_enemies.clear();
+
+		// Play A.SAN (background loader)
+		debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background)");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV02/P2/02P02_A.SAN", 12);
+		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
+		_rebelPhaseState = _rebelWaveState;
 
-		// Now select gameplay variant (B through F)
-		if (_playerShield > 0) {
-			int variant = getRandomVariant(5);
-			const char *variants[] = {"B", "C", "D", "E", "F"};
-			Common::String filename = Common::String::format("LEV02/P2/02P02_%s.SAN", variants[variant]);
+		// Phase 2 wave loop: state-based variant selection
+		// Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
+		while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+			if (_vm->shouldQuit()) return kLevelQuit;
 
-			debug("Rebel2: Level 2 Phase 2 - playing %s (gameplay)", filename.c_str());
+			int waveSelect = _rebelPhaseState & 0x0e;  // masked enemy state
+
+			// If no specific pattern: randomize high bits (original lines 71-74)
+			// When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
+			if ((waveSelect & 0x0c) == 0) {
+				waveSelect = _vm->_rnd.getRandomNumber(2) + 0x10;
+			}
+
+			// Variant selection matching original switch (FUN_418063 lines 75-96)
+			const char *filename;
+			switch (waveSelect) {
+			case 4: case 6:
+				filename = "LEV02/P2/02P02_B.SAN"; break;
+			case 8: case 10:
+				filename = "LEV02/P2/02P02_C.SAN"; break;
+			case 0x0c: case 0x0e:
+				filename = "LEV02/P2/02P02_A.SAN"; break;
+			case 0x11:
+				filename = "LEV02/P2/02P02_E.SAN"; break;
+			case 0x12:
+				filename = "LEV02/P2/02P02_F.SAN"; break;
+			default:
+				filename = "LEV02/P2/02P02_D.SAN"; break;
+			}
+
+			debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
 			splayer->setCurVideoFlags(0x28);
-			splayer->play(filename.c_str(), 12);
+			splayer->play(filename, 12);
 			_deathFrame = splayer->_frame;
-		}
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+			_rebelPhaseState = _rebelWaveState;
+			debug("Rebel2: Phase 2 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
+		}
 
-		if (_playerShield == 0) {
-			// Died in phase 2
-			debug("Rebel2: Level 2 Phase 2 death");
-			playLevelDeathVariant(2, 2, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
+		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
 
-			_playerLives--;
-			if (_playerLives <= 0) {
-				playLevelGameOver(2);
-				return kLevelGameOver;
-			}
-			playLevelRetry(2);
-			if (_vm->shouldQuit()) return kLevelQuit;
-			continue;  // Restart from beginning
-		}
+		if (_playerDamage >= 255) goto level2_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
 
 		// Post segment 2 (02PST2.SAN)
 		splayer->setCurVideoFlags(0x20);
 		splayer->play("LEV02/02PST2.SAN", 12);
 		if (_vm->shouldQuit()) return kLevelQuit;
 
+		totalKills += _rebelKillCounter;
+		totalMisses += _rebelHitCounter;
+
+		// ===== PHASE 3: P3/02P03_X.SAN =====
 		_currentPhase = 3;
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+		_enemies.clear();
+		prevWaveState = 0;
 
-		// Reset bit table before Phase 3 gameplay starts
-		clearBit(0);
+		// Play A.SAN (background loader)
+		debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background)");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV02/P3/02P03_A.SAN", 12);
+		_deathFrame = splayer->_frame;
 
-		// ===== PHASE 3: P3/02P03_X.SAN =====
-		// First play variant A which contains the background IACT for this phase
+		if (_vm->shouldQuit()) return kLevelQuit;
+		_rebelPhaseState = _rebelWaveState;
+
+		// Phase 3 wave loop: state-based with randomization
+		// Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
 		{
-			debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background loader)");
-			splayer->setCurVideoFlags(0x28);
-			splayer->play("LEV02/P3/02P03_A.SAN", 12);
-			_deathFrame = splayer->_frame;
-		}
+			int waveSelect = _rebelPhaseState & 0x0e;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+			while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+				if (_vm->shouldQuit()) return kLevelQuit;
 
-		// Now select gameplay variant (B through I)
-		if (_playerShield > 0) {
-			int variant = getRandomVariant(8);
-			const char *variants[] = {"B", "C", "D", "E", "F", "G", "H", "I"};
-			Common::String filename = Common::String::format("LEV02/P3/02P03_%s.SAN", variants[variant]);
+				// Phase 3 randomization (original lines 113-115):
+				// If previous wave state bit 0 was clear AND random(8)==0, set bit 0
+				if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
+					waveSelect |= 1;
+				}
+				prevWaveState = waveSelect;
+
+				// Variant selection matching original switch (FUN_418063 lines 117-144)
+				const char *filename;
+				switch (waveSelect) {
+				case 0:
+					filename = "LEV02/P3/02P03_H.SAN"; break;
+				case 2:
+					filename = "LEV02/P3/02P03_G.SAN"; break;
+				case 4:
+					filename = "LEV02/P3/02P03_F.SAN"; break;
+				case 6:
+					filename = "LEV02/P3/02P03_E.SAN"; break;
+				case 8:
+					filename = "LEV02/P3/02P03_D.SAN"; break;
+				case 10:
+					filename = "LEV02/P3/02P03_C.SAN"; break;
+				case 0x0c:
+					filename = "LEV02/P3/02P03_B.SAN"; break;
+				case 0x0e:
+					filename = "LEV02/P3/02P03_A.SAN"; break;
+				default:
+					filename = "LEV02/P3/02P03_I.SAN"; break;
+				}
 
-			debug("Rebel2: Level 2 Phase 3 - playing %s (gameplay)", filename.c_str());
-			splayer->setCurVideoFlags(0x28);
-			splayer->play(filename.c_str(), 12);
-			_deathFrame = splayer->_frame;
+				debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
+				splayer->setCurVideoFlags(0x28);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				_rebelPhaseState = _rebelWaveState;
+				waveSelect = _rebelPhaseState & 0x0e;
+				debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
+			}
 		}
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+		totalKills += _rebelKillCounter;
 
-		if (_playerShield == 0) {
-			// Died in phase 3
-			debug("Rebel2: Level 2 Phase 3 death");
-			playLevelDeathVariant(2, 3, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255) goto level2_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
 
-			_playerLives--;
-			if (_playerLives <= 0) {
-				playLevelGameOver(2);
-				return kLevelGameOver;
+		// Level completed! Calculate accuracy score.
+		{
+			int accuracy = 0;
+			int totalShots = totalKills + totalMisses + _rebelHitCounter;
+			if (totalKills > 0 && totalShots > 0) {
+				accuracy = (totalKills * 100) / totalShots;
 			}
-			playLevelRetry(2);
-			if (_vm->shouldQuit()) return kLevelQuit;
-			continue;  // Restart from beginning
+			debug("Rebel2: Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
+				totalKills, totalMisses, accuracy, bonusCount);
 		}
 
-		// Level completed!
-		debug("Rebel2: Level 2 completed! bonusCount=%d", bonusCount);
 		playLevelEnd(2);
 		_levelUnlocked[2] = true;  // Unlock level 3
 		return kLevelNextLevel;
+
+	level2_death:
+		// Player died — play death sequence and retry or game over
+		debug("Rebel2: Level 2 Phase %d death", _currentPhase);
+		playCinematic("LEV02/02DIE.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(2);
+			return kLevelGameOver;
+		}
+		playCinematic("LEV02/02RETRY.SAN");
+		_playerDamage = 0;
+		if (_vm->shouldQuit()) return kLevelQuit;
+		continue;  // Restart from beginning
 	}
 
 	return kLevelQuit;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c6e96de2d28..3c5e92b1492 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -429,6 +429,7 @@ public:
 
 	struct enemy {
 		int id;
+		int type;                 // Enemy type/group from IACT opcode 4 par3 (determines DAT_0047ab98 bit)
 		Common::Rect rect;
 		bool active;
 		bool destroyed;           // Set when enemy is shot - prevents re-activation
@@ -439,7 +440,7 @@ public:
 		int savedBgHeight;        // Height of saved background
 	};
 
-	void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame);
+	void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type = 0);
 	void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	Common::List<enemy> _enemies;
@@ -607,8 +608,16 @@ public:
 
 	// Rebel per-level counters / flags mapped from retail globals
 	int _rebelHitCounter;    // DAT_0047ab80 - hit counter / state tracker
+	int _rebelKillCounter;   // DAT_0047ab88 - enemies destroyed this phase
 	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
 
+	// Enemy wave/phase state tracking (FUN_004028c5 / FUN_00417b61)
+	// DAT_0047ab98: Per-wave enemy kill state. Bits set when enemy types are destroyed.
+	// DAT_0047ab9c: Per-phase accumulated state. Copied from _rebelWaveState between waves.
+	// Phase completion: (_rebelPhaseState & mask) == mask (all required enemy types killed)
+	int _rebelWaveState;     // DAT_0047ab98 - current wave enemy kill flags
+	int _rebelPhaseState;    // DAT_0047ab9c - accumulated phase enemy kill flags
+
 	// Opcode 6 state variables (from FUN_41CADB case 4)
 	int _rebelAutopilot;     // DAT_00457904 - autopilot flag (0 or 1)
 	int _rebelDamageLevel;   // DAT_0045790a - damage level (0-5)


Commit: f2c9358f1b04086e9cbcceb016a9a20cd4177aeb
    https://github.com/scummvm/scummvm/commit/f2c9358f1b04086e9cbcceb016a9a20cd4177aeb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:04+02:00

Commit Message:
SCUMM: RA2: Refactor level 2 rendering code

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 480bca2168f..28fd6a5d3d1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -917,96 +917,9 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// For Mode 1: DAT_0045790c = damageLevel * -5 - 14, range -39 (covered) to -14 (uncovered)
 	//
 	// From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
-	// This creates a "scrolling window" effect as the character enters/exits cover.
-	if (_rebelHandler == 25 && renderBitmap) {
-		// Calculate pitch and buffer height for both corridor overlay and GRD001 rendering
-		int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
-		int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
-		// FIRST: Restore Level 2 background (par4=5) as the base layer
-		// This is the scene background that enemies (FOBJ) draw on top of.
-		// Without this, enemies are drawn onto black/empty buffer and appear invisible.
-		// The corridor overlay (par4=4) is a HUD frame drawn ON TOP of this background.
-		if (_level2BackgroundLoaded && _level2Background) {
-			for (int y = 0; y < MIN(200, bufHeight); y++) {
-				memcpy(renderBitmap + y * pitch, _level2Background + y * 320, MIN(320, pitch));
-			}
-			debug("Rebel2 Handler25 PRE: Restored _level2Background to renderBitmap");
-		} else {
-			debug("Rebel2 Handler25 PRE: WARNING - _level2Background NOT restored (loaded=%d ptr=%p)",
-				_level2BackgroundLoaded, (void*)_level2Background);
-		}
-
-		EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
-		if (corridorOverlay.valid && corridorOverlay.pixels) {
-
-			// Calculate source offset and destination position (FUN_00428a10 lines 31-43)
-			// If position is negative, skip source pixels and draw at screen edge
-			// Use _rebelViewOffsetX which is DAT_0045790c (damageLevel * -5 - 14)
-			int srcOffsetX = 0;
-			int srcOffsetY = 0;
-			int destX = _rebelViewOffsetX;   // DAT_0045790c
-			int destY = _rebelViewOffsetY;   // DAT_0045790e
-			int drawWidth = corridorOverlay.width;
-			int drawHeight = corridorOverlay.height;
-
-			// Handle negative X position: skip source pixels, draw at X=0
-			if (destX < 0) {
-				srcOffsetX = -destX;
-				drawWidth -= srcOffsetX;
-				destX = 0;
-			}
-			// Handle negative Y position: skip source pixels, draw at Y=0
-			if (destY < 0) {
-				srcOffsetY = -destY;
-				drawHeight -= srcOffsetY;
-				destY = 0;
-			}
-
-			// Clip to screen bounds
-			if (destX + drawWidth > pitch) {
-				drawWidth = pitch - destX;
-			}
-			if (destY + drawHeight > bufHeight) {
-				drawHeight = bufHeight - destY;
-			}
-
-			// Bounds check: ensure srcOffsetX doesn't exceed image width
-			if (srcOffsetX >= corridorOverlay.width) {
-				debug("Rebel2 procPreRendering: srcOffsetX (%d) >= image width (%d), skipping", srcOffsetX, corridorOverlay.width);
-			}
-			// Bounds check: ensure we have valid draw dimensions
-			else if (drawWidth > 0 && drawHeight > 0) {
-				// Additional safety: clamp to source image bounds
-				int maxDrawWidth = corridorOverlay.width - srcOffsetX;
-				int maxDrawHeight = corridorOverlay.height - srcOffsetY;
-				if (drawWidth > maxDrawWidth) drawWidth = maxDrawWidth;
-				if (drawHeight > maxDrawHeight) drawHeight = maxDrawHeight;
-
-				if (drawWidth > 0 && drawHeight > 0) {
-					for (int y = 0; y < drawHeight; y++) {
-						for (int x = 0; x < drawWidth; x++) {
-							int srcIdx = (srcOffsetY + y) * corridorOverlay.width + (srcOffsetX + x);
-							byte pixel = corridorOverlay.pixels[srcIdx];
-							if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
-								renderBitmap[(destY + y) * pitch + (destX + x)] = pixel;
-							}
-						}
-					}
-				}
-			}
-
-			debug("Rebel2 procPreRendering: Corridor overlay viewOff=(%d,%d) damageLevel=%d autopilot=%d srcOff=(%d,%d) dest=(%d,%d) draw=(%d,%d) imgSize=(%d,%d)",
-				_rebelViewOffsetX, _rebelViewOffsetY, _rebelDamageLevel, _rebelAutopilot,
-				srcOffsetX, srcOffsetY, destX, destY, drawWidth, drawHeight,
-				corridorOverlay.width, corridorOverlay.height);
-		}
-
-		// Draw GRD001 (wall/cockpit overlay) AFTER corridor but BEFORE FOBJ enemies.
-		// This ensures enemies from FOBJ chunks draw ON TOP of the cockpit overlay.
-		// Uses width-halving logic from FUN_0041db5e lines 202-221.
-		renderHandler25ShipPre(renderBitmap, pitch, pitch, bufHeight);
-	}
+	// Handler 25: Corridor overlay and FOBJ position offsets are set during
+	// IACT opcode 6 processing (iactRebel2Opcode6), matching the original
+	// FUN_41CADB architecture. No corridor drawing needed here.
 }
 
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -1219,14 +1132,15 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 		if (targetId < 1 || targetId >= 0x200)
 			return;
 
-		// Handler 8 specific: FUN_401234 case 0, par3==1, par4!=0
+		// Handler 8/25: FUN_401234 case 0 / FUN_41E7C2 par3==1, par4!=0
 		// "If enemy type par4 has been killed (bit set in wave state), disable targetId"
-		// This conditionally hides entities based on which enemy groups have been destroyed
-		if (_rebelHandler == 8 && value != 0) {
+		// This conditionally hides entities based on which enemy groups have been destroyed.
+		// Both Handler 8 (on-foot) and Handler 25 (FPS/cover) use DAT_0047ab98 for wave state.
+		if ((_rebelHandler == 8 || _rebelHandler == 25) && value != 0) {
 			int bitMask = 1 << (value & 0x1f);
 			if ((_rebelWaveState & bitMask) != 0) {
 				setBit(targetId);
-				debug("Rebel2 Opcode2 (H8): Disable target=%d (type %d killed, wave=0x%x)", targetId, value, _rebelWaveState);
+				debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
 			}
 			return;
 		}
@@ -1631,11 +1545,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 				_rebelLinks[i][1] = 0;
 				_rebelLinks[i][2] = 0;
 			}
+			// Reset wave state to accumulated phase state (same as Handler 8)
+			// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
+			_rebelWaveState = _rebelPhaseState;
 			// Initialize to covered state - player starts behind cover
 			// This ensures the corridor overlay shows the "covered" position initially
 			_rebelAutopilot = 1;    // DAT_00457904 = 1 (covered)
 			_rebelDamageLevel = 5;  // DAT_0045790a = 5 (fully in cover)
-			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED");
+			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED, wave=0x%x", _rebelWaveState);
 		}
 
 		// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
@@ -1778,6 +1695,47 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
 			_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
 
+		// Set FOBJ position offsets (FUN_00424510 in original, line 214)
+		// All subsequent FOBJs in this frame will be shifted by these offsets
+		if (_player) {
+			_player->_fobjOffsetX = _rebelViewOffsetX;
+			_player->_fobjOffsetY = _rebelViewOffsetY;
+		}
+
+		// Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216)
+		// This wipes previous frame content so codec 23 delta skip regions show clean corridor
+		if (renderBitmap) {
+			EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
+			if (corridorOverlay.valid && corridorOverlay.pixels) {
+				int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+				int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+				int srcOffsetX = 0;
+				int srcOffsetY = 0;
+				int destX = _rebelViewOffsetX;
+				int destY = _rebelViewOffsetY;
+				int drawWidth = corridorOverlay.width;
+				int drawHeight = corridorOverlay.height;
+
+				if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
+				if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
+				if (destX + drawWidth > pitch) drawWidth = pitch - destX;
+				if (destY + drawHeight > bufHeight) drawHeight = bufHeight - destY;
+				if (drawWidth > corridorOverlay.width - srcOffsetX) drawWidth = corridorOverlay.width - srcOffsetX;
+				if (drawHeight > corridorOverlay.height - srcOffsetY) drawHeight = corridorOverlay.height - srcOffsetY;
+
+				if (drawWidth > 0 && drawHeight > 0) {
+					for (int y = 0; y < drawHeight; y++) {
+						memcpy(renderBitmap + (destY + y) * pitch + destX,
+							   corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
+							   drawWidth);
+					}
+				}
+				debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
+					_rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
+			}
+		}
+
 		return;
 	}
 
@@ -2722,12 +2680,16 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 	// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
 
 	// Update enemy list for hit detection
+	// Enemy type comes from par4 (IACT offset +6), NOT par3 (offset +4).
+	// In the original (FUN_004028C5/FUN_0041E7C2): sVar5/sVar2 = *(short *)(*local + 6)
+	// This maps to par4 (userId field). Used for DAT_0047ab98 wave state bitmask:
+	//   DAT_0047ab98 |= 1 << (type & 0x1f)
 	bool found = false;
 	Common::List<enemy>::iterator it;
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
 		if (it->id == enemyId) {
 			it->rect = Common::Rect(x, y, x + w, y + h);
-			it->type = par3;  // Update enemy type/group
+			it->type = par4;  // Enemy type from IACT offset +6 (userId)
 			// Only re-activate if not destroyed
 			if (!it->destroyed) {
 				it->active = true;
@@ -2737,7 +2699,7 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 		}
 	}
 	if (!found) {
-		init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par3);
+		init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
 	}
 }
 
@@ -4090,6 +4052,8 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	renderHandler7Ship(renderBitmap, pitch, width, height);
 	renderHandler8Ship(renderBitmap, pitch, width, height);
+	// GRD001 (wall/cockpit) drawn AFTER FOBJs per original FUN_0041DB5E lines 202-210
+	renderHandler25ShipPre(renderBitmap, pitch, width, height);
 	renderHandler25Ship(renderBitmap, pitch, width, height);
 	renderFallbackShip(renderBitmap, pitch, width, height);
 
@@ -4129,6 +4093,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// HUD score/lives rendering (FUN_0041c012)
 	renderScoreHUD(renderBitmap, pitch, width, height, 0);
 
+	// Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
+	if (_player) {
+		_player->_fobjOffsetX = 0;
+		_player->_fobjOffsetY = 0;
+	}
+
 	// Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
 	frameEndCleanup();
 }
@@ -4364,9 +4334,10 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 		if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
 			continue;
 
-		// For Handler 25: Skip slot 4 (corridor overlay) here - it's already drawn
-		// in procPreRendering BEFORE FOBJ enemies. Drawing it again here (after FOBJs)
-		// would cover the enemies and make them invisible.
+		// Handler 25: Skip slot 4 (corridor overlay) in post-rendering.
+		// The corridor is a full background image (no color 0 transparent center).
+		// Drawing it here would cover enemies. It's already drawn in procPreRendering
+		// with transparency to preserve frame persistence for codec 23 delta.
 		if (_rebelHandler == 25 && hudSlot == 4) {
 			continue;
 		}
@@ -4635,8 +4606,8 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 		spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
 }
 
-// Handler 25 PRE-rendering: Draw GRD001 BEFORE FOBJ decoding
-// This is called from procPreRendering so enemies draw ON TOP of GRD001
+// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
+// Per original FUN_0041DB5E, GRD sprites are drawn AFTER FOBJ enemies, before GRD002.
 //
 // From FUN_0041db5e disassembly (lines 202-221):
 // - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
@@ -4714,9 +4685,8 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 }
 
 void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 25 POST-rendering: Draw ONLY GRD002 (character sprite)
-	// GRD001 (wall/cockpit) is now drawn in renderHandler25ShipPre() during procPreRendering
-	// so that FOBJ enemies can draw ON TOP of it.
+	// Handler 25 POST-rendering: Draw GRD002 (character sprite) on top of enemies.
+	// GRD001 (wall/cockpit) is drawn before this via renderHandler25ShipPre().
 	//
 	// From FUN_0041db5e disassembly (lines 230-248):
 	// GRD002 is drawn LAST (after enemies) so the character appears in front.
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index bed00ea3049..806e14e4435 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -262,6 +262,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 
 	_skipNext = false;
 	_ra2FastForwarding = false;
+	_fobjOffsetX = 0;
+	_fobjOffsetY = 0;
 	_dst = nullptr;
 	_storeFrame = false;
 	_compressedFileMode = false;
@@ -436,19 +438,23 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
 	assert(subSize >= 6);
 
-	// For RA2 Handlers 8 and 25, skip FTCH because the frame buffer only contains the
+	// For RA2 Handler 25, skip FTCH because the frame buffer only contains the
 	// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
 	// immediately in frame 0. Restoring would erase those overlays and make
 	// enemies invisible since they draw on top of the erased areas.
 	//
-	// Handler 8 (Level 2 on-foot): Background is drawn in procPreRendering from
-	// _level2Background. FTCH would overwrite FOBJ-drawn enemies.
-	// Handler 25 (Level 2 speeder bike): Same issue with corridor overlays.
+	// Handler 25 (Level 2 speeder bike): Corridor overlay is drawn in procPreRendering
+	// BEFORE FOBJs. FTCH would erase the overlay, making enemies draw on wrong background.
+	//
+	// Handler 8 (Level 2 on-foot): FTCH is NOT skipped here. FTCH restores the clean
+	// background each frame, which properly erases old enemy sprite positions before
+	// new FOBJs draw updated positions. procPreRendering also restores _level2Background,
+	// but FTCH provides the authoritative clean slate from frame 0's stored background.
 	if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
 		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
 		int handler = rebel2->getHandler();
-		if (handler == 8 || handler == 25) {
-			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler %d - preserving overlays", handler);
+		if (handler == 25) {
+			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
 			return;
 		}
 	}
@@ -1081,6 +1087,32 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if (_dst == _specialBuffer)
 		pitch = _width;
 
+	// RA2: Apply global FOBJ position offsets (matching original FUN_00423A50)
+	// These are set by InsaneRebel2 during IACT opcode 6 processing
+	if (_vm->_game.id == GID_REBEL2) {
+		left += _fobjOffsetX;
+		top += _fobjOffsetY;
+	}
+
+	// Bounds check: clamp FOBJ to destination buffer dimensions
+	int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+	if (top < 0) {
+		height += top;
+		top = 0;
+	}
+	if (left < 0) {
+		width += left;
+		left = 0;
+	}
+	if (top + height > bufHeight) {
+		height = bufHeight - top;
+	}
+	if (left + width > pitch) {
+		width = pitch - left;
+	}
+	if (width <= 0 || height <= 0)
+		return;
+
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
 	case SMUSH_CODEC_RLE_ALT:
@@ -1208,11 +1240,28 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		const int32 subSize = b.readUint32BE();
 		const int32 subOffset = b.pos();
 
-		// Debug: Log all chunk types for first few frames
-		if (_vm->_game.id == GID_REBEL2 && _frame < 5) {
+		// Debug: Log all chunk types for first 25 frames
+		if (_vm->_game.id == GID_REBEL2 && _frame < 25) {
 			debug("SmushPlayer::handleFrame: frame=%d chunk=%s size=%d", _frame, tag2str(subType), subSize);
 		}
 
+		// RA2 SKIP mechanism (matching original FUN_00423A50 bVar6):
+		// When _skipNext is set, skip the NEXT chunk of ANY type (FOBJ, PSAD, SKIP, etc.)
+		// In the original, bVar6 is checked at the top of the loop before the type switch,
+		// corrupting the tag so no handler matches. This consumes exactly one chunk.
+		// Critical: this must also skip SKIP chunks to prevent SKIP→SKIP→FOBJ misalignment.
+		if (_vm->_game.id == GID_REBEL2 && _skipNext) {
+			_skipNext = false;
+			debug("SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+			frameSize -= subSize + 8;
+			b.seek(subOffset + subSize, SEEK_SET);
+			if (subSize & 1) {
+				b.skip(1);
+				frameSize--;
+			}
+			continue;
+		}
+
 		switch (subType) {
 		case MKTAG('N','P','A','L'):
 			handleNewPalette(subSize, b);
@@ -1224,13 +1273,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleZlibFrameObject(subSize, b);
 			break;
 		case MKTAG('P','S','A','D'):
-			// Rebel Assault 2 only: Skip sound when _skipNext is set (enemy killed)
-			// This mirrors the original game's FUN_00423A50 behavior where
-			// SKIP tags cause subsequent PSAD/SAUD chunks to be skipped
-			if (_vm->_game.id == GID_REBEL2 && _skipNext) {
-				_skipNext = false;
-				break;
-			}
 			if (!_compressedFileMode) {
 				audioChunk = (uint8 *)malloc(subSize + 8);
 				b.seek(-8, SEEK_CUR);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 62b0e0c1500..43ecc269d9b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -162,6 +162,12 @@ private:
 	bool _ra2FastForwarding;  // Fast-forwarding RA2 BEG video to establish background
 	uint32 _frame;
 
+	// RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
+	// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
+	// Applied to all FOBJ left/top positions during decoding.
+	int16 _fobjOffsetX;
+	int16 _fobjOffsetY;
+
 	Audio::SoundHandle *_IACTchannel;
 	Audio::QueuingAudioStream *_IACTstream;
 


Commit: 41488ed0aaaa25e04c79d55863b0c720675559d6
    https://github.com/scummvm/scummvm/commit/41488ed0aaaa25e04c79d55863b0c720675559d6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:04+02:00

Commit Message:
SCUMM: RA2: Implement per-enemy explosion handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 28fd6a5d3d1..c8b8f7235e2 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -734,11 +734,33 @@ int32 InsaneRebel2::processMouse() {
 					it->id, it->type, mousePos.x, mousePos.y,
 					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
 
-				// Spawn explosion using native system
-				// Use width / 2 as the scale parameter
-				spawnExplosion((it->rect.left + it->rect.right) / 2,
-							   (it->rect.top + it->rect.bottom) / 2,
-							   it->rect.width() / 2);
+				// Spawn visual explosion based on handler and enemy type.
+				//
+				// Each handler's explosion rendering (FUN_409FBC, FUN_402696,
+				// FUN_40F1C5, FUN_41F29A) checks a per-level flags field:
+				//   (*(ushort *)(&DAT_0047e108 + chapter*0x242 + level*0x22) & 1) == 0
+				// When bit 0 is SET, explosion NUT sprites are NOT rendered even
+				// though the counter ticks down. The flags come from GAME.TRS.
+				//
+				// Handler 8 (FUN_4028C5 line 94): Only type 0 sets the explosion
+				// counter at all. Types 1-4 get BLAST sound, no visual explosion.
+				//
+				// Handler 25 (FUN_41E7C2 line 74): Types > 3 DO set the counter,
+				// but rendering is suppressed by flags & 1 for on-foot levels.
+				// The counter serves only as a timer (sound panning, tracking).
+				// Handler 25 is specifically for on-foot corridor/FPS sections;
+				// space combat uses handler 7 instead.
+				//
+				// Handlers 0x26, 7: All types get visual explosions.
+				if (_rebelHandler != 8 && _rebelHandler != 25) {
+					spawnExplosion((it->rect.left + it->rect.right) / 2,
+								   (it->rect.top + it->rect.bottom) / 2,
+								   it->rect.width() / 2);
+				} else if (_rebelHandler == 8 && it->type == 0) {
+					spawnExplosion((it->rect.left + it->rect.right) / 2,
+								   (it->rect.top + it->rect.bottom) / 2,
+								   it->rect.width() / 2);
+				}
 
 				// Disable self (prevents sprite from rendering via SKIP chunks)
 				setBit(it->id);
@@ -759,7 +781,7 @@ int32 InsaneRebel2::processMouse() {
 				if (id >= 0 && id < 512) {
 					// Slot 2: Enable (Explosion?)
 					if (_rebelLinks[id][2] != 0) {
-						clearBit(_rebelLinks[id][2]); 
+						clearBit(_rebelLinks[id][2]);
 						debug("Rebel2: Enabled dependency Slot 2 (ID=%d) for Parent %d", _rebelLinks[id][2], id);
 					}
 					// Slot 1: Enable (Explosion?)
@@ -774,9 +796,15 @@ int32 InsaneRebel2::processMouse() {
 					}
 				}
 
-				// Note: Background saving and masking is handled in procPostRendering
-				// where we have access to the render bitmap
-				// TODO: Play explosion sound
+				// Play explosion sound.
+				// Handler 8 types 1-4: BLAST.SAD (slot 0) via different sound function
+				// All others: EXPLODE.SAD (slot 2)
+				// TODO: Implement actual SAD sound playback
+				if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
+					debug("Rebel2: BLAST sound for enemy type %d (no visual explosion)", it->type);
+				} else {
+					debug("Rebel2: EXPLODE sound for enemy type %d", it->type);
+				}
 
 				// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
 				// Score value comes from lookup table DAT_0047e0fe indexed by difficulty
@@ -2684,6 +2712,9 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 	// In the original (FUN_004028C5/FUN_0041E7C2): sVar5/sVar2 = *(short *)(*local + 6)
 	// This maps to par4 (userId field). Used for DAT_0047ab98 wave state bitmask:
 	//   DAT_0047ab98 |= 1 << (type & 0x1f)
+	debug(5, "Rebel2 Opcode4: handler=%d enemyId=%d par2=%d par3=%d par4/type=%d pos=(%d,%d) size=(%d,%d)",
+		_rebelHandler, enemyId, par2, par3, par4, x, y, w, h);
+
 	bool found = false;
 	Common::List<enemy>::iterator it;
 	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -3124,8 +3155,15 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 end_parsing:;
 }
 
+// Spawn explosion into the shared 5-slot system.
+// In the original, each handler has its own spawn logic inside its enemy processing function:
+//   Handler 0x26: FUN_40A2E0 (0x40A2E0) — spawns in slot arrays DAT_0044368e[]
+//   Handler 8:    FUN_4028C5 (0x4028C5) — spawns in slot arrays DAT_0043f854[]
+//   Handler 7:    FUN_40F628 (0x40F628) — spawns in slot arrays DAT_00443770[]
+//   Handler 25:   FUN_41E7C2 (0x41E7C2) — spawns in slot arrays DAT_0045792c[]
+// All share the same logic: find first free slot (counter==0), set counter=10,
+// scale=objectHalfWidth, position=enemy center, velocity=0.
 void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
-	// Find first free slot (FUN_40A2E0 logic)
 	for (int i = 0; i < 5; i++) {
 		if (!_explosions[i].active || _explosions[i].counter <= 0) {
 			_explosions[i].active = true;
@@ -3133,7 +3171,6 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 			_explosions[i].x = x;
 			_explosions[i].y = y;
 			_explosions[i].scale = objectHalfWidth;
-			// TODO: Play sound via FUN_0041189e equivalent
 			break;
 		}
 	}
@@ -4915,9 +4952,172 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 	}
 }
 
+// Dispatcher — calls per-handler explosion render function.
+// Original code has separate functions per handler, each with its own
+// position transformation, scale thresholds, and secondary NUT rendering.
 void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
-	// Draw explosion animations from 5-slot system
+	switch (_rebelHandler) {
+	case 0x26:
+		renderTurretExplosions(renderBitmap, pitch, width, height);
+		break;
+	case 8:
+		renderVehicleExplosions(renderBitmap, pitch, width, height);
+		break;
+	case 7:
+		renderSpaceExplosions(renderBitmap, pitch, width, height);
+		break;
+	case 25:
+		renderHandler25Explosions(renderBitmap, pitch, width, height);
+		break;
+	default:
+		break;
+	}
+}
+
+// FUN_409FBC — Handler 0x26 (Turret/Cockpit) explosion rendering.
+// Position: Uses FUN_0041c720 for 3D→2D projection. At low-res, world coords ≈ screen coords.
+// Scale thresholds: Fixed (<11, <21).
+// Secondary NUT: DAT_0047fe80 (rendered if DAT_0047a7fc >= 0).
+// Hi-res: Coordinates doubled when DAT_0047a808 >= 2.
+void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
+	if (!_smush_iconsNut)
+		return;
+
+	for (int i = 0; i < 5; i++) {
+		if (!_explosions[i].active)
+			continue;
+
+		if (_explosions[i].counter <= 0) {
+			_explosions[i].active = false;
+			continue;
+		}
+
+		// FUN_409FBC: Fixed thresholds (0x0b=11, 0x15=21)
+		int baseIndex;
+		if (_explosions[i].scale < 11) {
+			baseIndex = 9;   // Small (sprites 11-20)
+		} else if (_explosions[i].scale < 21) {
+			baseIndex = 19;  // Medium (sprites 21-30)
+		} else {
+			baseIndex = 29;  // Large (sprites 31-40)
+		}
+
+		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+		// Position: world coords passed through FUN_0041c720 (3D→2D projection).
+		// At 320x200 low-res turret view, projection is effectively identity.
+		int screenX = _explosions[i].x;
+		int screenY = _explosions[i].y;
+
+		if (_smush_iconsNut->getNumChars() > spriteIndex) {
+			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+			renderNutSprite(renderBitmap, pitch, width, height,
+				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+		}
+
+		_explosions[i].counter--;
+	}
+}
+
+// FUN_402696 — Handler 8 (Third-Person On-Foot) explosion rendering.
+// Position: World coords minus camera offset (DAT_0043e006/DAT_0043e008 = _viewX/_viewY).
+// Scale thresholds: Fixed (<11, <21) — same as handler 0x26.
+// Secondary NUT: None (only DAT_0047a828).
+void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
+	if (!_smush_iconsNut)
+		return;
+
+	for (int i = 0; i < 5; i++) {
+		if (!_explosions[i].active)
+			continue;
+
+		if (_explosions[i].counter <= 0) {
+			_explosions[i].active = false;
+			continue;
+		}
+
+		// FUN_402696: Fixed thresholds (0x0b=11, 0x15=21)
+		int baseIndex;
+		if (_explosions[i].scale < 11) {
+			baseIndex = 9;
+		} else if (_explosions[i].scale < 21) {
+			baseIndex = 19;
+		} else {
+			baseIndex = 29;
+		}
+
+		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+		// FUN_402696 line 22-23: screenX = worldX - DAT_0043e006, screenY = worldY - DAT_0043e008
+		int screenX = _explosions[i].x - _viewX;
+		int screenY = _explosions[i].y - _viewY;
+
+		if (_smush_iconsNut->getNumChars() > spriteIndex) {
+			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+			renderNutSprite(renderBitmap, pitch, width, height,
+				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+		}
+
+		_explosions[i].counter--;
+	}
+}
+
+// FUN_40F1C5 — Handler 7 (Third-Person Ship) explosion rendering.
+// Position: Uses FUN_0041c720 for 3D→2D projection.
+// Scale thresholds: Resolution-dependent (low-res: <11/<21, high-res: <21/<41).
+// Secondary NUT: DAT_0047ff00 (FLY004, rendered if DAT_0047a7fc >= 0).
+void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
+	if (!_smush_iconsNut)
+		return;
+
+	for (int i = 0; i < 5; i++) {
+		if (!_explosions[i].active)
+			continue;
+
+		if (_explosions[i].counter <= 0) {
+			_explosions[i].active = false;
+			continue;
+		}
+
+		// FUN_40F1C5 lines 41-51: Resolution-dependent thresholds.
+		// Low-res (DAT_0047a808 < 2): thresholds 20, 10
+		// High-res: thresholds 40, 20
+		// We run at low-res (320x200), so use 10/20 (same as fixed handlers).
+		int baseIndex;
+		if (_explosions[i].scale < 11) {
+			baseIndex = 9;
+		} else if (_explosions[i].scale < 21) {
+			baseIndex = 19;
+		} else {
+			baseIndex = 29;
+		}
 
+		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
+
+		// Position: world coords through FUN_0041c720 (3D→2D projection).
+		// At low-res, this is close to identity for the ship view.
+		int screenX = _explosions[i].x;
+		int screenY = _explosions[i].y;
+
+		if (_smush_iconsNut->getNumChars() > spriteIndex) {
+			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+			renderNutSprite(renderBitmap, pitch, width, height,
+				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+		}
+
+		_explosions[i].counter--;
+	}
+}
+
+// FUN_41F29A — Handler 25 (FPS/Mixed) explosion rendering.
+// Position: World coords + view offset (DAT_0045790c/DAT_0045790e = _rebelViewOffsetX/_rebelViewOffsetY).
+// Scale thresholds: Resolution-dependent (same formula as Handler 7).
+// Secondary NUT: DAT_00482260 (hi-res HUD alternative, rendered if DAT_0047a7fc >= 0).
+// Note: No per-frame sound panning update (unlike handlers 0x26, 8, 7).
+void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
 
@@ -4930,25 +5130,27 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
 			continue;
 		}
 
-		// Determine base sprite index based on scale (FUN_409FBC logic)
+		// FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
 		int baseIndex;
 		if (_explosions[i].scale < 11) {
-			baseIndex = 9;   // Small/Medium
+			baseIndex = 9;
 		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;  // Medium/Large
+			baseIndex = 19;
 		} else {
-			baseIndex = 29;  // Large/XL
+			baseIndex = 29;
 		}
 
-		// Formula: Base + (12 - Counter)
 		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
 
+		// FUN_41F29A line 22-23: screenX = worldX + DAT_0045790c, screenY = worldY + DAT_0045790e
+		int screenX = _explosions[i].x + _rebelViewOffsetX;
+		int screenY = _explosions[i].y + _rebelViewOffsetY;
+
 		if (_smush_iconsNut->getNumChars() > spriteIndex) {
 			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
 			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			int cx = _explosions[i].x - ew / 2;
-			int cy = _explosions[i].y - eh / 2;
-			renderNutSprite(renderBitmap, pitch, width, height, cx, cy, _smush_iconsNut, spriteIndex);
+			renderNutSprite(renderBitmap, pitch, width, height,
+				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
 		}
 
 		_explosions[i].counter--;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 3c5e92b1492..e526234ac13 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -350,7 +350,7 @@ public:
 	// Draw enemy indicator brackets and erase destroyed enemy areas
 	void renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth);
 
-	// Draw explosion animations from 5-slot system
+	// Draw explosion animations from 5-slot system (dispatcher)
 	void renderExplosions(byte *renderBitmap, int pitch, int width, int height);
 
 	// Draw laser shot beams and impacts
@@ -709,6 +709,12 @@ public:
 	void spawnHandler25Shot(int x, int y); // Handler 25 (speeder bike)
 	void spawnShot(int x, int y);          // Dispatcher based on current handler
 
+	// Handler-specific explosion rendering
+	void renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height);     // FUN_409FBC (Handler 0x26)
+	void renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height);    // FUN_402696 (Handler 8)
+	void renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height);      // FUN_40F1C5 (Handler 7)
+	void renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height);  // FUN_41F29A (Handler 25)
+
 	// Handler-specific laser rendering (FUN_40AD63, FUN_402ED0, FUN_40FADF, FUN_0041f004)
 	void renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height);
 	void renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height);


Commit: bcd6615899b34794ee059d90d9c09dd411172652
    https://github.com/scummvm/scummvm/commit/bcd6615899b34794ee059d90d9c09dd411172652
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:04+02:00

Commit Message:
SCUMM: RA2: Implement damage handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c8b8f7235e2..b4b17c1881a 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1065,7 +1065,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 
 		if (par2 == 0x0D || par2 == 0x0E) {
 			// Register the collision zone from the remaining IACT data
-			registerCollisionZone(b, par2);
+			// par4 (userId from IACT header) is the filter value used by FUN_4092D9
+			// for the < 1000 test (offset +6 in the original stored pointer)
+			registerCollisionZone(b, par2, par4);
 		}
 
 	} else if (par1 == 7) {
@@ -1218,18 +1220,77 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 	}
 }
 void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// FUN_00401234 case 1 (Handler 8): IACT opcode 3 — damage and hit counter.
+	// IACT opcode 3 — damage and hit counter processing.
+	// Based on FUN_4092D9 (Handler 0x26), FUN_40E35E (Handler 7), FUN_401234 (Handler 8).
 	//
-	// par3 == 5: Damage path — probability check, accumulate damage, trigger visual effect
-	//   Original reads probability from per-level table DAT_0047e0fc[levelIdx]
-	//   and damage amount from DAT_0047e0f8[levelIdx].
-	//   triggerDamageEffect (FUN_0042073B) is called OUTSIDE invulnerability check.
+	// The common dispatcher (FUN_4033CF) stores opcode 3 entries in the projectile impact
+	// list (DAT_0043f9e0). For handlers 0x26/7 these are processed per-frame by the
+	// per-handler collision function (FUN_4092D9/FUN_40E35E). For handlers 8/25 they're
+	// processed immediately during IACT dispatch.
 	//
-	// par3 == 1: Hit counter increment (DAT_0047ab80)
+	// FUN_403ba9() loop in FUN_4092D9 (lines 209-239):
+	//   par3 == 1/2: Direct hit — increment hit counter, apply damage if conditions met
+	//     - body[0] (offset +8): srcId for isBitSet check
+	//     - par4 != 0: damage from DAT_0047e0f4 (direct hit damage table)
+	//     - par3==1: par4 must be 1..9 for damage
+	//     - par3==2: par4 must be > 99, with wave state bit check for par4 >= 101
+	//
+	//   par3 == 5: Probabilistic damage — probability check from DAT_0047e0fc
+	//     - body[1] (offset +10): srcId for isBitSet check (different from par3=1/2!)
+	//     - Damage from DAT_0047e0f8 (probabilistic damage table)
+	//
+	// Stream position on entry: at offset +8 (body[0], first word after 8-byte header)
+
+	if (par3 == 1 || par3 == 2) {
+		// Direct hit path — FUN_4092D9 lines 209-227
+		int16 srcId = b.readSint16LE(); // body[0] (offset +8): source enemy ID
+
+		debug("Rebel2 Opcode3: par3=%d par4=%d srcId=%d isBitSet=%d",
+			par3, par4, srcId, isBitSet(srcId));
+
+		// FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
+		if (!isBitSet(srcId)) {
+			// Always increment hit counter (DAT_0047ab80) — line 215
+			_rebelHitCounter++;
+			debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
+
+			// Damage path: par4 must be non-zero AND direct hit damage table > 0
+			// TODO: Read actual damage from per-level table DAT_0047e0f4
+			int directHitDamage = 8 + (_difficulty * 4);
+
+			if (par4 != 0 && directHitDamage > 0) {
+				bool shouldDamage = false;
+
+				if (par3 == 1 && par4 < 10) {
+					// par3=1: simple direct hit, par4 is hit strength (1..9)
+					shouldDamage = true;
+				} else if (par3 == 2 && par4 > 99) {
+					// par3=2: wave-gated hit, par4 > 99 required
+					// Additional check: for par4 >= 101 (0x65), verify wave state bit
+					// Original: (DAT_0047ab9c & (1 << ((par4 + 0x9b) & 0x1f))) == 0
+					if (par4 < 0x65 || (_rebelPhaseState & (1 << ((par4 + 0x9b) & 0x1f))) == 0) {
+						shouldDamage = true;
+					}
+				}
 
-	if (par3 == 5) {
-		b.skip(2); // Offset +8 (unused)
-		int16 srcId = b.readSint16LE(); // Offset +10: source enemy ID (local_14[5])
+				if (shouldDamage) {
+					// Apply damage unless invulnerable (DAT_0047ab64 == 0)
+					if (!_rebelInvulnerable) {
+						_playerDamage += directHitDamage;
+						if (_playerDamage > 255) _playerDamage = 255;
+						debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
+							srcId, par3, par4, directHitDamage, _playerDamage);
+					}
+					// Visual effect — FUN_00420515 (palette flash)
+					initDamageFlash();
+					// TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) — play hit sound
+				}
+			}
+		}
+	} else if (par3 == 5) {
+		// Probabilistic damage path — FUN_4092D9 lines 228-239
+		b.skip(2); // Skip body[0] (offset +8, not used for par3=5)
+		int16 srcId = b.readSint16LE(); // body[1] (offset +10): source enemy ID
 
 		debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
 
@@ -1251,7 +1312,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 					int damageAmount = 5 + (_difficulty * 2);
 					_playerDamage += damageAmount;
 					if (_playerDamage > 255) _playerDamage = 255;
-					debug("Rebel2: Damage HIT by Enemy %d. Damage=%d", srcId, _playerDamage);
+					debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
+						srcId, damageAmount, _playerDamage);
 				}
 				// Visual effect — called regardless of invulnerability.
 				// Handler 8: FUN_0042073B — palette flash + screen shake
@@ -1264,10 +1326,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 				// TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) — play hit sound
 			}
 		}
-	} else if (par3 == 1) {
-		// Hit counter increment: unconditional in original (no isBitSet or par4 check)
-		_rebelHitCounter++;
-		debug("Rebel2: Incremented hit counter DAT_0047ab80 -> %d", _rebelHitCounter);
+	} else {
+		debug("Rebel2 Opcode3: UNHANDLED par3=%d par4=%d", par3, par4);
 	}
 }
 
@@ -3663,31 +3723,39 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
 // Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
 // Zones are quadrilaterals registered via IACT opcode 5
 
-void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode) {
-	// IACT Opcode 5 data layout (from info.md):
-	//   +0x00: opcode (5) - already read by caller
-	//   +0x02: sub-opcode (0x0D or 0x0E) - passed as parameter
-	//   +0x04: par3 (flags)
-	//   +0x06: zoneType
-	//   +0x08: frameStart
-	//   +0x0A: frameEnd
-	//   +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
+void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4) {
+	// IACT Opcode 5 data layout — corrected from FUN_4033CF / FUN_4092D9 analysis:
+	//
+	// Original game stores pointer to full IACT data (starting at opcode).
+	// SmushPlayer reads the first 8 bytes as header (code/flags/unknown/userId),
+	// so our stream starts at body[0] (IACT byte offset +8).
 	//
-	// The stream position is currently at offset +0x04 (after opcode and sub-opcode)
-
-	int16 par3 = b.readSint16LE();       // +0x04 (flags - unused for now)
-	(void)par3;  // Suppress unused variable warning
-	int16 zoneType = b.readSint16LE();   // +0x06
-	int16 frameStart = b.readSint16LE(); // +0x08
-	int16 frameEnd = b.readSint16LE();   // +0x0A
-	int16 x1 = b.readSint16LE();         // +0x0C
-	int16 y1 = b.readSint16LE();         // +0x0E
-	int16 x2 = b.readSint16LE();         // +0x10
-	int16 y2 = b.readSint16LE();         // +0x12
-	int16 x3 = b.readSint16LE();         // +0x14
-	int16 y3 = b.readSint16LE();         // +0x16
-	int16 x4 = b.readSint16LE();         // +0x18
-	int16 y4 = b.readSint16LE();         // +0x1A
+	// FUN_4092D9 field mapping (byte offsets from stored pointer):
+	//   +0x00: opcode (5) — already consumed by SmushPlayer
+	//   +0x02: par2 (sub-opcode) — already consumed, passed as parameter
+	//   +0x04: par3 — already consumed by SmushPlayer
+	//   +0x06: par4 (userId) — filter value for < 1000 test, passed as parameter
+	//   +0x08: body[0] (sVar1) — control field 1 (frame check: field2-1 == field1)
+	//   +0x0A: body[1] (sVar2) — control field 2
+	//   +0x0C: body[2] — vertex 1 X
+	//   +0x0E: body[3] — vertex 1 Y
+	//   +0x10: body[4] — vertex 2 X
+	//   +0x12: body[5] — vertex 2 Y
+	//   +0x14: body[6] — vertex 3 X
+	//   +0x16: body[7] — vertex 3 Y
+	//   +0x18: body[8] — vertex 4 X
+	//   +0x1A: body[9] — vertex 4 Y
+
+	int16 field1 = b.readSint16LE();     // body[0] — control field 1
+	int16 field2 = b.readSint16LE();     // body[1] — control field 2
+	int16 x1 = b.readSint16LE();         // body[2] — vertex 1 X
+	int16 y1 = b.readSint16LE();         // body[3] — vertex 1 Y
+	int16 x2 = b.readSint16LE();         // body[4] — vertex 2 X
+	int16 y2 = b.readSint16LE();         // body[5] — vertex 2 Y
+	int16 x3 = b.readSint16LE();         // body[6] — vertex 3 X
+	int16 y3 = b.readSint16LE();         // body[7] — vertex 3 Y
+	int16 x4 = b.readSint16LE();         // body[8] — vertex 4 X
+	int16 y4 = b.readSint16LE();         // body[9] — vertex 4 Y
 
 	CollisionZone zone;
 	zone.x1 = x1;
@@ -3698,24 +3766,22 @@ void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 su
 	zone.y3 = y3;
 	zone.x4 = x4;
 	zone.y4 = y4;
-	zone.frameStart = frameStart;
-	zone.frameEnd = frameEnd;
-	zone.zoneType = zoneType;
+	zone.field1 = field1;
+	zone.field2 = field2;
+	zone.filterValue = par4;
 	zone.subOpcode = subOpcode;
 	zone.active = true;
 
 	// Register zone into appropriate table based on sub-opcode
 	if (subOpcode == 0x0D && _primaryZoneCount < kMaxCollisionZones) {
-		// Primary collision zones (obstacles)
 		_primaryZones[_primaryZoneCount++] = zone;
-		debug("Rebel2: Registered PRIMARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
-			_primaryZoneCount - 1, zoneType, frameStart, frameEnd,
+		debug("Rebel2: Registered PRIMARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+			_primaryZoneCount - 1, par4, field1, field2,
 			x1, y1, x2, y2, x3, y3, x4, y4);
 	} else if (subOpcode == 0x0E && _secondaryZoneCount < kMaxCollisionZones) {
-		// Secondary collision zones (boundaries)
 		_secondaryZones[_secondaryZoneCount++] = zone;
-		debug("Rebel2: Registered SECONDARY collision zone %d: type=%d frames=[%d-%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
-			_secondaryZoneCount - 1, zoneType, frameStart, frameEnd,
+		debug("Rebel2: Registered SECONDARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
+			_secondaryZoneCount - 1, par4, field1, field2,
 			x1, y1, x2, y2, x3, y3, x4, y4);
 	} else {
 		debug("Rebel2: WARNING - Could not register zone (subOpcode=%d, primary=%d, secondary=%d)",
@@ -3730,6 +3796,112 @@ void InsaneRebel2::resetCollisionZones() {
 	_secondaryZoneCount = 0;
 }
 
+void InsaneRebel2::checkCollisionZones() {
+	// Per-frame collision checking — FUN_4092D9 first loop (lines 39-202).
+	// Tests aim/ship position against primary collision zone quadrilaterals.
+	//
+	// Original coordinate system:
+	//   Zone vertices are in 424x260 buffer space, centered by subtracting (0xD4=212, 0x82=130).
+	//   Aim position (DAT_00443668/DAT_0044366a) is in centered coords [-52..52, -45..45].
+	//   In FUN_407FCB: DAT_00443668 is a smoothed mouse-derived position.
+	//
+	// For our implementation:
+	//   Map mouse position to centered coords matching the original range.
+	//   Mouse X 0..320 → centered X ≈ [-52..52] (with smoothing in original)
+	//   Mouse Y 0..200 → centered Y ≈ [-45..45]
+
+	if (_primaryZoneCount == 0) return;
+
+	// Calculate aim position in centered coordinates.
+	// Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
+	// Simplified mapping: mouse 0..320 → [-52..52], mouse 0..200 → [-45..45]
+	int16 aimX = (int16)((_vm->_mouse.x - 160) * 52 / 160);
+	int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
+
+	// Clamp to original ranges (DAT_0047a7fc < 1 path)
+	if (aimX > 0x34) aimX = 0x34;
+	if (aimX < -0x34) aimX = -0x34;
+	if (aimY > 0x2d) aimY = 0x2d;
+	if (aimY < -0x2d) aimY = -0x2d;
+
+	for (int i = 0; i < _primaryZoneCount; i++) {
+		CollisionZone &zone = _primaryZones[i];
+		if (!zone.active) continue;
+
+		// Filter: only process zones with filterValue < 1000 (par4 from IACT header)
+		// Original: *(short *)(*local_c + 6) < 1000
+		if (zone.filterValue >= 1000) continue;
+
+		// Frame check: field2 - 1 == field1
+		// Original: sVar2 + -1 == (int)sVar1
+		if (zone.field2 - 1 != zone.field1) continue;
+
+		// Center zone vertices by subtracting buffer center (0xD4=212, 0x82=130)
+		// Original: sVar4 = x1 - 0xD4, sVar8 = y1 - 0x82, etc.
+		int cx1 = zone.x1 - 0xD4;
+		int cy1 = zone.y1 - 0x82;
+		int cx2 = zone.x2 - 0xD4;
+		int cy2 = zone.y2 - 0x82;
+		int cx3 = zone.x3 - 0xD4;
+		int cy3 = zone.y3 - 0x82;
+		int cx4 = zone.x4 - 0xD4;
+		int cy4 = zone.y4 - 0x82;
+
+		// Point-in-quadrilateral test — FUN_4092D9 lines 119-128
+		// Tests if aim position is OUTSIDE the safe corridor (= collision with obstacle).
+		// Original uses 4 edge interpolation tests connected by OR (any failure = collision).
+		//
+		// Edge 1: interpolate Y along top edge (v1→v2) at aim X position
+		//   if aimY < interpolated Y → outside top edge → collision
+		// Edge 2: interpolate Y along bottom edge (v4→v3) at aim X position
+		//   if interpolated Y < aimY → outside bottom edge → collision
+		// Edge 3: interpolate X along left edge (v1→v4) at aim Y position
+		//   if aimX < interpolated X → outside left edge → collision
+		// Edge 4: interpolate X along right edge (v2→v3) at aim Y position
+		//   if interpolated X < aimX → outside right edge → collision
+		bool collision = false;
+
+		// Avoid division by zero for degenerate edges
+		if (cx2 != cx1) {
+			int interpY1 = ((aimX - cx1) * (cy2 - cy1)) / (cx2 - cx1) + cy1;
+			if (aimY < interpY1) collision = true;
+		}
+		if (!collision && cx3 != cx4) {
+			int interpY2 = ((aimX - cx4) * (cy3 - cy4)) / (cx3 - cx4) + cy4;
+			if (interpY2 < aimY) collision = true;
+		}
+		if (!collision && cy4 != cy1) {
+			int interpX1 = ((aimY - cy1) * (cx4 - cx1)) / (cy4 - cy1) + cx1;
+			if (aimX < interpX1) collision = true;
+		}
+		if (!collision && cy3 != cy2) {
+			int interpX2 = ((aimY - cy2) * (cx3 - cx2)) / (cy3 - cy2) + cx2;
+			if (interpX2 < aimX) collision = true;
+		}
+
+		if (collision) {
+			// Collision detected — apply damage from collision damage table
+			// Original: DAT_0047a7ec += DAT_0047e0f6[levelIdx]
+			// TODO: Read from per-level collision damage table DAT_0047e0f6
+			int collisionDamage = 3 + (_difficulty * 2);
+
+			if (!_rebelInvulnerable) {
+				_playerDamage += collisionDamage;
+				if (_playerDamage > 255) _playerDamage = 255;
+				debug("Rebel2: COLLISION damage! zone=%d aim=(%d,%d) damage=%d total=%d",
+					i, aimX, aimY, collisionDamage, _playerDamage);
+			}
+			// Visual effect — FUN_00420515 (palette flash)
+			initDamageFlash();
+			// TODO: FUN_0041189e sound based on collision direction
+		} else {
+			// Safely passed — award score bonus
+			// Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
+			addScore(1);
+		}
+	}
+}
+
 void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
                             int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
 	// Draw a quadrilateral by connecting its 4 vertices with lines
@@ -4119,6 +4291,12 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		updateDamageFlashPalette();
 	}
 
+	// Per-frame collision checking against registered zones (FUN_4092D9 first loop)
+	// Active for Handler 0x26 (turret) and Handler 7 (third-person ship)
+	if (_rebelHandler == 0x26 || _rebelHandler == 7) {
+		checkCollisionZones();
+	}
+
 	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
 	if (_rebelHandler == 7 || _rebelHandler == 8) {
 		drawCollisionZones(renderBitmap, pitch, width, height, 0);
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index e526234ac13..10282a8de88 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -533,14 +533,14 @@ public:
 	//   +0x0C-0x1A: X1,Y1,X2,Y2,X3,Y3,X4,Y4 vertex coordinates
 
 	struct CollisionZone {
-		int16 x1, y1;  // Top-left
-		int16 x2, y2;  // Top-right
-		int16 x3, y3;  // Bottom-right
-		int16 x4, y4;  // Bottom-left
-		int16 frameStart;
-		int16 frameEnd;
-		int16 zoneType;
-		int16 subOpcode;  // 0x0D = primary, 0x0E = secondary
+		int16 x1, y1;  // Vertex 1 (body[2], body[3])
+		int16 x2, y2;  // Vertex 2 (body[4], body[5])
+		int16 x3, y3;  // Vertex 3 (body[6], body[7])
+		int16 x4, y4;  // Vertex 4 (body[8], body[9])
+		int16 field1;   // body[0] - control field (frame check: field2 - 1 == field1)
+		int16 field2;   // body[1] - control field
+		int16 filterValue; // par4 from IACT header - used for < 1000 filter
+		int16 subOpcode;   // 0x0D = primary, 0x0E = secondary
 		bool active;
 	};
 
@@ -569,11 +569,16 @@ public:
 	              int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color);
 
 	// Register a collision zone from IACT opcode 5 data
-	void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode);
+	void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4);
 
 	// Reset collision zone counters (called at end of frame)
 	void resetCollisionZones();
 
+	// Per-frame collision checking against registered zones (FUN_4092D9 first loop)
+	// Tests aim/ship position against primary zone quadrilaterals
+	// Applies collision damage from DAT_0047e0f6 when inside obstacle zone
+	void checkCollisionZones();
+
 	int16 _playerDamage;  // Legacy damage counter (kept for compatibility/telemetry)
 	int16 _playerShield;  // Shields: 0..255 where 255 = full
 	int16 _playerLives;


Commit: ca8874e261869f35a37126bf65b22eff5204a61e
    https://github.com/scummvm/scummvm/commit/ca8874e261869f35a37126bf65b22eff5204a61e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:05+02:00

Commit Message:
SCUMM: RA2: Handle damage in level 3

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index b4b17c1881a..661df671547 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -260,10 +260,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_primaryZones[i].active = false;
 		_secondaryZones[i].active = false;
 	}
+	// Corridor boundaries in game coordinate space (FUN_40C040 lines 21-24)
+	// DAT_00443b0a=0, DAT_00443b0c=0, DAT_00443b0e=0x1a8(424), DAT_00443b10=0x104(260)
 	_corridorLeftX = 0;
 	_corridorTopY = 0;
-	_corridorRightX = 320;
-	_corridorBottomY = 200;
+	_corridorRightX = 0x1A8;   // 424 — full game buffer width
+	_corridorBottomY = 0x104;  // 260 — full game buffer height
 	_hitCooldown = 0;
 
 	// Initialize legacy shot system (backwards compatibility)
@@ -1071,41 +1073,61 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		}
 
 	} else if (par1 == 7) {
-		// Opcode 7: Sprite/HUD control for Handler 7 (third-person ship levels like Level 3)
-		// par2 = control type (41 = sprite selection?)
-		// par3 = usually 0
-		// par4 = sprite/slot ID (0 or 5 seen in Level 3)
+		// Opcode 7: Handler 7 corridor/velocity control (FUN_40C3CC case 5)
+		// IACT header: par1=7, par2=flags, par3=0, par4=sub-opcode
+		// Body contains 2 int16 values (body[0], body[1])
 		//
-		// This opcode may control which ship direction sprite to display
-		// or reference embedded graphics loaded elsewhere
-		debug("Rebel2 IACT Opcode 7: par2=%d par3=%d par4=%d handler=%d",
-			par2, par3, par4, _rebelHandler);
-
-		// Read remaining IACT data to understand structure
-		int64 startPos = b.pos();
-		int64 remaining = b.size() - startPos;
-		if (remaining > 0 && remaining <= 64) {
-			byte payload[64];
-			int bytesRead = b.read(payload, MIN((int64)64, remaining));
-			debug("Rebel2 Opcode 7: payload (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X",
-				bytesRead,
-				bytesRead > 0 ? payload[0] : 0, bytesRead > 1 ? payload[1] : 0,
-				bytesRead > 2 ? payload[2] : 0, bytesRead > 3 ? payload[3] : 0,
-				bytesRead > 4 ? payload[4] : 0, bytesRead > 5 ? payload[5] : 0,
-				bytesRead > 6 ? payload[6] : 0, bytesRead > 7 ? payload[7] : 0);
-			b.seek(startPos);
-		}
-
-		// par2 == 41 (0x29) seems to be a common value
-		// This might be a "show sprite" command referencing par4 as the slot
-		if (par2 == 41) {
-			// par4 could be a HUD slot or sprite index
-			// For Handler 7, set which embedded HUD frame to display
-			if (_rebelHandler == 7 && par4 >= 0 && par4 < 16) {
-				// Mark this slot as the active one for direction-based rendering
-				// This will be used in post-rendering to know which frame to show
-				debug("Rebel2 Opcode 7: Activating HUD slot %d for Handler 7", par4);
+		// par4 sub-opcodes (from FUN_40C3CC case 5 switch on param_5[3]):
+		//   0: Set velocity params (DAT_00443b12, DAT_00443b14)
+		//   1: Set left X + top Y corridor boundaries (DAT_00443b0a, DAT_00443b0c)
+		//   2: Set right X + bottom Y corridor boundaries (DAT_00443b0e, DAT_00443b10)
+		//   5: Set flag (DAT_00443b52)
+
+		int16 body0 = 0, body1 = 0;
+		if (b.size() - b.pos() >= 4) {
+			body0 = b.readSint16LE();
+			body1 = b.readSint16LE();
+		}
+
+		switch (par4) {
+		case 0:
+			// Velocity/wind data — affects ship drift in FUN_40C3CC physics
+			// DAT_00443b12 = horizontal wind, DAT_00443b14 = vertical wind
+			debug("Rebel2 Opcode 7 par4=0: velocity=(%d,%d)", body0, body1);
+			break;
+		case 1:
+			// Set LEFT X boundary and TOP Y boundary
+			_corridorLeftX = body0;
+			_corridorTopY = body1;
+			// Mode-dependent margin adjustment (FUN_40C3CC lines 341-351)
+			if (_flyControlMode == 2) {
+				_corridorLeftX += 15;
+			} else if (_flyControlMode == 0) {
+				_corridorLeftX += 20;
+			}
+			debug("Rebel2 Opcode 7 par4=1: corridor left=%d top=%d (adjusted left=%d)",
+				body0, body1, _corridorLeftX);
+			break;
+		case 2:
+			// Set RIGHT X boundary and BOTTOM Y boundary
+			_corridorRightX = body0;
+			_corridorBottomY = body1;
+			// Mode-dependent margin adjustment (FUN_40C3CC lines 356-365)
+			if (_flyControlMode == 2) {
+				_corridorRightX -= 15;
+			} else if (_flyControlMode == 0) {
+				_corridorRightX -= 20;
 			}
+			debug("Rebel2 Opcode 7 par4=2: corridor right=%d bottom=%d (adjusted right=%d)",
+				body0, body1, _corridorRightX);
+			break;
+		case 5:
+			// Flag value
+			debug("Rebel2 Opcode 7 par4=5: flag=%d", body0);
+			break;
+		default:
+			debug("Rebel2 Opcode 7 par4=%d: body=(%d,%d) — unknown sub-opcode", par4, body0, body1);
+			break;
 		}
 
 	} else if (par1 == 6) {
@@ -1497,24 +1519,38 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	// Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
 	// Used for Level 3 and similar space combat levels
 	if (_rebelHandler == 7) {
-		// Set control mode (DAT_004437c0 = par3 in FUN_40C3CC case 4)
-		// This determines shooting capability:
-		//   Mode 0: Flight/avoid mode - no shooting
-		//   Mode 1: Alternate flight mode - no shooting
-		//   Mode 2: Combat mode - shooting ENABLED
-		_flyControlMode = par3;
+		// Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
+		// This determines collision mode and shooting capability:
+		//   Mode 0: Obstacle avoidance — SECONDARY zones, corridor boundaries
+		//   Mode 1: Tunnel flight — PRIMARY zones, per-edge push-back (hMargin=0x28)
+		//   Mode 2: Combat mode — shooting ENABLED, SECONDARY zones
+		//   Mode 3: Tunnel flight — PRIMARY zones, per-edge push-back (hMargin=0x0f)
+		_flyControlMode = par4;
 		debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
-			par3, (par3 == 2) ? "ENABLED" : "DISABLED");
-
-		// If par4 == 1, enable status bar
-		if (par4 == 1) {
+			par4, (par4 == 2) ? "ENABLED" : "DISABLED");
+
+		// Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
+		// In our parsing, par3 maps to param_5[2] and the body follows par4.
+		// FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
+		// par3 is param_5[2], which the original doesn't use here.
+		// The body word for status bar is read separately below.
+		int16 bodyStatusFlag = 0;
+		if (b.size() - b.pos() >= 2) {
+			bodyStatusFlag = b.readSint16LE();
+		}
+		if (bodyStatusFlag == 1) {
 			_rebelStatusBarSprite = 5;  // Status bar sprite
-			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled");
+			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
 		}
 
-		// Update ship screen position from mouse
-		// Handler 7 uses DAT_0044370c (Y) and DAT_0044370e (X) for screen position
-		// Get raw mouse position
+		// Update ship position from mouse input.
+		// CRITICAL: Ship position is in game coordinate space [20,404]x[20,240],
+		// centered at (212,130) — matching DAT_00443708/DAT_0044370a in the original.
+		// Collision zones are in this same space. Mouse coords must be converted.
+		//
+		// FUN_40C3CC case 4: original uses smoothed mouse velocity → position delta (±12/frame).
+		// Simplified here: direct mouse position mapping with smooth interpolation.
+
 		int16 rawMouseX = _vm->_mouse.x;
 		int16 rawMouseY = _vm->_mouse.y;
 
@@ -1528,66 +1564,97 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			mouseY = (rawMouseY * 200) / _player->_height;
 		}
 
-		// Clamp to screen bounds (matching FUN_0040c3cc bounds)
-		if (mouseX < 0) mouseX = 0;
-		if (mouseX > 319) mouseX = 319;
-		if (mouseY < 0) mouseY = 0;
-		if (mouseY > 199) mouseY = 199;
-
-		// Update ship position with smooth interpolation
-		// FUN_0040c3cc uses complex smoothing, we use simpler immediate response
-		const int16 maxStep = 15;
-		if (_shipPosX < mouseX) {
-			_shipPosX = MIN((int16)(_shipPosX + maxStep), mouseX);
-		} else if (_shipPosX > mouseX) {
-			_shipPosX = MAX((int16)(_shipPosX - maxStep), mouseX);
-		}
-		if (_shipPosY < mouseY) {
-			_shipPosY = MIN((int16)(_shipPosY + maxStep), mouseY);
-		} else if (_shipPosY > mouseY) {
-			_shipPosY = MAX((int16)(_shipPosY - maxStep), mouseY);
-		}
-
-		// Update Handler 7 screen position (DAT_0044370c/e)
-		// These track the actual on-screen position for direction calculation
-		_flyShipScreenX = _shipPosX;
-		_flyShipScreenY = _shipPosY;
-
-		// Calculate ship direction from position (FUN_0040d836 lines 88-106)
-		// Formula from assembly:
-		//   hDir = (0xa0 - posX) >> 6  (with signed rounding)
-		//   vDir = (0x95 - posY) / 0x2b
-		//   dirIndex = hDir * 7 + vDir
-		//
-		// Note: The assembly formula gives:
-		//   hDir: 0-4 where 2 is center (0xa0=160, range is -96 to +96, >> 6 gives -1 to 1, but clamped to 0-4)
-		//   vDir: 0-6 where 3 is center (0x95=149, 0x2b=43, so 149/43 ≈ 3.5)
-		//
-		// Simplified direction calculation based on mouse position relative to center
+		// Convert mouse coords [0,319]x[0,199] → game coords [20,404]x[20,240]
+		// Game center (212,130) maps to screen center (160,100)
+		int16 targetGameX = (int16)(mouseX * 384 / 319 + 20);
+		int16 targetGameY = (int16)(mouseY * 220 / 199 + 20);
+
+		// Clamp to game coordinate bounds (FUN_40C3CC lines 245-256)
+		if (targetGameX < 0x14) targetGameX = 0x14;   // 20
+		if (targetGameX > 0x194) targetGameX = 0x194;  // 404
+		if (targetGameY < 0x14) targetGameY = 0x14;    // 20
+		if (targetGameY > 0xF0) targetGameY = 0xF0;    // 240
+
+		// Smooth interpolation (original uses ±12 per frame max delta)
+		const int16 maxStep = 12;
+		if (_flyShipScreenX < targetGameX) {
+			_flyShipScreenX = MIN((int16)(_flyShipScreenX + maxStep), targetGameX);
+		} else if (_flyShipScreenX > targetGameX) {
+			_flyShipScreenX = MAX((int16)(_flyShipScreenX - maxStep), targetGameX);
+		}
+		if (_flyShipScreenY < targetGameY) {
+			_flyShipScreenY = MIN((int16)(_flyShipScreenY + maxStep), targetGameY);
+		} else if (_flyShipScreenY > targetGameY) {
+			_flyShipScreenY = MAX((int16)(_flyShipScreenY - maxStep), targetGameY);
+		}
+
+		// Corridor boundary collision (FUN_40C3CC lines 257-284)
+		// Mode 0/2: Ship X is clamped to corridor boundaries with damage
+		if (_flyControlMode == 0 || _flyControlMode == 2) {
+			// Right boundary collision
+			if (_corridorRightX < _flyShipScreenX) {
+				_flyShipScreenX = _corridorRightX;
+				if (_hitCooldown < 5 && !_rebelInvulnerable) {
+					int damage = 3 + (_difficulty * 2);
+					_playerDamage += damage;
+					if (_playerDamage > 255) _playerDamage = 255;
+					_rebelHitCounter++;
+					_hitCooldown = 10;
+					_spaceShotDirection = 1;
+					initDamageFlash();
+					debug("Rebel2: Handler7 RIGHT CORRIDOR HIT ship=(%d,%d) boundary=%d",
+						_flyShipScreenX, _flyShipScreenY, _corridorRightX);
+				}
+			}
+			// Left boundary collision
+			if (_flyShipScreenX < _corridorLeftX) {
+				_flyShipScreenX = _corridorLeftX;
+				if (_hitCooldown < 5 && !_rebelInvulnerable) {
+					int damage = 3 + (_difficulty * 2);
+					_playerDamage += damage;
+					if (_playerDamage > 255) _playerDamage = 255;
+					_rebelHitCounter++;
+					_hitCooldown = 10;
+					_spaceShotDirection = 0;
+					initDamageFlash();
+					debug("Rebel2: Handler7 LEFT CORRIDOR HIT ship=(%d,%d) boundary=%d",
+						_flyShipScreenX, _flyShipScreenY, _corridorLeftX);
+				}
+			}
+			// Y boundary clamping (no damage, just clamp) — lines 285-292
+			if (_corridorBottomY < _flyShipScreenY) {
+				_flyShipScreenY = _corridorBottomY;
+			}
+			if (_flyShipScreenY < _corridorTopY) {
+				_flyShipScreenY = _corridorTopY;
+			}
+		}
 
-		// Horizontal direction (0-4, center=2)
-		// Formula: (160 - posX) >> 6, clamped to 0-4
-		int16 hDiff = 160 - _flyShipScreenX;
-		int16 hDir = (hDiff + 64) >> 6;  // Add 64 to shift range, divide by 64
+		// Also update _shipPosX/Y for other systems that use screen coords
+		_shipPosX = (int16)((_flyShipScreenX - 20) * 319 / 384);
+		_shipPosY = (int16)((_flyShipScreenY - 20) * 199 / 220);
+
+		// Direction calculation from ship game position
+		// Original FUN_0040d836 uses DAT_0044370c (smoothed velocity) for direction.
+		// Simplified: derive direction from position offset relative to center (212, 130).
+		// hDir: 0-4 where 2=center, based on offset from center X (212)
+		// vDir: 0-6 where 3=center, based on offset from center Y (130)
+		int16 hDiff = 0xD4 - _flyShipScreenX;  // 212 - shipX
+		int16 hDir = (hDiff + 64) >> 6;
 		if (hDir < 0) hDir = 0;
 		if (hDir > 4) hDir = 4;
 
-		// Vertical direction (0-6, center=3)
-		// Formula: (149 - posY) / 43, clamped to 0-6
-		int16 vDir = (149 - _flyShipScreenY) / 43;
+		int16 vDir = (130 - _flyShipScreenY) / 37;  // adjusted divisor for game coord range
 		if (vDir < 0) vDir = 0;
 		if (vDir > 6) vDir = 6;
 
-		// Additional adjustment from assembly (lines 90-105):
-		// If vDir==3 and abs(posY) > 10, adjust by +/-1
-		// If hDir==2 and abs(posX) > 15, adjust by +/-1
-		// This creates a "deadzone" at center to reduce flicker
-		if (vDir == 3 && ABS(_flyShipScreenY - 100) > 10) {
-			if (_flyShipScreenY < 100) vDir = 2;
+		// Deadzone at center to reduce flicker
+		if (vDir == 3 && ABS(_flyShipScreenY - 130) > 13) {
+			if (_flyShipScreenY < 130) vDir = 2;
 			else vDir = 4;
 		}
-		if (hDir == 2 && ABS(_flyShipScreenX - 160) > 15) {
-			if (_flyShipScreenX < 160) hDir = 3;
+		if (hDir == 2 && ABS(_flyShipScreenX - 212) > 20) {
+			if (_flyShipScreenX < 212) hDir = 3;
 			else hDir = 1;
 		}
 
@@ -3902,6 +3969,184 @@ void InsaneRebel2::checkCollisionZones() {
 	}
 }
 
+void InsaneRebel2::checkHandler7CollisionZones() {
+	// FUN_40E35E — Handler 7 per-frame collision system.
+	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
+	// Two modes depending on _flyControlMode:
+	//   Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
+	//   Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
+
+	// Decrement hit cooldown per frame
+	if (_hitCooldown > 0)
+		_hitCooldown--;
+
+	if (_flyControlMode == 0 || _flyControlMode == 2) {
+		// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
+		// Original lines 52-132: Point-in-quad test with 15px inward margin.
+		// Inside the quad = collision with obstacle.
+		const int margin = 15;  // local_14 = 0x0f, local_20 = 0x0f
+
+		for (int i = 0; i < _secondaryZoneCount; i++) {
+			CollisionZone &zone = _secondaryZones[i];
+			if (!zone.active) continue;
+
+			int x1 = zone.x1, y1 = zone.y1;
+			int x2 = zone.x2, y2 = zone.y2;
+			int x3 = zone.x3, y3 = zone.y3;
+			int x4 = zone.x4, y4 = zone.y4;
+
+			// Point-in-quad test (lines 75-89)
+			// Start assuming inside, clear if outside any edge (with margin)
+			bool inside = true;
+
+			// Top edge: interpolate Y along v1→v2 at shipX, +15 margin
+			if (x2 != x1) {
+				int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
+				if (_flyShipScreenY < interpY) inside = false;
+			}
+			// Bottom edge: interpolate Y along v4→v3 at shipX, -15 margin
+			if (inside && x3 != x4) {
+				int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
+				if (interpY < _flyShipScreenY) inside = false;
+			}
+			// Left edge: interpolate X along v1→v4 at shipY, +15 margin
+			if (inside && y4 != y1) {
+				int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
+				if (_flyShipScreenX < interpX) inside = false;
+			}
+			// Right edge: interpolate X along v2→v3 at shipY, -15 margin
+			if (inside && y3 != y2) {
+				int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
+				if (interpX < _flyShipScreenX) inside = false;
+			}
+
+			// Frame match: field2 - 1 == field1 (line 90)
+			if (zone.field2 - 1 == zone.field1) {
+				if (inside) {
+					// Collision with obstacle — apply damage and break
+					_hitCooldown = 10;
+					_spaceShotDirection = zone.filterValue + 2;
+
+					int collisionDamage = 3 + (_difficulty * 2);
+					if (!_rebelInvulnerable) {
+						_playerDamage += collisionDamage;
+						if (_playerDamage > 255) _playerDamage = 255;
+					}
+					_rebelHitCounter++;
+					initDamageFlash();
+					debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
+						i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
+					break;  // Only one collision per frame (original breaks)
+				} else {
+					// Safely avoided obstacle — award score
+					addScore(1);
+				}
+			}
+		}
+
+		// Corridor boundary proximity (lines 127-131)
+		// These flags are used for directional indicators (not critical for damage)
+
+	} else {
+		// ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
+		// Original lines 133-235: Per-edge interpolation with push-back.
+		// Ship position is clamped to wall boundaries when hitting.
+		int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f;  // local_14
+		const int16 vMargin = 0x0f;  // local_20
+
+		for (int i = 0; i < _primaryZoneCount; i++) {
+			CollisionZone &zone = _primaryZones[i];
+			if (!zone.active) continue;
+
+			int x1 = zone.x1, y1 = zone.y1;
+			int x2 = zone.x2, y2 = zone.y2;
+			int x3 = zone.x3, y3 = zone.y3;
+			int x4 = zone.x4, y4 = zone.y4;
+
+			// Top edge: interpolate Y along v1→v2 at shipX (lines 152-166)
+			if (x2 != x1) {
+				int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
+				if (_flyShipScreenY < edgeY) {
+					// Ship above top wall — push down
+					if (_hitCooldown < 5 && !_rebelInvulnerable) {
+						int damage = 3 + (_difficulty * 2);
+						_playerDamage += damage;
+						if (_playerDamage > 255) _playerDamage = 255;
+						_rebelHitCounter++;
+						_hitCooldown = 10;
+						debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
+							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
+					}
+					_spaceShotDirection = 2;  // Direction: pushed down
+					_flyShipScreenY = edgeY;  // Push-back
+					initDamageFlash();
+				}
+			}
+
+			// Bottom edge: interpolate Y along v4→v3 at shipX (lines 167-183)
+			if (x3 != x4) {
+				int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
+				_corridorBottomY = vMargin + edgeY;  // DAT_00443b10 update
+				if (edgeY < _flyShipScreenY) {
+					// Ship below bottom wall — push up
+					if (_hitCooldown < 5 && !_rebelInvulnerable) {
+						int damage = 3 + (_difficulty * 2);
+						_playerDamage += damage;
+						if (_playerDamage > 255) _playerDamage = 255;
+						_rebelHitCounter++;
+						_hitCooldown = 10;
+						debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
+							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
+					}
+					_spaceShotDirection = 3;  // Direction: pushed up
+					_flyShipScreenY = edgeY;  // Push-back
+					initDamageFlash();
+				}
+			}
+
+			// Left edge: interpolate X along v1→v4 at shipY (lines 184-199)
+			if (y4 != y1) {
+				int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
+				if (_flyShipScreenX < edgeX) {
+					// Ship left of left wall — push right
+					_flyShipScreenX = edgeX;  // Push-back
+					if (_hitCooldown < 5 && !_rebelInvulnerable) {
+						int damage = 3 + (_difficulty * 2);
+						_playerDamage += damage;
+						if (_playerDamage > 255) _playerDamage = 255;
+						_rebelHitCounter++;
+						_hitCooldown = 10;
+						debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
+							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
+					}
+					_spaceShotDirection = 0;  // Direction: pushed right
+					initDamageFlash();
+				}
+			}
+
+			// Right edge: interpolate X along v2→v3 at shipY (lines 200-215)
+			if (y3 != y2) {
+				int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
+				if (edgeX < _flyShipScreenX) {
+					// Ship right of right wall — push left
+					_flyShipScreenX = edgeX;  // Push-back
+					if (_hitCooldown < 5 && !_rebelInvulnerable) {
+						int damage = 3 + (_difficulty * 2);
+						_playerDamage += damage;
+						if (_playerDamage > 255) _playerDamage = 255;
+						_rebelHitCounter++;
+						_hitCooldown = 10;
+						debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
+							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
+					}
+					_spaceShotDirection = 1;  // Direction: pushed left
+					initDamageFlash();
+				}
+			}
+		}
+	}
+}
+
 void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
                             int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
 	// Draw a quadrilateral by connecting its 4 vertices with lines
@@ -3956,7 +4201,7 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
 	}
 
 	// Draw corridor boundaries as a rectangle (from IACT opcode 7)
-	if (_corridorLeftX != 0 || _corridorRightX != 320) {
+	if (_corridorLeftX != 0 || _corridorRightX != 0x1A8) {
 		const byte corridorColor = 45;  // Cyan for corridor boundaries
 		// Draw vertical lines for left/right boundaries
 		drawLine(dst, pitch, width, height,
@@ -4279,22 +4524,31 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	//   Handler 8:    FUN_401CCF line 119 → FUN_00420754 (palette flash + screen shake)
 	//   Handler 0x19: FUN_41DB5E line 192 → FUN_00420562 (palette flash only, every frame)
 	//   Handler 0x26: FUN_4092D9 lines 135/225/237 → FUN_00420515 trigger + palette flash
-	//   Handler 7:    No damage effects
+	//   Handler 7:    FUN_40E35E → FUN_00420515 trigger + palette flash
 	if (_rebelHandler == 8) {
 		// Full damage effect: palette flash + screen shake
 		// Suppressed during autopilot (mode 4) and cutscene (mode 5)
 		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
 			updateDamageEffect(renderBitmap, pitch, width, height);
 		}
-	} else if (_rebelHandler == 0x19 || _rebelHandler == 0x26) {
-		// Palette flash only — no screen shake for turret/FPS handlers
+	} else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
+		// Palette flash only — no screen shake for turret/FPS/ship handlers
 		updateDamageFlashPalette();
 	}
 
-	// Per-frame collision checking against registered zones (FUN_4092D9 first loop)
-	// Active for Handler 0x26 (turret) and Handler 7 (third-person ship)
-	if (_rebelHandler == 0x26 || _rebelHandler == 7) {
+	// Per-frame collision checking against registered zones.
+	//
+	// Handler 0x26 (turret): FUN_4092D9 — aim position vs primary zones (centered coords)
+	//   Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
+	//
+	// Handler 7 (ship): FUN_40E35E — ship position vs zones per control mode:
+	//   Mode 0/2: SECONDARY zones (0x0E) — obstacle collision (inside quad = hit)
+	//   Mode 1/3: PRIMARY zones (0x0D) — wall/boundary per-edge with push-back
+	//   Uses ship position in raw buffer coords, hit cooldown, directional damage.
+	if (_rebelHandler == 0x26) {
 		checkCollisionZones();
+	} else if (_rebelHandler == 7) {
+		checkHandler7CollisionZones();
 	}
 
 	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
@@ -4726,10 +4980,11 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 		return;
 
 	// Base position at screen center with direction offset
+	// Ship position is in game coords (center 212,130), convert to screen offset
 	int baseX = 160;
 	int baseY = 105;
-	int16 posOffsetX = (_flyShipScreenX - 160) / 10;
-	int16 posOffsetY = (_flyShipScreenY - 100) / 10;
+	int16 posOffsetX = (_flyShipScreenX - 0xD4) / 13;  // (shipX - 212) / 13
+	int16 posOffsetY = (_flyShipScreenY - 0x82) / 11;   // (shipY - 130) / 11
 	int shipScreenX = baseX + posOffsetX;
 	int shipScreenY = baseY + posOffsetY;
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 10282a8de88..a1342cad786 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -579,6 +579,12 @@ public:
 	// Applies collision damage from DAT_0047e0f6 when inside obstacle zone
 	void checkCollisionZones();
 
+	// Handler 7 collision system (FUN_40E35E)
+	// Mode 0/2: Obstacle collision using secondary zones — inside quad = hit
+	// Mode 1/3: Wall/boundary collision using primary zones — per-edge push-back
+	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords
+	void checkHandler7CollisionZones();
+
 	int16 _playerDamage;  // Legacy damage counter (kept for compatibility/telemetry)
 	int16 _playerShield;  // Shields: 0..255 where 255 = full
 	int16 _playerLives;


Commit: 7d6172651310b73b59bb577afbc63a0564b24471
    https://github.com/scummvm/scummvm/commit/7d6172651310b73b59bb577afbc63a0564b24471
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:05+02:00

Commit Message:
SCUMM: RA2: Improve ship control for level 3

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 661df671547..61aa13ac4d0 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -339,6 +339,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_flyHiResSprite = nullptr;   // FLY004 - high-res alternative
 	_flyShipScreenX = 0xd4;      // Start at center (212) - matches DAT_00443708 default
 	_flyShipScreenY = 0x82;      // Start at center (130) - matches DAT_0044370a default
+	_smoothedVelocity = 0;       // DAT_0044370c
+	_verticalInput = 0;          // DAT_0044370e
+	memset(_velocityHistory, 0, sizeof(_velocityHistory));  // DAT_00443716
+	memset(_windHistoryX, 0, sizeof(_windHistoryX));         // DAT_00443b16
+	memset(_windHistoryY, 0, sizeof(_windHistoryY));         // DAT_00443b34
+	_windParamX = 0;             // DAT_00443b12
+	_windParamY = 0;             // DAT_00443b14
+	_perspectiveX = 0;           // DAT_00443712
+	_perspectiveY = 0;           // DAT_00443714
+	_viewShift = 0;              // DAT_00443710
+	_facingRight = false;        // DAT_0047ab8c
 
 	// Initialize Handler 25 GRD ship system
 	_grd001Sprite = nullptr;     // DAT_00482240 - GRD001 primary ship
@@ -1093,7 +1104,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		case 0:
 			// Velocity/wind data — affects ship drift in FUN_40C3CC physics
 			// DAT_00443b12 = horizontal wind, DAT_00443b14 = vertical wind
-			debug("Rebel2 Opcode 7 par4=0: velocity=(%d,%d)", body0, body1);
+			_windParamX = body0;
+			_windParamY = body1;
+			debug("Rebel2 Opcode 7 par4=0: wind=(%d,%d)", body0, body1);
 			break;
 		case 1:
 			// Set LEFT X boundary and TOP Y boundary
@@ -1543,85 +1556,165 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
 		}
 
-		// Update ship position from mouse input.
-		// CRITICAL: Ship position is in game coordinate space [20,404]x[20,240],
-		// centered at (212,130) — matching DAT_00443708/DAT_0044370a in the original.
-		// Collision zones are in this same space. Mouse coords must be converted.
+		// ============================================================
+		// Ship position update — FUN_40C3CC case 4, lines 49-327
+		// ============================================================
+		// Velocity-based physics with momentum/inertia:
+		//   Mouse offset from center → scaled input [-127,127]
+		//   → velocity history averaging → physics delta (clamped ±12/frame)
+		//   → position clamping → corridor collision → perspective offsets
 		//
-		// FUN_40C3CC case 4: original uses smoothed mouse velocity → position delta (±12/frame).
-		// Simplified here: direct mouse position mapping with smooth interpolation.
+		// Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
+		//   offset 0: smoothing param (>>4 +1 = window size)
+		//   offset 2: Y speed          offset 4: X speed (levelSpeed)
+		//   offset 6: wind multiplier  offset 14: corridor damage
+		// We don't have the actual level data, so we use calibrated defaults.
+
+		// --- Step 1: Mouse input as offset from screen center ---
+		// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
+		// _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
+		// consistent with handler 8 which uses _vm->_mouse.x directly.
+		int16 inputX = (int16)(_vm->_mouse.x - 160);  // DAT_0047a7e0
+		int16 inputY = (int16)(_vm->_mouse.y - 100);  // DAT_0047a7e2
+
+		// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
+		if (inputX > 160) inputX = 160;
+		if (inputX < -160) inputX = -160;
+		if (inputY > 127) inputY = 127;
+		if (inputY < -127) inputY = -127;
+
+		// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
+		// Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
+		int16 local_c = (int16)((inputX * 127) / 160);
+		int16 local_14 = inputY;  // Y already in [-127, 127]
+
+		// --- Step 3: Velocity history + smoothed average (lines 141-157) ---
+		for (int i = 24; i > 0; i--) {
+			_velocityHistory[i] = _velocityHistory[i - 1];
+		}
+		_velocityHistory[0] = local_c;
+
+		// Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
+		const int smoothWindow = 5;
+		int velSum = 0;
+		for (int i = 0; i < smoothWindow; i++) {
+			velSum += _velocityHistory[i];
+		}
+		_smoothedVelocity = (int16)(velSum / smoothWindow);  // DAT_0044370c
+
+		// --- Step 4: Wind history (lines 158-173) ---
+		// Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
+		const int16 windMult = 0;
+		int windSumX = 0, windSumY = 0;
+		for (int i = 14; i > 0; i--) {
+			_windHistoryX[i] = _windHistoryX[i - 1];
+			windSumX += _windHistoryX[i];
+		}
+		_windHistoryX[0] = _windParamX;
+		int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
+
+		for (int i = 14; i > 0; i--) {
+			_windHistoryY[i] = _windHistoryY[i - 1];
+			windSumY += _windHistoryY[i];
+		}
+		_windHistoryY[0] = _windParamY;
+		int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
+
+		// --- Step 5: Position delta (lines 174-242) ---
+		// levelSpeed (offset 4): calibrated so max velocity (127) → delta 12.
+		//   12 = (speed * 127) >> 9 → speed ≈ 48
+		// levelYSpeed (offset 2): calibrated so max input (127) → delta ~8.
+		//   8 = (speed * 127) >> 10 → speed ≈ 64
+		const int16 levelSpeed = 48;
+		const int16 levelYSpeed = 64;
+		int16 absSmoothVel = ABS(_smoothedVelocity);
+		int16 positionDeltaX;
+
+		if (_flyControlMode == 1) {
+			// Mode 1: Full cross-axis coupling (lines 174-186)
+			// Banking: vertical input deflects horizontal movement
+			if (local_c < 1) {
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * local_14 - windEffectX) >> 9);
+			} else {
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * local_14 - windEffectX) >> 9);
+			}
+		} else {
+			// Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
+			if (local_c < 1) {
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+			} else {
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+			}
+		}
 
-		int16 rawMouseX = _vm->_mouse.x;
-		int16 rawMouseY = _vm->_mouse.y;
+		// Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
+		if (positionDeltaX < -11) positionDeltaX = -12;
+		if (positionDeltaX > 11) positionDeltaX = 12;
 
-		// Scale mouse to 320x200 logical space if video is larger
-		int16 mouseX = rawMouseX;
-		int16 mouseY = rawMouseY;
-		if (_player && _player->_width > 320) {
-			mouseX = (rawMouseX * 320) / _player->_width;
-		}
-		if (_player && _player->_height > 200) {
-			mouseY = (rawMouseY * 200) / _player->_height;
+		// Apply X delta (line 193 / 237)
+		_flyShipScreenX += positionDeltaX;
+
+		// Y delta
+		if (_flyControlMode == 1) {
+			// Mode 1: clamped to ±12 with wind (lines 194-216)
+			int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
+			int yDelta = yCalc >> 10;
+			if (yDelta < -12) yDelta = -12;
+			if (yDelta > 12) yDelta = 12;
+			_flyShipScreenY -= (int16)yDelta;
+		} else {
+			// Mode 0/2/3: unclamped (lines 238-241)
+			_flyShipScreenY -= (int16)((levelYSpeed * local_14) >> 10);
 		}
 
-		// Convert mouse coords [0,319]x[0,199] → game coords [20,404]x[20,240]
-		// Game center (212,130) maps to screen center (160,100)
-		int16 targetGameX = (int16)(mouseX * 384 / 319 + 20);
-		int16 targetGameY = (int16)(mouseY * 220 / 199 + 20);
+		// Store vertical input for direction sprite (line 243)
+		_verticalInput = local_14;  // DAT_0044370e
 
-		// Clamp to game coordinate bounds (FUN_40C3CC lines 245-256)
-		if (targetGameX < 0x14) targetGameX = 0x14;   // 20
-		if (targetGameX > 0x194) targetGameX = 0x194;  // 404
-		if (targetGameY < 0x14) targetGameY = 0x14;    // 20
-		if (targetGameY > 0xF0) targetGameY = 0xF0;    // 240
+		// Ship facing direction (line 244)
+		_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
 
-		// Smooth interpolation (original uses ±12 per frame max delta)
-		const int16 maxStep = 12;
-		if (_flyShipScreenX < targetGameX) {
-			_flyShipScreenX = MIN((int16)(_flyShipScreenX + maxStep), targetGameX);
-		} else if (_flyShipScreenX > targetGameX) {
-			_flyShipScreenX = MAX((int16)(_flyShipScreenX - maxStep), targetGameX);
-		}
-		if (_flyShipScreenY < targetGameY) {
-			_flyShipScreenY = MIN((int16)(_flyShipScreenY + maxStep), targetGameY);
-		} else if (_flyShipScreenY > targetGameY) {
-			_flyShipScreenY = MAX((int16)(_flyShipScreenY - maxStep), targetGameY);
-		}
+		// --- Step 6: Position clamping (lines 245-256) ---
+		if (_flyShipScreenX > 0x194) _flyShipScreenX = 0x194;  // 404
+		if (_flyShipScreenY > 0xF0) _flyShipScreenY = 0xF0;    // 240
+		if (_flyShipScreenX < 0x14) _flyShipScreenX = 0x14;    // 20
+		if (_flyShipScreenY < 0x14) _flyShipScreenY = 0x14;    // 20
 
-		// Corridor boundary collision (FUN_40C3CC lines 257-284)
-		// Mode 0/2: Ship X is clamped to corridor boundaries with damage
+		// --- Step 7: Corridor collision — mode 0/2 only (lines 257-292) ---
 		if (_flyControlMode == 0 || _flyControlMode == 2) {
-			// Right boundary collision
+			// Right boundary (lines 258-270)
+			// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
 			if (_corridorRightX < _flyShipScreenX) {
 				_flyShipScreenX = _corridorRightX;
-				if (_hitCooldown < 5 && !_rebelInvulnerable) {
-					int damage = 3 + (_difficulty * 2);
-					_playerDamage += damage;
-					if (_playerDamage > 255) _playerDamage = 255;
-					_rebelHitCounter++;
+				if (_hitCooldown < 5) {
+					for (int i = 0; i < 25; i++) _velocityHistory[i] = -127;
 					_hitCooldown = 10;
 					_spaceShotDirection = 1;
 					initDamageFlash();
-					debug("Rebel2: Handler7 RIGHT CORRIDOR HIT ship=(%d,%d) boundary=%d",
-						_flyShipScreenX, _flyShipScreenY, _corridorRightX);
+					if (!_rebelInvulnerable) {
+						int damage = 3 + (_difficulty * 2);
+						_playerDamage += damage;
+						if (_playerDamage > 255) _playerDamage = 255;
+					}
+					_rebelHitCounter++;
 				}
 			}
-			// Left boundary collision
+			// Left boundary (lines 271-283)
 			if (_flyShipScreenX < _corridorLeftX) {
 				_flyShipScreenX = _corridorLeftX;
-				if (_hitCooldown < 5 && !_rebelInvulnerable) {
-					int damage = 3 + (_difficulty * 2);
-					_playerDamage += damage;
-					if (_playerDamage > 255) _playerDamage = 255;
-					_rebelHitCounter++;
+				if (_hitCooldown < 5) {
+					for (int i = 0; i < 25; i++) _velocityHistory[i] = 127;
 					_hitCooldown = 10;
 					_spaceShotDirection = 0;
 					initDamageFlash();
-					debug("Rebel2: Handler7 LEFT CORRIDOR HIT ship=(%d,%d) boundary=%d",
-						_flyShipScreenX, _flyShipScreenY, _corridorLeftX);
+					if (!_rebelInvulnerable) {
+						int damage = 3 + (_difficulty * 2);
+						_playerDamage += damage;
+						if (_playerDamage > 255) _playerDamage = 255;
+					}
+					_rebelHitCounter++;
 				}
 			}
-			// Y boundary clamping (no damage, just clamp) — lines 285-292
+			// Y boundary clamping — no damage (lines 285-292)
 			if (_corridorBottomY < _flyShipScreenY) {
 				_flyShipScreenY = _corridorBottomY;
 			}
@@ -1630,49 +1723,66 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			}
 		}
 
-		// Also update _shipPosX/Y for other systems that use screen coords
-		_shipPosX = (int16)((_flyShipScreenX - 20) * 319 / 384);
-		_shipPosY = (int16)((_flyShipScreenY - 20) * 199 / 220);
+		// --- Step 8: Perspective offsets (lines 293-316) ---
+		// f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
+		// Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d
+		// Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19
+		{
+			int absOffX = ABS(_flyShipScreenX - 0xd4);
+			int16 focalX = 0x2b;  // Far view default for Level 3
+			if (absOffX > 0) {
+				_perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
+					((0xd4 - focalX) * absOffX + focalX * 0xd4));
+			} else {
+				_perspectiveX = 0;
+			}
+			if (_flyShipScreenX < 0xd5) _perspectiveX = -_perspectiveX;
+
+			int absOffY = ABS(_flyShipScreenY - 0x82);
+			int16 focalY = 0x19;  // Far view default for Level 3
+			if (absOffY > 0) {
+				_perspectiveY = (int16)((focalY * 0x82 * absOffY) /
+					((0x82 - focalY) * absOffY + focalY * 0x82));
+			} else {
+				_perspectiveY = 0;
+			}
+			if (_flyShipScreenY < 0x83) _perspectiveY = -_perspectiveY;
+		}
 
-		// Direction calculation from ship game position
-		// Original FUN_0040d836 uses DAT_0044370c (smoothed velocity) for direction.
-		// Simplified: derive direction from position offset relative to center (212, 130).
-		// hDir: 0-4 where 2=center, based on offset from center X (212)
-		// vDir: 0-6 where 3=center, based on offset from center Y (130)
-		int16 hDiff = 0xD4 - _flyShipScreenX;  // 212 - shipX
-		int16 hDir = (hDiff + 64) >> 6;
-		if (hDir < 0) hDir = 0;
-		if (hDir > 4) hDir = 4;
+		// View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
+		_viewShift = _smoothedVelocity;
+		if (_viewShift > 127) _viewShift = 127;
+		if (_viewShift < -127) _viewShift = -127;
 
-		int16 vDir = (130 - _flyShipScreenY) / 37;  // adjusted divisor for game coord range
+		// --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
+		// 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
+		// vDir from vertical input: (0xa0 - verticalInput) >> 6
+		int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
 		if (vDir < 0) vDir = 0;
-		if (vDir > 6) vDir = 6;
+		if (vDir > 4) vDir = 4;
 
-		// Deadzone at center to reduce flicker
-		if (vDir == 3 && ABS(_flyShipScreenY - 130) > 13) {
-			if (_flyShipScreenY < 130) vDir = 2;
-			else vDir = 4;
+		// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
+		int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
+		if (hDir < 0) hDir = 0;
+		if (hDir > 6) hDir = 6;
+
+		// Hysteresis at center (lines 90-97, 98-105)
+		if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
+			hDir = (_smoothedVelocity < 1) ? 4 : 2;
 		}
-		if (hDir == 2 && ABS(_flyShipScreenX - 212) > 20) {
-			if (_flyShipScreenX < 212) hDir = 3;
-			else hDir = 1;
+		if (vDir == 2 && ABS(_verticalInput) > 15) {
+			vDir = (_verticalInput < 1) ? 3 : 1;
 		}
 
-		_shipDirectionH = hDir;
-		_shipDirectionV = vDir;
-		_shipDirectionIndex = hDir * 7 + vDir;
-
-		// Clamp direction index to valid range (0-34)
+		_shipDirectionIndex = vDir * 7 + hDir;
 		if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
 		if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
 
-		// Update firing state
-		_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+		_shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
 
-		debug("Rebel2 Handler7: rawMouse=(%d,%d) scaled=(%d,%d) shipPos=(%d,%d) screenPos=(%d,%d) dir=(%d,%d) idx=%d flySprite=%p",
-			rawMouseX, rawMouseY, mouseX, mouseY, _shipPosX, _shipPosY,
-			_flyShipScreenX, _flyShipScreenY, _shipDirectionH, _shipDirectionV, _shipDirectionIndex,
-			(void*)_flyShipSprite);
+		debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
+			_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
+			_verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
 
 		return;
 	}
@@ -4974,50 +5084,57 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 
 void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
-	// Uses _flyShipSprite (FLY001) with 35 direction frames (5x7 grid)
+	// Based on FUN_0040d836 lines 173-185:
+	//   FUN_004236e0(buf, frameInfo, screenX - 0xd4, screenY - 0x82, 0, sprite, frameIdx, 1, 0)
+	// The ship sprite is drawn at the perspective-transformed position offset from center.
+	// FUN_0041c720 transforms game coords (shipX, shipY) using perspective offsets.
 
 	if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
 		return;
 
-	// Base position at screen center with direction offset
-	// Ship position is in game coords (center 212,130), convert to screen offset
-	int baseX = 160;
-	int baseY = 105;
-	int16 posOffsetX = (_flyShipScreenX - 0xD4) / 13;  // (shipX - 212) / 13
-	int16 posOffsetY = (_flyShipScreenY - 0x82) / 11;   // (shipY - 130) / 11
-	int shipScreenX = baseX + posOffsetX;
-	int shipScreenY = baseY + posOffsetY;
-
 	int numSprites = _flyShipSprite->getNumChars();
 	int spriteIndex = _shipDirectionIndex;
 	if (spriteIndex < 0) spriteIndex = 0;
 	if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
 
-	// Center sprite at position
+	// Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
+	// The perspective transform shifts the ship position based on perspective offsets.
+	// Close view: FOBJ offset = (-52 - perspX, -45 - perspY), ship at screen center.
+	// For now, use a simplified perspective: ship position = center + offset from center
+	// scaled by perspective. In the original, FUN_00424510 shifts all FOBJ sprites.
+	//
+	// Screen position for sprite drawing (FUN_0040d836 line 174):
+	//   drawX = transformedX - 0xd4, drawY = transformedY - 0x82
+	// Where transformedX/Y come from FUN_0041c720(shipX, shipY, perspX, perspY, viewShift)
+	//
+	// Simplified: screenX = 160 + (shipX - 212) * perspFactor
+	// With the perspective formula, objects near center barely move, objects at edges move more.
+	int drawX = (_flyShipScreenX - 0xd4) + _perspectiveX;
+	int drawY = (_flyShipScreenY - 0x82) + _perspectiveY;
+
+	// Convert from game-center-relative to screen coordinates
+	// The sprite system expects coordinates relative to the 320x200 frame
+	// Center of frame = (160, 100), so offset = game position - game center
+	drawX += 160 + _viewX;
+	drawY += 100 + _viewY;
+
+	// Center the sprite on the position
 	int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
 	int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
-	int drawX = shipScreenX - spriteW / 2 + _viewX;
-	int drawY = shipScreenY - spriteH / 2 + _viewY;
+	drawX -= spriteW / 2;
+	drawY -= spriteH / 2;
 
 	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
 
-	// Laser overlay if firing
+	// Laser overlay if firing (same position as ship)
 	if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
 		int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
 		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
 	}
 
-	// Targeting overlay
-	if (_flyTargetSprite && _flyTargetSprite->getNumChars() > 0) {
-		int targetW = _flyTargetSprite->getCharWidth(0);
-		int targetH = _flyTargetSprite->getCharHeight(0);
-		int targetX = shipScreenX - targetW / 2 + _viewX;
-		int targetY = shipScreenY - targetH / 2 + _viewY;
-		renderNutSprite(renderBitmap, pitch, width, height, targetX, targetY, _flyTargetSprite, 0);
-	}
-
-	debug("Rebel2 Handler7: Ship at (%d,%d) sprite=%d/%d dir=(%d,%d) idx=%d",
-		drawX, drawY, spriteIndex, numSprites, _shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+	debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d",
+		drawX, drawY, spriteIndex, numSprites, _flyShipScreenX, _flyShipScreenY,
+		_perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput);
 }
 
 void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index a1342cad786..ec3067cc805 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -820,17 +820,33 @@ public:
 	// - DAT_0047fef0: Laser fire sprites (FLY002, par3=3)
 	// - DAT_0047fef8: Targeting overlay (FLY003, par3=2)
 	// - DAT_0047ff00: High-res alternative (FLY004, par3=11)
-	// - DAT_0044370c: Ship Y screen position
-	// - DAT_0044370e: Ship X screen position
+	// - DAT_00443708: Ship X position, DAT_0044370a: Ship Y position
+	// - DAT_0044370c: Smoothed horizontal velocity, DAT_0044370e: Vertical input
 
 	NutRenderer *_flyShipSprite;     // DAT_0047fee8 - FLY001 (35 direction frames)
 	NutRenderer *_flyLaserSprite;    // DAT_0047fef0 - FLY002
 	NutRenderer *_flyTargetSprite;   // DAT_0047fef8 - FLY003
 	NutRenderer *_flyHiResSprite;    // DAT_0047ff00 - FLY004
 
-	// Handler 7 screen position (different from Handler 8's raw positions)
-	int16 _flyShipScreenX;           // DAT_0044370e - Ship X screen position
-	int16 _flyShipScreenY;           // DAT_0044370c - Ship Y screen position
+	// Handler 7 ship state (FUN_40C3CC / FUN_0040d836)
+	// Position in game coordinate space [20,404]x[20,240], center=(212,130)
+	int16 _flyShipScreenX;           // DAT_00443708 - Ship X game position
+	int16 _flyShipScreenY;           // DAT_0044370a - Ship Y game position
+
+	// Physics state (velocity-based movement system from FUN_40C3CC case 4)
+	int16 _smoothedVelocity;         // DAT_0044370c - Averaged horizontal velocity (from history)
+	int16 _verticalInput;            // DAT_0044370e - Stored vertical input component
+	int16 _velocityHistory[25];      // DAT_00443716 - Horizontal velocity ring buffer (25 entries)
+	int16 _windHistoryX[15];         // DAT_00443b16 - Wind X history buffer
+	int16 _windHistoryY[15];         // DAT_00443b34 - Wind Y history buffer
+	int16 _windParamX;               // DAT_00443b12 - Wind X (from opcode 7 par4=0)
+	int16 _windParamY;               // DAT_00443b14 - Wind Y (from opcode 7 par4=0)
+
+	// Perspective view offsets (computed from ship position, used for rendering)
+	int16 _perspectiveX;             // DAT_00443712 - Perspective shift X
+	int16 _perspectiveY;             // DAT_00443714 - Perspective shift Y
+	int16 _viewShift;                // DAT_00443710 - Clamped smoothed velocity for view transform
+	bool _facingRight;               // DAT_0047ab8c - Ship facing right of center
 
 	// ======================= Handler 25 (0x19) GRD Ship System =======================
 	// For mixed mode missions (Level 2 speeder bike, etc.), Handler 25 uses GRD NUT


Commit: 1f23ead15c614a553930171395a246403931e186
    https://github.com/scummvm/scummvm/commit/1f23ead15c614a553930171395a246403931e186
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:05+02:00

Commit Message:
SCUMM: RA2: Implement ship explosions for level 3

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 61aa13ac4d0..8089125e3d4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1622,11 +1622,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 		// --- Step 5: Position delta (lines 174-242) ---
 		// levelSpeed (offset 4): calibrated so max velocity (127) → delta 12.
-		//   12 = (speed * 127) >> 9 → speed ≈ 48
-		// levelYSpeed (offset 2): calibrated so max input (127) → delta ~8.
-		//   8 = (speed * 127) >> 10 → speed ≈ 64
-		const int16 levelSpeed = 48;
-		const int16 levelYSpeed = 64;
+		//   8 = (speed * 127) >> 9 → speed ≈ 32
+		// levelYSpeed (offset 2): calibrated so max input (127) → delta ~6.
+		//   6 = (speed * 127) >> 10 → speed ≈ 48
+		const int16 levelSpeed = 32;
+		const int16 levelYSpeed = 48;
 		int16 absSmoothVel = ABS(_smoothedVelocity);
 		int16 positionDeltaX;
 
@@ -4086,9 +4086,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 	//   Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
 	//   Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
 
-	// Decrement hit cooldown per frame
-	if (_hitCooldown > 0)
-		_hitCooldown--;
+	// Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
+	// to match the original where the decrement happens during rendering.
 
 	if (_flyControlMode == 0 || _flyControlMode == 2) {
 		// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
@@ -5622,6 +5621,7 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 	if (!_smush_iconsNut)
 		return;
 
+	// --- Part 1: Space shot explosions (FUN_40F1C5 lines 19-60) ---
 	for (int i = 0; i < 5; i++) {
 		if (!_explosions[i].active)
 			continue;
@@ -5660,6 +5660,59 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 
 		_explosions[i].counter--;
 	}
+
+	// --- Part 2: Corridor/zone hit explosion (FUN_40F1C5 lines 61-85) ---
+	// Rendered when _hitCooldown > 0 (DAT_0044374c). Decrement happens HERE
+	// (matching original where FUN_40F1C5 decrements DAT_0044374c during render).
+	// _spaceShotDirection (DAT_0044374e) determines explosion side:
+	//   0 = left side (hit left boundary), 1 = right side (hit right boundary)
+	//   2 = bottom (zone push down), 3 = top (zone push up)
+	// Sprite frames: 0x15 - cooldown = 21 - cooldown (frames 12→21 as cooldown 9→0)
+	if (_hitCooldown != 0) {
+		_hitCooldown--;
+
+		int numChars = _smush_iconsNut->getNumChars();
+		int spriteIndex = 0x15 - _hitCooldown;  // 21 - remaining cooldown
+
+		if (spriteIndex >= 0 && spriteIndex < numChars) {
+			// Compute ship screen position (simplified FUN_0041c720 transform)
+			int shipDrawX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
+			int shipDrawY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
+
+			// Per-direction offset from ship center.
+			// Original uses lookup tables (DAT_004438da etc.) indexed by
+			// _shipDirectionIndex (35 entries per direction). We approximate
+			// with fixed offsets since we don't have the table data.
+			int offsetX = 0, offsetY = 0;
+			switch (_spaceShotDirection) {
+			case 0:  // Left wall hit → explosion on left side of ship
+				offsetX = -35;
+				break;
+			case 1:  // Right wall hit → explosion on right side of ship
+				offsetX = 35;
+				break;
+			case 2:  // Zone push down → explosion on bottom
+				offsetY = 20;
+				break;
+			case 3:  // Zone push up → explosion on top
+				offsetY = -20;
+				break;
+			default:
+				break;
+			}
+
+			int drawX = shipDrawX + offsetX;
+			int drawY = shipDrawY + offsetY;
+
+			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+			renderNutSprite(renderBitmap, pitch, width, height,
+				drawX - ew / 2, drawY - eh / 2, _smush_iconsNut, spriteIndex);
+
+			debug("Rebel2 H7 corridor explosion: dir=%d frame=%d pos=(%d,%d) cooldown=%d",
+				_spaceShotDirection, spriteIndex, drawX, drawY, _hitCooldown);
+		}
+	}
 }
 
 // FUN_41F29A — Handler 25 (FPS/Mixed) explosion rendering.


Commit: 1e2e7e108e7a7b545dcce43461d89bc69f01864b
    https://github.com/scummvm/scummvm/commit/1e2e7e108e7a7b545dcce43461d89bc69f01864b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:06+02:00

Commit Message:
SCUMM: RA2: Add explosion sounds

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 8089125e3d4..352c30efc11 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -367,6 +367,13 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_audioTrackActive[i] = false;
 	}
 
+	// Initialize and load sound effects (SYSTM/*.SAD files)
+	for (i = 0; i < kRA2NumSfx; i++) {
+		_sfxData[i] = nullptr;
+		_sfxSize[i] = 0;
+	}
+	loadSfx();
+
 	// Initialize menu system
 	_gameState = kStateMainMenu;  // Start at main menu
 	_menuSelection = 0;           // First item selected
@@ -433,6 +440,7 @@ InsaneRebel2::~InsaneRebel2() {
 	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
 
 	terminateAudio();
+	freeSfx();
 	delete _rebelMsgFont;
 	delete _menuFont;
 	delete _smush_dispfontNut;
@@ -809,14 +817,16 @@ int32 InsaneRebel2::processMouse() {
 					}
 				}
 
-				// Play explosion sound.
-				// Handler 8 types 1-4: BLAST.SAD (slot 0) via different sound function
-				// All others: EXPLODE.SAD (slot 2)
-				// TODO: Implement actual SAD sound playback
-				if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
-					debug("Rebel2: BLAST sound for enemy type %d (no visual explosion)", it->type);
-				} else {
-					debug("Rebel2: EXPLODE sound for enemy type %d", it->type);
+				// Play explosion sound (FUN_0041189e).
+				// Pan based on enemy center X position: (screenX - 160) mapped to [-127,127]
+				{
+					int enemyCenterX = (it->rect.left + it->rect.right) / 2 - _viewX;
+					int sfxPan = CLIP((enemyCenterX - 160) * 127 / 160, -127, 127);
+					if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
+						playSfx(0, 127, sfxPan);  // BLAST.SAD for handler 8 types 1-4
+					} else {
+						playSfx(2, 127, sfxPan);  // EXPLODE.SAD for all other enemies
+					}
 				}
 
 				// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
@@ -1696,6 +1706,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 						if (_playerDamage > 255) _playerDamage = 255;
 					}
 					_rebelHitCounter++;
+					playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
 				}
 			}
 			// Left boundary (lines 271-283)
@@ -1712,6 +1723,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 						if (_playerDamage > 255) _playerDamage = 255;
 					}
 					_rebelHitCounter++;
+					playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
 				}
 			}
 			// Y boundary clamping — no damage (lines 285-292)
@@ -4143,6 +4155,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					}
 					_rebelHitCounter++;
 					initDamageFlash();
+					// Pan based on ship X position relative to screen center
+					playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
 					debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
 						i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
 					break;  // Only one collision per frame (original breaks)
@@ -4183,6 +4197,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
+						playSfx(1, 127, 0);  // CRASH.SAD, top wall → center pan
 						debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
 					}
@@ -4204,6 +4219,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
+						playSfx(1, 127, 0);  // CRASH.SAD, bottom wall → center pan
 						debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
 					}
@@ -4225,6 +4241,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
+						playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
 						debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
 					}
@@ -4245,6 +4262,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
+						playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
 						debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
 					}
@@ -6334,6 +6352,121 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
 	}
 }
 
+// ========== Sound Effects (SAD files) ==========
+// Loads standalone SAUD files from SYSTM/ for one-shot SFX playback.
+// Original game loads these via FUN_0042a3b0 at init into DAT_00456888[0..7].
+
+static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
+	"SYSTM/BLAST.SAD",    // 0 - Player laser fire
+	"SYSTM/CRASH.SAD",    // 1 - Corridor/wall collision
+	"SYSTM/EXPLODE.SAD",  // 2 - Enemy explosion
+	"SYSTM/ALERT.SAD",    // 3 - Alert/warning
+	"SYSTM/LOCKON.SAD",   // 4 - Target lock-on
+	"SYSTM/BONUS.SAD",    // 5 - Bonus pickup
+	"SYSTM/HBLAST.SAD",   // 6 - Heavy blast (player weapon)
+	"SYSTM/TBLAST.SAD"    // 7 - TIE blast
+};
+
+void InsaneRebel2::loadSfx() {
+	for (int i = 0; i < kRA2NumSfx; i++) {
+		ScummFile *file = _vm->instantiateScummFile();
+		_vm->openFile(*file, kRA2SfxFiles[i]);
+		if (!file->isOpen()) {
+			debug("InsaneRebel2::loadSfx: Could not open %s", kRA2SfxFiles[i]);
+			delete file;
+			continue;
+		}
+
+		// SAUD file structure: SAUD header (8) + STRK sub-chunk + SDAT sub-chunk
+		// We scan for the SDAT tag to find the PCM data.
+		uint32 fileSize = file->size();
+		if (fileSize < 38) {  // Minimum: 8 (SAUD) + 22 (STRK) + 8 (SDAT header)
+			debug("InsaneRebel2::loadSfx: %s too small (%d bytes)", kRA2SfxFiles[i], fileSize);
+			file->close();
+			delete file;
+			continue;
+		}
+
+		// Verify SAUD tag
+		uint32 tag = file->readUint32BE();
+		if (tag != MKTAG('S', 'A', 'U', 'D')) {
+			debug("InsaneRebel2::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA2SfxFiles[i], tag);
+			file->close();
+			delete file;
+			continue;
+		}
+		file->readUint32BE();  // Skip SAUD size
+
+		// Scan for SDAT chunk (skip STRK and any other sub-chunks)
+		bool foundSdat = false;
+		while (file->pos() + 8 <= (int64)fileSize) {
+			uint32 chunkTag = file->readUint32BE();
+			uint32 chunkSize = file->readUint32BE();
+
+			if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
+				// Found PCM data
+				uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
+				_sfxData[i] = (byte *)malloc(pcmSize);
+				if (_sfxData[i]) {
+					file->read(_sfxData[i], pcmSize);
+					_sfxSize[i] = pcmSize;
+					debug("InsaneRebel2::loadSfx: Loaded %s (%d bytes PCM)", kRA2SfxFiles[i], pcmSize);
+				}
+				foundSdat = true;
+				break;
+			} else {
+				// Skip this sub-chunk
+				file->seek(chunkSize, SEEK_CUR);
+			}
+		}
+
+		if (!foundSdat) {
+			debug("InsaneRebel2::loadSfx: No SDAT chunk in %s", kRA2SfxFiles[i]);
+		}
+
+		file->close();
+		delete file;
+	}
+}
+
+void InsaneRebel2::freeSfx() {
+	for (int i = 0; i < kRA2NumSfx; i++) {
+		// Stop any playing SFX on this slot
+		_vm->_mixer->stopHandle(_sfxHandles[i]);
+		free(_sfxData[i]);
+		_sfxData[i] = nullptr;
+		_sfxSize[i] = 0;
+	}
+}
+
+void InsaneRebel2::playSfx(int slot, int volume, int pan) {
+	if (slot < 0 || slot >= kRA2NumSfx || !_sfxData[slot] || _sfxSize[slot] == 0) {
+		return;
+	}
+
+	// Stop any previous instance of this SFX slot
+	_vm->_mixer->stopHandle(_sfxHandles[slot]);
+
+	// Make a copy of the PCM data (makeRawStream with DisposeAfterUse::YES will free it)
+	byte *pcmCopy = (byte *)malloc(_sfxSize[slot]);
+	if (!pcmCopy) {
+		return;
+	}
+	memcpy(pcmCopy, _sfxData[slot], _sfxSize[slot]);
+
+	// Create a one-shot raw audio stream: 8-bit unsigned mono at 11025 Hz
+	Audio::SeekableAudioStream *stream = Audio::makeRawStream(
+		pcmCopy, _sfxSize[slot], 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+
+	// Scale volume from 0-127 to ScummVM's 0-255 range
+	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+
+	_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_sfxHandles[slot],
+		stream, -1, scaledVolume, pan);
+
+	debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
+}
+
 // ======================= Menu System Implementation =======================
 // Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index ec3067cc805..199bcb9ed02 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -946,6 +946,28 @@ public:
 	// Queue audio data for playback on a specific track
 	void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
 
+	// ========== Sound Effects (SAD files) ==========
+	// 8 standalone SAUD files in SYSTM/ loaded at init for one-shot SFX.
+	// Slot mapping (from FUN_0042a3b0 init):
+	//   0=BLAST.SAD   1=CRASH.SAD   2=EXPLODE.SAD  3=ALERT.SAD
+	//   4=LOCKON.SAD  5=BONUS.SAD   6=HBLAST.SAD   7=TBLAST.SAD
+
+	static const int kRA2NumSfx = 8;
+
+	byte *_sfxData[kRA2NumSfx];         // Loaded PCM data for each SAD slot
+	uint32 _sfxSize[kRA2NumSfx];        // PCM data size per slot
+	Audio::SoundHandle _sfxHandles[kRA2NumSfx]; // Mixer handles for SFX playback
+
+	// Load all SAD files from SYSTM/ directory
+	void loadSfx();
+
+	// Free all loaded SFX data
+	void freeSfx();
+
+	// Play a one-shot sound effect
+	// slot: 0-7 (SAD file index), volume: 0-127, pan: -127..+127
+	void playSfx(int slot, int volume, int pan);
+
 };
 
 } // End of namespace Insane


Commit: 07c45898d71a4001048892c99b0e44557db98a6b
    https://github.com/scummvm/scummvm/commit/07c45898d71a4001048892c99b0e44557db98a6b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:06+02:00

Commit Message:
SCUMM: RA2: Add shooting sounds

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 352c30efc11..08158b978d6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -3467,10 +3467,9 @@ void InsaneRebel2::spawnShot(int x, int y) {
 void InsaneRebel2::spawnTurretShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
 		if (_turretShots[i].counter == 0) {
-			// Play sound based on level type
-			// FUN_0041189e(-(ushort)(_rebelLevelType == 5) & 7, i + 1, 0x7f, 0, 0)
-			// Sound ID: 0 for type 5, 7 for others
-			// TODO: Play laser sound via audio system
+			// FUN_0041189e(-(ushort)(DAT_004436de == 5) & 7, i + 1, 0x7f, 0, 0)
+			// levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
+			playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
 
 			_turretShots[i].counter = getShotMaxDuration();
 			_turretShots[i].seqNum = _turretShotSeqCounter;
@@ -3482,11 +3481,12 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
 	}
 }
 
-// Handler 8 Vehicle shot spawn (based on FUN_401CCF)
+// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
 void InsaneRebel2::spawnVehicleShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
 		if (_vehicleShots[i].counter == 0) {
-			// TODO: Play laser sound
+			// FUN_0041189e(6, local_c + 1, 0x7f, 0, 0) — HBLAST.SAD
+			playSfx(6, 127, 0);
 			_vehicleShots[i].counter = getShotMaxDuration();
 			_vehicleShots[i].targetX = x + _viewX;
 			_vehicleShots[i].targetY = y + _viewY;
@@ -3505,8 +3505,8 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 
 	for (int i = 0; i < 2; i++) {
 		if (_turretShots[i].counter == 0) {
-			// Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
-			// TODO: Play laser sound
+			// FUN_0041189e(6, local_1c + 1, 0x7f, 0, 0) — HBLAST.SAD
+			playSfx(6, 127, 0);
 
 			_turretShots[i].counter = getShotMaxDuration();
 			_turretShots[i].seqNum = _turretShotSeqCounter;
@@ -3527,8 +3527,8 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 void InsaneRebel2::spawnSpaceShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
 		if (_spaceShots[i].counter == 0) {
-			// Play sound: FUN_0041189e(6, i + 1, 0x7f, 0, 0)
-			// TODO: Play laser sound
+			// FUN_0041189e(6, local_2c + 1, 0x7f, 0, 0) — HBLAST.SAD
+			playSfx(6, 127, 0);
 
 			_spaceShots[i].counter = getShotMaxDuration();
 			_spaceShots[i].targetX = x;  // Screen coords


Commit: 31a1ecf0cf2e46d4a911bf14b7bee8d05bcdbeb2
    https://github.com/scummvm/scummvm/commit/31a1ecf0cf2e46d4a911bf14b7bee8d05bcdbeb2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:06+02:00

Commit Message:
SCUMM: RA2: Render gun shooting

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 08158b978d6..7b769ee3ad4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -280,6 +280,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		_turretShots[i].targetX = 0;
 		_turretShots[i].targetY = 0;
 		_turretShots[i].seqNum = 0;
+		_turretShots[i].gunX = 0;
+		_turretShots[i].gunY = 0;
 	}
 	_turretShotSeqCounter = 0;
 
@@ -3495,8 +3497,11 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
 	}
 }
 
-// Handler 25 Speeder bike shot spawn (based on FUN_0041db5e lines 170-190)
-// Similar to turret but with character-based gun position
+// Handler 25 on-foot shot spawn (based on FUN_0041db5e lines 170-190)
+// Gun position computed from GRD002 character sprite.
+// Original stores: DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
+//                  DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
+// Render adds view offset back, so screen gun = table[idx] + spriteOffset.
 void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 	// Handler 25 can only shoot when uncovered (damage == 0)
 	if (_rebelDamageLevel != 0) {
@@ -3516,8 +3521,68 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 			_turretShots[i].targetX = x;
 			_turretShots[i].targetY = y;
 
-			debug("Rebel2 Handler25: Spawned shot %d target (%d,%d)",
-				i, _turretShots[i].targetX, _turretShots[i].targetY);
+			// Compute gun position from GRD002 character sprite.
+			// Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
+			// We approximate from the NUT sprite center + directional offset.
+			if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
+				// Compute current sprite index (same logic as renderHandler25Ship)
+				int spriteIdx;
+				if (_rebelDamageLevel == 0) {
+					// Uncovered: compute from crosshair position zones
+					int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
+					int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
+					int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
+					int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
+					int areaWidth = areaRight - areaLeft;
+					int areaHeight = areaBottom - areaTop;
+					int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80;
+					int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;
+					int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (x - areaLeft)) / zoneWidth : 2;
+					int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (y - areaTop)) / zoneHeight : 0;
+					if (xZone < 0) xZone = 0;
+					if (xZone > 4) xZone = 4;
+					if (yZone < 0) yZone = 0;
+					if (yZone > 1) yZone = 1;
+					if (_rebelFlightDir == (yZone & 1)) {
+						xZone = 4 - xZone;
+					}
+					spriteIdx = yZone * 5 + xZone + 5;
+				} else {
+					spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
+				}
+				int numSprites = _grd002Sprite->getNumChars();
+				if (spriteIdx < 0) spriteIdx = 0;
+				if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+
+				// Get sprite rendering position (same as in renderHandler25Ship)
+				int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
+				int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
+				int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
+				int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
+				bool shouldMirror = (_rebelFlightDir != 0 && _rebelDamageLevel == 0);
+
+				int drawX;
+				if (shouldMirror) {
+					drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+				} else {
+					drawX = _rebelViewOffset2X + spriteXOffset;
+				}
+				int drawY = spriteYOffset + _rebelViewOffset2Y;
+
+				// Gun barrel is approximately at the character's hand level:
+				// X: center of sprite ± directional offset toward the target
+				// Y: about 60% down the sprite height (hand/arm level)
+				_turretShots[i].gunX = drawX + spriteW / 2;
+				_turretShots[i].gunY = drawY + (spriteH * 3) / 5;
+			} else {
+				// Fallback: approximate center-bottom of character area
+				_turretShots[i].gunX = _rebelViewOffset2X + 160;
+				_turretShots[i].gunY = _rebelViewOffset2Y + 140;
+			}
+
+			debug("Rebel2 Handler25: Spawned shot %d target (%d,%d) gun (%d,%d)",
+				i, _turretShots[i].targetX, _turretShots[i].targetY,
+				_turretShots[i].gunX, _turretShots[i].gunY);
 			break;
 		}
 	}
@@ -6008,16 +6073,10 @@ void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int
 		int16 targetX = _turretShots[i].targetX;
 		int16 targetY = _turretShots[i].targetY;
 
-		// Gun position: In FUN_0041f004, the gun is at a fixed offset based on character sprite
-		// From FUN_0041db5e lines 178-187, the gun position is:
-		// gunX = (spriteOffset + DAT_00457910) - viewOffsetX
-		// gunY = (spriteOffset + DAT_00457912) - viewOffsetY
-		//
-		// For simplicity, use a position near the bottom-center of the screen
-		// where the character's gun would be in the speeder bike cockpit.
-		// The character is typically around (160, 150) in the cockpit view.
-		int16 gunX = 160;  // Center of screen
-		int16 gunY = 170;  // Near bottom where character sits
+		// Gun position computed at spawn time from GRD002 sprite data
+		// Original: DAT_0045791c[i] + DAT_0045790c, DAT_00457920[i] + DAT_0045790e
+		int16 gunX = _turretShots[i].gunX;
+		int16 gunY = _turretShots[i].gunY;
 
 		int16 progress = maxDuration - _turretShots[i].counter;
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 199bcb9ed02..be88484b07d 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -663,6 +663,8 @@ public:
 		int16 targetX;     // DAT_0044367e[i] - target X position
 		int16 targetY;     // DAT_00443682[i] - target Y position
 		int16 seqNum;      // DAT_0044368a[i] - shot sequence (for alternating)
+		int16 gunX;        // DAT_0045791c[i] - gun barrel X (Handler 25, screen coords)
+		int16 gunY;        // DAT_00457920[i] - gun barrel Y (Handler 25, screen coords)
 	};
 	TurretShot _turretShots[2];
 	int16 _turretShotSeqCounter;  // DAT_0047fe94 - global sequence counter


Commit: 285f7ff2b11ed928858db12aeebdf0f360edc492
    https://github.com/scummvm/scummvm/commit/285f7ff2b11ed928858db12aeebdf0f360edc492
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:07+02:00

Commit Message:
SCUMM: RA2: Prevent damage while covered

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 7b769ee3ad4..781e3717bf1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1288,89 +1288,125 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 	//
 	// Stream position on entry: at offset +8 (body[0], first word after 8-byte header)
 
-	if (par3 == 1 || par3 == 2) {
-		// Direct hit path — FUN_4092D9 lines 209-227
+	// Handler 25 has a different opcode 3 structure (FUN_41CADB case 1):
+	//   par3==5: probabilistic damage WITH cover check (DAT_0045790a < 2)
+	//   par3==1: increment hit counter ONLY (NO damage), requires par4 != 4
+	//   par4==100: direct damage (separate check after par3 branches, NO cover check)
+	// Other handlers (0x26/7/8) use FUN_4092D9/FUN_40E35E/FUN_401234 with different logic.
+	if (_rebelHandler == 25) {
+		// Handler 25 opcode 3 — FUN_41CADB case 1
+		int16 srcIdBody0 = b.readSint16LE(); // body[0] (offset +8)
+		int16 srcIdBody1 = b.readSint16LE(); // body[1] (offset +10)
+
+		if (par3 == 5) {
+			// Probabilistic damage with cover check (lines 81-92)
+			debug("Rebel2 Opcode3: H25 par3=5 srcId=%d isBitSet=%d damageLevel=%d",
+				srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
+
+			if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
+				int probability = 20 + _difficulty * 20;
+				if (probability < 5) probability = 5;
+				if (probability > 90) probability = 90;
+				int roll = _vm->_rnd.getRandomNumber(99);
+				debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
+
+				if (roll < probability) {
+					if (!_rebelInvulnerable) {
+						int damageAmount = 5 + (_difficulty * 2);
+						_playerDamage += damageAmount;
+						if (_playerDamage > 255) _playerDamage = 255;
+						debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
+							srcIdBody1, damageAmount, _playerDamage);
+					}
+					initDamageFlash();
+				}
+			} else {
+				debug("Rebel2 Opcode3: H25 par3=5 BLOCKED (damageLevel=%d isBitSet=%d)",
+					_rebelDamageLevel, isBitSet(srcIdBody1));
+			}
+		} else if (par3 == 1 && !isBitSet(srcIdBody0) && par4 != 4) {
+			// Hit counter only — NO damage (lines 94-98)
+			_rebelHitCounter++;
+			debug("Rebel2: H25 hit counter++ -> %d (par3=1 par4=%d, no damage)",
+				_rebelHitCounter, par4);
+		} else {
+			debug("Rebel2 Opcode3: H25 par3=%d par4=%d (no action)", par3, par4);
+		}
+
+		// Direct damage: par4==100, separate from par3 branches (lines 99-111)
+		if (par4 == 100 && !isBitSet(srcIdBody0)) {
+			if (!_rebelInvulnerable) {
+				int directHitDamage = 8 + (_difficulty * 4);
+				_playerDamage += directHitDamage;
+				if (_playerDamage > 255) _playerDamage = 255;
+				debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
+					directHitDamage, _playerDamage);
+			}
+			initDamageFlash();
+		}
+	} else if (par3 == 1 || par3 == 2) {
+		// Non-Handler-25 direct hit path — FUN_4092D9 lines 209-227
 		int16 srcId = b.readSint16LE(); // body[0] (offset +8): source enemy ID
 
 		debug("Rebel2 Opcode3: par3=%d par4=%d srcId=%d isBitSet=%d",
 			par3, par4, srcId, isBitSet(srcId));
 
-		// FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
 		if (!isBitSet(srcId)) {
-			// Always increment hit counter (DAT_0047ab80) — line 215
 			_rebelHitCounter++;
 			debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
 
-			// Damage path: par4 must be non-zero AND direct hit damage table > 0
-			// TODO: Read actual damage from per-level table DAT_0047e0f4
 			int directHitDamage = 8 + (_difficulty * 4);
 
 			if (par4 != 0 && directHitDamage > 0) {
 				bool shouldDamage = false;
 
 				if (par3 == 1 && par4 < 10) {
-					// par3=1: simple direct hit, par4 is hit strength (1..9)
 					shouldDamage = true;
 				} else if (par3 == 2 && par4 > 99) {
-					// par3=2: wave-gated hit, par4 > 99 required
-					// Additional check: for par4 >= 101 (0x65), verify wave state bit
-					// Original: (DAT_0047ab9c & (1 << ((par4 + 0x9b) & 0x1f))) == 0
 					if (par4 < 0x65 || (_rebelPhaseState & (1 << ((par4 + 0x9b) & 0x1f))) == 0) {
 						shouldDamage = true;
 					}
 				}
 
 				if (shouldDamage) {
-					// Apply damage unless invulnerable (DAT_0047ab64 == 0)
 					if (!_rebelInvulnerable) {
 						_playerDamage += directHitDamage;
 						if (_playerDamage > 255) _playerDamage = 255;
 						debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
 							srcId, par3, par4, directHitDamage, _playerDamage);
 					}
-					// Visual effect — FUN_00420515 (palette flash)
 					initDamageFlash();
-					// TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) — play hit sound
 				}
 			}
 		}
 	} else if (par3 == 5) {
-		// Probabilistic damage path — FUN_4092D9 lines 228-239
-		b.skip(2); // Skip body[0] (offset +8, not used for par3=5)
-		int16 srcId = b.readSint16LE(); // body[1] (offset +10): source enemy ID
+		// Non-Handler-25 probabilistic damage — FUN_4092D9 lines 228-239
+		b.skip(2); // Skip body[0]
+		int16 srcId = b.readSint16LE(); // body[1] (offset +10)
 
 		debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
 
-		// FUN_00423970(srcId): only proceed if source enemy is active (bit clear)
 		if (!isBitSet(srcId)) {
-			// Probability check: original reads from per-level table DAT_0047e0fc
-			// TODO: Use actual per-level probability table instead of hardcoded values
 			int probability = 20 + _difficulty * 20;
 			if (probability < 5) probability = 5;
 			if (probability > 90) probability = 90;
 
-			int roll = _vm->_rnd.getRandomNumber(99); // FUN_004233a0(100) returns [0,99]
+			int roll = _vm->_rnd.getRandomNumber(99);
 			debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
 
 			if (roll < probability) {
-				// Apply damage unless invulnerable (DAT_0047ab64 == 0)
 				if (!_rebelInvulnerable) {
-					// TODO: Read damage amount from per-level table DAT_0047e0f8
 					int damageAmount = 5 + (_difficulty * 2);
 					_playerDamage += damageAmount;
 					if (_playerDamage > 255) _playerDamage = 255;
 					debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
 						srcId, damageAmount, _playerDamage);
 				}
-				// Visual effect — called regardless of invulnerability.
-				// Handler 8: FUN_0042073B — palette flash + screen shake
-				// Other handlers: FUN_00420515 — palette flash only (no screen shake)
 				if (_rebelHandler == 8) {
 					triggerDamageEffect();
 				} else {
 					initDamageFlash();
 				}
-				// TODO: FUN_0041189e(1, 0, 0x7f, 0, 0) — play hit sound
 			}
 		}
 	} else {
@@ -8599,6 +8635,99 @@ int InsaneRebel2::runLevel1() {
 	return kLevelQuit;
 }
 
+// =============================================================================
+// Wave State Management - FUN_00417b61
+// Waits for video completion, accumulates kill state, redistributes kill credits.
+// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
+// =============================================================================
+
+uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
+	// FUN_00417b61: Core wave management function
+	// Called after each wave video plays. Handles:
+	// 1. Waiting for video to finish (with early exit on enemy completion)
+	// 2. Copying wave state to accumulated phase state
+	// 3. Redistributing kill credits from the budget
+	//
+	// Returns: kill bits credited this wave, or 0xFFFF on death/quit/completion
+
+	uint16 result = 0;
+
+	// Step 1: Wait for video to finish (lines 21-32)
+	// Original loop: while (damage < 0xff && frame < maxFrame-1 && !escPressed)
+	// The SmushPlayer::play() call already blocks until video ends, so this step
+	// is handled implicitly. The early-exit logic (threshold > 0: if frame > 50
+	// AND all required enemy type bits are set, count up and break when > threshold)
+	// would need per-frame callbacks to work precisely. For now, the primary effect
+	// is covered by the video playing to completion and accumulating state.
+	// TODO: Implement per-frame early exit callback for threshold-based wave termination.
+
+	// Step 2: Copy wave state to phase state (line 33)
+	// DAT_0047ab9c = DAT_0047ab98
+	_rebelPhaseState = _rebelWaveState;
+	debug("Rebel2 processWaveEnd: waveState=0x%x -> phaseState=0x%x mask=0x%x budget=%d threshold=%d flags=%d",
+		_rebelWaveState, _rebelPhaseState, mask, budget ? *budget : -1, threshold, flags);
+
+	// Step 3: Kill redistribution - add random unkilled types (lines 34-47)
+	// Only when (flags & 2) != 0. Level 2 always passes flags=0, so inactive for Level 2.
+	if ((flags & 2) != 0) {
+		// Collect unkilled enemy type bits that are within the mask
+		byte unkilled[8];
+		int16 numUnkilled = 0;
+		for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
+			if ((_rebelPhaseState & (2 << (b & 0x1f))) == 0) {
+				unkilled[numUnkilled] = (byte)(2 << (b & 0x1f));
+				numUnkilled++;
+			}
+		}
+		if (numUnkilled > 0) {
+			// Randomly add one unkilled type to phase state
+			int idx = _vm->_rnd.getRandomNumber(numUnkilled - 1);
+			_rebelPhaseState |= unkilled[idx];
+			if (budget) (*budget)++;
+		}
+	}
+
+	// Step 4: Kill credit transfer (lines 48-73)
+	// Collect all SET enemy type bits from phase state
+	byte killed[8];
+	int16 numKilled = 0;
+	for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
+		if ((_rebelPhaseState & (2 << (b & 0x1f))) != 0) {
+			killed[numKilled] = (byte)(2 << (b & 0x1f));
+			numKilled++;
+		}
+	}
+
+	// Max credits: 8 normally, 2 if flag bit 0 set
+	int16 maxCredits = ((flags & 1) == 0) ? 8 : 2;
+
+	// Transfer kills from phase state to result, limited by budget
+	int16 creditCount = 0;
+	while (creditCount < maxCredits && numKilled > 0 && budget && *budget > 0) {
+		int idx = _vm->_rnd.getRandomNumber(numKilled - 1);
+		_rebelPhaseState -= killed[idx];   // Remove from accumulated state
+		result |= killed[idx];              // Credit to return value
+		(*budget)--;
+
+		// Remove from array (shift remaining elements)
+		for (int i = idx; i + 1 < numKilled; i++) {
+			killed[i] = killed[i + 1];
+		}
+		numKilled--;
+		creditCount++;
+	}
+
+	debug("Rebel2 processWaveEnd: result=0x%x phaseState=0x%x (after redistribution) budget=%d",
+		result, _rebelPhaseState, budget ? *budget : -1);
+
+	// Step 5: Return value (lines 74-78)
+	// Return 0xFFFF if: dead, phase complete, or quit
+	if (_playerDamage >= 255 || (int16)_rebelPhaseState >= mask || _vm->shouldQuit()) {
+		return 0xFFFF;
+	}
+	return result;
+}
+
 // =============================================================================
 // Level 2 Handler - FUN_00418063
 // Multiple parts with P1/P2/P3 subdirectories
@@ -8612,6 +8741,13 @@ int InsaneRebel2::runLevel2() {
 	// Phase 1 mask: 0x06 (enemy types 1,2)
 	// Phase 2 mask: 0x0e (enemy types 1,2,3)
 	// Phase 3 mask: 0x0e (enemy types 1,2,3)
+	//
+	// Kill credit budget (from level data table DAT_0047e0e8):
+	// Each phase gets a budget = tableBase + random(3). processWaveEnd() uses
+	// this budget to randomly redistribute kill credits, creating non-deterministic
+	// wave progression. Using calibrated defaults until exact table values extracted.
+	static const int16 kLevel2BudgetBase[3] = { 3, 3, 3 };  // Phase 1, 2, 3
+
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	int bonusCount = 0;     // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
 	int totalKills = 0;     // local_c: accumulated kill count across phases
@@ -8623,9 +8759,14 @@ int InsaneRebel2::runLevel2() {
 	if (_vm->shouldQuit()) return kLevelQuit;
 
 	// Play level beginning cinematic (02BEG.SAN)
+	// Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
+	// Includes text overlay from GAME.TRS — deferred until text rendering is ready.
 	playLevelBegin(2);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
+	// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state (before retry loop)
+	clearBit(0);
+
 	// Main gameplay retry loop (restarts from beginning on death)
 	while (!_vm->shouldQuit()) {
 		_playerShield = 255;
@@ -8635,8 +8776,7 @@ int InsaneRebel2::runLevel2() {
 		totalKills = 0;
 		totalMisses = 0;
 
-		// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
-		clearBit(0);
+		// FUN_0041c7d0: Reset per-attempt state
 		_enemies.clear();
 		for (int i = 0; i < 512; i++) {
 			_rebelLinks[i][0] = 0;
@@ -8651,16 +8791,20 @@ int InsaneRebel2::runLevel2() {
 		_rebelPhaseState = 0;
 		_rebelWaveState = 0;
 
-		// Play A.SAN (background loader)
-		debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background)");
+		// Initialize kill budget from level data table + random(3)
+		// Original: sVar4 = levelData[phase1Offset]; local_14[0] = sVar4 + random(3)
+		int16 budget = kLevel2BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
+
+		// Play A.SAN (background loader) — flags 0x28 (preserve buffer, gameplay mode)
+		debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/P1/02P01_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
 
-		// Copy wave state to phase state after A.SAN
-		_rebelPhaseState = _rebelWaveState;
+		// processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
+		processWaveEnd(0x36, &budget, 0, 0);
 
 		// Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
 		// Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
@@ -8674,14 +8818,15 @@ int InsaneRebel2::runLevel2() {
 				"LEV02/P1/02P01_C.SAN",
 				"LEV02/P1/02P01_D.SAN"
 			};
-			debug("Rebel2: Phase 1 wave - playing %s (state=0x%x)", variants[variant], _rebelPhaseState);
-			splayer->setCurVideoFlags(0x28);
+			debug("Rebel2: Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
+			// Wave videos use flags 0x428 (original: FUN_0041f4d0 param_2=0x428)
+			splayer->setCurVideoFlags(0x428);
 			splayer->play(variants[variant], 12);
 			_deathFrame = splayer->_frame;
 
-			// After wave: copy wave state to phase state (FUN_00417b61 line 33)
-			_rebelPhaseState = _rebelWaveState;
-			debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06)", _rebelPhaseState);
+			// processWaveEnd with threshold=0x14 (20) — enables early exit when enemies killed
+			processWaveEnd(0x36, &budget, 0x14, 0);
+			debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06) budget=%d", _rebelPhaseState, budget);
 		}
 
 		// Check for bonus (bit 4 = 0x10)
@@ -8706,22 +8851,24 @@ int InsaneRebel2::runLevel2() {
 		_rebelWaveState = 0;
 		_enemies.clear();
 
+		// Initialize Phase 2 budget
+		budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
+
 		// Play A.SAN (background loader)
-		debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background)");
+		debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/P2/02P02_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
-		_rebelPhaseState = _rebelWaveState;
 
-		// Phase 2 wave loop: state-based variant selection
-		// Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
-		while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+		// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
+		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
+		while (true) {
+			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
+			if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e) break;
 			if (_vm->shouldQuit()) return kLevelQuit;
 
-			int waveSelect = _rebelPhaseState & 0x0e;  // masked enemy state
-
 			// If no specific pattern: randomize high bits (original lines 71-74)
 			// When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
 			if ((waveSelect & 0x0c) == 0) {
@@ -8745,13 +8892,10 @@ int InsaneRebel2::runLevel2() {
 				filename = "LEV02/P2/02P02_D.SAN"; break;
 			}
 
-			debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
-			splayer->setCurVideoFlags(0x28);
+			debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
+			splayer->setCurVideoFlags(0x428);
 			splayer->play(filename, 12);
 			_deathFrame = splayer->_frame;
-
-			_rebelPhaseState = _rebelWaveState;
-			debug("Rebel2: Phase 2 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
 		}
 
 		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
@@ -8776,21 +8920,23 @@ int InsaneRebel2::runLevel2() {
 		_enemies.clear();
 		prevWaveState = 0;
 
+		// Initialize Phase 3 budget
+		budget = kLevel2BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
+
 		// Play A.SAN (background loader)
-		debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background)");
+		debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/P3/02P03_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
-		_rebelPhaseState = _rebelWaveState;
 
-		// Phase 3 wave loop: state-based with randomization
-		// Original: while (local_10 >= 0 && (DAT_0047ab9c & 0xe) != 0xe)
+		// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
+		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
 		{
-			int waveSelect = _rebelPhaseState & 0x0e;
+			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
 
-			while (_playerDamage < 255 && (_rebelPhaseState & 0x0e) != 0x0e) {
+			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
 				if (_vm->shouldQuit()) return kLevelQuit;
 
 				// Phase 3 randomization (original lines 113-115):
@@ -8823,14 +8969,14 @@ int InsaneRebel2::runLevel2() {
 					filename = "LEV02/P3/02P03_I.SAN"; break;
 				}
 
-				debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x)", filename, _rebelPhaseState, waveSelect);
-				splayer->setCurVideoFlags(0x28);
+				debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
+				splayer->setCurVideoFlags(0x428);
 				splayer->play(filename, 12);
 				_deathFrame = splayer->_frame;
 
-				_rebelPhaseState = _rebelWaveState;
-				waveSelect = _rebelPhaseState & 0x0e;
-				debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e)", _rebelPhaseState);
+				// processWaveEnd at BOTTOM with threshold=0x14
+				waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
+				debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
 			}
 		}
 
@@ -8841,9 +8987,12 @@ int InsaneRebel2::runLevel2() {
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		// Level completed! Calculate accuracy score.
+		// Original: FUN_00417327 with score thresholds and medal ranks
+		// Score presentation deferred until GAME.TRS text rendering is implemented.
 		{
+			totalMisses += _rebelHitCounter;
 			int accuracy = 0;
-			int totalShots = totalKills + totalMisses + _rebelHitCounter;
+			int totalShots = totalKills + totalMisses;
 			if (totalKills > 0 && totalShots > 0) {
 				accuracy = (totalKills * 100) / totalShots;
 			}
@@ -8857,12 +9006,17 @@ int InsaneRebel2::runLevel2() {
 
 	level2_death:
 		// Player died — play death sequence and retry or game over
+		// Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
 		debug("Rebel2: Level 2 Phase %d death", _currentPhase);
 		playCinematic("LEV02/02DIE.SAN");
 		if (_vm->shouldQuit()) return kLevelQuit;
 
+		// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
+		// DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
+		// Currently not tracked — will be wired when bonus life system is implemented.
 		_playerLives--;
 		if (_playerLives <= 0) {
+			// Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
 			playLevelGameOver(2);
 			return kLevelGameOver;
 		}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index be88484b07d..c91ba5ac25b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -243,6 +243,15 @@ public:
 	int runLevel5();   // FUN_00418EC6 - Single gameplay phase
 	int runLevel6();   // FUN_00419317 - Two phases with per-phase retry
 
+	// Wave state management (FUN_00417b61)
+	// Waits for current video to finish, accumulates kill state, redistributes
+	// kill credits from the budget. Returns credited kill bits, or 0xFFFF on death/quit.
+	// mask: required enemy bits (0x36 for Phase 1, 0x3e for Phases 2/3)
+	// budget: kill credit budget counter (decremented per credit transfer)
+	// threshold: early-exit frame threshold (0=disabled, 0x14=20 for wave loops)
+	// flags: bit 1 = add random unkilled types, bit 0 = limit credits to 2 (else 8)
+	uint16 processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
+
 	// Random number helper (emulates FUN_004233a0)
 	int getRandomVariant(int max);
 


Commit: 713aa7bd917e1f02a2d04aaca2e7997039379693
    https://github.com/scummvm/scummvm/commit/713aa7bd917e1f02a2d04aaca2e7997039379693
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:07+02:00

Commit Message:
SCUMM: RA2: Fix covered and uncovered phase cycling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 781e3717bf1..47bc3acc47b 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1863,11 +1863,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			// Reset wave state to accumulated phase state (same as Handler 8)
 			// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
 			_rebelWaveState = _rebelPhaseState;
-			// Initialize to covered state - player starts behind cover
-			// This ensures the corridor overlay shows the "covered" position initially
-			_rebelAutopilot = 1;    // DAT_00457904 = 1 (covered)
-			_rebelDamageLevel = 5;  // DAT_0045790a = 5 (fully in cover)
-			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, starting COVERED, wave=0x%x", _rebelWaveState);
+			// NOTE: autopilot and damageLevel are NOT reset here (verified against
+			// FUN_41CADB case 4 lines 114-121). They persist across video boundaries
+			// so the player keeps their cover state when transitioning between waves.
+			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
+				_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
 		}
 
 		// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
@@ -8776,6 +8776,12 @@ int InsaneRebel2::runLevel2() {
 		totalKills = 0;
 		totalMisses = 0;
 
+		// Reset Handler 25 cover state — player starts uncovered at level start
+		// DAT_00457904 and DAT_0045790a are zero-initialized globals in the original
+		_rebelAutopilot = 0;
+		_rebelDamageLevel = 0;
+		_rebelControlMode = 0;
+
 		// FUN_0041c7d0: Reset per-attempt state
 		_enemies.clear();
 		for (int i = 0; i < 512; i++) {


Commit: 7ae1ddbd5a4342f07d3ed98fbdc1218541e28d78
    https://github.com/scummvm/scummvm/commit/7ae1ddbd5a4342f07d3ed98fbdc1218541e28d78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:07+02:00

Commit Message:
SCUMM: RA2: Reset handlers around level 2 cinematics

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 47bc3acc47b..615c2411851 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -8116,15 +8116,16 @@ void InsaneRebel2::playCinematic(const char *filename) {
 	// Play a cinematic/cutscene video with proper intro mode setup
 	// This helper ensures:
 	// 1. Handler is reset to 0 (no HUD, no shooting)
-	// 2. Video flags are set to 0x20 (cinematic mode)
+	// 2. Video flags are set to 0x28 (cinematic with buffer preserve)
 	//
-	// Original: DAT_0047ee84 is only set by IACT opcode 6 during gameplay videos
-	// Cinematics don't have opcode 6, so handler stays 0
+	// Original: All video wrapper functions (FUN_00417168, FUN_004171c5,
+	// FUN_00417ab2, FUN_00417327) add | 8 to the base flags before calling
+	// FUN_0041f4d0, so the 0x08 bit (preserve buffer) is always set.
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during cinematics
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);  // Cinematic mode
+	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve (0x20 | 0x08)
 	splayer->play(filename, 12);
 }
 
@@ -8182,7 +8183,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		// First play the cutscene
 		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing cutscene %s", filename.c_str());
-		splayer->setCurVideoFlags(0x20);
+		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
@@ -8196,14 +8197,17 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
 		// Post segment 1
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
 		filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x20);
+		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
 		// Part 2
+		_rebelHandler = 8;
 		splayer->setCurVideoFlags(0x28);
 		filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
@@ -8212,14 +8216,17 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
 		// Post segment 2
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
 		filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x20);
+		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
 		// Part 3
+		_rebelHandler = 8;
 		splayer->setCurVideoFlags(0x28);
 		filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
@@ -8237,14 +8244,16 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
 		// Post segment
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
 		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x20);
+		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
-		// Phase 2
+		// Phase 2 — handler will be re-set by IACT opcode 6
 		splayer->setCurVideoFlags(0x28);
 		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
@@ -8256,7 +8265,7 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		// Level 4: Has cutscene, then single gameplay
 		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing cutscene %s", filename.c_str());
-		splayer->setCurVideoFlags(0x20);
+		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
 		if (_vm->shouldQuit()) return false;
@@ -8283,14 +8292,16 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
 		// Post segment
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
 		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x20);
+		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
 		if (_vm->shouldQuit() || _playerShield == 0) return false;
 
-		// Phase 2
+		// Phase 2 — handler will be re-set by IACT opcode 6
 		splayer->setCurVideoFlags(0x28);
 		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
 		debug("Rebel2: Playing %s", filename.c_str());
@@ -8324,7 +8335,8 @@ void InsaneRebel2::playLevelEnd(int levelId) {
 	debug("Rebel2: Playing level %d end: %s", levelId, filename.c_str());
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);  // Cinematic mode
+	// Original: FUN_00417327 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play(filename.c_str(), 12);
 }
 
@@ -8350,7 +8362,8 @@ void InsaneRebel2::playLevelDeath(int levelId) {
 	debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play(filename.c_str(), 12);
 }
 
@@ -8368,7 +8381,8 @@ void InsaneRebel2::playLevelRetry(int levelId) {
 	debug("Rebel2: Playing level %d retry: %s", levelId, filename.c_str());
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play(filename.c_str(), 12);
 }
 
@@ -8386,7 +8400,8 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
 	debug("Rebel2: Playing level %d game over: %s", levelId, filename.c_str());
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417ab2 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play(filename.c_str(), 12);
 }
 
@@ -8546,7 +8561,8 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 	debug("Rebel2: Playing death video: %s (phase=%d, frame=%d)", filename.c_str(), phase, frame);
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play(filename.c_str(), 12);
 }
 
@@ -8571,7 +8587,8 @@ void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
 	debug("Rebel2: Playing retry video: %s (phase=%d)", filename.c_str(), phase);
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play(filename.c_str(), 12);
 }
 
@@ -8842,7 +8859,12 @@ int InsaneRebel2::runLevel2() {
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		// Post segment 1 (02PST1.SAN)
-		splayer->setCurVideoFlags(0x20);
+		// Original: FUN_00417168("02PST1.SAN", 0x20) → flags = 0x20 | 0x08 = 0x28
+		// FUN_00417168 adds | 8 to preserve the screen buffer between gameplay and transition
+		// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
+		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/02PST1.SAN", 12);
 		if (_vm->shouldQuit()) return kLevelQuit;
 
@@ -8860,6 +8882,9 @@ int InsaneRebel2::runLevel2() {
 		// Initialize Phase 2 budget
 		budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
 
+		// Restore handler for gameplay — will be confirmed by IACT opcode 6
+		_rebelHandler = 8;
+
 		// Play A.SAN (background loader)
 		debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
 		splayer->setCurVideoFlags(0x28);
@@ -8910,7 +8935,11 @@ int InsaneRebel2::runLevel2() {
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		// Post segment 2 (02PST2.SAN)
-		splayer->setCurVideoFlags(0x20);
+		// Original: FUN_00417168("02PST2.SAN", 0x20) → flags = 0x20 | 0x08 = 0x28
+		// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
+		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/02PST2.SAN", 12);
 		if (_vm->shouldQuit()) return kLevelQuit;
 
@@ -8929,6 +8958,9 @@ int InsaneRebel2::runLevel2() {
 		// Initialize Phase 3 budget
 		budget = kLevel2BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
 
+		// Restore handler for gameplay — will be confirmed by IACT opcode 6
+		_rebelHandler = 8;
+
 		// Play A.SAN (background loader)
 		debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
 		splayer->setCurVideoFlags(0x28);
@@ -9091,7 +9123,11 @@ int InsaneRebel2::runLevel3() {
 	if (_vm->shouldQuit()) return kLevelQuit;
 
 	// Post segment 1 (03POST1.SAN)
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;
+	splayer->setCurVideoFlags(0x28);
 	splayer->play("LEV03/03POST1.SAN", 12);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
@@ -9150,7 +9186,8 @@ int InsaneRebel2::runLevel4() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
 	// Play cutscene (04CUT.SAN)
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	splayer->setCurVideoFlags(0x28);
 	splayer->play("LEV04/04CUT.SAN", 12);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
@@ -9311,7 +9348,11 @@ int InsaneRebel2::runLevel6() {
 	if (_vm->shouldQuit()) return kLevelQuit;
 
 	// Post segment 1 (06POST1.SAN)
-	splayer->setCurVideoFlags(0x20);
+	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
+	// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;
+	splayer->setCurVideoFlags(0x28);
 	splayer->play("LEV06/06POST1.SAN", 12);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 806e14e4435..314c2748953 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1369,6 +1369,16 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 		if (!_skipPalette) {
 			byte *palettePtr = &headerContent[6];
 			memcpy(_pal, palettePtr, sizeof(_pal));
+
+			// Reset XPAL delta palette state from the new base palette.
+			// Without this, stale _deltaPal/_shiftedDeltaPal values from a
+			// previous video leak into the new one, corrupting the palette
+			// when XPAL command 256 (apply deltas) is encountered.
+			for (int j = 0; j < 768; ++j) {
+				_shiftedDeltaPal[j] = _pal[j] << 7;
+			}
+			memset(_deltaPal, 0, sizeof(_deltaPal));
+
 			setDirtyColors(0, 255);
 		}
 


Commit: 9130d5bd4b1753dd4230b0f92601acfd963b5ff4
    https://github.com/scummvm/scummvm/commit/9130d5bd4b1753dd4230b0f92601acfd963b5ff4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:08+02:00

Commit Message:
SCUMM: RA2: Fix SKIP and opcode 2 bit handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 615c2411851..d4e9638ffce 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -933,16 +933,19 @@ void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 
 	if (!par2) {
 		// Single ID mode: skip next chunk if this object's bit is set (disabled)
-		if (isBitSet(par1)) {
+		bool bit1 = isBitSet(par1);
+		if (bit1) {
 			_player->_skipNext = true;
-			debug("Rebel2 SKIP: ID=%d bit is set, skipping next chunk", par1);
 		}
+		debug("Rebel2 SKIP: single ID=%d bit=%d skip=%d frame=%d", par1, bit1 ? 1 : 0, _player->_skipNext ? 1 : 0, _player->_frame);
 	} else {
 		// Dual ID mode: skip if bits are different (XOR logic)
-		if (isBitSet(par1) != isBitSet(par2)) {
+		bool bit1 = isBitSet(par1);
+		bool bit2 = isBitSet(par2);
+		if (bit1 != bit2) {
 			_player->_skipNext = true;
-			debug("Rebel2 SKIP: ID=%d and ID=%d bits differ, skipping next chunk", par1, par2);
 		}
+		debug("Rebel2 SKIP: dual ID1=%d(bit=%d) ID2=%d(bit=%d) skip=%d frame=%d", par1, bit1 ? 1 : 0, par2, bit2 ? 1 : 0, _player->_skipNext ? 1 : 0, _player->_frame);
 	}
 }
 
@@ -1184,18 +1187,26 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 		// when childId <= 0. The original game's setBit(0)/clearBit(0) affects ALL bits,
 		// which would disable/enable all enemies at once - not the intended linking behavior.
 		if (parentId >= 1 && parentId < 512 && childId >= 1 && childId < 512) {
-			// Shift links
+			// Shift links (original: 4 link slots at DAT_0045797c/817c/897c/917c)
 			_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
 			_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
 			_rebelLinks[parentId][0] = childId;
 
-			// DO NOT modify bits here! All bits are reset to CLEAR by clearBit(0) at level start.
-			// SKIP chunks use XOR logic: skip when bits DIFFER.
-			// - Both bits CLEAR (alive) -> same -> don't skip -> enemy visible
-			// - Parent SET (dead), child SET (disabled) -> same -> skip -> enemy hidden
-			// The bits are only modified when enemies are actually destroyed (setBit/clearBit
-			// calls in processMouse() enemy destruction handling).
-			debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0)", childId, parentId);
+			// Mirror parent's bit state to child (INVERTED):
+			// - Parent alive (bit clear) → setBit(child) → child hidden
+			// - Parent dead (bit set) → clearBit(child) → child shown
+			// From FUN_0041CADB case 0, par3==4:
+			//   bVar3 = FUN_00423970(parentId);
+			//   if (bVar3 == 0) setBit(childId); else clearBit(childId);
+			// This ensures linked children (explosion/death sprites) are hidden
+			// while the parent is alive, and revealed when the parent is destroyed.
+			if (!isBitSet(parentId)) {
+				setBit(childId);
+				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child DISABLED (parent alive)", childId, parentId);
+			} else {
+				clearBit(childId);
+				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child ENABLED (parent dead)", childId, parentId);
+			}
 		} else {
 			debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
 		}
@@ -1209,15 +1220,22 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 		if (targetId < 1 || targetId >= 0x200)
 			return;
 
-		// Handler 8/25: FUN_401234 case 0 / FUN_41E7C2 par3==1, par4!=0
-		// "If enemy type par4 has been killed (bit set in wave state), disable targetId"
-		// This conditionally hides entities based on which enemy groups have been destroyed.
-		// Both Handler 8 (on-foot) and Handler 25 (FPS/cover) use DAT_0047ab98 for wave state.
+		// Handler 8/25: FUN_401234 case 0 / FUN_0041CADB case 0 par3==1
+		// From original FUN_0041CADB:
+		//   if (par4 == 100) clearBit(body0);  // Force enable
+		//   else { bitMask = 1 << (par4 & 0x1f); if (waveState & bitMask) setBit(body0); }
 		if ((_rebelHandler == 8 || _rebelHandler == 25) && value != 0) {
-			int bitMask = 1 << (value & 0x1f);
-			if ((_rebelWaveState & bitMask) != 0) {
-				setBit(targetId);
-				debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
+			if (value == 100) {
+				// par4==100: Force enable the target (original: FUN_00423a00)
+				clearBit(targetId);
+				debug("Rebel2 Opcode2 (H%d): Force ENABLE target=%d (par4=100)", _rebelHandler, targetId);
+			} else {
+				// Check wave state: if enemy type has been killed, disable target
+				int bitMask = 1 << (value & 0x1f);
+				if ((_rebelWaveState & bitMask) != 0) {
+					setBit(targetId);
+					debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
+				}
 			}
 			return;
 		}
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 314c2748953..16edd4fadc7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -338,6 +338,7 @@ void SmushPlayer::init(int32 speed) {
 	_frame = 0;
 	_speed = speed;
 	_endOfFile = false;
+	_storeFrame = false;
 
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
@@ -438,18 +439,20 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
 	assert(subSize >= 6);
 
+	// Read FTCH data: 2 unknown + 2 X offset + 2 Y offset
+	// Original (FUN_00423A50 lines 91-103): reads X/Y from chunk data and
+	// re-renders stored FOBJ at position (X + param_3 + DAT_00482c1c, Y + param_4 + DAT_00482c20)
+	int16 ftchUnknown = b.readSint16LE();
+	int16 ftchX = b.readSint16LE();
+	int16 ftchY = b.readSint16LE();
+
+	debug("SmushPlayer::handleFetch: frame=%d unknown=%d x=%d y=%d",
+		_frame, ftchUnknown, ftchX, ftchY);
+
 	// For RA2 Handler 25, skip FTCH because the frame buffer only contains the
 	// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
 	// immediately in frame 0. Restoring would erase those overlays and make
 	// enemies invisible since they draw on top of the erased areas.
-	//
-	// Handler 25 (Level 2 speeder bike): Corridor overlay is drawn in procPreRendering
-	// BEFORE FOBJs. FTCH would erase the overlay, making enemies draw on wrong background.
-	//
-	// Handler 8 (Level 2 on-foot): FTCH is NOT skipped here. FTCH restores the clean
-	// background each frame, which properly erases old enemy sprite positions before
-	// new FOBJs draw updated positions. procPreRendering also restores _level2Background,
-	// but FTCH provides the authoritative clean slate from frame 0's stored background.
 	if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
 		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
 		int handler = rebel2->getHandler();


Commit: 899667bc9541ebfa179253aa61ec8430f1ad5878
    https://github.com/scummvm/scummvm/commit/899667bc9541ebfa179253aa61ec8430f1ad5878
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:08+02:00

Commit Message:
SCUMM: RA2: Clear IACT bits for level 2 waves

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index d4e9638ffce..bdb3c923125 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1469,6 +1469,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 		// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
 		if (_shipLevelMode != 0 && par4 == 1) {
+			// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
+			clearBit(0);
 			// Clear link tables
 			for (int i = 0; i < 512; i++) {
 				_rebelLinks[i][0] = 0;
@@ -1872,6 +1874,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
 		if (par5 == 1) {
 			_rebelStatusBarSprite = 5;
+			// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
+			// at IACT callback registration time. Each new wave video starts with
+			// a clean bit table so enemy IDs reused across videos work correctly.
+			clearBit(0);
 			// Reset link tables (DAT_0045797c through DAT_0045917c)
 			for (int i = 0; i < 512; i++) {
 				_rebelLinks[i][0] = 0;
@@ -1881,9 +1887,6 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			// Reset wave state to accumulated phase state (same as Handler 8)
 			// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
 			_rebelWaveState = _rebelPhaseState;
-			// NOTE: autopilot and damageLevel are NOT reset here (verified against
-			// FUN_41CADB case 4 lines 114-121). They persist across video boundaries
-			// so the player keeps their cover state when transitioning between waves.
 			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
 				_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
 		}
@@ -2078,8 +2081,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
 		debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
 
+		// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
+		clearBit(0);
+
 		// Clear link tables (DAT_0045797c through DAT_0045917c)
-		// These are 4 tables of 0x400 (1024) shorts each
 		for (int i = 0; i < 512; i++) {
 			_rebelLinks[i][0] = 0;
 			_rebelLinks[i][1] = 0;


Commit: 6af2ff75d6c63cfee3aad70125122ae26202f584
    https://github.com/scummvm/scummvm/commit/6af2ff75d6c63cfee3aad70125122ae26202f584
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:08+02:00

Commit Message:
SCUMM: RA2: Apply FOBJ offsets to enemy overlays

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bdb3c923125..abcb7b2c2bc 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -5627,13 +5627,19 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 	if (_difficulty >= 2)
 		return;
 
+	// FOBJ sprites are rendered with _fobjOffsetX/Y applied (set from _rebelViewOffsetX/Y
+	// for Handler 25). Brackets must use the same offset so they align with the sprites.
+	int fobjOffX = _player ? _player->_fobjOffsetX : 0;
+	int fobjOffY = _player ? _player->_fobjOffsetY : 0;
+
 	Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
 
 	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
 		if (it->destroyed || !it->active || isBitSet(it->id))
 			continue;
 
-		Common::Rect r = it->rect;
+		Common::Rect r(it->rect.left + fobjOffX, it->rect.top + fobjOffY,
+		               it->rect.right + fobjOffX, it->rect.bottom + fobjOffY);
 		if (r.right <= viewRect.left || r.left >= viewRect.right ||
 		    r.bottom <= viewRect.top || r.top >= viewRect.bottom)
 			continue;


Commit: 6da68ffd2bd2c947b51c109436393188658d4526
    https://github.com/scummvm/scummvm/commit/6da68ffd2bd2c947b51c109436393188658d4526
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00

Commit Message:
SCUMM: RA2: Implement additional level handlers

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index abcb7b2c2bc..5b7f98334f1 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -8382,7 +8382,7 @@ void InsaneRebel2::playLevelDeath(int levelId) {
 
 	// Most levels have DIE_A, some have just DIE
 	Common::String filename;
-	if (levelId == 2 || levelId == 4) {
+	if (levelId == 2 || levelId == 4 || levelId == 10 || levelId == 12 || levelId == 14) {
 		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
 	} else {
 		filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
@@ -8495,8 +8495,25 @@ int InsaneRebel2::runLevel(int levelId) {
 		return runLevel5();
 	case 6:
 		return runLevel6();
+	case 7:
+		return runLevel7();
+	case 8:
+		return runLevel8();
+	case 9:
+		return runLevel9();
+	case 10:
+		return runLevel10();
+	case 11:
+		return runLevel11();
+	case 12:
+		return runLevel12();
+	case 13:
+		return runLevel13();
+	case 14:
+		return runLevel14();
+	case 15:
+		return runLevel15();
 	default:
-		// Levels 7-15: Use generic handler (similar to Level 1)
 		return runLevel1();
 	}
 }
@@ -8553,17 +8570,87 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 		return (getRandomVariant(2) == 0) ? "A" : "B";
 
 	case 6:
-		// Level 6: Similar to Level 3 (two phases with frame-based selection)
+		// Level 6 (FUN_004190D6): Phase-based with detailed frame selection
 		if (phase == 1) {
-			if (frame < 300) return "A";
-			if (frame < 600) return "B";
-			return "C";
+			// DAT_0047a7f8 == 5 (phase 1)
+			if (frame < 0x4e) return "D";
+			if (frame < 0xe0) return "A";
+			if (frame < 0x122) return "D";
+			if (frame < 0x1b4) return "B";
+			if (frame < 499) return "D";
+			if (frame < 0x286) return "C";
+			return "D";
 		} else {
-			if (frame < 400) return "A";
-			if (frame < 800) return "B";
-			return "C";
+			// DAT_0047a7f8 == 6 (phase 2)
+			if (frame < 0xcc) return "E";
+			if (frame < 0xfe) return "G";
+			if (frame < 0x122) return "E";
+			if (frame < 0x149) return "G";
+			if (frame < 0x166) return "F";
+			if (frame < 0x174) return "E";
+			if (frame < 0x19f) return "F";
+			if (frame < 0x1b2) return "G";
+			if (frame < 0x1c8) return "F";
+			if (frame < 0x207) return "E";
+			if (frame < 0x217) return "F";
+			if (frame < 0x23b) return "G";
+			if (frame < 0x25b) return "F";
+			if (frame < 0x285) return "E";
+			return "G";
 		}
 
+	case 7:
+		// Level 7 (FUN_0041974C): Based on DAT_0047ab8c (fork state)
+		// DAT_0047ab8c != 0 → DIE_B; == 0 → DIE_A
+		// We use phase as a proxy (phase 2 = reached fork)
+		return (phase >= 2) ? "B" : "A";
+
+	case 8:
+		// Level 8 (FUN_00419976): Random A or B
+		return (getRandomVariant(2) == 0) ? "A" : "B";
+
+	case 9:
+		// Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
+		// 0→A, 1→C, else→B. Use phase as proxy.
+		return "A";  // Default; exact tracking of DAT_0047ab94 deferred
+
+	case 10:
+		// Level 10 (FUN_00419E0A): Single death video (no variant suffix)
+		return "";
+
+	case 11:
+		// Level 11 (FUN_0041A00C): Phase-based death videos
+		// Phase 1 → DIE_A, Phase 2 → DIE_B, Phase 3 → DIE_C
+		if (phase <= 1) return "A";
+		if (phase == 2) return "B";
+		return "C";
+
+	case 12:
+		// Level 12 (FUN_0041AA14): Single death video (no variants)
+		return "";
+
+	case 13:
+		// Level 13 (FUN_0041B3E1): Frame-based
+		if (frame < 0x1c2) return "A";
+		if (frame < 0x302) return "B";
+		if (frame < 0x4ec) return "C";
+		if (frame < 0x5b4) return "B";
+		return "D";
+
+	case 14:
+		// Level 14 (FUN_0041B6E8): Single death video (no variant suffix)
+		return "";
+
+	case 15:
+		// Level 15 (FUN_0041B8D7): Frame-based with many thresholds
+		if (frame < 0x21e) return "A";
+		if (frame < 0x2f9) return "B";
+		if (frame < 0x3e5) return "C";
+		if (frame < 0x4a0) return "B";
+		if (frame < 0x588) return "C";
+		if (frame < 0x65e) return "B";
+		return "D";
+
 	default:
 		return "A";
 	}
@@ -8605,8 +8692,8 @@ void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String filename;
 
-	if (levelId == 3 && phase == 2) {
-		// Level 3 phase 2 has its own retry video: 03RETRYB.SAN
+	if ((levelId == 3 || levelId == 6) && phase == 2) {
+		// Level 3/6 phase 2 has its own retry video: xxRETRYB.SAN
 		filename = Common::String::format("%s/%sRETRYB.SAN", dir.c_str(), prefix.c_str());
 	} else {
 		// Standard retry video
@@ -9330,99 +9417,1275 @@ int InsaneRebel2::runLevel5() {
 // =============================================================================
 
 int InsaneRebel2::runLevel6() {
+	// FUN_004190d6 — Mos Eisley: two-phase on-foot (Handler 8)
+	// Phase 1 (levelId=5): 06PLAY1.SAN, mid-switch to 06PLAY1B.SAN at frame 0x2a8
+	// Phase 2 (levelId=6): 06PLAY2.SAN, play until near-end
+	// Original structure: outer do-while for phase 1 retries, inner while(true) for
+	// phase 2 retries + death handling. Phase 1 death breaks inner → RETRY at outer bottom.
+	// Phase 2 death → RETRYB → re-enters phase 2 within inner loop.
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int phase1Score = 0;
+	int totalScore = 0;
 
 	// Play level beginning cinematic (06BEG.SAN)
+	// Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
 	playLevelBegin(6);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
-	// ===== PHASE 1 retry loop =====
+	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040 — handler init done by IACT opcode 6
+
+	// Outer retry loop — restarts phase 1 on phase 1 death
+	while (!_vm->shouldQuit()) {
+		// FUN_00407d10 — reset shot/hit counters
+		clearBit(0);
+
+		// DAT_0047ab9c = 0xffffffff — init phase state
+		_rebelPhaseState = 0xffffffff;
+
+		// ===== PHASE 1 =====
+		_rebelLevelType = 5;  // DAT_0047a7f8 = 5
+		_currentPhase = 1;
+
+		debug("Rebel2: Level 6 Phase 1");
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV06/06PLAY1.SAN", 12);
+		// TODO: Mid-level switch at frame 0x2a8 to 06PLAY1B.SAN (flags 0x468)
+		// + score checkpoint (FUN_00407f55) — needs per-frame callback
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield <= 0) {
+			// Died in phase 1
+			debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
+			playLevelDeathVariant(6, 1, _deathFrame);
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			_playerLives--;
+			if (_playerLives <= 0) {
+				playLevelGameOver(6);
+				return kLevelGameOver;
+			}
+
+			// Phase 1 retry (06RETRY.SAN) → restart outer loop
+			playLevelRetryVariant(6, 1);
+			if (_vm->shouldQuit()) return kLevelQuit;
+			continue;
+		}
+
+		// Phase 1 survived — save score, play POST1
+		totalScore = _playerScore;  // local_8 = DAT_0047ab84
+
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV06/06POST1.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== PHASE 2 retry loop (inner while(true) in original) =====
+		while (!_vm->shouldQuit()) {
+			_rebelLevelType = 6;  // DAT_0047a7f8 = 6
+			_currentPhase = 2;
+			clearBit(0);  // FUN_00407d10
+
+			debug("Rebel2: Level 6 Phase 2");
+			splayer->setCurVideoFlags(0x28);
+			splayer->play("LEV06/06PLAY2.SAN", 12);
+			_deathFrame = splayer->_frame;
+
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			// Accumulate score: local_8 = DAT_0047ab84 + local_8
+			totalScore += _playerScore;
+
+			if (_playerShield > 0) {
+				// Level completed!
+				debug("Rebel2: Level 6 completed!");
+				playLevelEnd(6);
+				_levelUnlocked[6] = true;
+				return kLevelNextLevel;
+			}
+
+			// Died in phase 2
+			debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
+			playLevelDeathVariant(6, 2, _deathFrame);
+			if (_vm->shouldQuit()) return kLevelQuit;
+
+			_playerLives--;
+			if (_playerLives <= 0) {
+				playLevelGameOver(6);
+				return kLevelGameOver;
+			}
+
+			// Phase 2 retry (06RETRYB.SAN) → re-enter phase 2
+			playLevelRetryVariant(6, 2);
+			if (_vm->shouldQuit()) return kLevelQuit;
+		}
+
+		break;  // Should only reach here on shouldQuit
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 7 Handler - FUN_0041974C
+// "TIE Training" - Canyon flight with fork at frame 1592
+// Single gameplay phase (07PLAY.SAN), optional second segment (07PLAYB.SAN)
+// Death: DAT_0047ab8c-based (fork reached → DIE_B, not reached → DIE_A)
+// =============================================================================
+
+int InsaneRebel2::runLevel7() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	bool reachedFork = false;  // DAT_0047ab8c equivalent — tracks if 07PLAYB was played
+
+	// Play cutscene (07CUT.SAN)
+	playCinematic("LEV07/07CUT.SAN");
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (07BEG.SAN)
+	playLevelBegin(7);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
+	clearBit(0);
+
 	while (!_vm->shouldQuit()) {
 		_playerShield = 255;
 		_playerDamage = 0;
-		_currentPhase = 1;
+		_deathFrame = 0;
+		reachedFork = false;
 
-		// Reset bit table before gameplay starts
 		clearBit(0);
 
-		// Play phase 1 gameplay (06PLAY1.SAN)
-		debug("Rebel2: Level 6 Phase 1");
+		// Play gameplay (07PLAY.SAN)
+		// Original: FUN_0041f4d0("07PLAY.SAN", 0x28, -1, -1, 0)
+		// At frame 0x638 (1592), if DAT_0047ab8c != 0: play 07PLAYB.SAN (0x468)
+		// TODO: Mid-level fork at frame 1592 requires per-frame callback.
+		// For now, play the main video. The fork video (07PLAYB) would be triggered
+		// by IACT callbacks setting a state flag during gameplay.
 		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV06/06PLAY1.SAN", 12);
+		splayer->play("LEV07/07PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		if (_playerShield > 0) {
-			phase1Score = _playerScore;
-			break;
+			debug("Rebel2: Level 7 completed!");
+			playLevelEnd(7);
+			_levelUnlocked[7] = true;
+			return kLevelNextLevel;
 		}
 
-		// Died in phase 1
-		debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
-		playLevelDeathVariant(6, 1, _deathFrame);
+		// Death video: DIE_B if fork reached, DIE_A if not
+		// Original: s_LEV07_07DIE_B + ((DAT_0047ab8c != 0) - 1 & 0x14)
+		debug("Rebel2: Level 7 death at frame %d, fork=%d", _deathFrame, reachedFork);
+		if (reachedFork) {
+			playCinematic("LEV07/07DIE_B.SAN");
+		} else {
+			playCinematic("LEV07/07DIE_A.SAN");
+		}
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
-			playLevelGameOver(6);
+			playLevelGameOver(7);
 			return kLevelGameOver;
 		}
 
-		playLevelRetryVariant(6, 1);
+		playCinematic("LEV07/07RETRY.SAN");
 		if (_vm->shouldQuit()) return kLevelQuit;
 	}
 
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 8 Handler - FUN_00419976
+// "Flight to Imdaar" - Y-Wing space battle (single phase)
+// No cutscene (starts with BEG). flags=0x08 for gameplay.
+// Death: random A or B
+// =============================================================================
+
+int InsaneRebel2::runLevel8() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// No cutscene — starts directly with BEG
+	// Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
+	playLevelBegin(8);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
-	// Post segment 1 (06POST1.SAN)
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;
-	splayer->setCurVideoFlags(0x28);
-	splayer->play("LEV06/06POST1.SAN", 12);
+	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+	clearBit(0);
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_deathFrame = 0;
+
+		clearBit(0);
+
+		// Play gameplay (08PLAY.SAN)
+		// Original: FUN_0041f4d0("08PLAY.SAN", 8, -1, -1, 0) — note flags=0x08
+		splayer->setCurVideoFlags(0x08);
+		splayer->play("LEV08/08PLAY.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 8 completed! accuracy=%d%%", accuracy);
+			playLevelEnd(8);
+			_levelUnlocked[8] = true;
+			return kLevelNextLevel;
+		}
+
+		// Death: random A or B
+		// Original: random(2) → A or B via string pointer arithmetic
+		debug("Rebel2: Level 8 death at frame %d", _deathFrame);
+		playLevelDeathVariant(8, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(8);
+			return kLevelGameOver;
+		}
+
+		playCinematic("LEV08/08RETRY.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 9 Handler - FUN_00419B86
+// "The Mine Field" - Navigate through force fields (single phase)
+// No cutscene. Initial phaseState = 0xfffffffe (all bits set except bit 0).
+// Mid-events at frames 0x19f (415) and 0x352 (850): FUN_00407f55 (score checkpoint)
+// Death: DAT_0047ab94-based (0→A, 1→C, else→B)
+// =============================================================================
+
+int InsaneRebel2::runLevel9() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// No cutscene — starts directly with BEG
+	// Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
+	playLevelBegin(9);
 	if (_vm->shouldQuit()) return kLevelQuit;
 
-	// ===== PHASE 2 retry loop =====
-	_currentPhase = 2;
+	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+	clearBit(0);
 
 	while (!_vm->shouldQuit()) {
 		_playerShield = 255;
 		_playerDamage = 0;
+		_deathFrame = 0;
 
-		// Reset bit table before gameplay starts
 		clearBit(0);
 
-		// Play phase 2 gameplay (06PLAY2.SAN)
-		debug("Rebel2: Level 6 Phase 2");
+		// Original: DAT_0047ab9c = 0xfffffffe (initial phase state — all bits except 0)
+		_rebelPhaseState = 0xfffffffe;
+
+		// Play gameplay (09PLAY.SAN)
+		// Original: FUN_0041f4d0("09PLAY.SAN", 0x28, -1, -1, 0)
+		// Mid-events at frames 415 and 850: FUN_00407f55 (score save)
+		// These are handled implicitly — the IACT callbacks manage scoring.
 		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV06/06PLAY2.SAN", 12);
+		splayer->play("LEV09/09PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		if (_playerShield > 0) {
-			// Level completed!
-			debug("Rebel2: Level 6 completed!");
-			playLevelEnd(6);
-			_levelUnlocked[6] = true;  // Unlock level 7
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 9 completed! accuracy=%d%%", accuracy);
+			playLevelEnd(9);
+			_levelUnlocked[9] = true;
+			return kLevelNextLevel;
+		}
+
+		// Death: based on DAT_0047ab94 (death cause tracking)
+		// Original: 0→DIE_A, 1→DIE_C, else→DIE_B
+		debug("Rebel2: Level 9 death at frame %d", _deathFrame);
+		playLevelDeathVariant(9, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(9);
+			return kLevelGameOver;
+		}
+
+		playCinematic("LEV09/09RETRY.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 10 Handler - FUN_00419E0A
+// "Speeder Bikes" - Forest speeder chase (single phase)
+// Has cutscene. Single death video (10DIE.SAN, no variants).
+// Original plays DIE then RETRY in sequence (no separate check).
+// =============================================================================
+
+int InsaneRebel2::runLevel10() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Play cutscene (10CUT.SAN)
+	playCinematic("LEV10/10CUT.SAN");
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (10BEG.SAN)
+	// Original: FUN_004171c5("10BEG.SAN", 0x20, 0xb3, 0xa0, 10, 2, 0x46)
+	playLevelBegin(10);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
+	clearBit(0);
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_deathFrame = 0;
+
+		clearBit(0);
+
+		// Play gameplay (10PLAY.SAN)
+		// Original: FUN_0041f4d0("10PLAY.SAN", 0x28, -1, -1, 0)
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV10/10PLAY.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 10 completed! accuracy=%d%%", accuracy);
+			playLevelEnd(10);
+			_levelUnlocked[10] = true;
+			return kLevelNextLevel;
+		}
+
+		// Death + Retry: original plays both in sequence
+		// Original: lives--, if 0 break to game over, else DIE+RETRY
+		debug("Rebel2: Level 10 death at frame %d", _deathFrame);
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(10);
+			return kLevelGameOver;
+		}
+
+		playCinematic("LEV10/10DIE.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+		playCinematic("LEV10/10RETRY.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 11 Handler - FUN_0041A00C
+// "Inside the Terror" - Three phases + bridge puzzle (Handler 8, on-foot)
+//
+// Phase 1: P1/11P01_X (A,B,C,D) - behind barrels, mask 0x0e
+// Phase 2: P2/11P02_X (A,B,C,D) - walls on right, mask 0x0e, flags=3
+// Phase 3 first half: P3/11P03_X (A-F) - bridge puzzle, mask 0x7e
+//   Exit when (phaseState & 0x70) == 0x70
+// POST3/POST3B/POST3C bridge cinematics
+// Phase 3 second half: P3/11P03_X (G-L) - after bridge, mask 0x0e
+// =============================================================================
+
+int InsaneRebel2::runLevel11() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	int totalKills = 0;
+	int totalMisses = 0;
+	int prevPhaseState = 0;
+
+	// Kill credit budget bases per phase (from level data table DAT_0047e0e8)
+	static const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
+
+	// Play cutscene (11CUT.SAN)
+	playCinematic("LEV11/11CUT.SAN");
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (11BEG.SAN)
+	playLevelBegin(11);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
+	clearBit(0);
+
+	// Main gameplay retry loop (restarts from Phase 1 on death)
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+		totalKills = 0;
+		totalMisses = 0;
+		prevPhaseState = 0;
+
+		// Reset Handler 8 cover state
+		_rebelAutopilot = 0;
+		_rebelDamageLevel = 0;
+		_rebelControlMode = 0;
+
+		// FUN_0041c7d0: Reset per-attempt state
+		_enemies.clear();
+		for (int i = 0; i < 512; i++) {
+			_rebelLinks[i][0] = 0;
+			_rebelLinks[i][1] = 0;
+			_rebelLinks[i][2] = 0;
+		}
+
+		// ===== PHASE 1: P1/11P01_X.SAN =====
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+
+		int16 budget = kLevel11BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
+
+		// Play A.SAN (background loader)
+		debug("Rebel2: Level 11 Phase 1 - playing 11P01_A.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV11/P1/11P01_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+
+			// Phase 1 wave loop: random(2) | (waveSelect & 8) → variants
+			// 0→D, 1→C, 8→B, 9→A
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				// Bonus sound check
+				if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
+					// FUN_00411931 bonus sound — not yet implemented
+				}
+				prevPhaseState = _rebelPhaseState;
+
+				int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 8);
+				const char *filename;
+				switch (sel) {
+				case 1:  filename = "LEV11/P1/11P01_C.SAN"; break;
+				case 8:  filename = "LEV11/P1/11P01_B.SAN"; break;
+				case 9:  filename = "LEV11/P1/11P01_A.SAN"; break;
+				default: filename = "LEV11/P1/11P01_D.SAN"; break;  // sel == 0
+				}
+
+				debug("Rebel2: Level 11 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level11_death_phase1;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// Post segment 1 (11POST1.SAN)
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV11/11POST1.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		totalKills += _rebelKillCounter;
+		totalMisses += _rebelHitCounter;
+
+		// ===== PHASE 2: P2/11P02_X.SAN =====
+		_currentPhase = 2;
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+		_enemies.clear();
+
+		budget = kLevel11BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
+		_rebelHandler = 8;
+
+		// Play A.SAN (background loader)
+		debug("Rebel2: Level 11 Phase 2 - playing 11P02_A.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV11/P2/11P02_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			// Phase 2: flags=3 (maxCredits=2, redistribution ON)
+			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 3);
+
+			// Random(4) for variant selection: A, B, C, D
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				int variant = _vm->_rnd.getRandomNumber(3);
+				const char *filename;
+				switch (variant) {
+				case 0:  filename = "LEV11/P2/11P02_A.SAN"; break;
+				case 1:  filename = "LEV11/P2/11P02_B.SAN"; break;
+				case 2:  filename = "LEV11/P2/11P02_C.SAN"; break;
+				default: filename = "LEV11/P2/11P02_D.SAN"; break;
+				}
+
+				debug("Rebel2: Level 11 Phase 2 wave - %s (state=0x%x)", filename, _rebelPhaseState);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level11_death_phase2;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// Post segment 2 (11POST2.SAN)
+		_rebelHandler = 0;
+		_rebelStatusBarSprite = 0;
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV11/11POST2.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		totalKills += _rebelKillCounter;
+		totalMisses += _rebelHitCounter;
+
+		// ===== PHASE 3 FIRST HALF: P3/11P03_X (A-F) =====
+		// Bridge puzzle — exit when (phaseState & 0x70) == 0x70
+		_currentPhase = 3;
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+		_enemies.clear();
+		prevPhaseState = 0;
+
+		budget = kLevel11BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
+		_rebelHandler = 8;
+
+		debug("Rebel2: Level 11 Phase 3 first half - playing 11P03_A.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV11/P3/11P03_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
+			int local_8 = 0;  // Tracks variant for randomization threshold
+
+			// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
+			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				// Bonus sound: (phaseState & 0xe) == 0xe and previous wasn't
+				if ((_rebelPhaseState & 0x0e) == 0x0e && (prevPhaseState & 0x0e) != 0x0e) {
+					// FUN_00411931 bonus sound — not yet implemented
+				}
+				prevPhaseState = _rebelPhaseState;
+
+				// Randomization: wider range for first few waves
+				if (local_8 < 3) {
+					local_8 = _vm->_rnd.getRandomNumber(7);  // 0-7
+				} else {
+					local_8 = _vm->_rnd.getRandomNumber(2);  // 0-2
+				}
+
+				const char *filename;
+				switch (local_8) {
+				case 0:  filename = "LEV11/P3/11P03_A.SAN"; break;
+				case 1:  filename = "LEV11/P3/11P03_B.SAN"; break;
+				case 2:  filename = "LEV11/P3/11P03_C.SAN"; break;
+				case 3:  filename = "LEV11/P3/11P03_D.SAN"; break;
+				case 4:  filename = "LEV11/P3/11P03_E.SAN"; break;
+				case 5:  filename = "LEV11/P3/11P03_F.SAN"; break;
+				case 6:  filename = "LEV11/P3/11P03_F.SAN"; break;  // duplicate F
+				default: filename = "LEV11/P3/11P03_E.SAN"; break;  // duplicate E
+				}
+
+				debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				// Threshold only for higher variants (original: (2 < local_8) - 1 & 0x14)
+				int16 threshold = (local_8 > 2) ? 0x14 : 0;
+				waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level11_death_phase3;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== PHASE 3 BRIDGE CINEMATICS =====
+		{
+			bool allBasicKilled = (_rebelPhaseState & 0x0e) >= 0x0e;
+			if (!allBasicKilled) {
+				// Normal bridge drop cinematic
+				playCinematic("LEV11/11POST3.SAN");
+				// Bonus checks (FUN_0042aa70) — deferred, play standard path
+				// Original checks 0x77 and 0x62 for special POST3C cinematic
+			} else {
+				// All enemy types killed — bridge dropped successfully
+				playCinematic("LEV11/11POST3B.SAN");
+			}
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
+		// Reset shots/explosions (FUN_0041ca6a equivalent)
+		for (int i = 0; i < 5; i++) {
+			_explosions[i].active = false;
+		}
+		_enemies.clear();
+
+		// Preserve only bits 1-3 of phase state (original: DAT_0047ab9c &= 0xe)
+		_rebelPhaseState &= 0x0e;
+		_rebelWaveState &= 0x0e;
+
+		_rebelHandler = 8;
+
+		budget = kLevel11BudgetBase[3] + _vm->_rnd.getRandomNumber(2);
+
+		// Play G.SAN (background loader for second half)
+		debug("Rebel2: Level 11 Phase 3 second half - playing 11P03_G.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV11/P3/11P03_G.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// Only enter wave loop if not all basic types killed already
+		if ((_rebelPhaseState & 0x0e) < 0x0e) {
+			int local_8 = 0;
+			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				// Wider randomization for first few waves
+				if (local_8 < 4) {
+					local_8 = _vm->_rnd.getRandomNumber(8);  // 0-8
+				} else {
+					local_8 = _vm->_rnd.getRandomNumber(2);  // 0-2
+				}
+
+				const char *filename;
+				switch (local_8) {
+				case 0:  filename = "LEV11/P3/11P03_G.SAN"; break;
+				case 1:  filename = "LEV11/P3/11P03_H.SAN"; break;
+				case 2:  filename = "LEV11/P3/11P03_I.SAN"; break;
+				case 3:  filename = "LEV11/P3/11P03_G.SAN"; break;  // G again
+				case 4:  filename = "LEV11/P3/11P03_H.SAN"; break;  // H again
+				case 5:  filename = "LEV11/P3/11P03_I.SAN"; break;  // I again
+				case 6:  filename = "LEV11/P3/11P03_J.SAN"; break;
+				case 7:  filename = "LEV11/P3/11P03_K.SAN"; break;
+				default: filename = "LEV11/P3/11P03_L.SAN"; break;
+				}
+
+				debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				int16 threshold = (local_8 > 2) ? 0x14 : 0;
+				waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
+			}
+		}
+
+		totalKills += _rebelKillCounter;
+
+		if (_playerDamage >= 255) goto level11_death_phase3;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== LEVEL COMPLETED =====
+		{
+			totalMisses += _rebelHitCounter;
+			int accuracy = 0;
+			int totalShots = totalKills + totalMisses;
+			if (totalKills > 0 && totalShots > 0) {
+				accuracy = (totalKills * 100) / totalShots;
+			}
+			debug("Rebel2: Level 11 completed! kills=%d misses=%d accuracy=%d%%",
+				totalKills, totalMisses, accuracy);
+		}
+
+		playLevelEnd(11);
+		_levelUnlocked[11] = true;  // Unlock level 12
+		return kLevelNextLevel;
+
+	level11_death_phase1:
+		debug("Rebel2: Level 11 Phase 1 death");
+		playCinematic("LEV11/11DIE_A.SAN");
+		goto level11_retry;
+
+	level11_death_phase2:
+		debug("Rebel2: Level 11 Phase 2 death");
+		playCinematic("LEV11/11DIE_B.SAN");
+		goto level11_retry;
+
+	level11_death_phase3:
+		debug("Rebel2: Level 11 Phase 3 death");
+		playCinematic("LEV11/11DIE_C.SAN");
+		goto level11_retry;
+
+	level11_retry:
+		if (_vm->shouldQuit()) return kLevelQuit;
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(11);
+			return kLevelGameOver;
+		}
+		playCinematic("LEV11/11RETRY.SAN");
+		_playerDamage = 0;
+		if (_vm->shouldQuit()) return kLevelQuit;
+		continue;  // Restart from Phase 1
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 12 Handler - FUN_0041AA14
+// "Sewers" - Four phases FPS corridor shooting (Handler 25)
+//
+// Each phase: init video (P05/P06/P07/P08) → first wave → wave loop
+// Phase 1: P1/12P01_X (A,B,C,D) mask=6
+// Phase 2: P2/12P02_X (A,B,C,D,E,F) mask=6
+// Phase 3: P3/12P03_X (A,B,C,D,F) mask=6
+// Phase 4: P4/12P04_X (A,B,C,D,E,F) mask=0xe
+// Closing: 12P09.SAN
+// =============================================================================
+
+int InsaneRebel2::runLevel12() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Kill credit budget bases per phase
+	static const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
+
+	// Play cutscene (12CUT.SAN)
+	playCinematic("LEV12/12CUT.SAN");
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (12BEG.SAN)
+	playLevelBegin(12);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_0041c7d0 + FUN_00407d10 + FUN_0040c040: Reset game state
+	clearBit(0);
+
+	// Main gameplay retry loop (restarts from Phase 1 on death)
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_currentPhase = 1;
+
+		// Reset state
+		_rebelAutopilot = 0;
+		_rebelDamageLevel = 0;
+		_rebelControlMode = 0;
+
+		_enemies.clear();
+		for (int i = 0; i < 512; i++) {
+			_rebelLinks[i][0] = 0;
+			_rebelLinks[i][1] = 0;
+			_rebelLinks[i][2] = 0;
+		}
+
+		// ===== PHASE 1: 12P05 → P1/12P01_X =====
+		// FUN_00401000: Reset at top of each retry
+		_rebelKillCounter = 0;
+		_rebelHitCounter = 0;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+
+		int16 budget = kLevel12BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
+
+		// Initialization video (12P05.SAN)
+		debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV12/12P05.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+		processWaveEnd(1, &budget, 0, 0);
+
+		// First wave (P1/12P01_A.SAN)
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/P1/12P01_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+
+			// Wave loop: random(2) | (waveSelect & 2) → 0:C, 1:D, 2:A, 3:B
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
+				const char *filename;
+				switch (sel) {
+				case 0:  filename = "LEV12/P1/12P01_C.SAN"; break;
+				case 1:  filename = "LEV12/P1/12P01_D.SAN"; break;
+				case 2:  filename = "LEV12/P1/12P01_A.SAN"; break;
+				default: filename = "LEV12/P1/12P01_B.SAN"; break;
+				}
+
+				debug("Rebel2: Level 12 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level12_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== PHASE 2: 12P06 → P2/12P02_X =====
+		_currentPhase = 2;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+
+		budget = kLevel12BudgetBase[1] + _vm->_rnd.getRandomNumber(3);
+
+		// Initialization video (12P06.SAN)
+		debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/12P06.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+		processWaveEnd(1, &budget, 0, 0);
+
+		// First wave (P2/12P02_A.SAN)
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/P2/12P02_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				// Variant selection: (waveSelect & 2) controls which set
+				int local_8;
+				if ((waveSelect & 2) == 0) {
+					local_8 = _vm->_rnd.getRandomNumber(2) + 3;  // 3, 4, or 5
+				} else {
+					local_8 = _vm->_rnd.getRandomNumber(2);      // 0, 1, or 2
+				}
+
+				const char *filename;
+				switch (local_8) {
+				case 0:  filename = "LEV12/P2/12P02_A.SAN"; break;
+				case 1:  filename = "LEV12/P2/12P02_B.SAN"; break;
+				case 2:  filename = "LEV12/P2/12P02_E.SAN"; break;
+				case 3:  filename = "LEV12/P2/12P02_C.SAN"; break;
+				case 4:  filename = "LEV12/P2/12P02_D.SAN"; break;
+				default: filename = "LEV12/P2/12P02_F.SAN"; break;
+				}
+
+				debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				// Variants E(2) and F(5) reset threshold to 0
+				int16 threshold = (local_8 == 2 || local_8 == 5) ? 0 : 0x14;
+				waveSelect = processWaveEnd(6, &budget, threshold, 0);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level12_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== PHASE 3: 12P07 → P3/12P03_X =====
+		_currentPhase = 3;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+
+		budget = kLevel12BudgetBase[2] + _vm->_rnd.getRandomNumber(3);
+
+		// Initialization video (12P07.SAN)
+		debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/12P07.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+		processWaveEnd(1, &budget, 0, 0);
+
+		// First wave (P3/12P03_A.SAN)
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/P3/12P03_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			int local_8 = 0;
+			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				// Wider randomization for first few waves
+				if (local_8 < 4) {
+					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
+				} else {
+					local_8 = _vm->_rnd.getRandomNumber(3);  // 0-3
+				}
+
+				const char *filename;
+				switch (local_8) {
+				case 0:  filename = "LEV12/P3/12P03_C.SAN"; break;
+				case 1:  filename = "LEV12/P3/12P03_D.SAN"; break;
+				case 2:  filename = "LEV12/P3/12P03_A.SAN"; break;
+				case 3:  filename = "LEV12/P3/12P03_B.SAN"; break;
+				case 4:  filename = "LEV12/P3/12P03_F.SAN"; break;
+				default: filename = "LEV12/P3/12P03_F.SAN"; break;  // duplicate F
+				}
+
+				debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level12_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== PHASE 4: 12P08 → P4/12P04_X =====
+		_currentPhase = 4;
+		_rebelPhaseState = 0;
+		_rebelWaveState = 0;
+
+		budget = kLevel12BudgetBase[3] + _vm->_rnd.getRandomNumber(3);
+
+		// Initialization video (12P08.SAN)
+		debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/12P08.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+		processWaveEnd(1, &budget, 0, 0);
+
+		// First wave (P4/12P04_A.SAN)
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/P4/12P04_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		{
+			int local_8 = 0;
+			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+
+			while (waveSelect != 0xFFFF) {
+				if (_vm->shouldQuit()) return kLevelQuit;
+
+				if (local_8 < 4) {
+					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
+				} else {
+					local_8 = _vm->_rnd.getRandomNumber(3);  // 0-3
+				}
+
+				const char *filename;
+				switch (local_8) {
+				case 0:  filename = "LEV12/P4/12P04_C.SAN"; break;
+				case 1:  filename = "LEV12/P4/12P04_D.SAN"; break;
+				case 2:  filename = "LEV12/P4/12P04_A.SAN"; break;
+				case 3:  filename = "LEV12/P4/12P04_B.SAN"; break;
+				case 4:  filename = "LEV12/P4/12P04_E.SAN"; break;
+				default: filename = "LEV12/P4/12P04_F.SAN"; break;
+				}
+
+				debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				splayer->setCurVideoFlags(0x428);
+				splayer->play(filename, 12);
+				_deathFrame = splayer->_frame;
+
+				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+			}
+		}
+
+		if (_playerDamage >= 255) goto level12_death;
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// ===== CLOSING: 12P09.SAN =====
+		splayer->setCurVideoFlags(0x428);
+		splayer->play("LEV12/12P09.SAN", 12);
+		if (_vm->shouldQuit()) return kLevelQuit;
+		processWaveEnd(1, &budget, 0, 0);
+
+		// ===== LEVEL COMPLETED =====
+		{
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 12 completed! kills=%d misses=%d accuracy=%d%%",
+				_rebelKillCounter, _rebelHitCounter, accuracy);
+		}
+
+		// Bonus checks: FUN_0042aa70(0x61), FUN_0042aa70(99), FUN_0042aa70(0x74)
+		// If all three bonuses found, play special ending (12END_Z.SAN)
+		// Deferred until bonus tracking is implemented
+
+		playLevelEnd(12);
+		_levelUnlocked[12] = true;  // Unlock level 13
+		return kLevelNextLevel;
+
+	level12_death:
+		// Single death video for all phases
+		debug("Rebel2: Level 12 Phase %d death", _currentPhase);
+		playCinematic("LEV12/12DIE.SAN");
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(12);
+			return kLevelGameOver;
+		}
+		playCinematic("LEV12/12RETRY.SAN");
+		_playerDamage = 0;
+		if (_vm->shouldQuit()) return kLevelQuit;
+		continue;  // Restart from Phase 1
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 13 Handler - FUN_0041B3E1
+// "Escaping the Star Destroyer" - Two-phase flight/escape
+// Phase A: 13PLAY_A.SAN (main flight), transitions to Phase B at maxFrame-10
+// Phase B: 13PLAY_B.SAN (reactor loop, flags 0x468) — plays until
+//   (DAT_0047ab90 == 0 && DAT_0047ab7c == 0) meaning all targets destroyed.
+// Death: frame-based (A/B/C/B/D pattern)
+// =============================================================================
+
+int InsaneRebel2::runLevel13() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// No cutscene — starts directly with BEG
+	// Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
+	playLevelBegin(13);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+	clearBit(0);
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_deathFrame = 0;
+
+		clearBit(0);
+
+		// Phase A: Main escape flight (13PLAY_A.SAN)
+		// Original: FUN_0041f4d0("13PLAY_A.SAN", 0x28, -1, -1, 0)
+		// First inner loop runs until frame reaches maxFrame-10
+		// Then Phase B (13PLAY_B.SAN, flags 0x468) plays at that exact frame
+		// The 0x468 flags indicate seamless mid-video transition.
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV13/13PLAY_A.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		// If alive after Phase A, play Phase B (reactor destruction loop)
+		// Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
+		// Then loop while (DAT_0047ab90 != 0 || DAT_0047ab7c != 0)
+		// For now, play B as a sequential video. The IACT callbacks will manage
+		// the reactor target state through opcode interactions.
+		if (_playerShield > 0) {
+			splayer->setCurVideoFlags(0x468);
+			splayer->play("LEV13/13PLAY_B.SAN", 12);
+			_deathFrame = splayer->_frame;
+		}
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 13 completed! accuracy=%d%%", accuracy);
+			playLevelEnd(13);
+			_levelUnlocked[13] = true;
+			return kLevelNextLevel;
+		}
+
+		// Death: frame-based variant selection (FUN_0041B3E1 lines 47-61)
+		debug("Rebel2: Level 13 death at frame %d", _deathFrame);
+		playLevelDeathVariant(13, 1, _deathFrame);
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(13);
+			return kLevelGameOver;
+		}
+
+		playCinematic("LEV13/13RETRY.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 14 Handler - FUN_0041B6E8
+// "TIE Attack" - Final space battle (single phase)
+// No cutscene. Single death video (14DIE.SAN, no variants).
+// =============================================================================
+
+int InsaneRebel2::runLevel14() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// No cutscene — starts directly with BEG
+	// Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
+	playLevelBegin(14);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+	clearBit(0);
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_deathFrame = 0;
+
+		clearBit(0);
+
+		// Play gameplay (14PLAY.SAN)
+		// Original: FUN_0041f4d0("14PLAY.SAN", 0x28, -1, -1, 0)
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV14/14PLAY.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 14 completed! accuracy=%d%%", accuracy);
+			playLevelEnd(14);
+			_levelUnlocked[14] = true;
+			return kLevelNextLevel;
+		}
+
+		// Death: single video (14DIE.SAN)
+		debug("Rebel2: Level 14 death at frame %d", _deathFrame);
+		playCinematic("LEV14/14DIE.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		_playerLives--;
+		if (_playerLives <= 0) {
+			playLevelGameOver(14);
+			return kLevelGameOver;
+		}
+
+		playCinematic("LEV14/14RETRY.SAN");
+		if (_vm->shouldQuit()) return kLevelQuit;
+	}
+
+	return kLevelQuit;
+}
+
+// =============================================================================
+// Level 15 Handler - FUN_0041B8D7
+// "Imdaar Alpha" - Final mission (single long phase with level ID switch)
+// Has cutscene. Mid-level: DAT_0047a7f8 changes from 0xf to 0x10 at frame 0x21e.
+// This represents a transition from the tunnel section to the core section.
+// Death: frame-based (A/B/C/B/C/B/D pattern with 7 thresholds)
+// On completion → FUN_0041BBE8 (credits/end game, not a playable level)
+// =============================================================================
+
+int InsaneRebel2::runLevel15() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Play cutscene (15CUT.SAN)
+	playCinematic("LEV15/15CUT.SAN");
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// Play level beginning cinematic (15BEG.SAN)
+	// Original: FUN_004171c5("15BEG.SAN", 0x20, 0xb8, 0xa0, 10, 2, 0x46)
+	playLevelBegin(15);
+	if (_vm->shouldQuit()) return kLevelQuit;
+
+	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
+	clearBit(0);
+
+	while (!_vm->shouldQuit()) {
+		_playerShield = 255;
+		_playerDamage = 0;
+		_deathFrame = 0;
+
+		clearBit(0);
+
+		// Original: DAT_0047a7f8 = 0xf (level 15) before gameplay
+		// At frame 0x21e (542): DAT_0047a7f8 = 0x10 (switches to "level 16" internally)
+		// After gameplay: DAT_0047a7f8 = 0x10 (stays at 16)
+		// This level ID switch affects which difficulty data is used mid-level.
+		// The IACT callbacks handle gameplay regardless of this ID.
+
+		// Play gameplay (15PLAY.SAN)
+		// Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
+		splayer->setCurVideoFlags(0x28);
+		splayer->play("LEV15/15PLAY.SAN", 12);
+		_deathFrame = splayer->_frame;
+
+		if (_vm->shouldQuit()) return kLevelQuit;
+
+		if (_playerShield > 0) {
+			int accuracy = 0;
+			if (_rebelKillCounter > 0) {
+				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
+			}
+			debug("Rebel2: Level 15 completed! accuracy=%d%%", accuracy);
+			playLevelEnd(15);
+			_levelUnlocked[15] = true;
+			// Level 15 completion leads to credits (FUN_0041BBE8)
 			return kLevelNextLevel;
 		}
 
-		// Died in phase 2
-		debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
-		playLevelDeathVariant(6, 2, _deathFrame);
+		// Death: frame-based variant selection (FUN_0041B8D7 lines 46-65)
+		debug("Rebel2: Level 15 death at frame %d", _deathFrame);
+		playLevelDeathVariant(15, 1, _deathFrame);
 		if (_vm->shouldQuit()) return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
-			playLevelGameOver(6);
+			playLevelGameOver(15);
 			return kLevelGameOver;
 		}
 
-		playLevelRetryVariant(6, 2);
+		playCinematic("LEV15/15RETRY.SAN");
 		if (_vm->shouldQuit()) return kLevelQuit;
 	}
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c91ba5ac25b..7cbedcecef2 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -241,7 +241,16 @@ public:
 	int runLevel3();   // FUN_0041885F - Two phases with per-phase retry
 	int runLevel4();   // Cutscene + single gameplay
 	int runLevel5();   // FUN_00418EC6 - Single gameplay phase
-	int runLevel6();   // FUN_00419317 - Two phases with per-phase retry
+	int runLevel6();   // FUN_004190D6 - Two phases with mid-level video switch
+	int runLevel7();   // FUN_0041974C - TIE Training: single + fork at frame 1592
+	int runLevel8();   // FUN_00419976 - Flight to Imdaar: single phase space battle
+	int runLevel9();   // FUN_00419B86 - Mine Field: single phase with mid-events
+	int runLevel10();  // FUN_00419E0A - Speeder Bikes: single phase
+	int runLevel11();  // FUN_0041A00C - Inside the Terror: 3 phases + bridge (Handler 8)
+	int runLevel12();  // FUN_0041AA14 - Sewers: 4 phases FPS (Handler 25)
+	int runLevel13();  // FUN_0041B3E1 - Escaping Star Destroyer: two-phase A→B
+	int runLevel14();  // FUN_0041B6E8 - TIE Attack: single phase
+	int runLevel15();  // FUN_0041B8D7 - Imdaar Alpha: single + level ID switch
 
 	// Wave state management (FUN_00417b61)
 	// Waits for current video to finish, accumulates kill state, redistributes


Commit: 06cc4d01d428c9f2c9bd673c755ef95bf7eb3742
    https://github.com/scummvm/scummvm/commit/06cc4d01d428c9f2c9bd673c755ef95bf7eb3742
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00

Commit Message:
SCUMM: RA2: Improve menu video handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5b7f98334f1..ae4125e38bb 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -976,6 +976,25 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// Handler 25: Corridor overlay and FOBJ position offsets are set during
 	// IACT opcode 6 processing (iactRebel2Opcode6), matching the original
 	// FUN_41CADB architecture. No corridor drawing needed here.
+
+	// Chapter selection: Set FOBJ offset to scroll preview thumbnails in O_LEVEL.SAN.
+	// Original (FUN_00415CF8): offsets start at (0,0) for the first display update,
+	// then FUN_00425170 sets them to (-90, chapter*-50+75) AFTER each frame.
+	// Frame 0 must use (0,0) so the 80x800 preview strip at X=320 renders off-screen
+	// and STOR captures it cleanly. Frames 1+ use the scroll offset so FTCH re-renders
+	// the strip at the correct preview position.
+	if (_gameState == kStateChapterSelect && _player) {
+		if (_player->_frame > 0) {
+			// Clear screen to black before FTCH re-renders the preview strip.
+			// Our FTCH only re-draws the preview area (80px wide at X=230);
+			// without clearing, old menu text and preview artifacts persist.
+			if (renderBitmap) {
+				memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
+			}
+			_player->_fobjOffsetX = _previewOffsetX;
+			_player->_fobjOffsetY = _previewOffsetY;
+		}
+	}
 }
 
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -4619,16 +4638,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		delete cursor;
 		CursorMan.showMouse(true);
 
-		// Fill screen with BLACK background
-		// The original game plays O_LEVEL.SAN (640x400) which has a dark/black background
-		// with chapter preview thumbnails embedded. For 320x200 mode, we fill with black
-		// to match the visual appearance and overlay the UI elements.
-		// Note: O_MENU_X.SAN (320x200) doesn't contain chapter-specific preview images,
-		// those are only in O_LEVEL.SAN (640x400). We use a styled placeholder instead.
-		// From FUN_00415CF8 line 57: FUN_0041f4d0(s_OPEN_O_LEVEL_SAN_004806e0,8,0xffff,0xffff,0);
-		for (int y = 0; y < height; y++) {
-			memset(renderBitmap + y * pitch, 0, width);
-		}
+		// O_LEVEL.SAN provides the background with chapter preview thumbnails.
+		// The FOBJ offset system (set in procPreRendering) scrolls the correct preview
+		// into the preview box area. No black fill needed — video frame shows through.
 
 		// Process chapter selection input - emulates FUN_00415CF8 input handling
 		int selection = processChapterSelectInput();
@@ -7362,19 +7374,36 @@ int InsaneRebel2::runChapterSelect() {
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
-	// Chapter selection background - emulates FUN_00415CF8 line 57
-	// Original uses O_LEVEL.SAN (640x400). We use menu video for 320x200 mode.
+	// Initialize preview offset for initial selection
+	_previewOffsetX = -90;
+	_previewOffsetY = _chapterSelection * -50 + 75;
+
+	// Set iactBits for chapter unlock state (FUN_00415CF8 lines 79-86)
+	// Bits 16..1 correspond to chapters 0..15: set if unlocked, clear if locked.
+	// These control SKIP chunks in O_LEVEL.SAN for locked/unlocked preview variants.
+	for (int i = 0; i < 16; i++) {
+		if (_chapterUnlocked[i])
+			setBit(16 - i);
+		else
+			clearBit(16 - i);
+	}
+
+	// Chapter selection background - FUN_00415CF8 line 57:
+	// FUN_0041f4d0(s_OPEN_O_LEVEL_SAN, 8, 0xffff, 0xffff, 0)
+	// O_LEVEL.SAN contains chapter preview thumbnails at specific FOBJ positions.
+	// The FOBJ offset system scrolls the correct preview into the preview box area.
 	while (!_vm->shouldQuit()) {
 		_vm->_smushVideoShouldFinish = false;
 
-		Common::String menuVideo = getRandomMenuVideo();
-		debug("Rebel2: Playing chapter select background: %s", menuVideo.c_str());
+		debug("Rebel2: Playing chapter select background: OPEN/O_LEVEL.SAN");
 
-		// Set video flags for menu mode
+		// Flags: 0x20 (overlay drawing). No 0x08 (preserve) — we want a black
+		// background. O_LEVEL.SAN has no full-screen background FOBJ; the visible
+		// screen area stays black, and preview thumbnails render at X=230 via offset.
 		splayer->setCurVideoFlags(0x20);
 
-		// Play the menu video as chapter selection background
-		splayer->play(menuVideo.c_str(), 12);
+		// Play O_LEVEL.SAN — preview thumbnails are rendered by FOBJ offset
+		splayer->play("OPEN/O_LEVEL.SAN", 12);
 
 		if (_vm->shouldQuit()) {
 			_menuInputActive = false;
@@ -7439,7 +7468,9 @@ int InsaneRebel2::processChapterSelectInput() {
 				if (_chapterSelection < 0) {
 					_chapterSelection = _chapterItemCount - 1;
 				}
-				debug("ChapterSelect: Selection changed to %d (UP)", _chapterSelection);
+				// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
+				_previewOffsetY = _chapterSelection * -50 + 75;
+				debug("ChapterSelect: Selection changed to %d (UP) offsetY=%d", _chapterSelection, _previewOffsetY);
 				break;
 
 			case Common::KEYCODE_DOWN:
@@ -7448,7 +7479,9 @@ int InsaneRebel2::processChapterSelectInput() {
 				if (_chapterSelection >= _chapterItemCount) {
 					_chapterSelection = 0;
 				}
-				debug("ChapterSelect: Selection changed to %d (DOWN)", _chapterSelection);
+				// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
+				_previewOffsetY = _chapterSelection * -50 + 75;
+				debug("ChapterSelect: Selection changed to %d (DOWN) offsetY=%d", _chapterSelection, _previewOffsetY);
 				break;
 
 			case Common::KEYCODE_RETURN:
@@ -7486,19 +7519,28 @@ int InsaneRebel2::processChapterSelectInput() {
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
+			// Click confirms the current selection (original: DAT_0047a7e4 & 1)
+			if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
+				result = _chapterSelection;
+				debug("ChapterSelect: Item %d confirmed (CLICK)", _chapterSelection);
+			}
+			break;
+
+		case Common::EVENT_MOUSEMOVE:
 			{
-				// Mouse click - check if clicking on a menu item
-				// From FUN_0041F5AE (low-res, left-aligned):
-				// Item Y = 17 * -5 + i * 10 + 104 = 19 + i * 10
-				int baseY = _chapterItemCount * -5 + 0x68;  // = 19
+				// Mouse hover changes highlight (original FUN_0041f5ae mouse mode).
+				// Item Y = numItems * -5 + i * 10 + 0x68
+				int baseY = _chapterItemCount * -5 + 0x68;
 				int mouseY = event.mouse.y;
 
 				for (int i = 0; i < _chapterItemCount; i++) {
 					int itemY = baseY + i * 10;
 					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
-						_chapterSelection = i;
-						result = i;
-						debug("ChapterSelect: Item %d clicked at Y=%d", i, mouseY);
+						if (i != _chapterSelection) {
+							_chapterSelection = i;
+							_previewOffsetY = _chapterSelection * -50 + 75;
+							debug(5, "ChapterSelect: Hover changed to %d", _chapterSelection);
+						}
 						break;
 					}
 				}
@@ -7741,11 +7783,15 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
 	// Emulates FUN_00415CF8 rendering via shared drawMenuItems(leftAligned=true)
 	//
-	// Menu structure (18 entries, 17 selectable):
-	//   items[0]     = title = TRS 60 (locked chapter 0 header, contains ^f02 for TITLFONT)
-	//   items[1..16] = chapters 1-16 = TRS 41-56 (unlocked) or TRS 61-76 (locked)
-	//   items[17]    = "RETURN TO PILOTS" = TRS 77
+	// GAME.TRS chapter selection strings:
+	//   TRS 40     = "^f02Chapters" (title)
+	//   TRS 41-56  = unlocked chapter names (e.g. "^f01^c244Chapter 1 - The Dreighton Triangle")
+	//   TRS 57     = "^f01^c240RETURN TO PILOTS"
+	//   TRS 60     = "^f02Chapters" (title, locked section duplicate)
+	//   TRS 61-76  = locked chapter names (e.g. "^f01^c244Chapter 1 -")
+	//   TRS 77     = "^f01^c240RETURN TO PILOTS" (locked section duplicate)
 	//
+	// Menu array: items[0]=title, items[1..16]=chapters, items[17]=RETURN TO PILOTS
 	// FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -7754,9 +7800,9 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	// Build items array matching original DAT_004577a8 layout
 	const char *items[18];
 
-	// items[0] = title (always locked version = TRS 60, contains "^f02CHAPTERS")
-	items[0] = splayer->getString(60);
-	if (!items[0] || !items[0][0]) items[0] = "^f02CHAPTERS";
+	// items[0] = title = TRS 40 ("^f02Chapters")
+	items[0] = splayer->getString(40);
+	if (!items[0] || !items[0][0]) items[0] = "^f02Chapters";
 
 	// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
 	for (int i = 1; i <= 16; i++) {
@@ -7766,21 +7812,17 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 		if (!items[i] || !items[i][0]) items[i] = "";
 	}
 
-	// items[17] = "RETURN TO PILOTS" (always locked version = TRS 77)
-	items[17] = splayer->getString(77);
-	if (!items[17] || !items[17][0]) items[17] = "RETURN TO PILOTS";
+	// items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
+	items[17] = splayer->getString(57);
+	if (!items[17] || !items[17][0]) items[17] = "^f01^c240RETURN TO PILOTS";
 
 	// Render menu using shared renderer with left-aligned mode
 	drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
 
-	// Draw preview box on the right side (FUN_004292d0 calls at lines 128-133)
+	// Draw preview box border on the right side (FUN_004292d0 calls at lines 128-133)
+	// The actual preview image is rendered by O_LEVEL.SAN FOBJ via the offset system
 	drawPreviewBox(renderBitmap, pitch, width, height);
 
-	// Draw preview thumbnail for chapters 0-15 (not RETURN TO PILOTS)
-	if (_chapterSelection >= 0 && _chapterSelection < 16) {
-		drawPreviewThumbnail(renderBitmap, pitch, width, height, _chapterSelection);
-	}
-
 	// Draw score/info line at bottom
 	drawChapterInfoLine(renderBitmap, pitch, width, height);
 }
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 16edd4fadc7..9b172697af6 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -257,6 +257,13 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_frameBuffer = nullptr;
 	_specialBuffer = nullptr;
 	_specialBufferSize = 0;
+	_storedFobjData = nullptr;
+	_storedFobjDataSize = 0;
+	_storedFobjCodec = 0;
+	_storedFobjLeft = 0;
+	_storedFobjTop = 0;
+	_storedFobjWidth = 0;
+	_storedFobjHeight = 0;
 
 	_seekPos = -1;
 
@@ -328,6 +335,8 @@ SmushPlayer::~SmushPlayer() {
 	_frameBuffer = nullptr;
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
+	free(_storedFobjData);
+	_storedFobjData = nullptr;
 	free(_loadBuffer);
 	_loadBuffer = nullptr;
 }
@@ -340,12 +349,28 @@ void SmushPlayer::init(int32 speed) {
 	_endOfFile = false;
 	_storeFrame = false;
 
+	// Reset FOBJ offsets between videos. These are set per-video by
+	// procPreRendering (chapter select preview scrolling) or IACT opcode 6
+	// (corridor overlay positioning). Without this, offsets from O_LEVEL.SAN
+	// would persist and shift FOBJs in subsequent videos → buffer overflows.
+	_fobjOffsetX = 0;
+	_fobjOffsetY = 0;
+
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
 
 	_vm->setDirtyColors(0, 255);
 	_dst = vs->getPixels(0, 0);
 
+	// For RA2: Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
+	// have no NPAL chunk and inherit the palette from the previous video. Since
+	// play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
+	// This is safe for videos WITH NPAL too — the NPAL handler immediately
+	// overwrites _pal and re-marks dirty.
+	if (_vm->_game.id == GID_REBEL2) {
+		setDirtyColors(0, 255);
+	}
+
 	// For Rebel Assault 2, handle background preservation between videos:
 	// - Cinematic videos (flags 0x20) clear the buffer for a fresh start
 	// - Gameplay videos (flags 0x28) preserve the existing screen content
@@ -404,6 +429,10 @@ void SmushPlayer::release() {
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
 
+	free(_storedFobjData);
+	_storedFobjData = nullptr;
+	_storedFobjDataSize = 0;
+
 	// For Rebel Assault 2, preserve _frameBuffer across videos so that
 	// gameplay videos (which have no background FOBJ) can use the stored
 	// background from the previous BEG cinematic video.
@@ -462,6 +491,17 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 		}
 	}
 
+	// RA2: Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
+	// The stored FOBJ's original position is combined with the current _fobjOffsetX/Y,
+	// so scrolling the chapter preview works correctly on each FTCH.
+	if (_vm->_game.id == GID_REBEL2 && _storedFobjData != nullptr) {
+		decodeFrameObject(_storedFobjCodec, _storedFobjData,
+			_storedFobjLeft, _storedFobjTop,
+			_storedFobjWidth, _storedFobjHeight,
+			_storedFobjDataSize);
+		return;
+	}
+
 	if (_frameBuffer != nullptr) {
 		memcpy(_dst, _frameBuffer, _width * _height);
 	}
@@ -512,9 +552,9 @@ void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
 		_totalLoadChunks = totalChunks;
 
 		// Allocate/reallocate buffer if needed
-		// Estimate buffer size: typical LOAD data per chunk is ~350 bytes
-		// Total expected: totalChunks * 400 bytes (with margin)
-		int32 estimatedSize = totalChunks * 400;
+		// LOAD data per chunk varies (up to ~500 bytes observed).
+		// Allocate generously to avoid overflow.
+		int32 estimatedSize = totalChunks * 600;
 		if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
 			free(_loadBuffer);
 			_loadBufferSize = estimatedSize;
@@ -1160,7 +1200,9 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		error("Invalid codec for frame object : %d", codec);
 	}
 
-	if (_storeFrame) {
+	// For non-RA2 games, save rendered bitmap when STOR is pending.
+	// RA2 handles STOR in handleFrameObject by saving raw FOBJ data instead.
+	if (_storeFrame && _vm->_game.id != GID_REBEL2) {
 		if (_frameBuffer == nullptr) {
 			_frameBuffer = (byte *)malloc(_width * _height);
 		}
@@ -1224,6 +1266,24 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	assert(chunk_buffer);
 	b.read(chunk_buffer, chunk_size);
 
+	// RA2: When STOR is pending, save raw FOBJ data for later re-decoding by FTCH.
+	// The original (FUN_00423A50 bVar5) stores the next FOBJ's raw chunk data in
+	// DAT_00482c04. FTCH then re-renders from this stored data with current FOBJ
+	// offsets. This is critical for O_LEVEL.SAN where the stored FOBJ is the 80x800
+	// preview strip, and FTCH must re-render it at the current scroll offset.
+	if (_storeFrame && _vm->_game.id == GID_REBEL2) {
+		free(_storedFobjData);
+		_storedFobjData = (byte *)malloc(chunk_size);
+		memcpy(_storedFobjData, chunk_buffer, chunk_size);
+		_storedFobjDataSize = chunk_size;
+		_storedFobjCodec = codec;
+		_storedFobjLeft = left;
+		_storedFobjTop = top;
+		_storedFobjWidth = width;
+		_storedFobjHeight = height;
+		_storeFrame = false;
+	}
+
 	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
 
 	free(chunk_buffer);
@@ -1310,6 +1370,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		case MKTAG('L','O','A','D'):
 			handleLoad(subSize, b);
 			break;
+		case MKTAG('G','O','S','T'):
+			// GOST = ghost sprite overlay. Re-renders previous FOBJ data at a new
+			// position with priority/transparency flags. Data: 2-byte priority type
+			// (0/1/2 → 0x2000/0x4000/0x6000), 2-byte x, 2-byte y.
+			// TODO: Implement proper ghost rendering by saving previous FOBJ data
+			// and re-rendering it with modified coordinates and priority flags.
+			break;
 		default:
 			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 		}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 43ecc269d9b..2ebd816193b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -152,6 +152,19 @@ private:
 	byte *_specialBuffer;
 	int _specialBufferSize;
 
+	// RA2: Raw FOBJ data stored by STOR chunk (matching original DAT_00482c04).
+	// The original stores raw FOBJ chunk data and re-decodes it on FTCH with
+	// current FOBJ offsets. This is essential for O_LEVEL.SAN where the stored
+	// FOBJ is the 80x800 preview strip at X=320, and FTCH must re-render it
+	// at the current scroll offset each frame.
+	byte *_storedFobjData;
+	int32 _storedFobjDataSize;
+	int _storedFobjCodec;
+	int _storedFobjLeft;
+	int _storedFobjTop;
+	int _storedFobjWidth;
+	int _storedFobjHeight;
+
 	Common::String _seekFile;
 	uint32 _startFrame;
 	uint32 _startTime;


Commit: c9a07cc6a1dffeb4b28cec89fb16b7465fcb77b0
    https://github.com/scummvm/scummvm/commit/c9a07cc6a1dffeb4b28cec89fb16b7465fcb77b0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00

Commit Message:
SCUMM: RA2: Clean up menu handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ae4125e38bb..c4e5e1442b6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -7428,19 +7428,12 @@ int InsaneRebel2::runChapterSelect() {
 		}
 
 		if (_chapterSelection >= 0 && _chapterSelection < 16) {
-			// Chapter selected
-			if (_chapterUnlocked[_chapterSelection]) {
-				// Chapter is unlocked - start it
-				_selectedChapter = _chapterSelection;
-				debug("Rebel2: Chapter %d selected (unlocked)", _selectedChapter + 1);
-				_menuInputActive = false;
-				return kChapterSelectPlay;
-			} else {
-				// Chapter is locked - check password (lines 239-257 of FUN_00415CF8)
-				// For now, just play error sound and continue
-				debug("Rebel2: Chapter %d is locked", _chapterSelection + 1);
-				continue;
-			}
+			// Chapter selected - start it regardless of unlock state.
+			// TODO: locked chapters should require password (FUN_00415CF8 lines 239-257)
+			_selectedChapter = _chapterSelection;
+			debug("Rebel2: Chapter %d selected", _selectedChapter + 1);
+			_menuInputActive = false;
+			return kChapterSelectPlay;
 		}
 	}
 


Commit: 20b45a1ce0ac4c573cd7606b2e83347b85294fe9
    https://github.com/scummvm/scummvm/commit/20b45a1ce0ac4c573cd7606b2e83347b85294fe9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:09+02:00

Commit Message:
SCUMM: RA2: Split implementation into subsystem files

Changed paths:
  A engines/scumm/insane/insane_rebel_audio.cpp
  A engines/scumm/insane/insane_rebel_iact.cpp
  A engines/scumm/insane/insane_rebel_levels.cpp
  A engines/scumm/insane/insane_rebel_menu.cpp
  A engines/scumm/insane/insane_rebel_render.cpp
  A engines/scumm/insane/insane_rebel_runlevels.cpp
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/module.mk


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c4e5e1442b6..5dacef70047 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -23,13 +23,9 @@
 
 #include "engines/engine.h"
 #include "common/system.h"
-#include "common/memstream.h"
 #include "common/events.h"
 #include "common/util.h"
 
-#include "graphics/cursorman.h"
-#include "graphics/wincursor.h"
-
 #include "scumm/actor.h"
 #include "scumm/file.h"
 #include "scumm/resource.h"
@@ -49,10 +45,6 @@
 
 namespace Scumm {
 
-// External codec functions from codec1.cpp
-extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
 InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_vm = scumm;
@@ -949,9782 +941,5 @@ void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 	}
 }
 
-void InsaneRebel2::procPreRendering(byte *renderBitmap) {
-	// Call base class implementation first (handles Full Throttle state machine)
-	Insane::procPreRendering(renderBitmap);
-
-	// For Level 2 gameplay (Handler 8 only), restore the background BEFORE FOBJ decoding.
-	// The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
-	// clear old ones. By restoring the full background each frame, we ensure old sprite
-	// positions are erased before new ones are drawn.
-	//
-	// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
-	if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
-		for (int y = 0; y < 200; y++) {
-			memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
-		}
-	}
-
-	// For Handler 25 (Level 2 speeder bike), draw the corridor overlay BEFORE FOBJ decoding.
-	// The corridor overlay (par3=4 -> _rebelEmbeddedHud[4]) is DAT_00482268, a 350x230 buffer.
-	// From FUN_0041cadb line 216: FUN_00428a10(param_1,0,DAT_0045790c,DAT_0045790e,DAT_00482268)
-	// It's drawn at (DAT_0045790c, DAT_0045790e) which are _rebelViewOffsetX/Y.
-	//
-	// For Mode 1: DAT_0045790c = damageLevel * -5 - 14, range -39 (covered) to -14 (uncovered)
-	//
-	// From FUN_00428a10: When position is negative, we skip source pixels and draw at 0.
-	// Handler 25: Corridor overlay and FOBJ position offsets are set during
-	// IACT opcode 6 processing (iactRebel2Opcode6), matching the original
-	// FUN_41CADB architecture. No corridor drawing needed here.
-
-	// Chapter selection: Set FOBJ offset to scroll preview thumbnails in O_LEVEL.SAN.
-	// Original (FUN_00415CF8): offsets start at (0,0) for the first display update,
-	// then FUN_00425170 sets them to (-90, chapter*-50+75) AFTER each frame.
-	// Frame 0 must use (0,0) so the 80x800 preview strip at X=320 renders off-screen
-	// and STOR captures it cleanly. Frames 1+ use the scroll offset so FTCH re-renders
-	// the strip at the correct preview position.
-	if (_gameState == kStateChapterSelect && _player) {
-		if (_player->_frame > 0) {
-			// Clear screen to black before FTCH re-renders the preview strip.
-			// Our FTCH only re-draws the preview area (80px wide at X=230);
-			// without clearing, old menu text and preview artifacts persist.
-			if (renderBitmap) {
-				memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
-			}
-			_player->_fobjOffsetX = _previewOffsetX;
-			_player->_fobjOffsetY = _previewOffsetY;
-		}
-	}
-}
-
-void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
-					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
-					  int16 par1, int16 par2, int16 par3, int16 par4) {
-	// Debug: Log all IACT opcodes
-	debug("Rebel2 IACT: opcode=%d par2=%d par3=%d par4=%d gameState=%d sceneId=%d",
-		par1, par2, par3, par4, _gameState, _currSceneId);
-
-	if (_keyboardDisable)
-		return;
-
-	// Handle menu IACT - menu videos have embedded ANIM data in IACT chunks
-	// Menu IACTs have par1=8 (code), par2=46 (flags), par4>=1000 (userId)
-	// The embedded ANIM contains the full menu frame
-	if (_gameState == kStateMainMenu && par1 == 8 && par4 >= 1000) {
-		debug("Rebel2 IACT: Menu mode - processing embedded ANIM (userId=%d)", par4);
-
-		// Scan for embedded ANIM tag in the IACT data
-		int64 startPos = b.pos();
-		int64 totalSize = b.size();
-		debug("Rebel2 IACT: stream pos=%d, size=%d, remaining=%d",
-			(int)startPos, (int)totalSize, (int)(totalSize - startPos));
-
-		if (totalSize > startPos) {
-			int64 remaining = totalSize - startPos;
-			int scanSize = (int)MIN<int64>(remaining, 65536);
-			byte *scanBuf = (byte *)malloc(scanSize);
-			if (scanBuf) {
-				int bytesRead = b.read(scanBuf, scanSize);
-				debug("Rebel2 IACT: Read %d bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-					bytesRead, scanBuf[0], scanBuf[1], scanBuf[2], scanBuf[3],
-					scanBuf[4], scanBuf[5], scanBuf[6], scanBuf[7],
-					scanBuf[8], scanBuf[9], scanBuf[10], scanBuf[11],
-					scanBuf[12], scanBuf[13], scanBuf[14], scanBuf[15]);
-
-				// Look for ANIM tag (embedded SAN containing menu frame)
-				for (int i = 0; i + 8 <= bytesRead; ++i) {
-					if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
-						int64 animStreamPos = startPos + i;
-						uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, totalSize - animStreamPos);
-						debug("Rebel2 IACT: Found embedded ANIM at offset %d, size %d", (int)i, (int)animReportedSize);
-						if (toCopy > 0) {
-							byte *animData = (byte *)malloc(toCopy);
-							if (animData) {
-								b.seek(animStreamPos);
-								b.read(animData, toCopy);
-								// Use userId as the HUD slot (1000 -> slot 0 for menu background)
-								loadEmbeddedSan(0, animData, toCopy, renderBitmap);
-								free(animData);
-							}
-						}
-						b.seek(startPos);
-						free(scanBuf);
-						return;
-					}
-				}
-
-				debug("Rebel2 IACT: No ANIM tag found in menu IACT data");
-				b.seek(startPos);
-				free(scanBuf);
-			}
-		}
-		return;
-	}
-
-	if (_currSceneId == 1)
-		iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
-}
-
-
-void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
-				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
-				  int16 par1, int16 par2, int16 par3, int16 par4) {
-	// par1 is the Opcode (word at offset +0)
-	// par2 is word at offset +2
-	// par3 is word at offset +4
-	// par4 is word at offset +6
-	//
-	// Based on disassembly of FUN_4028C5 and FUN_4033CF:
-	// 
-	// For IACT opcode 4 (enemy position update), the structure is:
-	//   Offset +0x06: Type/SubType (par3)
-	//   Offset +0x08: Enemy ID
-	//   Offset +0x0A: X position
-	//   Offset +0x0C: Y position
-	//   Offset +0x0E: Width
-	//   Offset +0x10: Height
-	//
-	// The original game calculates bounding box center:
-	//   centerX = X + (Width / 2)
-	//   centerY = Y + (Height / 2)
-	// Then subtracts scroll offsets:
-	//   screenX = centerX - DAT_0043e006 (scrollX)
-	//   screenY = centerY - DAT_0043e008 (scrollY)
-
-	//   screenX = centerX - DAT_0043e006 (scrollX)
-	//   screenY = centerY - DAT_0043e008 (scrollY)
-
-	if (par1 == 4) {
-		enemyUpdate(renderBitmap, b, par2, par3, par4);
-	} else if (par1 == 2) {
-		// Delegate handling to dedicated opcode 2 handler
-		iactRebel2Opcode2(b, par2, par3, par4);
-	} else if (par1 == 3) {
-		iactRebel2Opcode3(b, par2, par3, par4);
-	}
-	else if (par1 == 5) {
-		// Opcode 5: Collision Zone Registration (FUN_004033cf case 5)
-		// Sub-opcode 0x0D (13) = Primary collision zones (obstacles)
-		// Sub-opcode 0x0E (14) = Secondary collision zones (boundaries)
-		// par2 is the sub-opcode that determines which zone table to use
-		debug("Rebel2 IACT Opcode 5: par2=%d par3=%d par4=%d", par2, par3, par4);
-
-		if (par2 == 0x0D || par2 == 0x0E) {
-			// Register the collision zone from the remaining IACT data
-			// par4 (userId from IACT header) is the filter value used by FUN_4092D9
-			// for the < 1000 test (offset +6 in the original stored pointer)
-			registerCollisionZone(b, par2, par4);
-		}
-
-	} else if (par1 == 7) {
-		// Opcode 7: Handler 7 corridor/velocity control (FUN_40C3CC case 5)
-		// IACT header: par1=7, par2=flags, par3=0, par4=sub-opcode
-		// Body contains 2 int16 values (body[0], body[1])
-		//
-		// par4 sub-opcodes (from FUN_40C3CC case 5 switch on param_5[3]):
-		//   0: Set velocity params (DAT_00443b12, DAT_00443b14)
-		//   1: Set left X + top Y corridor boundaries (DAT_00443b0a, DAT_00443b0c)
-		//   2: Set right X + bottom Y corridor boundaries (DAT_00443b0e, DAT_00443b10)
-		//   5: Set flag (DAT_00443b52)
-
-		int16 body0 = 0, body1 = 0;
-		if (b.size() - b.pos() >= 4) {
-			body0 = b.readSint16LE();
-			body1 = b.readSint16LE();
-		}
-
-		switch (par4) {
-		case 0:
-			// Velocity/wind data — affects ship drift in FUN_40C3CC physics
-			// DAT_00443b12 = horizontal wind, DAT_00443b14 = vertical wind
-			_windParamX = body0;
-			_windParamY = body1;
-			debug("Rebel2 Opcode 7 par4=0: wind=(%d,%d)", body0, body1);
-			break;
-		case 1:
-			// Set LEFT X boundary and TOP Y boundary
-			_corridorLeftX = body0;
-			_corridorTopY = body1;
-			// Mode-dependent margin adjustment (FUN_40C3CC lines 341-351)
-			if (_flyControlMode == 2) {
-				_corridorLeftX += 15;
-			} else if (_flyControlMode == 0) {
-				_corridorLeftX += 20;
-			}
-			debug("Rebel2 Opcode 7 par4=1: corridor left=%d top=%d (adjusted left=%d)",
-				body0, body1, _corridorLeftX);
-			break;
-		case 2:
-			// Set RIGHT X boundary and BOTTOM Y boundary
-			_corridorRightX = body0;
-			_corridorBottomY = body1;
-			// Mode-dependent margin adjustment (FUN_40C3CC lines 356-365)
-			if (_flyControlMode == 2) {
-				_corridorRightX -= 15;
-			} else if (_flyControlMode == 0) {
-				_corridorRightX -= 20;
-			}
-			debug("Rebel2 Opcode 7 par4=2: corridor right=%d bottom=%d (adjusted right=%d)",
-				body0, body1, _corridorRightX);
-			break;
-		case 5:
-			// Flag value
-			debug("Rebel2 Opcode 7 par4=5: flag=%d", body0);
-			break;
-		default:
-			debug("Rebel2 Opcode 7 par4=%d: body=(%d,%d) — unknown sub-opcode", par4, body0, body1);
-			break;
-		}
-
-	} else if (par1 == 6) {
-		// Opcode 6: Level setup / mode switch (FUN_41CADB case 4)
-		iactRebel2Opcode6(renderBitmap, b, size, par2, par3, par4);
-	} else if (par1 == 8) {
-		// Opcode 8: HUD resource loading (FUN_41CADB case 6)
-		iactRebel2Opcode8(renderBitmap, b, size, par2, par3, par4);
-	} else if (par1 == 9) {
-		// Opcode 9: Text/subtitle display
-		iactRebel2Opcode9(renderBitmap, b, par2, par3, par4);
-	} else if (par1 == 0 || par1 == 1) {
-		// Low Opcodes seen in logs
-		debug("Rebel2 IACT: Low Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
-	} else {
-		debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
-	}
-}
-void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// Handle IACT opcode 2 subcases based on par3 (type). Mirrors FUN_00407fcb behavior where relevant.
-	// Keep existing linking behavior (par3 == 4) for compatibility.
-
-	// Link case: par3 == 4
-	if (par3 == 4) {
-		int16 childId = b.readSint16LE(); // Offset +8
-		int16 parentId = b.readSint16LE(); // Offset +10
-
-		// Validate BOTH parentId AND childId to avoid triggering "set/clear ALL bits" behavior
-		// when childId <= 0. The original game's setBit(0)/clearBit(0) affects ALL bits,
-		// which would disable/enable all enemies at once - not the intended linking behavior.
-		if (parentId >= 1 && parentId < 512 && childId >= 1 && childId < 512) {
-			// Shift links (original: 4 link slots at DAT_0045797c/817c/897c/917c)
-			_rebelLinks[parentId][2] = _rebelLinks[parentId][1];
-			_rebelLinks[parentId][1] = _rebelLinks[parentId][0];
-			_rebelLinks[parentId][0] = childId;
-
-			// Mirror parent's bit state to child (INVERTED):
-			// - Parent alive (bit clear) → setBit(child) → child hidden
-			// - Parent dead (bit set) → clearBit(child) → child shown
-			// From FUN_0041CADB case 0, par3==4:
-			//   bVar3 = FUN_00423970(parentId);
-			//   if (bVar3 == 0) setBit(childId); else clearBit(childId);
-			// This ensures linked children (explosion/death sprites) are hidden
-			// while the parent is alive, and revealed when the parent is destroyed.
-			if (!isBitSet(parentId)) {
-				setBit(childId);
-				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child DISABLED (parent alive)", childId, parentId);
-			} else {
-				clearBit(childId);
-				debug("Rebel2: Linked ID=%d to Parent=%d (Slot 0) - child ENABLED (parent dead)", childId, parentId);
-			}
-		} else {
-			debug("Rebel2: Skipping link with invalid IDs childId=%d parentId=%d", childId, parentId);
-		}
-		return;
-	} else if (par3 == 1) { // Probabilistic / counter cases: par3 == 1
-		int16 value = par4; // sVar6
-		int16 targetId = b.readSint16LE(); // Offset +8 (sVar7)
-
-		// Validate targetId >= 1 to avoid triggering "set/clear ALL bits" behavior
-		// The original game's setBit(0)/clearBit(0) affects ALL bits, not intended here
-		if (targetId < 1 || targetId >= 0x200)
-			return;
-
-		// Handler 8/25: FUN_401234 case 0 / FUN_0041CADB case 0 par3==1
-		// From original FUN_0041CADB:
-		//   if (par4 == 100) clearBit(body0);  // Force enable
-		//   else { bitMask = 1 << (par4 & 0x1f); if (waveState & bitMask) setBit(body0); }
-		if ((_rebelHandler == 8 || _rebelHandler == 25) && value != 0) {
-			if (value == 100) {
-				// par4==100: Force enable the target (original: FUN_00423a00)
-				clearBit(targetId);
-				debug("Rebel2 Opcode2 (H%d): Force ENABLE target=%d (par4=100)", _rebelHandler, targetId);
-			} else {
-				// Check wave state: if enemy type has been killed, disable target
-				int bitMask = 1 << (value & 0x1f);
-				if ((_rebelWaveState & bitMask) != 0) {
-					setBit(targetId);
-					debug("Rebel2 Opcode2 (H%d): Disable target=%d (type %d killed, wave=0x%x)", _rebelHandler, targetId, value, _rebelWaveState);
-				}
-			}
-			return;
-		}
-
-		if (value > 1 && value < 10) { // 1 < value < 10: random disable
-			if (_vm->_rnd.getRandomNumber(value) == 0) {
-				setBit(targetId);
-				debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
-			}
-		} else if (value > 10 && value < 20) { // 10 < value < 20: enable/disable with special value==11 = force enable
-			if (value == 11) {
-				clearBit(targetId);
-				debug("Rebel2 IACT Opcode2: FORCE ENABLE target=%d (value=11)", targetId);
-			} else {
-				if (_vm->_rnd.getRandomNumber(value - 10) == 0) {
-					clearBit(targetId);
-					debug("Rebel2 IACT Opcode2: Random ENABLE target=%d (value=%d)", targetId, value);
-				} else {
-					setBit(targetId);
-					debug("Rebel2 IACT Opcode2: Random DISABLE target=%d (value=%d)", targetId, value);
-				}
-			}
-		} else if (value > 99 && value < 110) { // 99 < value < 110: increment value counter if target active
-			if (!isBitSet(targetId)) {
-				int idx = value - 100;
-				if (idx >= 0 && idx < 10) {
-					_rebelValueCounters[idx]++;
-					_rebelLastCounter = _rebelValueCounters[idx];
-					debug("Rebel2 IACT Opcode2: Increment VAL counter[%d] -> %d (target=%d)", value, _rebelValueCounters[idx], targetId);
-				}
-			}
-
-		} else if (value > 0x3ff) { // Bitmask case: value > 0x3FF
- 			for (int slot = 1; slot <= 9; ++slot) {
-				if ((value & (1 << (slot - 1))) != 0) {
-					if (!isBitSet(targetId)) {
-						_rebelMaskCounters[slot]++;
-						_rebelLastCounter = _rebelMaskCounters[slot];
-						debug("Rebel2 IACT Opcode2: Increment MASK counter[%d] -> %d (target=%d)", slot, _rebelMaskCounters[slot], targetId);
-					}
-				}
-			}
-		}
-
-		// Unknown sub-type: log and return
-		debug("Rebel2 IACT Opcode2: Unhandled par3=%d par4=%d", par3, par4);
-	}
-}
-void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// IACT opcode 3 — damage and hit counter processing.
-	// Based on FUN_4092D9 (Handler 0x26), FUN_40E35E (Handler 7), FUN_401234 (Handler 8).
-	//
-	// The common dispatcher (FUN_4033CF) stores opcode 3 entries in the projectile impact
-	// list (DAT_0043f9e0). For handlers 0x26/7 these are processed per-frame by the
-	// per-handler collision function (FUN_4092D9/FUN_40E35E). For handlers 8/25 they're
-	// processed immediately during IACT dispatch.
-	//
-	// FUN_403ba9() loop in FUN_4092D9 (lines 209-239):
-	//   par3 == 1/2: Direct hit — increment hit counter, apply damage if conditions met
-	//     - body[0] (offset +8): srcId for isBitSet check
-	//     - par4 != 0: damage from DAT_0047e0f4 (direct hit damage table)
-	//     - par3==1: par4 must be 1..9 for damage
-	//     - par3==2: par4 must be > 99, with wave state bit check for par4 >= 101
-	//
-	//   par3 == 5: Probabilistic damage — probability check from DAT_0047e0fc
-	//     - body[1] (offset +10): srcId for isBitSet check (different from par3=1/2!)
-	//     - Damage from DAT_0047e0f8 (probabilistic damage table)
-	//
-	// Stream position on entry: at offset +8 (body[0], first word after 8-byte header)
-
-	// Handler 25 has a different opcode 3 structure (FUN_41CADB case 1):
-	//   par3==5: probabilistic damage WITH cover check (DAT_0045790a < 2)
-	//   par3==1: increment hit counter ONLY (NO damage), requires par4 != 4
-	//   par4==100: direct damage (separate check after par3 branches, NO cover check)
-	// Other handlers (0x26/7/8) use FUN_4092D9/FUN_40E35E/FUN_401234 with different logic.
-	if (_rebelHandler == 25) {
-		// Handler 25 opcode 3 — FUN_41CADB case 1
-		int16 srcIdBody0 = b.readSint16LE(); // body[0] (offset +8)
-		int16 srcIdBody1 = b.readSint16LE(); // body[1] (offset +10)
-
-		if (par3 == 5) {
-			// Probabilistic damage with cover check (lines 81-92)
-			debug("Rebel2 Opcode3: H25 par3=5 srcId=%d isBitSet=%d damageLevel=%d",
-				srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
-
-			if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
-				int probability = 20 + _difficulty * 20;
-				if (probability < 5) probability = 5;
-				if (probability > 90) probability = 90;
-				int roll = _vm->_rnd.getRandomNumber(99);
-				debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
-
-				if (roll < probability) {
-					if (!_rebelInvulnerable) {
-						int damageAmount = 5 + (_difficulty * 2);
-						_playerDamage += damageAmount;
-						if (_playerDamage > 255) _playerDamage = 255;
-						debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
-							srcIdBody1, damageAmount, _playerDamage);
-					}
-					initDamageFlash();
-				}
-			} else {
-				debug("Rebel2 Opcode3: H25 par3=5 BLOCKED (damageLevel=%d isBitSet=%d)",
-					_rebelDamageLevel, isBitSet(srcIdBody1));
-			}
-		} else if (par3 == 1 && !isBitSet(srcIdBody0) && par4 != 4) {
-			// Hit counter only — NO damage (lines 94-98)
-			_rebelHitCounter++;
-			debug("Rebel2: H25 hit counter++ -> %d (par3=1 par4=%d, no damage)",
-				_rebelHitCounter, par4);
-		} else {
-			debug("Rebel2 Opcode3: H25 par3=%d par4=%d (no action)", par3, par4);
-		}
-
-		// Direct damage: par4==100, separate from par3 branches (lines 99-111)
-		if (par4 == 100 && !isBitSet(srcIdBody0)) {
-			if (!_rebelInvulnerable) {
-				int directHitDamage = 8 + (_difficulty * 4);
-				_playerDamage += directHitDamage;
-				if (_playerDamage > 255) _playerDamage = 255;
-				debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
-					directHitDamage, _playerDamage);
-			}
-			initDamageFlash();
-		}
-	} else if (par3 == 1 || par3 == 2) {
-		// Non-Handler-25 direct hit path — FUN_4092D9 lines 209-227
-		int16 srcId = b.readSint16LE(); // body[0] (offset +8): source enemy ID
-
-		debug("Rebel2 Opcode3: par3=%d par4=%d srcId=%d isBitSet=%d",
-			par3, par4, srcId, isBitSet(srcId));
-
-		if (!isBitSet(srcId)) {
-			_rebelHitCounter++;
-			debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
-
-			int directHitDamage = 8 + (_difficulty * 4);
-
-			if (par4 != 0 && directHitDamage > 0) {
-				bool shouldDamage = false;
-
-				if (par3 == 1 && par4 < 10) {
-					shouldDamage = true;
-				} else if (par3 == 2 && par4 > 99) {
-					if (par4 < 0x65 || (_rebelPhaseState & (1 << ((par4 + 0x9b) & 0x1f))) == 0) {
-						shouldDamage = true;
-					}
-				}
-
-				if (shouldDamage) {
-					if (!_rebelInvulnerable) {
-						_playerDamage += directHitDamage;
-						if (_playerDamage > 255) _playerDamage = 255;
-						debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
-							srcId, par3, par4, directHitDamage, _playerDamage);
-					}
-					initDamageFlash();
-				}
-			}
-		}
-	} else if (par3 == 5) {
-		// Non-Handler-25 probabilistic damage — FUN_4092D9 lines 228-239
-		b.skip(2); // Skip body[0]
-		int16 srcId = b.readSint16LE(); // body[1] (offset +10)
-
-		debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
-
-		if (!isBitSet(srcId)) {
-			int probability = 20 + _difficulty * 20;
-			if (probability < 5) probability = 5;
-			if (probability > 90) probability = 90;
-
-			int roll = _vm->_rnd.getRandomNumber(99);
-			debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
-
-			if (roll < probability) {
-				if (!_rebelInvulnerable) {
-					int damageAmount = 5 + (_difficulty * 2);
-					_playerDamage += damageAmount;
-					if (_playerDamage > 255) _playerDamage = 255;
-					debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
-						srcId, damageAmount, _playerDamage);
-				}
-				if (_rebelHandler == 8) {
-					triggerDamageEffect();
-				} else {
-					initDamageFlash();
-				}
-			}
-		}
-	} else {
-		debug("Rebel2 Opcode3: UNHANDLED par3=%d par4=%d", par3, par4);
-	}
-}
-
-void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
-	// Opcode 6: Level setup / mode switch
-	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
-	//
-	// For Handler 8 (third-person on foot) - FUN_00401234 case 4:
-	// - par3 sets ship level mode (DAT_0043e000)
-	// - par4 == 1 triggers status bar display and state reset
-	// - Updates ship position based on mouse input
-	//
-	// For Handler 0x26/0x19 (turret/FPS):
-	// - Same par4 == 1 behavior
-	// - Different view offset calculations
-
-	debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
-
-	// Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
-	if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
-		// Reset Level 2 background flag when transitioning away from Handler 8
-		if (_rebelHandler == 8 && par2 != 8) {
-			_level2BackgroundLoaded = false;
-		}
-		_rebelHandler = par2;
-		debug("Rebel2 Opcode 6: Setting handler=%d", par2);
-	}
-
-	// Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4
-	if (_rebelHandler == 8) {
-		// Set ship level mode (DAT_0043e000 = par3)
-		_shipLevelMode = par3;
-
-		// If par4 == 1, enable status bar
-		if (par4 == 1) {
-			_rebelStatusBarSprite = 5;  // Status bar sprite for Handler 8
-		}
-
-		// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
-		if (_shipLevelMode != 0 && par4 == 1) {
-			// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
-			clearBit(0);
-			// Clear link tables
-			for (int i = 0; i < 512; i++) {
-				_rebelLinks[i][0] = 0;
-				_rebelLinks[i][1] = 0;
-				_rebelLinks[i][2] = 0;
-			}
-			// DAT_0047ab98 = DAT_0047ab9c: Reset wave state to accumulated phase state
-			_rebelWaveState = _rebelPhaseState;
-			debug("Rebel2 Opcode 6 (Handler 8): State reset, wave=0x%x", _rebelWaveState);
-		}
-
-		// Skip position calculation for special modes 4 and 5
-		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
-			// ===== Movement Range Transition (Covered vs Shooting) =====
-			// Based on FUN_00401234 lines 85-120:
-			// Mode 2 = "Covered" state - contract movement range to 41 (0x29)
-			// Other modes = "Shooting" state - expand movement range to 127 (0x7f)
-			// Transition happens gradually at ±10 per frame for smooth animation
-			if (_shipLevelMode == 2) {
-				// Covered state - contract movement range
-				if (_movementRangeLimit > 41) {
-					_movementRangeLimit -= 10;
-				}
-				if (_movementRangeLimit < 41) {
-					_movementRangeLimit = 41;
-				}
-			} else {
-				// Shooting state - expand movement range
-				if (_movementRangeLimit < 127) {
-					_movementRangeLimit += 10;
-				}
-				if (_movementRangeLimit > 127) {
-					_movementRangeLimit = 127;
-				}
-			}
-
-			// Calculate target position from mouse input
-			// Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
-			// Based on FUN_00401234 lines 151-166:
-			// local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
-			// local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
-
-			// Map mouse position (-127 to 127 range) to ship target
-			// Mouse is 0-320, center is 160. Map to -127 to 127 range
-			int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
-			int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
-
-			// Clamp X offset to movement range limit (covered/shooting state)
-			// Based on FUN_00401234 lines 119-136
-			if (mouseOffsetX > _movementRangeLimit) mouseOffsetX = _movementRangeLimit;
-			if (mouseOffsetX < -_movementRangeLimit) mouseOffsetX = -_movementRangeLimit;
-			// Y offset always uses full range (±127)
-			if (mouseOffsetY > 127) mouseOffsetY = 127;
-			if (mouseOffsetY < -127) mouseOffsetY = -127;
-
-			// Calculate target positions using the original formula
-			_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
-			_shipTargetY = (int16)(-((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
-
-			// Smooth interpolation toward target (max 50 pixels per frame)
-			const int16 maxStep = 50;  // 0x32 in hex
-			if (_shipPosX < _shipTargetX) {
-				int16 newX = _shipPosX + maxStep;
-				_shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
-			} else if (_shipPosX > _shipTargetX) {
-				int16 newX = _shipPosX - maxStep;
-				_shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
-			}
-
-			if (_shipPosY < _shipTargetY) {
-				int16 newY = _shipPosY + maxStep;
-				_shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
-			} else if (_shipPosY > _shipTargetY) {
-				int16 newY = _shipPosY - maxStep;
-				_shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
-			}
-
-			// Calculate ship direction indices for sprite selection
-			// Map mouse position to 5x7 direction grid (like Handler 7)
-			int16 mouseX = _vm->_mouse.x;
-			int16 mouseY = _vm->_mouse.y;
-
-			// Scale mouse if video is larger than 320x200
-			if (_player && _player->_width > 320) {
-				mouseX = (mouseX * 320) / _player->_width;
-			}
-			if (_player && _player->_height > 200) {
-				mouseY = (mouseY * 200) / _player->_height;
-			}
-
-			// Horizontal: 5 zones (0=far left, 2=center, 4=far right)
-			if (mouseX < 64) _shipDirectionH = 0;
-			else if (mouseX < 128) _shipDirectionH = 1;
-			else if (mouseX < 192) _shipDirectionH = 2;
-			else if (mouseX < 256) _shipDirectionH = 3;
-			else _shipDirectionH = 4;
-
-			// Vertical: 7 zones (0=far up, 3=center, 6=far down)
-			if (mouseY < 28) _shipDirectionV = 0;
-			else if (mouseY < 57) _shipDirectionV = 1;
-			else if (mouseY < 86) _shipDirectionV = 2;
-			else if (mouseY < 114) _shipDirectionV = 3;
-			else if (mouseY < 143) _shipDirectionV = 4;
-			else if (mouseY < 171) _shipDirectionV = 5;
-			else _shipDirectionV = 6;
-
-			_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
-		}
-
-		// Update firing state from mouse button
-		// Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
-		if (_shipLevelMode == 4) {
-			_shipFiring = false;
-		} else {
-			_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
-		}
-
-		debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
-			_shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
-			_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
-
-		// Handler 8 doesn't use the same view offset logic as other handlers
-		// Skip the rest of the function for Handler 8
-		return;
-	}
-
-	// Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
-	// Used for Level 3 and similar space combat levels
-	if (_rebelHandler == 7) {
-		// Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
-		// This determines collision mode and shooting capability:
-		//   Mode 0: Obstacle avoidance — SECONDARY zones, corridor boundaries
-		//   Mode 1: Tunnel flight — PRIMARY zones, per-edge push-back (hMargin=0x28)
-		//   Mode 2: Combat mode — shooting ENABLED, SECONDARY zones
-		//   Mode 3: Tunnel flight — PRIMARY zones, per-edge push-back (hMargin=0x0f)
-		_flyControlMode = par4;
-		debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
-			par4, (par4 == 2) ? "ENABLED" : "DISABLED");
-
-		// Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
-		// In our parsing, par3 maps to param_5[2] and the body follows par4.
-		// FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
-		// par3 is param_5[2], which the original doesn't use here.
-		// The body word for status bar is read separately below.
-		int16 bodyStatusFlag = 0;
-		if (b.size() - b.pos() >= 2) {
-			bodyStatusFlag = b.readSint16LE();
-		}
-		if (bodyStatusFlag == 1) {
-			_rebelStatusBarSprite = 5;  // Status bar sprite
-			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
-		}
-
-		// ============================================================
-		// Ship position update — FUN_40C3CC case 4, lines 49-327
-		// ============================================================
-		// Velocity-based physics with momentum/inertia:
-		//   Mouse offset from center → scaled input [-127,127]
-		//   → velocity history averaging → physics delta (clamped ±12/frame)
-		//   → position clamping → corridor collision → perspective offsets
-		//
-		// Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
-		//   offset 0: smoothing param (>>4 +1 = window size)
-		//   offset 2: Y speed          offset 4: X speed (levelSpeed)
-		//   offset 6: wind multiplier  offset 14: corridor damage
-		// We don't have the actual level data, so we use calibrated defaults.
-
-		// --- Step 1: Mouse input as offset from screen center ---
-		// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
-		// _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
-		// consistent with handler 8 which uses _vm->_mouse.x directly.
-		int16 inputX = (int16)(_vm->_mouse.x - 160);  // DAT_0047a7e0
-		int16 inputY = (int16)(_vm->_mouse.y - 100);  // DAT_0047a7e2
-
-		// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
-		if (inputX > 160) inputX = 160;
-		if (inputX < -160) inputX = -160;
-		if (inputY > 127) inputY = 127;
-		if (inputY < -127) inputY = -127;
-
-		// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
-		// Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
-		int16 local_c = (int16)((inputX * 127) / 160);
-		int16 local_14 = inputY;  // Y already in [-127, 127]
-
-		// --- Step 3: Velocity history + smoothed average (lines 141-157) ---
-		for (int i = 24; i > 0; i--) {
-			_velocityHistory[i] = _velocityHistory[i - 1];
-		}
-		_velocityHistory[0] = local_c;
-
-		// Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
-		const int smoothWindow = 5;
-		int velSum = 0;
-		for (int i = 0; i < smoothWindow; i++) {
-			velSum += _velocityHistory[i];
-		}
-		_smoothedVelocity = (int16)(velSum / smoothWindow);  // DAT_0044370c
-
-		// --- Step 4: Wind history (lines 158-173) ---
-		// Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
-		const int16 windMult = 0;
-		int windSumX = 0, windSumY = 0;
-		for (int i = 14; i > 0; i--) {
-			_windHistoryX[i] = _windHistoryX[i - 1];
-			windSumX += _windHistoryX[i];
-		}
-		_windHistoryX[0] = _windParamX;
-		int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
-
-		for (int i = 14; i > 0; i--) {
-			_windHistoryY[i] = _windHistoryY[i - 1];
-			windSumY += _windHistoryY[i];
-		}
-		_windHistoryY[0] = _windParamY;
-		int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
-
-		// --- Step 5: Position delta (lines 174-242) ---
-		// levelSpeed (offset 4): calibrated so max velocity (127) → delta 12.
-		//   8 = (speed * 127) >> 9 → speed ≈ 32
-		// levelYSpeed (offset 2): calibrated so max input (127) → delta ~6.
-		//   6 = (speed * 127) >> 10 → speed ≈ 48
-		const int16 levelSpeed = 32;
-		const int16 levelYSpeed = 48;
-		int16 absSmoothVel = ABS(_smoothedVelocity);
-		int16 positionDeltaX;
-
-		if (_flyControlMode == 1) {
-			// Mode 1: Full cross-axis coupling (lines 174-186)
-			// Banking: vertical input deflects horizontal movement
-			if (local_c < 1) {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * local_14 - windEffectX) >> 9);
-			} else {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * local_14 - windEffectX) >> 9);
-			}
-		} else {
-			// Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
-			if (local_c < 1) {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
-			} else {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
-			}
-		}
-
-		// Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
-		if (positionDeltaX < -11) positionDeltaX = -12;
-		if (positionDeltaX > 11) positionDeltaX = 12;
-
-		// Apply X delta (line 193 / 237)
-		_flyShipScreenX += positionDeltaX;
-
-		// Y delta
-		if (_flyControlMode == 1) {
-			// Mode 1: clamped to ±12 with wind (lines 194-216)
-			int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
-			int yDelta = yCalc >> 10;
-			if (yDelta < -12) yDelta = -12;
-			if (yDelta > 12) yDelta = 12;
-			_flyShipScreenY -= (int16)yDelta;
-		} else {
-			// Mode 0/2/3: unclamped (lines 238-241)
-			_flyShipScreenY -= (int16)((levelYSpeed * local_14) >> 10);
-		}
-
-		// Store vertical input for direction sprite (line 243)
-		_verticalInput = local_14;  // DAT_0044370e
-
-		// Ship facing direction (line 244)
-		_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
-
-		// --- Step 6: Position clamping (lines 245-256) ---
-		if (_flyShipScreenX > 0x194) _flyShipScreenX = 0x194;  // 404
-		if (_flyShipScreenY > 0xF0) _flyShipScreenY = 0xF0;    // 240
-		if (_flyShipScreenX < 0x14) _flyShipScreenX = 0x14;    // 20
-		if (_flyShipScreenY < 0x14) _flyShipScreenY = 0x14;    // 20
-
-		// --- Step 7: Corridor collision — mode 0/2 only (lines 257-292) ---
-		if (_flyControlMode == 0 || _flyControlMode == 2) {
-			// Right boundary (lines 258-270)
-			// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
-			if (_corridorRightX < _flyShipScreenX) {
-				_flyShipScreenX = _corridorRightX;
-				if (_hitCooldown < 5) {
-					for (int i = 0; i < 25; i++) _velocityHistory[i] = -127;
-					_hitCooldown = 10;
-					_spaceShotDirection = 1;
-					initDamageFlash();
-					if (!_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
-					}
-					_rebelHitCounter++;
-					playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
-				}
-			}
-			// Left boundary (lines 271-283)
-			if (_flyShipScreenX < _corridorLeftX) {
-				_flyShipScreenX = _corridorLeftX;
-				if (_hitCooldown < 5) {
-					for (int i = 0; i < 25; i++) _velocityHistory[i] = 127;
-					_hitCooldown = 10;
-					_spaceShotDirection = 0;
-					initDamageFlash();
-					if (!_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
-					}
-					_rebelHitCounter++;
-					playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
-				}
-			}
-			// Y boundary clamping — no damage (lines 285-292)
-			if (_corridorBottomY < _flyShipScreenY) {
-				_flyShipScreenY = _corridorBottomY;
-			}
-			if (_flyShipScreenY < _corridorTopY) {
-				_flyShipScreenY = _corridorTopY;
-			}
-		}
-
-		// --- Step 8: Perspective offsets (lines 293-316) ---
-		// f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
-		// Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d
-		// Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19
-		{
-			int absOffX = ABS(_flyShipScreenX - 0xd4);
-			int16 focalX = 0x2b;  // Far view default for Level 3
-			if (absOffX > 0) {
-				_perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
-					((0xd4 - focalX) * absOffX + focalX * 0xd4));
-			} else {
-				_perspectiveX = 0;
-			}
-			if (_flyShipScreenX < 0xd5) _perspectiveX = -_perspectiveX;
-
-			int absOffY = ABS(_flyShipScreenY - 0x82);
-			int16 focalY = 0x19;  // Far view default for Level 3
-			if (absOffY > 0) {
-				_perspectiveY = (int16)((focalY * 0x82 * absOffY) /
-					((0x82 - focalY) * absOffY + focalY * 0x82));
-			} else {
-				_perspectiveY = 0;
-			}
-			if (_flyShipScreenY < 0x83) _perspectiveY = -_perspectiveY;
-		}
-
-		// View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
-		_viewShift = _smoothedVelocity;
-		if (_viewShift > 127) _viewShift = 127;
-		if (_viewShift < -127) _viewShift = -127;
-
-		// --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
-		// 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
-		// vDir from vertical input: (0xa0 - verticalInput) >> 6
-		int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
-		if (vDir < 0) vDir = 0;
-		if (vDir > 4) vDir = 4;
-
-		// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
-		int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
-		if (hDir < 0) hDir = 0;
-		if (hDir > 6) hDir = 6;
-
-		// Hysteresis at center (lines 90-97, 98-105)
-		if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
-			hDir = (_smoothedVelocity < 1) ? 4 : 2;
-		}
-		if (vDir == 2 && ABS(_verticalInput) > 15) {
-			vDir = (_verticalInput < 1) ? 3 : 1;
-		}
-
-		_shipDirectionIndex = vDir * 7 + hDir;
-		if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
-		if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
-
-		_shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
-
-		debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
-			_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
-			_verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
-
-		return;
-	}
-
-	// Handler 25 (0x19) specific logic (mixed mode - speeder bike)
-	// Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
-	if (_rebelHandler == 25) {
-		// Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code)
-		// The stream position should be at offset 8 after par4 was read
-		// From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
-		int16 par5 = 0;
-		if (b.pos() + 2 <= b.size()) {
-			int64 savedPos = b.pos();
-			par5 = b.readSint16LE();
-			b.seek(savedPos);  // Don't consume the stream
-		}
-
-		// If par5 == 1, enable status bar and reset state (lines 114-121)
-		// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
-		if (par5 == 1) {
-			_rebelStatusBarSprite = 5;
-			// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
-			// at IACT callback registration time. Each new wave video starts with
-			// a clean bit table so enemy IDs reused across videos work correctly.
-			clearBit(0);
-			// Reset link tables (DAT_0045797c through DAT_0045917c)
-			for (int i = 0; i < 512; i++) {
-				_rebelLinks[i][0] = 0;
-				_rebelLinks[i][1] = 0;
-				_rebelLinks[i][2] = 0;
-			}
-			// Reset wave state to accumulated phase state (same as Handler 8)
-			// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
-			_rebelWaveState = _rebelPhaseState;
-			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
-				_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
-		}
-
-		// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
-		// From FUN_0041cadb line 122: DAT_00457900 = local_14[3];
-		// In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4
-		// Mode 1: Uncovered, shooting position - sprite on left
-		// Mode 2: Covered, vertical shift
-		// Mode 3: Transition between covered/uncovered - sprite position depends on direction
-		// Mode 4: Alternative uncovered position - sprite on right
-		_grdSpriteMode = par4;  // local_14[3] maps to par4 (offset 6-7)
-
-		debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
-			par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
-
-		// Autopilot logic (lines 123-146)
-		// From original FUN_0041cadb - NO damageLevel check, toggle happens immediately
-		// The damage level counter provides the smooth visual transition
-		if (!_rebelInvulnerable) {
-			if (_rebelAutopilot == 0) {
-				// Uncovered: RIGHT button enters cover
-				if ((_rebelControlMode & 2) != 0) {
-					_rebelAutopilot = 1;
-					debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
-				}
-			} else {
-				// Covered: ANY button exits cover
-				if (_rebelControlMode != 0) {
-					_rebelAutopilot = 0;
-					debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
-				}
-			}
-			// Clear control mode after processing (sticky flags consumed)
-			_rebelControlMode = 0;
-		} else {
-			// Invulnerable mode: random autopilot changes
-			if (_rebelAutopilot == 0) {
-				if (_vm->_rnd.getRandomNumber(100) == 0) {
-					_rebelAutopilot = 1;
-				}
-			} else {
-				if (_vm->_rnd.getRandomNumber(15) == 0) {
-					_rebelAutopilot = 0;
-					_rebelFlightDir = _vm->_rnd.getRandomNumber(2);
-				}
-			}
-		}
-
-		// Update damage level counter (lines 147-154)
-		// This provides the smooth transition animation between covered/uncovered states
-		int prevDamageLevel = _rebelDamageLevel;
-		if (_rebelAutopilot == 0) {
-			// Uncovered: decrement damage level towards 0
-			if (_rebelDamageLevel > 0) {
-				_rebelDamageLevel--;
-			}
-		} else {
-			// Covered: increment damage level towards 5
-			if (_rebelDamageLevel < 5) {
-				_rebelDamageLevel++;
-			}
-		}
-		if (_rebelDamageLevel != prevDamageLevel) {
-			debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
-				prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
-		}
-
-		// Flight direction logic for mode 3 (lines 155-177)
-		if (_grdSpriteMode == 3) {
-			if (_rebelDamageLevel == 5) {
-				// At max damage, check for direction change input
-				// For now, use mouse X position to determine direction
-				int16 mouseX = _vm->_mouse.x;
-				if (_player && _player->_width > 320) {
-					mouseX = (mouseX * 320) / _player->_width;
-				}
-				if (mouseX > 235) {  // 0x4b + 160 = 235
-					_rebelFlightDir = 1;
-				}
-				if (mouseX < 85) {   // 160 - 0x4b = 85
-					_rebelFlightDir = 0;
-				}
-			}
-		} else {
-			_rebelFlightDir = 0;
-		}
-
-		// Calculate sprite and view offset positions based on mode (lines 182-213)
-		// DAT_0045790c = view offset X (for corridor overlay)
-		// DAT_0045790e = view offset Y (for corridor overlay)
-		// DAT_00457910 = sprite position X (relative to center)
-		// DAT_00457912 = sprite position Y (relative to center)
-		if (_grdSpriteMode == 1) {
-			// Mode 1: Uncovered, shooting - sprite shifts left as damage increases
-			_rebelViewMode1 = 0x0e;
-			_rebelViewMode2 = 0;
-			_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;   // DAT_0045790c
-			_rebelViewOffset2X = _rebelDamageLevel * -22;       // DAT_00457910
-			_rebelViewOffsetY = 0;                              // DAT_0045790e
-			_rebelViewOffset2Y = 0;                             // DAT_00457912
-		} else if (_grdSpriteMode == 4) {
-			// Mode 4: Alternative uncovered - sprite shifts right
-			_rebelViewMode1 = 0x22;
-			_rebelViewMode2 = 0;
-			_rebelViewOffsetX = _rebelDamageLevel * 10 + -16;   // DAT_0045790c
-			_rebelViewOffset2X = _rebelDamageLevel * 17 + -85;  // DAT_00457910 (0x11 = 17, -0x55 = -85)
-			_rebelViewOffsetY = 0;
-			_rebelViewOffset2Y = 0;
-		} else if (_grdSpriteMode == 2) {
-			// Mode 2: Covered - vertical shift
-			_rebelViewMode1 = 0;
-			_rebelViewMode2 = 0x0e;
-			_rebelViewOffsetY = _rebelDamageLevel * -5 + -14;   // DAT_0045790e
-			_rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60;  // DAT_00457912 (0xf = 15, -0x3c = -60)
-			_rebelViewOffsetX = 0;
-			_rebelViewOffset2X = 0;
-		} else if (_grdSpriteMode == 3) {
-			// Mode 3: Transition - direction-dependent horizontal shift
-			_rebelViewMode1 = 0x0f;
-			_rebelViewMode2 = 0;
-			// (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
-			int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
-			// (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
-			int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
-			_rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15;  // DAT_0045790c
-			_rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel);      // DAT_00457910
-			_rebelViewOffsetY = 0;
-			_rebelViewOffset2Y = 0;
-		} else {
-			// Mode 0 or unknown: use Mode 1 defaults as fallback
-			_rebelViewMode1 = 0x0e;
-			_rebelViewMode2 = 0;
-			_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
-			_rebelViewOffset2X = _rebelDamageLevel * -22;
-			_rebelViewOffsetY = 0;
-			_rebelViewOffset2Y = 0;
-			debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
-		}
-
-		debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
-			_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
-			_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
-
-		// Set FOBJ position offsets (FUN_00424510 in original, line 214)
-		// All subsequent FOBJs in this frame will be shifted by these offsets
-		if (_player) {
-			_player->_fobjOffsetX = _rebelViewOffsetX;
-			_player->_fobjOffsetY = _rebelViewOffsetY;
-		}
-
-		// Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216)
-		// This wipes previous frame content so codec 23 delta skip regions show clean corridor
-		if (renderBitmap) {
-			EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
-			if (corridorOverlay.valid && corridorOverlay.pixels) {
-				int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
-				int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
-				int srcOffsetX = 0;
-				int srcOffsetY = 0;
-				int destX = _rebelViewOffsetX;
-				int destY = _rebelViewOffsetY;
-				int drawWidth = corridorOverlay.width;
-				int drawHeight = corridorOverlay.height;
-
-				if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
-				if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
-				if (destX + drawWidth > pitch) drawWidth = pitch - destX;
-				if (destY + drawHeight > bufHeight) drawHeight = bufHeight - destY;
-				if (drawWidth > corridorOverlay.width - srcOffsetX) drawWidth = corridorOverlay.width - srcOffsetX;
-				if (drawHeight > corridorOverlay.height - srcOffsetY) drawHeight = corridorOverlay.height - srcOffsetY;
-
-				if (drawWidth > 0 && drawHeight > 0) {
-					for (int y = 0; y < drawHeight; y++) {
-						memcpy(renderBitmap + (destY + y) * pitch + destX,
-							   corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
-							   drawWidth);
-					}
-				}
-				debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
-					_rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
-			}
-		}
-
-		return;
-	}
-
-	// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
-	if (par4 == 1) {
-		// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
-		_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
-		debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
-
-		// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
-		clearBit(0);
-
-		// Clear link tables (DAT_0045797c through DAT_0045917c)
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
-
-		// DAT_0047ab98 = DAT_0047ab9c: At the start of each wave video,
-		// reset wave state to accumulated phase state. Enemies killed in
-		// previous waves stay killed; new kills add during this wave.
-		_rebelWaveState = _rebelPhaseState;
-		_rebelHitCounter = 0;
-		debug("Rebel2 Opcode 6: Wave state reset to phase state 0x%x", _rebelWaveState);
-	}
-
-	// Step 2: Set level type (DAT_00457900 = par3)
-	_rebelLevelType = par3;
-
-	// Step 3: Autopilot/control mode logic (lines 123-146)
-	// This determines whether the ship flies on autopilot or manual control
-	if (!_rebelInvulnerable) {
-		// Normal mode: check control mode flags
-		if (_rebelAutopilot == 0) {
-			if ((_rebelControlMode & 2) != 0) {
-				_rebelAutopilot = 1;
-			}
-		} else {
-			if (_rebelControlMode != 0) {
-				_rebelAutopilot = 0;
-			}
-		}
-	} else {
-		// Invulnerable mode: random autopilot changes
-		if (_rebelAutopilot == 0) {
-			if (_vm->_rnd.getRandomNumber(100) == 0) {
-				_rebelAutopilot = 1;
-			}
-		} else {
-			if (_vm->_rnd.getRandomNumber(15) == 0) {
-				_rebelAutopilot = 0;
-				_rebelFlightDir = _vm->_rnd.getRandomNumber(2);
-			}
-		}
-	}
-
-	// Step 4: Update damage level counter (lines 147-154)
-	if (_rebelAutopilot == 0) {
-		if (_rebelDamageLevel > 0) {
-			_rebelDamageLevel--;
-		}
-	} else {
-		if (_rebelDamageLevel < 5) {
-			_rebelDamageLevel++;
-		}
-	}
-
-	// Handle level type 3 special direction logic (lines 155-181)
-	if (_rebelLevelType == 3) {
-		if (_rebelDamageLevel == 5) {
-			// Check for joystick/key input to change direction
-			// Simplified: use mouse position
-			if (_vm->_mouse.x > 75) {
-				_rebelFlightDir = 1;
-			}
-			if (_vm->_mouse.x < -75) {
-				_rebelFlightDir = 0;
-			}
-		}
-	} else {
-		_rebelFlightDir = 0;
-	}
-
-	// Step 5: Calculate view offsets based on level type (lines 182-213)
-	switch (_rebelLevelType) {
-	case 1:
-		// Type 1: Vertical movement
-		_rebelViewMode1 = 0x0e;
-		_rebelViewMode2 = 0;
-		_rebelViewOffsetX = _rebelDamageLevel * -5 - 0x0e;
-		_rebelViewOffset2X = _rebelDamageLevel * -0x16;
-		_rebelViewOffsetY = 0;
-		_rebelViewOffset2Y = 0;
-		break;
-
-	case 4:
-		// Type 4: Different vertical movement
-		_rebelViewMode1 = 0x22;
-		_rebelViewMode2 = 0;
-		_rebelViewOffsetX = _rebelDamageLevel * 10 - 0x10;
-		_rebelViewOffset2X = _rebelDamageLevel * 0x11 - 0x55;
-		_rebelViewOffsetY = 0;
-		_rebelViewOffset2Y = 0;
-		break;
-
-	case 2:
-		// Type 2: Horizontal movement
-		_rebelViewMode1 = 0;
-		_rebelViewMode2 = 0x0e;
-		_rebelViewOffsetY = _rebelDamageLevel * -5 - 0x0e;
-		_rebelViewOffset2Y = (5 - _rebelDamageLevel) * 0x0f - 0x3c;
-		_rebelViewOffsetX = 0;
-		_rebelViewOffset2X = 0;
-		break;
-
-	case 3:
-		// Type 3: Direction-based movement
-		_rebelViewMode1 = 0x0f;
-		_rebelViewMode2 = 0;
-		{
-			int dirFactor = (_rebelFlightDir == 0) ? 3 : -3;  // (-(ushort)(DAT_00457902 == 0) & 6) - 3
-			int dirFactor2 = (_rebelFlightDir == 0) ? 0x14 : -0x14;  // (-(ushort)(DAT_00457902 == 0) & 0x28) - 0x14
-			_rebelViewOffsetX = dirFactor * (5 - _rebelDamageLevel) - 0x0f;
-			_rebelViewOffset2X = dirFactor2 * (5 - _rebelDamageLevel);
-		}
-		_rebelViewOffsetY = 0;
-		_rebelViewOffset2Y = 0;
-		break;
-
-	default:
-		// Default: No special offsets
-		_rebelViewMode1 = 0;
-		_rebelViewMode2 = 0;
-		_rebelViewOffsetX = 0;
-		_rebelViewOffsetY = 0;
-		_rebelViewOffset2X = 0;
-		_rebelViewOffset2Y = 0;
-		break;
-	}
-
-	debug("Rebel2 Opcode 6: levelType=%d autopilot=%d damageLevel=%d viewOffset=(%d,%d)",
-		_rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
-
-	// Detect and load embedded ANIM (SAN) within the remaining IACT payload
-	// Note: chunkSize is the remaining IACT payload size after par1-par4 header
-	{
-		int64 startPos = b.pos();
-		// Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream)
-		int64 remaining = chunkSize;
-		if (remaining > 0) {
-			int scanSize = (int)MIN<int64>(remaining, 65536);
-			byte *scanBuf = (byte *)malloc(scanSize);
-			if (scanBuf) {
-				int bytesRead = b.read(scanBuf, scanSize);
-				for (int i = 0; i + 8 <= bytesRead; ++i) {
-					if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
-						int64 animStreamPos = startPos + i;
-						uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-						// Limit to remaining IACT payload (chunkSize - offset into payload)
-						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
-						if (toCopy > 0) {
-							byte *animData = (byte *)malloc(toCopy);
-							if (animData) {
-								b.seek(animStreamPos);
-								b.read(animData, toCopy);
-								loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
-								free(animData);
-							}
-						}
-						b.seek(startPos);
-						free(scanBuf);
-						return;
-					}
-				}
-				b.seek(startPos);
-				free(scanBuf);
-			}
-		}
-	}
-}
-
-void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
-	// Opcode 8: HUD/Ship resource loading
-	// Dispatches to handler-specific loading functions based on current handler and parameters.
-	//
-	// Handler-specific routing (based on retail disassembly):
-	//   Handler 7 (FUN_0040c3cc):  FLY NUT sprites via par4 (1, 2, 3, 11)
-	//   Handler 8 (FUN_00401234):  POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
-	//   Handler 0x26 (FUN_00407fcb): Turret HUD NUT via par3 (1-4)
-	//   Handler 0x19: Mixed turret mode, similar to 0x26
-	//
-	// Sound loading: par3 in range 21-47
-
-	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
-		_rebelHandler, par2, par3, par4, _gameState);
-
-	int64 startPos = b.pos();
-	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
-
-	// ===== Handler 7: FLY NUT Loading (Third-Person Ship) =====
-	// FUN_0040c3cc case 6: par4 determines FLY sprite slot
-	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
-	if (isHandler7FLY && remaining >= 14) {
-		if (loadHandler7FlySprites(b, remaining, par4)) {
-			b.seek(startPos);
-			return;
-		}
-		b.seek(startPos);
-	}
-
-	// ===== Sound Loading (par3 21-47) =====
-	if (par3 >= 21 && par3 <= 47) {
-		debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
-		// TODO: Implement sound loading via FUN_004118df equivalent
-		return;
-	}
-
-	// ===== Scan for embedded ANIM data =====
-	// Remaining handlers require finding ANIM tag in the stream
-	debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
-		(long long)startPos, (long long)remaining);
-
-	if (remaining <= 0) {
-		return;
-	}
-
-	int scanSize = (int)MIN<int64>(remaining, 65536);
-	byte *scanBuf = (byte *)malloc(scanSize);
-	if (!scanBuf) {
-		return;
-	}
-
-	int bytesRead = b.read(scanBuf, scanSize);
-	debug("Rebel2 Opcode 8: Read %d bytes for ANIM scan", bytesRead);
-
-	// Find ANIM tag
-	int animOffset = -1;
-	for (int i = 0; i + 8 <= bytesRead; ++i) {
-		if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
-			animOffset = i;
-			debug("Rebel2 Opcode 8: Found ANIM at offset %d", i);
-			break;
-		}
-	}
-
-	if (animOffset < 0) {
-		debug("Rebel2 Opcode 8: No ANIM tag found");
-		free(scanBuf);
-		b.seek(startPos);
-		return;
-	}
-
-	// Extract ANIM data
-	uint32 animReportedSize = READ_BE_UINT32(scanBuf + animOffset + 4);
-	int32 animDataSize = (int)MIN<int64>((int64)animReportedSize + 8, remaining - animOffset);
-	if (animDataSize <= 0) {
-		free(scanBuf);
-		b.seek(startPos);
-		return;
-	}
-
-	byte *animData = (byte *)malloc(animDataSize);
-	if (!animData) {
-		free(scanBuf);
-		b.seek(startPos);
-		return;
-	}
-
-	b.seek(startPos + animOffset);
-	b.read(animData, animDataSize);
-
-	bool handled = false;
-
-	// ===== Handler 0x26/0x19: Turret HUD Overlays =====
-	// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
-	if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
-		if (par3 >= 1 && par3 <= 4) {
-			handled = loadTurretHudOverlay(animData, animDataSize, par3);
-		}
-	}
-
-	// ===== Handler 8: POV Ship Sprites or Background =====
-	// FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
-	// NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
-	if (!handled && _rebelHandler == 8) {
-		// Check for background loading first (par4=5)
-		if (par4 == 5) {
-			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
-		}
-		// Check for POV NUT sprites (par4=1,3,6,7)
-		else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
-			handled = loadHandler8ShipSprites(animData, animDataSize, par4);
-		}
-	}
-
-	// ===== Handler 25 (0x19): Level 2 GRD Ship Sprites and Background =====
-	// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
-	//   par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
-	//   par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
-	//   par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
-	//   par4=5: 320x200 background -> DAT_0048226c
-	//   par4=6: Overlay -> DAT_00482250, draws immediately
-	//   par4=7: Overlay -> DAT_00482248, draws immediately
-	if (!handled && _rebelHandler == 25) {
-		if (par4 == 1 || par4 == 2) {
-			// GRD ship sprites - load into NutRenderer for per-frame rendering
-			handled = loadHandler25GrdSprites(animData, animDataSize, par4);
-		} else if (par4 == 5) {
-			// Background (320x200) - stored for per-frame restoration
-			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
-		} else if (par4 == 4 || par4 == 6 || par4 == 7) {
-			// Overlays - draw immediately to renderBitmap
-			// These complete the visual scene along with the background
-			debug("Rebel2 Opcode 8: Handler 25 overlay par4=%d - drawing to screen", par4);
-			loadEmbeddedSan(par4, animData, animDataSize, renderBitmap);
-			handled = true;
-		}
-	}
-
-	// ===== Fallback: Embedded SAN HUD overlays =====
-	// For other cases, load as embedded SAN frame to HUD overlay slots
-	if (!handled) {
-		// Skip high-res data (par3 == 2, 4)
-		if (par3 == 2 || par3 == 4) {
-			debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
-			handled = true;
-		} else {
-			// Determine userId: Handler 0x19 uses par3, others use par4
-			// Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3
-			int userId;
-			bool usePar3 = (_rebelHandler == 0x19);
-			if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
-				usePar3 = true;
-			}
-			userId = usePar3 ? par3 : par4;
-
-			// Skip audio tracks (userId >= 1000)
-			if (userId > 0 && userId < 1000) {
-				debug("Rebel2 Opcode 8: Loading embedded SAN HUD userId=%d (handler=%d par3=%d par4=%d)",
-					userId, _rebelHandler, par3, par4);
-				loadEmbeddedSan(userId, animData, animDataSize, renderBitmap);
-				handled = true;
-			}
-		}
-	}
-
-	if (!handled) {
-		debug("Rebel2 Opcode 8: Unhandled case - handler=%d par3=%d par4=%d", _rebelHandler, par3, par4);
-	}
-
-	free(animData);
-	free(scanBuf);
-	b.seek(startPos);
-}
-
-// ======================= Opcode 8 Helper Functions =======================
-// These helper functions are extracted from the original monolithic iactRebel2Opcode8
-// to improve code readability and match the retail FUN_* function structure.
-
-bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4) {
-	// Handler 7 FLY NUT loading - FUN_0040c3cc case 6 (opcode 8)
-	// IACT structure after par1-par4 (we're at offset +8):
-	//   +0-5 (6 bytes): additional header
-	//   +6-9 (4 bytes): NUT data size (little-endian)
-	//   +10+: NUT data
-	//
-	// par4 values (param_5[3] - 1 in assembly):
-	//   1 -> case 0: FLY001 - Ship direction sprites (DAT_0047fee8)
-	//   2 -> case 1: FLY003 - Targeting overlay (DAT_0047fef8)
-	//   3 -> case 2: FLY002 - Laser fire sprites (DAT_0047fef0)
-	//  11 -> case 10: FLY004 - High-res alternative (DAT_0047ff00)
-
-	if (remaining < 14) {
-		return false;
-	}
-
-	// Read additional header and size from fixed offset
-	byte header[10];
-	if (b.read(header, 10) != 10) {
-		return false;
-	}
-
-	debug("Rebel2 loadHandler7FlySprites: header bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-		header[0], header[1], header[2], header[3], header[4],
-		header[5], header[6], header[7], header[8], header[9]);
-
-	// Size is at offset 14 from IACT start = bytes 6-9 of our header buffer
-	uint32 nutSize = READ_LE_UINT32(header + 6);
-	debug("Rebel2 loadHandler7FlySprites: par4=%d nutSize=%u remaining=%lld",
-		par4, nutSize, (long long)remaining);
-
-	if (nutSize == 0 || nutSize > (uint32)(remaining - 10)) {
-		return false;
-	}
-
-	byte *nutData = (byte *)malloc(nutSize);
-	if (!nutData) {
-		return false;
-	}
-
-	int bytesRead = b.read(nutData, nutSize);
-	debug("Rebel2 loadHandler7FlySprites: Read %d/%u bytes, first 16: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-		bytesRead, nutSize,
-		bytesRead > 0 ? nutData[0] : 0, bytesRead > 1 ? nutData[1] : 0,
-		bytesRead > 2 ? nutData[2] : 0, bytesRead > 3 ? nutData[3] : 0,
-		bytesRead > 4 ? nutData[4] : 0, bytesRead > 5 ? nutData[5] : 0,
-		bytesRead > 6 ? nutData[6] : 0, bytesRead > 7 ? nutData[7] : 0,
-		bytesRead > 8 ? nutData[8] : 0, bytesRead > 9 ? nutData[9] : 0,
-		bytesRead > 10 ? nutData[10] : 0, bytesRead > 11 ? nutData[11] : 0,
-		bytesRead > 12 ? nutData[12] : 0, bytesRead > 13 ? nutData[13] : 0,
-		bytesRead > 14 ? nutData[14] : 0, bytesRead > 15 ? nutData[15] : 0);
-
-	if (bytesRead != (int)nutSize) {
-		warning("Rebel2 loadHandler7FlySprites: Short read! Got %d expected %u", bytesRead, nutSize);
-		free(nutData);
-		return false;
-	}
-
-	// Verify ANIM header
-	if (bytesRead >= 8) {
-		uint32 animTag = READ_BE_UINT32(nutData);
-		if (animTag != MKTAG('A','N','I','M')) {
-			warning("Rebel2 loadHandler7FlySprites: No ANIM tag! Data may be corrupted");
-			free(nutData);
-			return false;
-		}
-	}
-
-	// Load as NUT
-	NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
-	if (!newNut || newNut->getNumChars() <= 0) {
-		debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
-		delete newNut;
-		free(nutData);
-		return false;
-	}
-
-	debug("Rebel2 loadHandler7FlySprites: Loaded FLY NUT par4=%d with %d sprites",
-		par4, newNut->getNumChars());
-
-	// Assign to appropriate slot based on par4 (matches FUN_0040c3cc case 6 switch)
-	bool assigned = true;
-	switch (par4) {
-	case 1:  // FLY001 - Ship direction sprites (35 frames)
-		delete _flyShipSprite;
-		_flyShipSprite = newNut;
-		debug("Rebel2: _flyShipSprite set with %d sprites", newNut->getNumChars());
-		break;
-	case 2:  // FLY003 - Targeting overlay
-		delete _flyTargetSprite;
-		_flyTargetSprite = newNut;
-		break;
-	case 3:  // FLY002 - Laser fire sprites
-		delete _flyLaserSprite;
-		_flyLaserSprite = newNut;
-		break;
-	case 11: // FLY004 - High-res alternative
-		delete _flyHiResSprite;
-		_flyHiResSprite = newNut;
-		break;
-	default:
-		delete newNut;
-		assigned = false;
-		break;
-	}
-
-	free(nutData);
-	return assigned;
-}
-
-bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
-	// Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
-	// Resolution-dependent loading:
-	//   par3 == 1: Low-res primary HUD (DAT_0047fe78 / _hudOverlayNut)
-	//   par3 == 2: High-res primary HUD (skip in 320x200 mode)
-	//   par3 == 3: Low-res secondary HUD (DAT_0047fe80 / _hudOverlay2Nut)
-	//   par3 == 4: High-res secondary HUD (skip in 320x200 mode)
-
-	if (!animData || size <= 0) {
-		return false;
-	}
-
-	// ScummVM runs at 320x200 (low-res), skip high-res data
-	if (par3 == 2 || par3 == 4) {
-		debug("Rebel2 loadTurretHudOverlay: Skipping high-res HUD par3=%d (running in low-res mode)", par3);
-		return true;  // Successfully "handled" by skipping
-	}
-
-	if (par3 != 1 && par3 != 3) {
-		return false;  // Not a turret HUD slot
-	}
-
-	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
-	if (!newNut || newNut->getNumChars() <= 0) {
-		debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
-		delete newNut;
-		return false;
-	}
-
-	debug("Rebel2 loadTurretHudOverlay: Loaded turret HUD NUT par3=%d with %d sprites",
-		par3, newNut->getNumChars());
-
-	if (par3 == 1) {
-		// Low-res primary HUD overlay
-		delete _hudOverlayNut;
-		_hudOverlayNut = newNut;
-	} else {  // par3 == 3
-		// Low-res secondary HUD overlay
-		delete _hudOverlay2Nut;
-		_hudOverlay2Nut = newNut;
-	}
-
-	return true;
-}
-
-bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par4) {
-	// Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
-	// par4 values (from IACT data offset +6, NOT par3 which is always 0):
-	//   1: POV001 - Primary ship sprite (DAT_0047e010 / _shipSprite)
-	//   3: POV004 - Secondary ship sprite (DAT_0047e028 / _shipSprite2)
-	//   6: POV002 - Ship overlay 1 (DAT_0047e020 / _shipOverlay1)
-	//   7: POV003 - Ship overlay 2 (DAT_0047e018 / _shipOverlay2)
-
-	if (!animData || size <= 0) {
-		return false;
-	}
-
-	// Only handle valid POV sprite slots
-	if (par4 != 1 && par4 != 3 && par4 != 6 && par4 != 7) {
-		return false;
-	}
-
-	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
-	if (!newNut || newNut->getNumChars() <= 0) {
-		debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
-		delete newNut;
-		return false;
-	}
-
-	debug("Rebel2 loadHandler8ShipSprites: Loaded ship NUT par4=%d with %d sprites",
-		par4, newNut->getNumChars());
-
-	switch (par4) {
-	case 1:  // POV001 - Primary ship sprite
-		delete _shipSprite;
-		_shipSprite = newNut;
-		break;
-	case 3:  // POV004 - Secondary ship sprite
-		delete _shipSprite2;
-		_shipSprite2 = newNut;
-		break;
-	case 6:  // POV002 - Ship overlay 1
-		delete _shipOverlay1;
-		_shipOverlay1 = newNut;
-		break;
-	case 7:  // POV003 - Ship overlay 2
-		delete _shipOverlay2;
-		_shipOverlay2 = newNut;
-		break;
-	default:
-		delete newNut;
-		return false;
-	}
-
-	return true;
-}
-
-bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par4) {
-	// Handler 25 GRD ship NUT loading - FUN_0041cadb case 6 (opcode 8)
-	// par4 values (from IACT data offset +6):
-	//   1: GRD001 - Primary ship sprite (DAT_00482240 / _grd001Sprite)
-	//   2: GRD002 - Secondary ship sprite (DAT_00482238 / _grd002Sprite)
-
-	if (!animData || size <= 0) {
-		return false;
-	}
-
-	// Only handle valid GRD sprite slots
-	if (par4 != 1 && par4 != 2) {
-		return false;
-	}
-
-	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
-	if (!newNut || newNut->getNumChars() <= 0) {
-		debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
-		delete newNut;
-		return false;
-	}
-
-	debug("Rebel2 loadHandler25GrdSprites: Loaded GRD NUT par4=%d with %d sprites",
-		par4, newNut->getNumChars());
-
-	switch (par4) {
-	case 1:  // GRD001 - Primary ship sprite
-		delete _grd001Sprite;
-		_grd001Sprite = newNut;
-		debug("Rebel2: _grd001Sprite set with %d sprites", newNut->getNumChars());
-		break;
-	case 2:  // GRD002 - Secondary ship sprite
-		delete _grd002Sprite;
-		_grd002Sprite = newNut;
-		debug("Rebel2: _grd002Sprite set with %d sprites", newNut->getNumChars());
-		break;
-	default:
-		delete newNut;
-		return false;
-	}
-
-	return true;
-}
-
-bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
-	// Level 2 background loading from embedded ANIM - FUN_00401234 case 5
-	// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
-	// Creates 320x200 buffer (DAT_0047e030 / _level2Background)
-
-	if (!animData || size < 8) {
-		return false;
-	}
-
-	debug("Rebel2 loadLevel2Background: Loading Level 2 background (animSize=%d)", size);
-
-	// Allocate background buffer if needed (320x200 = 64000 bytes)
-	if (_level2Background == nullptr) {
-		_level2Background = (byte *)malloc(320 * 200);
-		if (!_level2Background) {
-			return false;
-		}
-		memset(_level2Background, 0, 320 * 200);
-	}
-
-	// Parse embedded ANIM to find FOBJ
-	// Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
-	int animOffset = 0;
-	if (READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
-		uint32 animSize = READ_BE_UINT32(animData + 4);
-		debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
-
-		// Skip ANIM header (8 bytes) + AHDR chunk
-		if (size >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
-			uint32 ahdrSize = READ_BE_UINT32(animData + 12);
-			animOffset = 8 + 8 + ahdrSize;  // After ANIM tag + AHDR
-			debug("Rebel2 loadLevel2Background: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
-		}
-	}
-
-	// Look for FRME containing FOBJ
-	bool foundBackground = false;
-	for (int scanPos = animOffset; scanPos + 16 < size && !foundBackground; scanPos++) {
-		if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
-			int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
-			debug("Rebel2 loadLevel2Background: Found FRME at %d, size=%d", scanPos, frmeSize);
-
-			for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < size; fobjPos++) {
-				if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
-					byte *fobjData = animData + fobjPos + 8;
-
-					// FOBJ header: codec(2), x(2), y(2), w(2), h(2)
-					int16 codec = READ_LE_INT16(fobjData);
-					int16 fobjX = READ_LE_INT16(fobjData + 2);
-					int16 fobjY = READ_LE_INT16(fobjData + 4);
-					int16 fobjW = READ_LE_INT16(fobjData + 6);
-					int16 fobjH = READ_LE_INT16(fobjData + 8);
-
-					debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
-						codec, fobjX, fobjY, fobjW, fobjH);
-
-					// Decode codec 3 (RLE) into background buffer
-					// Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
-					// The standard smushDecodeRLE treats color 0 as transparent, which causes
-					// the background to appear as a "sketch" with black pixels missing.
-					if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
-						byte *rleData = fobjData + 14;  // Skip full 14-byte FOBJ header
-						smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
-
-						debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
-							fobjW, fobjH, fobjX, fobjY);
-						_level2BackgroundLoaded = true;
-						foundBackground = true;
-
-						// Copy to render bitmap immediately if provided
-						if (renderBitmap) {
-							for (int by = 0; by < 200; by++) {
-								memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
-							}
-							debug("Rebel2 loadLevel2Background: Copied to renderBitmap");
-						}
-					}
-					break;
-				}
-			}
-			break;
-		}
-	}
-
-	if (!foundBackground) {
-		debug("Rebel2 loadLevel2Background: Failed to find/decode background FOBJ");
-	}
-
-	return foundBackground;
-}
-
-void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// Opcode 9: Text/Subtitle Display via IACT chunk
-	// Note: Most RA2 subtitles use TRES chunks handled by SmushPlayer::handleTextResource()
-	// This opcode handles inline text in IACT chunks (less common)
-	//
-	// IACT Chunk Layout (par1-par4 already read by handleIACT):
-	// +0x00 (2): opcode = 9 (par1, already read)
-	// +0x02 (2): par2 (already read)
-	// +0x04 (2): par3 (already read)
-	// +0x06 (2): par4 (already read)
-	// +0x08 onwards: Text data structure
-	//
-	// Text Data Structure:
-	// +0x00 (2): X position
-	// +0x02 (2): Y position
-	// +0x04 (2): flags (bit 0=center, bit 1=right, bit 2=wrap, bit 3=difficulty gated)
-	// +0x06 (2): clipX (when flag & 4)
-	// +0x08 (2): clipY
-	// +0x0A (2): clipW
-	// +0x0C (2): clipH
-	// +0x10 onwards: NUL-terminated text string
-
-	int64 startPos = b.pos();
-
-	// Check for "TRES" tag (0x54524553) indicating string resource lookup
-	uint32 tag = b.readUint32BE();
-
-	const char *textStr = nullptr;
-	char textBuffer[512];
-	int16 posX = 160;  // Default center position
-	int16 posY = 150;  // Default bottom-ish position
-	int16 textFlags = 1;  // Default: center aligned
-	int16 clipX = 16, clipY = 16, clipW = 288, clipH = 168;
-
-	if (tag == MKTAG('T','R','E','S')) {
-		// String resource lookup via TRES tag
-		// The string index follows after the tag
-		int32 stringIndex = b.readSint32LE();
-
-		// Try to get string from SMUSH player's string resource
-		if (_player && _player->getString(stringIndex)) {
-			textStr = _player->getString(stringIndex);
-			debug("Rebel2 Opcode 9: TRES string index=%d -> \"%s\"", stringIndex, textStr);
-		} else {
-			debug("Rebel2 Opcode 9: TRES string index=%d not found", stringIndex);
-			return;
-		}
-
-		// After TRES + index, read positioning data
-		// The remaining data contains X, Y, flags etc.
-		if (b.size() - b.pos() >= 14) {
-			posX = b.readSint16LE();
-			posY = b.readSint16LE();
-			textFlags = b.readSint16LE();
-			clipX = b.readSint16LE();
-			clipY = b.readSint16LE();
-			clipW = b.readSint16LE();
-			clipH = b.readSint16LE();
-		}
-	} else {
-		// Inline text data - go back and read positioning structure
-		b.seek(startPos);
-
-		// Read text data structure
-		posX = b.readSint16LE();      // +0x00
-		posY = b.readSint16LE();      // +0x02
-		textFlags = b.readSint16LE(); // +0x04
-		clipX = b.readSint16LE();     // +0x06
-		clipY = b.readSint16LE();     // +0x08
-		clipW = b.readSint16LE();     // +0x0A
-		clipH = b.readSint16LE();     // +0x0C
-		b.skip(2);                    // +0x0E padding
-
-		// Read inline text string (NUL-terminated)
-		int textLen = 0;
-		while (textLen < (int)sizeof(textBuffer) - 1) {
-			byte ch = b.readByte();
-			if (ch == 0 || b.eos()) break;
-			textBuffer[textLen++] = ch;
-		}
-		textBuffer[textLen] = '\0';
-		textStr = textBuffer;
-
-		debug("Rebel2 Opcode 9: Inline text at (%d,%d) flags=0x%x -> \"%s\"", posX, posY, textFlags, textStr);
-	}
-
-	if (!textStr || textStr[0] == '\0') {
-		debug("Rebel2 Opcode 9: Empty text string, skipping");
-		return;
-	}
-
-	// Check difficulty gate (flag bit 3 = 0x08)
-	// If set, only show text if difficulty check passes (we skip this check for simplicity)
-	// In retail: FUN_00425d30(0) is called
-
-	// Get render buffer dimensions
-	int width = (_player && _player->_width > 0) ? _player->_width : 320;
-	int height = (_player && _player->_height > 0) ? _player->_height : 200;
-
-	// Apply coordinate clamping (from FUN_004033cf disassembly)
-	// Low-res: X clamped to [16, 304], Y clamped to [16, 196]
-	if (posX < 16) posX = 16;
-	if (posX > 304) posX = 304;
-	if (posY < 16) posY = 16;
-	if (posY > 196) posY = 196;
-
-	// Use the message font loaded during initialization (DIHIFONT.NUT)
-	if (!_rebelMsgFont) {
-		debug("Rebel2 Opcode 9: No message font loaded (_rebelMsgFont is null)");
-		return;
-	}
-
-	// Calculate clipping rectangle
-	if (!(textFlags & 0x04)) {
-		// No clip rect specified, use default full-screen clip
-		clipX = 0;
-		clipY = 0;
-		clipW = width;
-		clipH = height;
-	}
-
-	Common::Rect clipRect(
-		MAX<int>(0, clipX),
-		MAX<int>(0, clipY),
-		MIN<int>(clipX + clipW, width),
-		MIN<int>(clipY + clipH, height)
-	);
-
-	// Determine text alignment flags
-	TextStyleFlags styleFlags = kStyleAlignLeft;
-	if (textFlags & 0x01) {
-		styleFlags = kStyleAlignCenter;
-	} else if (textFlags & 0x02) {
-		styleFlags = kStyleAlignRight;
-	}
-	if (textFlags & 0x04) {
-		styleFlags = (TextStyleFlags)(styleFlags | kStyleWordWrap);
-	}
-
-	// Use white color (index 255) for subtitle text
-	// The original uses colors from the palette, commonly white or yellow for subtitles
-	int16 textColor = 255;
-
-	// RA2 fonts (like DIHIFONT.NUT) have only 58 characters starting at ASCII 32 (space).
-	// We need to convert ASCII codes to font indices by subtracting 32.
-	// Character mapping: font index = ASCII code - 32
-	// So 'D' (68) becomes index 36, 'A' (65) becomes index 33, etc.
-	// IMPORTANT: Skip format codes (^f00, ^c255, ^l) which TextRenderer parses as raw ASCII.
-	char convertedText[512];
-	int srcLen = strlen(textStr);
-	int dstIdx = 0;
-	int numChars = _rebelMsgFont->getNumChars();
-
-	for (int i = 0; i < srcLen && dstIdx < (int)sizeof(convertedText) - 1; i++) {
-		byte ch = (byte)textStr[i];
-
-		// Check for format codes (^f, ^c, ^l) - keep them as raw ASCII
-		if (ch == '^' && i + 1 < srcLen) {
-			byte next = (byte)textStr[i + 1];
-			if (next == 'f' && i + 3 < srcLen) {
-				// ^fXX - font switch (4 chars total)
-				convertedText[dstIdx++] = textStr[i++];  // ^
-				convertedText[dstIdx++] = textStr[i++];  // f
-				convertedText[dstIdx++] = textStr[i++];  // X
-				convertedText[dstIdx++] = textStr[i];    // X
-				continue;
-			} else if (next == 'c' && i + 4 < srcLen) {
-				// ^cXXX - color switch (5 chars total)
-				convertedText[dstIdx++] = textStr[i++];  // ^
-				convertedText[dstIdx++] = textStr[i++];  // c
-				convertedText[dstIdx++] = textStr[i++];  // X
-				convertedText[dstIdx++] = textStr[i++];  // X
-				convertedText[dstIdx++] = textStr[i];    // X
-				continue;
-			} else if (next == 'l') {
-				// ^l - line break marker (2 chars)
-				convertedText[dstIdx++] = textStr[i++];  // ^
-				convertedText[dstIdx++] = textStr[i];    // l
-				continue;
-			} else if (next == '^') {
-				// ^^ - escaped caret (becomes single ^)
-				i++;  // Skip first ^
-				// Fall through to convert second ^ as normal char
-				ch = '^';
-			}
-		}
-
-		// Convert regular characters from ASCII to font index
-		// First convert lowercase to uppercase (the font likely only has uppercase)
-		if (ch >= 'a' && ch <= 'z') {
-			ch = ch - 'a' + 'A';  // Convert to uppercase
-		}
-
-		if (ch >= 32 && ch < (byte)(32 + numChars)) {
-			convertedText[dstIdx++] = ch - 32;  // Convert ASCII to font index
-		} else if (ch == '\n' || ch == '\r') {
-			convertedText[dstIdx++] = ch;  // Keep control characters as-is
-		} else {
-			convertedText[dstIdx++] = 0;  // Replace invalid characters with space (index 0)
-		}
-	}
-	convertedText[dstIdx] = '\0';
-
-	// Draw the text string (with converted character indices)
-	if (textFlags & 0x04) {
-		// Word-wrapped text
-		_rebelMsgFont->drawStringWrap(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
-	} else {
-		// Single-line text
-		_rebelMsgFont->drawString(convertedText, renderBitmap, clipRect, posX, posY, textColor, styleFlags);
-	}
-
-	debug("Rebel2 Opcode 9: Rendered subtitle at (%d,%d) flags=0x%x clip=(%d,%d,%d,%d)",
-		posX, posY, textFlags, clipX, clipY, clipW, clipH);
-}
-
-void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
-	// Opcode 4: Enemy position update
-	// Read 5 shorts from the stream (offset +8 through +16)
-	int16 enemyId = b.readSint16LE();  // Offset +8
-	int16 x = b.readSint16LE();        // Offset +10 (0x0A)
-
-	// If enemy is disabled in bit table, skip update
-	bool disabled = isBitSet(enemyId);
-
-	int16 y = b.readSint16LE();        // Offset +12 (0x0C)
-	int16 w = b.readSint16LE();        // Offset +14 (0x0E) - Width
-	int16 h = b.readSint16LE();        // Offset +16 (0x10) - Height
-
-	// If disabled, stop processing this object
-	if (disabled) {
-		// debug("Rebel2: Skipping Opcode 4 for disabled enemy ID=%d", enemyId);
-		return;
-	}
-
-	// The disassembly shows half-width/half-height are used for centering:
-	//   halfW = w >> 1
-	//   halfH = h >> 1
-	//   centerX = x + halfW
-	//   centerY = y + halfH
-	// But for drawing the bounding box, we want the top-left corner (x, y) and full dimensions.
-
-	// Update enemy list for hit detection
-	// Enemy type comes from par4 (IACT offset +6), NOT par3 (offset +4).
-	// In the original (FUN_004028C5/FUN_0041E7C2): sVar5/sVar2 = *(short *)(*local + 6)
-	// This maps to par4 (userId field). Used for DAT_0047ab98 wave state bitmask:
-	//   DAT_0047ab98 |= 1 << (type & 0x1f)
-	debug(5, "Rebel2 Opcode4: handler=%d enemyId=%d par2=%d par3=%d par4/type=%d pos=(%d,%d) size=(%d,%d)",
-		_rebelHandler, enemyId, par2, par3, par4, x, y, w, h);
-
-	bool found = false;
-	Common::List<enemy>::iterator it;
-	for (it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (it->id == enemyId) {
-			it->rect = Common::Rect(x, y, x + w, y + h);
-			it->type = par4;  // Enemy type from IACT offset +6 (userId)
-			// Only re-activate if not destroyed
-			if (!it->destroyed) {
-				it->active = true;
-			}
-			found = true;
-			break;
-		}
-	}
-	if (!found) {
-		init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
-	}
-}
-
-void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
-	enemy e;
-	e.id = id;
-	e.type = type;
-	e.rect = Common::Rect(x, y, x + w, y + h);
-	e.active = active;
-	e.destroyed = destroyed;
-	e.explosionFrame = explosionFrame;
-	e.savedBackground = nullptr;
-	e.savedBgWidth = 0;
-	e.savedBgHeight = 0;
-	_enemies.push_back(e);
-}
-
-// ======================= Embedded Frame Codec Decoders =======================
-// These implement the retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0
-
-void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
-	// Codec 21/44: Line Update codec (FUN_0042BD60)
-	// Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
-	for (int row = 0; row < height; row++) {
-		int lineDataSize = READ_LE_UINT16(src);
-		src += 2;
-		const byte *lineEnd = src + lineDataSize;
-		byte *lineDst = dst + row * width;
-		int x = 0;
-
-		while (src < lineEnd && x < width) {
-			int skip = READ_LE_UINT16(src);
-			src += 2;
-			x += skip;
-			if (src >= lineEnd) break;
-
-			int count = READ_LE_UINT16(src) + 1;
-			src += 2;
-			while (count-- > 0 && x < width && src < lineEnd) {
-				lineDst[x++] = *src++;
-			}
-		}
-		src = lineEnd;
-	}
-}
-
-void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
-	// Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
-	// Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
-	const byte *dataEnd = src + dataSize;
-
-	for (int row = 0; row < height && src < dataEnd; row++) {
-		int lineDataSize = READ_LE_UINT16(src);
-		src += 2;
-		const byte *lineEnd = src + lineDataSize;
-		byte *lineDst = dst + row * width;
-		int x = 0;
-
-		while (src < lineEnd && x < width) {
-			int skip = READ_LE_UINT16(src);
-			src += 2;
-			x += skip;
-			if (src >= lineEnd || x >= width) break;
-
-			int runSize = READ_LE_UINT16(src);
-			src += 2;
-
-			// Decode RLE within this run
-			const byte *runEnd = src + runSize;
-			while (src < runEnd && x < width) {
-				byte code = *src++;
-				int num = (code >> 1) + 1;
-				if (num > width - x) num = width - x;
-
-				if (code & 1) {
-					// RLE run
-					byte color = (src < runEnd) ? *src++ : 0;
-					for (int i = 0; i < num && x < width; i++) {
-						lineDst[x++] = color;
-					}
-				} else {
-					// Literal run
-					for (int i = 0; i < num && x < width && src < runEnd; i++) {
-						lineDst[x++] = *src++;
-					}
-				}
-			}
-			src = runEnd;
-		}
-		src = lineEnd;
-	}
-}
-
-void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
-	// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
-	// May have a 6-byte sub-header starting with "01 FE"
-
-	debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-		src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7],
-		src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
-		src[16], src[17], src[18], src[19]);
-
-	// Probe for header offset
-	int headerSkip = 0;
-	bool foundValidOffset = false;
-
-	// Check for known 6-byte header pattern: 01 FE XX XX XX XX
-	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
-		headerSkip = 6;
-		debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
-		foundValidOffset = true;
-	}
-
-	// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
-	if (!foundValidOffset) {
-		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-			int testLineSize = READ_LE_UINT16(src + testOffset);
-			// A valid first line size should be: > 0, <= width*2
-			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
-				// Validate by summing line sizes
-				int sumTest = 0;
-				int linesTest = 0;
-				const byte *testPtr = src + testOffset;
-				bool validSum = true;
-
-				while (linesTest < height && testPtr + 2 <= src + dataSize) {
-					int ls = READ_LE_UINT16(testPtr);
-					if (ls <= 0 || ls > width * 2) {
-						validSum = false;
-						break;
-					}
-					sumTest += ls + 2;
-					testPtr += ls + 2;
-					linesTest++;
-				}
-
-				// Accept if we got close to expected number of lines
-				if (validSum && linesTest >= height - 1) {
-					headerSkip = testOffset;
-					foundValidOffset = true;
-					debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
-					break;
-				}
-			}
-		}
-	}
-
-	if (!foundValidOffset) {
-		debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
-	}
-
-	const byte *srcPtr = src + headerSkip;
-	const byte *dataEnd = src + dataSize;
-
-	// Check if this is per-line RLE or continuous RLE
-	int firstVal = READ_LE_UINT16(srcPtr);
-	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
-
-	if (perLineMode) {
-		debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
-		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-			int lineSize = READ_LE_UINT16(srcPtr);
-			srcPtr += 2;
-			if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
-
-			const byte *lineEnd = srcPtr + lineSize;
-			byte *rowDst = dst + row * width;
-			int x = 0;
-
-			while (srcPtr < lineEnd && x < width) {
-				byte ctrl = *srcPtr++;
-				int count = (ctrl >> 1) + 1;
-				if (ctrl & 1) {
-					byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
-					for (int i = 0; i < count && x < width; i++) rowDst[x++] = color;
-				} else {
-					for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
-						rowDst[x++] = *srcPtr++;
-				}
-			}
-			srcPtr = lineEnd;
-		}
-	} else {
-		// Continuous BOMP RLE (no per-line headers)
-		debug("Rebel2: Codec 45 using continuous BOMP RLE");
-		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-			byte *rowDst = dst + row * width;
-			int x = 0;
-
-			while (x < width && srcPtr < dataEnd) {
-				byte ctrl = *srcPtr++;
-				int count = (ctrl >> 1) + 1;
-
-				if (ctrl & 1) {
-					// RLE fill
-					byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
-					for (int i = 0; i < count && x < width; i++) {
-						rowDst[x++] = color;
-					}
-				} else {
-					// Literal copy
-					for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
-						rowDst[x++] = *srcPtr++;
-					}
-				}
-			}
-		}
-	}
-
-	// Count non-zero pixels for debug
-	int nonZero = 0;
-	for (int i = 0; i < width * height; i++) {
-		if (dst[i] != 0) nonZero++;
-	}
-	debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
-		width, height, nonZero, (nonZero * 100) / (width * height));
-}
-
-void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
-	// Render the decoded embedded frame to the video buffer
-	// Skip immediate draw for handlers that render HUD during post-processing:
-	// - Handler 7/8: Ship direction sprites selected based on direction
-	// - Handler 0x26/0x19: Cockpit HUD positioned based on mouse/crosshair
-	//
-	// Exception: Handler 25 (0x19) background overlays (par4/userId=4, 6, 7) should draw immediately.
-	// These complete the visual scene and are NOT positioned by mouse/crosshair.
-	bool skipImmediateDraw = (_rebelHandler == 7 || _rebelHandler == 8 ||
-	                          _rebelHandler == 0x26 || _rebelHandler == 0x19);
-
-	// Handler 25 overlays:
-	// - userId 4 (corridor overlay): Draw during procPostRendering at view offset, NOT immediately
-	// - userId 6, 7 (static overlays): Draw immediately (they don't move)
-	if (_rebelHandler == 0x19 && (userId == 6 || userId == 7)) {
-		skipImmediateDraw = false;
-		debug("Rebel2: Handler 25 static overlay userId=%d - forcing immediate draw", userId);
-	}
-	// userId 4 should NOT draw immediately - it will be drawn at view offset each frame
-
-	if (!frame.valid || !renderBitmap || skipImmediateDraw) {
-		if (skipImmediateDraw && frame.valid) {
-			debug("Rebel2: Skipped immediate draw for Handler %d HUD %d (will render during post-processing)",
-				_rebelHandler, userId);
-		}
-		return;
-	}
-
-	int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
-	int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
-	for (int y = 0; y < frame.height && (frame.renderY + y) < bufHeight; y++) {
-		for (int x = 0; x < frame.width && (frame.renderX + x) < pitch; x++) {
-			byte pixel = frame.pixels[y * frame.width + x];
-			if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
-				int destX = frame.renderX + x;
-				int destY = frame.renderY + y;
-				if (destX >= 0 && destY >= 0) {
-					renderBitmap[destY * pitch + destX] = pixel;
-				}
-			}
-		}
-	}
-	debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
-}
-
-void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
-	// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
-	if (userId < 0 || userId > 15 || !animData || size < 32) {
-		debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
-		return;
-	}
-	
-	Common::MemoryReadStream stream(animData, size);
-	
-	// Read ANIM header
-	uint32 animTag = stream.readUint32BE();
-	if (animTag != MKTAG('A','N','I','M')) {
-		debug("Rebel2: Embedded SAN missing ANIM tag, got 0x%08X", animTag);
-		return;
-	}
-	uint32 animSize = stream.readUint32BE();
-	debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
-	
-	// Iterate through chunks to find FRME -> FOBJ
-	while (!stream.eos() && stream.pos() < size) {
-		uint32 tag = stream.readUint32BE();
-		uint32 chunkSize = stream.readUint32BE();
-		int32 nextChunkPos = stream.pos() + chunkSize;
-
-		if (tag == MKTAG('F','R','M','E')) {
-			// Iterate sub-chunks in FRME
-			while (stream.pos() < nextChunkPos && !stream.eos()) {
-				uint32 subTag = stream.readUint32BE();
-				uint32 subSize = stream.readUint32BE();
-				int32 nextSubPos = stream.pos() + subSize;
-
-				if (subTag == MKTAG('F','O','B','J')) {
-					// Found FOBJ - Embedded HUD Frame
-					// Dump raw FOBJ bytes for analysis
-					int32 fobjStart = stream.pos();
-					byte rawHeader[20];
-					int headerBytesToRead = MIN((int)subSize, 20);
-					stream.read(rawHeader, headerBytesToRead);
-					stream.seek(fobjStart);  // Reset to read normally
-
-					debug("Rebel2: Raw FOBJ header (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-						headerBytesToRead,
-						rawHeader[0], rawHeader[1], rawHeader[2], rawHeader[3],
-						rawHeader[4], rawHeader[5], rawHeader[6], rawHeader[7],
-						rawHeader[8], rawHeader[9], rawHeader[10], rawHeader[11],
-						rawHeader[12], rawHeader[13], rawHeader[14], rawHeader[15],
-						rawHeader[16], rawHeader[17], rawHeader[18], rawHeader[19]);
-
-					// Read FOBJ header
-					int codec = stream.readUint16LE();
-					int left = stream.readUint16LE();
-					int top = stream.readUint16LE();
-					int width = stream.readUint16LE();
-					int height = stream.readUint16LE();
-					stream.readUint16LE();  // unknown
-					stream.readUint16LE();  // unknown
-
-					debug("Rebel2: Embedded HUD frame: userId=%d, %dx%d at (%d,%d), codec=%d",
-						userId, width, height, left, top, codec);
-
-					// Skip high-resolution frames - ScummVM runs at 320x200
-					// If frame dimensions exceed low-res screen size, it's high-res data
-					if (width > 400 || height > 250) {
-						debug("Rebel2: SKIPPING high-res embedded frame: userId=%d, %dx%d (exceeds 400x250)",
-							userId, width, height);
-						stream.seek(nextSubPos);
-						continue;
-					}
-
-					// Allocate storage for the decoded frame
-					EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
-
-					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
-						if (frame.width != width || frame.height != height || !frame.pixels) {
-							free(frame.pixels);
-							frame.pixels = (byte *)malloc(width * height);
-							frame.width = width;
-							frame.height = height;
-						}
-						// Clear buffer before decode (important for delta codecs)
-						memset(frame.pixels, 0, width * height);
-
-						// Update render position from FOBJ header
-						frame.renderX = left;
-						frame.renderY = top;
-						
-						// Read the raw FOBJ data
-						int32 dataSize = subSize - 14;
-						if (dataSize > 0) {
-							byte *fobjData = (byte *)malloc(dataSize);
-							stream.read(fobjData, dataSize);
-
-							// Decode based on codec - use extracted helper functions (FUN_0042BD60, etc.)
-							if (codec == 1 || codec == 3) {
-								// Codec 1/3: RLE - use existing decoder (FUN_0042C590)
-								smushDecodeRLE(frame.pixels, fobjData, 0, 0, width, height, width);
-								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec %d/RLE): %dx%d", codec, width, height);
-							} else if (codec == 20) {
-								// Codec 20: Uncompressed (FUN_0042C400)
-								smushDecodeUncompressed(frame.pixels, fobjData, 0, 0, width, height, width);
-								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
-							} else if (codec == 21 || codec == 44) {
-								// Codec 21/44: Line update (FUN_0042BD60)
-								decodeCodec21(frame.pixels, fobjData, width, height);
-								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
-							} else if (codec == 45) {
-								// Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
-								decodeCodec45(frame.pixels, fobjData, width, height, dataSize);
-								frame.valid = true;
-							} else if (codec == 23) {
-								// Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
-								decodeCodec23(frame.pixels, fobjData, width, height, dataSize);
-								frame.valid = true;
-								debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
-							} else {
-								debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
-								frame.valid = false;
-							}
-
-							// Count non-zero pixels to verify frame has content
-							if (frame.valid) {
-								int nonZeroPixels = 0;
-								for (int i = 0; i < width * height; i++) {
-									if (frame.pixels[i] != 0) nonZeroPixels++;
-								}
-								debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
-									userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
-							}
-
-							// Render the decoded frame to the video buffer
-							renderEmbeddedFrame(renderBitmap, frame, userId);
-
-							free(fobjData);
-						}
-					}
-					
-					// Done with FOBJ - assume only one relevant frame per embedded SAN
-					stream.seek(nextChunkPos);
-					goto end_parsing;
-				} else {
-					// Skip other sub-chunks (AHDR inside FRME?) or padding
-					stream.seek(nextSubPos);
-					if (subSize & 1) stream.skip(1);
-				}
-			}
-		} else {
-			// Skip non-FRME chunks (AHDR, etc at top level)
-			stream.seek(nextChunkPos);
-			if (chunkSize & 1) stream.skip(1);
-		}
-	}
-	
-	debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
-
-end_parsing:;
-}
-
-// Spawn explosion into the shared 5-slot system.
-// In the original, each handler has its own spawn logic inside its enemy processing function:
-//   Handler 0x26: FUN_40A2E0 (0x40A2E0) — spawns in slot arrays DAT_0044368e[]
-//   Handler 8:    FUN_4028C5 (0x4028C5) — spawns in slot arrays DAT_0043f854[]
-//   Handler 7:    FUN_40F628 (0x40F628) — spawns in slot arrays DAT_00443770[]
-//   Handler 25:   FUN_41E7C2 (0x41E7C2) — spawns in slot arrays DAT_0045792c[]
-// All share the same logic: find first free slot (counter==0), set counter=10,
-// scale=objectHalfWidth, position=enemy center, velocity=0.
-void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
-	for (int i = 0; i < 5; i++) {
-		if (!_explosions[i].active || _explosions[i].counter <= 0) {
-			_explosions[i].active = true;
-			_explosions[i].counter = 10;
-			_explosions[i].x = x;
-			_explosions[i].y = y;
-			_explosions[i].scale = objectHalfWidth;
-			break;
-		}
-	}
-}
-
-// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
-// For now, use a reasonable default based on observed values
-int16 InsaneRebel2::getShotMaxDuration() {
-	// The original uses: DAT_0047e0f0 + DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
-	// Typical values observed: 2-5 frames
-	return 4;
-}
-
-// Dispatcher - calls appropriate spawn function based on current handler
-void InsaneRebel2::spawnShot(int x, int y) {
-	switch (_rebelHandler) {
-	case 0x26:  // Turret
-		spawnTurretShot(x, y);
-		break;
-	case 8:     // Vehicle
-		spawnVehicleShot(x, y);
-		break;
-	case 7:     // Space combat
-		spawnSpaceShot(x, y);
-		break;
-	case 25:    // Speeder bike - uses turret shot array with different gun position
-		spawnHandler25Shot(x, y);
-		break;
-	default:
-		// Legacy fallback
-		for (int i = 0; i < 2; i++) {
-			if (!_shots[i].active) {
-				_shots[i].active = true;
-				_shots[i].counter = getShotMaxDuration();
-				_shots[i].x = x + _viewX;
-				_shots[i].y = y + _viewY;
-				break;
-			}
-		}
-		break;
-	}
-}
-
-// Handler 0x26 Turret shot spawn (based on FUN_4089AB lines 127-140)
-void InsaneRebel2::spawnTurretShot(int x, int y) {
-	for (int i = 0; i < 2; i++) {
-		if (_turretShots[i].counter == 0) {
-			// FUN_0041189e(-(ushort)(DAT_004436de == 5) & 7, i + 1, 0x7f, 0, 0)
-			// levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
-			playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
-
-			_turretShots[i].counter = getShotMaxDuration();
-			_turretShots[i].seqNum = _turretShotSeqCounter;
-			_turretShotSeqCounter++;
-			_turretShots[i].targetX = x + _viewX;  // DAT_0044366e in original
-			_turretShots[i].targetY = y + _viewY;  // DAT_00443670 in original
-			break;
-		}
-	}
-}
-
-// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
-void InsaneRebel2::spawnVehicleShot(int x, int y) {
-	for (int i = 0; i < 2; i++) {
-		if (_vehicleShots[i].counter == 0) {
-			// FUN_0041189e(6, local_c + 1, 0x7f, 0, 0) — HBLAST.SAD
-			playSfx(6, 127, 0);
-			_vehicleShots[i].counter = getShotMaxDuration();
-			_vehicleShots[i].targetX = x + _viewX;
-			_vehicleShots[i].targetY = y + _viewY;
-			break;
-		}
-	}
-}
-
-// Handler 25 on-foot shot spawn (based on FUN_0041db5e lines 170-190)
-// Gun position computed from GRD002 character sprite.
-// Original stores: DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
-//                  DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
-// Render adds view offset back, so screen gun = table[idx] + spriteOffset.
-void InsaneRebel2::spawnHandler25Shot(int x, int y) {
-	// Handler 25 can only shoot when uncovered (damage == 0)
-	if (_rebelDamageLevel != 0) {
-		return;  // Can't shoot while taking cover
-	}
-
-	for (int i = 0; i < 2; i++) {
-		if (_turretShots[i].counter == 0) {
-			// FUN_0041189e(6, local_1c + 1, 0x7f, 0, 0) — HBLAST.SAD
-			playSfx(6, 127, 0);
-
-			_turretShots[i].counter = getShotMaxDuration();
-			_turretShots[i].seqNum = _turretShotSeqCounter;
-			_turretShotSeqCounter++;
-
-			// Target position is where player clicked (screen coords)
-			_turretShots[i].targetX = x;
-			_turretShots[i].targetY = y;
-
-			// Compute gun position from GRD002 character sprite.
-			// Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
-			// We approximate from the NUT sprite center + directional offset.
-			if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
-				// Compute current sprite index (same logic as renderHandler25Ship)
-				int spriteIdx;
-				if (_rebelDamageLevel == 0) {
-					// Uncovered: compute from crosshair position zones
-					int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
-					int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
-					int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
-					int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
-					int areaWidth = areaRight - areaLeft;
-					int areaHeight = areaBottom - areaTop;
-					int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80;
-					int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;
-					int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (x - areaLeft)) / zoneWidth : 2;
-					int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (y - areaTop)) / zoneHeight : 0;
-					if (xZone < 0) xZone = 0;
-					if (xZone > 4) xZone = 4;
-					if (yZone < 0) yZone = 0;
-					if (yZone > 1) yZone = 1;
-					if (_rebelFlightDir == (yZone & 1)) {
-						xZone = 4 - xZone;
-					}
-					spriteIdx = yZone * 5 + xZone + 5;
-				} else {
-					spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
-				}
-				int numSprites = _grd002Sprite->getNumChars();
-				if (spriteIdx < 0) spriteIdx = 0;
-				if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
-
-				// Get sprite rendering position (same as in renderHandler25Ship)
-				int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
-				int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
-				int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
-				int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
-				bool shouldMirror = (_rebelFlightDir != 0 && _rebelDamageLevel == 0);
-
-				int drawX;
-				if (shouldMirror) {
-					drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
-				} else {
-					drawX = _rebelViewOffset2X + spriteXOffset;
-				}
-				int drawY = spriteYOffset + _rebelViewOffset2Y;
-
-				// Gun barrel is approximately at the character's hand level:
-				// X: center of sprite ± directional offset toward the target
-				// Y: about 60% down the sprite height (hand/arm level)
-				_turretShots[i].gunX = drawX + spriteW / 2;
-				_turretShots[i].gunY = drawY + (spriteH * 3) / 5;
-			} else {
-				// Fallback: approximate center-bottom of character area
-				_turretShots[i].gunX = _rebelViewOffset2X + 160;
-				_turretShots[i].gunY = _rebelViewOffset2Y + 140;
-			}
-
-			debug("Rebel2 Handler25: Spawned shot %d target (%d,%d) gun (%d,%d)",
-				i, _turretShots[i].targetX, _turretShots[i].targetY,
-				_turretShots[i].gunX, _turretShots[i].gunY);
-			break;
-		}
-	}
-}
-
-// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
-void InsaneRebel2::spawnSpaceShot(int x, int y) {
-	for (int i = 0; i < 2; i++) {
-		if (_spaceShots[i].counter == 0) {
-			// FUN_0041189e(6, local_2c + 1, 0x7f, 0, 0) — HBLAST.SAD
-			playSfx(6, 127, 0);
-
-			_spaceShots[i].counter = getShotMaxDuration();
-			_spaceShots[i].targetX = x;  // Screen coords
-			_spaceShots[i].targetY = y;
-
-			// Calculate gun positions from direction-based lookup tables
-			// In the original, these come from tables indexed by _shipDirectionIndex
-			// DAT_004437c2/DAT_00443808 for left gun, DAT_0044384e/DAT_00443894 for right gun
-			// For now, use simplified positions relative to ship
-			int shipScreenX = 160 + ((_shipPosX - 160) >> 3);
-			int shipScreenY = 105 + ((_shipPosY - 40) >> 2);
-
-			// Gun offsets (approximate from disassembly)
-			_spaceShots[i].leftGunX = shipScreenX - 28;
-			_spaceShots[i].leftGunY = shipScreenY + 10;
-			_spaceShots[i].rightGunX = shipScreenX + 28;
-			_spaceShots[i].rightGunY = shipScreenY + 10;
-			_spaceShots[i].variant = _spaceShotDirection;
-			break;
-		}
-	}
-}
-
-void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
-	if (!nut || spriteIdx >= nut->getNumChars()) return;
-
-	const byte *srcData = nut->getCharData(spriteIdx);
-	int texW = nut->getCharWidth(spriteIdx);
-	int texH = nut->getCharHeight(spriteIdx);
-	
-	if (!srcData || texW <= 0 || texH <= 0) return;
-	if (v < 0) v = 0;
-	if (v >= texH) v = texH - 1;
-
-	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
-	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
-	int err = dx + dy, e2;
-	
-	// Total length approximation for UV mapping
-	int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
-	if (totalDist == 0) totalDist = 1;
-	
-	int currentDist = 0;
-
-	for (;;) {
-		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
-			// Map currentDist/totalDist to 0..texW (Run along texture width)
-			int u = (currentDist * texW) / totalDist;
-			if (u >= texW) u = texW - 1;
-			
-			byte color = srcData[v * texW + u];
-			
-			// Check for transparency (0 and optionally 231)
-			if (color != 0 && (!mask231 || color != 231)) { 
-				dst[y0 * pitch + x0] = color;
-			}
-		}
-		
-		if (x0 == x1 && y0 == y1) break;
-		e2 = 2 * err;
-		if (e2 >= dy) { err += dy; x0 += sx; }
-		if (e2 <= dx) { err += dx; y0 += sy; }
-		
-		currentDist++;
-	}
-}
-
-// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
-	// Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
-	int sVar4 = 0;                // left
-	int sVar1 = 0;                // top
-	int sVar7 = width - 1;        // right
-	int sVar10 = height - 1;      // bottom
-
-	int px0 = param_3;
-	int py0 = param_4;
-	int px1 = param_5;
-	int py1 = param_6;
-
-	// Clip against screen bounds (translation of original clipping logic)
-	if (px0 == px1) {
-		if (px0 < sVar4 || px0 > sVar7) return;
-	} else {
-		if (px0 < sVar4) {
-			if (px1 < sVar4) return;
-			py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
-			px0 = sVar4;
-		} else if (px0 > sVar7) {
-			if (px1 > sVar7) return;
-			py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
-			px0 = sVar7;
-		}
-		if (px1 < sVar4) {
-			py1 = py0 + ((py1 - py0) * (sVar4 - px0)) / (px1 - px0);
-			px1 = sVar4;
-		} else if (px1 > sVar7) {
-			py1 = py0 + ((py1 - py0) * (sVar7 - px0)) / (px1 - px0);
-			px1 = sVar7;
-		}
-	}
-
-	if (py0 == py1) {
-		if (py0 < sVar1 || py0 > sVar10) return;
-	} else {
-		if (py0 < sVar1) {
-			if (py1 < sVar1) return;
-			px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
-			py0 = sVar1;
-		} else if (py0 > sVar10) {
-			if (py1 > sVar10) return;
-			px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
-			py0 = sVar10;
-		}
-		if (py1 < sVar1) {
-			px1 = px0 + ((px1 - px0) * (sVar1 - py0)) / (py1 - py0);
-			py1 = sVar1;
-		} else if (py1 > sVar10) {
-			px1 = px0 + ((px1 - px0) * (sVar10 - py0)) / (py1 - py0);
-			py1 = sVar10;
-		}
-	}
-
-	int dx = px1 - px0;
-	int dy = py1 - py0;
-	int absdx = dx < 0 ? -dx : dx;
-	int absdy = dy < 0 ? -dy : dy;
-
-	// pointer into destination and texture
-	byte *baseDst = dst;
-	const byte *texPtr = param_8;
-
-	if (absdx == 0) {
-		if (absdy == 0) {
-			if (*texPtr != 0) baseDst[py0 * pitch + px0] = *texPtr;
-			return;
-		}
-		// vertical-ish
-		int step = absdy + 1;
-		int curY = py0;
-		int signY = dy > 0 ? 1 : -1;
-		int iVar9 = step; // adv counter
-		for (int i = 0; i < step; i++) {
-			if (*texPtr != 0) baseDst[curY * pitch + px0] = *texPtr;
-			curY += signY;
-			iVar9 -= param_7;
-			while (iVar9 < 0) { texPtr++; iVar9 += step; }
-		}
-		return;
-	}
-
-	if (absdy == 0) {
-		// horizontal-ish
-		int step = absdx + 1;
-		int curX = px0;
-		int signX = dx > 0 ? 1 : -1;
-		int iVar11 = step;
-		for (int i = 0; i < step; i++) {
-			if (*texPtr != 0) baseDst[py0 * pitch + curX] = *texPtr;
-			curX += signX;
-			iVar11 -= param_7;
-			while (iVar11 < 0) { texPtr++; iVar11 += step; }
-		}
-		return;
-	}
-
-	// general case
-	int steps = (absdx > absdy) ? absdx + 1 : absdy + 1;
-	int x = px0, y = py0;
-	int sx = dx > 0 ? 1 : -1;
-	int sy = dy > 0 ? 1 : -1;
-	int err = absdx - absdy;
-	int iVar12 = steps;
-
-	for (int i = 0; i < steps; i++) {
-		if (x >= 0 && x < width && y >= 0 && y < height) {
-			if (*texPtr != 0) baseDst[y * pitch + x] = *texPtr;
-		}
-		int e2 = 2 * err;
-		if (e2 > -absdy) { err -= absdy; x += sx; }
-		if (e2 < absdx) { err += absdx; y += sy; }
-		iVar12 -= param_7;
-		if (iVar12 < 0) { texPtr++; iVar12 += steps; }
-	}
-}
-
-
-// Initialize laser texture buffer from NUT sprite (FUN_0040BAB0)
-// This pre-renders a sprite into a buffer that drawLaserBeam uses
-void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
-	if (!nut || spriteIdx >= nut->getNumChars())
-		return;
-
-	// Get sprite dimensions (FUN_0040BAB0 lines 13-14)
-	int16 texWidth = nut->getCharWidth(spriteIdx);
-	int16 texHeight = nut->getCharHeight(spriteIdx);
-
-	// Clamp height to max 15 pixels (FUN_0040BAB0 lines 15-17)
-	if (texHeight > 15) {
-		texHeight = 15;
-	}
-
-	// Free existing texture if any (FUN_0040BAB0 lines 18-20)
-	freeLaserTexture();
-
-	// Allocate new buffer (FUN_0040BAB0 line 21)
-	_laserTexture.width = texWidth;
-	_laserTexture.height = texHeight;
-	_laserTexture.pixels = (byte *)calloc(texWidth * texHeight, 1);
-
-	if (!_laserTexture.pixels)
-		return;
-
-	// Render sprite into buffer (FUN_0040BAB0 lines 23-24)
-	// We copy the sprite data directly since it's already in the right format
-	const byte *srcData = nut->getCharData(spriteIdx);
-	if (srcData) {
-		int srcHeight = nut->getCharHeight(spriteIdx);
-		int copyHeight = MIN(texHeight, (int16)srcHeight);
-		memcpy(_laserTexture.pixels, srcData, texWidth * copyHeight);
-	}
-
-	debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
-}
-
-// Free laser texture buffer (FUN_0040BBD1)
-void InsaneRebel2::freeLaserTexture() {
-	free(_laserTexture.pixels);
-	_laserTexture.pixels = nullptr;
-	_laserTexture.width = 0;
-	_laserTexture.height = 0;
-}
-
-// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-// This is a direct port of the assembly function
-//
-// Parameters (matching FUN_0040bbf6):
-//   dst, pitch, width, height: destination buffer info
-//   gunX, gunY (param_3, param_4): gun/start position
-//   targetX, targetY (param_5, param_6): target/end position
-//   animFrame (param_7): current animation frame (shot counter)
-//   maxFrames (param_8): max animation frames (shot duration)
-//   widthScale (param_9): width scaling factor for perspective
-//   heightScale (param_10): height/thickness multiplier
-//   thickness (param_11): base line thickness
-void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
-                                  int16 gunX, int16 gunY, int16 targetX, int16 targetY,
-                                  int16 animFrame, int16 maxFrames,
-                                  int16 widthScale, int16 heightScale, int16 thickness) {
-	// Check if laser texture is initialized
-	if (!_laserTexture.pixels || _laserTexture.width <= 0 || _laserTexture.height <= 0)
-		return;
-
-	int16 texW = _laserTexture.width;
-	int16 texH = _laserTexture.height;
-	byte *texPixels = _laserTexture.pixels;
-
-	// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
-	if (maxFrames == 0) maxFrames = 1;
-	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
-
-	// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
-	int16 dx = targetX - gunX;
-	int16 dy = targetY - gunY;
-	int16 sVar6 = (int16)(((int)dx * (thickness + 1)) / (int)thickness);
-	int16 sVar1 = (int16)(((int)dy * (thickness + 1)) / (int)thickness);
-
-	// FUN_0040BBF6 lines 26-29: Calculate adjusted start and end points
-	// Start point (closer to gun, adjusted by animation progress)
-	int16 startX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (sVar7 + 16));
-	int16 startY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (sVar7 + 16));
-	// End point (closer to target)
-	int16 endX = (sVar6 + gunX) - (int16)(((int)sVar6 * 16) / (widthScale + sVar7 + 16));
-	int16 endY = (sVar1 + gunY) - (int16)(((int)sVar1 * 16) / (widthScale + sVar7 + 16));
-
-	// FUN_0040BBF6 line 30: Get texture pixel pointer
-	byte *local_28 = texPixels;
-
-	// FUN_0040BBF6 lines 31-32: Calculate abs differences (FUN_004356e4 = abs)
-	int iVar2 = abs(startY - endY);  // |dy| of beam
-	int iVar3 = abs(startX - endX);  // |dx| of beam
-
-	// FUN_0040BBF6 line 33: Choose rendering path based on beam orientation
-	if (iVar2 < iVar3) {
-		// Mostly horizontal beam - draw vertical scanlines
-		// FUN_0040BBF6 lines 34-37
-		iVar2 = abs(startX - endX);
-		int temp = iVar2 * texH * heightScale;
-		int16 numLines = (int16)((temp >> 3) / texW) + 2;
-		int16 local_24 = -numLines;
-		int16 halfLines = numLines >> 1;
-
-		// FUN_0040BBF6 lines 39-46: Draw parallel lines
-		for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
-			// Draw one textured segment (vertical offset for this scanline)
-			drawTexturedSegment(dst, pitch, width, height,
-			                    startX, (startY - halfLines) + lineIdx,
-			                    endX, (endY - halfLines) + lineIdx,
-			                    texW, local_28);
-
-			// Advance texture pointer (step through texture rows)
-			for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
-				local_28 += texW;
-			}
-		}
-	} else {
-		// Mostly vertical beam - draw horizontal scanlines
-		// FUN_0040BBF6 lines 54-56
-		iVar2 = abs(startY - endY);
-		int16 numLines = (int16)((iVar2 * texH) / texW) + 2;
-		int16 local_24 = -numLines;
-
-		// FUN_0040BBF6 lines 58-60: Clamp to texture height
-		if (texH < numLines) {
-			numLines = texH;
-		}
-
-		int16 halfLines = numLines >> 1;
-
-		// FUN_0040BBF6 lines 61-68: Draw parallel lines
-		for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
-			// Draw one textured segment (horizontal offset for this scanline)
-			drawTexturedSegment(dst, pitch, width, height,
-			                    (startX - halfLines) + lineIdx, startY,
-			                    (endX - halfLines) + lineIdx, endY,
-			                    texW, local_28);
-
-			// Advance texture pointer
-			for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
-				local_28 += texW;
-			}
-		}
-	}
-}
-void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
-	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
-	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
-	int err = dx + dy, e2;
-
-	for (;;) {
-		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
-			dst[y0 * pitch + x0] = color;
-		}
-		if (x0 == x1 && y0 == y1) break;
-		e2 = 2 * err;
-		if (e2 >= dy) { err += dy; x0 += sx; }
-		if (e2 <= dx) { err += dx; y0 += sy; }
-	}
-}
-
-void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color) {
-	// Draw L-shaped brackets at corners of the rect (x,y,w,h)
-	// Bracket size: approx 8 pixels
-	int armLen = 2;
-	if (armLen > w / 2) armLen = w / 2;
-	if (armLen > h / 2) armLen = h / 2;
-
-	int x2 = x + w - 1;
-	int y2 = y + h - 1;
-
-	// Top-Left Corner
-	drawLine(dst, pitch, width, height, x, y, x + armLen, y, color);
-	drawLine(dst, pitch, width, height, x, y, x, y + armLen, color);
-
-	// Top-Right Corner
-	drawLine(dst, pitch, width, height, x2 - armLen, y, x2, y, color);
-	drawLine(dst, pitch, width, height, x2, y, x2, y + armLen, color);
-
-	// Bottom-Left Corner
-	drawLine(dst, pitch, width, height, x, y2, x + armLen, y2, color);
-	drawLine(dst, pitch, width, height, x, y2 - armLen, x, y2, color);
-
-	// Bottom-Right Corner
-	drawLine(dst, pitch, width, height, x2 - armLen, y2, x2, y2, color);
-	drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
-}
-
-// ============================================================
-// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
-// ============================================================
-// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
-// Zones are quadrilaterals registered via IACT opcode 5
-
-void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4) {
-	// IACT Opcode 5 data layout — corrected from FUN_4033CF / FUN_4092D9 analysis:
-	//
-	// Original game stores pointer to full IACT data (starting at opcode).
-	// SmushPlayer reads the first 8 bytes as header (code/flags/unknown/userId),
-	// so our stream starts at body[0] (IACT byte offset +8).
-	//
-	// FUN_4092D9 field mapping (byte offsets from stored pointer):
-	//   +0x00: opcode (5) — already consumed by SmushPlayer
-	//   +0x02: par2 (sub-opcode) — already consumed, passed as parameter
-	//   +0x04: par3 — already consumed by SmushPlayer
-	//   +0x06: par4 (userId) — filter value for < 1000 test, passed as parameter
-	//   +0x08: body[0] (sVar1) — control field 1 (frame check: field2-1 == field1)
-	//   +0x0A: body[1] (sVar2) — control field 2
-	//   +0x0C: body[2] — vertex 1 X
-	//   +0x0E: body[3] — vertex 1 Y
-	//   +0x10: body[4] — vertex 2 X
-	//   +0x12: body[5] — vertex 2 Y
-	//   +0x14: body[6] — vertex 3 X
-	//   +0x16: body[7] — vertex 3 Y
-	//   +0x18: body[8] — vertex 4 X
-	//   +0x1A: body[9] — vertex 4 Y
-
-	int16 field1 = b.readSint16LE();     // body[0] — control field 1
-	int16 field2 = b.readSint16LE();     // body[1] — control field 2
-	int16 x1 = b.readSint16LE();         // body[2] — vertex 1 X
-	int16 y1 = b.readSint16LE();         // body[3] — vertex 1 Y
-	int16 x2 = b.readSint16LE();         // body[4] — vertex 2 X
-	int16 y2 = b.readSint16LE();         // body[5] — vertex 2 Y
-	int16 x3 = b.readSint16LE();         // body[6] — vertex 3 X
-	int16 y3 = b.readSint16LE();         // body[7] — vertex 3 Y
-	int16 x4 = b.readSint16LE();         // body[8] — vertex 4 X
-	int16 y4 = b.readSint16LE();         // body[9] — vertex 4 Y
-
-	CollisionZone zone;
-	zone.x1 = x1;
-	zone.y1 = y1;
-	zone.x2 = x2;
-	zone.y2 = y2;
-	zone.x3 = x3;
-	zone.y3 = y3;
-	zone.x4 = x4;
-	zone.y4 = y4;
-	zone.field1 = field1;
-	zone.field2 = field2;
-	zone.filterValue = par4;
-	zone.subOpcode = subOpcode;
-	zone.active = true;
-
-	// Register zone into appropriate table based on sub-opcode
-	if (subOpcode == 0x0D && _primaryZoneCount < kMaxCollisionZones) {
-		_primaryZones[_primaryZoneCount++] = zone;
-		debug("Rebel2: Registered PRIMARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
-			_primaryZoneCount - 1, par4, field1, field2,
-			x1, y1, x2, y2, x3, y3, x4, y4);
-	} else if (subOpcode == 0x0E && _secondaryZoneCount < kMaxCollisionZones) {
-		_secondaryZones[_secondaryZoneCount++] = zone;
-		debug("Rebel2: Registered SECONDARY zone %d: filter=%d fields=[%d,%d] quad=(%d,%d)-(%d,%d)-(%d,%d)-(%d,%d)",
-			_secondaryZoneCount - 1, par4, field1, field2,
-			x1, y1, x2, y2, x3, y3, x4, y4);
-	} else {
-		debug("Rebel2: WARNING - Could not register zone (subOpcode=%d, primary=%d, secondary=%d)",
-			subOpcode, _primaryZoneCount, _secondaryZoneCount);
-	}
-}
-
-void InsaneRebel2::resetCollisionZones() {
-	// Reset zone counters at end of frame (FUN_403240 equivalent)
-	// This clears the zone tables so they can be rebuilt from the next frame's IACT chunks
-	_primaryZoneCount = 0;
-	_secondaryZoneCount = 0;
-}
-
-void InsaneRebel2::checkCollisionZones() {
-	// Per-frame collision checking — FUN_4092D9 first loop (lines 39-202).
-	// Tests aim/ship position against primary collision zone quadrilaterals.
-	//
-	// Original coordinate system:
-	//   Zone vertices are in 424x260 buffer space, centered by subtracting (0xD4=212, 0x82=130).
-	//   Aim position (DAT_00443668/DAT_0044366a) is in centered coords [-52..52, -45..45].
-	//   In FUN_407FCB: DAT_00443668 is a smoothed mouse-derived position.
-	//
-	// For our implementation:
-	//   Map mouse position to centered coords matching the original range.
-	//   Mouse X 0..320 → centered X ≈ [-52..52] (with smoothing in original)
-	//   Mouse Y 0..200 → centered Y ≈ [-45..45]
-
-	if (_primaryZoneCount == 0) return;
-
-	// Calculate aim position in centered coordinates.
-	// Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
-	// Simplified mapping: mouse 0..320 → [-52..52], mouse 0..200 → [-45..45]
-	int16 aimX = (int16)((_vm->_mouse.x - 160) * 52 / 160);
-	int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
-
-	// Clamp to original ranges (DAT_0047a7fc < 1 path)
-	if (aimX > 0x34) aimX = 0x34;
-	if (aimX < -0x34) aimX = -0x34;
-	if (aimY > 0x2d) aimY = 0x2d;
-	if (aimY < -0x2d) aimY = -0x2d;
-
-	for (int i = 0; i < _primaryZoneCount; i++) {
-		CollisionZone &zone = _primaryZones[i];
-		if (!zone.active) continue;
-
-		// Filter: only process zones with filterValue < 1000 (par4 from IACT header)
-		// Original: *(short *)(*local_c + 6) < 1000
-		if (zone.filterValue >= 1000) continue;
-
-		// Frame check: field2 - 1 == field1
-		// Original: sVar2 + -1 == (int)sVar1
-		if (zone.field2 - 1 != zone.field1) continue;
-
-		// Center zone vertices by subtracting buffer center (0xD4=212, 0x82=130)
-		// Original: sVar4 = x1 - 0xD4, sVar8 = y1 - 0x82, etc.
-		int cx1 = zone.x1 - 0xD4;
-		int cy1 = zone.y1 - 0x82;
-		int cx2 = zone.x2 - 0xD4;
-		int cy2 = zone.y2 - 0x82;
-		int cx3 = zone.x3 - 0xD4;
-		int cy3 = zone.y3 - 0x82;
-		int cx4 = zone.x4 - 0xD4;
-		int cy4 = zone.y4 - 0x82;
-
-		// Point-in-quadrilateral test — FUN_4092D9 lines 119-128
-		// Tests if aim position is OUTSIDE the safe corridor (= collision with obstacle).
-		// Original uses 4 edge interpolation tests connected by OR (any failure = collision).
-		//
-		// Edge 1: interpolate Y along top edge (v1→v2) at aim X position
-		//   if aimY < interpolated Y → outside top edge → collision
-		// Edge 2: interpolate Y along bottom edge (v4→v3) at aim X position
-		//   if interpolated Y < aimY → outside bottom edge → collision
-		// Edge 3: interpolate X along left edge (v1→v4) at aim Y position
-		//   if aimX < interpolated X → outside left edge → collision
-		// Edge 4: interpolate X along right edge (v2→v3) at aim Y position
-		//   if interpolated X < aimX → outside right edge → collision
-		bool collision = false;
-
-		// Avoid division by zero for degenerate edges
-		if (cx2 != cx1) {
-			int interpY1 = ((aimX - cx1) * (cy2 - cy1)) / (cx2 - cx1) + cy1;
-			if (aimY < interpY1) collision = true;
-		}
-		if (!collision && cx3 != cx4) {
-			int interpY2 = ((aimX - cx4) * (cy3 - cy4)) / (cx3 - cx4) + cy4;
-			if (interpY2 < aimY) collision = true;
-		}
-		if (!collision && cy4 != cy1) {
-			int interpX1 = ((aimY - cy1) * (cx4 - cx1)) / (cy4 - cy1) + cx1;
-			if (aimX < interpX1) collision = true;
-		}
-		if (!collision && cy3 != cy2) {
-			int interpX2 = ((aimY - cy2) * (cx3 - cx2)) / (cy3 - cy2) + cx2;
-			if (interpX2 < aimX) collision = true;
-		}
-
-		if (collision) {
-			// Collision detected — apply damage from collision damage table
-			// Original: DAT_0047a7ec += DAT_0047e0f6[levelIdx]
-			// TODO: Read from per-level collision damage table DAT_0047e0f6
-			int collisionDamage = 3 + (_difficulty * 2);
-
-			if (!_rebelInvulnerable) {
-				_playerDamage += collisionDamage;
-				if (_playerDamage > 255) _playerDamage = 255;
-				debug("Rebel2: COLLISION damage! zone=%d aim=(%d,%d) damage=%d total=%d",
-					i, aimX, aimY, collisionDamage, _playerDamage);
-			}
-			// Visual effect — FUN_00420515 (palette flash)
-			initDamageFlash();
-			// TODO: FUN_0041189e sound based on collision direction
-		} else {
-			// Safely passed — award score bonus
-			// Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
-			addScore(1);
-		}
-	}
-}
-
-void InsaneRebel2::checkHandler7CollisionZones() {
-	// FUN_40E35E — Handler 7 per-frame collision system.
-	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
-	// Two modes depending on _flyControlMode:
-	//   Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
-	//   Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
-
-	// Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
-	// to match the original where the decrement happens during rendering.
-
-	if (_flyControlMode == 0 || _flyControlMode == 2) {
-		// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
-		// Original lines 52-132: Point-in-quad test with 15px inward margin.
-		// Inside the quad = collision with obstacle.
-		const int margin = 15;  // local_14 = 0x0f, local_20 = 0x0f
-
-		for (int i = 0; i < _secondaryZoneCount; i++) {
-			CollisionZone &zone = _secondaryZones[i];
-			if (!zone.active) continue;
-
-			int x1 = zone.x1, y1 = zone.y1;
-			int x2 = zone.x2, y2 = zone.y2;
-			int x3 = zone.x3, y3 = zone.y3;
-			int x4 = zone.x4, y4 = zone.y4;
-
-			// Point-in-quad test (lines 75-89)
-			// Start assuming inside, clear if outside any edge (with margin)
-			bool inside = true;
-
-			// Top edge: interpolate Y along v1→v2 at shipX, +15 margin
-			if (x2 != x1) {
-				int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
-				if (_flyShipScreenY < interpY) inside = false;
-			}
-			// Bottom edge: interpolate Y along v4→v3 at shipX, -15 margin
-			if (inside && x3 != x4) {
-				int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
-				if (interpY < _flyShipScreenY) inside = false;
-			}
-			// Left edge: interpolate X along v1→v4 at shipY, +15 margin
-			if (inside && y4 != y1) {
-				int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
-				if (_flyShipScreenX < interpX) inside = false;
-			}
-			// Right edge: interpolate X along v2→v3 at shipY, -15 margin
-			if (inside && y3 != y2) {
-				int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
-				if (interpX < _flyShipScreenX) inside = false;
-			}
-
-			// Frame match: field2 - 1 == field1 (line 90)
-			if (zone.field2 - 1 == zone.field1) {
-				if (inside) {
-					// Collision with obstacle — apply damage and break
-					_hitCooldown = 10;
-					_spaceShotDirection = zone.filterValue + 2;
-
-					int collisionDamage = 3 + (_difficulty * 2);
-					if (!_rebelInvulnerable) {
-						_playerDamage += collisionDamage;
-						if (_playerDamage > 255) _playerDamage = 255;
-					}
-					_rebelHitCounter++;
-					initDamageFlash();
-					// Pan based on ship X position relative to screen center
-					playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
-					debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
-						i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
-					break;  // Only one collision per frame (original breaks)
-				} else {
-					// Safely avoided obstacle — award score
-					addScore(1);
-				}
-			}
-		}
-
-		// Corridor boundary proximity (lines 127-131)
-		// These flags are used for directional indicators (not critical for damage)
-
-	} else {
-		// ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
-		// Original lines 133-235: Per-edge interpolation with push-back.
-		// Ship position is clamped to wall boundaries when hitting.
-		int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f;  // local_14
-		const int16 vMargin = 0x0f;  // local_20
-
-		for (int i = 0; i < _primaryZoneCount; i++) {
-			CollisionZone &zone = _primaryZones[i];
-			if (!zone.active) continue;
-
-			int x1 = zone.x1, y1 = zone.y1;
-			int x2 = zone.x2, y2 = zone.y2;
-			int x3 = zone.x3, y3 = zone.y3;
-			int x4 = zone.x4, y4 = zone.y4;
-
-			// Top edge: interpolate Y along v1→v2 at shipX (lines 152-166)
-			if (x2 != x1) {
-				int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
-				if (_flyShipScreenY < edgeY) {
-					// Ship above top wall — push down
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						playSfx(1, 127, 0);  // CRASH.SAD, top wall → center pan
-						debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
-					}
-					_spaceShotDirection = 2;  // Direction: pushed down
-					_flyShipScreenY = edgeY;  // Push-back
-					initDamageFlash();
-				}
-			}
-
-			// Bottom edge: interpolate Y along v4→v3 at shipX (lines 167-183)
-			if (x3 != x4) {
-				int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
-				_corridorBottomY = vMargin + edgeY;  // DAT_00443b10 update
-				if (edgeY < _flyShipScreenY) {
-					// Ship below bottom wall — push up
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						playSfx(1, 127, 0);  // CRASH.SAD, bottom wall → center pan
-						debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
-					}
-					_spaceShotDirection = 3;  // Direction: pushed up
-					_flyShipScreenY = edgeY;  // Push-back
-					initDamageFlash();
-				}
-			}
-
-			// Left edge: interpolate X along v1→v4 at shipY (lines 184-199)
-			if (y4 != y1) {
-				int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
-				if (_flyShipScreenX < edgeX) {
-					// Ship left of left wall — push right
-					_flyShipScreenX = edgeX;  // Push-back
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
-						debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
-					}
-					_spaceShotDirection = 0;  // Direction: pushed right
-					initDamageFlash();
-				}
-			}
-
-			// Right edge: interpolate X along v2→v3 at shipY (lines 200-215)
-			if (y3 != y2) {
-				int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
-				if (edgeX < _flyShipScreenX) {
-					// Ship right of right wall — push left
-					_flyShipScreenX = edgeX;  // Push-back
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
-						debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
-					}
-					_spaceShotDirection = 1;  // Direction: pushed left
-					initDamageFlash();
-				}
-			}
-		}
-	}
-}
-
-void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
-                            int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
-	// Draw a quadrilateral by connecting its 4 vertices with lines
-	// Vertex order: top-left (1), top-right (2), bottom-right (3), bottom-left (4)
-	drawLine(dst, pitch, width, height, x1, y1, x2, y2, color);  // Top edge
-	drawLine(dst, pitch, width, height, x2, y2, x3, y3, color);  // Right edge
-	drawLine(dst, pitch, width, height, x3, y3, x4, y4, color);  // Bottom edge
-	drawLine(dst, pitch, width, height, x4, y4, x1, y1, color);  // Left edge
-}
-
-void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int height, byte color) {
-	// Draw all active collision zones as wireframe quadrilaterals for debugging
-	// Uses different colors for primary vs secondary zones
-
-	const byte primaryColor = 44;    // Bright red for primary (obstacle) zones
-	const byte secondaryColor = 47;  // Yellow for secondary (boundary) zones
-
-	// Draw primary zones (sub-opcode 0x0D - obstacles)
-	for (int i = 0; i < _primaryZoneCount; i++) {
-		CollisionZone &zone = _primaryZones[i];
-		if (!zone.active) continue;
-
-		// Apply view offset to convert from video coords to screen coords
-		int x1 = zone.x1 + _viewX;
-		int y1 = zone.y1 + _viewY;
-		int x2 = zone.x2 + _viewX;
-		int y2 = zone.y2 + _viewY;
-		int x3 = zone.x3 + _viewX;
-		int y3 = zone.y3 + _viewY;
-		int x4 = zone.x4 + _viewX;
-		int y4 = zone.y4 + _viewY;
-
-		drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, primaryColor);
-	}
-
-	// Draw secondary zones (sub-opcode 0x0E - boundaries)
-	for (int i = 0; i < _secondaryZoneCount; i++) {
-		CollisionZone &zone = _secondaryZones[i];
-		if (!zone.active) continue;
-
-		// Apply view offset
-		int x1 = zone.x1 + _viewX;
-		int y1 = zone.y1 + _viewY;
-		int x2 = zone.x2 + _viewX;
-		int y2 = zone.y2 + _viewY;
-		int x3 = zone.x3 + _viewX;
-		int y3 = zone.y3 + _viewY;
-		int x4 = zone.x4 + _viewX;
-		int y4 = zone.y4 + _viewY;
-
-		drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, secondaryColor);
-	}
-
-	// Draw corridor boundaries as a rectangle (from IACT opcode 7)
-	if (_corridorLeftX != 0 || _corridorRightX != 0x1A8) {
-		const byte corridorColor = 45;  // Cyan for corridor boundaries
-		// Draw vertical lines for left/right boundaries
-		drawLine(dst, pitch, width, height,
-			_corridorLeftX + _viewX, _corridorTopY + _viewY,
-			_corridorLeftX + _viewX, _corridorBottomY + _viewY, corridorColor);
-		drawLine(dst, pitch, width, height,
-			_corridorRightX + _viewX, _corridorTopY + _viewY,
-			_corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
-		// Draw horizontal lines for top/bottom boundaries
-		drawLine(dst, pitch, width, height,
-			_corridorLeftX + _viewX, _corridorTopY + _viewY,
-			_corridorRightX + _viewX, _corridorTopY + _viewY, corridorColor);
-		drawLine(dst, pitch, width, height,
-			_corridorLeftX + _viewX, _corridorBottomY + _viewY,
-			_corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
-	}
-}
-
-void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
-	renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
-}
-
-// Render NUT sprite with optional horizontal mirroring
-// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
-void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
-	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
-
-	int w = nut->getCharWidth(spriteIdx);
-	int h = nut->getCharHeight(spriteIdx);
-	const byte *src = nut->getCharData(spriteIdx);
-
-	// Clipping
-	int drawX = x;
-	int drawY = y;
-	int drawW = w;
-	int drawH = h;
-	int srcOffsetX = 0;
-	int srcOffsetY = 0;
-
-	if (drawX < 0) {
-		srcOffsetX = -drawX;
-		drawW += drawX;
-		drawX = 0;
-	}
-	if (drawY < 0) {
-		srcOffsetY = -drawY;
-		drawH += drawY;
-		drawY = 0;
-	}
-
-	if (drawX + drawW > width) {
-		drawW = width - drawX;
-	}
-	if (drawY + drawH > height) {
-		drawH = height - drawY;
-	}
-
-	if (drawW <= 0 || drawH <= 0) return;
-
-	// Draw loop - with optional horizontal mirroring
-	for (int iy = 0; iy < drawH; iy++) {
-		const byte *s = src + (srcOffsetY + iy) * w;
-		byte *d = dst + (drawY + iy) * pitch + drawX;
-		for (int ix = 0; ix < drawW; ix++) {
-			int srcX;
-			if (mirror) {
-				// When mirrored, read from the opposite side of the sprite
-				srcX = (w - 1) - (srcOffsetX + ix);
-			} else {
-				srcX = srcOffsetX + ix;
-			}
-			byte px = s[srcX];
-			if (px != 231 && px != 0) { // Check both 0 and 231 (0xE7) for transparency
-				d[ix] = px;
-			}
-		}
-	}
-}
-
-void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
-							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
-
-	// Determine correct pitch for the video buffer (usually 320 for Rebel2)
-	int width = _player->_width;
-	int height = _player->_height;
-	if (width == 0) width = _vm->_screenWidth;
-	if (height == 0) height = _vm->_screenHeight;
-	int pitch = width;
-
-	// Calculate View/Scroll Offsets
-	// Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200)
-	// Map mouse X (0-320) to Scroll X (0-104)
-	// Map mouse Y (0-200) to Scroll Y (0-60)
-	int maxScrollX = width - _vm->_screenWidth;
-	int maxScrollY = height - _vm->_screenHeight;
-	
-	if (maxScrollX < 0) maxScrollX = 0;
-	if (maxScrollY < 0) maxScrollY = 0;
-	
-	// Simple linear mapping: Center of screen corresponds to center of buffer
-	_viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
-	_viewY = (_vm->_mouse.y * maxScrollY) / _vm->_screenHeight;
-	
-	_player->setScrollOffset(_viewX, _viewY);
-
-	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
-	// Based on FUN_004089ab:
-	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
-	// 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
-	// 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
-	//
-	// In FUN_0041c012:
-	// - Sprites are drawn to buffer DAT_00482204 at position (0,0)
-	// - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
-	// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
-	//
-	// We draw directly to screen at Y=180
-	
-	// Use video content coordinates, NOT buffer coordinates
-	const int videoWidth = 320;    // Native video width
-	const int videoHeight = 200;   // Native video height  
-	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
-
-	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
-	// The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
-	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
-
-	// Check if we're in menu mode (menu state + intro flag)
-	bool menuMode = (introPlaying && _gameState == kStateMainMenu);
-	bool pilotSelectMode = (introPlaying && (_gameState == kStatePilotSelect || _gameState == kStateDifficultySelect));
-	bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
-
-	// Handle pilot selection input and rendering (FUN_00414A41)
-	// This is the pilot/save slot selection screen with centered menu
-	if (pilotSelectMode) {
-		// Show the standard Windows arrow cursor
-		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-		CursorMan.replaceCursor(cursor);
-		delete cursor;
-		CursorMan.showMouse(true);
-
-		// Process pilot selection input - emulates FUN_00414A41 input handling
-		int selection = processLevelSelectInput();
-
-		// Draw pilot selection overlay - centered menu like main menu
-		drawLevelSelectOverlay(renderBitmap, pitch, width, height);
-
-		// If a selection was confirmed, signal video to stop
-		if (selection >= 0) {
-			debug("Rebel2: Pilot selection confirmed: %d", selection);
-			_vm->_smushVideoShouldFinish = true;
-		}
-
-		// Skip normal HUD rendering in pilot select mode
-		return;
-	}
-
-	// Handle chapter selection input and rendering (FUN_00415CF8)
-	// This is the actual level/chapter selection screen with preview and password
-	if (chapterSelectMode) {
-		// Show the standard Windows arrow cursor (same as menu)
-		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-		CursorMan.replaceCursor(cursor);
-		delete cursor;
-		CursorMan.showMouse(true);
-
-		// O_LEVEL.SAN provides the background with chapter preview thumbnails.
-		// The FOBJ offset system (set in procPreRendering) scrolls the correct preview
-		// into the preview box area. No black fill needed — video frame shows through.
-
-		// Process chapter selection input - emulates FUN_00415CF8 input handling
-		int selection = processChapterSelectInput();
-
-		// Draw chapter selection overlay - emulates FUN_00415CF8 rendering
-		drawChapterSelectOverlay(renderBitmap, pitch, width, height);
-
-		// If a selection was confirmed, signal video to stop
-		if (selection >= 0) {
-			debug("Rebel2: Chapter selection confirmed: %d", selection);
-			_vm->_smushVideoShouldFinish = true;
-		}
-
-		// Skip normal HUD rendering in chapter select mode
-		return;
-	}
-
-	// Handle menu input and rendering if in menu mode
-	if (menuMode) {
-		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
-		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
-		// MSTOVER.NUT is a background overlay, NOT a cursor
-		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-		CursorMan.replaceCursor(cursor);
-		delete cursor;
-		CursorMan.showMouse(true);
-
-		// Process menu input during each frame
-		int selection = processMenuInput();
-
-		// Update inactivity timer (only increments when no input is received)
-		// Input resets timer in processMenuInput()
-		_menuInactivityTimer++;
-
-		// Check for inactivity timeout
-		// From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode)
-		// At 12fps video rate, 300 frames = ~25 seconds of inactivity
-		// The original checks: if (local_8 > 299) return 0;
-		if (_menuInactivityTimer > 300) {
-			debug("Rebel2: Menu inactivity timeout - ending video to loop");
-			// Signal video to end so menu loop plays new video
-			// This emulates the attract mode behavior where a new random
-			// menu video is selected after inactivity
-			_menuInactivityTimer = 0;
-			// Don't set _smushVideoShouldFinish here - let video end naturally
-			// This will cause runMainMenu to loop and play a new random video
-		}
-
-		// Draw menu selection overlay
-		drawMenuOverlay(renderBitmap, pitch, width, height);
-
-		// If a selection was confirmed, signal video to stop
-		if (selection >= 0) {
-			debug("Rebel2: Menu selection confirmed: %d", selection);
-			_vm->_smushVideoShouldFinish = true;
-		}
-
-		// Skip normal HUD rendering in menu mode
-		return;
-	}
-
-	// During intro/cinematic sequences:
-	// - Hide the mouse cursor (original: ShowCursor(0) at startup in FUN_00420c70)
-	// - Skip all HUD/status bar/crosshair rendering
-	// - Skip mouse input processing (no shooting during intros)
-	//
-	// Original behavior from FUN_00403240:
-	// - if (DAT_0047a814 == 0) { switch(DAT_0047ee84) { ... } }
-	// - DAT_0047ee84 (handler) is only set by IACT opcode 6 during gameplay videos
-	// - Cinematics/intros don't have opcode 6, so handler stays 0
-	// - We use _rebelHandler == 0 as the primary indicator for intro/cinematic mode
-	if (_rebelHandler == 0) {
-		// Hide mouse cursor during intro - no crosshair, no clicking
-		CursorMan.showMouse(false);
-
-		// Track state transition for debugging
-		if (!_introCursorPushed) {
-			_introCursorPushed = true;
-			debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
-				  _player->_curVideoFlags, _gameState);
-		}
-		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
-		return;
-	} else {
-		// Gameplay mode - handler was set by IACT opcode 6
-		if (_introCursorPushed) {
-			_introCursorPushed = false;
-			debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
-				  _rebelHandler, _player->_curVideoFlags, _gameState);
-		}
-	}
-
-	// From here on, we're in gameplay mode (_rebelHandler != 0)
-	// Process mouse input for shooting
-	// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
-	processMouse();
-
-	// NOTE: Level 2 background is drawn ONCE during IACT opcode 8 par4=5 processing
-	// (in procIACT when the background ANIM is first loaded). The 0x08 video flag
-	// (preserve background) prevents the frame buffer from being cleared, so the
-	// background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
-	// We do NOT redraw the background here as that would overwrite FOBJ content.
-
-	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
-	// 1. FUN_004288c0: Fill status bar background at Y=0xb4 (180)
-	// 2. FUN_004089ab: Draw turret overlays, targeting reticle, crosshair
-	// 3. FUN_0041c012: Draw status bar sprites LAST (on top)
-
-	// STEP 0: Fill status bar background (FUN_004288c0)
-	renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
-
-	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
-	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
-
-	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
-	renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
-
-	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
-	renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
-
-	// Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
-	debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
-		_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
-		_shipSprite ? _shipSprite->getNumChars() : 0,
-		_flyShipSprite ? _flyShipSprite->getNumChars() : 0);
-
-	renderHandler7Ship(renderBitmap, pitch, width, height);
-	renderHandler8Ship(renderBitmap, pitch, width, height);
-	// GRD001 (wall/cockpit) drawn AFTER FOBJs per original FUN_0041DB5E lines 202-210
-	renderHandler25ShipPre(renderBitmap, pitch, width, height);
-	renderHandler25Ship(renderBitmap, pitch, width, height);
-	renderFallbackShip(renderBitmap, pitch, width, height);
-
-	// Enemy indicators and destroyed enemy area erase
-	renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
-
-	// Explosion animations (FUN_409FBC)
-	renderExplosions(renderBitmap, pitch, width, height);
-
-	// Laser shot beams and impacts
-	renderLaserShots(renderBitmap, pitch, width, height);
-
-	// Damage visual effects — handler-specific per original architecture:
-	//   Handler 8:    FUN_401CCF line 119 → FUN_00420754 (palette flash + screen shake)
-	//   Handler 0x19: FUN_41DB5E line 192 → FUN_00420562 (palette flash only, every frame)
-	//   Handler 0x26: FUN_4092D9 lines 135/225/237 → FUN_00420515 trigger + palette flash
-	//   Handler 7:    FUN_40E35E → FUN_00420515 trigger + palette flash
-	if (_rebelHandler == 8) {
-		// Full damage effect: palette flash + screen shake
-		// Suppressed during autopilot (mode 4) and cutscene (mode 5)
-		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
-			updateDamageEffect(renderBitmap, pitch, width, height);
-		}
-	} else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
-		// Palette flash only — no screen shake for turret/FPS/ship handlers
-		updateDamageFlashPalette();
-	}
-
-	// Per-frame collision checking against registered zones.
-	//
-	// Handler 0x26 (turret): FUN_4092D9 — aim position vs primary zones (centered coords)
-	//   Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
-	//
-	// Handler 7 (ship): FUN_40E35E — ship position vs zones per control mode:
-	//   Mode 0/2: SECONDARY zones (0x0E) — obstacle collision (inside quad = hit)
-	//   Mode 1/3: PRIMARY zones (0x0D) — wall/boundary per-edge with push-back
-	//   Uses ship position in raw buffer coords, hit cooldown, directional damage.
-	if (_rebelHandler == 0x26) {
-		checkCollisionZones();
-	} else if (_rebelHandler == 7) {
-		checkHandler7CollisionZones();
-	}
-
-	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
-	if (_rebelHandler == 7 || _rebelHandler == 8) {
-		drawCollisionZones(renderBitmap, pitch, width, height, 0);
-	}
-
-	// Crosshair/reticle (FUN_004089ab, FUN_0040d836)
-	renderCrosshair(renderBitmap, pitch, width, height);
-
-	// HUD score/lives rendering (FUN_0041c012)
-	renderScoreHUD(renderBitmap, pitch, width, height, 0);
-
-	// Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
-	if (_player) {
-		_player->_fobjOffsetX = 0;
-		_player->_fobjOffsetY = 0;
-	}
-
-	// Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
-	frameEndCleanup();
-}
-
-// ======================= Damage Visual Effect Functions =======================
-// Palette flash + screen shake when the player takes damage.
-// Original retail functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501
-
-// FUN_00420501 - Reset palette flash counter.
-// Called at level start / scene transitions to clear any in-progress flash.
-void InsaneRebel2::resetDamageFlash() {
-	_damageFlashCounter = 0;
-}
-
-// FUN_00420515 - Save current palette and initiate a 5-frame flash.
-// If a flash is already in progress, just resets the counter to 5
-// (the palette was already saved on the first hit).
-void InsaneRebel2::initDamageFlash() {
-	if (_damageFlashCounter == 0) {
-		// Save current SMUSH palette before modifying it
-		memcpy(_damageSavedPalette, _player->_pal, 0x300);
-	}
-	_damageFlashCounter = 5;
-}
-
-// FUN_0042073B - Trigger both palette flash and screen shake.
-// Called from the damage hit handler when the player takes damage.
-void InsaneRebel2::triggerDamageEffect() {
-	initDamageFlash();
-	_damageShakeCounter = 10;
-}
-
-// FUN_00420562 - Per-frame palette modification.
-//
-// Two modes determined by _damageHighFlashCounter:
-//
-//   Normal hit flash (_damageHighFlashCounter == 0 or odd):
-//     Decrements _damageFlashCounter. On even counter values, all 768 palette
-//     bytes (RGB) are blended from inverted toward the saved original:
-//       output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
-//     Counter 5→4(apply)→3(skip)→2(apply)→1(skip)→0(apply=original). The
-//     alternating apply/skip creates a strobe-like flash effect.
-//
-//   High-damage red pulse (_playerDamage >= 0xFF, even counter):
-//     Only the R channel (every 3rd byte) is modified using the same formula
-//     with _damageHighFlashCounter. Creates a pulsing red tint overlay.
-void InsaneRebel2::updateDamageFlashPalette() {
-	// High-damage mode: persistent red pulsing when damage is maxed out
-	if (_playerDamage < 0xFF) {
-		_damageHighFlashCounter = 0;
-	} else {
-		if (_damageHighFlashCounter == 0) {
-			// Save palette on first frame of high-damage mode
-			memcpy(_damageSavedPalette, _player->_pal, 0x300);
-		}
-		if (_damageHighFlashCounter < 0x10) {
-			_damageHighFlashCounter++;
-		}
-	}
-
-	if (_damageHighFlashCounter == 0 || (_damageHighFlashCounter & 1) != 0) {
-		// Normal hit flash path: decrement counter, apply on even values.
-		// Original C: if ((counter != 0) && (counter--, (counter & 1) == 0))
-		if (_damageFlashCounter != 0) {
-			_damageFlashCounter--;
-			if ((_damageFlashCounter & 1) == 0) {
-				// Apply palette inversion on ALL RGB channels
-				byte modPal[0x300];
-				int blend = 0x10 - _damageFlashCounter;
-				for (int i = 0; i < 0x300; i++) {
-					modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
-				}
-				_player->setPalette(modPal);
-			}
-		}
-	} else {
-		// High-damage red-only flash (even _damageHighFlashCounter):
-		// Modify only R channel (stride 3), G and B stay unchanged.
-		byte modPal[0x300];
-		memcpy(modPal, _player->_pal, 0x300);
-		int blend = 0x10 - _damageHighFlashCounter;
-		for (int i = 0; i < 0x300; i += 3) {
-			modPal[i] = 0xFF - (((0xFF - _damageSavedPalette[i]) * blend) >> 4);
-		}
-		_player->setPalette(modPal);
-	}
-}
-
-// FUN_00420754 - Per-frame screen shake + palette flash.
-//
-// Screen shake randomly shifts scanlines left or right for visual distortion.
-// The number of affected scanlines decreases each frame (counter * 5),
-// creating a diminishing shake effect over 10 frames.
-//
-// Called every frame from procPostRendering when not in cutscene modes
-// (shipLevelMode != 4 and != 5, matching original: DAT_0043e000 != 4 && != 5).
-void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
-	if (_damageShakeCounter != 0) {
-		_damageShakeCounter--;
-		int numLines = _damageShakeCounter * 5;
-
-		// Temporary buffer for scanline rotation (case 1 in original)
-		byte tempLine[640];
-
-		for (int n = numLines; n > 0; n--) {
-			// Pick a random scanline within the gameplay area (0..179, not status bar)
-			int maxY = MIN(height, 180);
-			int scanline = _vm->_rnd.getRandomNumber(maxY - 1);
-
-			byte *linePtr = renderBitmap + pitch * scanline;
-			int offset = _vm->_rnd.getRandomNumber(4) + 1;  // 1..5 pixel shift
-			int direction = _vm->_rnd.getRandomNumber(4);    // 0..4
-
-			int copyLen = pitch - offset;
-			if (copyLen <= 0)
-				continue;
-
-			switch (direction) {
-			case 0:
-			case 3:
-				// Shift left: copy line[offset..] -> line[0..]
-				memmove(linePtr, linePtr + offset, copyLen);
-				break;
-			case 1:
-				// Shift right with wrap: save, then copy
-				memcpy(tempLine, linePtr, MIN(copyLen, (int)sizeof(tempLine)));
-				memmove(linePtr + offset, tempLine, MIN(copyLen, (int)sizeof(tempLine)));
-				break;
-			case 2:
-			case 4:
-				// Shift right: copy line[0..] -> line[offset..]
-				memmove(linePtr + offset, linePtr, copyLen);
-				break;
-			}
-		}
-	}
-
-	// Palette flash runs every frame (even without shake)
-	updateDamageFlashPalette();
-}
-
-// ======================= Rendering Helper Functions =======================
-// These are extracted from procPostRendering for better readability
-
-void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
-											 int videoWidth, int videoHeight, int statusBarY) {
-	// Fill status bar background (FUN_004288c0 equivalent)
-	// Original assembly: FUN_004288c0(local_8, 0, 0, 0xb4, 0x140, 0x14, 4)
-	// This fills width=320, height=20 starting at Y=180 with color index 4
-	const byte statusBarBgColor = 4;
-
-	for (int y = statusBarY; y < videoHeight; y++) {
-		int destY = y + _viewY;
-		if (destY >= height) continue;
-		for (int x = 0; x < videoWidth; x++) {
-			int destX = x + _viewX;
-			if (destX >= pitch) continue;
-			renderBitmap[destY * pitch + destX] = statusBarBgColor;
-		}
-	}
-}
-
-void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
-	// Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
-	// From FUN_004089ab disassembly (lines 195-226):
-	// - DAT_0047fe78 (_hudOverlayNut): Primary HUD overlay with 6 animation frames
-	// - Position formula (low-res):
-	//   X = 160 + (mouseOffsetX >> 4) - (width / 2) - spriteOffsetX
-	//   Y = 182 - (mouseOffsetY >> 4) - height - spriteOffsetY
-	// - Animation: spriteIndex = (frameCounter / 2) % 6
-
-	if ((_rebelHandler != 0x26 && _rebelHandler != 0x19) || !_hudOverlayNut || _hudOverlayNut->getNumChars() <= 0)
-		return;
-
-	// Calculate mouse offset (clamped to -127..127)
-	int mouseOffsetX = (_vm->_mouse.x - 160);
-	int mouseOffsetY = (_vm->_mouse.y - 100);
-	if (mouseOffsetX > 127) mouseOffsetX = 127;
-	if (mouseOffsetX < -127) mouseOffsetX = -127;
-	if (mouseOffsetY > 127) mouseOffsetY = 127;
-	if (mouseOffsetY < -127) mouseOffsetY = -127;
-
-	// Animation frame cycling: (frameCounter / 2) % 6
-	int numSprites = _hudOverlayNut->getNumChars();
-	int animFrameCount = MIN(numSprites, 6);
-	int animFrame = 0;
-	if (animFrameCount > 0) {
-		animFrame = (curFrame / 2) % animFrameCount;
-	}
-
-	// Get sprite dimensions
-	int spriteW = _hudOverlayNut->getCharWidth(animFrame);
-	int spriteH = _hudOverlayNut->getCharHeight(animFrame);
-
-	// Position calculation from assembly (low-res mode)
-	int spriteOffsetX = 0;
-	int spriteOffsetY = 0;
-	int hudX = 160 + (mouseOffsetX >> 4) - (spriteW / 2) - spriteOffsetX;
-	int hudY = 182 - (mouseOffsetY >> 4) - spriteH - spriteOffsetY;
-
-	// Apply view offset for scrolling background
-	hudX += _viewX;
-	hudY += _viewY;
-
-	// Draw base cockpit (sprite 0 always drawn first)
-	renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, 0);
-
-	// Draw animation overlay frame if not frame 0
-	if (animFrame != 0 && animFrame < numSprites) {
-		renderNutSprite(renderBitmap, pitch, width, height, hudX, hudY, _hudOverlayNut, animFrame);
-	}
-
-	debug(5, "Rebel2 HUD: Drawing NUT overlay frame %d/%d at (%d,%d) mouseOffset=(%d,%d)",
-		  animFrame, numSprites, hudX, hudY, mouseOffsetX, mouseOffsetY);
-
-	// Draw secondary HUD overlay if present (DAT_0047fe80)
-	if (_hudOverlay2Nut && _hudOverlay2Nut->getNumChars() > 0) {
-		int spr2W = _hudOverlay2Nut->getCharWidth(0);
-		int spr2H = _hudOverlay2Nut->getCharHeight(0);
-		int hud2X = 160 + (mouseOffsetX >> 4) - (spr2W / 2) + _viewX;
-		int hud2Y = 182 - (mouseOffsetY >> 4) - spr2H + _viewY;
-		renderNutSprite(renderBitmap, pitch, width, height, hud2X, hud2Y, _hudOverlay2Nut, 0);
-	}
-}
-
-void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height) {
-	// Draw embedded SAN HUD overlays (from IACT chunks)
-	// For Handler 7 (Level 3): HUD elements are scattered across the screen
-	// For turret handlers: slots 1-2 form a two-part cockpit overlay
-
-	for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
-		EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
-		if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
-			continue;
-
-		// Handler 25: Skip slot 4 (corridor overlay) in post-rendering.
-		// The corridor is a full background image (no color 0 transparent center).
-		// Drawing it here would cover enemies. It's already drawn in procPreRendering
-		// with transparency to preserve frame persistence for codec 23 delta.
-		if (_rebelHandler == 25 && hudSlot == 4) {
-			continue;
-		}
-
-		// Skip small frames at (0,0) - likely animation patches
-		if (frame.renderX == 0 && frame.renderY == 0 && frame.width < 50 && frame.height < 60) {
-			debug(3, "Rebel2: Skipping small embedded frame at (0,0): slot=%d size=%dx%d",
-				hudSlot, frame.width, frame.height);
-			continue;
-		}
-
-		// For Handler 7: handle direction-based frame selection
-		if (_rebelHandler == 7) {
-			int groupMembers[16];
-			int groupCount = 0;
-
-			for (int id = 1; id < 16; id++) {
-				EmbeddedSanFrame &g = _rebelEmbeddedHud[id];
-				if (g.valid && g.renderX == frame.renderX && g.renderY == frame.renderY &&
-					g.width == frame.width && g.height == frame.height) {
-					groupMembers[groupCount++] = id;
-				}
-			}
-
-			if (groupCount > 1) {
-				int selectedOffset = _shipDirectionIndex % groupCount;
-				int selectedId = groupMembers[selectedOffset];
-
-				// Verify selected frame has pixels
-				EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
-				int nonZero = 0;
-				for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
-					if (selectedFrame.pixels[i] != 0) nonZero++;
-				}
-
-				if (nonZero == 0) {
-					for (int i = 0; i < groupCount; i++) {
-						EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
-						int altNonZero = 0;
-						for (int j = 0; j < altFrame.width * altFrame.height; j++) {
-							if (altFrame.pixels[j] != 0) altNonZero++;
-						}
-						if (altNonZero > 0) {
-							selectedId = groupMembers[i];
-							break;
-						}
-					}
-				}
-
-				if (hudSlot != selectedId)
-					continue;
-			}
-		}
-
-		// Calculate destination position
-		int destX = frame.renderX;
-		int destY = frame.renderY;
-
-		// Handler 0x26/0x19 turret positioning
-		if ((_rebelHandler == 0x26 || _rebelHandler == 0x19) && (hudSlot == 1 || hudSlot == 2)) {
-			destX = 160 - frame.width / 2 - frame.renderX;
-			destY = 200 - frame.height - frame.renderY;
-		}
-
-		// Handler 7 large cockpit frame positioning
-		if (_rebelHandler == 7 && (hudSlot == 1 || hudSlot == 2) && frame.width > 100) {
-			destX = 160 - frame.width / 2 - frame.renderX;
-			destY = 170 - frame.height - frame.renderY;
-		} else if (_rebelHandler == 7 && destX > 100 && destY > 50) {
-			int16 offsetX = (_shipPosX - 160) / 8;
-			int16 offsetY = (_shipPosY - 100) / 8;
-			destX += offsetX;
-			destY += offsetY;
-		}
-
-		destX += _viewX;
-		destY += _viewY;
-
-		debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d)",
-			hudSlot, frame.width, frame.height, destX, destY);
-
-		// Draw frame with transparency (pixel 0 and 231 = transparent)
-		for (int y = 0; y < frame.height && (destY + y) < height; y++) {
-			for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
-				byte pixel = frame.pixels[y * frame.width + x];
-				if (pixel != 0 && pixel != 231) {
-					int fx = destX + x;
-					int fy = destY + y;
-					if (fx >= 0 && fy >= 0) {
-						renderBitmap[fy * pitch + fx] = pixel;
-					}
-				}
-			}
-		}
-	}
-}
-
-void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
-										  int statusBarY, int32 curFrame) {
-	// Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
-	// DISPFONT.NUT contains:
-	//   Sprite 1: Status bar background frame
-	//   Sprites 2-5: Difficulty stars (1-4)
-	//   Sprite 6: Damage bar fill (with clip rect X=63, Y=9, W=64, H=6)
-	//   Sprite 7: Damage alert (flashing red when critical)
-
-	if (!_smush_cockpitNut)
-		return;
-
-	// Sprite 1: Status bar background
-	if (_smush_cockpitNut->getNumChars() > 1) {
-		renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
-	}
-
-	// Difficulty indicator (sprites 2-5)
-	int difficulty = 0;  // TODO: Read from game state
-	if (difficulty > 3) difficulty = 3;
-	int difficultySprite = difficulty + 2;
-	if (_smush_cockpitNut->getNumChars() > difficultySprite) {
-		renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
-	}
-
-	// Damage bar (sprite 6) with clipped width
-	if (_smush_cockpitNut->getNumChars() > 6) {
-		int drawWidth = (64 * _playerDamage) / 255;
-		if (drawWidth < 0) drawWidth = 0;
-		if (drawWidth > 64) drawWidth = 64;
-
-		const byte *src = _smush_cockpitNut->getCharData(6);
-		int sw = _smush_cockpitNut->getCharWidth(6);
-		int sh = _smush_cockpitNut->getCharHeight(6);
-
-		// Clip rect inside sprite: X=63, Y=9, W=64, H=6
-		const int clipX = 63, clipY = 9, clipW = 64, clipH = 6;
-
-		if (src && sw > 0 && sh > 0) {
-			int maxClipW = sw - clipX;
-			if (maxClipW < 0) maxClipW = 0;
-			int drawW = MIN(drawWidth, MIN(clipW, maxClipW));
-			int drawH = MIN(clipH, sh - clipY);
-			if (drawH < 0) drawH = 0;
-
-			for (int y = 0; y < drawH; y++) {
-				for (int x = 0; x < drawW; x++) {
-					int destX = clipX + x + _viewX;
-					int destY = statusBarY + clipY + y + _viewY;
-					if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
-						byte pixel = src[(clipY + y) * sw + (clipX + x)];
-						if (pixel != 0) {
-							renderBitmap[destY * pitch + destX] = pixel;
-						}
-					}
-				}
-			}
-		}
-	}
-
-	// Damage alert overlay (sprite 7) when damage > 170 and flashing
-	if (_smush_cockpitNut->getNumChars() > 7) {
-		if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
-			renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
-		}
-	}
-}
-
-void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
-	// Based on FUN_0040d836 lines 173-185:
-	//   FUN_004236e0(buf, frameInfo, screenX - 0xd4, screenY - 0x82, 0, sprite, frameIdx, 1, 0)
-	// The ship sprite is drawn at the perspective-transformed position offset from center.
-	// FUN_0041c720 transforms game coords (shipX, shipY) using perspective offsets.
-
-	if (_rebelHandler != 7 || !_flyShipSprite || _shipLevelMode == 5)
-		return;
-
-	int numSprites = _flyShipSprite->getNumChars();
-	int spriteIndex = _shipDirectionIndex;
-	if (spriteIndex < 0) spriteIndex = 0;
-	if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-
-	// Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
-	// The perspective transform shifts the ship position based on perspective offsets.
-	// Close view: FOBJ offset = (-52 - perspX, -45 - perspY), ship at screen center.
-	// For now, use a simplified perspective: ship position = center + offset from center
-	// scaled by perspective. In the original, FUN_00424510 shifts all FOBJ sprites.
-	//
-	// Screen position for sprite drawing (FUN_0040d836 line 174):
-	//   drawX = transformedX - 0xd4, drawY = transformedY - 0x82
-	// Where transformedX/Y come from FUN_0041c720(shipX, shipY, perspX, perspY, viewShift)
-	//
-	// Simplified: screenX = 160 + (shipX - 212) * perspFactor
-	// With the perspective formula, objects near center barely move, objects at edges move more.
-	int drawX = (_flyShipScreenX - 0xd4) + _perspectiveX;
-	int drawY = (_flyShipScreenY - 0x82) + _perspectiveY;
-
-	// Convert from game-center-relative to screen coordinates
-	// The sprite system expects coordinates relative to the 320x200 frame
-	// Center of frame = (160, 100), so offset = game position - game center
-	drawX += 160 + _viewX;
-	drawY += 100 + _viewY;
-
-	// Center the sprite on the position
-	int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
-	int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
-	drawX -= spriteW / 2;
-	drawY -= spriteH / 2;
-
-	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
-
-	// Laser overlay if firing (same position as ship)
-	if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
-		int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
-	}
-
-	debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d",
-		drawX, drawY, spriteIndex, numSprites, _flyShipScreenX, _flyShipScreenY,
-		_perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput);
-}
-
-void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
-	// Uses _shipSprite (POV001) with position-based offset
-
-	if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
-		return;
-
-	// Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
-	int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
-	int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
-
-	// Base screen position (low-res: X=160, Y=105)
-	int shipScreenX = 0xa0 + displayOffsetX;
-	int shipScreenY = 0x69 + displayOffsetY;
-
-	int numSprites = _shipSprite->getNumChars();
-	int spriteIndex = 0;
-
-	// Select sprite based on direction and sprite count
-	if (numSprites >= 35) {
-		spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-	} else if (numSprites >= 25) {
-		int vDir5 = (_shipDirectionV * 5) / 7;
-		spriteIndex = _shipDirectionH * 5 + vDir5;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-	} else if (numSprites >= 5) {
-		spriteIndex = _shipDirectionH;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
-	} else if (numSprites == 2) {
-		spriteIndex = _shipFiring ? 1 : 0;
-	}
-
-	// Center sprite at position
-	int spriteW = _shipSprite->getCharWidth(spriteIndex);
-	int spriteH = _shipSprite->getCharHeight(spriteIndex);
-	int drawX = shipScreenX - spriteW / 2 + _viewX;
-	int drawY = shipScreenY - spriteH / 2 + _viewY;
-
-	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
-
-	// Shadow sprite (POV004 / DAT_0047e028): drawn at same position as primary ship.
-	// Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index
-	// for both primary and shadow, NOT the direction-based spriteIndex.
-	if (_shipSprite2) {
-		int shadowIndex = _shipFiring ? 1 : 0;
-		if (shadowIndex < _shipSprite2->getNumChars()) {
-			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, shadowIndex);
-		}
-	}
-
-	debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
-		drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
-		spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
-}
-
-// Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
-// Per original FUN_0041DB5E, GRD sprites are drawn AFTER FOBJ enemies, before GRD002.
-//
-// From FUN_0041db5e disassembly (lines 202-221):
-// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
-// - Mode 4 with damage==0: Width halved AND buffer offset (right half only, pixels 160-319)
-// - All other cases: Full width (320 pixels)
-void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
-	if (_rebelHandler != 25)
-		return;
-
-	if (!_grd001Sprite || _grd001Sprite->getNumChars() <= 0)
-		return;
-
-	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
-	const int clipHeight = 180;
-	int renderHeight = MIN(height, clipHeight);
-
-	// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
-	// Each mode has specific conditions from FUN_0041db5e:
-	bool shouldDraw = false;
-	bool useHalfWidth = false;
-	bool useRightHalf = false;
-
-	// Mode 1 (lines 202-210): Draw with width halving when uncovered
-	if (_grdSpriteMode == 1) {
-		shouldDraw = true;
-		useHalfWidth = (_rebelDamageLevel == 0);  // Half width when uncovered
-	}
-	// Mode 2 (lines 222-224): Only draw when damaged (covered)
-	else if (_grdSpriteMode == 2 && _rebelDamageLevel != 0) {
-		shouldDraw = true;
-	}
-	// Mode 3 (lines 225-228): Always draw full width
-	else if (_grdSpriteMode == 3) {
-		shouldDraw = true;
-	}
-	// Mode 4 (lines 211-221): Draw to right half when uncovered
-	else if (_grdSpriteMode == 4) {
-		shouldDraw = true;
-		useHalfWidth = (_rebelDamageLevel == 0);
-		useRightHalf = (_rebelDamageLevel == 0);
-	}
-
-	if (shouldDraw) {
-		int spriteW = _grd001Sprite->getCharWidth(0);
-		int spriteH = _grd001Sprite->getCharHeight(0);
-		int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
-		int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
-
-		int drawX = _rebelViewOffset2X + spriteXOffset;
-		int drawY = _rebelViewOffset2Y + spriteYOffset;
-
-		// Apply width-halving logic from original assembly:
-		// When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
-		// This clips the sprite to only half the screen.
-		int renderWidth = width;
-		byte *dstBitmap = renderBitmap;
-
-		if (useHalfWidth) {
-			renderWidth = width / 2;  // Clip to half width (160 pixels)
-
-			if (useRightHalf) {
-				// Mode 4: Draw to right half by offsetting the destination buffer
-				// Original: DAT_00482230 += DAT_00482234 (adds 160 to buffer start)
-				// This makes drawing appear on the right half (pixels 160-319)
-				dstBitmap = renderBitmap + (width / 2);
-			}
-		}
-
-		renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
-
-		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
-			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
-			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, renderWidth);
-	}
-}
-
-void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
-	// Handler 25 POST-rendering: Draw GRD002 (character sprite) on top of enemies.
-	// GRD001 (wall/cockpit) is drawn before this via renderHandler25ShipPre().
-	//
-	// From FUN_0041db5e disassembly (lines 230-248):
-	// GRD002 is drawn LAST (after enemies) so the character appears in front.
-
-	if (_rebelHandler != 25)
-		return;
-
-	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
-	const int clipHeight = 180;
-	int renderHeight = MIN(height, clipHeight);
-
-	// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
-	// The sprite index is calculated based on damage level and aiming position
-	// From FUN_0041db5e lines 160-168:
-	//   If damage == 0: index = yZone * 5 + xZone + 5 (aiming-based, 5-14)
-	//   If damage != 0:
-	//     If direction == 0: index = 5 - damage (0-5, covered left)
-	//     If direction != 0: index = 25 - damage (20-25, covered right)
-	if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
-		// Calculate sprite index based on damage level and direction
-		int spriteIdx;
-		int numSprites = _grd002Sprite->getNumChars();
-
-		// Determine if we should mirror the sprite (from FUN_41DB5E lines 231-235)
-		// Mirror when: direction != 0 AND damage == 0 (fully uncovered, facing right)
-		bool shouldMirror = (_rebelFlightDir != 0) && (_rebelDamageLevel == 0);
-
-		if (_rebelDamageLevel == 0) {
-			// Uncovered state: use aiming-based sprite selection (5-14)
-			// Calculate zones from crosshair position relative to playable area
-			// From FUN_41DB5E lines 155-164
-			//
-			// The playable area bounds are defined by corridor boundaries.
-			// xZone = 0-4 (left to right), yZone = 0-1 (top to bottom)
-			// Default to center if bounds not set
-			int16 areaLeft = (_corridorLeftX > 0) ? _corridorLeftX : 0;
-			int16 areaRight = (_corridorRightX > 0) ? _corridorRightX : 320;
-			int16 areaTop = (_corridorTopY > 0) ? _corridorTopY : 0;
-			int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
-
-			// Get crosshair position (using mouse position scaled to game coords)
-			int16 crosshairX = _vm->_mouse.x;
-			int16 crosshairY = _vm->_mouse.y;
-			if (_player && _player->_width > 320) {
-				crosshairX = (crosshairX * 320) / _player->_width;
-				crosshairY = (crosshairY * 200) / _player->_height;
-			}
-
-			// Calculate zone widths
-			int areaWidth = areaRight - areaLeft;
-			int areaHeight = areaBottom - areaTop;
-			int zoneWidth = (areaWidth > 0) ? (areaWidth + 3) / 4 : 80;  // Divide into ~4 zones
-			int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;     // Divide into 2 zones
-
-			// Calculate xZone (0-4) and yZone (0-1) from crosshair position
-			int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (crosshairX - areaLeft)) / zoneWidth : 2;
-			int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (crosshairY - areaTop)) / zoneHeight : 0;
-
-			// Clamp to valid ranges
-			if (xZone < 0) xZone = 0;
-			if (xZone > 4) xZone = 4;
-			if (yZone < 0) yZone = 0;
-			if (yZone > 1) yZone = 1;
-
-			// Direction-based sprite flip logic (line 161-162 in decompiled)
-			// if (DAT_00457902 == (uVar7 & 1)) { local_58 = 4 - local_58; }
-			if (_rebelFlightDir == (yZone & 1)) {
-				xZone = 4 - xZone;
-			}
-
-			spriteIdx = yZone * 5 + xZone + 5;
-		} else {
-			// Transitioning/covered state: use direction-based sprite
-			// From FUN_41DB5E lines 166-168:
-			// sVar8 = ((-(ushort)(DAT_00457902 == 0) & 0xffec) + 0x19) - DAT_0045790a
-			// direction == 0: 5 - damage
-			// direction != 0: 25 - damage
-			if (_rebelFlightDir == 0) {
-				// Direction 0: sprites 0-5 (transition left)
-				spriteIdx = 5 - _rebelDamageLevel;
-			} else {
-				// Direction 1: sprites 20-25 (transition right)
-				spriteIdx = 25 - _rebelDamageLevel;
-			}
-		}
-
-		// Clamp to valid range
-		if (spriteIdx < 0) spriteIdx = 0;
-		if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
-
-		int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
-		int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
-
-		// Position calculation from FUN_41DB5E lines 237-247:
-		// GRD002 explicitly adds sprite internal offsets from NUT header:
-		//
-		// Normal case (direction==0 OR damage!=0):
-		//   local_60 = sprite_internal_x_offset (from NUT header +0x12)
-		//   X = DAT_00457910 + local_60
-		//   Y = sprite_internal_y_offset (from NUT header +0x14) + DAT_00457912
-		//
-		// Mirrored case (direction!=0 AND damage==0):
-		//   local_60 = 320 - sprite_width - sprite_internal_x_offset
-		//   X = DAT_00457910 + local_60
-		//   Y = sprite_internal_y_offset + DAT_00457912
-		//
-		// Now using actual NUT sprite offsets from NutRenderer!
-		int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
-		int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
-
-		int drawX, drawY;
-
-		if (shouldMirror) {
-			// Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
-			// From assembly lines 240-243
-			drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
-		} else {
-			// Normal position: X = DAT_00457910 + sprite_internal_x_offset
-			// From assembly line 238
-			drawX = _rebelViewOffset2X + spriteXOffset;
-		}
-
-		// Y = sprite_internal_y_offset + DAT_00457912
-		// From assembly line 246
-		drawY = spriteYOffset + _rebelViewOffset2Y;
-
-		renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
-
-		debug("Rebel2 Handler25: GRD002 at (%d,%d) nutOffset(%d,%d) viewOffset(%d,%d) size(%d,%d) spriteIdx=%d damage=%d dir=%d mirror=%d",
-			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y, spriteW, spriteH, spriteIdx, _rebelDamageLevel, _rebelFlightDir, shouldMirror ? 1 : 0);
-	}
-}
-
-void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
-	// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
-	// userId=11 contains the ship sprite strip
-
-	if ((_rebelHandler != 7 && _rebelHandler != 8) || _shipLevelMode == 5)
-		return;
-
-	// Skip if we have proper sprites
-	if (_rebelHandler == 7 && _flyShipSprite)
-		return;
-	if (_rebelHandler == 8 && _shipSprite)
-		return;
-
-	EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
-	if (!shipFrame.valid || !shipFrame.pixels || shipFrame.width <= 0 || shipFrame.height <= 0)
-		return;
-
-	// Calculate display offset
-	int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
-	int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
-	int shipScreenX = 0xa0 + displayOffsetX;
-	int shipScreenY = 0x69 + displayOffsetY;
-
-	// Detect sprite strip layout
-	int spriteW = shipFrame.width;
-	int spriteH = shipFrame.height;
-	int srcX = 0, srcY = 0;
-	int numHorizontal = 1, numVertical = 1;
-
-	if (spriteW >= 200 && spriteW % 5 == 0) {
-		numHorizontal = 5;
-		spriteW = shipFrame.width / 5;
-	}
-	if (spriteH >= 350 && spriteH % 7 == 0) {
-		numVertical = 7;
-		spriteH = shipFrame.height / 7;
-	}
-
-	int hDir = MIN((int)_shipDirectionH, numHorizontal - 1);
-	int vDir = MIN((int)_shipDirectionV, numVertical - 1);
-	srcX = hDir * spriteW;
-	srcY = vDir * spriteH;
-
-	int drawX = shipScreenX - spriteW / 2 + _viewX;
-	int drawY = shipScreenY - spriteH / 2 + _viewY;
-
-	// Blit from embedded HUD
-	for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
-		if (drawY + y < 0) continue;
-		for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
-			if (drawX + x < 0) continue;
-			int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
-			byte pixel = shipFrame.pixels[srcIdx];
-			if (pixel != 0 && pixel != 231) {
-				int dstIdx = (drawY + y) * pitch + (drawX + x);
-				renderBitmap[dstIdx] = pixel;
-			}
-		}
-	}
-
-	debug("Rebel2: Ship (fallback) at (%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
-		drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
-}
-
-void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
-	// Draw enemy indicator brackets for active enemies
-	//
-	// NOTE: Do NOT fill destroyed enemy areas with black. The original game does not do this.
-	// When an enemy is destroyed:
-	// 1. setBit(enemy_id) disables the enemy in the bit table
-	// 2. clearBit(dependency_id) enables dependent objects (explosion animations)
-	// 3. SKIP chunks in the video cause enemy FOBJ sprites to be skipped (via procSKIP)
-	// 4. renderExplosions() draws the explosion animation from the 5-slot system
-	// 5. The background video shows through where the enemy was
-
-	// Draw green brackets for active enemies (Easy/Medium difficulty only)
-	if (_difficulty >= 2)
-		return;
-
-	// FOBJ sprites are rendered with _fobjOffsetX/Y applied (set from _rebelViewOffsetX/Y
-	// for Handler 25). Brackets must use the same offset so they align with the sprites.
-	int fobjOffX = _player ? _player->_fobjOffsetX : 0;
-	int fobjOffY = _player ? _player->_fobjOffsetY : 0;
-
-	Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
-
-	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (it->destroyed || !it->active || isBitSet(it->id))
-			continue;
-
-		Common::Rect r(it->rect.left + fobjOffX, it->rect.top + fobjOffY,
-		               it->rect.right + fobjOffX, it->rect.bottom + fobjOffY);
-		if (r.right <= viewRect.left || r.left >= viewRect.right ||
-		    r.bottom <= viewRect.top || r.top >= viewRect.bottom)
-			continue;
-
-		const byte color = 5;  // Green
-		drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
-	}
-}
-
-// Dispatcher — calls per-handler explosion render function.
-// Original code has separate functions per handler, each with its own
-// position transformation, scale thresholds, and secondary NUT rendering.
-void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
-	switch (_rebelHandler) {
-	case 0x26:
-		renderTurretExplosions(renderBitmap, pitch, width, height);
-		break;
-	case 8:
-		renderVehicleExplosions(renderBitmap, pitch, width, height);
-		break;
-	case 7:
-		renderSpaceExplosions(renderBitmap, pitch, width, height);
-		break;
-	case 25:
-		renderHandler25Explosions(renderBitmap, pitch, width, height);
-		break;
-	default:
-		break;
-	}
-}
-
-// FUN_409FBC — Handler 0x26 (Turret/Cockpit) explosion rendering.
-// Position: Uses FUN_0041c720 for 3D→2D projection. At low-res, world coords ≈ screen coords.
-// Scale thresholds: Fixed (<11, <21).
-// Secondary NUT: DAT_0047fe80 (rendered if DAT_0047a7fc >= 0).
-// Hi-res: Coordinates doubled when DAT_0047a808 >= 2.
-void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
-	if (!_smush_iconsNut)
-		return;
-
-	for (int i = 0; i < 5; i++) {
-		if (!_explosions[i].active)
-			continue;
-
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_409FBC: Fixed thresholds (0x0b=11, 0x15=21)
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;   // Small (sprites 11-20)
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;  // Medium (sprites 21-30)
-		} else {
-			baseIndex = 29;  // Large (sprites 31-40)
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
-		// Position: world coords passed through FUN_0041c720 (3D→2D projection).
-		// At 320x200 low-res turret view, projection is effectively identity.
-		int screenX = _explosions[i].x;
-		int screenY = _explosions[i].y;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
-	}
-}
-
-// FUN_402696 — Handler 8 (Third-Person On-Foot) explosion rendering.
-// Position: World coords minus camera offset (DAT_0043e006/DAT_0043e008 = _viewX/_viewY).
-// Scale thresholds: Fixed (<11, <21) — same as handler 0x26.
-// Secondary NUT: None (only DAT_0047a828).
-void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
-	if (!_smush_iconsNut)
-		return;
-
-	for (int i = 0; i < 5; i++) {
-		if (!_explosions[i].active)
-			continue;
-
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_402696: Fixed thresholds (0x0b=11, 0x15=21)
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;
-		} else {
-			baseIndex = 29;
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
-		// FUN_402696 line 22-23: screenX = worldX - DAT_0043e006, screenY = worldY - DAT_0043e008
-		int screenX = _explosions[i].x - _viewX;
-		int screenY = _explosions[i].y - _viewY;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
-	}
-}
-
-// FUN_40F1C5 — Handler 7 (Third-Person Ship) explosion rendering.
-// Position: Uses FUN_0041c720 for 3D→2D projection.
-// Scale thresholds: Resolution-dependent (low-res: <11/<21, high-res: <21/<41).
-// Secondary NUT: DAT_0047ff00 (FLY004, rendered if DAT_0047a7fc >= 0).
-void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
-	if (!_smush_iconsNut)
-		return;
-
-	// --- Part 1: Space shot explosions (FUN_40F1C5 lines 19-60) ---
-	for (int i = 0; i < 5; i++) {
-		if (!_explosions[i].active)
-			continue;
-
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_40F1C5 lines 41-51: Resolution-dependent thresholds.
-		// Low-res (DAT_0047a808 < 2): thresholds 20, 10
-		// High-res: thresholds 40, 20
-		// We run at low-res (320x200), so use 10/20 (same as fixed handlers).
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;
-		} else {
-			baseIndex = 29;
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
-		// Position: world coords through FUN_0041c720 (3D→2D projection).
-		// At low-res, this is close to identity for the ship view.
-		int screenX = _explosions[i].x;
-		int screenY = _explosions[i].y;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
-	}
-
-	// --- Part 2: Corridor/zone hit explosion (FUN_40F1C5 lines 61-85) ---
-	// Rendered when _hitCooldown > 0 (DAT_0044374c). Decrement happens HERE
-	// (matching original where FUN_40F1C5 decrements DAT_0044374c during render).
-	// _spaceShotDirection (DAT_0044374e) determines explosion side:
-	//   0 = left side (hit left boundary), 1 = right side (hit right boundary)
-	//   2 = bottom (zone push down), 3 = top (zone push up)
-	// Sprite frames: 0x15 - cooldown = 21 - cooldown (frames 12→21 as cooldown 9→0)
-	if (_hitCooldown != 0) {
-		_hitCooldown--;
-
-		int numChars = _smush_iconsNut->getNumChars();
-		int spriteIndex = 0x15 - _hitCooldown;  // 21 - remaining cooldown
-
-		if (spriteIndex >= 0 && spriteIndex < numChars) {
-			// Compute ship screen position (simplified FUN_0041c720 transform)
-			int shipDrawX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
-			int shipDrawY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
-
-			// Per-direction offset from ship center.
-			// Original uses lookup tables (DAT_004438da etc.) indexed by
-			// _shipDirectionIndex (35 entries per direction). We approximate
-			// with fixed offsets since we don't have the table data.
-			int offsetX = 0, offsetY = 0;
-			switch (_spaceShotDirection) {
-			case 0:  // Left wall hit → explosion on left side of ship
-				offsetX = -35;
-				break;
-			case 1:  // Right wall hit → explosion on right side of ship
-				offsetX = 35;
-				break;
-			case 2:  // Zone push down → explosion on bottom
-				offsetY = 20;
-				break;
-			case 3:  // Zone push up → explosion on top
-				offsetY = -20;
-				break;
-			default:
-				break;
-			}
-
-			int drawX = shipDrawX + offsetX;
-			int drawY = shipDrawY + offsetY;
-
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				drawX - ew / 2, drawY - eh / 2, _smush_iconsNut, spriteIndex);
-
-			debug("Rebel2 H7 corridor explosion: dir=%d frame=%d pos=(%d,%d) cooldown=%d",
-				_spaceShotDirection, spriteIndex, drawX, drawY, _hitCooldown);
-		}
-	}
-}
-
-// FUN_41F29A — Handler 25 (FPS/Mixed) explosion rendering.
-// Position: World coords + view offset (DAT_0045790c/DAT_0045790e = _rebelViewOffsetX/_rebelViewOffsetY).
-// Scale thresholds: Resolution-dependent (same formula as Handler 7).
-// Secondary NUT: DAT_00482260 (hi-res HUD alternative, rendered if DAT_0047a7fc >= 0).
-// Note: No per-frame sound panning update (unlike handlers 0x26, 8, 7).
-void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
-	if (!_smush_iconsNut)
-		return;
-
-	for (int i = 0; i < 5; i++) {
-		if (!_explosions[i].active)
-			continue;
-
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;
-		} else {
-			baseIndex = 29;
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
-		// FUN_41F29A line 22-23: screenX = worldX + DAT_0045790c, screenY = worldY + DAT_0045790e
-		int screenX = _explosions[i].x + _rebelViewOffsetX;
-		int screenY = _explosions[i].y + _rebelViewOffsetY;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
-	}
-}
-
-// Dispatcher - calls appropriate render function based on current handler
-void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
-	switch (_rebelHandler) {
-	case 0x26:  // Turret - FUN_40AD63
-		renderTurretLaserShots(renderBitmap, pitch, width, height);
-		break;
-	case 8:     // Vehicle - FUN_402ED0
-		renderVehicleLaserShots(renderBitmap, pitch, width, height);
-		break;
-	case 7:     // Space combat - FUN_40FADF
-		renderSpaceLaserShots(renderBitmap, pitch, width, height);
-		break;
-	case 25:    // Speeder bike - FUN_0041f004
-		renderHandler25LaserShots(renderBitmap, pitch, width, height);
-		break;
-	default:
-		// No laser rendering for other handlers
-		break;
-	}
-}
-
-// Handler 0x26 Turret laser rendering (FUN_40AD63)
-// Gun positions depend on _rebelLevelType (DAT_004436de)
-void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height) {
-	// Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
-
-	int16 maxDuration = getShotMaxDuration();
-
-	for (int i = 0; i < 2; i++) {
-		if (_turretShots[i].counter <= 0)
-			continue;
-
-		// Calculate sound panning from target X position (FUN_004262f0 call)
-		// sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
-		int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - _viewX - 160)) / 2;
-		pan = CLIP<int16>(pan, -127, 127);
-		// TODO: Apply panning to sound channel i+1
-
-		int16 targetX = _turretShots[i].targetX;
-		int16 targetY = _turretShots[i].targetY;
-		int16 progress = maxDuration - _turretShots[i].counter;
-
-		// Gun positions based on level type (from FUN_40AD63 switch statement)
-		// Parameters from assembly: widthScale=0xC(12), heightScale=8, thickness=0xC(12)
-		switch (_rebelLevelType) {
-		case 1:
-			// Type 1: 3 guns (triple cannon configuration)
-			// Gun 1: (0x136, 0xaa) = (310, 170)
-			// Gun 2: (0xa0, 0x17c) = (160, 380)
-			// Gun 3: (0x0a, 0xaa) = (10, 170)
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				310 + _viewX, 170 + _viewY, targetX, targetY,
-				progress, maxDuration, 12, 8, 12);
-
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				160 + _viewX, 380 + _viewY, targetX, targetY,
-				progress, maxDuration, 12, 8, 12);
-
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				10 + _viewX, 170 + _viewY, targetX, targetY,
-				progress, maxDuration, 12, 8, 12);
-			break;
-
-		case 2:
-		case 5:
-			// Type 2/5: 2 guns (wing cannons)
-			// Left: (0x6e, 0xe6) = (110, 230)
-			// Right: (0xd2, 0xe6) = (210, 230)
-			// Assembly uses widthScale=0x19(25) for these types
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				110 + _viewX, 230 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
-
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				210 + _viewX, 230 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
-			break;
-
-		case 6:
-			// Type 6: 2 guns (offscreen - cinematic effect)
-			// Gun 1: (-100, 0)
-			// Gun 2: (0, 0)
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				-100 + _viewX, 0 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
-
-			drawLaserBeam(renderBitmap, pitch, width, height,
-				0 + _viewX, 0 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
-			break;
-
-		default:
-			// Default: 2 guns with alternating pattern based on shot sequence
-			// When seqNum & 1 == 0: Left (10, 50), Right (310, 130)
-			// When seqNum & 1 == 1: Left (310, 50), Right (10, 130)
-			if ((_turretShots[i].seqNum & 1) == 0) {
-				drawLaserBeam(renderBitmap, pitch, width, height,
-					10 + _viewX, 50 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
-
-				drawLaserBeam(renderBitmap, pitch, width, height,
-					310 + _viewX, 130 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
-			} else {
-				drawLaserBeam(renderBitmap, pitch, width, height,
-					310 + _viewX, 50 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
-
-				drawLaserBeam(renderBitmap, pitch, width, height,
-					10 + _viewX, 130 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
-			}
-			break;
-		}
-
-		_turretShots[i].counter--;
-	}
-}
-
-// Handler 8 Vehicle laser rendering (FUN_402ED0)
-// In the original, the laser is a short muzzle flash from gun barrel toward ship center,
-// NOT a projectile traveling across the screen. The "hit" effect is handled separately.
-void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
-	// No NUT check needed - uses pre-initialized _laserTexture
-
-	int16 maxDuration = getShotMaxDuration();
-
-	for (int i = 0; i < 2; i++) {
-		if (_vehicleShots[i].counter <= 0)
-			continue;
-
-		// Calculate sound panning from STORED target position (FUN_402ED0 lines 24-51)
-		// pan = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
-		int16 pan = ((2 - _vehicleShots[i].counter) * (_vehicleShots[i].targetX - _viewX - 160)) / 2;
-		pan = CLIP<int16>(pan, -127, 127);
-		// TODO: Apply panning
-
-		// Calculate positions from CURRENT ship position (FUN_402ED0 lines 53-122)
-		// The original game draws the laser from gun position toward ship center,
-		// creating a short muzzle flash effect (7 pixels horizontal, 25 pixels vertical).
-		//
-		// Low-res formula (DAT_0047a808 < 2):
-		// shipScreenY = ((shipPosY - 0x28) >> 2) + 0x69 = ((shipPosY - 40) >> 2) + 105
-		// shipScreenX = ((shipPosX - 0xa0) >> 3) + 0xa0 = ((shipPosX - 160) >> 3) + 160
-		// gunY = ((shipPosY - 0x28) >> 2) + 0x82 = shipScreenY + 25
-		// gunX = ((shipPosX - 0xa0) >> 3) + 0xa7 = shipScreenX + 7
-		int16 shipScreenX = ((_shipPosX - 160) >> 3) + 160;
-		int16 shipScreenY = ((_shipPosY - 40) >> 2) + 105;
-		int16 gunX = shipScreenX + 7;
-		int16 gunY = shipScreenY + 25;
-
-		int16 progress = maxDuration - _vehicleShots[i].counter;
-
-		// Draw beam from gun toward ship center (muzzle flash effect)
-		// From FUN_402ED0: widthScale=0x14(20), heightScale=8, thickness=4
-		// Parameters: gunX, gunY -> shipScreenX, shipScreenY (NOT the stored target!)
-		drawLaserBeam(renderBitmap, pitch, width, height,
-			gunX + _viewX, gunY + _viewY,
-			shipScreenX + _viewX, shipScreenY + _viewY,
-			progress, maxDuration, 20, 8, 4);
-
-		_vehicleShots[i].counter--;
-	}
-}
-
-// Handler 7 Space combat laser rendering (FUN_40FADF)
-// Dual beams from left and right gun positions
-void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height) {
-	// No NUT check needed - uses pre-initialized _laserTexture
-
-	int16 maxDuration = getShotMaxDuration();
-
-	for (int i = 0; i < 2; i++) {
-		if (_spaceShots[i].counter <= 0)
-			continue;
-
-		// Calculate sound panning
-		int16 pan = ((_spaceShots[i].targetX - 160) * (2 - _spaceShots[i].counter)) / 2;
-		pan = CLIP<int16>(pan, -127, 127);
-		// TODO: Apply panning
-
-		int16 targetX = _spaceShots[i].targetX;
-		int16 targetY = _spaceShots[i].targetY;
-		int16 leftGunX = _spaceShots[i].leftGunX;
-		int16 leftGunY = _spaceShots[i].leftGunY;
-		int16 rightGunX = _spaceShots[i].rightGunX;
-		int16 rightGunY = _spaceShots[i].rightGunY;
-		int16 progress = maxDuration - _spaceShots[i].counter;
-
-		// Draw dual beams
-		// From FUN_40FADF: widthScale=0xC(12), heightScale=4, thickness=6
-		// Left gun beam
-		drawLaserBeam(renderBitmap, pitch, width, height,
-			leftGunX, leftGunY, targetX, targetY,
-			progress, maxDuration, 12, 4, 6);
-
-		// Right gun beam
-		drawLaserBeam(renderBitmap, pitch, width, height,
-			rightGunX, rightGunY, targetX, targetY,
-			progress, maxDuration, 12, 4, 6);
-
-		_spaceShots[i].counter--;
-	}
-}
-
-// Handler 25 laser rendering (FUN_0041f004)
-// Speeder bike laser shots - draws beam from gun position to target
-void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height) {
-	// FUN_0041f004 uses turret-style shot slots with view offset adjustment
-	// Only render when player is uncovered (damage == 0)
-	if (_rebelDamageLevel != 0) {
-		return;  // Can't shoot while taking cover
-	}
-
-	int16 maxDuration = getShotMaxDuration();
-
-	for (int i = 0; i < 2; i++) {
-		if (_turretShots[i].counter <= 0)
-			continue;
-
-		// Calculate sound panning from target X position (FUN_004262f0)
-		// sVar1 = ((2 - counter) * (targetX - 160)) / 2, clamped to [-127, 127]
-		int16 pan = ((2 - _turretShots[i].counter) * (_turretShots[i].targetX - 160)) / 2;
-		pan = CLIP<int16>(pan, -127, 127);
-		// TODO: Apply panning to sound channel i+1
-
-		// Target position (where player clicked)
-		int16 targetX = _turretShots[i].targetX;
-		int16 targetY = _turretShots[i].targetY;
-
-		// Gun position computed at spawn time from GRD002 sprite data
-		// Original: DAT_0045791c[i] + DAT_0045790c, DAT_00457920[i] + DAT_0045790e
-		int16 gunX = _turretShots[i].gunX;
-		int16 gunY = _turretShots[i].gunY;
-
-		int16 progress = maxDuration - _turretShots[i].counter;
-
-		// From FUN_0041f004 parameters for FUN_0040bbf6:
-		// widthScale=0xC(12), heightScale=4, thickness=6
-		drawLaserBeam(renderBitmap, pitch, width, height,
-			gunX, gunY, targetX, targetY,
-			progress, maxDuration, 12, 4, 6);
-
-		_turretShots[i].counter--;
-
-		debug("Rebel2 Handler25: Laser shot %d from (%d,%d) to (%d,%d) progress=%d/%d",
-			i, gunX, gunY, targetX, targetY, progress, maxDuration);
-	}
-}
-
-void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
-	// From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
-	// Don't draw crosshair when shooting is disabled (flight-only segments)
-	if (!isShootingAllowed()) {
-		return;
-	}
-
-	// Handler 25 (0x19): From FUN_41DB5E lines 195-197, crosshair only drawn when
-	// DAT_0045790a == 0 (fully uncovered). Hide crosshair during cover transition.
-	if (_rebelHandler == 25 && _rebelDamageLevel != 0) {
-		return;
-	}
-
-	// Update target lock state and draw crosshair/reticle
-
-	// Target lock detection (DAT_00443676 equivalent)
-	Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
-	bool targetLocked = false;
-
-	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (it->active && !it->destroyed && it->rect.contains(worldMousePos)) {
-			targetLocked = true;
-			break;
-		}
-	}
-
-	if (targetLocked) {
-		_targetLockTimer = 7;
-	} else if (_targetLockTimer > 0) {
-		_targetLockTimer--;
-	}
-
-	// Draw crosshair
-	if (!_smush_iconsNut)
-		return;
-
-	int reticleIndex;
-	switch (_rebelHandler) {
-	case 7:    // Third-Person Ship
-	case 0x19: // FPS/Mixed (Handler 25)
-		reticleIndex = 47;  // 0x2F
-		break;
-	case 0x26: { // Turret/Cockpit - animated crosshair
-		static int turretAnimCounter = 0;
-		turretAnimCounter++;
-
-		int animOffset = (_targetLockTimer == 0) ? 0 : 3 - (turretAnimCounter & 3);
-
-		if (_rebelLevelType == 5) {
-			reticleIndex = 0x30 + animOffset;
-		} else {
-			reticleIndex = animOffset;
-		}
-		break;
-	}
-	case 8:    // Third-Person On Foot
-	default:
-		reticleIndex = 46;
-		break;
-	}
-
-	if (_smush_iconsNut->getNumChars() > reticleIndex) {
-		int cw = _smush_iconsNut->getCharWidth(reticleIndex);
-		int ch = _smush_iconsNut->getCharHeight(reticleIndex);
-
-		// Calculate crosshair position
-		int crosshairX = _vm->_mouse.x - cw / 2 + _viewX;
-		int crosshairY = _vm->_mouse.y - ch / 2 + _viewY;
-
-		// Handler 25 (0x19): Add view offset to crosshair position
-		// From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
-		if (_rebelHandler == 25) {
-			crosshairX += _rebelViewOffsetX;
-			crosshairY += _rebelViewOffsetY;
-		}
-
-		renderNutSprite(renderBitmap, pitch, width, height,
-			crosshairX, crosshairY,
-			_smush_iconsNut, reticleIndex);
-	}
-}
-
-void InsaneRebel2::frameEndCleanup() {
-	// Reset enemy active flags and collision zones at frame end
-	// The original game rebuilds lists from scratch each frame
-
-	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
-		if (!it->destroyed) {
-			it->active = false;
-		}
-	}
-
-	resetCollisionZones();
-}
-
-// ========== Audio Handling for Rebel Assault 2 ==========
-// RA2 doesn't use iMUSE - we handle audio directly through the mixer
-
-void InsaneRebel2::initAudio(int sampleRate) {
-	_audioSampleRate = sampleRate;
-	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
-		_audioStreams[i] = nullptr;
-		_audioTrackActive[i] = false;
-	}
-}
-
-void InsaneRebel2::terminateAudio() {
-	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
-		if (_audioTrackActive[i]) {
-			_vm->_mixer->stopHandle(_audioHandles[i]);
-			_audioTrackActive[i] = false;
-		}
-		if (_audioStreams[i]) {
-			_audioStreams[i]->finish();
-			_audioStreams[i] = nullptr;
-		}
-	}
-}
-
-void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
-	if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
-		debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
-		return;
-	}
-
-	debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
-
-	// Create audio stream if not already active
-	if (!_audioStreams[trackIdx]) {
-		// RA2 audio is 8-bit unsigned mono at the track's sample rate
-		debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
-		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
-		_audioTrackActive[trackIdx] = true;
-		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
-								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
-								DisposeAfterUse::NO);
-	}
-
-	debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
-
-	// Copy the audio data since queueBuffer may need to own it
-	byte *audioCopy = (byte *)malloc(size);
-	if (!audioCopy) {
-		return;
-	}
-	memcpy(audioCopy, data, size);
-
-	// Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
-	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
-	// Apply volume and pan to the channel
-	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
-	int scaledPan = (pan * 127) / 128;  // Convert -128..127 to -127..127
-	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
-	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
-}
-
-void InsaneRebel2::processAudioFrame(int16 feedSize) {
-	if (!_player) {
-		return;
-	}
-
-	// Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
-	if (_player->_smushTracksNeedInit) {
-		_player->_smushTracksNeedInit = false;
-		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
-			_player->_smushDispatch[i].fadeRemaining = 0;
-			_player->_smushDispatch[i].fadeVolume = 0;
-			_player->_smushDispatch[i].fadeSampleRate = 0;
-			_player->_smushDispatch[i].elapsedAudio = 0;
-			_player->_smushDispatch[i].audioLength = 0;
-		}
-	}
-
-	// Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
-	// Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
-	for (int i = 0; i < _player->_smushNumTracks; i++) {
-		SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
-		SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
-
-		if (track.state == TRK_STATE_INACTIVE) {
-			continue;
-		}
-
-		// Skip tracks that don't have valid buffer pointers yet
-		// Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
-		// so tracks in FADING state won't have it set yet - that's OK, they'll be
-		// transitioned below and then processed
-		if (!track.blockPtr) {
-			debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
-				  i, (void*)track.blockPtr, track.state);
-			continue;
-		}
-
-		// Check if this track type should be played
-		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
-
-		if (!isPlayableTrack) {
-			continue;
-		}
-
-		// Calculate base volume for this track type
-		int baseVolume;
-		switch (track.flags & TRK_TYPE_MASK) {
-		case IS_SFX:
-			baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
-			break;
-		case IS_BKG_MUSIC:
-			baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
-			break;
-		case IS_SPEECH:
-			baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
-			break;
-		default:
-			baseVolume = track.volume;
-			break;
-		}
-		int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
-
-		// Handle track state transitions: FADING -> PLAYING
-		if (track.state == TRK_STATE_FADING) {
-			dispatch.headerPtr = track.dataBuf;
-			dispatch.dataBuf = track.subChunkPtr;
-			dispatch.dataSize = track.dataSize;
-			dispatch.currentOffset = 0;
-			dispatch.audioLength = 0;
-			track.state = TRK_STATE_PLAYING;
-		}
-
-		// Process audio for this track
-		if (track.state != TRK_STATE_INACTIVE) {
-			int32 tmpFeedSize = feedSize;
-
-			while (tmpFeedSize > 0) {
-				int32 mixInFrameCount = dispatch.currentOffset;
-
-				// Use dispatch.dataBuf and dispatch.dataSize which are set consistently
-				// when the track transitions from FADING to PLAYING, and audioRemaining
-				// is calculated relative to these values by processAudioCodes
-				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
-					// Ensure audioRemaining is non-negative for proper circular buffer access
-					if (dispatch.audioRemaining < 0) {
-						debug(5, "InsaneRebel2: Resetting negative audioRemaining=%d for track %d", dispatch.audioRemaining, i);
-						dispatch.audioRemaining = 0;
-					}
-					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
-					// Limit to feed size proportional to sample rate
-					if (dispatch.sampleRate > 0 && _player->_smushAudioSampleRate > 0) {
-						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / _player->_smushAudioSampleRate;
-						if (mixInFrameCount > maxFrames) {
-							mixInFrameCount = maxFrames;
-						}
-					}
-
-					// Don't read past the buffer
-					if (offset + mixInFrameCount > dispatch.dataSize) {
-						mixInFrameCount = dispatch.dataSize - offset;
-					}
-
-					// Make sure we don't exceed available data
-					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
-						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
-						if (mixInFrameCount <= 0) {
-							// Track is ending - no more data
-							track.state = TRK_STATE_ENDING;
-							break;
-						}
-					}
-
-					if (mixInFrameCount > 0) {
-						// Safety check: verify the pointer and offset are within bounds
-						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize) {
-							debug(1, "InsaneRebel2: Invalid audio buffer access track=%d dataBuf=%p offset=%d mixInFrameCount=%d dataSize=%d",
-								  i, (void*)dispatch.dataBuf, offset, mixInFrameCount, dispatch.dataSize);
-							break;
-						}
-
-						// Queue audio data directly to our audio streams
-						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
-						// Update dispatch state
-						dispatch.currentOffset -= mixInFrameCount;
-						dispatch.audioRemaining += mixInFrameCount;
-
-						// Calculate how much feed time was consumed
-						if (dispatch.sampleRate > 0) {
-							int32 consumedFeed = mixInFrameCount * _player->_smushAudioSampleRate / dispatch.sampleRate;
-							tmpFeedSize -= consumedFeed;
-						} else {
-							tmpFeedSize -= mixInFrameCount;
-						}
-					}
-				}
-
-				// If currentOffset is depleted, process audio codes to get more
-				if (dispatch.currentOffset <= 0) {
-					// processAudioCodes returns true if there's more audio, false if done
-					if (!_player->processAudioCodes(i, tmpFeedSize, mixVolume)) {
-						break;
-					}
-					// If still no offset after processing codes, we're done
-					if (dispatch.currentOffset <= 0) {
-						break;
-					}
-				} else if (tmpFeedSize <= 0) {
-					break;
-				}
-			}
-		}
-
-		track.audioRemaining = dispatch.audioRemaining;
-		dispatch.state = track.state;
-	}
-}
-
-// ========== Sound Effects (SAD files) ==========
-// Loads standalone SAUD files from SYSTM/ for one-shot SFX playback.
-// Original game loads these via FUN_0042a3b0 at init into DAT_00456888[0..7].
-
-static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
-	"SYSTM/BLAST.SAD",    // 0 - Player laser fire
-	"SYSTM/CRASH.SAD",    // 1 - Corridor/wall collision
-	"SYSTM/EXPLODE.SAD",  // 2 - Enemy explosion
-	"SYSTM/ALERT.SAD",    // 3 - Alert/warning
-	"SYSTM/LOCKON.SAD",   // 4 - Target lock-on
-	"SYSTM/BONUS.SAD",    // 5 - Bonus pickup
-	"SYSTM/HBLAST.SAD",   // 6 - Heavy blast (player weapon)
-	"SYSTM/TBLAST.SAD"    // 7 - TIE blast
-};
-
-void InsaneRebel2::loadSfx() {
-	for (int i = 0; i < kRA2NumSfx; i++) {
-		ScummFile *file = _vm->instantiateScummFile();
-		_vm->openFile(*file, kRA2SfxFiles[i]);
-		if (!file->isOpen()) {
-			debug("InsaneRebel2::loadSfx: Could not open %s", kRA2SfxFiles[i]);
-			delete file;
-			continue;
-		}
-
-		// SAUD file structure: SAUD header (8) + STRK sub-chunk + SDAT sub-chunk
-		// We scan for the SDAT tag to find the PCM data.
-		uint32 fileSize = file->size();
-		if (fileSize < 38) {  // Minimum: 8 (SAUD) + 22 (STRK) + 8 (SDAT header)
-			debug("InsaneRebel2::loadSfx: %s too small (%d bytes)", kRA2SfxFiles[i], fileSize);
-			file->close();
-			delete file;
-			continue;
-		}
-
-		// Verify SAUD tag
-		uint32 tag = file->readUint32BE();
-		if (tag != MKTAG('S', 'A', 'U', 'D')) {
-			debug("InsaneRebel2::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA2SfxFiles[i], tag);
-			file->close();
-			delete file;
-			continue;
-		}
-		file->readUint32BE();  // Skip SAUD size
-
-		// Scan for SDAT chunk (skip STRK and any other sub-chunks)
-		bool foundSdat = false;
-		while (file->pos() + 8 <= (int64)fileSize) {
-			uint32 chunkTag = file->readUint32BE();
-			uint32 chunkSize = file->readUint32BE();
-
-			if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
-				// Found PCM data
-				uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
-				_sfxData[i] = (byte *)malloc(pcmSize);
-				if (_sfxData[i]) {
-					file->read(_sfxData[i], pcmSize);
-					_sfxSize[i] = pcmSize;
-					debug("InsaneRebel2::loadSfx: Loaded %s (%d bytes PCM)", kRA2SfxFiles[i], pcmSize);
-				}
-				foundSdat = true;
-				break;
-			} else {
-				// Skip this sub-chunk
-				file->seek(chunkSize, SEEK_CUR);
-			}
-		}
-
-		if (!foundSdat) {
-			debug("InsaneRebel2::loadSfx: No SDAT chunk in %s", kRA2SfxFiles[i]);
-		}
-
-		file->close();
-		delete file;
-	}
-}
-
-void InsaneRebel2::freeSfx() {
-	for (int i = 0; i < kRA2NumSfx; i++) {
-		// Stop any playing SFX on this slot
-		_vm->_mixer->stopHandle(_sfxHandles[i]);
-		free(_sfxData[i]);
-		_sfxData[i] = nullptr;
-		_sfxSize[i] = 0;
-	}
-}
-
-void InsaneRebel2::playSfx(int slot, int volume, int pan) {
-	if (slot < 0 || slot >= kRA2NumSfx || !_sfxData[slot] || _sfxSize[slot] == 0) {
-		return;
-	}
-
-	// Stop any previous instance of this SFX slot
-	_vm->_mixer->stopHandle(_sfxHandles[slot]);
-
-	// Make a copy of the PCM data (makeRawStream with DisposeAfterUse::YES will free it)
-	byte *pcmCopy = (byte *)malloc(_sfxSize[slot]);
-	if (!pcmCopy) {
-		return;
-	}
-	memcpy(pcmCopy, _sfxData[slot], _sfxSize[slot]);
-
-	// Create a one-shot raw audio stream: 8-bit unsigned mono at 11025 Hz
-	Audio::SeekableAudioStream *stream = Audio::makeRawStream(
-		pcmCopy, _sfxSize[slot], 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
-
-	// Scale volume from 0-127 to ScummVM's 0-255 range
-	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
-
-	_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_sfxHandles[slot],
-		stream, -1, scaledVolume, pan);
-
-	debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
-}
-
-// ======================= Menu System Implementation =======================
-// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
-
-void InsaneRebel2::resetMenu() {
-	_menuSelection = 0;
-	_menuInactivityTimer = 0;
-	_menuRepeatDelay = 0;
-}
-
-// Unlock all chapters for testing
-// Emulates the debug mode from original FUN_00415CF8 (lines 60-71)
-// where DAT_0047ab34 == 'd' enables level unlock via special codes
-void InsaneRebel2::unlockAllChapters() {
-	debug("Rebel2: Unlocking all chapters for testing");
-	_debugUnlockAll = true;
-	for (int i = 0; i < 16; i++) {
-		_chapterUnlocked[i] = true;
-		_levelUnlocked[i] = true;
-	}
-}
-
-Common::String InsaneRebel2::getRandomMenuVideo() {
-	// Emulates FUN_0041FDC8 - selects random menu video variant
-	//
-	// NOTE: The original game plays O_MENU.SAN when no progress flags are set,
-	// but that file contains ONLY audio (no FOBJ video frames). The O_MENU_X.SAN
-	// variants (A through O) contain actual 320x200 background images in Frame 0.
-	//
-	// We ALWAYS use a random variant to ensure a proper background is displayed.
-	// The original behavior of showing O_MENU.SAN (audio-only) would result in
-	// a black/undefined background which doesn't match the intended experience.
-
-	// Select random variant (0-14 maps to A-O), ensuring different from last
-	int variant;
-	do {
-		variant = _vm->_rnd.getRandomNumber(14);  // 0-14
-	} while (variant == _lastMenuVariant && _lastMenuVariant >= 0);
-	_lastMenuVariant = variant;
-
-	// Map 0-14 to A-O (case 0/default = A, 1 = B, etc.)
-	char letter = 'A' + variant;
-	debug("Rebel2: Selected menu variant %c", letter);
-	return Common::String::format("OPEN/O_MENU_%c.SAN", letter);
-}
-
-int InsaneRebel2::processMenuInput() {
-	// Emulates FUN_0041f5ae menu input handling
-	// Returns: -1 = no action, 0-4 = menu item selected
-	//
-	// Events are captured by notifyEvent() (EventObserver) which runs before
-	// ScummEngine::parseEvents() consumes them. This ensures we don't miss
-	// any input events even though we only process them on video frames.
-	//
-	// From FUN_0041f5ae disassembly:
-	// - Keyboard: Up/Down arrows navigate, Enter confirms
-	// - Mouse mode (DAT_0047a806 == 1): Y position maps to selection
-	// - Key codes: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b
-
-	int result = -1;
-
-	// Menu item Y positions (low-res 320x200 mode):
-	// From FUN_0041f5ae: baseY = numItems * -5 + 0x68
-	// With 8 total items (title + 7 options): 8 * -5 + 104 = 64
-	// Items at Y = 64, 74, 84, 94, 104, 114, 124 with spacing of 10
-	const int numItemsTotal = 8;  // Title + 7 selectable items (matching assembly)
-	const int baseY = numItemsTotal * -5 + 0x68;  // = 64
-	const int itemSpacing = 10;
-
-	// Process events from the queue (populated by notifyEvent)
-	while (!_menuEventQueue.empty()) {
-		Common::Event event = _menuEventQueue.pop();
-		switch (event.type) {
-		case Common::EVENT_KEYDOWN:
-			_menuInactivityTimer = 0;  // Reset inactivity timer on any input
-
-			switch (event.kbd.keycode) {
-			case Common::KEYCODE_UP:
-				// Navigate up (wrap around) - emulates key code 0x148
-				_menuSelection--;
-				if (_menuSelection < 0) {
-					_menuSelection = _menuItemCount - 1;
-				}
-				// Reset repeat delay counter (DAT_00459ce0)
-				_menuRepeatDelay = 3;
-				debug("Menu: Selection changed to %d (UP)", _menuSelection);
-				break;
-
-			case Common::KEYCODE_DOWN:
-				// Navigate down (wrap around) - emulates key code 0x150
-				_menuSelection++;
-				if (_menuSelection >= _menuItemCount) {
-					_menuSelection = 0;
-				}
-				_menuRepeatDelay = 3;
-				debug("Menu: Selection changed to %d (DOWN)", _menuSelection);
-				break;
-
-			case Common::KEYCODE_RETURN:
-			case Common::KEYCODE_KP_ENTER:
-				// Confirm selection - emulates key code 0x0d
-				if (_menuSelection >= 0 && _menuSelection < _menuItemCount) {
-					result = _menuSelection;
-					debug("Menu: Item %d selected (ENTER)", _menuSelection);
-				}
-				break;
-
-			case Common::KEYCODE_ESCAPE:
-				// ESC - Quit (index 4 = last item) - emulates key code 0x1b
-				result = _menuItemCount - 1;  // Select quit option
-				debug("Menu: ESC pressed - selecting quit (item %d)", result);
-				break;
-
-			default:
-				break;
-			}
-			break;
-
-		case Common::EVENT_LBUTTONDOWN:
-			_menuInactivityTimer = 0;
-			{
-				// Get mouse position from the event
-				int mouseY = event.mouse.y;
-
-				debug("Menu: Left click at Y=%d", mouseY);
-
-				// Check which item was clicked
-				// From FUN_0041f5ae mouse mode: selection = (mouseY + 100 - baseY) / 10
-				// But we use a simpler direct hit-test approach
-				for (int i = 0; i < _menuItemCount; i++) {
-					int itemY = baseY + i * itemSpacing;
-					// Hit area: itemY - 2 to itemY + 8 (10 pixel height)
-					if (mouseY >= itemY - 2 && mouseY < itemY + 8) {
-						_menuSelection = i;
-						result = i;
-						debug("Menu: Item %d clicked (itemY=%d)", i, itemY);
-						break;
-					}
-				}
-			}
-			break;
-
-		case Common::EVENT_MOUSEMOVE:
-			// Update hover selection based on Y position
-			// This emulates FUN_0041f5ae mouse mode behavior (DAT_0047a806 == 1)
-			{
-				int mouseY = event.mouse.y;
-				// Calculate selection from mouse Y position
-				// From assembly: DAT_00459988 = ((mouseY + 100) - (param_3 * -5 + 0x67)) / 10
-				int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
-
-				// Clamp to valid range
-				if (newSelection < 0) newSelection = 0;
-				if (newSelection >= _menuItemCount) newSelection = _menuItemCount - 1;
-
-				// Only update if within menu area (not too far above/below)
-				int topY = baseY - 5;
-				int bottomY = baseY + (_menuItemCount - 1) * itemSpacing + 10;
-				if (mouseY >= topY && mouseY <= bottomY) {
-					if (newSelection != _menuSelection) {
-						_menuSelection = newSelection;
-						debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
-					}
-				}
-			}
-			_vm->_mouse.x = event.mouse.x;
-			_vm->_mouse.y = event.mouse.y;
-			break;
-
-		case Common::EVENT_QUIT:
-		case Common::EVENT_RETURN_TO_LAUNCHER:
-			// Handle quit request - select quit option
-			result = _menuItemCount - 1;
-			break;
-
-		default:
-			break;
-		}
-	}
-
-	// Decrement repeat delay counter (for smooth keyboard navigation)
-	if (_menuRepeatDelay > 0) {
-		_menuRepeatDelay--;
-	}
-
-	return result;
-}
-
-void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
-                                  const char **items, int numItems, int selection,
-                                  bool leftAligned) {
-	// =====================================================================
-	// Shared menu renderer - Emulates FUN_0041f5ae
-	// Address: 0x41F5AE
-	// =====================================================================
-	//
-	// items[0] = title string, items[1..numItems] = selectable items
-	// numItems = number of selectable items (FUN_0041f5ae param_3)
-	// selection = currently highlighted item (0-based, maps to DAT_00459988)
-	// leftAligned = false: param_4==0 (centered), true: param_4==1 (left-aligned)
-	//
-	// Coordinate formulas from FUN_0041f5ae (low-res, DAT_0047a808 < 2):
-	// Centered (param_4=0):
-	//   Title X:     center - titleWidth/2  (centerX = 160)
-	//   Title Y:     param_3 * -5 + 0x51
-	//   Item X:      center - textWidth/2
-	//   Box X:       center - bracketWidth/2
-	// Left-aligned (param_4=1):
-	//   Title X:     0x28 = 40
-	//   Title Y:     param_3 * -5 + 0x56
-	//   Item X:      0x17 = 23
-	//   Box X:       0x14 = 20
-	// Both modes:
-	//   Item base Y: param_3 * -5 + 0x68
-	//   Item Y:      param_3 * -5 + i * 10 + 0x68
-	//   Box Y:       param_3 * -5 + i * 10 + 0x67  (1px above text)
-
-	const int centerX = width / 2;
-	const int titleY = numItems * -5 + (leftAligned ? 0x56 : 0x51);
-	const int itemBaseY = numItems * -5 + 0x68;
-	const int itemSpacing = 10;
-
-	// =====================================================================
-	// Font system - Emulates linked list from FUN_00403bd0
-	// =====================================================================
-	//   Font 0 (^f00): TALKFONT.NUT
-	//   Font 1 (^f01): SMALFONT.NUT (menu items)
-	//   Font 2 (^f02): TITLFONT.NUT (title)
-	NutRenderer *fonts[3] = {
-		_smush_talkfontNut,
-		_smush_smalfontNut,
-		_smush_titlefontNut
-	};
-
-	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
-	if (!defaultFont) {
-		debug(1, "drawMenuItems: no fonts available!");
-		return;
-	}
-
-	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
-	int actualPitch = _vm->_screenWidth;
-
-	// =====================================================================
-	// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
-	// =====================================================================
-	//   ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
-	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
-		if (*str != '^') return -1;
-
-		const char *p = str + 1;
-		if (*p == '^') {
-			str = p;
-			return -1;
-		}
-		if (*p == 'f') {
-			p++;
-			int fontIdx = 0;
-			while (*p >= '0' && *p <= '9') {
-				fontIdx = fontIdx * 10 + (*p - '0');
-				p++;
-			}
-			str = p;
-			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
-		}
-		if (*p == 'c') {
-			p++;
-			int color = 0;
-			while (*p >= '0' && *p <= '9') {
-				color = color * 10 + (*p - '0');
-				p++;
-			}
-			str = p;
-			outColor = color;
-			return -2;
-		}
-		if (*p == 'l') {
-			str = p + 1;
-			return -2;
-		}
-		return -1;
-	};
-
-	// String width calculation - Emulates FUN_00433da0
-	auto getStringWidth = [&](const char *str) -> int {
-		int w = 0;
-		NutRenderer *curFont = defaultFont;
-		int curColor = -1;
-
-		while (*str) {
-			int fontChange = parseFormatCode(str, curColor);
-			if (fontChange >= 0) {
-				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
-				continue;
-			}
-			if (fontChange == -2) continue;
-
-			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-			if (curFont && c < curFont->getNumChars()) {
-				w += curFont->getCharWidth(c);
-			}
-		}
-		return w;
-	};
-
-	// String rendering - Emulates FUN_00434d10
-	// Codec 44 color substitution: font pixels with value 1 → ^cNNN color
-	auto drawString = [&](const char *str, int x, int y) {
-		NutRenderer *curFont = defaultFont;
-		int curColor = 1;
-
-		while (*str) {
-			int fontChange = parseFormatCode(str, curColor);
-			if (fontChange >= 0) {
-				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
-				continue;
-			}
-			if (fontChange == -2) continue;
-
-			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-
-			if (!curFont) continue;
-			int numChars = curFont->getNumChars();
-			if (c >= numChars) continue;
-
-			int charW = curFont->getCharWidth(c);
-
-			if (x >= 0 && y >= 0 && charW > 0) {
-				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
-				                    kStyleAlignLeft, c, false, false);
-			}
-			x += charW;
-		}
-	};
-
-	// =====================================================================
-	// Draw title - items[0]
-	// Centered: X = center - titleWidth/2
-	// Left-aligned: X = 40 (0x28)
-	// =====================================================================
-	{
-		int titleWidth = getStringWidth(items[0]);
-		int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
-		drawString(items[0], titleX, titleY);
-	}
-
-	// =====================================================================
-	// Draw selectable items with selection highlight box
-	// Centered: item X = center - textWidth/2, box X = center - bracketWidth/2
-	// Left-aligned: item X = 23 (0x17), box X = 20 (0x14)
-	// =====================================================================
-	for (int i = 0; i < numItems; i++) {
-		int itemY = itemBaseY + i * itemSpacing;
-		const char *text = items[i + 1];
-
-		int textWidth = getStringWidth(text);
-		int textX = leftAligned ? 23 : (centerX - textWidth / 2);
-		drawString(text, textX, itemY);
-
-		// Selection highlight box - FUN_004292d0
-		if (i == selection) {
-			// Width: textWidth + ((DAT_0047a808 < 2) - 1 & 6) + 6 = textWidth + 6
-			int bracketWidth = textWidth + 6;
-			// Height: ((DAT_0047a808 < 2) - 1 & 10) + 10 = 10
-			int bracketHeight = 10;
-
-			// Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
-			// bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
-			static int frameCounter = 0;
-			frameCounter++;
-			byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
-
-			// Box position: Y = itemY - 1 (0x67 vs 0x68)
-			int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
-			int rightX = leftX + bracketWidth;
-			int topY = itemY - 1;
-			int bottomY = itemY + bracketHeight - 1;
-
-			int screenW = _vm->_screenWidth;
-			int screenH = _vm->_screenHeight;
-			if (leftX < 0) leftX = 0;
-			if (rightX >= screenW) rightX = screenW - 1;
-			if (topY < 0) topY = 0;
-			if (bottomY >= screenH) bottomY = screenH - 1;
-
-			// FUN_004292d0 - Draw rectangle border (4 lines)
-			for (int x = leftX; x <= rightX && x < screenW; x++) {
-				if (topY >= 0 && topY < screenH)
-					renderBitmap[topY * actualPitch + x] = highlightColor;
-				if (bottomY >= 0 && bottomY < screenH)
-					renderBitmap[bottomY * actualPitch + x] = highlightColor;
-			}
-			for (int py = topY; py <= bottomY && py < screenH; py++) {
-				if (leftX >= 0 && leftX < screenW)
-					renderBitmap[py * actualPitch + leftX] = highlightColor;
-				if (rightX >= 0 && rightX < screenW)
-					renderBitmap[py * actualPitch + rightX] = highlightColor;
-			}
-		}
-	}
-}
-
-void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// =====================================================================
-	// Main menu renderer - calls shared drawMenuItems()
-	// Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
-	// =====================================================================
-	//
-	// Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
-	//   TRS index 10: "^f02Game Main Menu"           -> Title (uses TITLFONT)
-	//   TRS index 11: "^f01^c005Start Game"          -> Item 0 (uses SMALFONT, color 5)
-	//   TRS index 12: "^f01^c009Options"             -> Item 1
-	//   TRS index 13: "^f01^c009Calibrate Joystick"  -> Item 2
-	//   TRS index 14: "^f01^c009Continue Intro"      -> Item 3
-	//   TRS index 15: "^f01^c009Show Top Pilots"     -> Item 4
-	//   TRS index 16: "^f01^c009Show Credits"        -> Item 5
-	//   TRS index 17: "^f01^c240Return to Launcher"  -> Item 6 (color 240)
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) {
-		debug(1, "drawMenuOverlay: SmushPlayer not available for TRS strings!");
-		return;
-	}
-
-	// Load TRS strings 10-17 (title + 7 selectable items)
-	const char *menuItems[8];
-	for (int i = 0; i < 8; i++) {
-		menuItems[i] = splayer->getString(10 + i);
-		if (!menuItems[i] || !menuItems[i][0]) {
-			debug(1, "drawMenuOverlay: TRS string %d not found!", 10 + i);
-			menuItems[i] = "";
-		}
-	}
-
-	// FUN_004147b2 line 25: param_3 = (DAT_0047a806 == 0) + 6 = 7 (keyboard mode)
-	drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
-}
-
-// ======================= Pause Overlay =======================
-// Emulates FUN_405A21 pause rendering (lines 242-305)
-// Creates a dimmed overlay effect and displays "PAUSED" text
-void InsaneRebel2::showPauseOverlay() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) {
-		debug("showPauseOverlay: No SmushPlayer active");
-		return;
-	}
-
-	// Get frame buffer and palette from SmushPlayer
-	// _dst points to the virtual screen pixels (the actual rendering destination)
-	// _frameBuffer is only used for store/fetch operations, not general rendering
-	byte *frameBuffer = splayer->_dst;
-	byte *palette = splayer->_pal;
-	int width = splayer->_width;
-	int height = splayer->_height;
-
-	if (!frameBuffer || !palette || width <= 0 || height <= 0) {
-		debug("showPauseOverlay: No frame buffer (%p), palette (%p), or invalid dimensions (%dx%d)",
-		      (void*)frameBuffer, (void*)palette, width, height);
-		return;
-	}
-
-	debug("showPauseOverlay: Applying dimming effect to %dx%d buffer", width, height);
-
-	// Apply dimming effect (emulates FUN_405A21 lines 242-251)
-	// Original algorithm:
-	//   For each pixel, take the green component of its palette entry
-	//   and the green component of the previous pixel's palette entry,
-	//   add them, divide by 8, add 16.
-	// This creates a dark dimmed effect.
-	int bufferSize = width * height;
-	byte prevPixel = 0;
-
-	for (int i = 0; i < bufferSize; i++) {
-		byte curPixel = frameBuffer[i];
-
-		// Get green components from palette (offset +1 in RGB triplets)
-		int greenCur = palette[curPixel * 3 + 1];
-		int greenPrev = palette[prevPixel * 3 + 1];
-
-		// Apply dimming formula: (green1 + green2) >> 3 + 0x10
-		byte dimmedValue = ((greenCur + greenPrev) >> 3) + 0x10;
-
-		frameBuffer[i] = dimmedValue;
-		prevPixel = curPixel;
-	}
-
-	// Draw border decorations (simplified version of FUN_405A21 lines 261-283)
-	// Draw horizontal lines at top and bottom of a centered box
-	int boxLeft = 12;
-	int boxRight = width - 12;
-	int boxTop = 23;   // 0x17
-	int boxBottom = height - 23;  // ~175 for 200 height
-
-	byte borderColor = 0x50;  // Gray border color
-
-	// Top and bottom borders
-	for (int x = boxLeft; x < boxRight; x++) {
-		if (boxTop >= 0 && boxTop < height)
-			frameBuffer[boxTop * width + x] = borderColor;
-		if (boxBottom >= 0 && boxBottom < height)
-			frameBuffer[boxBottom * width + x] = borderColor;
-	}
-
-	// Left and right borders
-	for (int y = boxTop; y < boxBottom; y++) {
-		if (boxLeft >= 0 && boxLeft < width)
-			frameBuffer[y * width + boxLeft] = borderColor;
-		if (boxRight >= 0 && boxRight < width)
-			frameBuffer[y * width + boxRight] = borderColor;
-	}
-
-	// Draw corner decorations (simplified)
-	byte cornerColor = 0x51;  // Slightly brighter for corners
-	for (int i = 0; i < 5; i++) {
-		// Top-left corner
-		if (boxTop + i < height && boxLeft + 5 < width)
-			frameBuffer[(boxTop + i) * width + boxLeft + 5] = cornerColor;
-		if (boxTop + 5 < height && boxLeft + i < width)
-			frameBuffer[(boxTop + 5) * width + boxLeft + i] = cornerColor;
-
-		// Top-right corner
-		if (boxTop + i < height && boxRight - 5 >= 0)
-			frameBuffer[(boxTop + i) * width + boxRight - 5] = cornerColor;
-		if (boxTop + 5 < height && boxRight - i >= 0)
-			frameBuffer[(boxTop + 5) * width + boxRight - i] = cornerColor;
-
-		// Bottom-left corner
-		if (boxBottom - i >= 0 && boxLeft + 5 < width)
-			frameBuffer[(boxBottom - i) * width + boxLeft + 5] = cornerColor;
-		if (boxBottom - 5 >= 0 && boxLeft + i < width)
-			frameBuffer[(boxBottom - 5) * width + boxLeft + i] = cornerColor;
-
-		// Bottom-right corner
-		if (boxBottom - i >= 0 && boxRight - 5 >= 0)
-			frameBuffer[(boxBottom - i) * width + boxRight - 5] = cornerColor;
-		if (boxBottom - 5 >= 0 && boxRight - i >= 0)
-			frameBuffer[(boxBottom - 5) * width + boxRight - i] = cornerColor;
-	}
-
-	// Draw "PAUSED" text centered
-	// Try to load from TRS - the exact index may vary by language version
-	// TRS index 80 (0x50) is likely "PAUSED" or equivalent (from DAT_004573f8)
-	// Note: splayer is already defined at the start of this function
-	const char *pauseText = splayer ? splayer->getString(80) : nullptr;
-	if (!pauseText || !pauseText[0]) {
-		// Fallback only if TRS string not available
-		pauseText = "PAUSED";
-	}
-
-	// Draw text using SmushFont if available
-	if (_menuFont) {
-		Common::Rect clipRect(0, 0, width, height);
-
-		// Calculate centered position
-		// Text should be centered horizontally and vertically in the box
-		int textX = width / 2;  // SmushFont handles centering with kStyleAlignCenter
-		int textY = height / 2 - 4;  // Slightly above center
-
-		// Draw with color 4 and background 0x10 (matching original parameters)
-		// FUN_00434cb0 params: x=10, y=10 or 20, color=4, bg=0x10
-		_menuFont->drawString(pauseText, frameBuffer, clipRect, textX, textY, 0x10, kStyleAlignCenter);
-	} else if (_smush_smalfontNut) {
-		// Fallback: draw using NutRenderer directly
-		NutRenderer *font = _smush_smalfontNut;
-		int numFontChars = font->getNumChars();
-		Common::Rect clipRect(0, 0, width, height);
-
-		// Calculate text width
-		int textWidth = 0;
-		const char *p = pauseText;
-		while (*p) {
-			byte c = (byte)*p++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-			if (c < numFontChars) {
-				textWidth += font->getCharWidth(c);
-			}
-		}
-
-		// Draw centered
-		int textX = (width - textWidth) / 2;
-		int textY = height / 2 - 4;
-
-		p = pauseText;
-		while (*p) {
-			byte c = (byte)*p++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-			if (c < numFontChars && textX >= 0 && textY >= 0) {
-				font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
-				                 kStyleAlignLeft, c, true, true);
-				textX += font->getCharWidth(c);
-			}
-		}
-	}
-
-	// Update the screen to show the pause overlay
-	// SmushPlayer uses copyRectToScreen to transfer the buffer to the display backend
-	_vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, width, height);
-	_vm->_system->updateScreen();
-
-	debug("showPauseOverlay: Overlay displayed");
-}
-
-int InsaneRebel2::runMainMenu() {
-	// Main menu loop - emulates FUN_004147B2
-	// Returns:
-	//   kMenuNewGame (2) = Start new game
-	//   kMenuContinue (4) = Continue game (level select)
-	//   kMenuCredits (1) = Show credits then return to menu
-	//   0 = Quit game
-
-	debug("Rebel2: Entering main menu");
-
-	resetMenu();
-	_gameState = kStateMainMenu;
-
-	// Enable menu input capture via EventObserver
-	_menuInputActive = true;
-	while (!_menuEventQueue.empty()) _menuEventQueue.pop();  // Clear any stale events
-
-	// Get the SmushPlayer from ScummEngine_v7
-	// Note: _player isn't set until SmushPlayer::initAudio() is called during playback
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Main menu loop
-	while (!_vm->shouldQuit()) {
-		// Reset video finish flag before playing menu
-		_vm->_smushVideoShouldFinish = false;
-
-		// Select and play a random menu video
-		Common::String menuVideo = getRandomMenuVideo();
-		debug("Rebel2: Playing menu video: %s", menuVideo.c_str());
-
-		// Set video flags for menu (0x20 = intro/menu flag)
-		// This tells procPostRendering we're in menu mode
-		splayer->setCurVideoFlags(0x20);
-
-		// Play the menu video
-		// Input is processed in procPostRendering during playback
-		// When user confirms selection, _vm->_smushVideoShouldFinish is set
-		splayer->play(menuVideo.c_str(), 12);
-
-		// Check for quit
-		if (_vm->shouldQuit()) {
-			_menuInputActive = false;
-			return 0;
-		}
-
-		// If video ended naturally (not by selection), loop back
-		if (!_vm->_smushVideoShouldFinish) {
-			// Video ended without selection (reached end or ESC during video)
-			// Continue looping menu videos
-			continue;
-		}
-
-		// Clear the flag
-		_vm->_smushVideoShouldFinish = false;
-
-		// A selection was made - process it
-		debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
-
-		// Process the menu result based on current selection
-		// Menu items matching GAME.TRS indices 11-17 (FUN_004147B2):
-		//   case 0 (TRS 11): Start Game -> pilot selection, returns 2
-		//   case 1 (TRS 12): Options -> FUN_00416787 options screen
-		//   case 2 (TRS 13): Calibrate Joystick -> FUN_00425820
-		//   case 3 (TRS 14): Continue Intro -> replay O_OPEN videos
-		//   case 4 (TRS 15): Show Top Pilots -> FUN_00420116(-1)
-		//   case 5 (TRS 16): Show Credits -> play O_CREDIT.SAN, returns 1
-		//   case 6 (TRS 17): Return to Launcher -> quit, returns 0
-		switch (_menuSelection) {
-		case 0:  // Start Game -> go to pilot selection
-			debug("Rebel2: Start Game selected - going to pilot selection");
-			_gameState = kStatePilotSelect;
-			_menuInputActive = false;
-			return kMenuNewGame;  // Return 2 (kMenuNewGame)
-
-		case 1:  // Options -> show options menu
-			debug("Rebel2: Options selected");
-			// TODO: Implement options menu (FUN_004167a6)
-			// Options: Music, Sound, Voices, Auto Control, Indicators,
-			// Arrows, Difficulty (0-5), Music Volume, SFX Volume
-			break;
-
-		case 2:  // Calibrate Joystick
-			debug("Rebel2: Calibrate Joystick selected");
-			// TODO: Implement joystick calibration (FUN_00425820)
-			// Plays O_CALIB.SAN with joystick calibration prompts
-			break;
-
-		case 3:  // Continue Intro -> replay intro videos
-			debug("Rebel2: Continue Intro selected - replaying intro");
-			// Temporarily switch to intro state to disable menu overlay
-			// This emulates FUN_004142BD case 0 behavior
-			_gameState = kStateIntro;
-			_menuInputActive = false;
-			// Play intro sequence again (O_OPEN_A/B)
-			splayer->setCurVideoFlags(0x20);
-			splayer->play("OPEN/O_OPEN_A.SAN", 12);
-			if (!_vm->shouldQuit()) {
-				splayer->play("OPEN/O_OPEN_B.SAN", 12);
-			}
-			// Restore menu state
-			_gameState = kStateMainMenu;
-			_menuInputActive = true;
-			break;
-
-		case 4:  // Show Top Pilots -> high score display
-			debug("Rebel2: Show Top Pilots selected");
-			// TODO: Implement high score display (FUN_00420116(-1))
-			break;
-
-		case 5:  // Show Credits -> play credits video
-			debug("Rebel2: Show Credits selected - playing O_CREDIT.SAN");
-			_gameState = kStateCredits;
-			_menuInputActive = false;
-			splayer->setCurVideoFlags(0x20);
-			splayer->play("OPEN/O_CREDIT.SAN", 12);
-			_gameState = kStateMainMenu;
-			_menuInputActive = true;
-			// Returns 1 in original -> stays at stage 1 (main menu)
-			break;
-
-		case 6:  // Return to Launcher -> quit game
-			debug("Rebel2: Return to Launcher selected");
-			_menuInputActive = false;
-			return 0;  // Return 0 to exit
-
-		default:
-			debug("Rebel2: Unknown menu selection %d", _menuSelection);
-			break;
-		}
-	}
-
-	_menuInputActive = false;
-	return 0;
-}
-
-// ==================== Chapter Selection Screen ====================
-// Emulates FUN_00415CF8 - Chapter selection with preview and password input
-// This is the actual level/chapter selection that players see after pilot select
-
-int InsaneRebel2::runChapterSelect() {
-	// Chapter selection screen loop - emulates FUN_00415CF8
-	// Returns:
-	//   kChapterSelectPlay (5) = Play selected chapter
-	//   kChapterSelectBack (2) = Return to main menu (ESC or BACK)
-	//   kChapterSelectQuit (0) = Quit game
-
-	debug("Rebel2: Entering chapter selection (FUN_00415CF8)");
-
-	// Enable menu input capture
-	_menuInputActive = true;
-	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
-
-	// Initialize chapter selection state
-	// Original (lines 51-54): local_10 = 0xf; while (local_10 > 0 && locked) local_10--;
-	// Finds highest unlocked chapter. With debug unlock all = 15 (FINALE).
-	_chapterSelection = 15;
-	while (_chapterSelection > 0 && !_chapterUnlocked[_chapterSelection]) {
-		_chapterSelection--;
-	}
-	_chapterItemCount = 17;  // 16 chapters + RETURN TO PILOTS
-	_selectedChapter = 0;
-	_passwordInput = "";
-	_menuRepeatDelay = 0;
-	_gameState = kStateChapterSelect;
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Initialize preview offset for initial selection
-	_previewOffsetX = -90;
-	_previewOffsetY = _chapterSelection * -50 + 75;
-
-	// Set iactBits for chapter unlock state (FUN_00415CF8 lines 79-86)
-	// Bits 16..1 correspond to chapters 0..15: set if unlocked, clear if locked.
-	// These control SKIP chunks in O_LEVEL.SAN for locked/unlocked preview variants.
-	for (int i = 0; i < 16; i++) {
-		if (_chapterUnlocked[i])
-			setBit(16 - i);
-		else
-			clearBit(16 - i);
-	}
-
-	// Chapter selection background - FUN_00415CF8 line 57:
-	// FUN_0041f4d0(s_OPEN_O_LEVEL_SAN, 8, 0xffff, 0xffff, 0)
-	// O_LEVEL.SAN contains chapter preview thumbnails at specific FOBJ positions.
-	// The FOBJ offset system scrolls the correct preview into the preview box area.
-	while (!_vm->shouldQuit()) {
-		_vm->_smushVideoShouldFinish = false;
-
-		debug("Rebel2: Playing chapter select background: OPEN/O_LEVEL.SAN");
-
-		// Flags: 0x20 (overlay drawing). No 0x08 (preserve) — we want a black
-		// background. O_LEVEL.SAN has no full-screen background FOBJ; the visible
-		// screen area stays black, and preview thumbnails render at X=230 via offset.
-		splayer->setCurVideoFlags(0x20);
-
-		// Play O_LEVEL.SAN — preview thumbnails are rendered by FOBJ offset
-		splayer->play("OPEN/O_LEVEL.SAN", 12);
-
-		if (_vm->shouldQuit()) {
-			_menuInputActive = false;
-			return kChapterSelectQuit;
-		}
-
-		// If video ended without selection, continue looping
-		if (!_vm->_smushVideoShouldFinish) {
-			continue;
-		}
-
-		_vm->_smushVideoShouldFinish = false;
-
-		debug("Rebel2: Chapter selection made: %d", _chapterSelection);
-
-		// Process chapter selection (lines 134-236 of FUN_00415CF8)
-		if (_chapterSelection == 16) {
-			// BACK selected (index 16 = 17th item)
-			debug("Rebel2: BACK to main menu selected");
-			_menuInputActive = false;
-			return kChapterSelectBack;
-		}
-
-		if (_chapterSelection >= 0 && _chapterSelection < 16) {
-			// Chapter selected - start it regardless of unlock state.
-			// TODO: locked chapters should require password (FUN_00415CF8 lines 239-257)
-			_selectedChapter = _chapterSelection;
-			debug("Rebel2: Chapter %d selected", _selectedChapter + 1);
-			_menuInputActive = false;
-			return kChapterSelectPlay;
-		}
-	}
-
-	_menuInputActive = false;
-	return kChapterSelectQuit;
-}
-
-int InsaneRebel2::processChapterSelectInput() {
-	// Process input for chapter selection screen
-	// Emulates input handling in FUN_00415CF8 (lines 95-133)
-	// Returns: -1 = no action, 0+ = item selected
-
-	int result = -1;
-
-	while (!_menuEventQueue.empty()) {
-		Common::Event event = _menuEventQueue.front();
-		_menuEventQueue.pop();
-
-		switch (event.type) {
-		case Common::EVENT_KEYDOWN:
-			switch (event.kbd.keycode) {
-			case Common::KEYCODE_UP:
-				// Move selection up, wrap to bottom
-				_chapterSelection--;
-				if (_chapterSelection < 0) {
-					_chapterSelection = _chapterItemCount - 1;
-				}
-				// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
-				_previewOffsetY = _chapterSelection * -50 + 75;
-				debug("ChapterSelect: Selection changed to %d (UP) offsetY=%d", _chapterSelection, _previewOffsetY);
-				break;
-
-			case Common::KEYCODE_DOWN:
-				// Move selection down, wrap to top
-				_chapterSelection++;
-				if (_chapterSelection >= _chapterItemCount) {
-					_chapterSelection = 0;
-				}
-				// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
-				_previewOffsetY = _chapterSelection * -50 + 75;
-				debug("ChapterSelect: Selection changed to %d (DOWN) offsetY=%d", _chapterSelection, _previewOffsetY);
-				break;
-
-			case Common::KEYCODE_RETURN:
-			case Common::KEYCODE_KP_ENTER:
-				if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
-					result = _chapterSelection;
-					debug("ChapterSelect: Item %d selected (ENTER)", _chapterSelection);
-				}
-				break;
-
-			case Common::KEYCODE_ESCAPE:
-				// ESC = Back to main menu (same as selecting BACK)
-				result = 16;  // BACK index
-				debug("ChapterSelect: ESC pressed - back to menu");
-				break;
-
-			case Common::KEYCODE_BACKSPACE:
-				// Backspace for password input (line 107-112 of FUN_00415CF8)
-				if (!_passwordInput.empty()) {
-					_passwordInput.deleteLastChar();
-					debug("ChapterSelect: Password backspace, now: %s", _passwordInput.c_str());
-				}
-				break;
-
-			default:
-				// Printable character for password input (lines 114-121 of FUN_00415CF8)
-				if (event.kbd.ascii >= 0x20 && event.kbd.ascii <= 0x7E) {
-					if (_passwordInput.size() < 8) {
-						_passwordInput += (char)event.kbd.ascii;
-						debug("ChapterSelect: Password input: %s", _passwordInput.c_str());
-					}
-				}
-				break;
-			}
-			break;
-
-		case Common::EVENT_LBUTTONDOWN:
-			// Click confirms the current selection (original: DAT_0047a7e4 & 1)
-			if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
-				result = _chapterSelection;
-				debug("ChapterSelect: Item %d confirmed (CLICK)", _chapterSelection);
-			}
-			break;
-
-		case Common::EVENT_MOUSEMOVE:
-			{
-				// Mouse hover changes highlight (original FUN_0041f5ae mouse mode).
-				// Item Y = numItems * -5 + i * 10 + 0x68
-				int baseY = _chapterItemCount * -5 + 0x68;
-				int mouseY = event.mouse.y;
-
-				for (int i = 0; i < _chapterItemCount; i++) {
-					int itemY = baseY + i * 10;
-					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
-						if (i != _chapterSelection) {
-							_chapterSelection = i;
-							_previewOffsetY = _chapterSelection * -50 + 75;
-							debug(5, "ChapterSelect: Hover changed to %d", _chapterSelection);
-						}
-						break;
-					}
-				}
-			}
-			break;
-
-		default:
-			break;
-		}
-	}
-
-	return result;
-}
-
-// Draw preview box border - emulates FUN_004292D0 calls at lines 128-133 of FUN_00415CF8
-void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int height) {
-	// Low-res (320x200) coordinates from FUN_00415CF8:
-	// Outer box: X=0xe4 (228), Y=0x49 (73), W=0x54 (84), H=0x36 (54), color=0xF8
-	// Inner box: X=0xe5 (229), Y=0x4a (74), W=0x52 (82), H=0x34 (52), color=4
-
-	// Outer border (bright)
-	int outerX = 228, outerY = 73, outerW = 84, outerH = 54;
-	byte outerColor = 0xF8;
-
-	// Draw outer box edges
-	// Top edge
-	for (int px = outerX; px < outerX + outerW && px < width; px++) {
-		if (outerY >= 0 && outerY < height && px >= 0)
-			renderBitmap[outerY * pitch + px] = outerColor;
-	}
-	// Bottom edge
-	int bottomY = outerY + outerH - 1;
-	if (bottomY < height) {
-		for (int px = outerX; px < outerX + outerW && px < width; px++) {
-			if (px >= 0)
-				renderBitmap[bottomY * pitch + px] = outerColor;
-		}
-	}
-	// Left edge
-	for (int py = outerY; py < outerY + outerH && py < height; py++) {
-		if (py >= 0 && outerX >= 0 && outerX < width)
-			renderBitmap[py * pitch + outerX] = outerColor;
-	}
-	// Right edge
-	int rightX = outerX + outerW - 1;
-	if (rightX < width) {
-		for (int py = outerY; py < outerY + outerH && py < height; py++) {
-			if (py >= 0)
-				renderBitmap[py * pitch + rightX] = outerColor;
-		}
-	}
-
-	// Inner border (dark)
-	int innerX = 229, innerY = 74, innerW = 82, innerH = 52;
-	byte innerColor = 4;
-
-	// Top edge
-	for (int px = innerX; px < innerX + innerW && px < width; px++) {
-		if (innerY >= 0 && innerY < height && px >= 0)
-			renderBitmap[innerY * pitch + px] = innerColor;
-	}
-	// Bottom edge
-	bottomY = innerY + innerH - 1;
-	if (bottomY < height) {
-		for (int px = innerX; px < innerX + innerW && px < width; px++) {
-			if (px >= 0)
-				renderBitmap[bottomY * pitch + px] = innerColor;
-		}
-	}
-	// Left edge
-	for (int py = innerY; py < innerY + innerH && py < height; py++) {
-		if (py >= 0 && innerX >= 0 && innerX < width)
-			renderBitmap[py * pitch + innerX] = innerColor;
-	}
-	// Right edge
-	rightX = innerX + innerW - 1;
-	if (rightX < width) {
-		for (int py = innerY; py < innerY + innerH && py < height; py++) {
-			if (py >= 0)
-				renderBitmap[py * pitch + rightX] = innerColor;
-		}
-	}
-}
-
-// Draw preview thumbnail content - emulates FUN_00428a10 + FUN_00429b40
-// Based on FUN_00415CF8 assembly analysis:
-//
-// The original uses O_LEVEL.SAN (640x400) with chapter previews stacked vertically.
-// Video offset (FUN_00425170) shifts which preview is visible:
-//   X offset = -90 (0xffa6)
-//   Y offset = chapter * -50 + 75
-//
-// For 320x200 mode, O_MENU_X.SAN doesn't contain chapter-specific preview images.
-// Those are only in O_LEVEL.SAN (640x400). We display a styled placeholder instead
-// with the chapter number and visual styling to match the original UI appearance.
-void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
-	// Preview destination area coordinates (inside the inner border)
-	// From assembly: Inner box at X=230, Y=75, W=80, H=50
-	const int destX = 230;
-	const int destY = 75;
-	const int thumbW = 80;  // 0x50
-	const int thumbH = 50;  // 0x32
-
-	// Fill preview area with a dark blue gradient background
-	// This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
-	for (int py = 0; py < thumbH; py++) {
-		int dy = destY + py;
-		if (dy < 0 || dy >= height) continue;
-
-		// Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
-		byte bgColor = 0x10 + (py * 8 / thumbH);
-
-		for (int px = 0; px < thumbW; px++) {
-			int dx = destX + px;
-			if (dx < 0 || dx >= width) continue;
-			renderBitmap[dy * pitch + dx] = bgColor;
-		}
-	}
-
-	// Draw chapter number overlay in the center of the preview
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font) return;
-
-	char chapterStr[16];
-	if (chapter < 15) {
-		snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
-	} else {
-		snprintf(chapterStr, sizeof(chapterStr), "FINALE");
-	}
-
-	// Calculate text width for centering
-	int textWidth = 0;
-	int numChars = font->getNumChars();
-	for (const char *c = chapterStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numChars) {
-			textWidth += font->getCharWidth(charIdx);
-		}
-	}
-
-	// Center the text in the preview area
-	int textX = destX + (thumbW - textWidth) / 2;
-	int textY = destY + thumbH / 2 - 4;
-
-	Common::Rect clipRect(0, 0, width, height);
-
-	// Draw text shadow (offset by 1,1)
-	int curX = textX + 1;
-	for (const char *c = chapterStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && textY + 1 >= 0 && textY + 1 < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, textY + 1, pitch, 0,
-				                 kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-
-	// Draw main text (bright)
-	curX = textX;
-	for (const char *c = chapterStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && textY >= 0 && textY < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, textY, pitch, -1,
-				                 kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-
-	// Draw lock icon for locked chapters
-	if (!_chapterUnlocked[chapter]) {
-		byte lockColor = 0xF8;
-		int lockX = destX + thumbW - 15;
-		int lockY = destY + 5;
-
-		// Draw padlock shape
-		for (int i = 2; i < 6; i++) {
-			if (lockX + i < width && lockY < height && lockY >= 0)
-				renderBitmap[lockY * pitch + lockX + i] = lockColor;
-		}
-		for (int i = 1; i < 4; i++) {
-			if (lockX + 2 < width && lockY + i < height && lockY + i >= 0)
-				renderBitmap[(lockY + i) * pitch + lockX + 2] = lockColor;
-			if (lockX + 5 < width && lockY + i < height && lockY + i >= 0)
-				renderBitmap[(lockY + i) * pitch + lockX + 5] = lockColor;
-		}
-		for (int y = 0; y < 4; y++) {
-			for (int x = 1; x < 7; x++) {
-				if (lockX + x < width && lockY + 4 + y < height && lockY + 4 + y >= 0)
-					renderBitmap[(lockY + 4 + y) * pitch + lockX + x] = lockColor;
-			}
-		}
-	}
-}
-
-// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
-// For unlocked chapters: score display using TRS 80 at (25, 190)
-// For locked chapters: password prompt at (30, 190)
-void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
-	if (_chapterSelection < 0 || _chapterSelection >= 16) return;
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) return;
-
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font) return;
-
-	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
-	int actualPitch = _vm->_screenWidth;
-
-	if (_chapterUnlocked[_chapterSelection]) {
-		// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
-		const char *scoreStr = splayer->getString(80);
-		if (!scoreStr || !scoreStr[0]) return;
-
-		int curX = 25;
-		int numChars = font->getNumChars();
-		for (const char *c = scoreStr; *c; c++) {
-			byte ch = (byte)*c;
-			if (ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
-			if (ch < numChars) {
-				int charW = font->getCharWidth(ch);
-				if (curX >= 0 && curX + charW <= width && 190 < height) {
-					font->drawCharV7(renderBitmap, clipRect, curX, 190, actualPitch, 1,
-					                 kStyleAlignLeft, ch, false, false);
-				}
-				curX += charW;
-			}
-		}
-	}
-}
-
-// Draw chapter selection overlay - called during O_LEVEL.SAN playback
-// FUN_00415CF8 - Chapter selection screen with preview thumbnail
-void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// Emulates FUN_00415CF8 rendering via shared drawMenuItems(leftAligned=true)
-	//
-	// GAME.TRS chapter selection strings:
-	//   TRS 40     = "^f02Chapters" (title)
-	//   TRS 41-56  = unlocked chapter names (e.g. "^f01^c244Chapter 1 - The Dreighton Triangle")
-	//   TRS 57     = "^f01^c240RETURN TO PILOTS"
-	//   TRS 60     = "^f02Chapters" (title, locked section duplicate)
-	//   TRS 61-76  = locked chapter names (e.g. "^f01^c244Chapter 1 -")
-	//   TRS 77     = "^f01^c240RETURN TO PILOTS" (locked section duplicate)
-	//
-	// Menu array: items[0]=title, items[1..16]=chapters, items[17]=RETURN TO PILOTS
-	// FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) return;
-
-	// Build items array matching original DAT_004577a8 layout
-	const char *items[18];
-
-	// items[0] = title = TRS 40 ("^f02Chapters")
-	items[0] = splayer->getString(40);
-	if (!items[0] || !items[0][0]) items[0] = "^f02Chapters";
-
-	// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
-	for (int i = 1; i <= 16; i++) {
-		bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
-		int trsIdx = unlocked ? (40 + i) : (60 + i);
-		items[i] = splayer->getString(trsIdx);
-		if (!items[i] || !items[i][0]) items[i] = "";
-	}
-
-	// items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
-	items[17] = splayer->getString(57);
-	if (!items[17] || !items[17][0]) items[17] = "^f01^c240RETURN TO PILOTS";
-
-	// Render menu using shared renderer with left-aligned mode
-	drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
-
-	// Draw preview box border on the right side (FUN_004292d0 calls at lines 128-133)
-	// The actual preview image is rendered by O_LEVEL.SAN FOBJ via the offset system
-	drawPreviewBox(renderBitmap, pitch, width, height);
-
-	// Draw score/info line at bottom
-	drawChapterInfoLine(renderBitmap, pitch, width, height);
-}
-
-// ==================== Pilot Selection Menu (FUN_00414A41) ====================
-// Emulates FUN_00414A41 - Pilot/save selection menu
-// This appears before chapter selection. All options go to chapter selection except MAIN MENU.
-
-int InsaneRebel2::runLevelSelect() {
-	// Pilot selection menu loop - emulates FUN_00414A41
-	// Returns:
-	//   kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
-	//   kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
-	//   kLevelSelectQuit (2) = Quit game
-	//
-	// Original action dispatch (FUN_00414A41):
-	//   sel < N        → saved pilot selected → return 3 (start game)
-	//   sel == N       → ADD NEW PILOT → difficulty submenu → loop back
-	//   sel == N+1     → COPY PILOT → source select (no-op if N==0)
-	//   sel == N+2     → DELETE PILOT → confirm select (no-op if N==0)
-	//   sel == N+3     → RETURN TO MAIN MENU → return 1
-	//   ESC            → return 1
-
-	debug("Rebel2: Entering pilot selection (FUN_00414A41)");
-
-	_menuInputActive = true;
-	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
-
-	// Number of saved pilots (TODO: implement save system)
-	int numPilots = 0;
-
-	_levelSelection = 0;
-	_levelItemCount = numPilots + 4;  // N pilots + NEW/COPY/DELETE/MAIN MENU
-	_selectedLevel = 1;
-	_menuRepeatDelay = 0;
-	_gameState = kStatePilotSelect;
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Pilot selection uses menu video as background (320x200 mode)
-	while (!_vm->shouldQuit()) {
-		_vm->_smushVideoShouldFinish = false;
-
-		Common::String menuVideo = getRandomMenuVideo();
-		debug("Rebel2: Playing pilot select background: %s", menuVideo.c_str());
-
-		splayer->setCurVideoFlags(0x20);
-		splayer->play(menuVideo.c_str(), 12);
-
-		if (_vm->shouldQuit()) {
-			_menuInputActive = false;
-			return kLevelSelectQuit;
-		}
-
-		if (!_vm->_smushVideoShouldFinish) {
-			continue;
-		}
-
-		_vm->_smushVideoShouldFinish = false;
-
-		// Dispatch based on current game state
-		if (_gameState == kStateDifficultySelect) {
-			// Difficulty submenu — selection made
-			debug("Rebel2: Difficulty %d selected", _difficultySelection);
-			// Original stores difficulty in pilot record and loops back.
-			// Since we have no save system, proceed to chapter select.
-			_gameState = kStatePilotSelect;
-			_menuInputActive = false;
-			return kLevelSelectPlay;
-		}
-
-		// Pilot menu — process selection
-		debug("Rebel2: Pilot selection made: %d (numPilots=%d)", _levelSelection, numPilots);
-
-		if (_levelSelection < numPilots) {
-			// Saved pilot selected — go to chapter selection
-			_selectedLevel = _levelSelection + 1;
-			debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
-			_menuInputActive = false;
-			return kLevelSelectPlay;
-		} else if (_levelSelection == numPilots) {
-			// ADD NEW PILOT — show difficulty submenu
-			debug("Rebel2: ADD NEW PILOT - showing difficulty submenu");
-			_gameState = kStateDifficultySelect;
-			_difficultySelection = 2;  // Default to 3rd option (matching original init)
-			// Continue loop — next video frame will render difficulty menu
-			continue;
-		} else if (_levelSelection == numPilots + 1) {
-			// COPY PILOT — no-op when numPilots==0 (original checks local_10 != 0)
-			if (numPilots > 0) {
-				debug("Rebel2: COPY PILOT selected");
-				// TODO: implement copy pilot sub-flow
-			} else {
-				debug("Rebel2: COPY PILOT - no pilots to copy");
-			}
-			continue;
-		} else if (_levelSelection == numPilots + 2) {
-			// DELETE PILOT — no-op when numPilots==0 (original checks local_10 != 0)
-			if (numPilots > 0) {
-				debug("Rebel2: DELETE PILOT selected");
-				// TODO: implement delete pilot sub-flow
-			} else {
-				debug("Rebel2: DELETE PILOT - no pilots to delete");
-			}
-			continue;
-		} else if (_levelSelection == numPilots + 3) {
-			// RETURN TO MAIN MENU
-			debug("Rebel2: Back to main menu selected");
-			_menuInputActive = false;
-			return kLevelSelectBack;
-		}
-	}
-
-	_menuInputActive = false;
-	return kLevelSelectQuit;
-}
-
-int InsaneRebel2::processLevelSelectInput() {
-	// Process input for pilot selection and difficulty submenu
-	// Handles both kStatePilotSelect and kStateDifficultySelect modes
-	// Returns: -1 = no action, 0+ = item selected
-
-	int result = -1;
-
-	// Determine which menu mode we're in
-	bool isDifficultyMode = (_gameState == kStateDifficultySelect);
-	int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
-	int itemCount = isDifficultyMode ? 6 : _levelItemCount;
-
-	// Mouse hit Y positions — must match drawMenuItems() formula
-	const int baseY = itemCount * -5 + 0x68;
-	const int itemHeight = 10;
-
-	while (!_menuEventQueue.empty()) {
-		Common::Event event = _menuEventQueue.pop();
-		switch (event.type) {
-		case Common::EVENT_KEYDOWN:
-			switch (event.kbd.keycode) {
-			case Common::KEYCODE_UP:
-				selection--;
-				if (selection < 0) {
-					selection = itemCount - 1;
-				}
-				debug("LevelSelect: Selection changed to %d (UP, %s)",
-				      selection, isDifficultyMode ? "difficulty" : "pilot");
-				break;
-
-			case Common::KEYCODE_DOWN:
-				selection++;
-				if (selection >= itemCount) {
-					selection = 0;
-				}
-				debug("LevelSelect: Selection changed to %d (DOWN, %s)",
-				      selection, isDifficultyMode ? "difficulty" : "pilot");
-				break;
-
-			case Common::KEYCODE_RETURN:
-			case Common::KEYCODE_KP_ENTER:
-				if (selection >= 0 && selection < itemCount) {
-					result = selection;
-					debug("LevelSelect: Item %d selected (ENTER, %s)",
-					      selection, isDifficultyMode ? "difficulty" : "pilot");
-				}
-				break;
-
-			case Common::KEYCODE_ESCAPE:
-				if (isDifficultyMode) {
-					// ESC in difficulty submenu — return to pilot menu
-					// Note: original has no ESC in difficulty submenu, but we add it as a
-					// graceful fallback
-					_gameState = kStatePilotSelect;
-					debug("LevelSelect: ESC in difficulty - back to pilot menu");
-				} else {
-					// ESC in pilot menu — back to main menu (return 1 in original)
-					result = _levelItemCount - 1;  // Last item = MAIN MENU
-					debug("LevelSelect: ESC pressed - back to main menu");
-				}
-				break;
-
-			default:
-				break;
-			}
-			break;
-
-		case Common::EVENT_LBUTTONDOWN:
-			{
-				int mouseY = event.mouse.y;
-				debug("LevelSelect: Click at Y=%d (%s)", mouseY,
-				      isDifficultyMode ? "difficulty" : "pilot");
-
-				for (int i = 0; i < itemCount; i++) {
-					int itemY = baseY + i * itemHeight;
-					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
-						selection = i;
-						result = i;
-						debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
-						break;
-					}
-				}
-			}
-			break;
-
-		case Common::EVENT_MOUSEMOVE:
-			_vm->_mouse.x = event.mouse.x;
-			_vm->_mouse.y = event.mouse.y;
-			break;
-
-		case Common::EVENT_QUIT:
-		case Common::EVENT_RETURN_TO_LAUNCHER:
-			if (isDifficultyMode) {
-				_gameState = kStatePilotSelect;
-			} else {
-				result = _levelItemCount - 1;
-			}
-			break;
-
-		default:
-			break;
-		}
-	}
-
-	return result;
-}
-
-void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// =====================================================================
-	// Pilot selection / difficulty submenu renderer
-	// Emulates FUN_00414A41 → FUN_0041f5ae
-	// =====================================================================
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) {
-		debug(1, "drawLevelSelectOverlay: SmushPlayer not available for TRS strings!");
-		return;
-	}
-
-	if (_gameState == kStateDifficultySelect) {
-		// =====================================================================
-		// Difficulty submenu - LAB_00414ff6
-		// FUN_0041f5ae(0, &DAT_00457458, 6, 0)
-		// DAT_00457458 = 7 entries loaded from TRS 110-116 (FUN_00414073 lines 47-50)
-		// param_3 = 6 → items[0]=title(TRS 110), items[1..6]=selectable(TRS 111-116)
-		// =====================================================================
-		const char *diffItems[7];
-		for (int i = 0; i < 7; i++) {
-			diffItems[i] = splayer->getString(110 + i);
-			if (!diffItems[i] || !diffItems[i][0]) {
-				diffItems[i] = "";
-			}
-		}
-		drawMenuItems(renderBitmap, pitch, width, height, diffItems, 6, _difficultySelection);
-		return;
-	}
-
-	// =====================================================================
-	// Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
-	// =====================================================================
-	// items[0]    = title (TRS 20)
-	// items[1..N] = saved pilots (formatted with ^f01^c005)
-	// items[N+1]  = TRS 21 (ADD NEW PILOT)
-	// items[N+2]  = TRS 22 (COPY PILOT)
-	// items[N+3]  = TRS 23 (DELETE PILOT)
-	// items[N+4]  = TRS 24 (RETURN TO MAIN MENU)
-
-	int numPilots = 0;  // TODO: implement save system
-
-	const char *pilotItems[11];
-	int idx = 0;
-
-	// Title: TRS 20
-	pilotItems[idx++] = splayer->getString(20);
-
-	// Saved pilot slots would be inserted here (items[1..numPilots])
-	// Each formatted as "^f01^c005<name>^f00" by the original
-
-	// Fixed options: TRS 21-24
-	for (int i = 0; i < 4; i++) {
-		pilotItems[idx++] = splayer->getString(21 + i);
-	}
-
-	for (int i = 0; i < idx; i++) {
-		if (!pilotItems[i] || !pilotItems[i][0]) {
-			pilotItems[i] = "";
-		}
-	}
-
-	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numPilots + 4, _levelSelection);
-
-	// Pilot info display at fixed coordinates when saved pilot selected
-	// FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
-	// TODO: Implement when save system is added
-}
-
-// ======================= Level Loading System =======================
-// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8
-// Based on disassembly analysis of the retail Rebel Assault 2 executable.
-
-Common::String InsaneRebel2::getLevelDir(int levelId) {
-	// Returns directory name like "LEV01" for level 1
-	return Common::String::format("LEV%02d", levelId);
-}
-
-Common::String InsaneRebel2::getLevelPrefix(int levelId) {
-	// Returns file prefix like "01" for level 1
-	return Common::String::format("%02d", levelId);
-}
-
-void InsaneRebel2::playIntroSequence() {
-	// Emulates case 0 in FUN_004142BD
-	// Plays the game intro sequence:
-	// 1. CREDITS/O_OPEN_C.SAN - Fox logo (if certain conditions)
-	// 2. CREDITS/O_OPEN_D.SAN - LucasArts logo (if certain conditions)
-	// 3. OPEN/O_OPEN_A.SAN - Main intro
-	// 4. OPEN/O_OPEN_B.SAN - Additional intro (if conditions)
-
-	debug("Rebel2: Playing intro sequence");
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Set intro flags (non-interactive)
-	splayer->setCurVideoFlags(0x20);
-
-	// Play Fox logo (CREDITS/O_OPEN_C.SAN)
-	// In retail, this checks if 'f', 'o', 'x' keys are held (easter egg)
-	// We'll play it unconditionally for now
-	debug("Rebel2: Playing Fox logo");
-	splayer->play("CREDITS/O_OPEN_C.SAN", 12);
-
-	if (_vm->shouldQuit()) return;
-
-	// Play LucasArts logo (CREDITS/O_OPEN_D.SAN)
-	// In retail, this checks if 'b', 'o', 't' keys are held
-	debug("Rebel2: Playing LucasArts logo");
-	splayer->play("CREDITS/O_OPEN_D.SAN", 12);
-
-	if (_vm->shouldQuit()) return;
-
-	// Play main intro (OPEN/O_OPEN_A.SAN)
-	debug("Rebel2: Playing main intro");
-	splayer->play("OPEN/O_OPEN_A.SAN", 12);
-
-	if (_vm->shouldQuit()) return;
-
-	// Play additional intro (OPEN/O_OPEN_B.SAN)
-	// In retail, this plays if DAT_0047ab45 or DAT_0047ab47 != 0
-	debug("Rebel2: Playing additional intro");
-	splayer->play("OPEN/O_OPEN_B.SAN", 12);
-}
-
-void InsaneRebel2::playMissionBriefing() {
-	// Emulates FUN_00415CF8 (partial - just the video)
-	// Plays OPEN/O_LEVEL.SAN which shows the mission briefing screen
-
-	debug("Rebel2: Playing mission briefing");
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x08);  // Briefing mode flag
-	splayer->play("OPEN/O_LEVEL.SAN", 12);
-}
-
-void InsaneRebel2::playCinematic(const char *filename) {
-	// Play a cinematic/cutscene video with proper intro mode setup
-	// This helper ensures:
-	// 1. Handler is reset to 0 (no HUD, no shooting)
-	// 2. Video flags are set to 0x28 (cinematic with buffer preserve)
-	//
-	// Original: All video wrapper functions (FUN_00417168, FUN_004171c5,
-	// FUN_00417ab2, FUN_00417327) add | 8 to the base flags before calling
-	// FUN_0041f4d0, so the 0x08 bit (preserve buffer) is always set.
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // No status bar during cinematics
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve (0x20 | 0x08)
-	splayer->play(filename, 12);
-}
-
-void InsaneRebel2::playLevelBegin(int levelId) {
-	// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
-	// Emulates FUN_004171c5 call in each level handler
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
-
-	debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
-	playCinematic(filename.c_str());
-}
-
-bool InsaneRebel2::playLevelGameplay(int levelId) {
-	// Play the main gameplay video(s) for a level
-	// Returns true if level completed (damage < 0xff), false if died
-	//
-	// Different levels have different gameplay structures:
-	// - Level 1, 4, 5: Single gameplay SAN (XXPXX.SAN or XXPLAY.SAN)
-	// - Level 2: Multiple parts with subdirectories (P1/, P2/, P3/)
-	// - Level 3, 6: Two gameplay phases (XXPLAY1.SAN, XXPLAY2.SAN)
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename;
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Set gameplay flags (interactive with HUD)
-	splayer->setCurVideoFlags(0x28);
-
-	// Reset damage/shield for this level
-	_playerShield = 255;
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // Will be set by IACT opcode 6 if par4==1
-
-	debug("Rebel2: Starting gameplay for level %d", levelId);
-
-	switch (levelId) {
-	case 1:
-		// Level 1: Single gameplay file (01P01.SAN)
-		// Level 1 uses Handler 0x26 (turret mode) - set before gameplay
-		_rebelHandler = 0x26;
-		filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 2:
-		// Level 2: Has cutscene first, then multiple parts
-		// Level 2 uses Handler 8 (third-person on foot mode) - set before gameplay
-		_rebelHandler = 8;
-		// First play the cutscene
-		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing cutscene %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Part 1 (multiple variations - play A for now)
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/P1/%sP01_A.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Post segment 1
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Part 2
-		_rebelHandler = 8;
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Post segment 2
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Part 3
-		_rebelHandler = 8;
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 3:
-		// Level 3: Two gameplay phases (third-person ship)
-		// Level 3 uses Handler 7 (third-person ship mode) - FUN_0040d836/FUN_0040c3cc
-		_rebelHandler = 7;
-		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Post segment
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Phase 2 — handler will be re-set by IACT opcode 6
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 4:
-		_rebelHandler = 0x26;
-		// Level 4: Has cutscene, then single gameplay
-		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing cutscene %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit()) return false;
-
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 5:
-		// Level 5: Single gameplay file
-		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 6:
-		// Level 6: Two gameplay phases
-		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Post segment
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
-
-		// Phase 2 — handler will be re-set by IACT opcode 6
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	default:
-		// For levels 7-15 (not in demo), try common patterns
-		// First try XXPLAY.SAN
-		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Trying %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-	}
-
-	// Return true if player survived (shield > 0), false if died
-	return (_playerShield > 0);
-}
-
-void InsaneRebel2::playLevelEnd(int levelId) {
-	// Play level completion video (LEVXX/XXEND.SAN)
-	// Emulates FUN_00417327 call
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // No status bar during end cinematic
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename = Common::String::format("%s/%sEND.SAN", dir.c_str(), prefix.c_str());
-
-	debug("Rebel2: Playing level %d end: %s", levelId, filename.c_str());
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417327 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelDeath(int levelId) {
-	// Play death video (LEVXX/XXDIE_X.SAN)
-	// The variant depends on the frame where player died
-	// For simplicity, we'll play the A variant
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-
-	// Most levels have DIE_A, some have just DIE
-	Common::String filename;
-	if (levelId == 2 || levelId == 4 || levelId == 10 || levelId == 12 || levelId == 14) {
-		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
-	} else {
-		filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
-	}
-
-	debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelRetry(int levelId) {
-	// Play retry prompt video (LEVXX/XXRETRY.SAN)
-	// Reset handler state for the retry cinematic
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
-
-	debug("Rebel2: Playing level %d retry: %s", levelId, filename.c_str());
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelGameOver(int levelId) {
-	// Play game over video (LEVXX/XXOVER.SAN)
-	// Emulates FUN_00417ab2 call
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // No status bar during game over cinematic
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename = Common::String::format("%s/%sOVER.SAN", dir.c_str(), prefix.c_str());
-
-	debug("Rebel2: Playing level %d game over: %s", levelId, filename.c_str());
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417ab2 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playCreditsSequence() {
-	// Play the end credits (OPEN/O_CREDIT.SAN)
-	// Individual credits are in CREDITS/CRED_XX.SAN
-
-	debug("Rebel2: Playing credits");
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	splayer->setCurVideoFlags(0x20);
-	splayer->play("OPEN/O_CREDIT.SAN", 12);
-}
-
-int InsaneRebel2::runLevel(int levelId) {
-	// Main level dispatcher - calls per-level handlers
-	// Each level handler emulates its retail counterpart (FUN_00417E53 etc.)
-
-	debug("Rebel2: Starting level %d", levelId);
-
-	// Validate level ID
-	if (levelId < 1 || levelId > 15) {
-		warning("Rebel2: Invalid level ID %d", levelId);
-		return kLevelReturnToMenu;
-	}
-
-	// Switch to gameplay state to stop menu overlay rendering
-	_gameState = kStateGameplay;
-	_menuInputActive = false;
-
-	// Clear the screen to remove any leftover menu pixels
-	VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
-	memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);
-	_vm->markRectAsDirty(kMainVirtScreen, 0, vs->w, 0, vs->h);
-
-	// Set the current level
-	_selectedLevel = levelId;
-
-	// Initialize common player state
-	_playerLives = 3;
-	_playerShield = 255;
-	_playerScore = 0;
-	_playerDamage = 0;
-	resetDamageFlash();
-	_damageHighFlashCounter = 0;
-	_damageShakeCounter = 0;
-	_currentPhase = 1;
-	_phaseScore = 0;
-	_phaseMisses = 0;
-
-	// Dispatch to per-level handler
-	switch (levelId) {
-	case 1:
-		return runLevel1();
-	case 2:
-		return runLevel2();
-	case 3:
-		return runLevel3();
-	case 4:
-		return runLevel4();
-	case 5:
-		return runLevel5();
-	case 6:
-		return runLevel6();
-	case 7:
-		return runLevel7();
-	case 8:
-		return runLevel8();
-	case 9:
-		return runLevel9();
-	case 10:
-		return runLevel10();
-	case 11:
-		return runLevel11();
-	case 12:
-		return runLevel12();
-	case 13:
-		return runLevel13();
-	case 14:
-		return runLevel14();
-	case 15:
-		return runLevel15();
-	default:
-		return runLevel1();
-	}
-}
-
-// =============================================================================
-// Helper functions
-// =============================================================================
-
-int InsaneRebel2::getRandomVariant(int max) {
-	// Emulates FUN_004233a0 - returns random number 0 to max-1
-	return _vm->_rnd.getRandomNumber(max - 1);
-}
-
-Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int frame) {
-	// Select death video variant based on level, phase, and death frame
-	// Emulates the frame-based death video selection in retail level handlers
-	//
-	// Returns variant suffix: "A", "B", "C", etc.
-
-	switch (levelId) {
-	case 1:
-		// Level 1: Random between A and B
-		return (getRandomVariant(2) == 0) ? "A" : "B";
-
-	case 2:
-		// Level 2: Just "DIE" (no variants)
-		return "";
-
-	case 3:
-		// Level 3: Based on death frame and phase
-		if (phase == 1) {
-			// Phase 1 death video selection (from FUN_0041885F lines 80-96)
-			if (frame < 0x10c) return "B";       // < 268
-			if (frame < 0x1a9) return "A";       // < 425
-			if (frame < 0x247) return "C";       // < 583
-			if (frame < 700) return "A";
-			if (frame < 900) return "B";
-			return "A";
-		} else {
-			// Phase 2 death video selection (from FUN_0041885F lines 53-67)
-			if (frame < 0x2f1) return "A";       // < 753
-			if (frame < 0x347) return "B";       // < 839
-			if (frame < 0x3b1) return "C";       // < 945
-			if (frame < 0x405) return "A";       // < 1029
-			return "C";
-		}
-
-	case 4:
-		// Level 4: Just "DIE" (no variants)
-		return "";
-
-	case 5:
-		// Level 5: Random between A and B (like Level 1)
-		return (getRandomVariant(2) == 0) ? "A" : "B";
-
-	case 6:
-		// Level 6 (FUN_004190D6): Phase-based with detailed frame selection
-		if (phase == 1) {
-			// DAT_0047a7f8 == 5 (phase 1)
-			if (frame < 0x4e) return "D";
-			if (frame < 0xe0) return "A";
-			if (frame < 0x122) return "D";
-			if (frame < 0x1b4) return "B";
-			if (frame < 499) return "D";
-			if (frame < 0x286) return "C";
-			return "D";
-		} else {
-			// DAT_0047a7f8 == 6 (phase 2)
-			if (frame < 0xcc) return "E";
-			if (frame < 0xfe) return "G";
-			if (frame < 0x122) return "E";
-			if (frame < 0x149) return "G";
-			if (frame < 0x166) return "F";
-			if (frame < 0x174) return "E";
-			if (frame < 0x19f) return "F";
-			if (frame < 0x1b2) return "G";
-			if (frame < 0x1c8) return "F";
-			if (frame < 0x207) return "E";
-			if (frame < 0x217) return "F";
-			if (frame < 0x23b) return "G";
-			if (frame < 0x25b) return "F";
-			if (frame < 0x285) return "E";
-			return "G";
-		}
-
-	case 7:
-		// Level 7 (FUN_0041974C): Based on DAT_0047ab8c (fork state)
-		// DAT_0047ab8c != 0 → DIE_B; == 0 → DIE_A
-		// We use phase as a proxy (phase 2 = reached fork)
-		return (phase >= 2) ? "B" : "A";
-
-	case 8:
-		// Level 8 (FUN_00419976): Random A or B
-		return (getRandomVariant(2) == 0) ? "A" : "B";
-
-	case 9:
-		// Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
-		// 0→A, 1→C, else→B. Use phase as proxy.
-		return "A";  // Default; exact tracking of DAT_0047ab94 deferred
-
-	case 10:
-		// Level 10 (FUN_00419E0A): Single death video (no variant suffix)
-		return "";
-
-	case 11:
-		// Level 11 (FUN_0041A00C): Phase-based death videos
-		// Phase 1 → DIE_A, Phase 2 → DIE_B, Phase 3 → DIE_C
-		if (phase <= 1) return "A";
-		if (phase == 2) return "B";
-		return "C";
-
-	case 12:
-		// Level 12 (FUN_0041AA14): Single death video (no variants)
-		return "";
-
-	case 13:
-		// Level 13 (FUN_0041B3E1): Frame-based
-		if (frame < 0x1c2) return "A";
-		if (frame < 0x302) return "B";
-		if (frame < 0x4ec) return "C";
-		if (frame < 0x5b4) return "B";
-		return "D";
-
-	case 14:
-		// Level 14 (FUN_0041B6E8): Single death video (no variant suffix)
-		return "";
-
-	case 15:
-		// Level 15 (FUN_0041B8D7): Frame-based with many thresholds
-		if (frame < 0x21e) return "A";
-		if (frame < 0x2f9) return "B";
-		if (frame < 0x3e5) return "C";
-		if (frame < 0x4a0) return "B";
-		if (frame < 0x588) return "C";
-		if (frame < 0x65e) return "B";
-		return "D";
-
-	default:
-		return "A";
-	}
-}
-
-void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
-	// Play death video with proper variant selection
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String variant = selectDeathVideoVariant(levelId, phase, frame);
-	Common::String filename;
-
-	if (variant.empty()) {
-		// No variant suffix (Level 2, 4)
-		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
-	} else {
-		filename = Common::String::format("%s/%sDIE_%s.SAN", dir.c_str(), prefix.c_str(), variant.c_str());
-	}
-
-	debug("Rebel2: Playing death video: %s (phase=%d, frame=%d)", filename.c_str(), phase, frame);
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
-void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
-	// Play retry video - phase-specific for multi-phase levels
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename;
-
-	if ((levelId == 3 || levelId == 6) && phase == 2) {
-		// Level 3/6 phase 2 has its own retry video: xxRETRYB.SAN
-		filename = Common::String::format("%s/%sRETRYB.SAN", dir.c_str(), prefix.c_str());
-	} else {
-		// Standard retry video
-		filename = Common::String::format("%s/%sRETRY.SAN", dir.c_str(), prefix.c_str());
-	}
-
-	debug("Rebel2: Playing retry video: %s (phase=%d)", filename.c_str(), phase);
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
-// =============================================================================
-// Level 1 Handler - FUN_00417E53
-// Single gameplay phase (01P01.SAN)
-// =============================================================================
-
-int InsaneRebel2::runLevel1() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Play level beginning cinematic (01BEG.SAN)
-	playLevelBegin(1);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Main gameplay retry loop
-	while (!_vm->shouldQuit()) {
-		// Reset shield for this attempt
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
-		// This ensures all enemies are visible (not skipped by SKIP chunks)
-		clearBit(0);
-
-		// Play gameplay (01P01.SAN with 0x28 flags)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV01/01P01.SAN", 12);
-
-		// Store death frame for video selection
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			// Level completed!
-			debug("Rebel2: Level 1 completed!");
-			playLevelEnd(1);
-			_levelUnlocked[1] = true;  // Unlock level 2
-			return kLevelNextLevel;
-		}
-
-		// Player died - play death video with random A/B variant
-		debug("Rebel2: Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
-		playLevelDeathVariant(1, 1, _deathFrame);
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(1);
-			return kLevelGameOver;
-		}
-
-		// Play retry prompt and loop
-		playLevelRetry(1);
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Wave State Management - FUN_00417b61
-// Waits for video completion, accumulates kill state, redistributes kill credits.
-// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
-// =============================================================================
-
-uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
-	// FUN_00417b61: Core wave management function
-	// Called after each wave video plays. Handles:
-	// 1. Waiting for video to finish (with early exit on enemy completion)
-	// 2. Copying wave state to accumulated phase state
-	// 3. Redistributing kill credits from the budget
-	//
-	// Returns: kill bits credited this wave, or 0xFFFF on death/quit/completion
-
-	uint16 result = 0;
-
-	// Step 1: Wait for video to finish (lines 21-32)
-	// Original loop: while (damage < 0xff && frame < maxFrame-1 && !escPressed)
-	// The SmushPlayer::play() call already blocks until video ends, so this step
-	// is handled implicitly. The early-exit logic (threshold > 0: if frame > 50
-	// AND all required enemy type bits are set, count up and break when > threshold)
-	// would need per-frame callbacks to work precisely. For now, the primary effect
-	// is covered by the video playing to completion and accumulating state.
-	// TODO: Implement per-frame early exit callback for threshold-based wave termination.
-
-	// Step 2: Copy wave state to phase state (line 33)
-	// DAT_0047ab9c = DAT_0047ab98
-	_rebelPhaseState = _rebelWaveState;
-	debug("Rebel2 processWaveEnd: waveState=0x%x -> phaseState=0x%x mask=0x%x budget=%d threshold=%d flags=%d",
-		_rebelWaveState, _rebelPhaseState, mask, budget ? *budget : -1, threshold, flags);
-
-	// Step 3: Kill redistribution - add random unkilled types (lines 34-47)
-	// Only when (flags & 2) != 0. Level 2 always passes flags=0, so inactive for Level 2.
-	if ((flags & 2) != 0) {
-		// Collect unkilled enemy type bits that are within the mask
-		byte unkilled[8];
-		int16 numUnkilled = 0;
-		for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
-			if ((_rebelPhaseState & (2 << (b & 0x1f))) == 0) {
-				unkilled[numUnkilled] = (byte)(2 << (b & 0x1f));
-				numUnkilled++;
-			}
-		}
-		if (numUnkilled > 0) {
-			// Randomly add one unkilled type to phase state
-			int idx = _vm->_rnd.getRandomNumber(numUnkilled - 1);
-			_rebelPhaseState |= unkilled[idx];
-			if (budget) (*budget)++;
-		}
-	}
-
-	// Step 4: Kill credit transfer (lines 48-73)
-	// Collect all SET enemy type bits from phase state
-	byte killed[8];
-	int16 numKilled = 0;
-	for (byte b = 0; (2 << (b & 0x1f)) < (int)(mask & 0x0e); b++) {
-		if ((_rebelPhaseState & (2 << (b & 0x1f))) != 0) {
-			killed[numKilled] = (byte)(2 << (b & 0x1f));
-			numKilled++;
-		}
-	}
-
-	// Max credits: 8 normally, 2 if flag bit 0 set
-	int16 maxCredits = ((flags & 1) == 0) ? 8 : 2;
-
-	// Transfer kills from phase state to result, limited by budget
-	int16 creditCount = 0;
-	while (creditCount < maxCredits && numKilled > 0 && budget && *budget > 0) {
-		int idx = _vm->_rnd.getRandomNumber(numKilled - 1);
-		_rebelPhaseState -= killed[idx];   // Remove from accumulated state
-		result |= killed[idx];              // Credit to return value
-		(*budget)--;
-
-		// Remove from array (shift remaining elements)
-		for (int i = idx; i + 1 < numKilled; i++) {
-			killed[i] = killed[i + 1];
-		}
-		numKilled--;
-		creditCount++;
-	}
-
-	debug("Rebel2 processWaveEnd: result=0x%x phaseState=0x%x (after redistribution) budget=%d",
-		result, _rebelPhaseState, budget ? *budget : -1);
-
-	// Step 5: Return value (lines 74-78)
-	// Return 0xFFFF if: dead, phase complete, or quit
-	if (_playerDamage >= 255 || (int16)_rebelPhaseState >= mask || _vm->shouldQuit()) {
-		return 0xFFFF;
-	}
-	return result;
-}
-
-// =============================================================================
-// Level 2 Handler - FUN_00418063
-// Multiple parts with P1/P2/P3 subdirectories
-// Random animation variants for each part
-// =============================================================================
-
-int InsaneRebel2::runLevel2() {
-	// FUN_00418063: Level 2 "Corellia Star" - Third-person on-foot shooting
-	// Three phases, each with looping enemy waves until all enemy types killed.
-	// Phase completion: (_rebelPhaseState & mask) == mask
-	// Phase 1 mask: 0x06 (enemy types 1,2)
-	// Phase 2 mask: 0x0e (enemy types 1,2,3)
-	// Phase 3 mask: 0x0e (enemy types 1,2,3)
-	//
-	// Kill credit budget (from level data table DAT_0047e0e8):
-	// Each phase gets a budget = tableBase + random(3). processWaveEnd() uses
-	// this budget to randomly redistribute kill credits, creating non-deterministic
-	// wave progression. Using calibrated defaults until exact table values extracted.
-	static const int16 kLevel2BudgetBase[3] = { 3, 3, 3 };  // Phase 1, 2, 3
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int bonusCount = 0;     // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
-	int totalKills = 0;     // local_c: accumulated kill count across phases
-	int totalMisses = 0;    // Accumulated misses (sVar1 + sVar2 from hit counters)
-	int prevWaveState = 0;  // local_8: previous wave's state for Phase 3 randomization
-
-	// Play cutscene (02CUT.SAN)
-	playCinematic("LEV02/02CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (02BEG.SAN)
-	// Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
-	// Includes text overlay from GAME.TRS — deferred until text rendering is ready.
-	playLevelBegin(2);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state (before retry loop)
-	clearBit(0);
-
-	// Main gameplay retry loop (restarts from beginning on death)
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-		bonusCount = 0;
-		totalKills = 0;
-		totalMisses = 0;
-
-		// Reset Handler 25 cover state — player starts uncovered at level start
-		// DAT_00457904 and DAT_0045790a are zero-initialized globals in the original
-		_rebelAutopilot = 0;
-		_rebelDamageLevel = 0;
-		_rebelControlMode = 0;
-
-		// FUN_0041c7d0: Reset per-attempt state
-		_enemies.clear();
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
-
-		// ===== PHASE 1: P1/02P01_X.SAN =====
-		// FUN_0041c7d0: Reset per-phase counters
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-
-		// Initialize kill budget from level data table + random(3)
-		// Original: sVar4 = levelData[phase1Offset]; local_14[0] = sVar4 + random(3)
-		int16 budget = kLevel2BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
-
-		// Play A.SAN (background loader) — flags 0x28 (preserve buffer, gameplay mode)
-		debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/P1/02P01_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
-		processWaveEnd(0x36, &budget, 0, 0);
-
-		// Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
-		// Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
-		while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
-			if (_vm->shouldQuit()) return kLevelQuit;
-
-			// Random variant B, C, or D
-			int variant = _vm->_rnd.getRandomNumber(2);  // 0-2
-			const char *variants[] = {
-				"LEV02/P1/02P01_B.SAN",
-				"LEV02/P1/02P01_C.SAN",
-				"LEV02/P1/02P01_D.SAN"
-			};
-			debug("Rebel2: Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
-			// Wave videos use flags 0x428 (original: FUN_0041f4d0 param_2=0x428)
-			splayer->setCurVideoFlags(0x428);
-			splayer->play(variants[variant], 12);
-			_deathFrame = splayer->_frame;
-
-			// processWaveEnd with threshold=0x14 (20) — enables early exit when enemies killed
-			processWaveEnd(0x36, &budget, 0x14, 0);
-			debug("Rebel2: Phase 1 wave done - state=0x%x (need 0x06) budget=%d", _rebelPhaseState, budget);
-		}
-
-		// Check for bonus (bit 4 = 0x10)
-		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
-
-		if (_playerDamage >= 255) goto level2_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Post segment 1 (02PST1.SAN)
-		// Original: FUN_00417168("02PST1.SAN", 0x20) → flags = 0x20 | 0x08 = 0x28
-		// FUN_00417168 adds | 8 to preserve the screen buffer between gameplay and transition
-		// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/02PST1.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		totalKills += _rebelKillCounter;
-		totalMisses += _rebelHitCounter;
-
-		// ===== PHASE 2: P2/02P02_X.SAN =====
-		_currentPhase = 2;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
-
-		// Initialize Phase 2 budget
-		budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
-
-		// Restore handler for gameplay — will be confirmed by IACT opcode 6
-		_rebelHandler = 8;
-
-		// Play A.SAN (background loader)
-		debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/P2/02P02_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
-		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
-		while (true) {
-			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
-			if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e) break;
-			if (_vm->shouldQuit()) return kLevelQuit;
-
-			// If no specific pattern: randomize high bits (original lines 71-74)
-			// When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
-			if ((waveSelect & 0x0c) == 0) {
-				waveSelect = _vm->_rnd.getRandomNumber(2) + 0x10;
-			}
-
-			// Variant selection matching original switch (FUN_418063 lines 75-96)
-			const char *filename;
-			switch (waveSelect) {
-			case 4: case 6:
-				filename = "LEV02/P2/02P02_B.SAN"; break;
-			case 8: case 10:
-				filename = "LEV02/P2/02P02_C.SAN"; break;
-			case 0x0c: case 0x0e:
-				filename = "LEV02/P2/02P02_A.SAN"; break;
-			case 0x11:
-				filename = "LEV02/P2/02P02_E.SAN"; break;
-			case 0x12:
-				filename = "LEV02/P2/02P02_F.SAN"; break;
-			default:
-				filename = "LEV02/P2/02P02_D.SAN"; break;
-			}
-
-			debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
-			splayer->setCurVideoFlags(0x428);
-			splayer->play(filename, 12);
-			_deathFrame = splayer->_frame;
-		}
-
-		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
-
-		if (_playerDamage >= 255) goto level2_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Post segment 2 (02PST2.SAN)
-		// Original: FUN_00417168("02PST2.SAN", 0x20) → flags = 0x20 | 0x08 = 0x28
-		// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/02PST2.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		totalKills += _rebelKillCounter;
-		totalMisses += _rebelHitCounter;
-
-		// ===== PHASE 3: P3/02P03_X.SAN =====
-		_currentPhase = 3;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
-		prevWaveState = 0;
-
-		// Initialize Phase 3 budget
-		budget = kLevel2BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
-
-		// Restore handler for gameplay — will be confirmed by IACT opcode 6
-		_rebelHandler = 8;
-
-		// Play A.SAN (background loader)
-		debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/P3/02P03_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
-		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
-		{
-			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
-
-			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				// Phase 3 randomization (original lines 113-115):
-				// If previous wave state bit 0 was clear AND random(8)==0, set bit 0
-				if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
-					waveSelect |= 1;
-				}
-				prevWaveState = waveSelect;
-
-				// Variant selection matching original switch (FUN_418063 lines 117-144)
-				const char *filename;
-				switch (waveSelect) {
-				case 0:
-					filename = "LEV02/P3/02P03_H.SAN"; break;
-				case 2:
-					filename = "LEV02/P3/02P03_G.SAN"; break;
-				case 4:
-					filename = "LEV02/P3/02P03_F.SAN"; break;
-				case 6:
-					filename = "LEV02/P3/02P03_E.SAN"; break;
-				case 8:
-					filename = "LEV02/P3/02P03_D.SAN"; break;
-				case 10:
-					filename = "LEV02/P3/02P03_C.SAN"; break;
-				case 0x0c:
-					filename = "LEV02/P3/02P03_B.SAN"; break;
-				case 0x0e:
-					filename = "LEV02/P3/02P03_A.SAN"; break;
-				default:
-					filename = "LEV02/P3/02P03_I.SAN"; break;
-				}
-
-				debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				// processWaveEnd at BOTTOM with threshold=0x14
-				waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
-				debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
-			}
-		}
-
-		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
-		totalKills += _rebelKillCounter;
-
-		if (_playerDamage >= 255) goto level2_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Level completed! Calculate accuracy score.
-		// Original: FUN_00417327 with score thresholds and medal ranks
-		// Score presentation deferred until GAME.TRS text rendering is implemented.
-		{
-			totalMisses += _rebelHitCounter;
-			int accuracy = 0;
-			int totalShots = totalKills + totalMisses;
-			if (totalKills > 0 && totalShots > 0) {
-				accuracy = (totalKills * 100) / totalShots;
-			}
-			debug("Rebel2: Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
-				totalKills, totalMisses, accuracy, bonusCount);
-		}
-
-		playLevelEnd(2);
-		_levelUnlocked[2] = true;  // Unlock level 3
-		return kLevelNextLevel;
-
-	level2_death:
-		// Player died — play death sequence and retry or game over
-		// Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
-		debug("Rebel2: Level 2 Phase %d death", _currentPhase);
-		playCinematic("LEV02/02DIE.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
-		// DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
-		// Currently not tracked — will be wired when bonus life system is implemented.
-		_playerLives--;
-		if (_playerLives <= 0) {
-			// Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
-			playLevelGameOver(2);
-			return kLevelGameOver;
-		}
-		playCinematic("LEV02/02RETRY.SAN");
-		_playerDamage = 0;
-		if (_vm->shouldQuit()) return kLevelQuit;
-		continue;  // Restart from beginning
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 3 Handler - FUN_0041885F
-// Two phases with per-phase retry handling
-// Phase 1: 03PLAY1.SAN, Phase 2: 03PLAY2.SAN
-// =============================================================================
-
-int InsaneRebel2::runLevel3() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int phase1Score = 0;  // Score preserved across phase 2 retries
-
-	// Play level beginning cinematic (03BEG.SAN)
-	playLevelBegin(3);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// ===== PHASE 1 retry loop =====
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-
-		// Reset bit table before gameplay starts - FUN_00423880 calls FUN_00423a00(0)
-		clearBit(0);
-
-		// Play phase 1 gameplay (03PLAY1.SAN)
-		debug("Rebel2: Level 3 Phase 1");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV03/03PLAY1.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			// Phase 1 completed - save score and proceed to phase 2
-			phase1Score = _playerScore;
-			break;
-		}
-
-		// Died in phase 1 - frame-based death video
-		debug("Rebel2: Level 3 Phase 1 death at frame %d", _deathFrame);
-		playLevelDeathVariant(3, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(3);
-			return kLevelGameOver;
-		}
-
-		// Phase 1 retry (03RETRY.SAN)
-		playLevelRetryVariant(3, 1);
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Post segment 1 (03POST1.SAN)
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;
-	splayer->setCurVideoFlags(0x28);
-	splayer->play("LEV03/03POST1.SAN", 12);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// ===== PHASE 2 retry loop (preserves phase 1 score) =====
-	_currentPhase = 2;
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-
-		// Reset bit table before gameplay starts
-		clearBit(0);
-
-		// Play phase 2 gameplay (03PLAY2.SAN)
-		debug("Rebel2: Level 3 Phase 2");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV03/03PLAY2.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			// Level completed!
-			debug("Rebel2: Level 3 completed!");
-			playLevelEnd(3);
-			_levelUnlocked[3] = true;  // Unlock level 4
-			return kLevelNextLevel;
-		}
-
-		// Died in phase 2 - frame-based death video
-		debug("Rebel2: Level 3 Phase 2 death at frame %d", _deathFrame);
-		playLevelDeathVariant(3, 2, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			// Use phase 2 specific game over (03OVER.SAN, same file but at different point)
-			playLevelGameOver(3);
-			return kLevelGameOver;
-		}
-
-		// Phase 2 retry (03RETRYB.SAN)
-		playLevelRetryVariant(3, 2);
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 4 Handler
-// Cutscene + single gameplay phase
-// =============================================================================
-
-int InsaneRebel2::runLevel4() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Play cutscene (04CUT.SAN)
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play("LEV04/04CUT.SAN", 12);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (04BEG.SAN)
-	playLevelBegin(4);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Main gameplay retry loop
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-
-		// Reset bit table before gameplay starts
-		clearBit(0);
-
-		// Play gameplay (04PLAY.SAN)
-		debug("Rebel2: Level 4 gameplay");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV04/04PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			// Level completed!
-			debug("Rebel2: Level 4 completed!");
-			playLevelEnd(4);
-			_levelUnlocked[4] = true;  // Unlock level 5
-			return kLevelNextLevel;
-		}
-
-		// Died - play death video (04DIE.SAN, no variants)
-		debug("Rebel2: Level 4 death");
-		playLevelDeathVariant(4, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(4);
-			return kLevelGameOver;
-		}
-
-		playLevelRetry(4);
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 5 Handler - FUN_00418EC6
-// Single gameplay phase (05PLAY.SAN)
-// Random A/B death video like Level 1
-// =============================================================================
-
-int InsaneRebel2::runLevel5() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Play level beginning cinematic (05BEG.SAN)
-	playLevelBegin(5);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Main gameplay retry loop
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-
-		// Reset bit table before gameplay starts
-		clearBit(0);
-
-		// Play gameplay (05PLAY.SAN)
-		debug("Rebel2: Level 5 gameplay");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV05/05PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			// Level completed!
-			debug("Rebel2: Level 5 completed!");
-			playLevelEnd(5);
-			_levelUnlocked[5] = true;  // Unlock level 6
-			return kLevelNextLevel;
-		}
-
-		// Died - play death video with random A/B variant
-		debug("Rebel2: Level 5 death at frame %d", _deathFrame);
-		playLevelDeathVariant(5, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(5);
-			return kLevelGameOver;
-		}
-
-		playLevelRetry(5);
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 6 Handler - FUN_00419317
-// Two phases with per-phase retry (like Level 3)
-// Phase 1: 06PLAY1.SAN, Phase 2: 06PLAY2.SAN
-// =============================================================================
-
-int InsaneRebel2::runLevel6() {
-	// FUN_004190d6 — Mos Eisley: two-phase on-foot (Handler 8)
-	// Phase 1 (levelId=5): 06PLAY1.SAN, mid-switch to 06PLAY1B.SAN at frame 0x2a8
-	// Phase 2 (levelId=6): 06PLAY2.SAN, play until near-end
-	// Original structure: outer do-while for phase 1 retries, inner while(true) for
-	// phase 2 retries + death handling. Phase 1 death breaks inner → RETRY at outer bottom.
-	// Phase 2 death → RETRYB → re-enters phase 2 within inner loop.
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int totalScore = 0;
-
-	// Play level beginning cinematic (06BEG.SAN)
-	// Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
-	playLevelBegin(6);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040 — handler init done by IACT opcode 6
-
-	// Outer retry loop — restarts phase 1 on phase 1 death
-	while (!_vm->shouldQuit()) {
-		// FUN_00407d10 — reset shot/hit counters
-		clearBit(0);
-
-		// DAT_0047ab9c = 0xffffffff — init phase state
-		_rebelPhaseState = 0xffffffff;
-
-		// ===== PHASE 1 =====
-		_rebelLevelType = 5;  // DAT_0047a7f8 = 5
-		_currentPhase = 1;
-
-		debug("Rebel2: Level 6 Phase 1");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV06/06PLAY1.SAN", 12);
-		// TODO: Mid-level switch at frame 0x2a8 to 06PLAY1B.SAN (flags 0x468)
-		// + score checkpoint (FUN_00407f55) — needs per-frame callback
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield <= 0) {
-			// Died in phase 1
-			debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
-			playLevelDeathVariant(6, 1, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
-
-			_playerLives--;
-			if (_playerLives <= 0) {
-				playLevelGameOver(6);
-				return kLevelGameOver;
-			}
-
-			// Phase 1 retry (06RETRY.SAN) → restart outer loop
-			playLevelRetryVariant(6, 1);
-			if (_vm->shouldQuit()) return kLevelQuit;
-			continue;
-		}
-
-		// Phase 1 survived — save score, play POST1
-		totalScore = _playerScore;  // local_8 = DAT_0047ab84
-
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV06/06POST1.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== PHASE 2 retry loop (inner while(true) in original) =====
-		while (!_vm->shouldQuit()) {
-			_rebelLevelType = 6;  // DAT_0047a7f8 = 6
-			_currentPhase = 2;
-			clearBit(0);  // FUN_00407d10
-
-			debug("Rebel2: Level 6 Phase 2");
-			splayer->setCurVideoFlags(0x28);
-			splayer->play("LEV06/06PLAY2.SAN", 12);
-			_deathFrame = splayer->_frame;
-
-			if (_vm->shouldQuit()) return kLevelQuit;
-
-			// Accumulate score: local_8 = DAT_0047ab84 + local_8
-			totalScore += _playerScore;
-
-			if (_playerShield > 0) {
-				// Level completed!
-				debug("Rebel2: Level 6 completed!");
-				playLevelEnd(6);
-				_levelUnlocked[6] = true;
-				return kLevelNextLevel;
-			}
-
-			// Died in phase 2
-			debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
-			playLevelDeathVariant(6, 2, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
-
-			_playerLives--;
-			if (_playerLives <= 0) {
-				playLevelGameOver(6);
-				return kLevelGameOver;
-			}
-
-			// Phase 2 retry (06RETRYB.SAN) → re-enter phase 2
-			playLevelRetryVariant(6, 2);
-			if (_vm->shouldQuit()) return kLevelQuit;
-		}
-
-		break;  // Should only reach here on shouldQuit
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 7 Handler - FUN_0041974C
-// "TIE Training" - Canyon flight with fork at frame 1592
-// Single gameplay phase (07PLAY.SAN), optional second segment (07PLAYB.SAN)
-// Death: DAT_0047ab8c-based (fork reached → DIE_B, not reached → DIE_A)
-// =============================================================================
-
-int InsaneRebel2::runLevel7() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	bool reachedFork = false;  // DAT_0047ab8c equivalent — tracks if 07PLAYB was played
-
-	// Play cutscene (07CUT.SAN)
-	playCinematic("LEV07/07CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (07BEG.SAN)
-	playLevelBegin(7);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-		reachedFork = false;
-
-		clearBit(0);
-
-		// Play gameplay (07PLAY.SAN)
-		// Original: FUN_0041f4d0("07PLAY.SAN", 0x28, -1, -1, 0)
-		// At frame 0x638 (1592), if DAT_0047ab8c != 0: play 07PLAYB.SAN (0x468)
-		// TODO: Mid-level fork at frame 1592 requires per-frame callback.
-		// For now, play the main video. The fork video (07PLAYB) would be triggered
-		// by IACT callbacks setting a state flag during gameplay.
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV07/07PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			debug("Rebel2: Level 7 completed!");
-			playLevelEnd(7);
-			_levelUnlocked[7] = true;
-			return kLevelNextLevel;
-		}
-
-		// Death video: DIE_B if fork reached, DIE_A if not
-		// Original: s_LEV07_07DIE_B + ((DAT_0047ab8c != 0) - 1 & 0x14)
-		debug("Rebel2: Level 7 death at frame %d, fork=%d", _deathFrame, reachedFork);
-		if (reachedFork) {
-			playCinematic("LEV07/07DIE_B.SAN");
-		} else {
-			playCinematic("LEV07/07DIE_A.SAN");
-		}
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(7);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV07/07RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 8 Handler - FUN_00419976
-// "Flight to Imdaar" - Y-Wing space battle (single phase)
-// No cutscene (starts with BEG). flags=0x08 for gameplay.
-// Death: random A or B
-// =============================================================================
-
-int InsaneRebel2::runLevel8() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// No cutscene — starts directly with BEG
-	// Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
-	playLevelBegin(8);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		clearBit(0);
-
-		// Play gameplay (08PLAY.SAN)
-		// Original: FUN_0041f4d0("08PLAY.SAN", 8, -1, -1, 0) — note flags=0x08
-		splayer->setCurVideoFlags(0x08);
-		splayer->play("LEV08/08PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 8 completed! accuracy=%d%%", accuracy);
-			playLevelEnd(8);
-			_levelUnlocked[8] = true;
-			return kLevelNextLevel;
-		}
-
-		// Death: random A or B
-		// Original: random(2) → A or B via string pointer arithmetic
-		debug("Rebel2: Level 8 death at frame %d", _deathFrame);
-		playLevelDeathVariant(8, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(8);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV08/08RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 9 Handler - FUN_00419B86
-// "The Mine Field" - Navigate through force fields (single phase)
-// No cutscene. Initial phaseState = 0xfffffffe (all bits set except bit 0).
-// Mid-events at frames 0x19f (415) and 0x352 (850): FUN_00407f55 (score checkpoint)
-// Death: DAT_0047ab94-based (0→A, 1→C, else→B)
-// =============================================================================
-
-int InsaneRebel2::runLevel9() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// No cutscene — starts directly with BEG
-	// Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
-	playLevelBegin(9);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		clearBit(0);
-
-		// Original: DAT_0047ab9c = 0xfffffffe (initial phase state — all bits except 0)
-		_rebelPhaseState = 0xfffffffe;
-
-		// Play gameplay (09PLAY.SAN)
-		// Original: FUN_0041f4d0("09PLAY.SAN", 0x28, -1, -1, 0)
-		// Mid-events at frames 415 and 850: FUN_00407f55 (score save)
-		// These are handled implicitly — the IACT callbacks manage scoring.
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV09/09PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 9 completed! accuracy=%d%%", accuracy);
-			playLevelEnd(9);
-			_levelUnlocked[9] = true;
-			return kLevelNextLevel;
-		}
-
-		// Death: based on DAT_0047ab94 (death cause tracking)
-		// Original: 0→DIE_A, 1→DIE_C, else→DIE_B
-		debug("Rebel2: Level 9 death at frame %d", _deathFrame);
-		playLevelDeathVariant(9, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(9);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV09/09RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 10 Handler - FUN_00419E0A
-// "Speeder Bikes" - Forest speeder chase (single phase)
-// Has cutscene. Single death video (10DIE.SAN, no variants).
-// Original plays DIE then RETRY in sequence (no separate check).
-// =============================================================================
-
-int InsaneRebel2::runLevel10() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Play cutscene (10CUT.SAN)
-	playCinematic("LEV10/10CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (10BEG.SAN)
-	// Original: FUN_004171c5("10BEG.SAN", 0x20, 0xb3, 0xa0, 10, 2, 0x46)
-	playLevelBegin(10);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		clearBit(0);
-
-		// Play gameplay (10PLAY.SAN)
-		// Original: FUN_0041f4d0("10PLAY.SAN", 0x28, -1, -1, 0)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV10/10PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 10 completed! accuracy=%d%%", accuracy);
-			playLevelEnd(10);
-			_levelUnlocked[10] = true;
-			return kLevelNextLevel;
-		}
-
-		// Death + Retry: original plays both in sequence
-		// Original: lives--, if 0 break to game over, else DIE+RETRY
-		debug("Rebel2: Level 10 death at frame %d", _deathFrame);
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(10);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV10/10DIE.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-		playCinematic("LEV10/10RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 11 Handler - FUN_0041A00C
-// "Inside the Terror" - Three phases + bridge puzzle (Handler 8, on-foot)
-//
-// Phase 1: P1/11P01_X (A,B,C,D) - behind barrels, mask 0x0e
-// Phase 2: P2/11P02_X (A,B,C,D) - walls on right, mask 0x0e, flags=3
-// Phase 3 first half: P3/11P03_X (A-F) - bridge puzzle, mask 0x7e
-//   Exit when (phaseState & 0x70) == 0x70
-// POST3/POST3B/POST3C bridge cinematics
-// Phase 3 second half: P3/11P03_X (G-L) - after bridge, mask 0x0e
-// =============================================================================
-
-int InsaneRebel2::runLevel11() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int totalKills = 0;
-	int totalMisses = 0;
-	int prevPhaseState = 0;
-
-	// Kill credit budget bases per phase (from level data table DAT_0047e0e8)
-	static const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
-
-	// Play cutscene (11CUT.SAN)
-	playCinematic("LEV11/11CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (11BEG.SAN)
-	playLevelBegin(11);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
-	clearBit(0);
-
-	// Main gameplay retry loop (restarts from Phase 1 on death)
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-		totalKills = 0;
-		totalMisses = 0;
-		prevPhaseState = 0;
-
-		// Reset Handler 8 cover state
-		_rebelAutopilot = 0;
-		_rebelDamageLevel = 0;
-		_rebelControlMode = 0;
-
-		// FUN_0041c7d0: Reset per-attempt state
-		_enemies.clear();
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
-
-		// ===== PHASE 1: P1/11P01_X.SAN =====
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-
-		int16 budget = kLevel11BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
-
-		// Play A.SAN (background loader)
-		debug("Rebel2: Level 11 Phase 1 - playing 11P01_A.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P1/11P01_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
-
-			// Phase 1 wave loop: random(2) | (waveSelect & 8) → variants
-			// 0→D, 1→C, 8→B, 9→A
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				// Bonus sound check
-				if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
-					// FUN_00411931 bonus sound — not yet implemented
-				}
-				prevPhaseState = _rebelPhaseState;
-
-				int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 8);
-				const char *filename;
-				switch (sel) {
-				case 1:  filename = "LEV11/P1/11P01_C.SAN"; break;
-				case 8:  filename = "LEV11/P1/11P01_B.SAN"; break;
-				case 9:  filename = "LEV11/P1/11P01_A.SAN"; break;
-				default: filename = "LEV11/P1/11P01_D.SAN"; break;  // sel == 0
-				}
-
-				debug("Rebel2: Level 11 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level11_death_phase1;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Post segment 1 (11POST1.SAN)
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/11POST1.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		totalKills += _rebelKillCounter;
-		totalMisses += _rebelHitCounter;
-
-		// ===== PHASE 2: P2/11P02_X.SAN =====
-		_currentPhase = 2;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
-
-		budget = kLevel11BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
-		_rebelHandler = 8;
-
-		// Play A.SAN (background loader)
-		debug("Rebel2: Level 11 Phase 2 - playing 11P02_A.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P2/11P02_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			// Phase 2: flags=3 (maxCredits=2, redistribution ON)
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 3);
-
-			// Random(4) for variant selection: A, B, C, D
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				int variant = _vm->_rnd.getRandomNumber(3);
-				const char *filename;
-				switch (variant) {
-				case 0:  filename = "LEV11/P2/11P02_A.SAN"; break;
-				case 1:  filename = "LEV11/P2/11P02_B.SAN"; break;
-				case 2:  filename = "LEV11/P2/11P02_C.SAN"; break;
-				default: filename = "LEV11/P2/11P02_D.SAN"; break;
-				}
-
-				debug("Rebel2: Level 11 Phase 2 wave - %s (state=0x%x)", filename, _rebelPhaseState);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level11_death_phase2;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Post segment 2 (11POST2.SAN)
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/11POST2.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		totalKills += _rebelKillCounter;
-		totalMisses += _rebelHitCounter;
-
-		// ===== PHASE 3 FIRST HALF: P3/11P03_X (A-F) =====
-		// Bridge puzzle — exit when (phaseState & 0x70) == 0x70
-		_currentPhase = 3;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
-		prevPhaseState = 0;
-
-		budget = kLevel11BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
-		_rebelHandler = 8;
-
-		debug("Rebel2: Level 11 Phase 3 first half - playing 11P03_A.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P3/11P03_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
-			int local_8 = 0;  // Tracks variant for randomization threshold
-
-			// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
-			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				// Bonus sound: (phaseState & 0xe) == 0xe and previous wasn't
-				if ((_rebelPhaseState & 0x0e) == 0x0e && (prevPhaseState & 0x0e) != 0x0e) {
-					// FUN_00411931 bonus sound — not yet implemented
-				}
-				prevPhaseState = _rebelPhaseState;
-
-				// Randomization: wider range for first few waves
-				if (local_8 < 3) {
-					local_8 = _vm->_rnd.getRandomNumber(7);  // 0-7
-				} else {
-					local_8 = _vm->_rnd.getRandomNumber(2);  // 0-2
-				}
-
-				const char *filename;
-				switch (local_8) {
-				case 0:  filename = "LEV11/P3/11P03_A.SAN"; break;
-				case 1:  filename = "LEV11/P3/11P03_B.SAN"; break;
-				case 2:  filename = "LEV11/P3/11P03_C.SAN"; break;
-				case 3:  filename = "LEV11/P3/11P03_D.SAN"; break;
-				case 4:  filename = "LEV11/P3/11P03_E.SAN"; break;
-				case 5:  filename = "LEV11/P3/11P03_F.SAN"; break;
-				case 6:  filename = "LEV11/P3/11P03_F.SAN"; break;  // duplicate F
-				default: filename = "LEV11/P3/11P03_E.SAN"; break;  // duplicate E
-				}
-
-				debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				// Threshold only for higher variants (original: (2 < local_8) - 1 & 0x14)
-				int16 threshold = (local_8 > 2) ? 0x14 : 0;
-				waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level11_death_phase3;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== PHASE 3 BRIDGE CINEMATICS =====
-		{
-			bool allBasicKilled = (_rebelPhaseState & 0x0e) >= 0x0e;
-			if (!allBasicKilled) {
-				// Normal bridge drop cinematic
-				playCinematic("LEV11/11POST3.SAN");
-				// Bonus checks (FUN_0042aa70) — deferred, play standard path
-				// Original checks 0x77 and 0x62 for special POST3C cinematic
-			} else {
-				// All enemy types killed — bridge dropped successfully
-				playCinematic("LEV11/11POST3B.SAN");
-			}
-		}
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
-		// Reset shots/explosions (FUN_0041ca6a equivalent)
-		for (int i = 0; i < 5; i++) {
-			_explosions[i].active = false;
-		}
-		_enemies.clear();
-
-		// Preserve only bits 1-3 of phase state (original: DAT_0047ab9c &= 0xe)
-		_rebelPhaseState &= 0x0e;
-		_rebelWaveState &= 0x0e;
-
-		_rebelHandler = 8;
-
-		budget = kLevel11BudgetBase[3] + _vm->_rnd.getRandomNumber(2);
-
-		// Play G.SAN (background loader for second half)
-		debug("Rebel2: Level 11 Phase 3 second half - playing 11P03_G.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P3/11P03_G.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// Only enter wave loop if not all basic types killed already
-		if ((_rebelPhaseState & 0x0e) < 0x0e) {
-			int local_8 = 0;
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
-
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				// Wider randomization for first few waves
-				if (local_8 < 4) {
-					local_8 = _vm->_rnd.getRandomNumber(8);  // 0-8
-				} else {
-					local_8 = _vm->_rnd.getRandomNumber(2);  // 0-2
-				}
-
-				const char *filename;
-				switch (local_8) {
-				case 0:  filename = "LEV11/P3/11P03_G.SAN"; break;
-				case 1:  filename = "LEV11/P3/11P03_H.SAN"; break;
-				case 2:  filename = "LEV11/P3/11P03_I.SAN"; break;
-				case 3:  filename = "LEV11/P3/11P03_G.SAN"; break;  // G again
-				case 4:  filename = "LEV11/P3/11P03_H.SAN"; break;  // H again
-				case 5:  filename = "LEV11/P3/11P03_I.SAN"; break;  // I again
-				case 6:  filename = "LEV11/P3/11P03_J.SAN"; break;
-				case 7:  filename = "LEV11/P3/11P03_K.SAN"; break;
-				default: filename = "LEV11/P3/11P03_L.SAN"; break;
-				}
-
-				debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				int16 threshold = (local_8 > 2) ? 0x14 : 0;
-				waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
-			}
-		}
-
-		totalKills += _rebelKillCounter;
-
-		if (_playerDamage >= 255) goto level11_death_phase3;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== LEVEL COMPLETED =====
-		{
-			totalMisses += _rebelHitCounter;
-			int accuracy = 0;
-			int totalShots = totalKills + totalMisses;
-			if (totalKills > 0 && totalShots > 0) {
-				accuracy = (totalKills * 100) / totalShots;
-			}
-			debug("Rebel2: Level 11 completed! kills=%d misses=%d accuracy=%d%%",
-				totalKills, totalMisses, accuracy);
-		}
-
-		playLevelEnd(11);
-		_levelUnlocked[11] = true;  // Unlock level 12
-		return kLevelNextLevel;
-
-	level11_death_phase1:
-		debug("Rebel2: Level 11 Phase 1 death");
-		playCinematic("LEV11/11DIE_A.SAN");
-		goto level11_retry;
-
-	level11_death_phase2:
-		debug("Rebel2: Level 11 Phase 2 death");
-		playCinematic("LEV11/11DIE_B.SAN");
-		goto level11_retry;
-
-	level11_death_phase3:
-		debug("Rebel2: Level 11 Phase 3 death");
-		playCinematic("LEV11/11DIE_C.SAN");
-		goto level11_retry;
-
-	level11_retry:
-		if (_vm->shouldQuit()) return kLevelQuit;
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(11);
-			return kLevelGameOver;
-		}
-		playCinematic("LEV11/11RETRY.SAN");
-		_playerDamage = 0;
-		if (_vm->shouldQuit()) return kLevelQuit;
-		continue;  // Restart from Phase 1
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 12 Handler - FUN_0041AA14
-// "Sewers" - Four phases FPS corridor shooting (Handler 25)
-//
-// Each phase: init video (P05/P06/P07/P08) → first wave → wave loop
-// Phase 1: P1/12P01_X (A,B,C,D) mask=6
-// Phase 2: P2/12P02_X (A,B,C,D,E,F) mask=6
-// Phase 3: P3/12P03_X (A,B,C,D,F) mask=6
-// Phase 4: P4/12P04_X (A,B,C,D,E,F) mask=0xe
-// Closing: 12P09.SAN
-// =============================================================================
-
-int InsaneRebel2::runLevel12() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Kill credit budget bases per phase
-	static const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
-
-	// Play cutscene (12CUT.SAN)
-	playCinematic("LEV12/12CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (12BEG.SAN)
-	playLevelBegin(12);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_0041c7d0 + FUN_00407d10 + FUN_0040c040: Reset game state
-	clearBit(0);
-
-	// Main gameplay retry loop (restarts from Phase 1 on death)
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-
-		// Reset state
-		_rebelAutopilot = 0;
-		_rebelDamageLevel = 0;
-		_rebelControlMode = 0;
-
-		_enemies.clear();
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
-
-		// ===== PHASE 1: 12P05 → P1/12P01_X =====
-		// FUN_00401000: Reset at top of each retry
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-
-		int16 budget = kLevel12BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
-
-		// Initialization video (12P05.SAN)
-		debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV12/12P05.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-		processWaveEnd(1, &budget, 0, 0);
-
-		// First wave (P1/12P01_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P1/12P01_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-
-			// Wave loop: random(2) | (waveSelect & 2) → 0:C, 1:D, 2:A, 3:B
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
-				const char *filename;
-				switch (sel) {
-				case 0:  filename = "LEV12/P1/12P01_C.SAN"; break;
-				case 1:  filename = "LEV12/P1/12P01_D.SAN"; break;
-				case 2:  filename = "LEV12/P1/12P01_A.SAN"; break;
-				default: filename = "LEV12/P1/12P01_B.SAN"; break;
-				}
-
-				debug("Rebel2: Level 12 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== PHASE 2: 12P06 → P2/12P02_X =====
-		_currentPhase = 2;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-
-		budget = kLevel12BudgetBase[1] + _vm->_rnd.getRandomNumber(3);
-
-		// Initialization video (12P06.SAN)
-		debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P06.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-		processWaveEnd(1, &budget, 0, 0);
-
-		// First wave (P2/12P02_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P2/12P02_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				// Variant selection: (waveSelect & 2) controls which set
-				int local_8;
-				if ((waveSelect & 2) == 0) {
-					local_8 = _vm->_rnd.getRandomNumber(2) + 3;  // 3, 4, or 5
-				} else {
-					local_8 = _vm->_rnd.getRandomNumber(2);      // 0, 1, or 2
-				}
-
-				const char *filename;
-				switch (local_8) {
-				case 0:  filename = "LEV12/P2/12P02_A.SAN"; break;
-				case 1:  filename = "LEV12/P2/12P02_B.SAN"; break;
-				case 2:  filename = "LEV12/P2/12P02_E.SAN"; break;
-				case 3:  filename = "LEV12/P2/12P02_C.SAN"; break;
-				case 4:  filename = "LEV12/P2/12P02_D.SAN"; break;
-				default: filename = "LEV12/P2/12P02_F.SAN"; break;
-				}
-
-				debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				// Variants E(2) and F(5) reset threshold to 0
-				int16 threshold = (local_8 == 2 || local_8 == 5) ? 0 : 0x14;
-				waveSelect = processWaveEnd(6, &budget, threshold, 0);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== PHASE 3: 12P07 → P3/12P03_X =====
-		_currentPhase = 3;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-
-		budget = kLevel12BudgetBase[2] + _vm->_rnd.getRandomNumber(3);
-
-		// Initialization video (12P07.SAN)
-		debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P07.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-		processWaveEnd(1, &budget, 0, 0);
-
-		// First wave (P3/12P03_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P3/12P03_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			int local_8 = 0;
-			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				// Wider randomization for first few waves
-				if (local_8 < 4) {
-					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
-				} else {
-					local_8 = _vm->_rnd.getRandomNumber(3);  // 0-3
-				}
-
-				const char *filename;
-				switch (local_8) {
-				case 0:  filename = "LEV12/P3/12P03_C.SAN"; break;
-				case 1:  filename = "LEV12/P3/12P03_D.SAN"; break;
-				case 2:  filename = "LEV12/P3/12P03_A.SAN"; break;
-				case 3:  filename = "LEV12/P3/12P03_B.SAN"; break;
-				case 4:  filename = "LEV12/P3/12P03_F.SAN"; break;
-				default: filename = "LEV12/P3/12P03_F.SAN"; break;  // duplicate F
-				}
-
-				debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== PHASE 4: 12P08 → P4/12P04_X =====
-		_currentPhase = 4;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-
-		budget = kLevel12BudgetBase[3] + _vm->_rnd.getRandomNumber(3);
-
-		// Initialization video (12P08.SAN)
-		debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P08.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-		processWaveEnd(1, &budget, 0, 0);
-
-		// First wave (P4/12P04_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P4/12P04_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		{
-			int local_8 = 0;
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
-
-			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
-
-				if (local_8 < 4) {
-					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
-				} else {
-					local_8 = _vm->_rnd.getRandomNumber(3);  // 0-3
-				}
-
-				const char *filename;
-				switch (local_8) {
-				case 0:  filename = "LEV12/P4/12P04_C.SAN"; break;
-				case 1:  filename = "LEV12/P4/12P04_D.SAN"; break;
-				case 2:  filename = "LEV12/P4/12P04_A.SAN"; break;
-				case 3:  filename = "LEV12/P4/12P04_B.SAN"; break;
-				case 4:  filename = "LEV12/P4/12P04_E.SAN"; break;
-				default: filename = "LEV12/P4/12P04_F.SAN"; break;
-				}
-
-				debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
-
-				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
-			}
-		}
-
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// ===== CLOSING: 12P09.SAN =====
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P09.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
-		processWaveEnd(1, &budget, 0, 0);
-
-		// ===== LEVEL COMPLETED =====
-		{
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 12 completed! kills=%d misses=%d accuracy=%d%%",
-				_rebelKillCounter, _rebelHitCounter, accuracy);
-		}
-
-		// Bonus checks: FUN_0042aa70(0x61), FUN_0042aa70(99), FUN_0042aa70(0x74)
-		// If all three bonuses found, play special ending (12END_Z.SAN)
-		// Deferred until bonus tracking is implemented
-
-		playLevelEnd(12);
-		_levelUnlocked[12] = true;  // Unlock level 13
-		return kLevelNextLevel;
-
-	level12_death:
-		// Single death video for all phases
-		debug("Rebel2: Level 12 Phase %d death", _currentPhase);
-		playCinematic("LEV12/12DIE.SAN");
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(12);
-			return kLevelGameOver;
-		}
-		playCinematic("LEV12/12RETRY.SAN");
-		_playerDamage = 0;
-		if (_vm->shouldQuit()) return kLevelQuit;
-		continue;  // Restart from Phase 1
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 13 Handler - FUN_0041B3E1
-// "Escaping the Star Destroyer" - Two-phase flight/escape
-// Phase A: 13PLAY_A.SAN (main flight), transitions to Phase B at maxFrame-10
-// Phase B: 13PLAY_B.SAN (reactor loop, flags 0x468) — plays until
-//   (DAT_0047ab90 == 0 && DAT_0047ab7c == 0) meaning all targets destroyed.
-// Death: frame-based (A/B/C/B/D pattern)
-// =============================================================================
-
-int InsaneRebel2::runLevel13() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// No cutscene — starts directly with BEG
-	// Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
-	playLevelBegin(13);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		clearBit(0);
-
-		// Phase A: Main escape flight (13PLAY_A.SAN)
-		// Original: FUN_0041f4d0("13PLAY_A.SAN", 0x28, -1, -1, 0)
-		// First inner loop runs until frame reaches maxFrame-10
-		// Then Phase B (13PLAY_B.SAN, flags 0x468) plays at that exact frame
-		// The 0x468 flags indicate seamless mid-video transition.
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV13/13PLAY_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		// If alive after Phase A, play Phase B (reactor destruction loop)
-		// Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
-		// Then loop while (DAT_0047ab90 != 0 || DAT_0047ab7c != 0)
-		// For now, play B as a sequential video. The IACT callbacks will manage
-		// the reactor target state through opcode interactions.
-		if (_playerShield > 0) {
-			splayer->setCurVideoFlags(0x468);
-			splayer->play("LEV13/13PLAY_B.SAN", 12);
-			_deathFrame = splayer->_frame;
-		}
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 13 completed! accuracy=%d%%", accuracy);
-			playLevelEnd(13);
-			_levelUnlocked[13] = true;
-			return kLevelNextLevel;
-		}
-
-		// Death: frame-based variant selection (FUN_0041B3E1 lines 47-61)
-		debug("Rebel2: Level 13 death at frame %d", _deathFrame);
-		playLevelDeathVariant(13, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(13);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV13/13RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 14 Handler - FUN_0041B6E8
-// "TIE Attack" - Final space battle (single phase)
-// No cutscene. Single death video (14DIE.SAN, no variants).
-// =============================================================================
-
-int InsaneRebel2::runLevel14() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// No cutscene — starts directly with BEG
-	// Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
-	playLevelBegin(14);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		clearBit(0);
-
-		// Play gameplay (14PLAY.SAN)
-		// Original: FUN_0041f4d0("14PLAY.SAN", 0x28, -1, -1, 0)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV14/14PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 14 completed! accuracy=%d%%", accuracy);
-			playLevelEnd(14);
-			_levelUnlocked[14] = true;
-			return kLevelNextLevel;
-		}
-
-		// Death: single video (14DIE.SAN)
-		debug("Rebel2: Level 14 death at frame %d", _deathFrame);
-		playCinematic("LEV14/14DIE.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(14);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV14/14RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
-
-// =============================================================================
-// Level 15 Handler - FUN_0041B8D7
-// "Imdaar Alpha" - Final mission (single long phase with level ID switch)
-// Has cutscene. Mid-level: DAT_0047a7f8 changes from 0xf to 0x10 at frame 0x21e.
-// This represents a transition from the tunnel section to the core section.
-// Death: frame-based (A/B/C/B/C/B/D pattern with 7 thresholds)
-// On completion → FUN_0041BBE8 (credits/end game, not a playable level)
-// =============================================================================
-
-int InsaneRebel2::runLevel15() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Play cutscene (15CUT.SAN)
-	playCinematic("LEV15/15CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// Play level beginning cinematic (15BEG.SAN)
-	// Original: FUN_004171c5("15BEG.SAN", 0x20, 0xb8, 0xa0, 10, 2, 0x46)
-	playLevelBegin(15);
-	if (_vm->shouldQuit()) return kLevelQuit;
-
-	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
-	clearBit(0);
-
-	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_deathFrame = 0;
-
-		clearBit(0);
-
-		// Original: DAT_0047a7f8 = 0xf (level 15) before gameplay
-		// At frame 0x21e (542): DAT_0047a7f8 = 0x10 (switches to "level 16" internally)
-		// After gameplay: DAT_0047a7f8 = 0x10 (stays at 16)
-		// This level ID switch affects which difficulty data is used mid-level.
-		// The IACT callbacks handle gameplay regardless of this ID.
-
-		// Play gameplay (15PLAY.SAN)
-		// Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV15/15PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
-			debug("Rebel2: Level 15 completed! accuracy=%d%%", accuracy);
-			playLevelEnd(15);
-			_levelUnlocked[15] = true;
-			// Level 15 completion leads to credits (FUN_0041BBE8)
-			return kLevelNextLevel;
-		}
-
-		// Death: frame-based variant selection (FUN_0041B8D7 lines 46-65)
-		debug("Rebel2: Level 15 death at frame %d", _deathFrame);
-		playLevelDeathVariant(15, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
-
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(15);
-			return kLevelGameOver;
-		}
-
-		playCinematic("LEV15/15RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
-	}
-
-	return kLevelQuit;
-}
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel_audio.cpp
new file mode 100644
index 00000000000..01f0be50075
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel_audio.cpp
@@ -0,0 +1,374 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "scumm/file.h"
+#include "scumm/scumm_v7.h"
+
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane_rebel.h"
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+
+namespace Scumm {
+
+// ========== Audio Handling for Rebel Assault 2 ==========
+// RA2 doesn't use iMUSE - we handle audio directly through the mixer
+
+void InsaneRebel2::initAudio(int sampleRate) {
+	_audioSampleRate = sampleRate;
+	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+		_audioStreams[i] = nullptr;
+		_audioTrackActive[i] = false;
+	}
+}
+
+void InsaneRebel2::terminateAudio() {
+	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
+		if (_audioTrackActive[i]) {
+			_vm->_mixer->stopHandle(_audioHandles[i]);
+			_audioTrackActive[i] = false;
+		}
+		if (_audioStreams[i]) {
+			_audioStreams[i]->finish();
+			_audioStreams[i] = nullptr;
+		}
+	}
+}
+
+void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+	if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
+		debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
+		return;
+	}
+
+	debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
+
+	// Create audio stream if not already active
+	if (!_audioStreams[trackIdx]) {
+		// RA2 audio is 8-bit unsigned mono at the track's sample rate
+		debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+		_audioTrackActive[trackIdx] = true;
+		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+								DisposeAfterUse::NO);
+	}
+
+	debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
+
+	// Copy the audio data since queueBuffer may need to own it
+	byte *audioCopy = (byte *)malloc(size);
+	if (!audioCopy) {
+		return;
+	}
+	memcpy(audioCopy, data, size);
+
+	// Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
+	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+	// Apply volume and pan to the channel
+	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+	int scaledPan = (pan * 127) / 128;  // Convert -128..127 to -127..127
+	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel2::processAudioFrame(int16 feedSize) {
+	if (!_player) {
+		return;
+	}
+
+	// Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
+	if (_player->_smushTracksNeedInit) {
+		_player->_smushTracksNeedInit = false;
+		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+			_player->_smushDispatch[i].fadeRemaining = 0;
+			_player->_smushDispatch[i].fadeVolume = 0;
+			_player->_smushDispatch[i].fadeSampleRate = 0;
+			_player->_smushDispatch[i].elapsedAudio = 0;
+			_player->_smushDispatch[i].audioLength = 0;
+		}
+	}
+
+	// Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
+	// Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
+	for (int i = 0; i < _player->_smushNumTracks; i++) {
+		SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
+		SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
+
+		if (track.state == TRK_STATE_INACTIVE) {
+			continue;
+		}
+
+		// Skip tracks that don't have valid buffer pointers yet
+		// Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
+		// so tracks in FADING state won't have it set yet - that's OK, they'll be
+		// transitioned below and then processed
+		if (!track.blockPtr) {
+			debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
+				  i, (void*)track.blockPtr, track.state);
+			continue;
+		}
+
+		// Check if this track type should be played
+		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
+
+		if (!isPlayableTrack) {
+			continue;
+		}
+
+		// Calculate base volume for this track type
+		int baseVolume;
+		switch (track.flags & TRK_TYPE_MASK) {
+		case IS_SFX:
+			baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
+			break;
+		case IS_BKG_MUSIC:
+			baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
+			break;
+		case IS_SPEECH:
+			baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
+			break;
+		default:
+			baseVolume = track.volume;
+			break;
+		}
+		int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
+
+		// Handle track state transitions: FADING -> PLAYING
+		if (track.state == TRK_STATE_FADING) {
+			dispatch.headerPtr = track.dataBuf;
+			dispatch.dataBuf = track.subChunkPtr;
+			dispatch.dataSize = track.dataSize;
+			dispatch.currentOffset = 0;
+			dispatch.audioLength = 0;
+			track.state = TRK_STATE_PLAYING;
+		}
+
+		// Process audio for this track
+		if (track.state != TRK_STATE_INACTIVE) {
+			int32 tmpFeedSize = feedSize;
+
+			while (tmpFeedSize > 0) {
+				int32 mixInFrameCount = dispatch.currentOffset;
+
+				// Use dispatch.dataBuf and dispatch.dataSize which are set consistently
+				// when the track transitions from FADING to PLAYING, and audioRemaining
+				// is calculated relative to these values by processAudioCodes
+				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {


Commit: 274be9ad63c8be10aed14b2d578239071ee816f6
    https://github.com/scummvm/scummvm/commit/274be9ad63c8be10aed14b2d578239071ee816f6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:10+02:00

Commit Message:
SCUMM: RA2: Improve intro playback

Changed paths:
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 160a8fdaa9d..07e4b0087b5 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -45,43 +45,38 @@ Common::String InsaneRebel2::getLevelPrefix(int levelId) {
 
 void InsaneRebel2::playIntroSequence() {
 	// Emulates case 0 in FUN_004142BD
-	// Plays the game intro sequence:
-	// 1. CREDITS/O_OPEN_C.SAN - Fox logo (if certain conditions)
-	// 2. CREDITS/O_OPEN_D.SAN - LucasArts logo (if certain conditions)
-	// 3. OPEN/O_OPEN_A.SAN - Main intro
-	// 4. OPEN/O_OPEN_B.SAN - Additional intro (if conditions)
+	//
+	// Original flow:
+	//   - If 'f','o','x' keys all held: play CREDITS/O_OPEN_C.SAN (Fox logo easter egg)
+	//   - If 'b','o','t' keys all held: play CREDITS/O_OPEN_D.SAN (LucasArts logo)
+	//     INSTEAD of the normal intro
+	//   - Else: play OPEN/O_OPEN_A.SAN (main intro - normal path)
+	//   - If DAT_0047ab45 || DAT_0047ab47: play OPEN/O_OPEN_B.SAN (additional intro)
+	//   - Fade out over 10 frames, clear palette, show top pilots, then -> main menu
+	//
+	// We skip the Fox/LucasArts easter eggs (require real-time key state during boot)
+	// and play both O_OPEN_A + O_OPEN_B unconditionally.
 
 	debug("Rebel2: Playing intro sequence");
 
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Set intro flags (non-interactive)
-	splayer->setCurVideoFlags(0x20);
-
-	// Play Fox logo (CREDITS/O_OPEN_C.SAN)
-	// In retail, this checks if 'f', 'o', 'x' keys are held (easter egg)
-	// We'll play it unconditionally for now
-	debug("Rebel2: Playing Fox logo");
-	splayer->play("CREDITS/O_OPEN_C.SAN", 12);
-
-	if (_vm->shouldQuit()) return;
-
-	// Play LucasArts logo (CREDITS/O_OPEN_D.SAN)
-	// In retail, this checks if 'b', 'o', 't' keys are held
-	debug("Rebel2: Playing LucasArts logo");
-	splayer->play("CREDITS/O_OPEN_D.SAN", 12);
+	_gameState = kStateIntro;
+	_menuInputActive = false;
 
-	if (_vm->shouldQuit()) return;
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
 	// Play main intro (OPEN/O_OPEN_A.SAN)
-	debug("Rebel2: Playing main intro");
+	// Original: FUN_0041f4d0("OPEN/O_OPEN_A.SAN", 0x28, 0xffff, 0xffff, 0)
+	debug("Rebel2: Playing main intro (O_OPEN_A.SAN)");
+	splayer->setCurVideoFlags(0x28);
 	splayer->play("OPEN/O_OPEN_A.SAN", 12);
 
 	if (_vm->shouldQuit()) return;
 
 	// Play additional intro (OPEN/O_OPEN_B.SAN)
-	// In retail, this plays if DAT_0047ab45 or DAT_0047ab47 != 0
-	debug("Rebel2: Playing additional intro");
+	// Original: conditional on DAT_0047ab45 || DAT_0047ab47
+	// We play unconditionally (matches "Continue Intro" menu behavior)
+	debug("Rebel2: Playing additional intro (O_OPEN_B.SAN)");
+	splayer->setCurVideoFlags(0x28);
 	splayer->play("OPEN/O_OPEN_B.SAN", 12);
 }
 
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 32e0de1006d..0c5ea36f7b7 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2685,8 +2685,11 @@ Common::Error ScummEngine::go() {
 			return Common::kNoError;
 		}
 
-		// Full game: Run the main menu loop
-		// This emulates the retail game flow from FUN_004142BD
+		// Full game: Emulates the retail game flow from FUN_004142BD
+		// Case 0: Play intro sequence (Fox logo, LucasArts logo, O_OPEN_A, O_OPEN_B)
+		rebel->playIntroSequence();
+
+		// Cases 1-4: Main menu -> pilot select -> chapter select -> gameplay loop
 		while (!shouldQuit()) {
 			// Run main menu and get result
 			int menuResult = rebel->runMainMenu();


Commit: 8723af81dbad8c0fb28b2538ebf416c838f02233
    https://github.com/scummvm/scummvm/commit/8723af81dbad8c0fb28b2538ebf416c838f02233
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:10+02:00

Commit Message:
SCUMM: RA2: Improve subtitles

Changed paths:
    engines/scumm/nut_renderer.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 9612093b54d..8d81f1a302c 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -422,14 +422,20 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
 	char color = (col != -1) ? col : 1;
 
 	if (_vm->_game.id == GID_REBEL2) {
-		// RA2 codec 44 fonts use pixel values 1-4 as body/gradient.
-		// The AHDR palette indices don't match the SMUSH video palette,
-		// so all non-transparent pixels get the caller's text color.
+		// RA2 codec 44 font rendering, matching the original behavior:
+		//   - Pixel value 1 → remapped to the caller's text color
+		//   - Other non-transparent values → used as-is (palette indices)
+		// The font's pixel layout: value 4 = black outline (38% of pixels),
+		// value 1 = body (11%, remapped to color), value 3 = gray AA (2%).
+		// The SMUSH video palette reserves indices 0-4 for text rendering:
+		//   0=(0,0,0), 1=(255,255,255), 2=(188,188,188), 3=(128,128,128), 4=(0,0,0).
 		for (int j = minY; j < height; j++) {
 			for (int i = minX; i < width; i++) {
 				int8 value = *src++;
-				if (value != _chars[chr].transparency)
+				if (value == 1)
 					dst[i] = color;
+				else if (value != _chars[chr].transparency)
+					dst[i] = value;
 			}
 			src += clipWdth;
 			dst += pitch;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 9b172697af6..4ca180b97c7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -843,7 +843,11 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 	// Query ConfMan here. However it may be slower, but
 	// player may want to switch the subtitles on or off during the
 	// playback. This fixes bug #2812
-	if ((!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
+	//
+	// RA2: The original game always shows subtitle text during cinematics
+	// (there is no subtitle toggle in the retail options menu). Skip
+	// this check so TRES text is always rendered.
+	if (_vm->_game.id != GID_REBEL2 && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
 		return;
 
 	bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
@@ -921,6 +925,9 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 		}
 		_multiFont->setDefaultFont(fontId);
 
+		debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
+			  _frame, fontId, color, flags, (int)flg, pos_x, pos_y, left, top, width, height, str);
+
 		if (flg & kStyleWordWrap) {
 			Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
 			_multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);


Commit: 49e3be755df26de433b45b732279ddb34668fd0b
    https://github.com/scummvm/scummvm/commit/49e3be755df26de433b45b732279ddb34668fd0b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:10+02:00

Commit Message:
SCUMM: RA2: Guard wave initialization in opcode 6

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 5dacef70047..bcfbb8d8c75 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -127,6 +127,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	memset(_damageSavedPalette, 0, sizeof(_damageSavedPalette));
 
 	// Retail globals mapped: hit counter, cooldown, invulnerability flag
+	_rebelOp6Initialized = false;
 	_rebelHitCounter = 0;
 	_rebelKillCounter = 0;
 	_rebelInvulnerable = false;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 7cbedcecef2..82a5ab56bb2 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -636,6 +636,7 @@ public:
 	byte _damageSavedPalette[0x300];     // DAT_00459990 - palette snapshot before flash
 
 	// Rebel per-level counters / flags mapped from retail globals
+	bool _rebelOp6Initialized; // Guard: opcode 6 init block (clearBit/links/wave) runs once per video
 	int _rebelHitCounter;    // DAT_0047ab80 - hit counter / state tracker
 	int _rebelKillCounter;   // DAT_0047ab88 - enemies destroyed this phase
 	bool _rebelInvulnerable; // DAT_0047ab64 - toggles invulnerability / state
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 99f90a4a0b1..36c3cc87ca1 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -39,6 +39,13 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// Call base class implementation first (handles Full Throttle state machine)
 	Insane::procPreRendering(renderBitmap);
 
+	// Reset opcode 6 init flag at the start of each new video.
+	// This ensures the per-wave init (clearBit, link table reset, wave state)
+	// fires exactly once per wave video, not every frame.
+	if (_player && _player->_frame == 0) {
+		_rebelOp6Initialized = false;
+	}
+
 	// For Level 2 gameplay (Handler 8 only), restore the background BEFORE FOBJ decoding.
 	// The tiny FOBJ sprites (7x10, 9x38 pixels) only draw new sprite positions but don't
 	// clear old ones. By restoring the full background each frame, we ensure old sprite
@@ -573,18 +580,17 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 
 		// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
-		if (_shipLevelMode != 0 && par4 == 1) {
-			// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
+		// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+		if (_shipLevelMode != 0 && par4 == 1 && !_rebelOp6Initialized) {
 			clearBit(0);
-			// Clear link tables
 			for (int i = 0; i < 512; i++) {
 				_rebelLinks[i][0] = 0;
 				_rebelLinks[i][1] = 0;
 				_rebelLinks[i][2] = 0;
 			}
-			// DAT_0047ab98 = DAT_0047ab9c: Reset wave state to accumulated phase state
 			_rebelWaveState = _rebelPhaseState;
-			debug("Rebel2 Opcode 6 (Handler 8): State reset, wave=0x%x", _rebelWaveState);
+			_rebelOp6Initialized = true;
+			debug("Rebel2 Opcode 6 (Handler 8): Wave init, wave=0x%x", _rebelWaveState);
 		}
 
 		// Skip position calculation for special modes 4 and 5
@@ -979,21 +985,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
 		if (par5 == 1) {
 			_rebelStatusBarSprite = 5;
-			// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
-			// at IACT callback registration time. Each new wave video starts with
-			// a clean bit table so enemy IDs reused across videos work correctly.
-			clearBit(0);
-			// Reset link tables (DAT_0045797c through DAT_0045917c)
-			for (int i = 0; i < 512; i++) {
-				_rebelLinks[i][0] = 0;
-				_rebelLinks[i][1] = 0;
-				_rebelLinks[i][2] = 0;
+			// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+			if (!_rebelOp6Initialized) {
+				clearBit(0);
+				for (int i = 0; i < 512; i++) {
+					_rebelLinks[i][0] = 0;
+					_rebelLinks[i][1] = 0;
+					_rebelLinks[i][2] = 0;
+				}
+				_rebelWaveState = _rebelPhaseState;
+				_rebelOp6Initialized = true;
+				debug("Rebel2 Opcode 6 (Handler 25): Wave init, wave=0x%x autopilot=%d damageLevel=%d",
+					_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
 			}
-			// Reset wave state to accumulated phase state (same as Handler 8)
-			// DAT_0047ab98 = DAT_0047ab9c: ensures new wave starts with correct state
-			_rebelWaveState = _rebelPhaseState;
-			debug("Rebel2 Opcode 6 (Handler 25): Status bar enabled, state reset, wave=0x%x autopilot=%d damageLevel=%d",
-				_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
 		}
 
 		// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
@@ -1184,24 +1188,23 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	if (par4 == 1) {
 		// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
 		_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
-		debug("Rebel2 Opcode 6: Status Bar ENABLED - sprite %d", _rebelStatusBarSprite);
 
-		// Clear ALL iactBits — matches FUN_00423880 calling FUN_00423a00(0)
-		clearBit(0);
-
-		// Clear link tables (DAT_0045797c through DAT_0045917c)
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
+		// Per-wave init: clear bits, links, reset wave state.
+		// In the original game, FUN_00423880 runs ONCE at video-start callback
+		// registration time, not per-frame. Guard with _rebelOp6Initialized so
+		// this fires once per wave video (reset in procPreRendering at frame 0).
+		if (!_rebelOp6Initialized) {
+			clearBit(0);
+			for (int i = 0; i < 512; i++) {
+				_rebelLinks[i][0] = 0;
+				_rebelLinks[i][1] = 0;
+				_rebelLinks[i][2] = 0;
+			}
+			_rebelWaveState = _rebelPhaseState;
+			_rebelHitCounter = 0;
+			_rebelOp6Initialized = true;
+			debug("Rebel2 Opcode 6: Wave init - cleared bits/links, waveState=0x%x", _rebelWaveState);
 		}
-
-		// DAT_0047ab98 = DAT_0047ab9c: At the start of each wave video,
-		// reset wave state to accumulated phase state. Enemies killed in
-		// previous waves stay killed; new kills add during this wave.
-		_rebelWaveState = _rebelPhaseState;
-		_rebelHitCounter = 0;
-		debug("Rebel2 Opcode 6: Wave state reset to phase state 0x%x", _rebelWaveState);
 	}
 
 	// Step 2: Set level type (DAT_00457900 = par3)


Commit: 3d79ed052683379452e7d4d813e24268051457eb
    https://github.com/scummvm/scummvm/commit/3d79ed052683379452e7d4d813e24268051457eb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:11+02:00

Commit Message:
SCUMM: RA2: Minimize impact outside the RA2 engine

Changed paths:
  A engines/scumm/smush/codec_ra2.cpp
  A engines/scumm/smush/smush_player_ra2.cpp
    engines/scumm/module.mk
    engines/scumm/smush/codec1.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index d7f021aa6dd..55a00dd601d 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -144,8 +144,10 @@ MODULE_OBJS += \
 	smush/codec20.o \
 	smush/codec37.o \
 	smush/codec47.o \
+	smush/codec_ra2.o \
 	smush/smush_multi_font.o \
-	smush/smush_player.o
+	smush/smush_player.o \
+	smush/smush_player_ra2.o
 
 ifdef USE_ARM_SMUSH_ASM
 MODULE_OBJS += \
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 0020c51d8cc..087e4d53aaf 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,245 +36,4 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
 	} while (--height);
 }
 
-/**
- * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
- * Use this for background images where color 0 should NOT be treated as transparent.
- * The standard smushDecodeRLE() treats color 0 as transparent, which is correct
- * for overlay sprites but wrong for background images.
- *
- * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
- */
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch;
-	do {
-		dst += left;
-		bompDecodeLine(dst, src + 2, width, true);  // setZero = TRUE to write all colors
-		src += READ_LE_UINT16(src) + 2;
-		dst += pitch - left;
-	} while (--height);
-}
-
-/**
- * Codec 21/44: Line Update codec
- * Used for fonts (NUT files) and some embedded HUD frames.
- * Format: Each line has a 2-byte size header, then 2-byte skip, 2-byte count pairs with literal pixels.
- * The count value needs +1 to get the actual number of pixels to copy.
- * Note: Skip regions preserve previous frame content (delta compression).
- */
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch + left;
-
-	while (height--) {
-		byte *dstPtrNext = dst + pitch;
-		const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
-		src += 2;  // Skip line size header
-		int len = width;
-		byte *lineDst = dst;
-
-		while (len > 0) {
-			// Read 2-byte LE skip value
-			int skip = READ_LE_UINT16(src);
-			src += 2;
-			lineDst += skip;
-			len -= skip;
-			if (len <= 0)
-				break;
-
-			// Read 2-byte LE copy count (+1 for actual count)
-			int count = READ_LE_UINT16(src) + 1;
-			src += 2;
-			if (count > len)
-				count = len;
-			len -= count;
-
-			// Copy literal pixels
-			memcpy(lineDst, src, count);
-			lineDst += count;
-			src += count;
-		}
-		dst = dstPtrNext;
-		src = srcPtrNext;
-	}
-}
-
-/**
- * Codec 23: Skip/Copy with embedded RLE
- * Used for video frames with skip regions.
- * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
- * Note: Skip regions preserve previous frame content (delta compression).
- */
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch + left;
-
-	for (int row = 0; row < height; row++) {
-		int lineDataSize = READ_LE_UINT16(src);
-		src += 2;
-		const byte *lineEnd = src + lineDataSize;
-		byte *lineDst = dst;
-		int x = 0;
-
-		while (src < lineEnd && x < width) {
-			int skip = READ_LE_UINT16(src);
-			src += 2;
-			x += skip;  // Skip preserves previous frame content
-			if (src >= lineEnd || x >= width)
-				break;
-
-			int runSize = READ_LE_UINT16(src);
-			src += 2;
-			const byte *runEnd = src + runSize;
-
-			// Decode RLE within this run - write ALL colors including 0
-			while (src < runEnd && x < width) {
-				byte code = *src++;
-				int num = (code >> 1) + 1;
-				if (num > width - x)
-					num = width - x;
-
-				if (code & 1) {
-					// RLE run - repeat color
-					byte color = (src < runEnd) ? *src++ : 0;
-					memset(lineDst + x, color, num);
-					x += num;
-				} else {
-					// Literal run - copy bytes
-					int toCopy = num;
-					if (toCopy > (int)(runEnd - src))
-						toCopy = (int)(runEnd - src);
-					memcpy(lineDst + x, src, toCopy);
-					src += toCopy;
-					x += toCopy;
-				}
-			}
-			src = runEnd;
-		}
-		src = lineEnd;
-		dst += pitch;
-	}
-}
-
-/**
- * Codec 45: RA2-specific BOMP RLE with variable header
- * Used for embedded ANIM frames, particularly small animation elements.
- * Has a variable-length header (commonly 6 bytes starting with 01 FE).
- * Note: For overlay sprites, color 0 is treated as transparent.
- */
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
-	dst += top * pitch + left;
-
-	// Detect header pattern and find RLE data start
-	int headerSkip = 0;
-
-	// Check for common 6-byte header pattern: 01 FE XX XX XX XX
-	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
-		headerSkip = 6;
-	} else {
-		// Probe offsets to find valid RLE start
-		// Valid start should have reasonable line size values
-		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-			int testLineSize = READ_LE_UINT16(src + testOffset);
-			// A valid line size should be positive and reasonable for the width
-			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
-				// Further validation: try to count valid line sizes
-				int linesTest = 0;
-				const byte *testPtr = src + testOffset;
-				bool validSum = true;
-
-				while (linesTest < height && testPtr + 2 <= src + dataSize) {
-					int ls = READ_LE_UINT16(testPtr);
-					if (ls <= 0 || ls > width * 2) {
-						validSum = false;
-						break;
-					}
-					testPtr += ls + 2;
-					linesTest++;
-				}
-
-				if (validSum && linesTest >= height - 1) {
-					headerSkip = testOffset;
-					break;
-				}
-			}
-		}
-	}
-
-	src += headerSkip;
-	const byte *dataEnd = src + (dataSize - headerSkip);
-
-	// Check first value to determine per-line vs continuous mode
-	int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
-	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
-
-	if (perLineMode) {
-		// Per-line RLE with 2-byte size headers
-		for (int row = 0; row < height && src < dataEnd; row++) {
-			int lineSize = READ_LE_UINT16(src);
-			src += 2;
-			if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
-				break;
-
-			const byte *lineEnd = src + lineSize;
-			byte *rowDst = dst + row * pitch;
-			int x = 0;
-
-			while (src < lineEnd && x < width) {
-				byte ctrl = *src++;
-				int count = (ctrl >> 1) + 1;
-
-				if (ctrl & 1) {
-					// RLE fill - color 0 is transparent for overlay sprites
-					byte color = (src < lineEnd) ? *src++ : 0;
-					if (color != 0) {
-						int num = (count > width - x) ? width - x : count;
-						memset(rowDst + x, color, num);
-					}
-					x += count;
-					if (x > width)
-						x = width;
-				} else {
-					// Literal copy - color 0 is transparent for overlay sprites
-					for (int i = 0; i < count && x < width && src < lineEnd; i++) {
-						byte color = *src++;
-						if (color != 0)
-							rowDst[x] = color;
-						x++;
-					}
-				}
-			}
-			src = lineEnd;
-		}
-	} else {
-		// Continuous BOMP RLE (no per-line headers)
-		for (int row = 0; row < height && src < dataEnd; row++) {
-			byte *rowDst = dst + row * pitch;
-			int x = 0;
-
-			while (x < width && src < dataEnd) {
-				byte ctrl = *src++;
-				int count = (ctrl >> 1) + 1;
-
-				if (ctrl & 1) {
-					// RLE fill - color 0 is transparent for overlay sprites
-					byte color = (src < dataEnd) ? *src++ : 0;
-					if (color != 0) {
-						int num = (count > width - x) ? width - x : count;
-						memset(rowDst + x, color, num);
-					}
-					x += count;
-					if (x > width)
-						x = width;
-				} else {
-					// Literal copy - color 0 is transparent for overlay sprites
-					for (int i = 0; i < count && x < width && src < dataEnd; i++) {
-						byte color = *src++;
-						if (color != 0)
-							rowDst[x] = color;
-						x++;
-					}
-				}
-			}
-		}
-	}
-}
-
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/codec_ra2.cpp b/engines/scumm/smush/codec_ra2.cpp
new file mode 100644
index 00000000000..7cda7943b59
--- /dev/null
+++ b/engines/scumm/smush/codec_ra2.cpp
@@ -0,0 +1,271 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 2 SMUSH video codecs
+
+#include "common/endian.h"
+
+#include "scumm/bomp.h"
+
+namespace Scumm {
+
+/**
+ * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
+ * Use this for background images where color 0 should NOT be treated as transparent.
+ * The standard smushDecodeRLE() treats color 0 as transparent, which is correct
+ * for overlay sprites but wrong for background images.
+ *
+ * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
+ */
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch;
+	do {
+		dst += left;
+		bompDecodeLine(dst, src + 2, width, true);  // setZero = TRUE to write all colors
+		src += READ_LE_UINT16(src) + 2;
+		dst += pitch - left;
+	} while (--height);
+}
+
+/**
+ * Codec 21/44: Line Update codec
+ * Used for fonts (NUT files) and some embedded HUD frames.
+ * Format: Each line has a 2-byte size header, then 2-byte skip, 2-byte count pairs with literal pixels.
+ * The count value needs +1 to get the actual number of pixels to copy.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch + left;
+
+	while (height--) {
+		byte *dstPtrNext = dst + pitch;
+		const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
+		src += 2;  // Skip line size header
+		int len = width;
+		byte *lineDst = dst;
+
+		while (len > 0) {
+			// Read 2-byte LE skip value
+			int skip = READ_LE_UINT16(src);
+			src += 2;
+			lineDst += skip;
+			len -= skip;
+			if (len <= 0)
+				break;
+
+			// Read 2-byte LE copy count (+1 for actual count)
+			int count = READ_LE_UINT16(src) + 1;
+			src += 2;
+			if (count > len)
+				count = len;
+			len -= count;
+
+			// Copy literal pixels
+			memcpy(lineDst, src, count);
+			lineDst += count;
+			src += count;
+		}
+		dst = dstPtrNext;
+		src = srcPtrNext;
+	}
+}
+
+/**
+ * Codec 23: Skip/Copy with embedded RLE
+ * Used for video frames with skip regions.
+ * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
+ * Note: Skip regions preserve previous frame content (delta compression).
+ */
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch + left;
+
+	for (int row = 0; row < height; row++) {
+		int lineDataSize = READ_LE_UINT16(src);
+		src += 2;
+		const byte *lineEnd = src + lineDataSize;
+		byte *lineDst = dst;
+		int x = 0;
+
+		while (src < lineEnd && x < width) {
+			int skip = READ_LE_UINT16(src);
+			src += 2;
+			x += skip;  // Skip preserves previous frame content
+			if (src >= lineEnd || x >= width)
+				break;
+
+			int runSize = READ_LE_UINT16(src);
+			src += 2;
+			const byte *runEnd = src + runSize;
+
+			// Decode RLE within this run - write ALL colors including 0
+			while (src < runEnd && x < width) {
+				byte code = *src++;
+				int num = (code >> 1) + 1;
+				if (num > width - x)
+					num = width - x;
+
+				if (code & 1) {
+					// RLE run - repeat color
+					byte color = (src < runEnd) ? *src++ : 0;
+					memset(lineDst + x, color, num);
+					x += num;
+				} else {
+					// Literal run - copy bytes
+					int toCopy = num;
+					if (toCopy > (int)(runEnd - src))
+						toCopy = (int)(runEnd - src);
+					memcpy(lineDst + x, src, toCopy);
+					src += toCopy;
+					x += toCopy;
+				}
+			}
+			src = runEnd;
+		}
+		src = lineEnd;
+		dst += pitch;
+	}
+}
+
+/**
+ * Codec 45: RA2-specific BOMP RLE with variable header
+ * Used for embedded ANIM frames, particularly small animation elements.
+ * Has a variable-length header (commonly 6 bytes starting with 01 FE).
+ * Note: For overlay sprites, color 0 is treated as transparent.
+ */
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	dst += top * pitch + left;
+
+	// Detect header pattern and find RLE data start
+	int headerSkip = 0;
+
+	// Check for common 6-byte header pattern: 01 FE XX XX XX XX
+	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
+		headerSkip = 6;
+	} else {
+		// Probe offsets to find valid RLE start
+		// Valid start should have reasonable line size values
+		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
+			int testLineSize = READ_LE_UINT16(src + testOffset);
+			// A valid line size should be positive and reasonable for the width
+			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
+				// Further validation: try to count valid line sizes
+				int linesTest = 0;
+				const byte *testPtr = src + testOffset;
+				bool validSum = true;
+
+				while (linesTest < height && testPtr + 2 <= src + dataSize) {
+					int ls = READ_LE_UINT16(testPtr);
+					if (ls <= 0 || ls > width * 2) {
+						validSum = false;
+						break;
+					}
+					testPtr += ls + 2;
+					linesTest++;
+				}
+
+				if (validSum && linesTest >= height - 1) {
+					headerSkip = testOffset;
+					break;
+				}
+			}
+		}
+	}
+
+	src += headerSkip;
+	const byte *dataEnd = src + (dataSize - headerSkip);
+
+	// Check first value to determine per-line vs continuous mode
+	int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
+	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
+
+	if (perLineMode) {
+		// Per-line RLE with 2-byte size headers
+		for (int row = 0; row < height && src < dataEnd; row++) {
+			int lineSize = READ_LE_UINT16(src);
+			src += 2;
+			if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
+				break;
+
+			const byte *lineEnd = src + lineSize;
+			byte *rowDst = dst + row * pitch;
+			int x = 0;
+
+			while (src < lineEnd && x < width) {
+				byte ctrl = *src++;
+				int count = (ctrl >> 1) + 1;
+
+				if (ctrl & 1) {
+					// RLE fill - color 0 is transparent for overlay sprites
+					byte color = (src < lineEnd) ? *src++ : 0;
+					if (color != 0) {
+						int num = (count > width - x) ? width - x : count;
+						memset(rowDst + x, color, num);
+					}
+					x += count;
+					if (x > width)
+						x = width;
+				} else {
+					// Literal copy - color 0 is transparent for overlay sprites
+					for (int i = 0; i < count && x < width && src < lineEnd; i++) {
+						byte color = *src++;
+						if (color != 0)
+							rowDst[x] = color;
+						x++;
+					}
+				}
+			}
+			src = lineEnd;
+		}
+	} else {
+		// Continuous BOMP RLE (no per-line headers)
+		for (int row = 0; row < height && src < dataEnd; row++) {
+			byte *rowDst = dst + row * pitch;
+			int x = 0;
+
+			while (x < width && src < dataEnd) {
+				byte ctrl = *src++;
+				int count = (ctrl >> 1) + 1;
+
+				if (ctrl & 1) {
+					// RLE fill - color 0 is transparent for overlay sprites
+					byte color = (src < dataEnd) ? *src++ : 0;
+					if (color != 0) {
+						int num = (count > width - x) ? width - x : count;
+						memset(rowDst + x, color, num);
+					}
+					x += count;
+					if (x > width)
+						x = width;
+				} else {
+					// Literal copy - color 0 is transparent for overlay sprites
+					for (int i = 0; i < count && x < width && src < dataEnd; i++) {
+						byte color = *src++;
+						if (color != 0)
+							rowDst[x] = color;
+						x++;
+					}
+				}
+			}
+		}
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 4ca180b97c7..e0391672c68 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,7 +40,6 @@
 #include "scumm/smush/codec37.h"
 #include "scumm/smush/codec47.h"
 #include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
@@ -257,35 +256,15 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_frameBuffer = nullptr;
 	_specialBuffer = nullptr;
 	_specialBufferSize = 0;
-	_storedFobjData = nullptr;
-	_storedFobjDataSize = 0;
-	_storedFobjCodec = 0;
-	_storedFobjLeft = 0;
-	_storedFobjTop = 0;
-	_storedFobjWidth = 0;
-	_storedFobjHeight = 0;
 
 	_seekPos = -1;
 
-	_skipNext = false;
-	_ra2FastForwarding = false;
-	_fobjOffsetX = 0;
-	_fobjOffsetY = 0;
 	_dst = nullptr;
-	_storeFrame = false;
 	_compressedFileMode = false;
 	_width = 0;
 	_height = 0;
 
-	// LOAD chunk streaming buffer (RA2)
-	_loadBuffer = nullptr;
-	_loadBufferSize = 0;
-	_loadBufferOffset = 0;
-	_loadReadOffset = 8;  // Original starts reading at offset 8 (skips header)
-	_lastLoadChunkIdx = -1;
-	_totalLoadChunks = 0;
-	_scrollX = 0;
-	_scrollY = 0;
+	ra2InitFields();
 	_IACTpos = 0;
 	_speed = -1;
 	_insanity = false;
@@ -314,11 +293,9 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_smushAudioInitialized = false;
 	_smushAudioCallbackEnabled = false;
 
-	// Rebel Assault 2 doesn't use iMUSE for audio, so _imuseDigital may be null
 	if (_imuseDigital) {
 		initAudio(_imuseDigital->getSampleRate(), 200000);
 	} else {
-		// RA2 audio is 11025 Hz
 		initAudio(11025, 200000);
 	}
 }
@@ -326,19 +303,13 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 SmushPlayer::~SmushPlayer() {
 	delete _IACTchannel;
 	delete _compressedFileSoundHandle;
-	delete _multiFont;
-	_multiFont = nullptr;
+	ra2DestroyFields();
 	terminateAudio();
 
-	// Free any preserved frame buffer (RA2 preserves this across videos)
 	free(_frameBuffer);
 	_frameBuffer = nullptr;
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
-	free(_storedFobjData);
-	_storedFobjData = nullptr;
-	free(_loadBuffer);
-	_loadBuffer = nullptr;
 }
 
 void SmushPlayer::init(int32 speed) {
@@ -348,11 +319,6 @@ void SmushPlayer::init(int32 speed) {
 	_speed = speed;
 	_endOfFile = false;
 	_storeFrame = false;
-
-	// Reset FOBJ offsets between videos. These are set per-video by
-	// procPreRendering (chapter select preview scrolling) or IACT opcode 6
-	// (corridor overlay positioning). Without this, offsets from O_LEVEL.SAN
-	// would persist and shift FOBJs in subsequent videos → buffer overflows.
 	_fobjOffsetX = 0;
 	_fobjOffsetY = 0;
 
@@ -362,39 +328,8 @@ void SmushPlayer::init(int32 speed) {
 	_vm->setDirtyColors(0, 255);
 	_dst = vs->getPixels(0, 0);
 
-	// For RA2: Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
-	// have no NPAL chunk and inherit the palette from the previous video. Since
-	// play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
-	// This is safe for videos WITH NPAL too — the NPAL handler immediately
-	// overwrites _pal and re-marks dirty.
-	if (_vm->_game.id == GID_REBEL2) {
-		setDirtyColors(0, 255);
-	}
-
-	// For Rebel Assault 2, handle background preservation between videos:
-	// - Cinematic videos (flags 0x20) clear the buffer for a fresh start
-	// - Gameplay videos (flags 0x28) preserve the existing screen content
-	//
-	// The virtual screen (_dst = vs->getPixels) persists between videos, so
-	// when a gameplay video starts, _dst already contains the last frame of
-	// the previous cinematic video - which is exactly what we want.
-	// We do NOT restore from _frameBuffer because STOR captures frame 0
-	// (often a black initialization frame), not the last frame.
-	if (_vm->_game.id == GID_REBEL2 && _dst != nullptr) {
-		if ((_curVideoFlags & 0x08) == 0) {
-			// Cinematic mode (flags 0x20) - clear buffer for fresh video
-			memset(_dst, 0, vs->w * vs->h);
-		} else {
-			// Gameplay mode (flags 0x28) - do nothing, preserve existing screen content
-			// Count non-zero pixels to verify there's actual content
-			int nonZero = 0;
-			for (int i = 0; i < vs->w * vs->h; i++) {
-				if (_dst[i] != 0) nonZero++;
-			}
-			debug("SmushPlayer::init: Preserving screen for gameplay video (%dx%d, %d%% non-zero)",
-				vs->w, vs->h, (nonZero * 100) / (vs->w * vs->h));
-		}
-	}
+	if (isRA2())
+		ra2InitVideo();
 
 	// HACK HACK HACK: This is an *evil* trick, beware!
 	// We do this to fix bug #1792. A proper solution would change all the
@@ -429,15 +364,9 @@ void SmushPlayer::release() {
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
 
-	free(_storedFobjData);
-	_storedFobjData = nullptr;
-	_storedFobjDataSize = 0;
-
-	// For Rebel Assault 2, preserve _frameBuffer across videos so that
-	// gameplay videos (which have no background FOBJ) can use the stored
-	// background from the previous BEG cinematic video.
-	// The _frameBuffer will be freed in the destructor or reused by the next video.
-	if (_vm->_game.id != GID_REBEL2) {
+	if (isRA2()) {
+		ra2ReleaseVideo();
+	} else {
 		free(_frameBuffer);
 		_frameBuffer = nullptr;
 	}
@@ -468,37 +397,8 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
 	assert(subSize >= 6);
 
-	// Read FTCH data: 2 unknown + 2 X offset + 2 Y offset
-	// Original (FUN_00423A50 lines 91-103): reads X/Y from chunk data and
-	// re-renders stored FOBJ at position (X + param_3 + DAT_00482c1c, Y + param_4 + DAT_00482c20)
-	int16 ftchUnknown = b.readSint16LE();
-	int16 ftchX = b.readSint16LE();
-	int16 ftchY = b.readSint16LE();
-
-	debug("SmushPlayer::handleFetch: frame=%d unknown=%d x=%d y=%d",
-		_frame, ftchUnknown, ftchX, ftchY);
-
-	// For RA2 Handler 25, skip FTCH because the frame buffer only contains the
-	// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
-	// immediately in frame 0. Restoring would erase those overlays and make
-	// enemies invisible since they draw on top of the erased areas.
-	if (_vm->_game.id == GID_REBEL2 && _insane != nullptr) {
-		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
-		int handler = rebel2->getHandler();
-		if (handler == 25) {
-			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
-			return;
-		}
-	}
-
-	// RA2: Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
-	// The stored FOBJ's original position is combined with the current _fobjOffsetX/Y,
-	// so scrolling the chapter preview works correctly on each FTCH.
-	if (_vm->_game.id == GID_REBEL2 && _storedFobjData != nullptr) {
-		decodeFrameObject(_storedFobjCodec, _storedFobjData,
-			_storedFobjLeft, _storedFobjTop,
-			_storedFobjWidth, _storedFobjHeight,
-			_storedFobjDataSize);
+	if (isRA2()) {
+		ra2HandleFetch(b);
 		return;
 	}
 
@@ -507,124 +407,23 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	}
 }
 
-/**
- * Handle LOAD chunk for Rebel Assault 2
- *
- * LOAD chunks stream embedded resource data (likely audio for streaming playback)
- * across multiple frames. The data is accumulated in a buffer and consumed by
- * the audio system.
- *
- * Chunk format (from FUN_00424450 in original):
- *   Offset 0 (2 bytes): totalChunks - Total number of LOAD chunks in sequence
- *   Offset 2 (2 bytes): chunkIndex - Current chunk index (0-based)
- *   Offset 4 (6 bytes): unknown/padding
- *   Offset 10+: Actual data payload
- *
- * Processing:
- *   - First chunk (index 0) resets the buffer
- *   - Chunks must arrive sequentially (lastIndex + 1 == currentIndex)
- *   - Data is accumulated until all chunks are received
- *   - Consumer reads from buffer via getLoadData() (DAT_00482c18 read offset)
- */
-void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
-	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
-
-	if (subSize < 10) {
-		warning("SmushPlayer::handleLoad: chunk too small (%d bytes)", subSize);
-		return;
-	}
-
-	// Read LOAD header
-	int16 totalChunks = b.readUint16LE();
-	int16 chunkIndex = b.readUint16LE();
-	b.skip(6);  // Unknown/padding
-
-	int32 dataSize = subSize - 10;
-
-	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: chunk %d/%d, dataSize=%d, bufferOffset=%d",
-		chunkIndex, totalChunks, dataSize, _loadBufferOffset);
-
-	// First chunk in sequence - reset buffer state
-	if (chunkIndex == 0) {
-		_loadBufferOffset = 0;
-		_loadReadOffset = 8;  // Original skips 8 bytes at start (header in accumulated data?)
-		_lastLoadChunkIdx = -1;
-		_totalLoadChunks = totalChunks;
-
-		// Allocate/reallocate buffer if needed
-		// LOAD data per chunk varies (up to ~500 bytes observed).
-		// Allocate generously to avoid overflow.
-		int32 estimatedSize = totalChunks * 600;
-		if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
-			free(_loadBuffer);
-			_loadBufferSize = estimatedSize;
-			_loadBuffer = (byte *)malloc(_loadBufferSize);
-			if (_loadBuffer == nullptr) {
-				warning("SmushPlayer::handleLoad: Failed to allocate %d bytes for LOAD buffer",
-					_loadBufferSize);
-				_loadBufferSize = 0;
-				return;
-			}
-			debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Allocated %d bytes for LOAD buffer",
-				_loadBufferSize);
-		}
-	}
-
-	// Check sequential order (original: DAT_00482c3c - sVar1 == -1)
-	if (_lastLoadChunkIdx + 1 != chunkIndex) {
-		debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Non-sequential chunk %d (expected %d), skipping",
-			chunkIndex, _lastLoadChunkIdx + 1);
-		return;
-	}
-
-	// Check buffer capacity (original: DAT_00482c14 + param_2 < DAT_00482c10)
-	if (_loadBuffer == nullptr || _loadBufferOffset + dataSize >= _loadBufferSize) {
-		warning("SmushPlayer::handleLoad: Buffer overflow - offset=%d size=%d limit=%d",
-			_loadBufferOffset, dataSize, _loadBufferSize);
-		return;
-	}
-
-	// Copy data to buffer
-	b.read(_loadBuffer + _loadBufferOffset, dataSize);
-	_loadBufferOffset += dataSize;
-	_lastLoadChunkIdx = chunkIndex;
-
-	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Accumulated %d bytes total", _loadBufferOffset);
-
-	// Check if sequence is complete
-	if (chunkIndex == totalChunks - 1) {
-		debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Sequence complete - %d chunks, %d bytes total",
-			totalChunks, _loadBufferOffset);
-	}
-}
-
 void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::IACT()");
 	assert(subSize >= 8);
 
-	// Embedded SAN detection moved to InsaneRebel2::iactRebel2Scene1
-	// (previously handled here in SmushPlayer; centralized to the engine)
-
-
 	int code = b.readUint16LE();
 	int flags = b.readUint16LE();
 	int unknown = b.readSint16LE();
 	int userId = b.readUint16LE();
 
-	debug("SmushPlayer::handleIACT: code=%d flags=%d unknown=%d userId=%d subSize=%d",
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleIACT: code=%d flags=%d unknown=%d userId=%d subSize=%d",
 		code, flags, unknown, userId, subSize);
 
-	// Route to procIACT for:
-	// 1. Non-audio IACT (code != 8 or flags != 46) - Full Throttle uses code=8, flags=46 for audio
-	// 2. ALL Rebel Assault 2 IACTs - RA2 uses a different IACT format where code=opcode, flags=par2
-	//    RA2 audio is handled through PSAD chunks, not IACT, so all RA2 IACTs go to procIACT
+	// Route to procIACT for non-audio IACTs and ALL RA2 IACTs
+	// (RA2 audio uses PSAD chunks, not IACT)
 	bool isAudioIACT = (code == 8) && (flags == 46);
-	bool isRA2 = (_vm->_game.id == GID_REBEL2);
 
-	if (!isAudioIACT || isRA2) {
-		debug("SmushPlayer::handleIACT: Routing to procIACT (isAudioIACT=%d, isRA2=%d)",
-			isAudioIACT, isRA2);
-		// Pass subSize - 8 as the remaining payload size (after 8-byte header)
+	if (!isAudioIACT || isRA2()) {
 		_vm->_insane->procIACT(_dst, 0, 0, 0, b, subSize - 8, 0, code, flags, unknown, userId);
 		return;
 	}
@@ -847,7 +646,7 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 	// RA2: The original game always shows subtitle text during cinematics
 	// (there is no subtitle toggle in the retail options menu). Skip
 	// this check so TRES text is always rendered.
-	if (_vm->_game.id != GID_REBEL2 && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
+	if (!isRA2() && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
 		return;
 
 	bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
@@ -915,28 +714,9 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 	// bit 7 - skip ^ codes (COMI)     0x80        (should be irrelevant for Smush, we strip these commands anyway)
 	// bit 8 - no vertical fix (COMI)  0x100       (COMI handles this in the printing method, but I haven't seen a case where it is used)
 
-	// For Rebel Assault 2, use SmushMultiFont to support inline font switching via ^fXX codes.
-	// The original game uses a linked list of fonts and switches between them mid-string.
-	// Other games (FT, DIG, CMI) only use ^f codes at the start of strings.
-	if (_vm->_game.id == GID_REBEL2) {
-		// Create multi-font renderer on first use
-		if (!_multiFont) {
-			_multiFont = new SmushMultiFont(_vm, this, true);
-		}
-		_multiFont->setDefaultFont(fontId);
-
-		debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
-			  _frame, fontId, color, flags, (int)flg, pos_x, pos_y, left, top, width, height, str);
-
-		if (flg & kStyleWordWrap) {
-			Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
-			_multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
-		} else {
-			Common::Rect clipRect(0, 0, _width, _height);
-			_multiFont->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
-		}
+	if (isRA2()) {
+		ra2HandleTextResource(str, fontId, color, pos_x, pos_y, left, top, width, height, flg);
 	} else {
-		// For other games, use single font (original behavior)
 		SmushFont *sf = getFont(fontId);
 		assert(sf != nullptr);
 
@@ -1044,9 +824,6 @@ byte *SmushPlayer::getVideoPalette() {
 
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 
 void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
 	if ((height == 242) && (width == 384)) {
@@ -1057,62 +834,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 			_specialBufferSize = bufSize;
 		}
 		_dst = _specialBuffer;
-	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		// Rebel2 uses SKIP chunks to conditionally skip FOBJ frames for destroyed enemies.
-		// The SKIP chunk mechanism (via procSKIP -> _skipNext) is checked at the START
-		// of handleFrameObject(), so destroyed enemy sprites are already skipped before
-		// reaching this point. No additional skip logic needed here.
-		//
-		// Rebel2 uses a special buffer for all non-matching frames.
-		// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
-		// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null
-		//          because first frames are small and don't need oversized buffer)
-		// Only allocate/expand buffer for frames LARGER than current buffer or screen
-		int bufSize = width * height;
-		if (bufSize > _vm->_screenWidth * _vm->_screenHeight) {
-			// Frame is larger than screen - need special buffer
-			if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
-				free(_specialBuffer);
-				_specialBuffer = (byte *)malloc(bufSize);
-				_specialBufferSize = bufSize;
-				_width = width;
-				_height = height;
-			}
-		}
-		// Use special buffer ONLY for oversized frames that need it.
-		// Small enemy sprites (like Level 2's 9x38 stormtroopers) should draw
-		// directly to the virtual screen, not to _specialBuffer.
-		// The special buffer is only needed when the CURRENT frame is larger than screen.
-		if (bufSize > _vm->_screenWidth * _vm->_screenHeight &&
-		    _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
-			_dst = _specialBuffer;
-			debug("SmushPlayer: Using _specialBuffer for oversized FOBJ %dx%d", width, height);
-		} else {
-			// For small RA2 sprites, check if we should use _specialBuffer or virtual screen.
-			//
-			// If _specialBuffer was allocated in this video (by a larger frame like Level 1's
-			// 424x260 background), small sprites should use it too so everything composites
-			// in the same buffer.
-			//
-			// If _specialBuffer is null (no large frames in this video, like Level 2), small
-			// sprites should use the virtual screen directly.
-			//
-			// This is important because release() frees _specialBuffer at the end of each video,
-			// so a new video starts with _specialBuffer = nullptr. Without this check, small
-			// sprites in Level 2 could incorrectly use a stale _specialBuffer pointer (though
-			// release() should have freed it, init() might not reset _dst if video flags are set).
-			if (_specialBuffer == nullptr) {
-				VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
-				_dst = vs->getPixels(0, 0);
-				debug("SmushPlayer: Reset _dst to virtual screen for FOBJ %dx%d at (%d,%d) _dst=%p",
-					width, height, left, top, (void*)_dst);
-			} else {
-				// Large frame was in this video, use _specialBuffer for compositing
-				_dst = _specialBuffer;
-				debug("SmushPlayer: Using _specialBuffer for small FOBJ %dx%d (compositing with large frame)",
-					width, height);
-			}
-		}
+	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+		ra2SelectFrameBuffer(width, height);
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;
 	// FT Insane uses smaller frames to draw overlays with moving objects
@@ -1124,10 +847,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if ((height == 242) && (width == 384)) {
 		_width = width;
 		_height = height;
-	} else if (_vm->_game.id == GID_REBEL2 && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		// Do not update _width/_height here - preserve original video size set during
-		// buffer allocation. Small overlay sprites (e.g., 8x7) need to use the background
-		// frame's pitch (424) to be drawn at the correct position in the buffer.
+	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
+		// RA2: preserve _width/_height set during buffer allocation
 	} else {
 		_width = _vm->_screenWidth;
 		_height = _vm->_screenHeight;
@@ -1137,31 +858,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if (_dst == _specialBuffer)
 		pitch = _width;
 
-	// RA2: Apply global FOBJ position offsets (matching original FUN_00423A50)
-	// These are set by InsaneRebel2 during IACT opcode 6 processing
-	if (_vm->_game.id == GID_REBEL2) {
-		left += _fobjOffsetX;
-		top += _fobjOffsetY;
-	}
-
-	// Bounds check: clamp FOBJ to destination buffer dimensions
-	int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
-	if (top < 0) {
-		height += top;
-		top = 0;
-	}
-	if (left < 0) {
-		width += left;
-		left = 0;
-	}
-	if (top + height > bufHeight) {
-		height = bufHeight - top;
-	}
-	if (left + width > pitch) {
-		width = pitch - left;
+	if (isRA2()) {
+		ra2AdjustFrameCoords(left, top, width, height, pitch);
+		if (width <= 0 || height <= 0)
+			return;
 	}
-	if (width <= 0 || height <= 0)
-		return;
 
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
@@ -1181,25 +882,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 			_deltaGlyphsCodec->decode(_dst, src);
 		break;
 	case SMUSH_CODEC_UNCOMPRESSED:
-		// Used by Full Throttle Classic (from Remastered)
 		smushDecodeUncompressed(_dst, src, left, top, width, height, _vm->_screenWidth);
 		break;
-	case SMUSH_CODEC_LINE_UPDATE:
-	case SMUSH_CODEC_LINE_UPDATE2:
-		// RA2: Skip/copy with literal pixels (used for fonts and HUD overlays)
-		smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
-		break;
-	case SMUSH_CODEC_SKIP_RLE:
-		// RA2: Skip/copy with embedded RLE (used for HUD frames with transparency)
-		smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
-		break;
-	case SMUSH_CODEC_RA2_BOMP:
-		// RA2: BOMP RLE with variable header (used for small animation elements)
-		smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
-		break;
 	default:
-		if (_vm->_game.id == GID_REBEL2) {
-			// Rebel Assault 2 may have other unknown codecs
+		if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
+			break;
+		if (isRA2()) {
 			debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
 				codec, left, top, width, height);
 			break;
@@ -1207,9 +895,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		error("Invalid codec for frame object : %d", codec);
 	}
 
-	// For non-RA2 games, save rendered bitmap when STOR is pending.
-	// RA2 handles STOR in handleFrameObject by saving raw FOBJ data instead.
-	if (_storeFrame && _vm->_game.id != GID_REBEL2) {
+	if (_storeFrame && !isRA2()) {
 		if (_frameBuffer == nullptr) {
 			_frameBuffer = (byte *)malloc(_width * _height);
 		}
@@ -1251,7 +937,7 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
 void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
 	assert(subSize >= 14);
 	if (_skipNext) {
-		debug("SmushPlayer::handleFrameObject: SKIPPING due to _skipNext, frame=%d", _frame);
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: SKIPPING due to _skipNext, frame=%d", _frame);
 		_skipNext = false;
 		return;
 	}
@@ -1265,7 +951,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	b.readUint16LE();
 	b.readUint16LE();
 
-	debug("SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
 		_frame, codec, left, top, width, height, subSize - 14);
 
 	int32 chunk_size = subSize - 14;
@@ -1273,22 +959,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	assert(chunk_buffer);
 	b.read(chunk_buffer, chunk_size);
 
-	// RA2: When STOR is pending, save raw FOBJ data for later re-decoding by FTCH.
-	// The original (FUN_00423A50 bVar5) stores the next FOBJ's raw chunk data in
-	// DAT_00482c04. FTCH then re-renders from this stored data with current FOBJ
-	// offsets. This is critical for O_LEVEL.SAN where the stored FOBJ is the 80x800
-	// preview strip, and FTCH must re-render it at the current scroll offset.
-	if (_storeFrame && _vm->_game.id == GID_REBEL2) {
-		free(_storedFobjData);
-		_storedFobjData = (byte *)malloc(chunk_size);
-		memcpy(_storedFobjData, chunk_buffer, chunk_size);
-		_storedFobjDataSize = chunk_size;
-		_storedFobjCodec = codec;
-		_storedFobjLeft = left;
-		_storedFobjTop = top;
-		_storedFobjWidth = width;
-		_storedFobjHeight = height;
-		_storeFrame = false;
+	if (_storeFrame && isRA2()) {
+		ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
 	}
 
 	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
@@ -1310,19 +982,10 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		const int32 subSize = b.readUint32BE();
 		const int32 subOffset = b.pos();
 
-		// Debug: Log all chunk types for first 25 frames
-		if (_vm->_game.id == GID_REBEL2 && _frame < 25) {
-			debug("SmushPlayer::handleFrame: frame=%d chunk=%s size=%d", _frame, tag2str(subType), subSize);
-		}
-
-		// RA2 SKIP mechanism (matching original FUN_00423A50 bVar6):
-		// When _skipNext is set, skip the NEXT chunk of ANY type (FOBJ, PSAD, SKIP, etc.)
-		// In the original, bVar6 is checked at the top of the loop before the type switch,
-		// corrupting the tag so no handler matches. This consumes exactly one chunk.
-		// Critical: this must also skip SKIP chunks to prevent SKIP→SKIP→FOBJ misalignment.
-		if (_vm->_game.id == GID_REBEL2 && _skipNext) {
+		// RA2: When _skipNext is set, skip the NEXT chunk of ANY type
+		if (isRA2() && _skipNext) {
 			_skipNext = false;
-			debug("SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
 			frameSize -= subSize + 8;
 			b.seek(subOffset + subSize, SEEK_SET);
 			if (subSize & 1) {
@@ -1400,15 +1063,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
 	}
 
-	// Debug: Check if updateScreen is being called
-	if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
-		debug("SmushPlayer: frame=%d _width=%d _height=%d _dst=%p", _frame, _width, _height, (void*)_dst);
-	}
-
 	if (_width != 0 && _height != 0) {
 		updateScreen();
-	} else if (_vm->_game.id == GID_REBEL2 && _frame < 3) {
-		debug("SmushPlayer: SKIPPING updateScreen (width=%d height=%d)", _width, _height);
 	}
 
 	_frame++;
@@ -1416,7 +1072,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 
 void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleAnimHeader()");
-	debug("SmushPlayer::handleAnimHeader: subSize=%d", subSize);
 	assert(subSize >= 0x300 + 6);
 	byte *headerContent = (byte *)malloc(subSize * sizeof(byte));
 
@@ -1447,31 +1102,17 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			byte *palettePtr = &headerContent[6];
 			memcpy(_pal, palettePtr, sizeof(_pal));
 
-			// Reset XPAL delta palette state from the new base palette.
-			// Without this, stale _deltaPal/_shiftedDeltaPal values from a
-			// previous video leak into the new one, corrupting the palette
-			// when XPAL command 256 (apply deltas) is encountered.
-			for (int j = 0; j < 768; ++j) {
-				_shiftedDeltaPal[j] = _pal[j] << 7;
-			}
-			memset(_deltaPal, 0, sizeof(_deltaPal));
+			if (isRA2())
+				ra2ResetDeltaPalette();
 
 			setDirtyColors(0, 255);
 		}
 
 		_width = READ_LE_UINT16(&headerContent[4]);
 		_height = READ_LE_UINT16(&headerContent[6]);
-		debug("SmushPlayer::handleAnimHeader: nbframes=%d width=%d height=%d", _nbframes, _width, _height);
-
-		// RA2 menu videos (O_MENU*.SAN) report width/height=0 in AHDR, but they DO have
-		// FOBJ frames with full 320x200 images. The FOBJ chunks contain the correct dimensions.
-		// We set default screen dimensions here so updateScreen() gets called and the
-		// _frameBuffer allocation in handleStore/handleFetch uses correct size.
-		if (_vm->_game.id == GID_REBEL2 && _width == 0 && _height == 0) {
-			_width = _vm->_screenWidth;   // 320
-			_height = _vm->_screenHeight; // 200
-			debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
-		}
+
+		if (isRA2())
+			ra2FixupAnimHeader();
 
 		free(headerContent);
 	}
@@ -1479,15 +1120,8 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 
 void SmushPlayer::setupAnim(const char *file) {
 	if (_insanity) {
-		if (_vm->_game.id == GID_REBEL2) {
-			// Rebel Assault 2 uses SYSTM/GAME.TRS for all subtitle strings
-			// The TRS file is ETRS-encoded (XOR with 0xCC)
+		if (isRA2()) {
 			_strings = getStrings(_vm, "SYSTM/GAME.TRS", true);
-			if (_strings) {
-				debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Loaded GAME.TRS string resources successfully");
-			} else {
-				debugC(DEBUG_SMUSH, "SmushPlayer::setupAnim: Failed to load GAME.TRS!");
-			}
 		} else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
 			readString("mineroad.trs");
 		}
@@ -1518,26 +1152,8 @@ SmushFont *SmushPlayer::getFont(int font) {
 
 			_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
 		}
-	} else if (_vm->_game.id == GID_REBEL2) {
-		// Rebel Assault 2 fonts:
-		// font 0: TALKFONT.NUT - main dialog/subtitle font
-		// font 1: DIHIFONT.NUT - high-res dialog font
-		// font 2: TITLFONT.NUT - title font
-		// font 3: SMALFONT.NUT - small font for HUD
-		const char *ra2_fonts[] = {
-			"SYSTM/TALKFONT.NUT",
-			"SYSTM/DIHIFONT.NUT",
-			"SYSTM/TITLFONT.NUT",
-			"SYSTM/SMALFONT.NUT"
-		};
-		int numFonts = ARRAYSIZE(ra2_fonts);
-		if (font >= 0 && font < numFonts) {
-			_sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
-		} else {
-			// Fallback to font 0 for unknown font indices
-			debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
-			_sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
-		}
+	} else if (isRA2()) {
+		return ra2GetFont(font);
 	} else {
 		int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
 		assert(font >= 0 && font < numFonts);
@@ -1627,12 +1243,8 @@ void SmushPlayer::parseNextFrame() {
 	if (_vm->_imuseDigital)
 		_vm->_imuseDigital->flushTracks();
 
-	// Rebel Assault 2 audio processing - call processDispatches directly since no iMUSE
-	if (!_imuseDigital && _vm->_game.id == GID_REBEL2) {
-		// Use a feed size based on frame rate (similar to iMUSE)
-		// 11025 Hz / 12 fps = ~918 samples per frame
-		processDispatches(_smushAudioSampleRate / 12);
-	}
+	if (!_imuseDigital && isRA2())
+		ra2ParseNextFrame();
 }
 
 void SmushPlayer::setPalette(const byte *palette) {
@@ -1749,9 +1361,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 	// Hide mouse
 	bool oldMouseState = CursorMan.showMouse(false);
-	if (_vm->_game.id == GID_REBEL2) {
+	if (isRA2())
 		insanity(true);
-	}
 
 	// Load the video
 	_seekFile = filename;
@@ -1908,7 +1519,6 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 void SmushPlayer::initAudio(int samplerate, int32 maxChunkSize) {
 	int32 maxSizes[SMUSH_MAX_TRACKS] = {100000, 100000, 100000, 400000};
 
-	// Rebel Assault 2 doesn't use iMUSE for audio
 	if (_imuseDigital)
 		_imuseDigital->setSmushPlayer(this);
 
@@ -2206,9 +1816,8 @@ void SmushPlayer::processDispatches(int16 feedSize) {
 	bool isPlayableTrack;
 	bool speechIsPlaying = false;
 
-	// Rebel Assault 2 doesn't use iMUSE for audio - use InsaneRebel2's audio handler
 	if (!_imuseDigital) {
-		if (_vm->_game.id == GID_REBEL2 && _insane) {
+		if (isRA2() && _insane) {
 			InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
 			rebel2->processAudioFrame(feedSize);
 		}
@@ -2644,7 +2253,6 @@ bool SmushPlayer::processAudioCodes(int idx, int32 &tmpFeedSize, int &mixVolume)
 }
 
 void SmushPlayer::sendAudioToDiMUSE(uint8 *mixBuf, int32 mixStartingPoint, int32 mixFeedSize, int32 mixInFrameCount, int volume, int pan) {
-	// Rebel Assault 2 doesn't use iMUSE for audio
 	if (!_imuseDigital)
 		return;
 
@@ -2716,36 +2324,4 @@ bool SmushPlayer::isAudioCallbackEnabled() {
 	return _smushAudioCallbackEnabled;
 }
 
-// Only used by Rebel Assault 2
-void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
-	// Check if the region already exists
-	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
-		if (*it == rect) {
-			return; // Already exists
-		}
-	}
-	_maskedRegions.push_back(rect);
-}
-
-// Only used by Rebel Assault 2
-void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
-	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
-		if (*it == rect) {
-			_maskedRegions.erase(it);
-			return;
-		}
-	}
-}
-
-// Only used by Rebel Assault 2
-void SmushPlayer::clearMaskedRegions() {
-	_maskedRegions.clear();
-}
-
-// Only used by Rebel Assault 2
-void SmushPlayer::setScrollOffset(int x, int y) { 
-	_scrollX = x;
-	_scrollY = y;
-}
-
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 2ebd816193b..85d6b60edbd 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -25,6 +25,7 @@
 #include "common/util.h"
 #include "common/list.h"
 #include "common/rect.h"
+#include "scumm/charset_v7.h"
 
 namespace Audio {
 class SoundHandle;
@@ -289,9 +290,30 @@ private:
 	void handleIACT(int32 subSize, Common::SeekableReadStream &);
 	void handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &);
 	void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
-	void handleLoad(int32 subSize, Common::SeekableReadStream &);
+	void handleLoad(int32 subSize, Common::SeekableReadStream &);  // RA2 only (impl in smush_player_ra2.cpp)
 	void readPalette(byte *, Common::SeekableReadStream &);
 
+	// RA2-specific methods (implemented in smush_player_ra2.cpp)
+	bool isRA2() const;
+	void ra2InitFields();
+	void ra2DestroyFields();
+	void ra2InitVideo();
+	void ra2ReleaseVideo();
+	void ra2HandleFetch(Common::SeekableReadStream &b);
+	void ra2HandleTextResource(const char *str, int fontId, int color,
+							   int pos_x, int pos_y, int left, int top,
+							   int width, int height, TextStyleFlags flg);
+	void ra2SelectFrameBuffer(int width, int height);
+	void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch);
+	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+						int width, int height, int pitch, int dataSize);
+	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+						  int left, int top, int width, int height);
+	void ra2ResetDeltaPalette();
+	SmushFont *ra2GetFont(int font);
+	void ra2ParseNextFrame();
+	void ra2FixupAnimHeader();
+
 	// LOAD chunk streaming buffer (RA2 - embedded resource data)
 	byte *_loadBuffer;        // Accumulated LOAD data
 	int32 _loadBufferSize;    // Allocated buffer size
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
new file mode 100644
index 00000000000..198ca7c200f
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -0,0 +1,449 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 2 specific SmushPlayer methods
+//
+// These are methods of the SmushPlayer class that contain RA2-specific logic.
+// Keeping them in a separate file minimizes the diff on smush_player.cpp and
+// reduces the risk of regressions in Full Throttle / The Dig / CMI.
+
+#include "common/endian.h"
+#include "common/rect.h"
+#include "common/system.h"
+
+#include "scumm/scumm.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane.h"
+#include "scumm/insane/insane_rebel.h"
+
+namespace Scumm {
+
+bool SmushPlayer::isRA2() const {
+	return _vm->_game.id == GID_REBEL2;
+}
+
+// Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+
+/**
+ * Initialize RA2-specific fields in the SmushPlayer constructor.
+ */
+void SmushPlayer::ra2InitFields() {
+	_multiFont = nullptr;
+	_storedFobjData = nullptr;
+	_storedFobjDataSize = 0;
+	_storedFobjCodec = 0;
+	_storedFobjLeft = 0;
+	_storedFobjTop = 0;
+	_storedFobjWidth = 0;
+	_storedFobjHeight = 0;
+	_skipNext = false;
+	_ra2FastForwarding = false;
+	_fobjOffsetX = 0;
+	_fobjOffsetY = 0;
+	_storeFrame = false;
+	_loadBuffer = nullptr;
+	_loadBufferSize = 0;
+	_loadBufferOffset = 0;
+	_loadReadOffset = 8;  // Original starts reading at offset 8 (skips header)
+	_lastLoadChunkIdx = -1;
+	_totalLoadChunks = 0;
+	_scrollX = 0;
+	_scrollY = 0;
+}
+
+/**
+ * Free RA2-specific resources in the SmushPlayer destructor.
+ */
+void SmushPlayer::ra2DestroyFields() {
+	delete _multiFont;
+	_multiFont = nullptr;
+	free(_storedFobjData);
+	_storedFobjData = nullptr;
+	free(_loadBuffer);
+	_loadBuffer = nullptr;
+}
+
+/**
+ * RA2-specific initialization in SmushPlayer::init().
+ * Re-pushes the SMUSH palette (videos without NPAL inherit from previous),
+ * and handles background preservation between cinematic and gameplay videos.
+ */
+void SmushPlayer::ra2InitVideo() {
+	// Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
+	// have no NPAL chunk and inherit the palette from the previous video.
+	// Since play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
+	setDirtyColors(0, 255);
+
+	// Handle background preservation between videos:
+	// - Cinematic videos (flags 0x20) clear the buffer for a fresh start
+	// - Gameplay videos (flags 0x28) preserve the existing screen content
+	if (_dst != nullptr) {
+		VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+		if ((_curVideoFlags & 0x08) == 0) {
+			// Cinematic mode (flags 0x20) - clear buffer for fresh video
+			memset(_dst, 0, vs->w * vs->h);
+		} else {
+			// Gameplay mode (flags 0x28) - do nothing, preserve existing screen content
+			int nonZero = 0;
+			for (int i = 0; i < vs->w * vs->h; i++) {
+				if (_dst[i] != 0) nonZero++;
+			}
+			debug("SmushPlayer::init: Preserving screen for gameplay video (%dx%d, %d%% non-zero)",
+				vs->w, vs->h, (nonZero * 100) / (vs->w * vs->h));
+		}
+	}
+}
+
+/**
+ * RA2-specific cleanup in SmushPlayer::release().
+ * Frees stored FOBJ data but preserves _frameBuffer across videos.
+ */
+void SmushPlayer::ra2ReleaseVideo() {
+	free(_storedFobjData);
+	_storedFobjData = nullptr;
+	_storedFobjDataSize = 0;
+	// Preserve _frameBuffer across videos so that gameplay videos (which have no
+	// background FOBJ) can use the stored background from the previous BEG video.
+}
+
+/**
+ * RA2-specific FTCH handling.
+ * For Handler 25, skips FTCH to preserve overlays.
+ * For other handlers, re-decodes stored FOBJ with current offsets.
+ */
+void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
+	int16 ftchUnknown = b.readSint16LE();
+	int16 ftchX = b.readSint16LE();
+	int16 ftchY = b.readSint16LE();
+
+	debug("SmushPlayer::handleFetch: frame=%d unknown=%d x=%d y=%d",
+		_frame, ftchUnknown, ftchX, ftchY);
+
+	// For Handler 25, skip FTCH because the frame buffer only contains the
+	// par4=5 base background without the overlays (par4=4, 6, 7) that were drawn
+	// immediately in frame 0. Restoring would erase those overlays.
+	if (_insane != nullptr) {
+		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+		int handler = rebel2->getHandler();
+		if (handler == 25) {
+			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
+			return;
+		}
+	}
+
+	// Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
+	if (_storedFobjData != nullptr) {
+		decodeFrameObject(_storedFobjCodec, _storedFobjData,
+			_storedFobjLeft, _storedFobjTop,
+			_storedFobjWidth, _storedFobjHeight,
+			_storedFobjDataSize);
+	}
+}
+
+/**
+ * Handle LOAD chunk for Rebel Assault 2.
+ *
+ * LOAD chunks stream embedded resource data across multiple frames.
+ * The data is accumulated in a buffer and consumed by the audio system.
+ */
+void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
+
+	if (subSize < 10) {
+		warning("SmushPlayer::handleLoad: chunk too small (%d bytes)", subSize);
+		return;
+	}
+
+	int16 totalChunks = b.readUint16LE();
+	int16 chunkIndex = b.readUint16LE();
+	b.skip(6);  // Unknown/padding
+
+	int32 dataSize = subSize - 10;
+
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: chunk %d/%d, dataSize=%d, bufferOffset=%d",
+		chunkIndex, totalChunks, dataSize, _loadBufferOffset);
+
+	// First chunk in sequence - reset buffer state
+	if (chunkIndex == 0) {
+		_loadBufferOffset = 0;
+		_loadReadOffset = 8;
+		_lastLoadChunkIdx = -1;
+		_totalLoadChunks = totalChunks;
+
+		int32 estimatedSize = totalChunks * 600;
+		if (_loadBuffer == nullptr || _loadBufferSize < estimatedSize) {
+			free(_loadBuffer);
+			_loadBufferSize = estimatedSize;
+			_loadBuffer = (byte *)malloc(_loadBufferSize);
+			if (_loadBuffer == nullptr) {
+				warning("SmushPlayer::handleLoad: Failed to allocate %d bytes for LOAD buffer",
+					_loadBufferSize);
+				_loadBufferSize = 0;
+				return;
+			}
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Allocated %d bytes for LOAD buffer",
+				_loadBufferSize);
+		}
+	}
+
+	// Check sequential order
+	if (_lastLoadChunkIdx + 1 != chunkIndex) {
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Non-sequential chunk %d (expected %d), skipping",
+			chunkIndex, _lastLoadChunkIdx + 1);
+		return;
+	}
+
+	// Check buffer capacity
+	if (_loadBuffer == nullptr || _loadBufferOffset + dataSize >= _loadBufferSize) {
+		warning("SmushPlayer::handleLoad: Buffer overflow - offset=%d size=%d limit=%d",
+			_loadBufferOffset, dataSize, _loadBufferSize);
+		return;
+	}
+
+	// Copy data to buffer
+	b.read(_loadBuffer + _loadBufferOffset, dataSize);
+	_loadBufferOffset += dataSize;
+	_lastLoadChunkIdx = chunkIndex;
+
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Accumulated %d bytes total", _loadBufferOffset);
+
+	if (chunkIndex == totalChunks - 1) {
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad: Sequence complete - %d chunks, %d bytes total",
+			totalChunks, _loadBufferOffset);
+	}
+}
+
+/**
+ * RA2-specific text rendering using SmushMultiFont for inline font switching.
+ */
+void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
+										int pos_x, int pos_y, int left, int top,
+										int width, int height, TextStyleFlags flg) {
+	// Create multi-font renderer on first use
+	if (!_multiFont) {
+		_multiFont = new SmushMultiFont(_vm, this, true);
+	}
+	_multiFont->setDefaultFont(fontId);
+
+	debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",
+		  _frame, fontId, color, (int)flg, (int)flg, pos_x, pos_y, left, top, width, height, str);
+
+	if (flg & kStyleWordWrap) {
+		Common::Rect clipRect(MAX<int>(0, left), MAX<int>(0, top), MIN<int>(left + width, _width), MIN<int>(top + height, _height));
+		_multiFont->drawStringWrap(str, _dst, clipRect, pos_x, pos_y, color, flg);
+	} else {
+		Common::Rect clipRect(0, 0, _width, _height);
+		_multiFont->drawString(str, _dst, clipRect, pos_x, pos_y, color, flg);
+	}
+}
+
+/**
+ * RA2-specific buffer selection for non-standard FOBJ dimensions.
+ * Returns the destination buffer to use and updates _dst, _width, _height.
+ */
+void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
+	// Rebel2 uses a special buffer for all non-matching frames.
+	// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
+	// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null)
+	int bufSize = width * height;
+	if (bufSize > _vm->_screenWidth * _vm->_screenHeight) {
+		// Frame is larger than screen - need special buffer
+		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+			free(_specialBuffer);
+			_specialBuffer = (byte *)malloc(bufSize);
+			_specialBufferSize = bufSize;
+			_width = width;
+			_height = height;
+		}
+	}
+
+	if (bufSize > _vm->_screenWidth * _vm->_screenHeight &&
+	    _specialBuffer != nullptr && _specialBufferSize >= bufSize) {
+		_dst = _specialBuffer;
+		debug("SmushPlayer: Using _specialBuffer for oversized FOBJ %dx%d", width, height);
+	} else {
+		if (_specialBuffer == nullptr) {
+			VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+			_dst = vs->getPixels(0, 0);
+			debug("SmushPlayer: Reset _dst to virtual screen for FOBJ %dx%d at (%d,%d) _dst=%p",
+				width, height, 0, 0, (void*)_dst);
+		} else {
+			// Large frame was in this video, use _specialBuffer for compositing
+			_dst = _specialBuffer;
+			debug("SmushPlayer: Using _specialBuffer for small FOBJ %dx%d (compositing with large frame)",
+				width, height);
+		}
+	}
+}
+
+/**
+ * Apply RA2 FOBJ position offsets and clamp to buffer bounds.
+ * Modifies left, top, width, height in place.
+ */
+void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch) {
+	left += _fobjOffsetX;
+	top += _fobjOffsetY;
+
+	int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+	if (top < 0) {
+		height += top;
+		top = 0;
+	}
+	if (left < 0) {
+		width += left;
+		left = 0;
+	}
+	if (top + height > bufHeight) {
+		height = bufHeight - top;
+	}
+	if (left + width > pitch) {
+		width = pitch - left;
+	}
+}
+
+/**
+ * Dispatch to RA2-specific codec functions.
+ * Returns true if the codec was handled, false for standard codecs.
+ */
+bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+								 int width, int height, int pitch, int dataSize) {
+	switch (codec) {
+	case SMUSH_CODEC_LINE_UPDATE:
+	case SMUSH_CODEC_LINE_UPDATE2:
+		smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
+		return true;
+	case SMUSH_CODEC_SKIP_RLE:
+		smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
+		return true;
+	case SMUSH_CODEC_RA2_BOMP:
+		smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
+		return true;
+	default:
+		return false;
+	}
+}
+
+/**
+ * Save raw FOBJ data when STOR is pending (for later re-decoding by FTCH).
+ */
+void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+								   int left, int top, int width, int height) {
+	free(_storedFobjData);
+	_storedFobjData = (byte *)malloc(dataSize);
+	memcpy(_storedFobjData, data, dataSize);
+	_storedFobjDataSize = dataSize;
+	_storedFobjCodec = codec;
+	_storedFobjLeft = left;
+	_storedFobjTop = top;
+	_storedFobjWidth = width;
+	_storedFobjHeight = height;
+	_storeFrame = false;
+}
+
+/**
+ * Reset XPAL delta palette from the current base palette.
+ * Prevents stale delta values from a previous video corrupting the palette.
+ */
+void SmushPlayer::ra2ResetDeltaPalette() {
+	for (int j = 0; j < 768; ++j) {
+		_shiftedDeltaPal[j] = _pal[j] << 7;
+	}
+	memset(_deltaPal, 0, sizeof(_deltaPal));
+}
+
+/**
+ * RA2 font path table.
+ */
+SmushFont *SmushPlayer::ra2GetFont(int font) {
+	const char *ra2_fonts[] = {
+		"SYSTM/TALKFONT.NUT",
+		"SYSTM/DIHIFONT.NUT",
+		"SYSTM/TITLFONT.NUT",
+		"SYSTM/SMALFONT.NUT"
+	};
+	int numFonts = ARRAYSIZE(ra2_fonts);
+	if (font >= 0 && font < numFonts) {
+		_sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+	} else {
+		debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
+		_sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+	}
+	return _sf[font];
+}
+
+/**
+ * RA2 per-frame audio processing (called from parseNextFrame).
+ */
+void SmushPlayer::ra2ParseNextFrame() {
+	// Call processDispatches directly since RA2 has no iMUSE
+	// 11025 Hz / 12 fps = ~918 samples per frame
+	processDispatches(_smushAudioSampleRate / 12);
+}
+
+/**
+ * RA2-specific handleAnimHeader fixup: when AHDR reports 0x0 dimensions,
+ * use screen dimensions instead.
+ */
+void SmushPlayer::ra2FixupAnimHeader() {
+	if (_width == 0 && _height == 0) {
+		_width = _vm->_screenWidth;   // 320
+		_height = _vm->_screenHeight; // 200
+		debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+	}
+}
+
+// Masked region management — used by InsaneRebel2
+
+void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
+	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+		if (*it == rect) {
+			return; // Already exists
+		}
+	}
+	_maskedRegions.push_back(rect);
+}
+
+void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
+	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+		if (*it == rect) {
+			_maskedRegions.erase(it);
+			return;
+		}
+	}
+}
+
+void SmushPlayer::clearMaskedRegions() {
+	_maskedRegions.clear();
+}
+
+void SmushPlayer::setScrollOffset(int x, int y) {
+	_scrollX = x;
+	_scrollY = y;
+}
+
+} // End of namespace Scumm


Commit: 38af869f7c41f6268408b0a7a5f6994f57115697
    https://github.com/scummvm/scummvm/commit/38af869f7c41f6268408b0a7a5f6994f57115697
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:11+02:00

Commit Message:
SCUMM: RA2: Improve UI rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bcfbb8d8c75..2742e9a8bc7 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -607,74 +607,39 @@ void InsaneRebel2::addScore(int points) {
 	debug("Rebel2: Score +%d = %d", points, _playerScore);
 }
 
-// Render score to HUD (part of FUN_0041c012)
-// Score is drawn using FUN_00434cb0 with format string "%07ld"
-// In retail, score is rendered to a status bar buffer, then blitted to screen at Y=180 (0xb4)
-// The text within the status bar is at local Y=4, so screen Y = 180 + 4 = 184
+// Render score text to HUD (part of FUN_0041c012)
+// FUN_0041c012 lines 133-137: calls FUN_00434cb0 with format "%07ld"
+// Position (low-res): X = 0x101 (257), Y = 4 within status bar → screen Y = 184
 void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY) {
-	// In retail, score is rendered by FUN_0041c012 which calls FUN_00434cb0
-	// The status bar is blitted to screen at Y = DAT_0047ab2c + 0xb4 (typically 0 + 180 = 180)
-	// Text position within status bar from FUN_0041c012 line 136-137:
-	//   X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 (257) for low-res
-	//   Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 for low-res
-	// So final screen position: X=257, Y=180+4=184
-	// Format: 7-digit zero-padded decimal ("%07ld")
-
-	(void)statusBarY; // Not used - we use fixed Y positions
-
-	// Use SMALFONT.NUT (NutRenderer) for rendering digits
-	// If not available, skip rendering
-	if (!_smush_dispfontNut) {
-		debug(1, "renderScoreHUD: _smush_dispfontNut is NULL!");
-		return;
-	}
+	(void)statusBarY;
 
-	// The SMUSH buffer is 424x260, but the visible screen is 320x200
-	// The view offset (_viewX, _viewY) determines where in the buffer the screen is showing
-	// To render at fixed screen positions, we add the view offset
+	if (!_smush_dispfontNut)
+		return;
 
-	// Convert score to 7-digit string
 	char scoreStr[16];
 	Common::sprintf_s(scoreStr, "%07d", _playerScore);
 
-	// Status bar is at Y=180 (0xb4), text within it at Y=4, so total Y=184
-	// Score X position is 257 (0x101)
-	const int STATUS_BAR_Y = 180;  // 0xb4 from FUN_41C012 line 149/152
-	const int SCORE_TEXT_Y = 4;    // Text position within status bar
-
+	// Score position from FUN_0041c012 assembly (low-res mode):
+	//   X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 = 257
+	//   Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar at Y=180)
 	int scoreX = 257 + _viewX;
-	int scoreY = STATUS_BAR_Y + SCORE_TEXT_Y + _viewY;
-
-	debug(5, "renderScoreHUD: Drawing score=%d at buffer(%d,%d) viewOffset(%d,%d)",
-		  _playerScore, scoreX, scoreY, _viewX, _viewY);
-
-	// Draw each character manually using NutRenderer::drawCharV7
-	Common::Rect clipRect(0, 0, width, height);
+	int scoreY = 180 + 4 + _viewY;
+
+	// Render each digit as a NUT sprite (direct pixel blit with color 0 transparency).
+	// This matches the original's FUN_00434cb0 → FUN_004341a0 text rendering which
+	// uses the NUT font's embedded palette colors (1=white, 3=gray, 4=black outline).
+	// Render each character applying xoffs/yoffs from NUT frame headers,
+	// matching FUN_0042cba0 lines 13-14:
+	//   param_3 = *(short *)(param_6 + 2) + param_3;  // X += xoffs
+	//   param_4 = *(short *)(param_6 + 4) + param_4;  // Y += yoffs
 	int x = scoreX;
 	for (int i = 0; scoreStr[i] != '\0'; i++) {
 		byte ch = (byte)scoreStr[i];
-		int charWidth = _smush_dispfontNut->getCharWidth(ch);
-		if (charWidth > 0) {
-			// Use drawCharV7 with color 255 (white) for visibility
-			_smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, scoreY, pitch, 255, kStyleAlignLeft, ch, true, true);
-			x += charWidth;
-		}
-	}
-
-	// Also draw lives counter - in status bar at X=168 (0xa8), Y=7 within bar
-	const int LIVES_TEXT_Y = 7;
-	char livesStr[8];
-	Common::sprintf_s(livesStr, "%d", _playerLives);
-	int livesX = 168 + _viewX;
-	int livesY = STATUS_BAR_Y + LIVES_TEXT_Y + _viewY;
-
-	x = livesX;
-	for (int i = 0; livesStr[i] != '\0'; i++) {
-		byte ch = (byte)livesStr[i];
-		int charWidth = _smush_dispfontNut->getCharWidth(ch);
-		if (charWidth > 0) {
-			_smush_dispfontNut->drawCharV7(renderBitmap, clipRect, x, livesY, pitch, 255, kStyleAlignLeft, ch, true, true);
-			x += charWidth;
+		if (ch < _smush_dispfontNut->getNumChars()) {
+			int charX = x + _smush_dispfontNut->getCharXOffset(ch);
+			int charY = scoreY + _smush_dispfontNut->getCharYOffset(ch);
+			renderNutSprite(renderBitmap, pitch, width, height, charX, charY, _smush_dispfontNut, ch);
+			x += _smush_dispfontNut->getCharWidth(ch);
 		}
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 0eb02660d2c..d288980a18d 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2132,55 +2132,60 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 
 void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
 										  int statusBarY, int32 curFrame) {
-	// Draw DISPFONT.NUT status bar sprites (FUN_0041c012 equivalent)
-	// DISPFONT.NUT contains:
-	//   Sprite 1: Status bar background frame
-	//   Sprites 2-5: Difficulty stars (1-4)
-	//   Sprite 6: Damage bar fill (with clip rect X=63, Y=9, W=64, H=6)
-	//   Sprite 7: Damage alert (flashing red when critical)
+	// FUN_0041c012 equivalent — renders DISPFONT.NUT status bar sprites.
+	// DISPFONT.NUT sprite layout:
+	//   Sprite 1:   Status bar background
+	//   Sprites 2-5: Difficulty variants (full status bar with 1-4 stars)
+	//   Sprite 6:   Bar fill element (reused for both damage and lives bars)
+	//   Sprite 7:   Damage alert overlay (flashing when critical)
 
 	if (!_smush_cockpitNut)
 		return;
 
-	// Sprite 1: Status bar background
-	if (_smush_cockpitNut->getNumChars() > 1) {
-		renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
+	int numSprites = _smush_cockpitNut->getNumChars();
+
+	// --- Sprite 1: Status bar background (always drawn first as base layer) ---
+	if (numSprites > 1) {
+		renderNutSprite(renderBitmap, pitch, width, height,
+			_viewX, statusBarY + _viewY, _smush_cockpitNut, 1);
 	}
 
-	// Difficulty indicator (sprites 2-5)
-	int difficulty = 0;  // TODO: Read from game state
+	// --- Difficulty sprite (2-5) overlaid on top ---
+	// FUN_0041c012 lines 33-43: sprite index = min(difficulty, 4) + 1
+	int difficulty = _difficulty;
 	if (difficulty > 3) difficulty = 3;
 	int difficultySprite = difficulty + 2;
-	if (_smush_cockpitNut->getNumChars() > difficultySprite) {
-		renderNutSprite(renderBitmap, pitch, width, height, _viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
+	if (numSprites > difficultySprite) {
+		renderNutSprite(renderBitmap, pitch, width, height,
+			_viewX, statusBarY + _viewY, _smush_cockpitNut, difficultySprite);
 	}
 
-	// Damage bar (sprite 6) with clipped width
-	if (_smush_cockpitNut->getNumChars() > 6) {
-		int drawWidth = (64 * _playerDamage) / 255;
-		if (drawWidth < 0) drawWidth = 0;
-		if (drawWidth > 64) drawWidth = 64;
+	// --- Damage/shield bar (sprite 6 within damage clip rect) ---
+	// FUN_0041c012 lines 44-76:
+	//   Clip rect (low-res): {X=0x3f, Y=9, W=0x40, H=6} = {63, 9, 64, 6}
+	//   Bar width = shield_value >> 2 (divide by 4, range 0-63)
+	//   If shield > 0xAA (170): alert blink with sprite 7
+	if (numSprites > 6) {
+		// Bar width from assembly: param_1 >> 2 (low-res)
+		int damageBarWidth = _playerDamage >> 2;
 
 		const byte *src = _smush_cockpitNut->getCharData(6);
 		int sw = _smush_cockpitNut->getCharWidth(6);
 		int sh = _smush_cockpitNut->getCharHeight(6);
 
-		// Clip rect inside sprite: X=63, Y=9, W=64, H=6
-		const int clipX = 63, clipY = 9, clipW = 64, clipH = 6;
-
-		if (src && sw > 0 && sh > 0) {
-			int maxClipW = sw - clipX;
-			if (maxClipW < 0) maxClipW = 0;
-			int drawW = MIN(drawWidth, MIN(clipW, maxClipW));
-			int drawH = MIN(clipH, sh - clipY);
-			if (drawH < 0) drawH = 0;
-
-			for (int y = 0; y < drawH; y++) {
-				for (int x = 0; x < drawW; x++) {
-					int destX = clipX + x + _viewX;
-					int destY = statusBarY + clipY + y + _viewY;
-					if (destX >= 0 && destX < pitch && destY >= 0 && destY < height) {
-						byte pixel = src[(clipY + y) * sw + (clipX + x)];
+		const int dmgClipX = 63, dmgClipY = 9, dmgClipW = 64, dmgClipH = 6;
+
+		if (src && sw > 0 && sh > 0 && damageBarWidth > 0) {
+			int drawW = MIN(damageBarWidth, MIN(dmgClipW, sw - dmgClipX));
+			int drawH = MIN(dmgClipH, sh - dmgClipY);
+			if (drawW > 0 && drawH > 0) {
+				for (int y = 0; y < drawH; y++) {
+					int destY = statusBarY + dmgClipY + y + _viewY;
+					if (destY < 0 || destY >= height) continue;
+					for (int x = 0; x < drawW; x++) {
+						int destX = dmgClipX + x + _viewX;
+						if (destX < 0 || destX >= pitch) continue;
+						byte pixel = src[(dmgClipY + y) * sw + (dmgClipX + x)];
 						if (pixel != 0) {
 							renderBitmap[destY * pitch + destX] = pixel;
 						}
@@ -2188,12 +2193,70 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 				}
 			}
 		}
+
+		// Damage alert overlay (sprite 7) — FUN_0041c012 lines 68-76:
+		// When damage > 0xAA (170) and frame counter bit 3 is clear, draw sprite 7
+		// at full clip rect width (64 pixels) to show flashing alert
+		if (numSprites > 7 && _playerDamage > 170 && ((curFrame & 8) == 0)) {
+			if (src && sw > 0 && sh > 0) {
+				int alertW = MIN(dmgClipW, sw - dmgClipX);
+				int alertH = MIN(dmgClipH, sh - dmgClipY);
+				if (alertW > 0 && alertH > 0) {
+					const byte *alertSrc = _smush_cockpitNut->getCharData(7);
+					int alertSW = _smush_cockpitNut->getCharWidth(7);
+					int alertSH = _smush_cockpitNut->getCharHeight(7);
+					if (alertSrc && alertSW > 0 && alertSH > 0) {
+						int aW = MIN(alertW, alertSW - dmgClipX);
+						int aH = MIN(alertH, alertSH - dmgClipY);
+						for (int y = 0; y < aH; y++) {
+							int destY = statusBarY + dmgClipY + y + _viewY;
+							if (destY < 0 || destY >= height) continue;
+							for (int x = 0; x < aW; x++) {
+								int destX = dmgClipX + x + _viewX;
+								if (destX < 0 || destX >= pitch) continue;
+								byte pixel = alertSrc[(dmgClipY + y) * alertSW + (dmgClipX + x)];
+								if (pixel != 0) {
+									renderBitmap[destY * pitch + destX] = pixel;
+								}
+							}
+						}
+					}
+				}
+			}
+		}
 	}
 
-	// Damage alert overlay (sprite 7) when damage > 170 and flashing
-	if (_smush_cockpitNut->getNumChars() > 7) {
-		if (_playerDamage > 170 && ((curFrame & 8) == 0)) {
-			renderNutSprite(renderBitmap, pitch, width, height, 63 + _viewX, statusBarY + 9 + _viewY, _smush_cockpitNut, 7);
+	// --- Lives bar (sprite 6 within lives clip rect) ---
+	// FUN_0041c012 lines 99-131:
+	//   Clip rect (low-res): {X=0xa8, Y=7, W=0x32, H=9} = {168, 7, 50, 9}
+	//   Bar width = min((lives * 5 - 5) * 2, 50) — only drawn when lives > 1
+	if (numSprites > 6 && _playerLives > 1) {
+		int livesBarWidth = (_playerLives * 5 - 5) * 2;
+		if (livesBarWidth > 50) livesBarWidth = 50;
+
+		const byte *src = _smush_cockpitNut->getCharData(6);
+		int sw = _smush_cockpitNut->getCharWidth(6);
+		int sh = _smush_cockpitNut->getCharHeight(6);
+
+		const int livClipX = 168, livClipY = 7, livClipW = 50, livClipH = 9;
+
+		if (src && sw > 0 && sh > 0 && livesBarWidth > 0) {
+			int drawW = MIN(livesBarWidth, MIN(livClipW, sw - livClipX));
+			int drawH = MIN(livClipH, sh - livClipY);
+			if (drawW > 0 && drawH > 0) {
+				for (int y = 0; y < drawH; y++) {
+					int destY = statusBarY + livClipY + y + _viewY;
+					if (destY < 0 || destY >= height) continue;
+					for (int x = 0; x < drawW; x++) {
+						int destX = livClipX + x + _viewX;
+						if (destX < 0 || destX >= pitch) continue;
+						byte pixel = src[(livClipY + y) * sw + (livClipX + x)];
+						if (pixel != 0) {
+							renderBitmap[destY * pitch + destX] = pixel;
+						}
+					}
+				}
+			}
 		}
 	}
 }


Commit: 696c68cd857acb181002a29a139e62edffe628a5
    https://github.com/scummvm/scummvm/commit/696c68cd857acb181002a29a139e62edffe628a5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:11+02:00

Commit Message:
SCUMM: RA2: Add pilot and difficulty menus

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 2742e9a8bc7..4c690606fa7 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -24,6 +24,7 @@
 #include "engines/engine.h"
 #include "common/system.h"
 #include "common/events.h"
+#include "common/savefile.h"
 #include "common/util.h"
 
 #include "scumm/actor.h"
@@ -409,12 +410,23 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_previewOffsetX = -90;
 	_previewOffsetY = 75;  // Chapter 0: 0 * -50 + 75 = 75
 
+	// Initialize pilot data system
+	_numPilots = 0;
+	_activePilot = 0;
+	for (i = 0; i < kMaxPilots; i++) {
+		_pilots[i].init();
+	}
+	loadPilots(); // Load saved pilots from disk
+
 	// Initialize pilot selection system (FUN_00414A41)
 	// Menu structure: [saved pilots] + 4 fixed options (NEW/DUPE/DELETE/MAIN MENU)
 	_levelSelection = 0;          // First item selected
-	_levelItemCount = 4;          // 0 saved pilots + 4 fixed options
+	_levelItemCount = _numPilots + 4; // N saved pilots + 4 fixed options
 	_selectedLevel = 1;           // Default selected level
 	_difficultySelection = 2;     // Default to 3rd difficulty (matching original init param_3=2)
+	_pilotMenuMode = kPilotModeSelect;
+	_pilotNameInput = "";
+	_pilotEditIndex = -1;
 
 	// Initialize menu input capture system
 	_menuInputActive = false;
@@ -550,6 +562,58 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 	return false;
 }
 
+// Per-level difficulty parameters extracted from RA2WIN95.EXE at VA 0x47e0f0
+// Original: 2D table indexed by DAT_0047a7fa (chapter/difficulty) * 0x242 + DAT_0047a7f8 (level+1) * 0x22
+// -1 = not applicable for this level type (e.g., no walls in turret levels)
+const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][15] = {
+	// Difficulty 0 (Easy) - levels 1-15
+	{
+		{  30,   15,    3,   75}, {  17,   18,    2,   75}, {  -1,   18,    3,   75},
+		{  19,   -1,    2,   75}, {  27,  180,    3,   75}, {  40,   -1,   -1,   -1},
+		{  -1,   21,    3,   50}, {  30,   15,    4,   60}, {  30,   -1,   70,   75},
+		{  -1,   10,    5,   65}, {  -1,   -1,    6,   65}, {  30,   20,    1,   85},
+		{  -1,   24,    2,   75}, {  -1,   -1,    3,   75}, {  30,  255,    4,   75},
+	},
+	// Difficulty 1 (Medium) - levels 1-15
+	{
+		{  35,   17,    5,   75}, {  30,   60,    4,   75}, {  -1,   28,    3,   75},
+		{  25,   -1,    4,   75}, {  35,  190,    4,   75}, {  65,   -1,   -1,   -1},
+		{  -1,   24,    3,   50}, {  45,   17,    5,   75}, {  35,   -1,   75,   75},
+		{  -1,   15,    5,   75}, {  -1,   -1,    8,   75}, {  35,   30,    1,   85},
+		{  -1,   28,    2,   75}, {  -1,   -1,    4,   75}, {  35,  255,    4,   75},
+	},
+	// Difficulty 2 (Hard) - levels 1-15
+	{
+		{  38,   20,    7,   75}, {  35,  100,    6,   75}, {  -1,   30,    4,   75},
+		{  30,   -1,    6,   75}, {  50,  200,   12,   75}, {  80,   -1,   -1,   -1},
+		{  -1,   27,    3,   60}, {  60,   19,    7,   75}, {  40,   -1,  100,   85},
+		{  -1,   20,    6,   75}, {  -1,   -1,   11,   75}, {  40,   40,    3,   76},
+		{  -1,   38,    4,   75}, {  -1,   -1,    7,   75}, {  40,  255,    7,   75},
+	},
+	// Difficulty 3 (Expert) - levels 1-15
+	{
+		{  42,   23,   12,   75}, {  50,  120,   16,   75}, {  -1,   55,    4,   75},
+		{  50,    0,   15,   79}, {  90,  220,   15,   90}, {  90,   -1,   -1,   -1},
+		{  -1,   35,    3,   68}, {  75,   30,   20,   80}, {  50,   -1,  110,   90},
+		{  -1,   30,    7,   75}, {  -1,   -1,   13,   75}, {  55,   55,    5,   77},
+		{  -1,   49,    4,   75}, {  -1,   -1,   10,   79}, {  45,  255,    8,   80},
+	},
+	// Difficulty 4 (identical to difficulty 2 in original data) - levels 1-15
+	{
+		{  38,   20,    7,   75}, {  35,  100,    6,   75}, {  -1,   30,    4,   75},
+		{  30,   -1,    6,   75}, {  50,  200,   12,   75}, {  80,   -1,   -1,   -1},
+		{  -1,   27,    3,   60}, {  60,   19,    7,   75}, {  40,   -1,  100,   85},
+		{  -1,   20,    6,   75}, {  -1,   -1,   11,   75}, {  40,   40,    3,   76},
+		{  -1,   38,    4,   75}, {  -1,   -1,    7,   75}, {  40,  255,    7,   75},
+	},
+};
+
+InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams(int levelId) const {
+	int diff = CLIP(_difficulty, 0, 4);
+	int lvIdx = CLIP(levelId - 1, 0, 14);  // levelId 1-15 → index 0-14
+	return kDifficultyTable[diff][lvIdx];
+}
+
 // Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
 // These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
 // For simplicity, we use fixed values based on difficulty level
@@ -644,6 +708,175 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 	}
 }
 
+// ======================= Pilot Data System =======================
+// Save/load pilot profiles using ScummVM's save file system (one pilot per slot).
+// Follows the Hypno/Wetlands pattern: each pilot = one ScummVM save slot.
+// Uses ScummEngine::makeSavegameName() for standard ScummVM file naming.
+// Original: FUN_00411980 (load) / FUN_00411A5D (save)
+
+static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
+static const uint16 kPilotSaveVersion = 1;
+
+bool InsaneRebel2::loadPilots() {
+	_numPilots = 0;
+
+	for (int i = 0; i < kMaxPilots; i++) {
+		Common::String filename = _vm->makeSavegameName(i, false);
+		Common::InSaveFile *sf = _vm->_saveFileMan->openForLoading(filename);
+		if (!sf)
+			break; // Slots are contiguous
+
+		uint32 magic = sf->readUint32BE();
+		if (magic != kPilotSaveMagic) {
+			delete sf;
+			break;
+		}
+
+		/* uint16 version = */ sf->readUint16LE();
+
+		PilotData &p = _pilots[i];
+		sf->read(p.name, kMaxPilotNameLen + 1);
+		p.name[kMaxPilotNameLen] = '\0';
+		for (int j = 0; j < kNumLevels; j++)
+			p.score[j] = sf->readSint32LE();
+		for (int j = 0; j < kNumLevels; j++)
+			p.lives[j] = sf->readSint32LE();
+		for (int j = 0; j < kNumLevels; j++)
+			p.damage[j] = sf->readSint32LE();
+		p.difficulty = sf->readSint16LE();
+		delete sf;
+
+		_numPilots = i + 1;
+	}
+
+	debug("Rebel2: Loaded %d pilot(s)", _numPilots);
+	return _numPilots > 0;
+}
+
+bool InsaneRebel2::savePilots() {
+	bool ok = true;
+
+	for (int i = 0; i < _numPilots; i++) {
+		Common::String filename = _vm->makeSavegameName(i, false);
+		Common::OutSaveFile *sf = _vm->_saveFileMan->openForSaving(filename, false);
+		if (!sf) {
+			warning("Rebel2: Failed to save pilot %d", i);
+			ok = false;
+			continue;
+		}
+
+		sf->writeUint32BE(kPilotSaveMagic);
+		sf->writeUint16LE(kPilotSaveVersion);
+
+		const PilotData &p = _pilots[i];
+		sf->write(p.name, kMaxPilotNameLen + 1);
+		for (int j = 0; j < kNumLevels; j++)
+			sf->writeSint32LE(p.score[j]);
+		for (int j = 0; j < kNumLevels; j++)
+			sf->writeSint32LE(p.lives[j]);
+		for (int j = 0; j < kNumLevels; j++)
+			sf->writeSint32LE(p.damage[j]);
+		sf->writeSint16LE(p.difficulty);
+
+		sf->finalize();
+		delete sf;
+	}
+
+	// Remove leftover files beyond current count
+	for (int i = _numPilots; i < kMaxPilots; i++) {
+		Common::String filename = _vm->makeSavegameName(i, false);
+		_vm->_saveFileMan->removeSavefile(filename);
+	}
+
+	debug("Rebel2: Saved %d pilot(s)", _numPilots);
+	return ok;
+}
+
+int InsaneRebel2::createNewPilot() {
+	// FUN_00411B9A: Create new pilot slot
+	if (_numPilots >= kMaxPilots)
+		return -1;
+
+	int idx = _numPilots;
+	_pilots[idx].init();
+	_numPilots++;
+	return idx;
+}
+
+void InsaneRebel2::deletePilot(int index) {
+	// FUN_00411D29: Delete pilot and shift remaining down
+	if (index < 0 || index >= _numPilots)
+		return;
+
+	for (int i = index; i < _numPilots - 1; i++) {
+		_pilots[i] = _pilots[i + 1];
+	}
+	_numPilots--;
+
+	// Clear the now-unused last slot
+	_pilots[_numPilots].init();
+}
+
+void InsaneRebel2::copyPilot(int srcIndex) {
+	// Copy pilot from srcIndex to a new slot
+	if (srcIndex < 0 || srcIndex >= _numPilots || _numPilots >= kMaxPilots)
+		return;
+
+	int newIdx = _numPilots;
+	_pilots[newIdx] = _pilots[srcIndex];
+
+	// Append " COPY" or truncate name to fit
+	Common::String name(_pilots[newIdx].name);
+	if (name.size() + 5 <= kMaxPilotNameLen) {
+		name += " COPY";
+	} else if (name.size() > 0) {
+		// Truncate and add marker
+		name = name.substr(0, kMaxPilotNameLen - 2) + " C";
+	}
+	Common::strlcpy(_pilots[newIdx].name, name.c_str(), sizeof(_pilots[newIdx].name));
+
+	_numPilots++;
+}
+
+void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage) {
+	if (_activePilot < 0 || _activePilot >= _numPilots)
+		return;
+	if (levelIndex < 0 || levelIndex >= kNumLevels)
+		return;
+
+	PilotData &pilot = _pilots[_activePilot];
+	pilot.score[levelIndex] = score;
+	pilot.lives[levelIndex] = lives;
+	pilot.damage[levelIndex] = damage;
+
+	// Unlock next level if this one completed with damage < 0xFF
+	if (damage < 0xFF && levelIndex + 1 < kNumLevels) {
+		if (pilot.damage[levelIndex + 1] == 0xFF) {
+			// Initialize next level as playable
+			pilot.score[levelIndex + 1] = 0;
+			pilot.lives[levelIndex + 1] = 4;
+			pilot.damage[levelIndex + 1] = 0;
+		}
+	}
+
+	savePilots();
+}
+
+int InsaneRebel2::getPilotHighestLevel() const {
+	if (_activePilot < 0 || _activePilot >= _numPilots)
+		return 0;
+
+	const PilotData &pilot = _pilots[_activePilot];
+	int highest = 0;
+	for (int i = kNumLevels - 1; i >= 0; i--) {
+		if (pilot.damage[i] < 0xFF) {
+			highest = i;
+			break;
+		}
+	}
+	return highest;
+}
+
 int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 82a5ab56bb2..bb2abaa4cd4 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -156,6 +156,54 @@ public:
 	// Draw score/info display at bottom of chapter select - emulates FUN_00434cb0 calls
 	void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
 
+	// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
+	// Original: 10 pilot slots × 0x118 (280) bytes at DAT_004568A8
+	// Stored via SaveFileManager in a custom save file
+
+	static const int kMaxPilots = 10;
+	static const int kMaxPilotNameLen = 15;
+	static const int kNumLevels = 16;
+
+	struct PilotData {
+		char name[kMaxPilotNameLen + 1]; // +0x04: Pilot name (15 chars + null)
+		int32 score[kNumLevels];         // +0x2C: Per-level score (0 = default, 0xFF = unplayed)
+		int32 lives[kNumLevels];         // +0x6C: Per-level lives (4 = default, 0xFF = unplayed)
+		int32 damage[kNumLevels];        // +0xAC: Per-level damage (0xFF = unplayed)
+		int16 difficulty;                // +0x10C: Difficulty setting (0-5)
+
+		void init() {
+			memset(name, 0, sizeof(name));
+			difficulty = 2; // Default to 3rd option
+			score[0] = 0;
+			lives[0] = 4;
+			damage[0] = 0;
+			for (int i = 1; i < kNumLevels; i++) {
+				score[i] = 0;
+				lives[i] = 0xFF;
+				damage[i] = 0xFF;
+			}
+		}
+	};
+
+	PilotData _pilots[kMaxPilots];       // DAT_004568A8 pilot array
+	int _numPilots;                      // DAT_00480318 number of valid pilots
+	int _activePilot;                    // DAT_0047a7ea selected pilot index
+
+	// Pilot save/load via SaveFileManager
+	bool loadPilots();
+	bool savePilots();
+
+	// Pilot management (FUN_00411B9A, FUN_00411D29)
+	int createNewPilot();                // Returns index of new pilot, or -1 if full
+	void deletePilot(int index);         // FUN_00411D29: shift remaining pilots down
+	void copyPilot(int srcIndex);        // Copy pilot to new slot
+
+	// Update pilot progress after level completion
+	void updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage);
+
+	// Get highest unlocked level for active pilot (checks damage[] < 0xFF)
+	int getPilotHighestLevel() const;
+
 	// ================= Pilot Selection Menu (FUN_00414A41) ====================
 	// This is the pilot/save selection menu (separate from chapter selection)
 
@@ -165,6 +213,18 @@ public:
 		kLevelSelectQuit = 2      // Quit game
 	};
 
+	// Pilot name input state (for NEW PILOT name entry)
+	enum PilotMenuMode {
+		kPilotModeSelect = 0,     // Normal pilot list selection
+		kPilotModeNameInput = 1,  // Typing a new pilot name
+		kPilotModeDifficulty = 2, // Difficulty submenu
+		kPilotModeDeleteConfirm = 3, // Delete confirmation
+		kPilotModeCopySelect = 4  // Copy source selection
+	};
+	PilotMenuMode _pilotMenuMode;
+	Common::String _pilotNameInput;      // Current name being typed
+	int _pilotEditIndex;                 // Index of pilot being edited/created
+
 	int _levelSelection;          // Current level selection (0-based)
 	int _levelItemCount;          // Number of level items (levels + options)
 	int _selectedLevel;           // Final selected level ID (1-15)
@@ -917,10 +977,32 @@ public:
 	NutRenderer *_hudOverlayNut;     // DAT_0047fe78 - Primary HUD overlay (animated)
 	NutRenderer *_hudOverlay2Nut;    // DAT_0047fe80 - Secondary HUD overlay
 
-	/* Difficulty Level (0, 1, 2 = Easy, Med, Hard) */
+	/* Difficulty Level (0-5, from pilot menu; clamped to 0-4 for table lookup) */
 	int _difficulty;
 	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
 
+	// ======================= Per-Level Difficulty Parameters =======================
+	// Extracted from RA2WIN95.EXE at VA 0x47e0f0
+	// 2D table indexed by difficulty (0-4) × game level (1-15)
+	// Original indexing: &DAT_0047e0f6 + chapter * 0x242 + level * 0x22
+	// -1 = not applicable for this level type (e.g., no walls in turret levels)
+
+	struct LevelDifficultyParams {
+		int16 wallDamage;           // +0x06 DAT_0047e0f6: Wall/obstacle collision damage
+		int16 directHitDamage;      // +0x04 DAT_0047e0f4: Direct hit damage (par4=100)
+		int16 enemyProjectileDamage;// +0x08 DAT_0047e0f8: Enemy projectile/probabilistic damage
+		int16 hitProbability;       // +0x0C DAT_0047e0fc: Enemy hit chance (0-100, -1=disabled)
+	};
+
+	// Table: 5 difficulty levels × 15 game levels
+	// Difficulty 4 is identical to difficulty 2 in the original data
+	static const LevelDifficultyParams kDifficultyTable[5][15];
+
+	// Look up difficulty parameters for current difficulty and level
+	// Returns the entry from kDifficultyTable, clamping difficulty to 0-4
+	// levelId is 1-based (1-15)
+	LevelDifficultyParams getDifficultyParams(int levelId) const;
+
 	// Score system (FUN_0041bf8d equivalent)
 	// Adds points to score and awards bonus life when crossing threshold
 	void addScore(int points);
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 36c3cc87ca1..a6ff44b1aaf 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -434,15 +434,14 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 				srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
 
 			if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
-				int probability = 20 + _difficulty * 20;
-				if (probability < 5) probability = 5;
-				if (probability > 90) probability = 90;
+				LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+				int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
 				int roll = _vm->_rnd.getRandomNumber(99);
 				debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
 
 				if (roll < probability) {
 					if (!_rebelInvulnerable) {
-						int damageAmount = 5 + (_difficulty * 2);
+						int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
 						_playerDamage += damageAmount;
 						if (_playerDamage > 255) _playerDamage = 255;
 						debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
@@ -466,7 +465,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 		// Direct damage: par4==100, separate from par3 branches (lines 99-111)
 		if (par4 == 100 && !isBitSet(srcIdBody0)) {
 			if (!_rebelInvulnerable) {
-				int directHitDamage = 8 + (_difficulty * 4);
+				LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
+				int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
 				_playerDamage += directHitDamage;
 				if (_playerDamage > 255) _playerDamage = 255;
 				debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
@@ -485,7 +485,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 			_rebelHitCounter++;
 			debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
 
-			int directHitDamage = 8 + (_difficulty * 4);
+			LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
+			int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
 
 			if (par4 != 0 && directHitDamage > 0) {
 				bool shouldDamage = false;
@@ -517,16 +518,15 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 		debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
 
 		if (!isBitSet(srcId)) {
-			int probability = 20 + _difficulty * 20;
-			if (probability < 5) probability = 5;
-			if (probability > 90) probability = 90;
+			LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+			int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
 
 			int roll = _vm->_rnd.getRandomNumber(99);
 			debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
 
 			if (roll < probability) {
 				if (!_rebelInvulnerable) {
-					int damageAmount = 5 + (_difficulty * 2);
+					int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
 					_playerDamage += damageAmount;
 					if (_playerDamage > 255) _playerDamage = 255;
 					debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
@@ -860,6 +860,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 		// --- Step 7: Corridor collision — mode 0/2 only (lines 257-292) ---
 		if (_flyControlMode == 0 || _flyControlMode == 2) {
+			LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
+			int corridorWallDmg = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
+
 			// Right boundary (lines 258-270)
 			// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
 			if (_corridorRightX < _flyShipScreenX) {
@@ -870,8 +873,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 					_spaceShotDirection = 1;
 					initDamageFlash();
 					if (!_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
+						_playerDamage += corridorWallDmg;
 						if (_playerDamage > 255) _playerDamage = 255;
 					}
 					_rebelHitCounter++;
@@ -887,8 +889,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 					_spaceShotDirection = 0;
 					initDamageFlash();
 					if (!_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
-						_playerDamage += damage;
+						_playerDamage += corridorWallDmg;
 						if (_playerDamage > 255) _playerDamage = 255;
 					}
 					_rebelHitCounter++;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index c298f0f7e09..06ef3445784 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -1262,28 +1262,20 @@ int InsaneRebel2::runLevelSelect() {
 	//   kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
 	//   kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
 	//   kLevelSelectQuit (2) = Quit game
-	//
-	// Original action dispatch (FUN_00414A41):
-	//   sel < N        → saved pilot selected → return 3 (start game)
-	//   sel == N       → ADD NEW PILOT → difficulty submenu → loop back
-	//   sel == N+1     → COPY PILOT → source select (no-op if N==0)
-	//   sel == N+2     → DELETE PILOT → confirm select (no-op if N==0)
-	//   sel == N+3     → RETURN TO MAIN MENU → return 1
-	//   ESC            → return 1
 
-	debug("Rebel2: Entering pilot selection (FUN_00414A41)");
+	debug("Rebel2: Entering pilot selection (FUN_00414A41), %d pilots loaded", _numPilots);
 
 	_menuInputActive = true;
 	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
 
-	// Number of saved pilots (TODO: implement save system)
-	int numPilots = 0;
-
 	_levelSelection = 0;
-	_levelItemCount = numPilots + 4;  // N pilots + NEW/COPY/DELETE/MAIN MENU
+	_levelItemCount = _numPilots + 4;  // N pilots + NEW/COPY/DELETE/MAIN MENU
 	_selectedLevel = 1;
 	_menuRepeatDelay = 0;
 	_gameState = kStatePilotSelect;
+	_pilotMenuMode = kPilotModeSelect;
+	_pilotNameInput = "";
+	_pilotEditIndex = -1;
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
@@ -1292,8 +1284,6 @@ int InsaneRebel2::runLevelSelect() {
 		_vm->_smushVideoShouldFinish = false;
 
 		Common::String menuVideo = getRandomMenuVideo();
-		debug("Rebel2: Playing pilot select background: %s", menuVideo.c_str());
-
 		splayer->setCurVideoFlags(0x20);
 		splayer->play(menuVideo.c_str(), 12);
 
@@ -1302,60 +1292,102 @@ int InsaneRebel2::runLevelSelect() {
 			return kLevelSelectQuit;
 		}
 
-		if (!_vm->_smushVideoShouldFinish) {
+		if (!_vm->_smushVideoShouldFinish)
 			continue;
-		}
 
 		_vm->_smushVideoShouldFinish = false;
 
-		// Dispatch based on current game state
-		if (_gameState == kStateDifficultySelect) {
-			// Difficulty submenu — selection made
-			debug("Rebel2: Difficulty %d selected", _difficultySelection);
-			// Original stores difficulty in pilot record and loops back.
-			// Since we have no save system, proceed to chapter select.
+		// --- Difficulty submenu completed ---
+		if (_pilotMenuMode == kPilotModeDifficulty) {
+			// Store difficulty in the new pilot and save
+			if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+				_pilots[_pilotEditIndex].difficulty = _difficultySelection;
+				_difficulty = _difficultySelection;
+				savePilots();
+				_activePilot = _pilotEditIndex;
+
+				// Update chapter unlock state from pilot data
+				for (int i = 0; i < 16; i++) {
+					_chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
+				}
+			}
+			_pilotMenuMode = kPilotModeSelect;
+			_levelItemCount = _numPilots + 4;
 			_gameState = kStatePilotSelect;
 			_menuInputActive = false;
 			return kLevelSelectPlay;
 		}
 
-		// Pilot menu — process selection
-		debug("Rebel2: Pilot selection made: %d (numPilots=%d)", _levelSelection, numPilots);
+		// --- Name input completed ---
+		if (_pilotMenuMode == kPilotModeNameInput) {
+			// Name was confirmed — now show difficulty submenu
+			if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+				Common::strlcpy(_pilots[_pilotEditIndex].name, _pilotNameInput.c_str(),
+				                sizeof(_pilots[_pilotEditIndex].name));
+			}
+			_pilotMenuMode = kPilotModeDifficulty;
+			_gameState = kStateDifficultySelect;
+			_difficultySelection = 2;
+			continue;
+		}
+
+		// --- Normal pilot menu selection ---
+		debug("Rebel2: Pilot selection: %d (numPilots=%d)", _levelSelection, _numPilots);
+
+		if (_levelSelection < _numPilots) {
+			// Existing pilot selected — activate and go to chapter select
+			_activePilot = _levelSelection;
+			_difficulty = _pilots[_activePilot].difficulty;
+
+			// Update chapter unlock state from pilot data
+			for (int i = 0; i < 16; i++) {
+				_chapterUnlocked[i] = _debugUnlockAll || (_pilots[_activePilot].damage[i] < 0xFF);
+			}
 
-		if (_levelSelection < numPilots) {
-			// Saved pilot selected — go to chapter selection
-			_selectedLevel = _levelSelection + 1;
-			debug("Rebel2: Pilot %d selected - going to chapter selection", _selectedLevel);
+			debug("Rebel2: Pilot '%s' selected (slot %d, difficulty %d)",
+			      _pilots[_activePilot].name, _activePilot, _difficulty);
 			_menuInputActive = false;
 			return kLevelSelectPlay;
-		} else if (_levelSelection == numPilots) {
-			// ADD NEW PILOT — show difficulty submenu
-			debug("Rebel2: ADD NEW PILOT - showing difficulty submenu");
-			_gameState = kStateDifficultySelect;
-			_difficultySelection = 2;  // Default to 3rd option (matching original init)
-			// Continue loop — next video frame will render difficulty menu
+
+		} else if (_levelSelection == _numPilots) {
+			// ADD NEW PILOT — create slot, enter name input mode
+			int newIdx = createNewPilot();
+			if (newIdx >= 0) {
+				_pilotEditIndex = newIdx;
+				_pilotNameInput = "";
+				_pilotMenuMode = kPilotModeNameInput;
+				_levelItemCount = _numPilots + 4;
+				debug("Rebel2: NEW PILOT - entering name for slot %d", newIdx);
+			}
 			continue;
-		} else if (_levelSelection == numPilots + 1) {
-			// COPY PILOT — no-op when numPilots==0 (original checks local_10 != 0)
-			if (numPilots > 0) {
-				debug("Rebel2: COPY PILOT selected");
-				// TODO: implement copy pilot sub-flow
-			} else {
-				debug("Rebel2: COPY PILOT - no pilots to copy");
+
+		} else if (_levelSelection == _numPilots + 1) {
+			// COPY PILOT
+			if (_numPilots > 0 && _numPilots < kMaxPilots) {
+				// Copy first pilot (slot 0) by default — original swaps with selected
+				int srcIdx = (_levelSelection > 0 && _levelSelection <= _numPilots) ? _levelSelection - 1 : 0;
+				copyPilot(srcIdx);
+				savePilots();
+				_levelItemCount = _numPilots + 4;
+				debug("Rebel2: Copied pilot %d, now %d pilots", srcIdx, _numPilots);
 			}
 			continue;
-		} else if (_levelSelection == numPilots + 2) {
-			// DELETE PILOT — no-op when numPilots==0 (original checks local_10 != 0)
-			if (numPilots > 0) {
-				debug("Rebel2: DELETE PILOT selected");
-				// TODO: implement delete pilot sub-flow
-			} else {
-				debug("Rebel2: DELETE PILOT - no pilots to delete");
+
+		} else if (_levelSelection == _numPilots + 2) {
+			// DELETE PILOT
+			if (_numPilots > 0) {
+				// Delete the first pilot (slot 0) — original has confirm sub-flow
+				deletePilot(0);
+				savePilots();
+				_levelItemCount = _numPilots + 4;
+				if (_levelSelection >= _levelItemCount)
+					_levelSelection = _levelItemCount - 1;
+				debug("Rebel2: Deleted pilot, %d remaining", _numPilots);
 			}
 			continue;
-		} else if (_levelSelection == numPilots + 3) {
+
+		} else if (_levelSelection == _numPilots + 3) {
 			// RETURN TO MAIN MENU
-			debug("Rebel2: Back to main menu selected");
 			_menuInputActive = false;
 			return kLevelSelectBack;
 		}
@@ -1367,12 +1399,61 @@ int InsaneRebel2::runLevelSelect() {
 
 int InsaneRebel2::processLevelSelectInput() {
 	// Process input for pilot selection and difficulty submenu
-	// Handles both kStatePilotSelect and kStateDifficultySelect modes
+	// Handles kPilotModeSelect, kPilotModeNameInput, and kStateDifficultySelect
 	// Returns: -1 = no action, 0+ = item selected
 
 	int result = -1;
 
-	// Determine which menu mode we're in
+	// Name input mode — keyboard goes to _pilotNameInput instead of menu nav
+	// Original: FUN_00414A41 lines 87-116
+	if (_pilotMenuMode == kPilotModeNameInput) {
+		while (!_menuEventQueue.empty()) {
+			Common::Event event = _menuEventQueue.pop();
+			if (event.type == Common::EVENT_KEYDOWN) {
+				if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+				    event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+					// Confirm name — signal back to runLevelSelect()
+					if (_pilotNameInput.size() > 0) {
+						_vm->_smushVideoShouldFinish = true;
+						debug("PilotName: confirmed '%s'", _pilotNameInput.c_str());
+					}
+				} else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+					// Cancel name entry — delete the pilot slot we created
+					if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+						deletePilot(_pilotEditIndex);
+					}
+					_pilotMenuMode = kPilotModeSelect;
+					_levelItemCount = _numPilots + 4;
+					debug("PilotName: cancelled");
+				} else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
+					// Backspace — remove last character
+					if (_pilotNameInput.size() > 0) {
+						_pilotNameInput.deleteLastChar();
+					}
+				} else {
+					// Printable ASCII (0x20-0x7E), max 15 chars
+					char c = (char)event.kbd.ascii;
+					if (c >= 0x20 && c <= 0x7E &&
+					    (int)_pilotNameInput.size() < kMaxPilotNameLen) {
+						_pilotNameInput += c;
+					}
+				}
+			} else if (event.type == Common::EVENT_MOUSEMOVE) {
+				_vm->_mouse.x = event.mouse.x;
+				_vm->_mouse.y = event.mouse.y;
+			} else if (event.type == Common::EVENT_QUIT ||
+			           event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+				if (_pilotEditIndex >= 0 && _pilotEditIndex < _numPilots) {
+					deletePilot(_pilotEditIndex);
+				}
+				_pilotMenuMode = kPilotModeSelect;
+				_levelItemCount = _numPilots + 4;
+			}
+		}
+		return -1;
+	}
+
+	// Normal menu navigation (pilot select or difficulty submenu)
 	bool isDifficultyMode = (_gameState == kStateDifficultySelect);
 	int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
 	int itemCount = isDifficultyMode ? 6 : _levelItemCount;
@@ -1391,8 +1472,6 @@ int InsaneRebel2::processLevelSelectInput() {
 				if (selection < 0) {
 					selection = itemCount - 1;
 				}
-				debug("LevelSelect: Selection changed to %d (UP, %s)",
-				      selection, isDifficultyMode ? "difficulty" : "pilot");
 				break;
 
 			case Common::KEYCODE_DOWN:
@@ -1400,30 +1479,20 @@ int InsaneRebel2::processLevelSelectInput() {
 				if (selection >= itemCount) {
 					selection = 0;
 				}
-				debug("LevelSelect: Selection changed to %d (DOWN, %s)",
-				      selection, isDifficultyMode ? "difficulty" : "pilot");
 				break;
 
 			case Common::KEYCODE_RETURN:
 			case Common::KEYCODE_KP_ENTER:
 				if (selection >= 0 && selection < itemCount) {
 					result = selection;
-					debug("LevelSelect: Item %d selected (ENTER, %s)",
-					      selection, isDifficultyMode ? "difficulty" : "pilot");
 				}
 				break;
 
 			case Common::KEYCODE_ESCAPE:
 				if (isDifficultyMode) {
-					// ESC in difficulty submenu — return to pilot menu
-					// Note: original has no ESC in difficulty submenu, but we add it as a
-					// graceful fallback
 					_gameState = kStatePilotSelect;
-					debug("LevelSelect: ESC in difficulty - back to pilot menu");
 				} else {
-					// ESC in pilot menu — back to main menu (return 1 in original)
 					result = _levelItemCount - 1;  // Last item = MAIN MENU
-					debug("LevelSelect: ESC pressed - back to main menu");
 				}
 				break;
 
@@ -1435,15 +1504,11 @@ int InsaneRebel2::processLevelSelectInput() {
 		case Common::EVENT_LBUTTONDOWN:
 			{
 				int mouseY = event.mouse.y;
-				debug("LevelSelect: Click at Y=%d (%s)", mouseY,
-				      isDifficultyMode ? "difficulty" : "pilot");
-
 				for (int i = 0; i < itemCount; i++) {
 					int itemY = baseY + i * itemHeight;
 					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
 						selection = i;
 						result = i;
-						debug("LevelSelect: Item %d clicked at Y=%d (itemY=%d)", i, mouseY, itemY);
 						break;
 					}
 				}
@@ -1506,22 +1571,35 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	// Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
 	// =====================================================================
 	// items[0]    = title (TRS 20)
-	// items[1..N] = saved pilots (formatted with ^f01^c005)
+	// items[1..N] = saved pilots (formatted with ^f01^c005<name>^f00)
 	// items[N+1]  = TRS 21 (ADD NEW PILOT)
 	// items[N+2]  = TRS 22 (COPY PILOT)
 	// items[N+3]  = TRS 23 (DELETE PILOT)
 	// items[N+4]  = TRS 24 (RETURN TO MAIN MENU)
 
-	int numPilots = 0;  // TODO: implement save system
+	// Build pilot name strings with font/color formatting
+	// Original uses "^f01^c005<name>^f00" for pilot entries
+	Common::String pilotNameStrs[kMaxPilots];
+	for (int i = 0; i < _numPilots; i++) {
+		if (_pilotMenuMode == kPilotModeNameInput && i == _pilotEditIndex) {
+			// Show name being typed with cursor (underscore)
+			pilotNameStrs[i] = Common::String::format("^f01^c005%s_^f00", _pilotNameInput.c_str());
+		} else {
+			pilotNameStrs[i] = Common::String::format("^f01^c005%s^f00", _pilots[i].name);
+		}
+	}
 
-	const char *pilotItems[11];
+	// Max items: 1 title + 10 pilots + 4 options = 15
+	const char *pilotItems[15];
 	int idx = 0;
 
 	// Title: TRS 20
 	pilotItems[idx++] = splayer->getString(20);
 
-	// Saved pilot slots would be inserted here (items[1..numPilots])
-	// Each formatted as "^f01^c005<name>^f00" by the original
+	// Saved pilot slots
+	for (int i = 0; i < _numPilots; i++) {
+		pilotItems[idx++] = pilotNameStrs[i].c_str();
+	}
 
 	// Fixed options: TRS 21-24
 	for (int i = 0; i < 4; i++) {
@@ -1534,11 +1612,7 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 		}
 	}
 
-	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, numPilots + 4, _levelSelection);
-
-	// Pilot info display at fixed coordinates when saved pilot selected
-	// FUN_00414A41 lines 78-86: FUN_00434cb0 at Y=0xb4(180) and Y=0xbe(190)
-	// TODO: Implement when save system is added
+	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
 }
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index d288980a18d..9f59af3d288 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1176,9 +1176,9 @@ void InsaneRebel2::checkCollisionZones() {
 
 		if (collision) {
 			// Collision detected — apply damage from collision damage table
-			// Original: DAT_0047a7ec += DAT_0047e0f6[levelIdx]
-			// TODO: Read from per-level collision damage table DAT_0047e0f6
-			int collisionDamage = 3 + (_difficulty * 2);
+			// Original: DAT_0047a7ec += DAT_0047e0f6[chapter * 0x242 + level * 0x22]
+			LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+			int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
 
 			if (!_rebelInvulnerable) {
 				_playerDamage += collisionDamage;
@@ -1254,7 +1254,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					_hitCooldown = 10;
 					_spaceShotDirection = zone.filterValue + 2;
 
-					int collisionDamage = 3 + (_difficulty * 2);
+					LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
+					int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
 					if (!_rebelInvulnerable) {
 						_playerDamage += collisionDamage;
 						if (_playerDamage > 255) _playerDamage = 255;
@@ -1282,6 +1283,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 		// Ship position is clamped to wall boundaries when hitting.
 		int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f;  // local_14
 		const int16 vMargin = 0x0f;  // local_20
+		LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
+		int wallDamage = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
 
 		for (int i = 0; i < _primaryZoneCount; i++) {
 			CollisionZone &zone = _primaryZones[i];
@@ -1298,7 +1301,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 				if (_flyShipScreenY < edgeY) {
 					// Ship above top wall — push down
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
+						int damage = wallDamage;
 						_playerDamage += damage;
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
@@ -1320,7 +1323,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 				if (edgeY < _flyShipScreenY) {
 					// Ship below bottom wall — push up
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
+						int damage = wallDamage;
 						_playerDamage += damage;
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
@@ -1342,7 +1345,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					// Ship left of left wall — push right
 					_flyShipScreenX = edgeX;  // Push-back
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
+						int damage = wallDamage;
 						_playerDamage += damage;
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
@@ -1363,7 +1366,7 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					// Ship right of right wall — push left
 					_flyShipScreenX = edgeX;  // Push-back
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = 3 + (_difficulty * 2);
+						int damage = wallDamage;
 						_playerDamage += damage;
 						if (_playerDamage > 255) _playerDamage = 255;
 						_rebelHitCounter++;
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 0c5ea36f7b7..ec0e19e74e2 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2728,12 +2728,17 @@ Common::Error ScummEngine::go() {
 					// Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
 					int result = rebel->runLevel(selectedLevel);
 
+					// Save pilot progress after level completion
+					if (result == InsaneRebel2::kLevelNextLevel) {
+						rebel->updatePilotProgress(selectedLevel - 1,
+							rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
+					}
+
 					if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
 						break;
 					}
 
 					// After level completion or game over, return to menu
-					// Could also handle kLevelNextLevel to auto-start next level
 				}
 				// If kChapterSelectBack, loop back to main menu
 			}


Commit: 9f75d49f081fdeb0e89e7eb99bce214736547f4e
    https://github.com/scummvm/scummvm/commit/9f75d49f081fdeb0e89e7eb99bce214736547f4e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:12+02:00

Commit Message:
SCUMM: RA2: Tune difficulty parameters

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 4c690606fa7..ad9204e895e 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -563,82 +563,121 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 }
 
 // Per-level difficulty parameters extracted from RA2WIN95.EXE at VA 0x47e0f0
-// Original: 2D table indexed by DAT_0047a7fa (chapter/difficulty) * 0x242 + DAT_0047a7f8 (level+1) * 0x22
-// -1 = not applicable for this level type (e.g., no walls in turret levels)
-const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][15] = {
-	// Difficulty 0 (Easy) - levels 1-15
+// Original: 2D table indexed by DAT_0047a7fa (difficulty) * 0x242 + DAT_0047a7f8 (levelType) * 0x22
+// 17 fields per entry (34 bytes), 17 entries per difficulty, 5 difficulties
+// Field names from official Difficulty Editor: {laserDelay, snapDistance, missDamage, dodgeDamage,
+//   shotDamage, specialDamage, shotAccuracy, hitPoints, dodgePoints, timePoints,
+//   levelPoints, specialPoints, flags, rollRate, liftRate, slideRate, driftRate}
+// -1 = not applicable for this level type
+const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17] = {
+	// Difficulty 0 (Beginner) - 17 level types
 	{
-		{  30,   15,    3,   75}, {  17,   18,    2,   75}, {  -1,   18,    3,   75},
-		{  19,   -1,    2,   75}, {  27,  180,    3,   75}, {  40,   -1,   -1,   -1},
-		{  -1,   21,    3,   50}, {  30,   15,    4,   60}, {  30,   -1,   70,   75},
-		{  -1,   10,    5,   65}, {  -1,   -1,    6,   65}, {  30,   20,    1,   85},
-		{  -1,   24,    2,   75}, {  -1,   -1,    3,   75}, {  30,  255,    4,   75},
+		{   5,    3,   15,   -1,    2,   -1,   75,   25,   -1,    2,  500,  250,    8,    5,    5,    6,   -1}, // Lv1
+		{   4,    3,   -1,   -1,    2,   -1,   40,   25,   -1,    0,  500,  250,    8,   90,   90,  120,   25}, // Lv2
+		{   6,    5,   15,   30,    3,   12,   75,   25,   50,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv3
+		{   5,    3,   18,   17,    2,   20,   75,   25,   50,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv4
+		{   5,    4,   18,   -1,    3,   -1,   75,   25,   -1,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv5
+		{   5,    5,   -1,   19,    2,   15,   75,   25,   50,    2,  500,   -1,    8,   -1,   -1,   -1,   -1}, // Lv6A
+		{   5,    5,  180,   27,    3,   -1,   75,   25,   50,    2,  500,  250,    8,  120,  120,  120,   75}, // Lv6B
+		{  -1,   -1,   -1,   40,   -1,   -1,   -1,   -1,   50,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv7
+		{   5,    5,   21,   -1,    3,   -1,   50,   25,   -1,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv8
+		{   5,    6,   15,   30,    4,   -1,   60,   25,   50,    2,  500,  250,    8,   90,   90,   90,  135}, // Lv9
+		{   5,   15,   -1,   30,   70,   -1,   75,   25,   50,    2,  500,  250,    8,   10,    6,    7,   -1}, // Lv10
+		{   4,    4,   10,   -1,    5,   -1,   65,   25,   -1,    0,  500,  250,    8,    5,    6,    7,    8}, // Lv11
+		{   4,    2,   -1,   -1,    6,   -1,   65,   25,   -1,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv12
+		{   5,    6,   20,   30,    1,   20,   85,   25,   50,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv13
+		{   5,    3,   24,   -1,    2,   -1,   75,   25,   -1,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv14
+		{   5,    8,   -1,   -1,    3,   -1,   75,   25,   -1,    2,  500,   -1,    8,   -1,   -1,   -1,   -1}, // Lv15A
+		{   5,    6,  255,   30,    4,   10,   75,   25,   50,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 1 (Medium) - levels 1-15
+	// Difficulty 1 (Easy) - 17 level types
 	{
-		{  35,   17,    5,   75}, {  30,   60,    4,   75}, {  -1,   28,    3,   75},
-		{  25,   -1,    4,   75}, {  35,  190,    4,   75}, {  65,   -1,   -1,   -1},
-		{  -1,   24,    3,   50}, {  45,   17,    5,   75}, {  35,   -1,   75,   75},
-		{  -1,   15,    5,   75}, {  -1,   -1,    8,   75}, {  35,   30,    1,   85},
-		{  -1,   28,    2,   75}, {  -1,   -1,    4,   75}, {  35,  255,    4,   75},
+		{   6,    1,   25,   -1,    3,   -1,   75,   50,   -1,    4, 1000,  500,   16,    6,    6,    7,   -1}, // Lv1
+		{   4,    2,   -1,   -1,    4,   -1,   40,   50,   -1,    0, 1000,  500,   16,  100,  100,  135,   30}, // Lv2
+		{   6,    4,   17,   35,    5,   12,   75,   50,  100,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv3
+		{   5,    2,   60,   30,    4,   20,   75,   50,  100,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv4
+		{   5,    1,   28,   -1,    3,   -1,   75,   50,   -1,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv5
+		{   5,    2,   -1,   25,    4,   15,   75,   50,  100,    4, 1000,   -1,   16,   -1,   -1,   -1,   -1}, // Lv6A
+		{   5,    2,  190,   35,    4,   -1,   75,   50,  100,    4, 1000,  500,   16,  140,  140,  140,   90}, // Lv6B
+		{  -1,   -1,   -1,   65,   -1,   -1,   -1,   -1,  100,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv7
+		{   5,    3,   24,   -1,    3,   -1,   50,   50,   -1,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv8
+		{   5,    4,   17,   45,    5,   -1,   75,   50,  100,    4, 1000,  500,   16,  100,  100,  100,  140}, // Lv9
+		{   5,   12,   -1,   35,   75,   -1,   75,   50,  100,    4, 1000,  500,   16,   10,    6,    7,   -1}, // Lv10
+		{   4,    2,   15,   -1,    5,   -1,   75,   50,   -1,    0, 1000,  500,   16,    5,    6,    7,    8}, // Lv11
+		{   4,    1,   -1,   -1,    8,   -1,   75,   50,   -1,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv12
+		{   5,    5,   30,   35,    1,   20,   85,   50,  100,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv13
+		{   5,    2,   28,   -1,    2,   -1,   75,   50,   -1,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv14
+		{   5,    7,   -1,   -1,    4,   -1,   75,   50,   -1,    4, 1000,   -1,   16,   -1,   -1,   -1,   -1}, // Lv15A
+		{   5,    6,  255,   35,    4,   10,   75,   50,  100,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 2 (Hard) - levels 1-15
+	// Difficulty 2 (Medium) - 17 level types
 	{
-		{  38,   20,    7,   75}, {  35,  100,    6,   75}, {  -1,   30,    4,   75},
-		{  30,   -1,    6,   75}, {  50,  200,   12,   75}, {  80,   -1,   -1,   -1},
-		{  -1,   27,    3,   60}, {  60,   19,    7,   75}, {  40,   -1,  100,   85},
-		{  -1,   20,    6,   75}, {  -1,   -1,   11,   75}, {  40,   40,    3,   76},
-		{  -1,   38,    4,   75}, {  -1,   -1,    7,   75}, {  40,  255,    7,   75},
+		{   7,    0,   35,   -1,    5,   -1,   75,   75,   -1,    6, 1500,  750,    0,    7,    7,    8,   -1}, // Lv1
+		{   4,    1,   -1,   -1,    6,   -1,   40,   75,   -1,    0, 1500,  750,    0,  110,  110,  150,   35}, // Lv2
+		{   6,    1,   20,   38,    7,   12,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv3
+		{   5,    1,  100,   35,    6,   20,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv4
+		{   6,    1,   30,   -1,    4,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv5
+		{   6,    1,   -1,   30,    6,   15,   75,   75,  150,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv6A
+		{   6,    1,  200,   50,   12,   -1,   75,   75,  150,    6, 1500,  750,    0,  160,  160,  160,  105}, // Lv6B
+		{  -1,   -1,   -1,   80,   -1,   -1,   -1,   -1,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv7
+		{   5,    1,   27,   -1,    3,   -1,   60,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv8
+		{   5,    3,   19,   60,    7,   -1,   75,   75,  150,    6, 1500,  750,    0,  110,  110,  110,  150}, // Lv9
+		{   5,    9,   -1,   40,  100,   -1,   85,   75,  150,    6, 1500,  750,    0,   11,    7,    8,   -1}, // Lv10
+		{   4,    1,   20,   -1,    6,   -1,   75,   75,   -1,    0, 1500,  750,    0,    6,    7,    8,    9}, // Lv11
+		{   4,    0,   -1,   -1,   11,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv12
+		{   5,    3,   40,   40,    3,   15,   76,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv13
+		{   5,    0,   38,   -1,    4,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv14
+		{   5,    4,   -1,   -1,    7,   -1,   75,   75,   -1,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv15A
+		{   5,    5,  255,   40,    7,   10,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 3 (Expert) - levels 1-15
+	// Difficulty 3 (Hard) - 17 level types
 	{
-		{  42,   23,   12,   75}, {  50,  120,   16,   75}, {  -1,   55,    4,   75},
-		{  50,    0,   15,   79}, {  90,  220,   15,   90}, {  90,   -1,   -1,   -1},
-		{  -1,   35,    3,   68}, {  75,   30,   20,   80}, {  50,   -1,  110,   90},
-		{  -1,   30,    7,   75}, {  -1,   -1,   13,   75}, {  55,   55,    5,   77},
-		{  -1,   49,    4,   75}, {  -1,   -1,   10,   79}, {  45,  255,    8,   80},
+		{   8,    0,   77,   -1,    7,   -1,   80,  100,   -1,    8, 2000, 1000,    4,    8,    8,    9,   -1}, // Lv1
+		{   4,    0,   -1,   -1,    7,   -1,   40,  100,   -1,    0, 2000, 1000,    4,  120,  120,  165,   40}, // Lv2
+		{   6,    0,   23,   42,   12,   10,   75,  100,  200,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv3
+		{   5,    0,  120,   50,   16,   10,   75,  100,  200,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv4
+		{   5,    0,   55,   -1,    4,   -1,   75,  100,   -1,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv5
+		{   6,    0,    0,   50,   15,   15,   79,  100,  200,    8, 2000,   -1,    4,   -1,   -1,   -1,   -1}, // Lv6A
+		{   6,    0,  220,   90,   15,   -1,   90,  100,  200,    8, 2000, 1000,    4,  180,  180,  180,  140}, // Lv6B
+		{  -1,   -1,   -1,   90,   -1,   -1,   -1,   -1,  200,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv7
+		{   5,    0,   35,   -1,    3,   -1,   68,  100,   -1,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv8
+		{   5,    2,   30,   75,   20,   -1,   80,  100,  200,    8, 2000, 1000,    4,  120,  120,  120,  200}, // Lv9
+		{   5,    8,   -1,   50,  110,   -1,   90,  100,  200,    8, 2000, 1000,    4,   12,    8,    9,   -1}, // Lv10
+		{   4,    0,   30,   -1,    7,   -1,   75,  100,   -1,    0, 2000, 1000,    4,    7,    8,    9,   10}, // Lv11
+		{   4,    0,   -1,   -1,   13,   -1,   75,  100,   -1,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv12
+		{   5,    3,   55,   55,    5,   12,   77,  100,  200,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv13
+		{   5,    0,   49,   -1,    4,   -1,   75,  100,   -1,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv14
+		{   5,    4,   -1,   -1,   10,   -1,   79,  100,   -1,    8, 2000,   -1,    4,   -1,   -1,   -1,   -1}, // Lv15A
+		{   5,    4,  255,   45,    8,    5,   80,  100,  200,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 4 (identical to difficulty 2 in original data) - levels 1-15
+	// Difficulty 4 (Jedi) — identical to difficulty 2 (Medium) in original data
 	{
-		{  38,   20,    7,   75}, {  35,  100,    6,   75}, {  -1,   30,    4,   75},
-		{  30,   -1,    6,   75}, {  50,  200,   12,   75}, {  80,   -1,   -1,   -1},
-		{  -1,   27,    3,   60}, {  60,   19,    7,   75}, {  40,   -1,  100,   85},
-		{  -1,   20,    6,   75}, {  -1,   -1,   11,   75}, {  40,   40,    3,   76},
-		{  -1,   38,    4,   75}, {  -1,   -1,    7,   75}, {  40,  255,    7,   75},
+		{   7,    0,   35,   -1,    5,   -1,   75,   75,   -1,    6, 1500,  750,    0,    7,    7,    8,   -1}, // Lv1
+		{   4,    1,   -1,   -1,    6,   -1,   40,   75,   -1,    0, 1500,  750,    0,  110,  110,  150,   35}, // Lv2
+		{   6,    1,   20,   38,    7,   12,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv3
+		{   5,    1,  100,   35,    6,   20,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv4
+		{   6,    1,   30,   -1,    4,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv5
+		{   6,    1,   -1,   30,    6,   15,   75,   75,  150,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv6A
+		{   6,    1,  200,   50,   12,   -1,   75,   75,  150,    6, 1500,  750,    0,  160,  160,  160,  105}, // Lv6B
+		{  -1,   -1,   -1,   80,   -1,   -1,   -1,   -1,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv7
+		{   5,    1,   27,   -1,    3,   -1,   60,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv8
+		{   5,    3,   19,   60,    7,   -1,   75,   75,  150,    6, 1500,  750,    0,  110,  110,  110,  150}, // Lv9
+		{   5,    9,   -1,   40,  100,   -1,   85,   75,  150,    6, 1500,  750,    0,   11,    7,    8,   -1}, // Lv10
+		{   4,    1,   20,   -1,    6,   -1,   75,   75,   -1,    0, 1500,  750,    0,    6,    7,    8,    9}, // Lv11
+		{   4,    0,   -1,   -1,   11,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv12
+		{   5,    3,   40,   40,    3,   15,   76,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv13
+		{   5,    0,   38,   -1,    4,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv14
+		{   5,    4,   -1,   -1,    7,   -1,   75,   75,   -1,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv15A
+		{   5,    5,  255,   40,    7,   10,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv15B
 	},
 };
 
-InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams(int levelId) const {
+InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
 	int diff = CLIP(_difficulty, 0, 4);
-	int lvIdx = CLIP(levelId - 1, 0, 14);  // levelId 1-15 → index 0-14
+	int lvIdx = CLIP((int)_rebelLevelType, 0, 16);
 	return kDifficultyTable[diff][lvIdx];
 }
 
-// Score lookup tables (from DAT_0047e0fe, DAT_0047e100, DAT_0047e102)
-// These are indexed by: DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
-// For simplicity, we use fixed values based on difficulty level
-// Values estimated from disassembly patterns (actual values would need extraction from game data)
-const int16 InsaneRebel2::kScoreTableEnemyDestroy[16] = {
-	100, 100, 100, 100,   // Easy (difficulty 0)
-	150, 150, 150, 150,   // Medium (difficulty 1)
-	200, 200, 200, 200,   // Hard (difficulty 2)
-	250, 250, 250, 250    // Expert (difficulty 3)
-};
-
-const int16 InsaneRebel2::kScoreTableSpecial[16] = {
-	50, 50, 50, 50,
-	75, 75, 75, 75,
-	100, 100, 100, 100,
-	125, 125, 125, 125
-};
-
-const int16 InsaneRebel2::kScoreTableTimeBonus[16] = {
-	1, 1, 1, 1,
-	2, 2, 2, 2,
-	3, 3, 3, 3,
-	4, 4, 4, 4
-};
-
 // Score system implementation (FUN_0041bf8d equivalent)
 // Adds points to score and awards bonus life when crossing threshold
 void InsaneRebel2::addScore(int points) {
@@ -948,23 +987,16 @@ int32 InsaneRebel2::processMouse() {
 					it->id, it->type, mousePos.x, mousePos.y,
 					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
 
-				// Spawn visual explosion based on handler and enemy type.
-				//
-				// Each handler's explosion rendering (FUN_409FBC, FUN_402696,
-				// FUN_40F1C5, FUN_41F29A) checks a per-level flags field:
-				//   (*(ushort *)(&DAT_0047e108 + chapter*0x242 + level*0x22) & 1) == 0
-				// When bit 0 is SET, explosion NUT sprites are NOT rendered even
-				// though the counter ticks down. The flags come from GAME.TRS.
+				// Spawn visual explosion based on handler, enemy type, and flags.
 				//
-				// Handler 8 (FUN_4028C5 line 94): Only type 0 sets the explosion
-				// counter at all. Types 1-4 get BLAST sound, no visual explosion.
-				//
-				// Handler 25 (FUN_41E7C2 line 74): Types > 3 DO set the counter,
-				// but rendering is suppressed by flags & 1 for on-foot levels.
-				// The counter serves only as a timer (sound panning, tracking).
-				// Handler 25 is specifically for on-foot corridor/FPS sections;
-				// space combat uses handler 7 instead.
+				// Rendering functions (FUN_409FBC, FUN_402696, FUN_40F1C5,
+				// FUN_41F29A) check DAT_0047e108 flags & 1 — when set,
+				// explosion NUT sprites are suppressed. This is checked
+				// during rendering in renderExplosions().
 				//
+				// Handler 8 (FUN_4028C5): Only type 0 spawns explosion.
+				// Handler 25 (FUN_41E7C2): Types > 3 set counter but
+				// rendering suppressed by flags bit 0.
 				// Handlers 0x26, 7: All types get visual explosions.
 				if (_rebelHandler != 8 && _rebelHandler != 25) {
 					spawnExplosion((it->rect.left + it->rect.right) / 2,
@@ -974,6 +1006,12 @@ int32 InsaneRebel2::processMouse() {
 					spawnExplosion((it->rect.left + it->rect.right) / 2,
 								   (it->rect.top + it->rect.bottom) / 2,
 								   it->rect.width() / 2);
+				} else if (_rebelHandler == 25 && it->type > 3) {
+					// Counter is set for timing/sound, but rendering
+					// may be suppressed by flags bit 0
+					spawnExplosion((it->rect.left + it->rect.right) / 2,
+								   (it->rect.top + it->rect.bottom) / 2,
+								   it->rect.width() / 2);
 				}
 
 				// Disable self (prevents sprite from rendering via SKIP chunks)
@@ -1023,10 +1061,12 @@ int32 InsaneRebel2::processMouse() {
 				}
 
 				// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
-				// Score value comes from lookup table DAT_0047e0fe indexed by difficulty
-				int scoreIndex = _difficulty * 4;  // Simplified index
-				if (scoreIndex >= 0 && scoreIndex < 16) {
-					addScore(kScoreTableEnemyDestroy[scoreIndex]);
+				// Score value comes from DAT_0047e0fe indexed by difficulty×level
+				{
+					LevelDifficultyParams dparams = getDifficultyParams();
+					if (dparams.hitPoints > 0) {
+						addScore(dparams.hitPoints);
+					}
 				}
 
 				// Only hit one enemy per click
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index bb2abaa4cd4..73f780de4a1 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -983,37 +983,45 @@ public:
 
 	// ======================= Per-Level Difficulty Parameters =======================
 	// Extracted from RA2WIN95.EXE at VA 0x47e0f0
-	// 2D table indexed by difficulty (0-4) × game level (1-15)
-	// Original indexing: &DAT_0047e0f6 + chapter * 0x242 + level * 0x22
-	// -1 = not applicable for this level type (e.g., no walls in turret levels)
+	// 2D table indexed by difficulty (0-4) × level type (0-16)
+	// Original indexing: &DAT_0047e0f0 + chapter * 0x242 + levelType * 0x22
+	// Level type (_rebelLevelType) is set by IACT opcode 6 par3
+	// 17 entries: Lv1-5(0-4), Lv6A/6B(5-6), Lv7-14(7-14), Lv15A/15B(15-16)
+	// -1 = not applicable for this level type
 
 	struct LevelDifficultyParams {
-		int16 wallDamage;           // +0x06 DAT_0047e0f6: Wall/obstacle collision damage
-		int16 directHitDamage;      // +0x04 DAT_0047e0f4: Direct hit damage (par4=100)
-		int16 enemyProjectileDamage;// +0x08 DAT_0047e0f8: Enemy projectile/probabilistic damage
-		int16 hitProbability;       // +0x0C DAT_0047e0fc: Enemy hit chance (0-100, -1=disabled)
+		int16 laserDelay;      // +0x00: Laser fire delay (lower = faster)
+		int16 snapDistance;    // +0x02: Crosshair snap distance to targets
+		int16 missDamage;      // +0x04: Damage from enemy misses / grazing hits
+		int16 dodgeDamage;     // +0x06: Damage from wall/obstacle collisions
+		int16 shotDamage;      // +0x08: Damage from enemy projectile hits
+		int16 specialDamage;   // +0x0A: Damage from special attacks
+		int16 shotAccuracy;    // +0x0C: Enemy shot accuracy (0-100, -1=disabled)
+		int16 hitPoints;       // +0x0E: Points awarded for destroying an enemy
+		int16 dodgePoints;     // +0x10: Points awarded for dodging an obstacle
+		int16 timePoints;      // +0x12: Time bonus points
+		int16 levelPoints;     // +0x14: End-of-level bonus points
+		int16 specialPoints;   // +0x16: Special action bonus points
+		int16 flags;           // +0x18: Behavior flags bitfield
+		int16 rollRate;        // +0x1A: Ship roll rate (flight controls)
+		int16 liftRate;        // +0x1C: Ship lift rate (flight controls)
+		int16 slideRate;       // +0x1E: Ship slide rate (flight controls)
+		int16 driftRate;       // +0x20: Ship drift rate (flight controls)
 	};
 
-	// Table: 5 difficulty levels × 15 game levels
-	// Difficulty 4 is identical to difficulty 2 in the original data
-	static const LevelDifficultyParams kDifficultyTable[5][15];
+	// Table: 5 difficulty levels × 17 level types
+	// Difficulty 4 (Jedi) is identical to difficulty 2 (Medium) in the original data
+	static const LevelDifficultyParams kDifficultyTable[5][17];
 
-	// Look up difficulty parameters for current difficulty and level
-	// Returns the entry from kDifficultyTable, clamping difficulty to 0-4
-	// levelId is 1-based (1-15)
-	LevelDifficultyParams getDifficultyParams(int levelId) const;
+	// Look up difficulty parameters for current _difficulty and _rebelLevelType
+	LevelDifficultyParams getDifficultyParams() const;
 
 	// Score system (FUN_0041bf8d equivalent)
 	// Adds points to score and awards bonus life when crossing threshold
 	void addScore(int points);
 
-	// Score lookup tables (indices into per-level point values)
-	// DAT_0047e0fe: Points for destroying enemies
-	// DAT_0047e100: Points for certain special events
-	// DAT_0047e102: Points awarded per frame (time bonus)
-	static const int16 kScoreTableEnemyDestroy[16];  // Per difficulty/level
-	static const int16 kScoreTableSpecial[16];
-	static const int16 kScoreTableTimeBonus[16];
+	// Score lookup uses LevelDifficultyParams fields:
+	// hitPoints (DAT_0047e0fe), dodgePoints (DAT_0047e100), timePoints (DAT_0047e102)
 
 	// Render score text to HUD (called from procPostRendering)
 	void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index a6ff44b1aaf..72697f8c4b0 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -434,14 +434,14 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 				srcIdBody1, isBitSet(srcIdBody1), _rebelDamageLevel);
 
 			if (_rebelDamageLevel < 2 && !isBitSet(srcIdBody1)) {
-				LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
-				int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
+				LevelDifficultyParams params = getDifficultyParams();
+				int probability = (params.shotAccuracy >= 0) ? params.shotAccuracy : 0;
 				int roll = _vm->_rnd.getRandomNumber(99);
 				debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
 
 				if (roll < probability) {
 					if (!_rebelInvulnerable) {
-						int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
+						int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
 						_playerDamage += damageAmount;
 						if (_playerDamage > 255) _playerDamage = 255;
 						debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
@@ -465,8 +465,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 		// Direct damage: par4==100, separate from par3 branches (lines 99-111)
 		if (par4 == 100 && !isBitSet(srcIdBody0)) {
 			if (!_rebelInvulnerable) {
-				LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
-				int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
+				LevelDifficultyParams dparams = getDifficultyParams();
+				int directHitDamage = (dparams.missDamage >= 0) ? dparams.missDamage : 0;
 				_playerDamage += directHitDamage;
 				if (_playerDamage > 255) _playerDamage = 255;
 				debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
@@ -485,8 +485,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 			_rebelHitCounter++;
 			debug("Rebel2: Incremented hit counter -> %d", _rebelHitCounter);
 
-			LevelDifficultyParams dparams = getDifficultyParams(_selectedLevel);
-			int directHitDamage = (dparams.directHitDamage >= 0) ? dparams.directHitDamage : 0;
+			LevelDifficultyParams dparams = getDifficultyParams();
+			int directHitDamage = (dparams.missDamage >= 0) ? dparams.missDamage : 0;
 
 			if (par4 != 0 && directHitDamage > 0) {
 				bool shouldDamage = false;
@@ -518,15 +518,15 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 		debug("Rebel2 Opcode3: par3=5 srcId=%d isBitSet=%d", srcId, isBitSet(srcId));
 
 		if (!isBitSet(srcId)) {
-			LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
-			int probability = (params.hitProbability >= 0) ? params.hitProbability : 0;
+			LevelDifficultyParams params = getDifficultyParams();
+			int probability = (params.shotAccuracy >= 0) ? params.shotAccuracy : 0;
 
 			int roll = _vm->_rnd.getRandomNumber(99);
 			debug("Rebel2 Opcode3: probability=%d roll=%d (need roll < prob)", probability, roll);
 
 			if (roll < probability) {
 				if (!_rebelInvulnerable) {
-					int damageAmount = (params.enemyProjectileDamage >= 0) ? params.enemyProjectileDamage : 0;
+					int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
 					_playerDamage += damageAmount;
 					if (_playerDamage > 255) _playerDamage = 255;
 					debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
@@ -860,8 +860,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 		// --- Step 7: Corridor collision — mode 0/2 only (lines 257-292) ---
 		if (_flyControlMode == 0 || _flyControlMode == 2) {
-			LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
-			int corridorWallDmg = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
+			LevelDifficultyParams wallParams = getDifficultyParams();
+			int corridorWallDmg = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
 
 			// Right boundary (lines 258-270)
 			// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 9f59af3d288..93a8a79cbee 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1177,8 +1177,8 @@ void InsaneRebel2::checkCollisionZones() {
 		if (collision) {
 			// Collision detected — apply damage from collision damage table
 			// Original: DAT_0047a7ec += DAT_0047e0f6[chapter * 0x242 + level * 0x22]
-			LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
-			int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
+			LevelDifficultyParams params = getDifficultyParams();
+			int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
 
 			if (!_rebelInvulnerable) {
 				_playerDamage += collisionDamage;
@@ -1192,7 +1192,10 @@ void InsaneRebel2::checkCollisionZones() {
 		} else {
 			// Safely passed — award score bonus
 			// Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
-			addScore(1);
+			LevelDifficultyParams scoreParams = getDifficultyParams();
+			if (scoreParams.dodgePoints > 0) {
+				addScore(scoreParams.dodgePoints);
+			}
 		}
 	}
 }
@@ -1254,8 +1257,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					_hitCooldown = 10;
 					_spaceShotDirection = zone.filterValue + 2;
 
-					LevelDifficultyParams params = getDifficultyParams(_selectedLevel);
-					int collisionDamage = (params.wallDamage >= 0) ? params.wallDamage : 0;
+					LevelDifficultyParams params = getDifficultyParams();
+					int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
 					if (!_rebelInvulnerable) {
 						_playerDamage += collisionDamage;
 						if (_playerDamage > 255) _playerDamage = 255;
@@ -1269,7 +1272,11 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					break;  // Only one collision per frame (original breaks)
 				} else {
 					// Safely avoided obstacle — award score
-					addScore(1);
+					// Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
+					LevelDifficultyParams scoreParams = getDifficultyParams();
+					if (scoreParams.dodgePoints > 0) {
+						addScore(scoreParams.dodgePoints);
+					}
 				}
 			}
 		}
@@ -1283,8 +1290,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 		// Ship position is clamped to wall boundaries when hitting.
 		int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f;  // local_14
 		const int16 vMargin = 0x0f;  // local_20
-		LevelDifficultyParams wallParams = getDifficultyParams(_selectedLevel);
-		int wallDamage = (wallParams.wallDamage >= 0) ? wallParams.wallDamage : 0;
+		LevelDifficultyParams wallParams = getDifficultyParams();
+		int wallDamage = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
 
 		for (int i = 0; i < _primaryZoneCount; i++) {
 			CollisionZone &zone = _primaryZones[i];
@@ -2693,7 +2700,25 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 // Dispatcher — calls per-handler explosion render function.
 // Original code has separate functions per handler, each with its own
 // position transformation, scale thresholds, and secondary NUT rendering.
+// Each handler's render function checks DAT_0047e108 flags & 1:
+// when bit 0 is set, explosion NUT sprites are suppressed (counter still ticks).
 void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
+	// Check flags bit 0: suppress explosion sprite rendering
+	LevelDifficultyParams dparams = getDifficultyParams();
+	bool suppressExplosionSprites = (dparams.flags & 1) != 0;
+
+	// Even when suppressed, still tick down explosion counters
+	if (suppressExplosionSprites) {
+		for (int i = 0; i < 5; i++) {
+			if (_explosions[i].active && _explosions[i].counter > 0) {
+				_explosions[i].counter--;
+				if (_explosions[i].counter <= 0)
+					_explosions[i].active = false;
+			}
+		}
+		return;
+	}
+
 	switch (_rebelHandler) {
 	case 0x26:
 		renderTurretExplosions(renderBitmap, pitch, width, height);


Commit: 9506f4af42656c1b2f60b1d808e6c7161ed42b7c
    https://github.com/scummvm/scummvm/commit/9506f4af42656c1b2f60b1d808e6c7161ed42b7c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:12+02:00

Commit Message:
SCUMM: RA2: Apply code conventions

Changed paths:
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/insane/insane_rebel_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 72697f8c4b0..a4300324b92 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -443,7 +443,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 					if (!_rebelInvulnerable) {
 						int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
 						_playerDamage += damageAmount;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 						debug("Rebel2: H25 PROBABILISTIC damage from %d. Damage=%d total=%d",
 							srcIdBody1, damageAmount, _playerDamage);
 					}
@@ -468,7 +469,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 				LevelDifficultyParams dparams = getDifficultyParams();
 				int directHitDamage = (dparams.missDamage >= 0) ? dparams.missDamage : 0;
 				_playerDamage += directHitDamage;
-				if (_playerDamage > 255) _playerDamage = 255;
+				if (_playerDamage > 255)
+					_playerDamage = 255;
 				debug("Rebel2: H25 DIRECT HIT par4=100 damage=%d total=%d",
 					directHitDamage, _playerDamage);
 			}
@@ -502,7 +504,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 				if (shouldDamage) {
 					if (!_rebelInvulnerable) {
 						_playerDamage += directHitDamage;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 						debug("Rebel2: DIRECT HIT damage from enemy %d. par3=%d par4=%d damage=%d total=%d",
 							srcId, par3, par4, directHitDamage, _playerDamage);
 					}
@@ -528,7 +531,8 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 				if (!_rebelInvulnerable) {
 					int damageAmount = (params.shotDamage >= 0) ? params.shotDamage : 0;
 					_playerDamage += damageAmount;
-					if (_playerDamage > 255) _playerDamage = 255;
+					if (_playerDamage > 255)
+						_playerDamage = 255;
 					debug("Rebel2: PROBABILISTIC damage from enemy %d. Damage=%d total=%d",
 						srcId, damageAmount, _playerDamage);
 				}
@@ -631,11 +635,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 			// Clamp X offset to movement range limit (covered/shooting state)
 			// Based on FUN_00401234 lines 119-136
-			if (mouseOffsetX > _movementRangeLimit) mouseOffsetX = _movementRangeLimit;
-			if (mouseOffsetX < -_movementRangeLimit) mouseOffsetX = -_movementRangeLimit;
+			if (mouseOffsetX > _movementRangeLimit)
+				mouseOffsetX = _movementRangeLimit;
+			if (mouseOffsetX < -_movementRangeLimit)
+				mouseOffsetX = -_movementRangeLimit;
 			// Y offset always uses full range (±127)
-			if (mouseOffsetY > 127) mouseOffsetY = 127;
-			if (mouseOffsetY < -127) mouseOffsetY = -127;
+			if (mouseOffsetY > 127)
+				mouseOffsetY = 127;
+			if (mouseOffsetY < -127)
+				mouseOffsetY = -127;
 
 			// Calculate target positions using the original formula
 			_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
@@ -673,20 +681,32 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			}
 
 			// Horizontal: 5 zones (0=far left, 2=center, 4=far right)
-			if (mouseX < 64) _shipDirectionH = 0;
-			else if (mouseX < 128) _shipDirectionH = 1;
-			else if (mouseX < 192) _shipDirectionH = 2;
-			else if (mouseX < 256) _shipDirectionH = 3;
-			else _shipDirectionH = 4;
+			if (mouseX < 64)
+				_shipDirectionH = 0;
+			else if (mouseX < 128)
+				_shipDirectionH = 1;
+			else if (mouseX < 192)
+				_shipDirectionH = 2;
+			else if (mouseX < 256)
+				_shipDirectionH = 3;
+			else
+				_shipDirectionH = 4;
 
 			// Vertical: 7 zones (0=far up, 3=center, 6=far down)
-			if (mouseY < 28) _shipDirectionV = 0;
-			else if (mouseY < 57) _shipDirectionV = 1;
-			else if (mouseY < 86) _shipDirectionV = 2;
-			else if (mouseY < 114) _shipDirectionV = 3;
-			else if (mouseY < 143) _shipDirectionV = 4;
-			else if (mouseY < 171) _shipDirectionV = 5;
-			else _shipDirectionV = 6;
+			if (mouseY < 28)
+				_shipDirectionV = 0;
+			else if (mouseY < 57)
+				_shipDirectionV = 1;
+			else if (mouseY < 86)
+				_shipDirectionV = 2;
+			else if (mouseY < 114)
+				_shipDirectionV = 3;
+			else if (mouseY < 143)
+				_shipDirectionV = 4;
+			else if (mouseY < 171)
+				_shipDirectionV = 5;
+			else
+				_shipDirectionV = 6;
 
 			_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
 		}
@@ -757,10 +777,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		int16 inputY = (int16)(_vm->_mouse.y - 100);  // DAT_0047a7e2
 
 		// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
-		if (inputX > 160) inputX = 160;
-		if (inputX < -160) inputX = -160;
-		if (inputY > 127) inputY = 127;
-		if (inputY < -127) inputY = -127;
+		if (inputX > 160)
+			inputX = 160;
+		if (inputX < -160)
+			inputX = -160;
+		if (inputY > 127)
+			inputY = 127;
+		if (inputY < -127)
+			inputY = -127;
 
 		// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
 		// Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
@@ -827,8 +851,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 
 		// Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
-		if (positionDeltaX < -11) positionDeltaX = -12;
-		if (positionDeltaX > 11) positionDeltaX = 12;
+		if (positionDeltaX < -11)
+			positionDeltaX = -12;
+		if (positionDeltaX > 11)
+			positionDeltaX = 12;
 
 		// Apply X delta (line 193 / 237)
 		_flyShipScreenX += positionDeltaX;
@@ -838,8 +864,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			// Mode 1: clamped to ±12 with wind (lines 194-216)
 			int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
 			int yDelta = yCalc >> 10;
-			if (yDelta < -12) yDelta = -12;
-			if (yDelta > 12) yDelta = 12;
+			if (yDelta < -12)
+				yDelta = -12;
+			if (yDelta > 12)
+				yDelta = 12;
 			_flyShipScreenY -= (int16)yDelta;
 		} else {
 			// Mode 0/2/3: unclamped (lines 238-241)
@@ -853,10 +881,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
 
 		// --- Step 6: Position clamping (lines 245-256) ---
-		if (_flyShipScreenX > 0x194) _flyShipScreenX = 0x194;  // 404
-		if (_flyShipScreenY > 0xF0) _flyShipScreenY = 0xF0;    // 240
-		if (_flyShipScreenX < 0x14) _flyShipScreenX = 0x14;    // 20
-		if (_flyShipScreenY < 0x14) _flyShipScreenY = 0x14;    // 20
+		if (_flyShipScreenX > 0x194)
+			_flyShipScreenX = 0x194;  // 404
+		if (_flyShipScreenY > 0xF0)
+			_flyShipScreenY = 0xF0;    // 240
+		if (_flyShipScreenX < 0x14)
+			_flyShipScreenX = 0x14;    // 20
+		if (_flyShipScreenY < 0x14)
+			_flyShipScreenY = 0x14;    // 20
 
 		// --- Step 7: Corridor collision — mode 0/2 only (lines 257-292) ---
 		if (_flyControlMode == 0 || _flyControlMode == 2) {
@@ -868,13 +900,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			if (_corridorRightX < _flyShipScreenX) {
 				_flyShipScreenX = _corridorRightX;
 				if (_hitCooldown < 5) {
-					for (int i = 0; i < 25; i++) _velocityHistory[i] = -127;
+					for (int i = 0; i < 25; i++)
+						_velocityHistory[i] = -127;
 					_hitCooldown = 10;
 					_spaceShotDirection = 1;
 					initDamageFlash();
 					if (!_rebelInvulnerable) {
 						_playerDamage += corridorWallDmg;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 					}
 					_rebelHitCounter++;
 					playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
@@ -884,13 +918,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			if (_flyShipScreenX < _corridorLeftX) {
 				_flyShipScreenX = _corridorLeftX;
 				if (_hitCooldown < 5) {
-					for (int i = 0; i < 25; i++) _velocityHistory[i] = 127;
+					for (int i = 0; i < 25; i++)
+						_velocityHistory[i] = 127;
 					_hitCooldown = 10;
 					_spaceShotDirection = 0;
 					initDamageFlash();
 					if (!_rebelInvulnerable) {
 						_playerDamage += corridorWallDmg;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 					}
 					_rebelHitCounter++;
 					playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
@@ -918,7 +954,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			} else {
 				_perspectiveX = 0;
 			}
-			if (_flyShipScreenX < 0xd5) _perspectiveX = -_perspectiveX;
+			if (_flyShipScreenX < 0xd5)
+				_perspectiveX = -_perspectiveX;
 
 			int absOffY = ABS(_flyShipScreenY - 0x82);
 			int16 focalY = 0x19;  // Far view default for Level 3
@@ -928,25 +965,32 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			} else {
 				_perspectiveY = 0;
 			}
-			if (_flyShipScreenY < 0x83) _perspectiveY = -_perspectiveY;
+			if (_flyShipScreenY < 0x83)
+				_perspectiveY = -_perspectiveY;
 		}
 
 		// View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
 		_viewShift = _smoothedVelocity;
-		if (_viewShift > 127) _viewShift = 127;
-		if (_viewShift < -127) _viewShift = -127;
+		if (_viewShift > 127)
+			_viewShift = 127;
+		if (_viewShift < -127)
+			_viewShift = -127;
 
 		// --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
 		// 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
 		// vDir from vertical input: (0xa0 - verticalInput) >> 6
 		int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
-		if (vDir < 0) vDir = 0;
-		if (vDir > 4) vDir = 4;
+		if (vDir < 0)
+			vDir = 0;
+		if (vDir > 4)
+			vDir = 4;
 
 		// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
 		int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
-		if (hDir < 0) hDir = 0;
-		if (hDir > 6) hDir = 6;
+		if (hDir < 0)
+			hDir = 0;
+		if (hDir > 6)
+			hDir = 6;
 
 		// Hysteresis at center (lines 90-97, 98-105)
 		if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
@@ -957,8 +1001,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 
 		_shipDirectionIndex = vDir * 7 + hDir;
-		if (_shipDirectionIndex < 0) _shipDirectionIndex = 0;
-		if (_shipDirectionIndex > 34) _shipDirectionIndex = 34;
+		if (_shipDirectionIndex < 0)
+			_shipDirectionIndex = 0;
+		if (_shipDirectionIndex > 34)
+			_shipDirectionIndex = 34;
 
 		_shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
 
@@ -1165,10 +1211,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 				if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
 				if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
-				if (destX + drawWidth > pitch) drawWidth = pitch - destX;
-				if (destY + drawHeight > bufHeight) drawHeight = bufHeight - destY;
-				if (drawWidth > corridorOverlay.width - srcOffsetX) drawWidth = corridorOverlay.width - srcOffsetX;
-				if (drawHeight > corridorOverlay.height - srcOffsetY) drawHeight = corridorOverlay.height - srcOffsetY;
+				if (destX + drawWidth > pitch)
+					drawWidth = pitch - destX;
+				if (destY + drawHeight > bufHeight)
+					drawHeight = bufHeight - destY;
+				if (drawWidth > corridorOverlay.width - srcOffsetX)
+					drawWidth = corridorOverlay.width - srcOffsetX;
+				if (drawHeight > corridorOverlay.height - srcOffsetY)
+					drawHeight = corridorOverlay.height - srcOffsetY;
 
 				if (drawWidth > 0 && drawHeight > 0) {
 					for (int y = 0; y < drawHeight; y++) {
@@ -1961,7 +2011,8 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
 		int textLen = 0;
 		while (textLen < (int)sizeof(textBuffer) - 1) {
 			byte ch = b.readByte();
-			if (ch == 0 || b.eos()) break;
+			if (ch == 0 || b.eos())
+				break;
 			textBuffer[textLen++] = ch;
 		}
 		textBuffer[textLen] = '\0';
@@ -1985,10 +2036,14 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
 
 	// Apply coordinate clamping (from FUN_004033cf disassembly)
 	// Low-res: X clamped to [16, 304], Y clamped to [16, 196]
-	if (posX < 16) posX = 16;
-	if (posX > 304) posX = 304;
-	if (posY < 16) posY = 16;
-	if (posY > 196) posY = 196;
+	if (posX < 16)
+		posX = 16;
+	if (posX > 304)
+		posX = 304;
+	if (posY < 16)
+		posY = 16;
+	if (posY > 196)
+		posY = 196;
 
 	// Use the message font loaded during initialization (DIHIFONT.NUT)
 	if (!_rebelMsgFont) {
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 07e4b0087b5..432ad35bb01 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -70,7 +70,8 @@ void InsaneRebel2::playIntroSequence() {
 	splayer->setCurVideoFlags(0x28);
 	splayer->play("OPEN/O_OPEN_A.SAN", 12);
 
-	if (_vm->shouldQuit()) return;
+	if (_vm->shouldQuit())
+		return;
 
 	// Play additional intro (OPEN/O_OPEN_B.SAN)
 	// Original: conditional on DAT_0047ab45 || DAT_0047ab47
@@ -165,7 +166,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Part 1 (multiple variations - play A for now)
 		splayer->setCurVideoFlags(0x28);
@@ -173,7 +175,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		debug("Rebel2: Playing %s", filename.c_str());
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Post segment 1
 		_rebelHandler = 0;
@@ -183,7 +186,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Part 2
 		_rebelHandler = 8;
@@ -192,7 +196,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		debug("Rebel2: Playing %s", filename.c_str());
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Post segment 2
 		_rebelHandler = 0;
@@ -202,7 +207,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Part 3
 		_rebelHandler = 8;
@@ -220,7 +226,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		debug("Rebel2: Playing %s", filename.c_str());
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Post segment
 		_rebelHandler = 0;
@@ -230,7 +237,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Phase 2 — handler will be re-set by IACT opcode 6
 		splayer->setCurVideoFlags(0x28);
@@ -247,7 +255,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit()) return false;
+		if (_vm->shouldQuit())
+			return false;
 
 		splayer->setCurVideoFlags(0x28);
 		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
@@ -268,7 +277,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		debug("Rebel2: Playing %s", filename.c_str());
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Post segment
 		_rebelHandler = 0;
@@ -278,7 +288,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 		splayer->setCurVideoFlags(0x28);
 		splayer->play(filename.c_str(), 12);
 
-		if (_vm->shouldQuit() || _playerShield == 0) return false;
+		if (_vm->shouldQuit() || _playerShield == 0)
+			return false;
 
 		// Phase 2 — handler will be re-set by IACT opcode 6
 		splayer->setCurVideoFlags(0x28);
@@ -496,18 +507,27 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 		// Level 3: Based on death frame and phase
 		if (phase == 1) {
 			// Phase 1 death video selection (from FUN_0041885F lines 80-96)
-			if (frame < 0x10c) return "B";       // < 268
-			if (frame < 0x1a9) return "A";       // < 425
-			if (frame < 0x247) return "C";       // < 583
-			if (frame < 700) return "A";
-			if (frame < 900) return "B";
+			if (frame < 0x10c)
+				return "B";       // < 268
+			if (frame < 0x1a9)
+				return "A";       // < 425
+			if (frame < 0x247)
+				return "C";       // < 583
+			if (frame < 700)
+				return "A";
+			if (frame < 900)
+				return "B";
 			return "A";
 		} else {
 			// Phase 2 death video selection (from FUN_0041885F lines 53-67)
-			if (frame < 0x2f1) return "A";       // < 753
-			if (frame < 0x347) return "B";       // < 839
-			if (frame < 0x3b1) return "C";       // < 945
-			if (frame < 0x405) return "A";       // < 1029
+			if (frame < 0x2f1)
+				return "A";       // < 753
+			if (frame < 0x347)
+				return "B";       // < 839
+			if (frame < 0x3b1)
+				return "C";       // < 945
+			if (frame < 0x405)
+				return "A";       // < 1029
 			return "C";
 		}
 
@@ -523,29 +543,49 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 		// Level 6 (FUN_004190D6): Phase-based with detailed frame selection
 		if (phase == 1) {
 			// DAT_0047a7f8 == 5 (phase 1)
-			if (frame < 0x4e) return "D";
-			if (frame < 0xe0) return "A";
-			if (frame < 0x122) return "D";
-			if (frame < 0x1b4) return "B";
-			if (frame < 499) return "D";
-			if (frame < 0x286) return "C";
+			if (frame < 0x4e)
+				return "D";
+			if (frame < 0xe0)
+				return "A";
+			if (frame < 0x122)
+				return "D";
+			if (frame < 0x1b4)
+				return "B";
+			if (frame < 499)
+				return "D";
+			if (frame < 0x286)
+				return "C";
 			return "D";
 		} else {
 			// DAT_0047a7f8 == 6 (phase 2)
-			if (frame < 0xcc) return "E";
-			if (frame < 0xfe) return "G";
-			if (frame < 0x122) return "E";
-			if (frame < 0x149) return "G";
-			if (frame < 0x166) return "F";
-			if (frame < 0x174) return "E";
-			if (frame < 0x19f) return "F";
-			if (frame < 0x1b2) return "G";
-			if (frame < 0x1c8) return "F";
-			if (frame < 0x207) return "E";
-			if (frame < 0x217) return "F";
-			if (frame < 0x23b) return "G";
-			if (frame < 0x25b) return "F";
-			if (frame < 0x285) return "E";
+			if (frame < 0xcc)
+				return "E";
+			if (frame < 0xfe)
+				return "G";
+			if (frame < 0x122)
+				return "E";
+			if (frame < 0x149)
+				return "G";
+			if (frame < 0x166)
+				return "F";
+			if (frame < 0x174)
+				return "E";
+			if (frame < 0x19f)
+				return "F";
+			if (frame < 0x1b2)
+				return "G";
+			if (frame < 0x1c8)
+				return "F";
+			if (frame < 0x207)
+				return "E";
+			if (frame < 0x217)
+				return "F";
+			if (frame < 0x23b)
+				return "G";
+			if (frame < 0x25b)
+				return "F";
+			if (frame < 0x285)
+				return "E";
 			return "G";
 		}
 
@@ -571,8 +611,10 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 	case 11:
 		// Level 11 (FUN_0041A00C): Phase-based death videos
 		// Phase 1 → DIE_A, Phase 2 → DIE_B, Phase 3 → DIE_C
-		if (phase <= 1) return "A";
-		if (phase == 2) return "B";
+		if (phase <= 1)
+			return "A";
+		if (phase == 2)
+			return "B";
 		return "C";
 
 	case 12:
@@ -581,10 +623,14 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 
 	case 13:
 		// Level 13 (FUN_0041B3E1): Frame-based
-		if (frame < 0x1c2) return "A";
-		if (frame < 0x302) return "B";
-		if (frame < 0x4ec) return "C";
-		if (frame < 0x5b4) return "B";
+		if (frame < 0x1c2)
+			return "A";
+		if (frame < 0x302)
+			return "B";
+		if (frame < 0x4ec)
+			return "C";
+		if (frame < 0x5b4)
+			return "B";
 		return "D";
 
 	case 14:
@@ -593,12 +639,18 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 
 	case 15:
 		// Level 15 (FUN_0041B8D7): Frame-based with many thresholds
-		if (frame < 0x21e) return "A";
-		if (frame < 0x2f9) return "B";
-		if (frame < 0x3e5) return "C";
-		if (frame < 0x4a0) return "B";
-		if (frame < 0x588) return "C";
-		if (frame < 0x65e) return "B";
+		if (frame < 0x21e)
+			return "A";
+		if (frame < 0x2f9)
+			return "B";
+		if (frame < 0x3e5)
+			return "C";
+		if (frame < 0x4a0)
+			return "B";
+		if (frame < 0x588)
+			return "C";
+		if (frame < 0x65e)
+			return "B";
 		return "D";
 
 	default:
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 06ef3445784..4ccbc90adca 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -186,8 +186,10 @@ int InsaneRebel2::processMenuInput() {
 				int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
 
 				// Clamp to valid range
-				if (newSelection < 0) newSelection = 0;
-				if (newSelection >= _menuItemCount) newSelection = _menuItemCount - 1;
+				if (newSelection < 0)
+					newSelection = 0;
+				if (newSelection >= _menuItemCount)
+					newSelection = _menuItemCount - 1;
 
 				// Only update if within menu area (not too far above/below)
 				int topY = baseY - 5;
@@ -282,7 +284,8 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	// =====================================================================
 	//   ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
 	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
-		if (*str != '^') return -1;
+		if (*str != '^')
+			return -1;
 
 		const char *p = str + 1;
 		if (*p == '^') {
@@ -329,10 +332,12 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
 				continue;
 			}
-			if (fontChange == -2) continue;
+			if (fontChange == -2)
+				continue;
 
 			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c >= 'a' && c <= 'z')
+				c = c - 'a' + 'A';
 			if (curFont && c < curFont->getNumChars()) {
 				w += curFont->getCharWidth(c);
 			}
@@ -352,14 +357,18 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
 				continue;
 			}
-			if (fontChange == -2) continue;
+			if (fontChange == -2)
+				continue;
 
 			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c >= 'a' && c <= 'z')
+				c = c - 'a' + 'A';
 
-			if (!curFont) continue;
+			if (!curFont)
+				continue;
 			int numChars = curFont->getNumChars();
-			if (c >= numChars) continue;
+			if (c >= numChars)
+				continue;
 
 			int charW = curFont->getCharWidth(c);
 
@@ -416,10 +425,14 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 
 			int screenW = _vm->_screenWidth;
 			int screenH = _vm->_screenHeight;
-			if (leftX < 0) leftX = 0;
-			if (rightX >= screenW) rightX = screenW - 1;
-			if (topY < 0) topY = 0;
-			if (bottomY >= screenH) bottomY = screenH - 1;
+			if (leftX < 0)
+				leftX = 0;
+			if (rightX >= screenW)
+				rightX = screenW - 1;
+			if (topY < 0)
+				topY = 0;
+			if (bottomY >= screenH)
+				bottomY = screenH - 1;
 
 			// FUN_004292d0 - Draw rectangle border (4 lines)
 			for (int x = leftX; x <= rightX && x < screenW; x++) {
@@ -609,7 +622,8 @@ void InsaneRebel2::showPauseOverlay() {
 		const char *p = pauseText;
 		while (*p) {
 			byte c = (byte)*p++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c >= 'a' && c <= 'z')
+				c = c - 'a' + 'A';
 			if (c < numFontChars) {
 				textWidth += font->getCharWidth(c);
 			}
@@ -622,7 +636,8 @@ void InsaneRebel2::showPauseOverlay() {
 		p = pauseText;
 		while (*p) {
 			byte c = (byte)*p++;
-			if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+			if (c >= 'a' && c <= 'z')
+				c = c - 'a' + 'A';
 			if (c < numFontChars && textX >= 0 && textY >= 0) {
 				font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
 				                 kStyleAlignLeft, c, true, true);
@@ -654,7 +669,9 @@ int InsaneRebel2::runMainMenu() {
 
 	// Enable menu input capture via EventObserver
 	_menuInputActive = true;
-	while (!_menuEventQueue.empty()) _menuEventQueue.pop();  // Clear any stale events
+	// Clear any stale events
+	while (!_menuEventQueue.empty())
+		_menuEventQueue.pop();
 
 	// Get the SmushPlayer from ScummEngine_v7
 	// Note: _player isn't set until SmushPlayer::initAudio() is called during playback
@@ -789,7 +806,8 @@ int InsaneRebel2::runChapterSelect() {
 
 	// Enable menu input capture
 	_menuInputActive = true;
-	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+	while (!_menuEventQueue.empty())
+		_menuEventQueue.pop();
 
 	// Initialize chapter selection state
 	// Original (lines 51-54): local_10 = 0xf; while (local_10 > 0 && locked) local_10--;
@@ -1073,21 +1091,24 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 	// This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
 	for (int py = 0; py < thumbH; py++) {
 		int dy = destY + py;
-		if (dy < 0 || dy >= height) continue;
+		if (dy < 0 || dy >= height)
+			continue;
 
 		// Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
 		byte bgColor = 0x10 + (py * 8 / thumbH);
 
 		for (int px = 0; px < thumbW; px++) {
 			int dx = destX + px;
-			if (dx < 0 || dx >= width) continue;
+			if (dx < 0 || dx >= width)
+				continue;
 			renderBitmap[dy * pitch + dx] = bgColor;
 		}
 	}
 
 	// Draw chapter number overlay in the center of the preview
 	NutRenderer *font = _smush_smalfontNut;
-	if (!font) return;
+	if (!font)
+		return;
 
 	char chapterStr[16];
 	if (chapter < 15) {
@@ -1170,13 +1191,16 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 // For unlocked chapters: score display using TRS 80 at (25, 190)
 // For locked chapters: password prompt at (30, 190)
 void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
-	if (_chapterSelection < 0 || _chapterSelection >= 16) return;
+	if (_chapterSelection < 0 || _chapterSelection >= 16)
+		return;
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) return;
+	if (!splayer)
+		return;
 
 	NutRenderer *font = _smush_smalfontNut;
-	if (!font) return;
+	if (!font)
+		return;
 
 	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
 	int actualPitch = _vm->_screenWidth;
@@ -1184,13 +1208,15 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 	if (_chapterUnlocked[_chapterSelection]) {
 		// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
 		const char *scoreStr = splayer->getString(80);
-		if (!scoreStr || !scoreStr[0]) return;
+		if (!scoreStr || !scoreStr[0])
+			return;
 
 		int curX = 25;
 		int numChars = font->getNumChars();
 		for (const char *c = scoreStr; *c; c++) {
 			byte ch = (byte)*c;
-			if (ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
+			if (ch >= 'a' && ch <= 'z')
+				ch = ch - 'a' + 'A';
 			if (ch < numChars) {
 				int charW = font->getCharWidth(ch);
 				if (curX >= 0 && curX + charW <= width && 190 < height) {
@@ -1220,26 +1246,30 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	// FUN_0041f5ae(0, &DAT_004577a8, 0x11, 1): param_3=17, param_4=1 (left-aligned)
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) return;
+	if (!splayer)
+		return;
 
 	// Build items array matching original DAT_004577a8 layout
 	const char *items[18];
 
 	// items[0] = title = TRS 40 ("^f02Chapters")
 	items[0] = splayer->getString(40);
-	if (!items[0] || !items[0][0]) items[0] = "^f02Chapters";
+	if (!items[0] || !items[0][0])
+		items[0] = "^f02Chapters";
 
 	// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
 	for (int i = 1; i <= 16; i++) {
 		bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
 		int trsIdx = unlocked ? (40 + i) : (60 + i);
 		items[i] = splayer->getString(trsIdx);
-		if (!items[i] || !items[i][0]) items[i] = "";
+		if (!items[i] || !items[i][0])
+			items[i] = "";
 	}
 
 	// items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
 	items[17] = splayer->getString(57);
-	if (!items[17] || !items[17][0]) items[17] = "^f01^c240RETURN TO PILOTS";
+	if (!items[17] || !items[17][0])
+		items[17] = "^f01^c240RETURN TO PILOTS";
 
 	// Render menu using shared renderer with left-aligned mode
 	drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
@@ -1266,7 +1296,8 @@ int InsaneRebel2::runLevelSelect() {
 	debug("Rebel2: Entering pilot selection (FUN_00414A41), %d pilots loaded", _numPilots);
 
 	_menuInputActive = true;
-	while (!_menuEventQueue.empty()) _menuEventQueue.pop();
+	while (!_menuEventQueue.empty())
+		_menuEventQueue.pop();
 
 	_levelSelection = 0;
 	_levelItemCount = _numPilots + 4;  // N pilots + NEW/COPY/DELETE/MAIN MENU
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 93a8a79cbee..aeacb6abaee 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -57,7 +57,8 @@ void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int heig
 			int skip = READ_LE_UINT16(src);
 			src += 2;
 			x += skip;
-			if (src >= lineEnd) break;
+			if (src >= lineEnd)
+				break;
 
 			int count = READ_LE_UINT16(src) + 1;
 			src += 2;
@@ -85,7 +86,8 @@ void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int heig
 			int skip = READ_LE_UINT16(src);
 			src += 2;
 			x += skip;
-			if (src >= lineEnd || x >= width) break;
+			if (src >= lineEnd || x >= width)
+				break;
 
 			int runSize = READ_LE_UINT16(src);
 			src += 2;
@@ -95,7 +97,8 @@ void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int heig
 			while (src < runEnd && x < width) {
 				byte code = *src++;
 				int num = (code >> 1) + 1;
-				if (num > width - x) num = width - x;
+				if (num > width - x)
+					num = width - x;
 
 				if (code & 1) {
 					// RLE run
@@ -186,7 +189,8 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
 			int lineSize = READ_LE_UINT16(srcPtr);
 			srcPtr += 2;
-			if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr)) break;
+			if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr))
+				break;
 
 			const byte *lineEnd = srcPtr + lineSize;
 			byte *rowDst = dst + row * width;
@@ -197,7 +201,8 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 				int count = (ctrl >> 1) + 1;
 				if (ctrl & 1) {
 					byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
-					for (int i = 0; i < count && x < width; i++) rowDst[x++] = color;
+					for (int i = 0; i < count && x < width; i++)
+						rowDst[x++] = color;
 				} else {
 					for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
 						rowDst[x++] = *srcPtr++;
@@ -235,7 +240,8 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	// Count non-zero pixels for debug
 	int nonZero = 0;
 	for (int i = 0; i < width * height; i++) {
-		if (dst[i] != 0) nonZero++;
+		if (dst[i] != 0)
+			nonZero++;
 	}
 	debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
 		width, height, nonZero, (nonZero * 100) / (width * height));
@@ -413,7 +419,8 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 							if (frame.valid) {
 								int nonZeroPixels = 0;
 								for (int i = 0; i < width * height; i++) {
-									if (frame.pixels[i] != 0) nonZeroPixels++;
+									if (frame.pixels[i] != 0)
+										nonZeroPixels++;
 								}
 								debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
 									userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
@@ -432,13 +439,15 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 				} else {
 					// Skip other sub-chunks (AHDR inside FRME?) or padding
 					stream.seek(nextSubPos);
-					if (subSize & 1) stream.skip(1);
+					if (subSize & 1)
+						stream.skip(1);
 				}
 			}
 		} else {
 			// Skip non-FRME chunks (AHDR, etc at top level)
 			stream.seek(nextChunkPos);
-			if (chunkSize & 1) stream.skip(1);
+			if (chunkSize & 1)
+				stream.skip(1);
 		}
 	}
 	
@@ -580,10 +589,14 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 					int zoneHeight = (areaHeight > 0) ? areaHeight / 2 : 90;
 					int xZone = (zoneWidth > 0) ? ((zoneWidth / 2) + (x - areaLeft)) / zoneWidth : 2;
 					int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (y - areaTop)) / zoneHeight : 0;
-					if (xZone < 0) xZone = 0;
-					if (xZone > 4) xZone = 4;
-					if (yZone < 0) yZone = 0;
-					if (yZone > 1) yZone = 1;
+					if (xZone < 0)
+						xZone = 0;
+					if (xZone > 4)
+						xZone = 4;
+					if (yZone < 0)
+						yZone = 0;
+					if (yZone > 1)
+						yZone = 1;
 					if (_rebelFlightDir == (yZone & 1)) {
 						xZone = 4 - xZone;
 					}
@@ -592,8 +605,10 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 					spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
 				}
 				int numSprites = _grd002Sprite->getNumChars();
-				if (spriteIdx < 0) spriteIdx = 0;
-				if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+				if (spriteIdx < 0)
+					spriteIdx = 0;
+				if (spriteIdx >= numSprites)
+					spriteIdx = numSprites - 1;
 
 				// Get sprite rendering position (same as in renderHandler25Ship)
 				int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
@@ -659,15 +674,19 @@ void InsaneRebel2::spawnSpaceShot(int x, int y) {
 }
 
 void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
-	if (!nut || spriteIdx >= nut->getNumChars()) return;
+	if (!nut || spriteIdx >= nut->getNumChars())
+		return;
 
 	const byte *srcData = nut->getCharData(spriteIdx);
 	int texW = nut->getCharWidth(spriteIdx);
 	int texH = nut->getCharHeight(spriteIdx);
 	
-	if (!srcData || texW <= 0 || texH <= 0) return;
-	if (v < 0) v = 0;
-	if (v >= texH) v = texH - 1;
+	if (!srcData || texW <= 0 || texH <= 0)
+		return;
+	if (v < 0)
+		v = 0;
+	if (v >= texH)
+		v = texH - 1;
 
 	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
 	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
@@ -675,7 +694,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	
 	// Total length approximation for UV mapping
 	int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
-	if (totalDist == 0) totalDist = 1;
+	if (totalDist == 0)
+		totalDist = 1;
 	
 	int currentDist = 0;
 
@@ -683,7 +703,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
 			// Map currentDist/totalDist to 0..texW (Run along texture width)
 			int u = (currentDist * texW) / totalDist;
-			if (u >= texW) u = texW - 1;
+			if (u >= texW)
+				u = texW - 1;
 			
 			byte color = srcData[v * texW + u];
 			
@@ -693,7 +714,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 			}
 		}
 		
-		if (x0 == x1 && y0 == y1) break;
+		if (x0 == x1 && y0 == y1)
+			break;
 		e2 = 2 * err;
 		if (e2 >= dy) { err += dy; x0 += sx; }
 		if (e2 <= dx) { err += dx; y0 += sy; }
@@ -717,14 +739,17 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 
 	// Clip against screen bounds (translation of original clipping logic)
 	if (px0 == px1) {
-		if (px0 < sVar4 || px0 > sVar7) return;
+		if (px0 < sVar4 || px0 > sVar7)
+			return;
 	} else {
 		if (px0 < sVar4) {
-			if (px1 < sVar4) return;
+			if (px1 < sVar4)
+				return;
 			py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
 			px0 = sVar4;
 		} else if (px0 > sVar7) {
-			if (px1 > sVar7) return;
+			if (px1 > sVar7)
+				return;
 			py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
 			px0 = sVar7;
 		}
@@ -738,14 +763,17 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 	}
 
 	if (py0 == py1) {
-		if (py0 < sVar1 || py0 > sVar10) return;
+		if (py0 < sVar1 || py0 > sVar10)
+			return;
 	} else {
 		if (py0 < sVar1) {
-			if (py1 < sVar1) return;
+			if (py1 < sVar1)
+				return;
 			px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
 			py0 = sVar1;
 		} else if (py0 > sVar10) {
-			if (py1 > sVar10) return;
+			if (py1 > sVar10)
+				return;
 			px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
 			py0 = sVar10;
 		}
@@ -769,7 +797,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 
 	if (absdx == 0) {
 		if (absdy == 0) {
-			if (*texPtr != 0) baseDst[py0 * pitch + px0] = *texPtr;
+			if (*texPtr != 0)
+				baseDst[py0 * pitch + px0] = *texPtr;
 			return;
 		}
 		// vertical-ish
@@ -778,7 +807,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 		int signY = dy > 0 ? 1 : -1;
 		int iVar9 = step; // adv counter
 		for (int i = 0; i < step; i++) {
-			if (*texPtr != 0) baseDst[curY * pitch + px0] = *texPtr;
+			if (*texPtr != 0)
+				baseDst[curY * pitch + px0] = *texPtr;
 			curY += signY;
 			iVar9 -= param_7;
 			while (iVar9 < 0) { texPtr++; iVar9 += step; }
@@ -793,7 +823,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 		int signX = dx > 0 ? 1 : -1;
 		int iVar11 = step;
 		for (int i = 0; i < step; i++) {
-			if (*texPtr != 0) baseDst[py0 * pitch + curX] = *texPtr;
+			if (*texPtr != 0)
+				baseDst[py0 * pitch + curX] = *texPtr;
 			curX += signX;
 			iVar11 -= param_7;
 			while (iVar11 < 0) { texPtr++; iVar11 += step; }
@@ -811,7 +842,8 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 
 	for (int i = 0; i < steps; i++) {
 		if (x >= 0 && x < width && y >= 0 && y < height) {
-			if (*texPtr != 0) baseDst[y * pitch + x] = *texPtr;
+			if (*texPtr != 0)
+				baseDst[y * pitch + x] = *texPtr;
 		}
 		int e2 = 2 * err;
 		if (e2 > -absdy) { err -= absdy; x += sx; }
@@ -893,7 +925,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	byte *texPixels = _laserTexture.pixels;
 
 	// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
-	if (maxFrames == 0) maxFrames = 1;
+	if (maxFrames == 0)
+		maxFrames = 1;
 	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
 
 	// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
@@ -978,7 +1011,8 @@ void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0,
 		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
 			dst[y0 * pitch + x0] = color;
 		}
-		if (x0 == x1 && y0 == y1) break;
+		if (x0 == x1 && y0 == y1)
+			break;
 		e2 = 2 * err;
 		if (e2 >= dy) { err += dy; x0 += sx; }
 		if (e2 <= dx) { err += dx; y0 += sy; }
@@ -989,8 +1023,10 @@ void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int heigh
 	// Draw L-shaped brackets at corners of the rect (x,y,w,h)
 	// Bracket size: approx 8 pixels
 	int armLen = 2;
-	if (armLen > w / 2) armLen = w / 2;
-	if (armLen > h / 2) armLen = h / 2;
+	if (armLen > w / 2)
+		armLen = w / 2;
+	if (armLen > h / 2)
+		armLen = h / 2;
 
 	int x2 = x + w - 1;
 	int y2 = y + h - 1;
@@ -1105,7 +1141,8 @@ void InsaneRebel2::checkCollisionZones() {
 	//   Mouse X 0..320 → centered X ≈ [-52..52] (with smoothing in original)
 	//   Mouse Y 0..200 → centered Y ≈ [-45..45]
 
-	if (_primaryZoneCount == 0) return;
+	if (_primaryZoneCount == 0)
+		return;
 
 	// Calculate aim position in centered coordinates.
 	// Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
@@ -1114,22 +1151,29 @@ void InsaneRebel2::checkCollisionZones() {
 	int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
 
 	// Clamp to original ranges (DAT_0047a7fc < 1 path)
-	if (aimX > 0x34) aimX = 0x34;
-	if (aimX < -0x34) aimX = -0x34;
-	if (aimY > 0x2d) aimY = 0x2d;
-	if (aimY < -0x2d) aimY = -0x2d;
+	if (aimX > 0x34)
+		aimX = 0x34;
+	if (aimX < -0x34)
+		aimX = -0x34;
+	if (aimY > 0x2d)
+		aimY = 0x2d;
+	if (aimY < -0x2d)
+		aimY = -0x2d;
 
 	for (int i = 0; i < _primaryZoneCount; i++) {
 		CollisionZone &zone = _primaryZones[i];
-		if (!zone.active) continue;
+		if (!zone.active)
+			continue;
 
 		// Filter: only process zones with filterValue < 1000 (par4 from IACT header)
 		// Original: *(short *)(*local_c + 6) < 1000
-		if (zone.filterValue >= 1000) continue;
+		if (zone.filterValue >= 1000)
+			continue;
 
 		// Frame check: field2 - 1 == field1
 		// Original: sVar2 + -1 == (int)sVar1
-		if (zone.field2 - 1 != zone.field1) continue;
+		if (zone.field2 - 1 != zone.field1)
+			continue;
 
 		// Center zone vertices by subtracting buffer center (0xD4=212, 0x82=130)
 		// Original: sVar4 = x1 - 0xD4, sVar8 = y1 - 0x82, etc.
@@ -1159,19 +1203,23 @@ void InsaneRebel2::checkCollisionZones() {
 		// Avoid division by zero for degenerate edges
 		if (cx2 != cx1) {
 			int interpY1 = ((aimX - cx1) * (cy2 - cy1)) / (cx2 - cx1) + cy1;
-			if (aimY < interpY1) collision = true;
+			if (aimY < interpY1)
+				collision = true;
 		}
 		if (!collision && cx3 != cx4) {
 			int interpY2 = ((aimX - cx4) * (cy3 - cy4)) / (cx3 - cx4) + cy4;
-			if (interpY2 < aimY) collision = true;
+			if (interpY2 < aimY)
+				collision = true;
 		}
 		if (!collision && cy4 != cy1) {
 			int interpX1 = ((aimY - cy1) * (cx4 - cx1)) / (cy4 - cy1) + cx1;
-			if (aimX < interpX1) collision = true;
+			if (aimX < interpX1)
+				collision = true;
 		}
 		if (!collision && cy3 != cy2) {
 			int interpX2 = ((aimY - cy2) * (cx3 - cx2)) / (cy3 - cy2) + cx2;
-			if (interpX2 < aimX) collision = true;
+			if (interpX2 < aimX)
+				collision = true;
 		}
 
 		if (collision) {
@@ -1182,7 +1230,8 @@ void InsaneRebel2::checkCollisionZones() {
 
 			if (!_rebelInvulnerable) {
 				_playerDamage += collisionDamage;
-				if (_playerDamage > 255) _playerDamage = 255;
+				if (_playerDamage > 255)
+					_playerDamage = 255;
 				debug("Rebel2: COLLISION damage! zone=%d aim=(%d,%d) damage=%d total=%d",
 					i, aimX, aimY, collisionDamage, _playerDamage);
 			}
@@ -1218,7 +1267,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 
 		for (int i = 0; i < _secondaryZoneCount; i++) {
 			CollisionZone &zone = _secondaryZones[i];
-			if (!zone.active) continue;
+			if (!zone.active)
+				continue;
 
 			int x1 = zone.x1, y1 = zone.y1;
 			int x2 = zone.x2, y2 = zone.y2;
@@ -1232,22 +1282,26 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 			// Top edge: interpolate Y along v1→v2 at shipX, +15 margin
 			if (x2 != x1) {
 				int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
-				if (_flyShipScreenY < interpY) inside = false;
+				if (_flyShipScreenY < interpY)
+					inside = false;
 			}
 			// Bottom edge: interpolate Y along v4→v3 at shipX, -15 margin
 			if (inside && x3 != x4) {
 				int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
-				if (interpY < _flyShipScreenY) inside = false;
+				if (interpY < _flyShipScreenY)
+					inside = false;
 			}
 			// Left edge: interpolate X along v1→v4 at shipY, +15 margin
 			if (inside && y4 != y1) {
 				int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
-				if (_flyShipScreenX < interpX) inside = false;
+				if (_flyShipScreenX < interpX)
+					inside = false;
 			}
 			// Right edge: interpolate X along v2→v3 at shipY, -15 margin
 			if (inside && y3 != y2) {
 				int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
-				if (interpX < _flyShipScreenX) inside = false;
+				if (interpX < _flyShipScreenX)
+					inside = false;
 			}
 
 			// Frame match: field2 - 1 == field1 (line 90)
@@ -1261,7 +1315,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
 					if (!_rebelInvulnerable) {
 						_playerDamage += collisionDamage;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 					}
 					_rebelHitCounter++;
 					initDamageFlash();
@@ -1295,7 +1350,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 
 		for (int i = 0; i < _primaryZoneCount; i++) {
 			CollisionZone &zone = _primaryZones[i];
-			if (!zone.active) continue;
+			if (!zone.active)
+				continue;
 
 			int x1 = zone.x1, y1 = zone.y1;
 			int x2 = zone.x2, y2 = zone.y2;
@@ -1310,7 +1366,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
 						int damage = wallDamage;
 						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
 						playSfx(1, 127, 0);  // CRASH.SAD, top wall → center pan
@@ -1332,7 +1389,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
 						int damage = wallDamage;
 						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
 						playSfx(1, 127, 0);  // CRASH.SAD, bottom wall → center pan
@@ -1354,7 +1412,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
 						int damage = wallDamage;
 						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
 						playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
@@ -1375,7 +1434,8 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
 						int damage = wallDamage;
 						_playerDamage += damage;
-						if (_playerDamage > 255) _playerDamage = 255;
+						if (_playerDamage > 255)
+							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
 						playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
@@ -1410,7 +1470,8 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
 	// Draw primary zones (sub-opcode 0x0D - obstacles)
 	for (int i = 0; i < _primaryZoneCount; i++) {
 		CollisionZone &zone = _primaryZones[i];
-		if (!zone.active) continue;
+		if (!zone.active)
+			continue;
 
 		// Apply view offset to convert from video coords to screen coords
 		int x1 = zone.x1 + _viewX;
@@ -1428,7 +1489,8 @@ void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int heigh
 	// Draw secondary zones (sub-opcode 0x0E - boundaries)
 	for (int i = 0; i < _secondaryZoneCount; i++) {
 		CollisionZone &zone = _secondaryZones[i];
-		if (!zone.active) continue;
+		if (!zone.active)
+			continue;
 
 		// Apply view offset
 		int x1 = zone.x1 + _viewX;
@@ -1470,7 +1532,8 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
 // Render NUT sprite with optional horizontal mirroring
 // Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
 void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
-	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars()) return;
+	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
+		return;
 
 	int w = nut->getCharWidth(spriteIdx);
 	int h = nut->getCharHeight(spriteIdx);
@@ -1502,7 +1565,8 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
 		drawH = height - drawY;
 	}
 
-	if (drawW <= 0 || drawH <= 0) return;
+	if (drawW <= 0 || drawH <= 0)
+		return;
 
 	// Draw loop - with optional horizontal mirroring
 	for (int iy = 0; iy < drawH; iy++) {
@@ -1530,8 +1594,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Determine correct pitch for the video buffer (usually 320 for Rebel2)
 	int width = _player->_width;
 	int height = _player->_height;
-	if (width == 0) width = _vm->_screenWidth;
-	if (height == 0) height = _vm->_screenHeight;
+	if (width == 0)
+		width = _vm->_screenWidth;
+	if (height == 0)
+		height = _vm->_screenHeight;
 	int pitch = width;
 
 	// Calculate View/Scroll Offsets
@@ -1541,8 +1607,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	int maxScrollX = width - _vm->_screenWidth;
 	int maxScrollY = height - _vm->_screenHeight;
 	
-	if (maxScrollX < 0) maxScrollX = 0;
-	if (maxScrollY < 0) maxScrollY = 0;
+	if (maxScrollX < 0)
+		maxScrollX = 0;
+	if (maxScrollY < 0)
+		maxScrollY = 0;
 	
 	// Simple linear mapping: Center of screen corresponds to center of buffer
 	_viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
@@ -1957,10 +2025,12 @@ void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int
 
 	for (int y = statusBarY; y < videoHeight; y++) {
 		int destY = y + _viewY;
-		if (destY >= height) continue;
+		if (destY >= height)
+			continue;
 		for (int x = 0; x < videoWidth; x++) {
 			int destX = x + _viewX;
-			if (destX >= pitch) continue;
+			if (destX >= pitch)
+				continue;
 			renderBitmap[destY * pitch + destX] = statusBarBgColor;
 		}
 	}
@@ -1981,10 +2051,14 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
 	// Calculate mouse offset (clamped to -127..127)
 	int mouseOffsetX = (_vm->_mouse.x - 160);
 	int mouseOffsetY = (_vm->_mouse.y - 100);
-	if (mouseOffsetX > 127) mouseOffsetX = 127;
-	if (mouseOffsetX < -127) mouseOffsetX = -127;
-	if (mouseOffsetY > 127) mouseOffsetY = 127;
-	if (mouseOffsetY < -127) mouseOffsetY = -127;
+	if (mouseOffsetX > 127)
+		mouseOffsetX = 127;
+	if (mouseOffsetX < -127)
+		mouseOffsetX = -127;
+	if (mouseOffsetY > 127)
+		mouseOffsetY = 127;
+	if (mouseOffsetY < -127)
+		mouseOffsetY = -127;
 
 	// Animation frame cycling: (frameCounter / 2) % 6
 	int numSprites = _hudOverlayNut->getNumChars();
@@ -2075,7 +2149,8 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 				EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
 				int nonZero = 0;
 				for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
-					if (selectedFrame.pixels[i] != 0) nonZero++;
+					if (selectedFrame.pixels[i] != 0)
+						nonZero++;
 				}
 
 				if (nonZero == 0) {
@@ -2083,7 +2158,8 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 						EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
 						int altNonZero = 0;
 						for (int j = 0; j < altFrame.width * altFrame.height; j++) {
-							if (altFrame.pixels[j] != 0) altNonZero++;
+							if (altFrame.pixels[j] != 0)
+								altNonZero++;
 						}
 						if (altNonZero > 0) {
 							selectedId = groupMembers[i];
@@ -2163,7 +2239,8 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 	// --- Difficulty sprite (2-5) overlaid on top ---
 	// FUN_0041c012 lines 33-43: sprite index = min(difficulty, 4) + 1
 	int difficulty = _difficulty;
-	if (difficulty > 3) difficulty = 3;
+	if (difficulty > 3)
+		difficulty = 3;
 	int difficultySprite = difficulty + 2;
 	if (numSprites > difficultySprite) {
 		renderNutSprite(renderBitmap, pitch, width, height,
@@ -2191,10 +2268,12 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 			if (drawW > 0 && drawH > 0) {
 				for (int y = 0; y < drawH; y++) {
 					int destY = statusBarY + dmgClipY + y + _viewY;
-					if (destY < 0 || destY >= height) continue;
+					if (destY < 0 || destY >= height)
+						continue;
 					for (int x = 0; x < drawW; x++) {
 						int destX = dmgClipX + x + _viewX;
-						if (destX < 0 || destX >= pitch) continue;
+						if (destX < 0 || destX >= pitch)
+							continue;
 						byte pixel = src[(dmgClipY + y) * sw + (dmgClipX + x)];
 						if (pixel != 0) {
 							renderBitmap[destY * pitch + destX] = pixel;
@@ -2220,10 +2299,12 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 						int aH = MIN(alertH, alertSH - dmgClipY);
 						for (int y = 0; y < aH; y++) {
 							int destY = statusBarY + dmgClipY + y + _viewY;
-							if (destY < 0 || destY >= height) continue;
+							if (destY < 0 || destY >= height)
+								continue;
 							for (int x = 0; x < aW; x++) {
 								int destX = dmgClipX + x + _viewX;
-								if (destX < 0 || destX >= pitch) continue;
+								if (destX < 0 || destX >= pitch)
+									continue;
 								byte pixel = alertSrc[(dmgClipY + y) * alertSW + (dmgClipX + x)];
 								if (pixel != 0) {
 									renderBitmap[destY * pitch + destX] = pixel;
@@ -2242,7 +2323,8 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 	//   Bar width = min((lives * 5 - 5) * 2, 50) — only drawn when lives > 1
 	if (numSprites > 6 && _playerLives > 1) {
 		int livesBarWidth = (_playerLives * 5 - 5) * 2;
-		if (livesBarWidth > 50) livesBarWidth = 50;
+		if (livesBarWidth > 50)
+			livesBarWidth = 50;
 
 		const byte *src = _smush_cockpitNut->getCharData(6);
 		int sw = _smush_cockpitNut->getCharWidth(6);
@@ -2256,10 +2338,12 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 			if (drawW > 0 && drawH > 0) {
 				for (int y = 0; y < drawH; y++) {
 					int destY = statusBarY + livClipY + y + _viewY;
-					if (destY < 0 || destY >= height) continue;
+					if (destY < 0 || destY >= height)
+						continue;
 					for (int x = 0; x < drawW; x++) {
 						int destX = livClipX + x + _viewX;
-						if (destX < 0 || destX >= pitch) continue;
+						if (destX < 0 || destX >= pitch)
+							continue;
 						byte pixel = src[(livClipY + y) * sw + (livClipX + x)];
 						if (pixel != 0) {
 							renderBitmap[destY * pitch + destX] = pixel;
@@ -2283,8 +2367,10 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 
 	int numSprites = _flyShipSprite->getNumChars();
 	int spriteIndex = _shipDirectionIndex;
-	if (spriteIndex < 0) spriteIndex = 0;
-	if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+	if (spriteIndex < 0)
+		spriteIndex = 0;
+	if (spriteIndex >= numSprites)
+		spriteIndex = numSprites - 1;
 
 	// Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
 	// The perspective transform shifts the ship position based on perspective offsets.
@@ -2347,14 +2433,17 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 	// Select sprite based on direction and sprite count
 	if (numSprites >= 35) {
 		spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+		if (spriteIndex >= numSprites)
+			spriteIndex = numSprites - 1;
 	} else if (numSprites >= 25) {
 		int vDir5 = (_shipDirectionV * 5) / 7;
 		spriteIndex = _shipDirectionH * 5 + vDir5;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+		if (spriteIndex >= numSprites)
+			spriteIndex = numSprites - 1;
 	} else if (numSprites >= 5) {
 		spriteIndex = _shipDirectionH;
-		if (spriteIndex >= numSprites) spriteIndex = numSprites - 1;
+		if (spriteIndex >= numSprites)
+			spriteIndex = numSprites - 1;
 	} else if (numSprites == 2) {
 		spriteIndex = _shipFiring ? 1 : 0;
 	}
@@ -2522,10 +2611,14 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 			int yZone = (zoneHeight > 0) ? ((zoneHeight / 2) + (crosshairY - areaTop)) / zoneHeight : 0;
 
 			// Clamp to valid ranges
-			if (xZone < 0) xZone = 0;
-			if (xZone > 4) xZone = 4;
-			if (yZone < 0) yZone = 0;
-			if (yZone > 1) yZone = 1;
+			if (xZone < 0)
+				xZone = 0;
+			if (xZone > 4)
+				xZone = 4;
+			if (yZone < 0)
+				yZone = 0;
+			if (yZone > 1)
+				yZone = 1;
 
 			// Direction-based sprite flip logic (line 161-162 in decompiled)
 			// if (DAT_00457902 == (uVar7 & 1)) { local_58 = 4 - local_58; }
@@ -2550,8 +2643,10 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		}
 
 		// Clamp to valid range
-		if (spriteIdx < 0) spriteIdx = 0;
-		if (spriteIdx >= numSprites) spriteIdx = numSprites - 1;
+		if (spriteIdx < 0)
+			spriteIdx = 0;
+		if (spriteIdx >= numSprites)
+			spriteIdx = numSprites - 1;
 
 		int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
 		int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
@@ -2644,9 +2739,11 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
 
 	// Blit from embedded HUD
 	for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
-		if (drawY + y < 0) continue;
+		if (drawY + y < 0)
+			continue;
 		for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
-			if (drawX + x < 0) continue;
+			if (drawX + x < 0)
+				continue;
 			int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
 			byte pixel = shipFrame.pixels[srcIdx];
 			if (pixel != 0 && pixel != 231) {
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 55c793689df..3a729f4deca 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -39,7 +39,8 @@ int InsaneRebel2::runLevel1() {
 
 	// Play level beginning cinematic (01BEG.SAN)
 	playLevelBegin(1);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Main gameplay retry loop
 	while (!_vm->shouldQuit()) {
@@ -59,7 +60,8 @@ int InsaneRebel2::runLevel1() {
 		// Store death frame for video selection
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			// Level completed!
@@ -73,7 +75,8 @@ int InsaneRebel2::runLevel1() {
 		debug("Rebel2: Level 1 death at frame %d, lives=%d", _deathFrame, _playerLives - 1);
 		playLevelDeathVariant(1, 1, _deathFrame);
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -83,7 +86,8 @@ int InsaneRebel2::runLevel1() {
 
 		// Play retry prompt and loop
 		playLevelRetry(1);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -137,7 +141,8 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 			// Randomly add one unkilled type to phase state
 			int idx = _vm->_rnd.getRandomNumber(numUnkilled - 1);
 			_rebelPhaseState |= unkilled[idx];
-			if (budget) (*budget)++;
+			if (budget)
+				(*budget)++;
 		}
 	}
 
@@ -210,13 +215,15 @@ int InsaneRebel2::runLevel2() {
 
 	// Play cutscene (02CUT.SAN)
 	playCinematic("LEV02/02CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (02BEG.SAN)
 	// Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
 	// Includes text overlay from GAME.TRS — deferred until text rendering is ready.
 	playLevelBegin(2);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state (before retry loop)
 	clearBit(0);
@@ -261,7 +268,8 @@ int InsaneRebel2::runLevel2() {
 		splayer->play("LEV02/P1/02P01_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
 		processWaveEnd(0x36, &budget, 0, 0);
@@ -269,7 +277,8 @@ int InsaneRebel2::runLevel2() {
 		// Phase 1 wave loop: random B/C/D until all type 1,2 enemies killed
 		// Original: while (uVar3 >= 0 && (DAT_0047ab9c & 6) != 6)
 		while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 
 			// Random variant B, C, or D
 			int variant = _vm->_rnd.getRandomNumber(2);  // 0-2
@@ -290,10 +299,13 @@ int InsaneRebel2::runLevel2() {
 		}
 
 		// Check for bonus (bit 4 = 0x10)
-		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+		if ((_rebelPhaseState & 0x10) != 0)
+			bonusCount++;
 
-		if (_playerDamage >= 255) goto level2_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level2_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Post segment 1 (02PST1.SAN)
 		// Original: FUN_00417168("02PST1.SAN", 0x20) → flags = 0x20 | 0x08 = 0x28
@@ -303,7 +315,8 @@ int InsaneRebel2::runLevel2() {
 		_rebelStatusBarSprite = 0;
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/02PST1.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
@@ -328,14 +341,17 @@ int InsaneRebel2::runLevel2() {
 		splayer->play("LEV02/P2/02P02_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
 		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
 		while (true) {
 			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
-			if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e) break;
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e)
+				break;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 
 			// If no specific pattern: randomize high bits (original lines 71-74)
 			// When (local_10 & 0xc) == 0: add random 0x10/0x11/0x12
@@ -366,10 +382,13 @@ int InsaneRebel2::runLevel2() {
 			_deathFrame = splayer->_frame;
 		}
 
-		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+		if ((_rebelPhaseState & 0x10) != 0)
+			bonusCount++;
 
-		if (_playerDamage >= 255) goto level2_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level2_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Post segment 2 (02PST2.SAN)
 		// Original: FUN_00417168("02PST2.SAN", 0x20) → flags = 0x20 | 0x08 = 0x28
@@ -378,7 +397,8 @@ int InsaneRebel2::runLevel2() {
 		_rebelStatusBarSprite = 0;
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV02/02PST2.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
@@ -404,7 +424,8 @@ int InsaneRebel2::runLevel2() {
 		splayer->play("LEV02/P3/02P03_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
 		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
@@ -412,7 +433,8 @@ int InsaneRebel2::runLevel2() {
 			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
 
 			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				// Phase 3 randomization (original lines 113-115):
 				// If previous wave state bit 0 was clear AND random(8)==0, set bit 0
@@ -455,11 +477,14 @@ int InsaneRebel2::runLevel2() {
 			}
 		}
 
-		if ((_rebelPhaseState & 0x10) != 0) bonusCount++;
+		if ((_rebelPhaseState & 0x10) != 0)
+			bonusCount++;
 		totalKills += _rebelKillCounter;
 
-		if (_playerDamage >= 255) goto level2_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level2_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Level completed! Calculate accuracy score.
 		// Original: FUN_00417327 with score thresholds and medal ranks
@@ -484,7 +509,8 @@ int InsaneRebel2::runLevel2() {
 		// Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
 		debug("Rebel2: Level 2 Phase %d death", _currentPhase);
 		playCinematic("LEV02/02DIE.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
 		// DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
@@ -497,7 +523,8 @@ int InsaneRebel2::runLevel2() {
 		}
 		playCinematic("LEV02/02RETRY.SAN");
 		_playerDamage = 0;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		continue;  // Restart from beginning
 	}
 
@@ -516,7 +543,8 @@ int InsaneRebel2::runLevel3() {
 
 	// Play level beginning cinematic (03BEG.SAN)
 	playLevelBegin(3);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// ===== PHASE 1 retry loop =====
 	while (!_vm->shouldQuit()) {
@@ -533,7 +561,8 @@ int InsaneRebel2::runLevel3() {
 		splayer->play("LEV03/03PLAY1.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			// Phase 1 completed - save score and proceed to phase 2
@@ -544,7 +573,8 @@ int InsaneRebel2::runLevel3() {
 		// Died in phase 1 - frame-based death video
 		debug("Rebel2: Level 3 Phase 1 death at frame %d", _deathFrame);
 		playLevelDeathVariant(3, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -554,10 +584,12 @@ int InsaneRebel2::runLevel3() {
 
 		// Phase 1 retry (03RETRY.SAN)
 		playLevelRetryVariant(3, 1);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Post segment 1 (03POST1.SAN)
 	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
@@ -566,7 +598,8 @@ int InsaneRebel2::runLevel3() {
 	_rebelStatusBarSprite = 0;
 	splayer->setCurVideoFlags(0x28);
 	splayer->play("LEV03/03POST1.SAN", 12);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// ===== PHASE 2 retry loop (preserves phase 1 score) =====
 	_currentPhase = 2;
@@ -584,7 +617,8 @@ int InsaneRebel2::runLevel3() {
 		splayer->play("LEV03/03PLAY2.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			// Level completed!
@@ -597,7 +631,8 @@ int InsaneRebel2::runLevel3() {
 		// Died in phase 2 - frame-based death video
 		debug("Rebel2: Level 3 Phase 2 death at frame %d", _deathFrame);
 		playLevelDeathVariant(3, 2, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -608,7 +643,8 @@ int InsaneRebel2::runLevel3() {
 
 		// Phase 2 retry (03RETRYB.SAN)
 		playLevelRetryVariant(3, 2);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -626,11 +662,13 @@ int InsaneRebel2::runLevel4() {
 	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
 	splayer->setCurVideoFlags(0x28);
 	splayer->play("LEV04/04CUT.SAN", 12);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (04BEG.SAN)
 	playLevelBegin(4);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Main gameplay retry loop
 	while (!_vm->shouldQuit()) {
@@ -647,7 +685,8 @@ int InsaneRebel2::runLevel4() {
 		splayer->play("LEV04/04PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			// Level completed!
@@ -660,7 +699,8 @@ int InsaneRebel2::runLevel4() {
 		// Died - play death video (04DIE.SAN, no variants)
 		debug("Rebel2: Level 4 death");
 		playLevelDeathVariant(4, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -669,7 +709,8 @@ int InsaneRebel2::runLevel4() {
 		}
 
 		playLevelRetry(4);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -686,7 +727,8 @@ int InsaneRebel2::runLevel5() {
 
 	// Play level beginning cinematic (05BEG.SAN)
 	playLevelBegin(5);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Main gameplay retry loop
 	while (!_vm->shouldQuit()) {
@@ -703,7 +745,8 @@ int InsaneRebel2::runLevel5() {
 		splayer->play("LEV05/05PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			// Level completed!
@@ -716,7 +759,8 @@ int InsaneRebel2::runLevel5() {
 		// Died - play death video with random A/B variant
 		debug("Rebel2: Level 5 death at frame %d", _deathFrame);
 		playLevelDeathVariant(5, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -725,7 +769,8 @@ int InsaneRebel2::runLevel5() {
 		}
 
 		playLevelRetry(5);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -750,7 +795,8 @@ int InsaneRebel2::runLevel6() {
 	// Play level beginning cinematic (06BEG.SAN)
 	// Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
 	playLevelBegin(6);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040 — handler init done by IACT opcode 6
 
@@ -773,13 +819,15 @@ int InsaneRebel2::runLevel6() {
 		// + score checkpoint (FUN_00407f55) — needs per-frame callback
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield <= 0) {
 			// Died in phase 1
 			debug("Rebel2: Level 6 Phase 1 death at frame %d", _deathFrame);
 			playLevelDeathVariant(6, 1, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 
 			_playerLives--;
 			if (_playerLives <= 0) {
@@ -789,7 +837,8 @@ int InsaneRebel2::runLevel6() {
 
 			// Phase 1 retry (06RETRY.SAN) → restart outer loop
 			playLevelRetryVariant(6, 1);
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 			continue;
 		}
 
@@ -800,7 +849,8 @@ int InsaneRebel2::runLevel6() {
 		_rebelStatusBarSprite = 0;
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV06/06POST1.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== PHASE 2 retry loop (inner while(true) in original) =====
 		while (!_vm->shouldQuit()) {
@@ -813,7 +863,8 @@ int InsaneRebel2::runLevel6() {
 			splayer->play("LEV06/06PLAY2.SAN", 12);
 			_deathFrame = splayer->_frame;
 
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 
 			// Accumulate score: local_8 = DAT_0047ab84 + local_8
 			totalScore += _playerScore;
@@ -829,7 +880,8 @@ int InsaneRebel2::runLevel6() {
 			// Died in phase 2
 			debug("Rebel2: Level 6 Phase 2 death at frame %d", _deathFrame);
 			playLevelDeathVariant(6, 2, _deathFrame);
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 
 			_playerLives--;
 			if (_playerLives <= 0) {
@@ -839,7 +891,8 @@ int InsaneRebel2::runLevel6() {
 
 			// Phase 2 retry (06RETRYB.SAN) → re-enter phase 2
 			playLevelRetryVariant(6, 2);
-			if (_vm->shouldQuit()) return kLevelQuit;
+			if (_vm->shouldQuit())
+				return kLevelQuit;
 		}
 
 		break;  // Should only reach here on shouldQuit
@@ -861,11 +914,13 @@ int InsaneRebel2::runLevel7() {
 
 	// Play cutscene (07CUT.SAN)
 	playCinematic("LEV07/07CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (07BEG.SAN)
 	playLevelBegin(7);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
 	clearBit(0);
@@ -888,7 +943,8 @@ int InsaneRebel2::runLevel7() {
 		splayer->play("LEV07/07PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			debug("Rebel2: Level 7 completed!");
@@ -905,7 +961,8 @@ int InsaneRebel2::runLevel7() {
 		} else {
 			playCinematic("LEV07/07DIE_A.SAN");
 		}
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -914,7 +971,8 @@ int InsaneRebel2::runLevel7() {
 		}
 
 		playCinematic("LEV07/07RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -933,7 +991,8 @@ int InsaneRebel2::runLevel8() {
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
 	playLevelBegin(8);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
 	clearBit(0);
@@ -951,7 +1010,8 @@ int InsaneRebel2::runLevel8() {
 		splayer->play("LEV08/08PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			int accuracy = 0;
@@ -968,7 +1028,8 @@ int InsaneRebel2::runLevel8() {
 		// Original: random(2) → A or B via string pointer arithmetic
 		debug("Rebel2: Level 8 death at frame %d", _deathFrame);
 		playLevelDeathVariant(8, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -977,7 +1038,8 @@ int InsaneRebel2::runLevel8() {
 		}
 
 		playCinematic("LEV08/08RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -997,7 +1059,8 @@ int InsaneRebel2::runLevel9() {
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
 	playLevelBegin(9);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
 	clearBit(0);
@@ -1020,7 +1083,8 @@ int InsaneRebel2::runLevel9() {
 		splayer->play("LEV09/09PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			int accuracy = 0;
@@ -1037,7 +1101,8 @@ int InsaneRebel2::runLevel9() {
 		// Original: 0→DIE_A, 1→DIE_C, else→DIE_B
 		debug("Rebel2: Level 9 death at frame %d", _deathFrame);
 		playLevelDeathVariant(9, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -1046,7 +1111,8 @@ int InsaneRebel2::runLevel9() {
 		}
 
 		playCinematic("LEV09/09RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -1064,12 +1130,14 @@ int InsaneRebel2::runLevel10() {
 
 	// Play cutscene (10CUT.SAN)
 	playCinematic("LEV10/10CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (10BEG.SAN)
 	// Original: FUN_004171c5("10BEG.SAN", 0x20, 0xb3, 0xa0, 10, 2, 0x46)
 	playLevelBegin(10);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_00407d10
 	clearBit(0);
@@ -1087,7 +1155,8 @@ int InsaneRebel2::runLevel10() {
 		splayer->play("LEV10/10PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			int accuracy = 0;
@@ -1110,9 +1179,11 @@ int InsaneRebel2::runLevel10() {
 		}
 
 		playCinematic("LEV10/10DIE.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		playCinematic("LEV10/10RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -1141,11 +1212,13 @@ int InsaneRebel2::runLevel11() {
 
 	// Play cutscene (11CUT.SAN)
 	playCinematic("LEV11/11CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (11BEG.SAN)
 	playLevelBegin(11);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_00407d10 + FUN_0040c040: Reset game state
 	clearBit(0);
@@ -1186,7 +1259,8 @@ int InsaneRebel2::runLevel11() {
 		splayer->play("LEV11/P1/11P01_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
@@ -1194,7 +1268,8 @@ int InsaneRebel2::runLevel11() {
 			// Phase 1 wave loop: random(2) | (waveSelect & 8) → variants
 			// 0→D, 1→C, 8→B, 9→A
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				// Bonus sound check
 				if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
@@ -1220,15 +1295,18 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level11_death_phase1;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level11_death_phase1;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Post segment 1 (11POST1.SAN)
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV11/11POST1.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
@@ -1250,7 +1328,8 @@ int InsaneRebel2::runLevel11() {
 		splayer->play("LEV11/P2/11P02_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			// Phase 2: flags=3 (maxCredits=2, redistribution ON)
@@ -1258,7 +1337,8 @@ int InsaneRebel2::runLevel11() {
 
 			// Random(4) for variant selection: A, B, C, D
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				int variant = _vm->_rnd.getRandomNumber(3);
 				const char *filename;
@@ -1278,15 +1358,18 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level11_death_phase2;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level11_death_phase2;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Post segment 2 (11POST2.SAN)
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV11/11POST2.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
@@ -1309,7 +1392,8 @@ int InsaneRebel2::runLevel11() {
 		splayer->play("LEV11/P3/11P03_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
@@ -1317,7 +1401,8 @@ int InsaneRebel2::runLevel11() {
 
 			// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
 			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				// Bonus sound: (phaseState & 0xe) == 0xe and previous wasn't
 				if ((_rebelPhaseState & 0x0e) == 0x0e && (prevPhaseState & 0x0e) != 0x0e) {
@@ -1355,8 +1440,10 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level11_death_phase3;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level11_death_phase3;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== PHASE 3 BRIDGE CINEMATICS =====
 		{
@@ -1372,7 +1459,8 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
 		// Reset shots/explosions (FUN_0041ca6a equivalent)
@@ -1395,7 +1483,8 @@ int InsaneRebel2::runLevel11() {
 		splayer->play("LEV11/P3/11P03_G.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// Only enter wave loop if not all basic types killed already
 		if ((_rebelPhaseState & 0x0e) < 0x0e) {
@@ -1403,7 +1492,8 @@ int InsaneRebel2::runLevel11() {
 			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
 
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				// Wider randomization for first few waves
 				if (local_8 < 4) {
@@ -1437,8 +1527,10 @@ int InsaneRebel2::runLevel11() {
 
 		totalKills += _rebelKillCounter;
 
-		if (_playerDamage >= 255) goto level11_death_phase3;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level11_death_phase3;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== LEVEL COMPLETED =====
 		{
@@ -1472,7 +1564,8 @@ int InsaneRebel2::runLevel11() {
 		goto level11_retry;
 
 	level11_retry:
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		_playerLives--;
 		if (_playerLives <= 0) {
 			playLevelGameOver(11);
@@ -1480,7 +1573,8 @@ int InsaneRebel2::runLevel11() {
 		}
 		playCinematic("LEV11/11RETRY.SAN");
 		_playerDamage = 0;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		continue;  // Restart from Phase 1
 	}
 
@@ -1507,11 +1601,13 @@ int InsaneRebel2::runLevel12() {
 
 	// Play cutscene (12CUT.SAN)
 	playCinematic("LEV12/12CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (12BEG.SAN)
 	playLevelBegin(12);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_0041c7d0 + FUN_00407d10 + FUN_0040c040: Reset game state
 	clearBit(0);
@@ -1547,21 +1643,24 @@ int InsaneRebel2::runLevel12() {
 		debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV12/12P05.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P1/12P01_A.SAN)
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/P1/12P01_A.SAN", 12);
 		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
 
 			// Wave loop: random(2) | (waveSelect & 2) → 0:C, 1:D, 2:A, 3:B
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
 				const char *filename;
@@ -1581,8 +1680,10 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level12_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== PHASE 2: 12P06 → P2/12P02_X =====
 		_currentPhase = 2;
@@ -1595,20 +1696,23 @@ int InsaneRebel2::runLevel12() {
 		debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/12P06.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P2/12P02_A.SAN)
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/P2/12P02_A.SAN", 12);
 		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
 
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				// Variant selection: (waveSelect & 2) controls which set
 				int local_8;
@@ -1639,8 +1743,10 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level12_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== PHASE 3: 12P07 → P3/12P03_X =====
 		_currentPhase = 3;
@@ -1653,21 +1759,24 @@ int InsaneRebel2::runLevel12() {
 		debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/12P07.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P3/12P03_A.SAN)
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/P3/12P03_A.SAN", 12);
 		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			int local_8 = 0;
 			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
 
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				// Wider randomization for first few waves
 				if (local_8 < 4) {
@@ -1695,8 +1804,10 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level12_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== PHASE 4: 12P08 → P4/12P04_X =====
 		_currentPhase = 4;
@@ -1709,21 +1820,24 @@ int InsaneRebel2::runLevel12() {
 		debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/12P08.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P4/12P04_A.SAN)
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/P4/12P04_A.SAN", 12);
 		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		{
 			int local_8 = 0;
 			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
 
 			while (waveSelect != 0xFFFF) {
-				if (_vm->shouldQuit()) return kLevelQuit;
+				if (_vm->shouldQuit())
+					return kLevelQuit;
 
 				if (local_8 < 4) {
 					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
@@ -1750,13 +1864,16 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255) goto level12_death;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_playerDamage >= 255)
+			goto level12_death;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// ===== CLOSING: 12P09.SAN =====
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/12P09.SAN", 12);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// ===== LEVEL COMPLETED =====
@@ -1782,7 +1899,8 @@ int InsaneRebel2::runLevel12() {
 		debug("Rebel2: Level 12 Phase %d death", _currentPhase);
 		playCinematic("LEV12/12DIE.SAN");
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		_playerLives--;
 		if (_playerLives <= 0) {
 			playLevelGameOver(12);
@@ -1790,7 +1908,8 @@ int InsaneRebel2::runLevel12() {
 		}
 		playCinematic("LEV12/12RETRY.SAN");
 		_playerDamage = 0;
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 		continue;  // Restart from Phase 1
 	}
 
@@ -1812,7 +1931,8 @@ int InsaneRebel2::runLevel13() {
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
 	playLevelBegin(13);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
 	clearBit(0);
@@ -1833,7 +1953,8 @@ int InsaneRebel2::runLevel13() {
 		splayer->play("LEV13/13PLAY_A.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		// If alive after Phase A, play Phase B (reactor destruction loop)
 		// Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
@@ -1846,7 +1967,8 @@ int InsaneRebel2::runLevel13() {
 			_deathFrame = splayer->_frame;
 		}
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			int accuracy = 0;
@@ -1862,7 +1984,8 @@ int InsaneRebel2::runLevel13() {
 		// Death: frame-based variant selection (FUN_0041B3E1 lines 47-61)
 		debug("Rebel2: Level 13 death at frame %d", _deathFrame);
 		playLevelDeathVariant(13, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -1871,7 +1994,8 @@ int InsaneRebel2::runLevel13() {
 		}
 
 		playCinematic("LEV13/13RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -1889,7 +2013,8 @@ int InsaneRebel2::runLevel14() {
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
 	playLevelBegin(14);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
 	clearBit(0);
@@ -1907,7 +2032,8 @@ int InsaneRebel2::runLevel14() {
 		splayer->play("LEV14/14PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			int accuracy = 0;
@@ -1923,7 +2049,8 @@ int InsaneRebel2::runLevel14() {
 		// Death: single video (14DIE.SAN)
 		debug("Rebel2: Level 14 death at frame %d", _deathFrame);
 		playCinematic("LEV14/14DIE.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -1932,7 +2059,8 @@ int InsaneRebel2::runLevel14() {
 		}
 
 		playCinematic("LEV14/14RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;
@@ -1952,12 +2080,14 @@ int InsaneRebel2::runLevel15() {
 
 	// Play cutscene (15CUT.SAN)
 	playCinematic("LEV15/15CUT.SAN");
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// Play level beginning cinematic (15BEG.SAN)
 	// Original: FUN_004171c5("15BEG.SAN", 0x20, 0xb8, 0xa0, 10, 2, 0x46)
 	playLevelBegin(15);
-	if (_vm->shouldQuit()) return kLevelQuit;
+	if (_vm->shouldQuit())
+		return kLevelQuit;
 
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
 	clearBit(0);
@@ -1981,7 +2111,8 @@ int InsaneRebel2::runLevel15() {
 		splayer->play("LEV15/15PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
 
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		if (_playerShield > 0) {
 			int accuracy = 0;
@@ -1998,7 +2129,8 @@ int InsaneRebel2::runLevel15() {
 		// Death: frame-based variant selection (FUN_0041B8D7 lines 46-65)
 		debug("Rebel2: Level 15 death at frame %d", _deathFrame);
 		playLevelDeathVariant(15, 1, _deathFrame);
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 
 		_playerLives--;
 		if (_playerLives <= 0) {
@@ -2007,7 +2139,8 @@ int InsaneRebel2::runLevel15() {
 		}
 
 		playCinematic("LEV15/15RETRY.SAN");
-		if (_vm->shouldQuit()) return kLevelQuit;
+		if (_vm->shouldQuit())
+			return kLevelQuit;
 	}
 
 	return kLevelQuit;


Commit: a03ea4774fb3f3059409c24fe07a1e52fd7dd42c
    https://github.com/scummvm/scummvm/commit/a03ea4774fb3f3059409c24fe07a1e52fd7dd42c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:12+02:00

Commit Message:
SCUMM: RA2: Improve game detection

Changed paths:
    engines/scumm/detection.h
    engines/scumm/detection_tables.h
    engines/scumm/metaengine.cpp


diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index fbf94c37023..5daff20041f 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -38,6 +38,7 @@ namespace Scumm {
 #define GAMEOPTION_COPY_PROTECTION                           GUIO_GAMEOPTIONS7
 #define GAMEOPTION_USE_REMASTERED_AUDIO                      GUIO_GAMEOPTIONS8
 #define GAMEOPTION_TTS                                       GUIO_GAMEOPTIONS9
+#define GAMEOPTION_REBEL2_HIRES                              GUIO_GAMEOPTIONS10
 
 /**
  * Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index ced21e29990..34835f71af0 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -49,6 +49,7 @@ static const char *const directoryGlobs[] = {
 	"Contents", // Mac Steam versions
 	"MacOS",    // Mac Steam versions
 	"Resources", // Mac SE/Remastered versions
+	"OPEN",     // RA2 demo detection (O_DEMO.SAN)
 	0
 };
 
@@ -227,8 +228,8 @@ static const GameSettings gameVariantsTable[] = {
 	{"ft",   "Remastered", 0, GID_FT,  7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"ft",   "Demo", 0, GID_FT,  7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 
-	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
-	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
+	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
+	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
 
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"dig",  "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
@@ -498,7 +499,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 	{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
 	{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
 
-	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, 0 },
+	{ "rebel2", "RA2START.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+	{ "rebel2", "O_DEMO.SAN", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
 
 	{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
 
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 6254aec88a0..4fe0f9a1511 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -878,6 +878,15 @@ static const ExtraGuiOption enableTTS = {
 };
 #endif
 
+static const ExtraGuiOption enableRebel2HiRes = {
+	_s("High resolution mode"),
+	_s("Run the game in 640x400 high resolution mode instead of 320x200."),
+	"rebel2_hires",
+	true,
+	0,
+	0
+};
+
 const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
 	ExtraGuiOptions options;
 	// Query the GUI options
@@ -919,6 +928,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
 		options.push_back(enableTTS);
 	}
 #endif
+	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_HIRES)) {
+		options.push_back(enableRebel2HiRes);
+	}
 	if (target.empty() || gameid == "comi") {
 		options.push_back(comiObjectLabelsOption);
 


Commit: de4c041f00325eda2051ca33d878b59d4c9eb4b1
    https://github.com/scummvm/scummvm/commit/de4c041f00325eda2051ca33d878b59d4c9eb4b1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:13+02:00

Commit Message:
SCUMM: RA2: Reset enemy state for level 2 waves

Changed paths:
    engines/scumm/insane/insane_rebel_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index a4300324b92..7a17b021d63 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -2195,10 +2195,14 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 		if (it->id == enemyId) {
 			it->rect = Common::Rect(x, y, x + w, y + h);
 			it->type = par4;  // Enemy type from IACT offset +6 (userId)
-			// Only re-activate if not destroyed
-			if (!it->destroyed) {
-				it->active = true;
-			}
+			// The _iactBits[] bit table is the authoritative alive/dead state.
+			// We only reach here when isBitSet(enemyId) == false, meaning
+			// the game considers this enemy alive. Reset destroyed/active
+			// to match — this is critical when clearBit(0) re-enables all
+			// enemies at wave start but the _enemies list still has stale
+			// destroyed=true from a previous wave.
+			it->active = true;
+			it->destroyed = false;
 			found = true;
 			break;
 		}


Commit: c4e1df0f2f63e5ae58b994faf00ab16a948801d8
    https://github.com/scummvm/scummvm/commit/c4e1df0f2f63e5ae58b994faf00ab16a948801d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:13+02:00

Commit Message:
SCUMM: RA2: Add intro level text overlay

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ad9204e895e..0c57a5ab083 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -127,6 +127,14 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_damageShakeCounter = 0;
 	memset(_damageSavedPalette, 0, sizeof(_damageSavedPalette));
 
+	// Text overlay state (FUN_004171c5 chapter title rendering)
+	_textOverlayActive = false;
+	_textOverlayID = 0;
+	_textOverlayX = 0;
+	_textOverlayY = 0;
+	_textOverlayFadeIn = 0;
+	_textOverlayFadeOut = 0;
+
 	// Retail globals mapped: hit counter, cooldown, invulnerability flag
 	_rebelOp6Initialized = false;
 	_rebelHitCounter = 0;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 73f780de4a1..8bc1c9d47cf 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -331,6 +331,19 @@ public:
 	// Play cinematic video by filename
 	void playCinematic(const char *filename);
 
+	// Play cinematic with text overlay (emulates FUN_004171c5)
+	// Text is progressively revealed during [fadeInFrame, fadeOutFrame)
+	void playVideoWithText(const char *filename, int textID, int textX, int textY,
+	                       int fadeInFrame, int fadeOutFrame);
+
+	// Text overlay state (active during playVideoWithText cinematics)
+	bool _textOverlayActive;      // True when text overlay should render
+	int _textOverlayID;           // TRS string ID
+	int _textOverlayX;            // X position for text rendering
+	int _textOverlayY;            // Y position for text rendering
+	int _textOverlayFadeIn;       // Frame to start progressive text reveal
+	int _textOverlayFadeOut;      // Frame to stop text rendering
+
 	// Play death video with proper variant selection
 	void playLevelDeathVariant(int levelId, int phase, int frame);
 
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 432ad35bb01..7bf13aa5c2c 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -109,16 +109,82 @@ void InsaneRebel2::playCinematic(const char *filename) {
 	splayer->play(filename, 12);
 }
 
+void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX, int textY,
+                                     int fadeInFrame, int fadeOutFrame) {
+	// Emulates FUN_004171c5: plays video with progressive text overlay
+	// Text string loaded from GAME.TRS via getString(textID)
+	// During frame range [fadeInFrame, fadeOutFrame):
+	//   displayLength = currentFrame + 10 - fadeInFrame, capped at 0xBE (190) chars
+	//   Text rendered at (textX, textY) using FUN_004341a0
+
+	_rebelHandler = 0;
+	_rebelStatusBarSprite = 0;
+
+	// Set up text overlay state — procPostRendering reads these each frame
+	_textOverlayActive = true;
+	_textOverlayID = textID;
+	_textOverlayX = textX;
+	_textOverlayY = textY;
+	_textOverlayFadeIn = fadeInFrame;
+	_textOverlayFadeOut = fadeOutFrame;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(0x28);
+	splayer->play(filename, 12);
+
+	_textOverlayActive = false;
+}
+
 void InsaneRebel2::playLevelBegin(int levelId) {
 	// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
 	// Emulates FUN_004171c5 call in each level handler
+	//
+	// Per-level text overlay parameters from original disassembly:
+	// All levels use FUN_004171c5 with a chapter title overlay from GAME.TRS.
+
+	struct TextOverlayParams {
+		int textID;      // TRS string ID (-1 = no text overlay)
+		int textX;
+		int textY;
+		int fadeInFrame;
+		int fadeOutFrame;
+	};
+
+	// Table of per-level text overlay parameters
+	// All levels use FUN_004171c5 (verified against decompiled level handlers)
+	// Text IDs are sequential: 0xAA (level 1) through 0xB8 (level 15)
+	static const TextOverlayParams levelTextParams[16] = {
+		{ -1,   0,  0,   0,    0},    // Level 0 (unused)
+		{0xAA, 0xA0, 10,   5, 0x4B},  // Level 1:  FUN_00417E53
+		{0xAB, 0xA0, 10,   2, 0x46},  // Level 2:  FUN_00418063
+		{0xAC, 0xA0, 10,   2, 0x46},  // Level 3:  FUN_0041885F
+		{0xAD, 0xA0, 10,   2,  100},  // Level 4:  FUN_00418CC4
+		{0xAE, 0xA0, 10,   5, 0x3C},  // Level 5:  FUN_00418EC6
+		{0xAF, 0xA0, 10,   5, 0x4B},  // Level 6:  FUN_004190D6
+		{0xB0, 0xA0, 10,   5, 0x4B},  // Level 7:  FUN_0041974C
+		{0xB1, 0xA0, 10,   5, 0x4B},  // Level 8:  FUN_00419976
+		{0xB2, 0xA0, 10, 200, 0x10E}, // Level 9:  FUN_00419B86
+		{0xB3, 0xA0, 10,   2, 0x46},  // Level 10: FUN_00419E0A
+		{0xB4, 0xA0, 10,   2, 0x46},  // Level 11: FUN_0041A00C
+		{0xB5, 0xA0, 10,   5, 0x4B},  // Level 12: FUN_0041A3EB
+		{0xB6, 0xA0, 10,   2, 0x46},  // Level 13: FUN_0041A806
+		{0xB7, 0xA0, 10,   2, 0x46},  // Level 14: FUN_0041ABB2
+		{0xB8, 0xA0, 10,   2, 0x46},  // Level 15: FUN_0041AEE8
+	};
 
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
 	Common::String filename = Common::String::format("%s/%sBEG.SAN", dir.c_str(), prefix.c_str());
 
 	debug("Rebel2: Playing level %d beginning: %s", levelId, filename.c_str());
-	playCinematic(filename.c_str());
+
+	if (levelId >= 1 && levelId <= 15 && levelTextParams[levelId].textID >= 0) {
+		const TextOverlayParams &p = levelTextParams[levelId];
+		playVideoWithText(filename.c_str(), p.textID, p.textX, p.textY,
+		                  p.fadeInFrame, p.fadeOutFrame);
+	} else {
+		playCinematic(filename.c_str());
+	}
 }
 
 bool InsaneRebel2::playLevelGameplay(int levelId) {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index aeacb6abaee..0fbf154869f 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1763,6 +1763,152 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
 				  _player->_curVideoFlags, _gameState);
 		}
+
+		// Text overlay rendering for FUN_004171c5-style cinematics
+		// Draws progressive chapter title text during [fadeInFrame, fadeOutFrame)
+		if (_textOverlayActive && curFrame >= _textOverlayFadeIn && curFrame < _textOverlayFadeOut) {
+			SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+			const char *text = splayer->getString(_textOverlayID);
+			debug(5, "Rebel2: Text overlay frame %d/%d-%d textID=0x%x text='%s'",
+			      curFrame, _textOverlayFadeIn, _textOverlayFadeOut, _textOverlayID,
+			      text ? text : "(null)");
+			if (text) {
+				// Progressive reveal: display length = currentFrame + 10 - fadeInFrame
+				// Capped at 0xBE (190) visible chars per original
+				int displayLen = curFrame + 10 - _textOverlayFadeIn;
+				if (displayLen > 0xBE)
+					displayLen = 0xBE;
+				if (displayLen < 0)
+					displayLen = 0;
+
+				// Font system — same as menu text (FUN_00434d10 / FUN_00433da0)
+				// ^fNN = font switch, ^cNNN = color code
+				NutRenderer *fonts[3] = {
+					_smush_talkfontNut,
+					_smush_smalfontNut,
+					_smush_titlefontNut
+				};
+				NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+				if (!defaultFont)
+					goto textOverlayDone;
+
+				{
+					Common::Rect clipRect(0, 0, width, height);
+
+					// Format code parser (same as drawMenuItems)
+					auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
+						if (*s != '^')
+							return false;
+						const char *p = s + 1;
+						if (*p == '^') { s = p; return false; }  // ^^ = literal ^
+						if (*p == 'f') {
+							p++;
+							int idx = 0;
+							while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
+							s = p;
+							curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+							return true;
+						}
+						if (*p == 'c') {
+							p++;
+							int col = 0;
+							while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
+							s = p;
+							curColor = col;
+							return true;
+						}
+						return false;
+					};
+
+					// FUN_004341a0 splits on \n and renders each line centered independently.
+					// The TRS parser joins multi-line strings with spaces (stripping \n//),
+					// so " ^f" marks where a line break was in the original TRS file.
+					// We split the text into lines first, then render each centered.
+
+					// Split text into lines at " ^f" boundaries
+					Common::Array<Common::String> lines;
+					{
+						Common::String cur;
+						const char *s = text;
+						while (*s) {
+							// Check for line break: space followed by ^f (font switch)
+							if (*s == ' ' && s[1] == '^' && s[2] == 'f') {
+								lines.push_back(cur);
+								cur.clear();
+								s++; // skip the space, keep the ^f for the next line
+								continue;
+							}
+							cur += *s++;
+						}
+						if (!cur.empty())
+							lines.push_back(cur);
+					}
+
+					int drawY = _textOverlayY;
+					int visCount = 0;
+
+					for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
+						const char *lineStr = lines[lineIdx].c_str();
+						const char *lineEnd = lineStr + lines[lineIdx].size();
+
+						// Measure this line's width (only visible chars up to displayLen)
+						int lineWidth = 0;
+						int lineVisCount = 0;
+						NutRenderer *lineFont = defaultFont;
+						{
+							const char *s = lineStr;
+							NutRenderer *mFont = defaultFont;
+							int mColor = 1;
+							while (s < lineEnd && (visCount + lineVisCount) < displayLen) {
+								if (parseFormat(s, mFont, mColor))
+									continue;
+								lineFont = mFont; // track font for line height
+								byte c = (byte)*s++;
+								if (c >= 'a' && c <= 'z')
+									c = c - 'a' + 'A';
+								if (mFont && c < mFont->getNumChars())
+									lineWidth += mFont->getCharWidth(c);
+								lineVisCount++;
+							}
+						}
+
+						// Draw this line centered at textX
+						int drawX = _textOverlayX - lineWidth / 2;
+						int lineCharsDrawn = 0;
+						{
+							const char *s = lineStr;
+							NutRenderer *curFont = defaultFont;
+							int curColor = 1;
+							while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
+								if (parseFormat(s, curFont, curColor))
+									continue;
+								byte c = (byte)*s++;
+								if (c >= 'a' && c <= 'z')
+									c = c - 'a' + 'A';
+								if (!curFont || c >= curFont->getNumChars()) {
+									lineCharsDrawn++;
+									continue;
+								}
+								int charW = curFont->getCharWidth(c);
+								if (drawX >= 0 && drawY >= 0 && charW > 0) {
+									curFont->drawCharV7(renderBitmap, clipRect, drawX, drawY,
+									                    pitch, curColor, kStyleAlignLeft, c, false, false);
+								}
+								drawX += charW;
+								lineCharsDrawn++;
+							}
+						}
+						visCount += lineCharsDrawn;
+
+						// Advance to next line — use the line's font height for spacing
+						int lineHeight = lineFont->getCharHeight('A') + 2;
+						drawY += lineHeight;
+					}
+				}
+			}
+		}
+		textOverlayDone:
+
 		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
 		return;
 	} else {


Commit: da4840d6b22e9278e1da06cea5623ad388411cd1
    https://github.com/scummvm/scummvm/commit/da4840d6b22e9278e1da06cea5623ad388411cd1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:13+02:00

Commit Message:
SCUMM: RA2: Refactor ship sprite rendering

Changed paths:
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 8bc1c9d47cf..807184f52fe 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -344,6 +344,9 @@ public:
 	int _textOverlayFadeIn;       // Frame to start progressive text reveal
 	int _textOverlayFadeOut;      // Frame to stop text rendering
 
+	// Render chapter title text overlay (emulates FUN_004341a0 in FUN_004171c5)
+	void renderTextOverlay(byte *renderBitmap, int pitch, int width, int height, int curFrame);
+
 	// Play death video with proper variant selection
 	void playLevelDeathVariant(int levelId, int phase, int frame);
 
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 0fbf154869f..01ab72b60e5 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1764,150 +1764,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 				  _player->_curVideoFlags, _gameState);
 		}
 
-		// Text overlay rendering for FUN_004171c5-style cinematics
-		// Draws progressive chapter title text during [fadeInFrame, fadeOutFrame)
-		if (_textOverlayActive && curFrame >= _textOverlayFadeIn && curFrame < _textOverlayFadeOut) {
-			SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-			const char *text = splayer->getString(_textOverlayID);
-			debug(5, "Rebel2: Text overlay frame %d/%d-%d textID=0x%x text='%s'",
-			      curFrame, _textOverlayFadeIn, _textOverlayFadeOut, _textOverlayID,
-			      text ? text : "(null)");
-			if (text) {
-				// Progressive reveal: display length = currentFrame + 10 - fadeInFrame
-				// Capped at 0xBE (190) visible chars per original
-				int displayLen = curFrame + 10 - _textOverlayFadeIn;
-				if (displayLen > 0xBE)
-					displayLen = 0xBE;
-				if (displayLen < 0)
-					displayLen = 0;
-
-				// Font system — same as menu text (FUN_00434d10 / FUN_00433da0)
-				// ^fNN = font switch, ^cNNN = color code
-				NutRenderer *fonts[3] = {
-					_smush_talkfontNut,
-					_smush_smalfontNut,
-					_smush_titlefontNut
-				};
-				NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
-				if (!defaultFont)
-					goto textOverlayDone;
-
-				{
-					Common::Rect clipRect(0, 0, width, height);
-
-					// Format code parser (same as drawMenuItems)
-					auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
-						if (*s != '^')
-							return false;
-						const char *p = s + 1;
-						if (*p == '^') { s = p; return false; }  // ^^ = literal ^
-						if (*p == 'f') {
-							p++;
-							int idx = 0;
-							while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
-							s = p;
-							curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
-							return true;
-						}
-						if (*p == 'c') {
-							p++;
-							int col = 0;
-							while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
-							s = p;
-							curColor = col;
-							return true;
-						}
-						return false;
-					};
-
-					// FUN_004341a0 splits on \n and renders each line centered independently.
-					// The TRS parser joins multi-line strings with spaces (stripping \n//),
-					// so " ^f" marks where a line break was in the original TRS file.
-					// We split the text into lines first, then render each centered.
-
-					// Split text into lines at " ^f" boundaries
-					Common::Array<Common::String> lines;
-					{
-						Common::String cur;
-						const char *s = text;
-						while (*s) {
-							// Check for line break: space followed by ^f (font switch)
-							if (*s == ' ' && s[1] == '^' && s[2] == 'f') {
-								lines.push_back(cur);
-								cur.clear();
-								s++; // skip the space, keep the ^f for the next line
-								continue;
-							}
-							cur += *s++;
-						}
-						if (!cur.empty())
-							lines.push_back(cur);
-					}
-
-					int drawY = _textOverlayY;
-					int visCount = 0;
-
-					for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
-						const char *lineStr = lines[lineIdx].c_str();
-						const char *lineEnd = lineStr + lines[lineIdx].size();
-
-						// Measure this line's width (only visible chars up to displayLen)
-						int lineWidth = 0;
-						int lineVisCount = 0;
-						NutRenderer *lineFont = defaultFont;
-						{
-							const char *s = lineStr;
-							NutRenderer *mFont = defaultFont;
-							int mColor = 1;
-							while (s < lineEnd && (visCount + lineVisCount) < displayLen) {
-								if (parseFormat(s, mFont, mColor))
-									continue;
-								lineFont = mFont; // track font for line height
-								byte c = (byte)*s++;
-								if (c >= 'a' && c <= 'z')
-									c = c - 'a' + 'A';
-								if (mFont && c < mFont->getNumChars())
-									lineWidth += mFont->getCharWidth(c);
-								lineVisCount++;
-							}
-						}
-
-						// Draw this line centered at textX
-						int drawX = _textOverlayX - lineWidth / 2;
-						int lineCharsDrawn = 0;
-						{
-							const char *s = lineStr;
-							NutRenderer *curFont = defaultFont;
-							int curColor = 1;
-							while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
-								if (parseFormat(s, curFont, curColor))
-									continue;
-								byte c = (byte)*s++;
-								if (c >= 'a' && c <= 'z')
-									c = c - 'a' + 'A';
-								if (!curFont || c >= curFont->getNumChars()) {
-									lineCharsDrawn++;
-									continue;
-								}
-								int charW = curFont->getCharWidth(c);
-								if (drawX >= 0 && drawY >= 0 && charW > 0) {
-									curFont->drawCharV7(renderBitmap, clipRect, drawX, drawY,
-									                    pitch, curColor, kStyleAlignLeft, c, false, false);
-								}
-								drawX += charW;
-								lineCharsDrawn++;
-							}
-						}
-						visCount += lineCharsDrawn;
-
-						// Advance to next line — use the line's font height for spacing
-						int lineHeight = lineFont->getCharHeight('A') + 2;
-						drawY += lineHeight;
-					}
-				}
-			}
-		}
-		textOverlayDone:
+		// Chapter title text overlay (FUN_004171c5)
+		if (_textOverlayActive)
+			renderTextOverlay(renderBitmap, pitch, width, height, curFrame);
 
 		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
 		return;
@@ -2162,6 +2021,140 @@ void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width,
 // ======================= Rendering Helper Functions =======================
 // These are extracted from procPostRendering for better readability
 
+void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, int height, int curFrame) {
+	// Emulates FUN_004171c5 text overlay: progressive chapter title during [fadeIn, fadeOut)
+	if (curFrame < _textOverlayFadeIn || curFrame >= _textOverlayFadeOut)
+		return;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	const char *text = splayer->getString(_textOverlayID);
+	debug(5, "Rebel2: Text overlay frame %d/%d-%d textID=0x%x text='%s'",
+	      curFrame, _textOverlayFadeIn, _textOverlayFadeOut, _textOverlayID,
+	      text ? text : "(null)");
+	if (!text)
+		return;
+
+	// Progressive reveal: displayLen = currentFrame + 10 - fadeInFrame, capped at 0xBE (190)
+	int displayLen = curFrame + 10 - _textOverlayFadeIn;
+	if (displayLen > 0xBE)
+		displayLen = 0xBE;
+	if (displayLen < 0)
+		return;
+
+	// Font system — ^fNN = font switch, ^cNNN = color code
+	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
+	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+	if (!defaultFont)
+		return;
+
+	Common::Rect clipRect(0, 0, width, height);
+
+	// Format code parser (same as drawMenuItems / FUN_00434d10)
+	auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
+		if (*s != '^')
+			return false;
+		const char *p = s + 1;
+		if (*p == '^') { s = p; return false; }
+		if (*p == 'f') {
+			p++;
+			int idx = 0;
+			while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
+			s = p;
+			curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+			return true;
+		}
+		if (*p == 'c') {
+			p++;
+			int col = 0;
+			while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
+			s = p;
+			curColor = col;
+			return true;
+		}
+		return false;
+	};
+
+	// The TRS parser joins multi-line strings with spaces (stripping \n//),
+	// so " ^f" marks where a line break was in the original TRS file.
+	// Split into lines, then render each centered at textX (FUN_004341a0).
+	Common::Array<Common::String> lines;
+	{
+		Common::String cur;
+		const char *s = text;
+		while (*s) {
+			if (*s == ' ' && s[1] == '^' && s[2] == 'f') {
+				lines.push_back(cur);
+				cur.clear();
+				s++; // skip the space, keep ^f for the next line
+				continue;
+			}
+			cur += *s++;
+		}
+		if (!cur.empty())
+			lines.push_back(cur);
+	}
+
+	int drawY = _textOverlayY;
+	int visCount = 0;
+
+	for (uint lineIdx = 0; lineIdx < lines.size() && visCount < displayLen; lineIdx++) {
+		const char *lineStr = lines[lineIdx].c_str();
+		const char *lineEnd = lineStr + lines[lineIdx].size();
+
+		// Measure visible chars up to displayLen
+		int lineWidth = 0;
+		int lineVisCount = 0;
+		NutRenderer *lineFont = defaultFont;
+		{
+			const char *s = lineStr;
+			NutRenderer *mFont = defaultFont;
+			int mColor = 1;
+			while (s < lineEnd && (visCount + lineVisCount) < displayLen) {
+				if (parseFormat(s, mFont, mColor))
+					continue;
+				lineFont = mFont;
+				byte c = (byte)*s++;
+				if (c >= 'a' && c <= 'z')
+					c = c - 'a' + 'A';
+				if (mFont && c < mFont->getNumChars())
+					lineWidth += mFont->getCharWidth(c);
+				lineVisCount++;
+			}
+		}
+
+		// Draw line centered at textX
+		int drawX = _textOverlayX - lineWidth / 2;
+		int lineCharsDrawn = 0;
+		{
+			const char *s = lineStr;
+			NutRenderer *curFont = defaultFont;
+			int curColor = 1;
+			while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
+				if (parseFormat(s, curFont, curColor))
+					continue;
+				byte c = (byte)*s++;
+				if (c >= 'a' && c <= 'z')
+					c = c - 'a' + 'A';
+				if (!curFont || c >= curFont->getNumChars()) {
+					lineCharsDrawn++;
+					continue;
+				}
+				int charW = curFont->getCharWidth(c);
+				if (drawX >= 0 && drawY >= 0 && charW > 0) {
+					curFont->drawCharV7(renderBitmap, clipRect, drawX, drawY,
+					                    pitch, curColor, kStyleAlignLeft, c, false, false);
+				}
+				drawX += charW;
+				lineCharsDrawn++;
+			}
+		}
+		visCount += lineCharsDrawn;
+
+		int lineHeight = lineFont->getCharHeight('A') + 2;
+		drawY += lineHeight;
+	}
+}
+
 void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
 											 int videoWidth, int videoHeight, int statusBarY) {
 	// Fill status bar background (FUN_004288c0 equivalent)


Commit: 528969fe7900e6b232576c027f1c933bdb8a8b7e
    https://github.com/scummvm/scummvm/commit/528969fe7900e6b232576c027f1c933bdb8a8b7e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:14+02:00

Commit Message:
SCUMM: RA2: Add basic unlocking and password handling

Changed paths:
    engines/scumm/detection.h
    engines/scumm/detection_tables.h
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/metaengine.cpp


diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index 5daff20041f..f0bf20c91ac 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -39,6 +39,7 @@ namespace Scumm {
 #define GAMEOPTION_USE_REMASTERED_AUDIO                      GUIO_GAMEOPTIONS8
 #define GAMEOPTION_TTS                                       GUIO_GAMEOPTIONS9
 #define GAMEOPTION_REBEL2_HIRES                              GUIO_GAMEOPTIONS10
+#define GAMEOPTION_REBEL2_UNLOCK_ALL                         GUIO_GAMEOPTIONS11
 
 /**
  * Descriptor of a specific SCUMM game. Used internally to store
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 34835f71af0..33b26872482 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -228,8 +228,8 @@ static const GameSettings gameVariantsTable[] = {
 	{"ft",   "Remastered", 0, GID_FT,  7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"ft",   "Demo", 0, GID_FT,  7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 
-	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
-	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO2(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES)},
+	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
+	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
 
 	{"dig",  "", 0, GID_DIG, 7, 0, MDT_NONE, 0, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"dig",  "Demo", 0, GID_DIG, 7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 0c57a5ab083..cc0a28aac3d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -41,6 +41,7 @@
 
 #include "scumm/insane/insane_rebel.h"
 
+#include "common/config-manager.h"
 #include "audio/audiostream.h"
 #include "audio/decoders/raw.h"
 
@@ -390,6 +391,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_menuInactivityTimer = 0;
 	_lastMenuVariant = -1;        // No previous menu video
 	_menuRepeatDelay = 0;
+	_menuSelectionConfirmed = false;
 	for (i = 0; i < 16; i++) {
 		_levelUnlocked[i] = (i == 0);  // Only level 1 unlocked initially
 	}
@@ -404,7 +406,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Debug flag to unlock all chapters for testing
 	// Based on original debug mode (DAT_0047ab34 == 'd') from FUN_00415CF8
 	// Set to true to bypass normal unlock progression
-	_debugUnlockAll = true;  // TODO: Set to false for release, or read from ScummVM config
+	_debugUnlockAll = ConfMan.getBool("rebel2_unlock_all");
 
 	for (i = 0; i < 16; i++) {
 		// If debug unlock is enabled, unlock all chapters
@@ -515,6 +517,7 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 					// In menu mode: Select quit option and confirm selection
 					// This emulates the assembly behavior from FUN_0041f5ae
 					_menuSelection = _menuItemCount - 1;  // Select last item (quit/back)
+					_menuSelectionConfirmed = true;
 					debug("Rebel2: ESC pressed in menu - selecting quit (item %d)", _menuSelection);
 				} else {
 					debug("Rebel2: ESC pressed - skipping video");
@@ -762,7 +765,7 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 // Original: FUN_00411980 (load) / FUN_00411A5D (save)
 
 static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
-static const uint16 kPilotSaveVersion = 1;
+static const uint16 kPilotSaveVersion = 2;
 
 bool InsaneRebel2::loadPilots() {
 	_numPilots = 0;
@@ -779,7 +782,7 @@ bool InsaneRebel2::loadPilots() {
 			break;
 		}
 
-		/* uint16 version = */ sf->readUint16LE();
+		uint16 version = sf->readUint16LE();
 
 		PilotData &p = _pilots[i];
 		sf->read(p.name, kMaxPilotNameLen + 1);
@@ -791,6 +794,10 @@ bool InsaneRebel2::loadPilots() {
 		for (int j = 0; j < kNumLevels; j++)
 			p.damage[j] = sf->readSint32LE();
 		p.difficulty = sf->readSint16LE();
+		if (version >= 2) {
+			for (int j = 0; j < kNumLevels; j++)
+				p.rating[j] = sf->readSint16LE();
+		}
 		delete sf;
 
 		_numPilots = i + 1;
@@ -824,6 +831,8 @@ bool InsaneRebel2::savePilots() {
 		for (int j = 0; j < kNumLevels; j++)
 			sf->writeSint32LE(p.damage[j]);
 		sf->writeSint16LE(p.difficulty);
+		for (int j = 0; j < kNumLevels; j++)
+			sf->writeSint16LE(p.rating[j]);
 
 		sf->finalize();
 		delete sf;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 807184f52fe..606a95bd3fb 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -87,6 +87,7 @@ public:
 	int _menuInactivityTimer;       // Timeout counter (300 frames = ~10 sec)
 	int _lastMenuVariant;           // Last random menu video shown (DAT_00482400)
 	int _menuRepeatDelay;           // Delay for key repeat (DAT_00459ce0)
+	bool _menuSelectionConfirmed;   // True only when user explicitly confirmed a selection
 	bool _levelUnlocked[16];        // Which levels are available (progress flags)
 
 	// Run the main menu loop - returns when game should start or quit
@@ -156,6 +157,12 @@ public:
 	// Draw score/info display at bottom of chapter select - emulates FUN_00434cb0 calls
 	void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
 
+	// Rating-to-medal string conversion (FUN_0042001f)
+	Common::String getRankString(int rating);
+
+	// Password table lookup (FUN_0041BCE0)
+	Common::String getChapterPassword(int level, int difficulty);
+
 	// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
 	// Original: 10 pilot slots × 0x118 (280) bytes at DAT_004568A8
 	// Stored via SaveFileManager in a custom save file
@@ -169,10 +176,12 @@ public:
 		int32 score[kNumLevels];         // +0x2C: Per-level score (0 = default, 0xFF = unplayed)
 		int32 lives[kNumLevels];         // +0x6C: Per-level lives (4 = default, 0xFF = unplayed)
 		int32 damage[kNumLevels];        // +0xAC: Per-level damage (0xFF = unplayed)
+		int16 rating[kNumLevels];        // +0xEC: Per-level difficulty rating (0-50)
 		int16 difficulty;                // +0x10C: Difficulty setting (0-5)
 
 		void init() {
 			memset(name, 0, sizeof(name));
+			memset(rating, 0, sizeof(rating));
 			difficulty = 2; // Default to 3rd option
 			score[0] = 0;
 			lives[0] = 4;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 4ccbc90adca..415d209bf31 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -42,6 +42,7 @@ void InsaneRebel2::resetMenu() {
 	_menuSelection = 0;
 	_menuInactivityTimer = 0;
 	_menuRepeatDelay = 0;
+	_menuSelectionConfirmed = false;
 }
 
 // Unlock all chapters for testing
@@ -49,7 +50,6 @@ void InsaneRebel2::resetMenu() {
 // where DAT_0047ab34 == 'd' enables level unlock via special codes
 void InsaneRebel2::unlockAllChapters() {
 	debug("Rebel2: Unlocking all chapters for testing");
-	_debugUnlockAll = true;
 	for (int i = 0; i < 16; i++) {
 		_chapterUnlocked[i] = true;
 		_levelUnlocked[i] = true;
@@ -154,26 +154,8 @@ int InsaneRebel2::processMenuInput() {
 
 		case Common::EVENT_LBUTTONDOWN:
 			_menuInactivityTimer = 0;
-			{
-				// Get mouse position from the event
-				int mouseY = event.mouse.y;
-
-				debug("Menu: Left click at Y=%d", mouseY);
-
-				// Check which item was clicked
-				// From FUN_0041f5ae mouse mode: selection = (mouseY + 100 - baseY) / 10
-				// But we use a simpler direct hit-test approach
-				for (int i = 0; i < _menuItemCount; i++) {
-					int itemY = baseY + i * itemSpacing;
-					// Hit area: itemY - 2 to itemY + 8 (10 pixel height)
-					if (mouseY >= itemY - 2 && mouseY < itemY + 8) {
-						_menuSelection = i;
-						result = i;
-						debug("Menu: Item %d clicked (itemY=%d)", i, itemY);
-						break;
-					}
-				}
-			}
+			// TODO: Re-enable click-to-confirm (currently disabled for easier testing)
+			// Original behavior: clicking on a menu item both highlights and confirms it.
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
@@ -701,15 +683,15 @@ int InsaneRebel2::runMainMenu() {
 			return 0;
 		}
 
-		// If video ended naturally (not by selection), loop back
-		if (!_vm->_smushVideoShouldFinish) {
-			// Video ended without selection (reached end or ESC during video)
-			// Continue looping menu videos
+		// Only process selection if user explicitly confirmed (ENTER/ESC),
+		// not when video ended naturally (EOF sets _smushVideoShouldFinish too)
+		if (!_menuSelectionConfirmed) {
 			continue;
 		}
 
-		// Clear the flag
+		// Clear the flags
 		_vm->_smushVideoShouldFinish = false;
+		_menuSelectionConfirmed = false;
 
 		// A selection was made - process it
 		debug("Rebel2: Menu video ended with selection=%d", _menuSelection);
@@ -860,12 +842,13 @@ int InsaneRebel2::runChapterSelect() {
 			return kChapterSelectQuit;
 		}
 
-		// If video ended without selection, continue looping
-		if (!_vm->_smushVideoShouldFinish) {
+		// Only process selection if user explicitly confirmed
+		if (!_menuSelectionConfirmed) {
 			continue;
 		}
 
 		_vm->_smushVideoShouldFinish = false;
+		_menuSelectionConfirmed = false;
 
 		debug("Rebel2: Chapter selection made: %d", _chapterSelection);
 
@@ -878,12 +861,36 @@ int InsaneRebel2::runChapterSelect() {
 		}
 
 		if (_chapterSelection >= 0 && _chapterSelection < 16) {
-			// Chapter selected - start it regardless of unlock state.
-			// TODO: locked chapters should require password (FUN_00415CF8 lines 239-257)
-			_selectedChapter = _chapterSelection;
-			debug("Rebel2: Chapter %d selected", _selectedChapter + 1);
-			_menuInputActive = false;
-			return kChapterSelectPlay;
+			if (_chapterUnlocked[_chapterSelection]) {
+				// Unlocked chapter — play it
+				_selectedChapter = _chapterSelection;
+				debug("Rebel2: Chapter %d selected (unlocked)", _selectedChapter + 1);
+				_menuInputActive = false;
+				return kChapterSelectPlay;
+			}
+
+			// Locked chapter — validate password (FUN_00415CF8 lines 239-257)
+			if (_activePilot >= 0 && _activePilot < _numPilots &&
+			    _pilots[_activePilot].difficulty < 6 && _chapterSelection > 0) {
+				Common::String expected = getChapterPassword(
+					_chapterSelection, _pilots[_activePilot].difficulty);
+				if (expected.empty() || _passwordInput.equalsIgnoreCase(expected)) {
+					// Password accepted — unlock chapter
+					PilotData &pilot = _pilots[_activePilot];
+					pilot.score[_chapterSelection] = 0;
+					pilot.damage[_chapterSelection] = 0;
+					pilot.lives[_chapterSelection] = 3;
+					pilot.rating[_chapterSelection] = 0;
+					savePilots();
+					_chapterUnlocked[_chapterSelection] = true;
+					_passwordInput.clear();
+					debug("Rebel2: Chapter %d unlocked via password", _chapterSelection + 1);
+					continue;  // Re-render with updated unlock state
+				}
+			}
+			// Wrong password or no password entered
+			_passwordInput.clear();
+			debug("Rebel2: Password rejected for chapter %d", _chapterSelection + 1);
 		}
 	}
 
@@ -911,6 +918,7 @@ int InsaneRebel2::processChapterSelectInput() {
 				if (_chapterSelection < 0) {
 					_chapterSelection = _chapterItemCount - 1;
 				}
+				_passwordInput.clear();
 				// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
 				_previewOffsetY = _chapterSelection * -50 + 75;
 				debug("ChapterSelect: Selection changed to %d (UP) offsetY=%d", _chapterSelection, _previewOffsetY);
@@ -922,6 +930,7 @@ int InsaneRebel2::processChapterSelectInput() {
 				if (_chapterSelection >= _chapterItemCount) {
 					_chapterSelection = 0;
 				}
+				_passwordInput.clear();
 				// Update preview offset (FUN_00425170: Y = selected * -50 + 75)
 				_previewOffsetY = _chapterSelection * -50 + 75;
 				debug("ChapterSelect: Selection changed to %d (DOWN) offsetY=%d", _chapterSelection, _previewOffsetY);
@@ -962,11 +971,8 @@ int InsaneRebel2::processChapterSelectInput() {
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
-			// Click confirms the current selection (original: DAT_0047a7e4 & 1)
-			if (_chapterSelection >= 0 && _chapterSelection < _chapterItemCount) {
-				result = _chapterSelection;
-				debug("ChapterSelect: Item %d confirmed (CLICK)", _chapterSelection);
-			}
+			// TODO: Re-enable click-to-confirm (currently disabled for easier testing)
+			// Original behavior: any click confirms current selection (DAT_0047a7e4 & 1)
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
@@ -1187,9 +1193,36 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 	}
 }
 
+// Rating-to-medal string conversion - emulates FUN_0042001f
+// Converts a rating value (0-50) to a string of medal characters for TALKFONT.NUT:
+//   Every 9 points → big medal (DAT_00482550)
+//   Every 3 points → medium medal (DAT_00482558)
+//   Every 1 point  → small medal (DAT_00482560)
+Common::String InsaneRebel2::getRankString(int rating) {
+	if (rating > 50)
+		rating = 50;
+	Common::String result;
+	// TODO: Medal char bytes are placeholders — verify against actual NUT font glyphs
+	// from DAT_00482550/58/60 in the game binary
+	while (rating >= 9) { result += '\x83'; rating -= 9; }  // big medal
+	while (rating >= 3) { result += '\x82'; rating -= 3; }  // medium medal
+	while (rating >= 1) { result += '\x81'; rating -= 1; }  // small medal
+	return result;
+}
+
+// Password table lookup - emulates FUN_0041BCE0
+// Original: 90-entry table at DAT_00481af0, 20 bytes each, XOR 0xAA
+// Index formula: (difficulty + (level * 3 - 3) * 2) * 20, level is 1-based
+Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
+	// TODO: Extract actual password table from game binary (FUN_0041BCE0)
+	// Table: 90 entries x 20 bytes at exe offset 0x481AF0, XOR 0xAA
+	// For now, return empty string (no password required — debug mode)
+	return "";
+}
+
 // Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
 // For unlocked chapters: score display using TRS 80 at (25, 190)
-// For locked chapters: password prompt at (30, 190)
+// For locked chapters: password prompt using TRS 81 at (30, 190)
 void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
 	if (_chapterSelection < 0 || _chapterSelection >= 16)
 		return;
@@ -1198,34 +1231,113 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 	if (!splayer)
 		return;
 
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font)
+	// Font system — same as drawMenuItems()
+	NutRenderer *fonts[3] = {
+		_smush_talkfontNut,
+		_smush_smalfontNut,
+		_smush_titlefontNut
+	};
+	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+	if (!defaultFont)
 		return;
 
 	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
 	int actualPitch = _vm->_screenWidth;
 
+	// Format code parser — same as drawMenuItems()
+	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
+		if (*str != '^')
+			return -1;
+		const char *p = str + 1;
+		if (*p == '^') { str = p; return -1; }
+		if (*p == 'f') {
+			p++;
+			int fontIdx = 0;
+			while (*p >= '0' && *p <= '9') { fontIdx = fontIdx * 10 + (*p - '0'); p++; }
+			str = p;
+			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
+		}
+		if (*p == 'c') {
+			p++;
+			int color = 0;
+			while (*p >= '0' && *p <= '9') { color = color * 10 + (*p - '0'); p++; }
+			str = p;
+			outColor = color;
+			return -2;
+		}
+		return -1;
+	};
+
+	// String rendering with format codes
+	auto drawString = [&](const char *str, int x, int y) {
+		NutRenderer *curFont = defaultFont;
+		int curColor = 1;
+
+		while (*str) {
+			int fontChange = parseFormatCode(str, curColor);
+			if (fontChange >= 0) {
+				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
+				continue;
+			}
+			if (fontChange == -2)
+				continue;
+
+			byte c = (byte)*str++;
+			if (c >= 'a' && c <= 'z')
+				c = c - 'a' + 'A';
+			if (!curFont)
+				continue;
+			int numChars = curFont->getNumChars();
+			if (c >= numChars)
+				continue;
+			int charW = curFont->getCharWidth(c);
+			if (x >= 0 && y >= 0 && charW > 0) {
+				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
+				                    kStyleAlignLeft, c, false, false);
+			}
+			x += charW;
+		}
+	};
+
 	if (_chapterUnlocked[_chapterSelection]) {
 		// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
-		const char *scoreStr = splayer->getString(80);
-		if (!scoreStr || !scoreStr[0])
+		// TRS 80 = "^f01^c248Pilots: %hd  Score: %ld  Rank: ^f00%s"
+		const char *fmtStr = splayer->getString(80);
+		if (!fmtStr || !fmtStr[0])
 			return;
 
-		int curX = 25;
-		int numChars = font->getNumChars();
-		for (const char *c = scoreStr; *c; c++) {
-			byte ch = (byte)*c;
-			if (ch >= 'a' && ch <= 'z')
-				ch = ch - 'a' + 'A';
-			if (ch < numChars) {
-				int charW = font->getCharWidth(ch);
-				if (curX >= 0 && curX + charW <= width && 190 < height) {
-					font->drawCharV7(renderBitmap, clipRect, curX, 190, actualPitch, 1,
-					                 kStyleAlignLeft, ch, false, false);
-				}
-				curX += charW;
-			}
+		// Get pilot data for this chapter
+		int32 pilotLives = 0;
+		int32 pilotScore = 0;
+		int16 pilotRating = 0;
+		if (_activePilot >= 0 && _activePilot < _numPilots) {
+			pilotLives = _pilots[_activePilot].lives[_chapterSelection];
+			pilotScore = _pilots[_activePilot].score[_chapterSelection];
+			pilotRating = _pilots[_activePilot].rating[_chapterSelection];
 		}
+		Common::String rankStr = getRankString(pilotRating);
+
+		// sprintf substitution: %hd → lives, %ld → score, %s → rank
+		Common::String displayStr = Common::String::format(fmtStr,
+			(short)pilotLives, (long)pilotScore, rankStr.c_str());
+
+		drawString(displayStr.c_str(), 25, 190);
+	} else {
+		// Locked: show password prompt using TRS 81 at X=30 (0x1e), Y=190 (0xbe)
+		// Format: "%s ^c005%s%c" (TRS 81 + green password input + blinking cursor)
+		const char *lockStr = splayer->getString(81);
+		if (!lockStr || !lockStr[0])
+			lockStr = "^f01^c248UNREGISTERED - PASSCODE REQUIRED";
+
+		// Blinking cursor: alternate '_' and ' ' (original uses bit 1 of frame counter)
+		static int cursorCounter = 0;
+		cursorCounter++;
+		char cursor = ((cursorCounter / 8) & 1) ? '_' : ' ';
+
+		Common::String displayStr = Common::String::format("%s ^c005%s%c",
+			lockStr, _passwordInput.c_str(), cursor);
+
+		drawString(displayStr.c_str(), 30, 190);
 	}
 }
 
@@ -1323,10 +1435,11 @@ int InsaneRebel2::runLevelSelect() {
 			return kLevelSelectQuit;
 		}
 
-		if (!_vm->_smushVideoShouldFinish)
+		if (!_menuSelectionConfirmed)
 			continue;
 
 		_vm->_smushVideoShouldFinish = false;
+		_menuSelectionConfirmed = false;
 
 		// --- Difficulty submenu completed ---
 		if (_pilotMenuMode == kPilotModeDifficulty) {
@@ -1445,6 +1558,7 @@ int InsaneRebel2::processLevelSelectInput() {
 				    event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
 					// Confirm name — signal back to runLevelSelect()
 					if (_pilotNameInput.size() > 0) {
+						_menuSelectionConfirmed = true;
 						_vm->_smushVideoShouldFinish = true;
 						debug("PilotName: confirmed '%s'", _pilotNameInput.c_str());
 					}
@@ -1491,7 +1605,6 @@ int InsaneRebel2::processLevelSelectInput() {
 
 	// Mouse hit Y positions — must match drawMenuItems() formula
 	const int baseY = itemCount * -5 + 0x68;
-	const int itemHeight = 10;
 
 	while (!_menuEventQueue.empty()) {
 		Common::Event event = _menuEventQueue.pop();
@@ -1533,17 +1646,7 @@ int InsaneRebel2::processLevelSelectInput() {
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
-			{
-				int mouseY = event.mouse.y;
-				for (int i = 0; i < itemCount; i++) {
-					int itemY = baseY + i * itemHeight;
-					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
-						selection = i;
-						result = i;
-						break;
-					}
-				}
-			}
+			// TODO: Re-enable click-to-confirm (currently disabled for easier testing)
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 01ab72b60e5..fe76fe6e137 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1663,6 +1663,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// If a selection was confirmed, signal video to stop
 		if (selection >= 0) {
 			debug("Rebel2: Pilot selection confirmed: %d", selection);
+			_menuSelectionConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
 
@@ -1692,6 +1693,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// If a selection was confirmed, signal video to stop
 		if (selection >= 0) {
 			debug("Rebel2: Chapter selection confirmed: %d", selection);
+			_menuSelectionConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
 
@@ -1736,6 +1738,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// If a selection was confirmed, signal video to stop
 		if (selection >= 0) {
 			debug("Rebel2: Menu selection confirmed: %d", selection);
+			_menuSelectionConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
 
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 4fe0f9a1511..0d9479e420b 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -887,6 +887,15 @@ static const ExtraGuiOption enableRebel2HiRes = {
 	0
 };
 
+static const ExtraGuiOption enableRebel2UnlockAll = {
+	_s("Unlock all levels"),
+	_s("All levels will be available without requiring passwords."),
+	"rebel2_unlock_all",
+	false,
+	0,
+	0
+};
+
 const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &target) const {
 	ExtraGuiOptions options;
 	// Query the GUI options
@@ -931,6 +940,9 @@ const ExtraGuiOptions ScummMetaEngine::getExtraGuiOptions(const Common::String &
 	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_HIRES)) {
 		options.push_back(enableRebel2HiRes);
 	}
+	if (target.empty() || guiOptions.contains(GAMEOPTION_REBEL2_UNLOCK_ALL)) {
+		options.push_back(enableRebel2UnlockAll);
+	}
 	if (target.empty() || gameid == "comi") {
 		options.push_back(comiObjectLabelsOption);
 


Commit: 52fcf017dcfb54abb24d346e347a368f84288e0c
    https://github.com/scummvm/scummvm/commit/52fcf017dcfb54abb24d346e347a368f84288e0c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:14+02:00

Commit Message:
SCUMM: RA2: Improve password handling

Changed paths:
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 7a17b021d63..f86f9ff630e 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -70,23 +70,18 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// IACT opcode 6 processing (iactRebel2Opcode6), matching the original
 	// FUN_41CADB architecture. No corridor drawing needed here.
 
-	// Chapter selection: Set FOBJ offset to scroll preview thumbnails in O_LEVEL.SAN.
-	// Original (FUN_00415CF8): offsets start at (0,0) for the first display update,
-	// then FUN_00425170 sets them to (-90, chapter*-50+75) AFTER each frame.
-	// Frame 0 must use (0,0) so the 80x800 preview strip at X=320 renders off-screen
-	// and STOR captures it cleanly. Frames 1+ use the scroll offset so FTCH re-renders
-	// the strip at the correct preview position.
+	// Chapter selection: Set FOBJ offset for O_LEVEL.SAN preview strip.
+	// The 80x800 FOBJ strip is at left=320. With offset X=-90, it renders at
+	// X=230 (inside the preview box). The Y offset scrolls the strip vertically
+	// so only the selected chapter's 50px slice appears at Y=75.
+	// STOR captures raw FOBJ data regardless of screen rendering, so the
+	// offset on frame 0 doesn't affect the STOR/FTCH mechanism.
 	if (_gameState == kStateChapterSelect && _player) {
-		if (_player->_frame > 0) {
-			// Clear screen to black before FTCH re-renders the preview strip.
-			// Our FTCH only re-draws the preview area (80px wide at X=230);
-			// without clearing, old menu text and preview artifacts persist.
-			if (renderBitmap) {
-				memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
-			}
-			_player->_fobjOffsetX = _previewOffsetX;
-			_player->_fobjOffsetY = _previewOffsetY;
+		if (renderBitmap) {
+			memset(renderBitmap, 0, _vm->_screenWidth * _vm->_screenHeight);
 		}
+		_player->_fobjOffsetX = _previewOffsetX;
+		_player->_fobjOffsetY = _previewOffsetY;
 	}
 }
 
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 415d209bf31..92166f4a927 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -829,10 +829,12 @@ int InsaneRebel2::runChapterSelect() {
 
 		debug("Rebel2: Playing chapter select background: OPEN/O_LEVEL.SAN");
 
-		// Flags: 0x20 (overlay drawing). No 0x08 (preserve) — we want a black
-		// background. O_LEVEL.SAN has no full-screen background FOBJ; the visible
-		// screen area stays black, and preview thumbnails render at X=230 via offset.
-		splayer->setCurVideoFlags(0x20);
+		// Flags: 0x20 (overlay/menu rendering) | 0x08 (preserve buffer, suppress
+		// AHDR speed override). Matches original FUN_0041f4d0 parameter 8.
+		// O_LEVEL.SAN AHDR specifies 15fps; flag 0x08 suppresses this override
+		// so we use our intended 12fps. The preview screen is cleared each frame
+		// by procPreRendering's memset, so buffer preservation is harmless.
+		splayer->setCurVideoFlags(0x28);
 
 		// Play O_LEVEL.SAN — preview thumbnails are rendered by FOBJ offset
 		splayer->play("OPEN/O_LEVEL.SAN", 12);
@@ -874,8 +876,8 @@ int InsaneRebel2::runChapterSelect() {
 			    _pilots[_activePilot].difficulty < 6 && _chapterSelection > 0) {
 				Common::String expected = getChapterPassword(
 					_chapterSelection, _pilots[_activePilot].difficulty);
-				if (expected.empty() || _passwordInput.equalsIgnoreCase(expected)) {
-					// Password accepted — unlock chapter
+				if (!expected.empty() && _passwordInput.equalsIgnoreCase(expected)) {
+					// Password accepted — unlock chapter (FUN_00415CF8 lines 253-257)
 					PilotData &pilot = _pilots[_activePilot];
 					pilot.score[_chapterSelection] = 0;
 					pilot.damage[_chapterSelection] = 0;
@@ -883,6 +885,8 @@ int InsaneRebel2::runChapterSelect() {
 					pilot.rating[_chapterSelection] = 0;
 					savePilots();
 					_chapterUnlocked[_chapterSelection] = true;
+					// Update iactBit for video preview (original jumps to LAB_00415d88)
+					setBit(16 - _chapterSelection);
 					_passwordInput.clear();
 					debug("Rebel2: Chapter %d unlocked via password", _chapterSelection + 1);
 					continue;  // Re-render with updated unlock state
@@ -1211,13 +1215,46 @@ Common::String InsaneRebel2::getRankString(int rating) {
 }
 
 // Password table lookup - emulates FUN_0041BCE0
-// Original: 90-entry table at DAT_00481af0, 20 bytes each, XOR 0xAA
-// Index formula: (difficulty + (level * 3 - 3) * 2) * 20, level is 1-based
+// 90 entries: 15 levels × 6 difficulty slots, extracted from RA2WIN95.EXE at 0x481AF0
+// Index formula: difficulty + (level * 3 - 3) * 2, level is 1-based (1-15), difficulty 0-5
+static const char *const kPasswordTable[90] = {
+	// Level 1:  diff 0-5
+	"JABBA",    "EWOKS",    "BANTHA",   "ANAKIN",   "WOOKIEE",  "WOOKIEE",
+	// Level 2:  diff 0-5
+	"ENDOR",    "CHEWIE",   "KATANA",   "KENOBI",   "DROID",    "DROID",
+	// Level 3:  diff 0-5
+	"LACHTON",  "DANKIN",   "DENGAR",   "FORTUNA",  "RODIAN",   "RODIAN",
+	// Level 4:  diff 0-5
+	"BORSK",    "NOGHRI",   "PELLAEON", "MODON",    "BPFASSH",  "BPFASSH",
+	// Level 5:  diff 0-5
+	"KROYIES",  "CHAMMA",   "ITHULL",   "OMMIN",    "KSHYY",    "KSHYY",
+	// Level 6:  diff 0-5
+	"AURIL",    "BOGGA",    "STENNESS", "REKKON",   "TORVE",    "TORVE",
+	// Level 7:  diff 0-5
+	"KAMPL",    "INCOM",    "MYRKR",    "SHAZEEN",  "SLUISSI",  "SLUISSI",
+	// Level 8:  diff 0-5
+	"FERRIER",  "KOTHLIS",  "CHURBA",   "KIIRIUM",  "PALANHI",  "PALANHI",
+	// Level 9:  diff 0-5
+	"GALIA",    "KRATH",    "ARTOO",    "GUNDARK",  "DROKKO",   "DROKKO",
+	// Level 10: diff 0-5
+	"DENARII",  "SIOSK",    "SATAL",    "DIANOGA",  "NATTH",    "NATTH",
+	// Level 11: diff 0-5
+	"SADOW",    "ADEGAN",   "LOBUE",    "ATUARRE",  "SABACC",   "SABACC",
+	// Level 12: diff 0-5
+	"ONDERON",  "AMANOA",   "DENEBA",   "ESSADA",   "ANDUR",    "ANDUR",
+	// Level 13: diff 0-5
+	"ALEEMA",   "AMBRIA",   "STURM",    "PAPLOO",   "ARKANIA",  "ARKANIA",
+	// Level 14: diff 0-5
+	"CATHAR",   "SYLVAR",   "CRADO",    "NASHTAH",  "DIATH",    "DIATH",
+	// Level 15: diff 0-5
+	"DOMINIS",  "MIRALUKA", "CARRACK",  "PESTAGE",  "DREEBO",   "DREEBO",
+};
+
 Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
-	// TODO: Extract actual password table from game binary (FUN_0041BCE0)
-	// Table: 90 entries x 20 bytes at exe offset 0x481AF0, XOR 0xAA
-	// For now, return empty string (no password required — debug mode)
-	return "";
+	if (level < 1 || level > 15 || difficulty < 0 || difficulty > 5)
+		return "";
+	int idx = difficulty + (level * 3 - 3) * 2;
+	return kPasswordTable[idx];
 }
 
 // Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
@@ -1364,22 +1401,24 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	// Build items array matching original DAT_004577a8 layout
 	const char *items[18];
 
-	// items[0] = title = TRS 40 ("^f02Chapters")
-	items[0] = splayer->getString(40);
+	// Original (FUN_00415CF8 lines 55-77): starts with ALL locked strings,
+	// then overrides unlocked chapters individually.
+	// items[0] = title = TRS 60 ("^f02Chapters")
+	items[0] = splayer->getString(60);
 	if (!items[0] || !items[0][0])
 		items[0] = "^f02Chapters";
 
 	// items[1..16] = chapters, using unlocked (TRS 41-56) or locked (TRS 61-76) strings
 	for (int i = 1; i <= 16; i++) {
-		bool unlocked = (i - 1 < 16) && _chapterUnlocked[i - 1];
+		bool unlocked = _chapterUnlocked[i - 1];
 		int trsIdx = unlocked ? (40 + i) : (60 + i);
 		items[i] = splayer->getString(trsIdx);
 		if (!items[i] || !items[i][0])
 			items[i] = "";
 	}
 
-	// items[17] = "RETURN TO PILOTS" = TRS 57 ("^f01^c240RETURN TO PILOTS")
-	items[17] = splayer->getString(57);
+	// items[17] = "RETURN TO PILOTS" = TRS 77
+	items[17] = splayer->getString(77);
 	if (!items[17] || !items[17][0])
 		items[17] = "^f01^c240RETURN TO PILOTS";
 
@@ -1387,7 +1426,8 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	drawMenuItems(renderBitmap, pitch, width, height, items, 17, _chapterSelection, true);
 
 	// Draw preview box border on the right side (FUN_004292d0 calls at lines 128-133)
-	// The actual preview image is rendered by O_LEVEL.SAN FOBJ via the offset system
+	// The preview thumbnail is rendered by O_LEVEL.SAN via FOBJ offset + STOR/FTCH.
+	// SKIP chunks in O_LEVEL.SAN use iactBits to show locked/unlocked preview variants.
 	drawPreviewBox(renderBitmap, pitch, width, height);
 
 	// Draw score/info line at bottom
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index e0391672c68..7338951cd68 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -858,16 +858,26 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if (_dst == _specialBuffer)
 		pitch = _width;
 
+	int srcSkipY = 0;
 	if (isRA2()) {
-		ra2AdjustFrameCoords(left, top, width, height, pitch);
+		ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
 		if (width <= 0 || height <= 0)
 			return;
 	}
 
+	// For RLE codecs, skip source rows that were clipped from the top.
+	// Each RLE row has a 2-byte size prefix, so we can advance past them.
+	const uint8 *adjustedSrc = src;
+	if (srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+		for (int i = 0; i < srcSkipY; i++) {
+			adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
+		}
+	}
+
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
 	case SMUSH_CODEC_RLE_ALT:
-		smushDecodeRLE(_dst, src, left, top, width, height, pitch);
+		smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
 		break;
 	case SMUSH_CODEC_DELTA_BLOCKS:
 		if (!_deltaBlocksCodec)
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 85d6b60edbd..7c12fcfa9ac 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -304,7 +304,7 @@ private:
 							   int pos_x, int pos_y, int left, int top,
 							   int width, int height, TextStyleFlags flg);
 	void ra2SelectFrameBuffer(int width, int height);
-	void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch);
+	void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY = nullptr);
 	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
 						int width, int height, int pitch, int dataSize);
 	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 198ca7c200f..63a314fd99f 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -304,13 +304,17 @@ void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
 /**
  * Apply RA2 FOBJ position offsets and clamp to buffer bounds.
  * Modifies left, top, width, height in place.
+ * When srcSkipY is non-null, outputs the number of source rows to skip
+ * when top is clipped from negative (for codecs with row-size prefixes).
  */
-void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch) {
+void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
 	left += _fobjOffsetX;
 	top += _fobjOffsetY;
 
 	int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
 	if (top < 0) {
+		if (srcSkipY)
+			*srcSkipY = -top;
 		height += top;
 		top = 0;
 	}


Commit: 7c74f5e1348bbe728827a4af7e50c10533a8e5ea
    https://github.com/scummvm/scummvm/commit/7c74f5e1348bbe728827a4af7e50c10533a8e5ea
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:14+02:00

Commit Message:
SCUMM: RA2: Refactor player shot rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cc0a28aac3d..d541d1aa47d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -82,6 +82,16 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
 		initLaserTexture(_smush_iconsNut, 5);
 	}
+
+	// Initialize edge blend table with default identity table (FUN_404BCE -> FUN_410510(NULL))
+	// Per-level tables are loaded later via IACT opcode 8 par4=1000
+	initEdgeTable(nullptr);
+	// DAT_0047a7fc: Controls edge highlight rendering and widescreen features.
+	// Set from param_10 of FUN_403BD0 (main game init). Values:
+	//   < 0: Edge highlights disabled (low-detail mode)
+	//   >= 0: Edge highlights enabled, >= 1: high-detail (secondary NUTs, widescreen)
+	// Always use high detail in ScummVM.
+	_rebelDetailMode = 1;
 	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
 
 	// Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 606a95bd3fb..43734d7398b 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -517,12 +517,42 @@ public:
 	};
 	LaserTexture _laserTexture;  // DAT_0047fee4
 
+	// ======================= Edge Blend Table (DAT_0046a7d0) =======================
+	// 256x256 lookup table used by drawEdgeHighlightLine() (FUN_410962) to compute
+	// glow colors at beam edges. For each pixel on the beam edge, the table maps
+	// [adjacent_pixel_above][adjacent_pixel_below] -> output color, producing a
+	// color-blended highlight that gives beams their distinctive glow.
+	//
+	// Default table (FUN_410510 with NULL): table[a][b] = min(a,b) (identity/transparent).
+	// Per-level tables loaded via IACT opcode 8 par4=1000 (FUN_405663 -> FUN_410510).
+	// The per-level table tunes the glow to the current palette (red for some levels,
+	// green for others).
+	//
+	// _rebelDetailMode (DAT_0047a7fc): Controls whether edge highlights are drawn.
+	// Set from IACT opcode 6. When >= 0, edge highlights are enabled.
+	byte _edgeTable[256 * 256];       // DAT_0046a7d0 - primary edge blend table
+	byte _edgeTableAlt[256 * 256];    // DAT_00443fb0 - secondary blend table (hi-res mode)
+	int16 _rebelDetailMode;           // DAT_0047a7fc - edge highlight enable flag
+
+	// Initialize edge blend table (FUN_410510)
+	// data == nullptr: fill with default identity table (min(a,b))
+	// data != nullptr: load 256x256 symmetric table from data (skips 8-byte header)
+	void initEdgeTable(const byte *data);
+
+	// Draw edge highlight line using the edge blend table (FUN_410962)
+	// Each pixel is blended from its perpendicular neighbors via _edgeTable lookup.
+	// For horizontal-dominant beams, reads pixels above/below the line.
+	// For vertical-dominant beams, reads pixels left/right of the line.
+	void drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
+	                           int16 x0, int16 y0, int16 x1, int16 y1);
+
 	// Initialize laser texture from NUT sprite (FUN_0040BAB0)
 	void initLaserTexture(NutRenderer *nut, int spriteIdx);
 	void freeLaserTexture();
 
 	// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-	// Parameters match the original assembly function
+	// Two-layer rendering: textured scanlines (beam body) + edge highlights (glow).
+	// Edge highlights are only drawn when _rebelDetailMode >= 0.
 	void drawLaserBeam(byte *dst, int pitch, int width, int height,
 	                   int16 gunX, int16 gunY, int16 targetX, int16 targetY,
 	                   int16 animFrame, int16 maxFrames,
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index f86f9ff630e..59fdc9ddcdf 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -573,9 +573,12 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// Set ship level mode (DAT_0043e000 = par3)
 		_shipLevelMode = par3;
 
-		// If par4 == 1, enable status bar
+		// If par4 == 1, enable status bar and re-render laser texture (FUN_0040bb87)
 		if (par4 == 1) {
-			_rebelStatusBarSprite = 5;  // Status bar sprite for Handler 8
+			_rebelStatusBarSprite = 5;
+			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+				initLaserTexture(_smush_iconsNut, 5);
+			}
 		}
 
 		// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
@@ -746,7 +749,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			bodyStatusFlag = b.readSint16LE();
 		}
 		if (bodyStatusFlag == 1) {
-			_rebelStatusBarSprite = 5;  // Status bar sprite
+			_rebelStatusBarSprite = 5;
+			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+				initLaserTexture(_smush_iconsNut, 5);
+			}
 			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
 		}
 
@@ -1027,6 +1033,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
 		if (par5 == 1) {
 			_rebelStatusBarSprite = 5;
+			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+				initLaserTexture(_smush_iconsNut, 5);
+			}
 			// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
 			if (!_rebelOp6Initialized) {
 				clearBit(0);
@@ -1230,15 +1239,50 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
-	// Step 1: If par4 == 1, initialize/reset state (lines 114-121)
-	if (par4 == 1) {
-		// Draw status bar sprite 5 (FUN_0040bb87 equivalent)
-		_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+	// Handler 0x26: FUN_407FCB line 77-79 — set level type from par4, read par5 for init trigger
+	// param_5[3] = par4 = levelType, param_5[4] = par5 = init flag
+	if (_rebelHandler == 0x26) {
+		_rebelLevelType = par4;
+
+		// Read par5 from IACT body (param_5[4])
+		int16 par5 = 0;
+		if (b.pos() + 2 <= b.size()) {
+			int64 savedPos = b.pos();
+			par5 = b.readSint16LE();
+			b.seek(savedPos);
+		}
+
+		if (par5 == 1) {
+			// Re-render laser texture for this level (FUN_0040bb87)
+			// levelType 5 uses sprite 53, all others use sprite 5
+			_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > _rebelStatusBarSprite) {
+				initLaserTexture(_smush_iconsNut, _rebelStatusBarSprite);
+			}
+
+			if (!_rebelOp6Initialized) {
+				clearBit(0);
+				for (int i = 0; i < 512; i++) {
+					_rebelLinks[i][0] = 0;
+					_rebelLinks[i][1] = 0;
+					_rebelLinks[i][2] = 0;
+				}
+				_rebelWaveState = _rebelPhaseState;
+				_rebelHitCounter = 0;
+				_rebelOp6Initialized = true;
+				debug("Rebel2 Opcode 6 (Handler 0x26): Wave init, levelType=%d waveState=0x%x",
+					_rebelLevelType, _rebelWaveState);
+			}
+		}
+	}
+
+	// Other handlers: par4 == 1 triggers init (NOT level type)
+	if (_rebelHandler != 0x26 && par4 == 1) {
+		_rebelStatusBarSprite = 5;
+		if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+			initLaserTexture(_smush_iconsNut, 5);
+		}
 
-		// Per-wave init: clear bits, links, reset wave state.
-		// In the original game, FUN_00423880 runs ONCE at video-start callback
-		// registration time, not per-frame. Guard with _rebelOp6Initialized so
-		// this fires once per wave video (reset in procPreRendering at frame 0).
 		if (!_rebelOp6Initialized) {
 			clearBit(0);
 			for (int i = 0; i < 512; i++) {
@@ -1253,9 +1297,6 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// Step 2: Set level type (DAT_00457900 = par3)
-	_rebelLevelType = par3;
-
 	// Step 3: Autopilot/control mode logic (lines 123-146)
 	// This determines whether the ship flies on autopilot or manual control
 	if (!_rebelInvulnerable) {
@@ -1437,6 +1478,29 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		b.seek(startPos);
 	}
 
+	// ===== Edge Blend Table Loading (par4 == 1000) =====
+	// FUN_405663: After all handler-specific opcode 8 processing, checks if par4==1000.
+	// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
+	// This table controls the edge glow color of laser beams (e.g. red vs green).
+	// Data starts at byte offset 18 in the IACT chunk (in_stack_00000014 + 9 shorts).
+	if (par4 == 1000 && remaining >= 18 + 8 + 32896) {
+		// Read the raw edge table data from the stream
+		// Layout: 18 bytes IACT header params already consumed by caller,
+		// but 'b' is positioned at startPos which is after par1..par4.
+		// The original code passes (param + 9 shorts) = data at byte offset 18 from chunk start.
+		// Since our stream starts after the 6 par shorts (12 bytes), the data is at offset 6 from startPos.
+		byte *edgeData = (byte *)malloc(8 + 32896);
+		if (edgeData) {
+			b.seek(startPos + 6);  // Skip 3 remaining shorts (par2, par3, par4 already read; 3 more padding shorts)
+			b.read(edgeData, 8 + 32896);
+			initEdgeTable(edgeData);
+			free(edgeData);
+			debug("Rebel2 Opcode 8: Loaded per-level edge blend table (par4=1000)");
+		}
+		b.seek(startPos);
+		return;
+	}
+
 	// ===== Sound Loading (par3 21-47) =====
 	if (par3 >= 21 && par3 <= 47) {
 		debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index fe76fe6e137..8fe2735945e 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -478,11 +478,17 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 }
 
 // Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
-// For now, use a reasonable default based on observed values
+// The original reads from a per-level per-difficulty table. The field at offset +0x00
+// (DAT_0047e0f0) is the first field of each record — our laserDelay field.
+// This value is used both as the initial shot counter AND as maxFrames for beam rendering.
 int16 InsaneRebel2::getShotMaxDuration() {
-	// The original uses: DAT_0047e0f0 + DAT_0047a7fa * 0x242 + DAT_0047a7f8 * 0x22
-	// Typical values observed: 2-5 frames
-	return 4;
+	LevelDifficultyParams params = getDifficultyParams();
+	// laserDelay = DAT_0047e0f0 field: shot duration in frames
+	// Clamp to reasonable range to avoid division by zero or extreme beams
+	int16 duration = params.laserDelay;
+	if (duration <= 0)
+		duration = 4;  // Fallback for -1 entries (disabled levels)
+	return duration;
 }
 
 // Dispatcher - calls appropriate spawn function based on current handler
@@ -838,7 +844,7 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 	int sx = dx > 0 ? 1 : -1;
 	int sy = dy > 0 ? 1 : -1;
 	int err = absdx - absdy;
-	int iVar12 = steps;
+	int iVar12 = steps - 1; // Original starts at majorAxis, not majorAxis+1
 
 	for (int i = 0; i < steps; i++) {
 		if (x >= 0 && x < width && y >= 0 && y < height) {
@@ -849,7 +855,7 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_
 		if (e2 > -absdy) { err -= absdy; x += sx; }
 		if (e2 < absdx) { err += absdx; y += sy; }
 		iVar12 -= param_7;
-		if (iVar12 < 0) { texPtr++; iVar12 += steps; }
+		while (iVar12 < 0) { texPtr++; iVar12 += steps; }
 	}
 }
 
@@ -890,6 +896,31 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
 	}
 
 	debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
+
+	// Diagnostic: dump texture pixel stats to verify data is loaded correctly
+	if (_laserTexture.pixels && texWidth > 0 && texHeight > 0) {
+		int third = texWidth / 3;
+		int band1 = 0, band2 = 0, band3 = 0;
+		for (int row = 0; row < texHeight; row++) {
+			for (int col = 0; col < texWidth; col++) {
+				if (_laserTexture.pixels[row * texWidth + col] != 0) {
+					if (col < third) band1++;
+					else if (col < third * 2) band2++;
+					else band3++;
+				}
+			}
+		}
+		debug("Rebel2: Texture non-zero pixels by band: [0-%d]=%d  [%d-%d]=%d  [%d-%d]=%d",
+			third - 1, band1, third, third * 2 - 1, band2, third * 2, texWidth - 1, band3);
+
+		// Dump first row hex (first 64 bytes)
+		Common::String hexRow;
+		int dumpLen = MIN(texWidth, (int16)64);
+		for (int col = 0; col < dumpLen; col++) {
+			hexRow += Common::String::format("%02x ", _laserTexture.pixels[col]);
+		}
+		debug("Rebel2: Texture row 0 (first %d): %s", dumpLen, hexRow.c_str());
+	}
 }
 
 // Free laser texture buffer (FUN_0040BBD1)
@@ -900,8 +931,300 @@ void InsaneRebel2::freeLaserTexture() {
 	_laserTexture.height = 0;
 }
 
+// Initialize edge blend tables (FUN_410510)
+// When data is nullptr, fills with default tables:
+//   _edgeTable[a*256+b] = min(a,b) (symmetric identity blend)
+//   _edgeTableAlt[a*256+b] = special blend for hi-res mode
+// When data is non-null, loads the primary table from data+8 (upper triangle, symmetric).
+void InsaneRebel2::initEdgeTable(const byte *data) {
+	if (data == nullptr) {
+		// Default table initialization (FUN_410510 param_1==NULL path, lines 12-36)
+		for (int a = 0; a < 256; a++) {
+			for (int b = a; b < 256; b++) {
+				// Primary table: table[a][b] = a (i.e. min(a,b) since b >= a)
+				_edgeTable[a + b * 256] = (byte)a;
+				_edgeTable[b + a * 256] = (byte)a;
+
+				// Secondary table: special blend rules (FUN_410510 lines 17-31)
+				if (a < 0x10 || b > 0x4f) {
+					// Outside blend range: use b if b==0, or (0xf < b && b < 0x50), or b==4
+					if (b == 0 || (b > 0xf && b < 0x50) || b == 4) {
+						_edgeTableAlt[a + b * 256] = (byte)b;
+					} else {
+						_edgeTableAlt[a + b * 256] = (byte)a;
+					}
+				} else {
+					// Blend range [0x10..0x4f]: average of a and b
+					_edgeTableAlt[a + b * 256] = (byte)((a + b) / 2);
+				}
+				_edgeTableAlt[b + a * 256] = _edgeTableAlt[a + b * 256];
+			}
+		}
+		// Special entries (FUN_410510 lines 33-36)
+		_edgeTable[0x42 * 256 + 0xf1] = 0x42;   // DAT_00447ff1
+		_edgeTable[0x42 + 0xf0 * 256] = 0x42;   // DAT_004480f0 (symmetric)
+		_edgeTable[0x41 * 256 + 0xb0] = 0x41;   // DAT_00447fb0
+		_edgeTableAlt[0x41 * 256 + 0xf0] = 0x41; // DAT_00443ff0
+	} else {
+		// Load table from IACT data (FUN_410510 non-NULL path, lines 39-47)
+		// Data format: 8-byte header + upper triangle of 256x256 symmetric table
+		const byte *src = data + 8;
+		for (int a = 0; a < 256; a++) {
+			for (int b = a; b < 256; b++) {
+				_edgeTable[a + b * 256] = *src;
+				_edgeTable[b + a * 256] = *src;
+				src++;
+			}
+		}
+	}
+}
+
+// Draw edge highlight line using the edge blend table (FUN_410962)
+// For each pixel along the line, reads the two adjacent pixels perpendicular to
+// the line direction and uses _edgeTable[above*256+below] as the output color.
+// This creates a glow/blend effect at beam edges.
+//
+// For horizontal-dominant lines (dx > dy), reads pixels above and below.
+// For vertical-dominant lines (dy > dx), reads pixels left and right.
+//
+// param_1 = dst buffer info (base pointer at [0], pitch at [1], width at word [1], height at byte offset 6)
+// param_2 = clip rect (or NULL for full buffer)
+// param_3..param_6 = x0, y0, x1, y1 line endpoints
+void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
+                                          int16 x0, int16 y0, int16 x1, int16 y1) {
+	// Clip region (FUN_410962 lines 19-30, simplified for our buffer layout)
+	int16 clipLeft = 1;
+	int16 clipTop = 1;
+	int16 clipRight = width - 2;
+	int16 clipBottom = height - 2;
+
+	// Clip X endpoints (FUN_410962 lines 35-69)
+	if (x0 == x1) {
+		if (x0 < clipLeft || x0 > clipRight)
+			return;
+	} else {
+		if (x0 < clipLeft) {
+			if (x1 < clipLeft) return;
+			y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipLeft - x1)) / (int)(x0 - x1));
+			x0 = clipLeft;
+		} else if (x0 > clipRight) {
+			if (x1 > clipRight) return;
+			y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipRight - x1)) / (int)(x0 - x1));
+			x0 = clipRight;
+		}
+		if (x1 < clipLeft) {
+			y1 = y0 + (int16)(((int)(y1 - y0) * (int)(clipLeft - x0)) / (int)(x1 - x0));
+			x1 = clipLeft;
+		} else if (x1 > clipRight) {
+			y1 = y0 + (int16)(((int)(y1 - y0) * (int)(clipRight - x0)) / (int)(x1 - x0));
+			x1 = clipRight;
+		}
+	}
+
+	// Clip Y endpoints (FUN_410962 lines 71-106)
+	if (y0 == y1) {
+		if (y0 < clipTop || y0 > clipBottom)
+			return;
+	} else {
+		if (y0 < clipTop) {
+			if (y1 < clipTop) return;
+			x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipTop - y1)) / (int)(y0 - y1));
+			y0 = clipTop;
+		} else if (y0 > clipBottom) {
+			if (y1 > clipBottom) return;
+			x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipBottom - y1)) / (int)(y0 - y1));
+			y0 = clipBottom;
+		}
+		if (y1 < clipTop) {
+			x1 = x0 + (int16)(((int)(x1 - x0) * (int)(clipTop - y0)) / (int)(y1 - y0));
+			y1 = clipTop;
+		} else if (y1 > clipBottom) {
+			x1 = x0 + (int16)(((int)(x1 - x0) * (int)(clipBottom - y0)) / (int)(y1 - y0));
+			y1 = clipBottom;
+		}
+	}
+
+	// Calculate starting pixel address and deltas (FUN_410962 lines 107-110)
+	byte *pixel = dst + y0 * pitch + x0;
+	int16 dx = x1 - x0;
+	int16 dy = y1 - y0;
+
+	// Bresenham line with perpendicular neighbor lookup (FUN_410962 lines 111-270)
+	// The key insight: for each pixel, the blend reads neighbors PERPENDICULAR to the line.
+	// - Horizontal lines: blend pixel_above * 256 + pixel_below
+	// - Vertical lines: blend pixel_left * 256 + pixel_right (reversed: left=[-1], right=[+1])
+	if (dx == 0) {
+		if (dy == 0) {
+			// Single pixel: blend from above/below
+			*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+		} else if (dy < 0) {
+			// Vertical line going up: read left/right neighbors
+			while (dy < 1) {
+				*pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+				pixel -= pitch;
+				dy++;
+			}
+		} else {
+			// Vertical line going down: read left/right neighbors
+			while (dy >= 0) {
+				*pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+				pixel += pitch;
+				dy--;
+			}
+		}
+	} else if (dy == 0) {
+		if (dx < 0) {
+			// Horizontal line going left: read above/below neighbors
+			while (dx < 1) {
+				*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+				pixel--;
+				dx++;
+			}
+		} else {
+			// Horizontal line going right: read above/below neighbors
+			while (dx >= 0) {
+				*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+				pixel++;
+				dx--;
+			}
+		}
+	} else if (dx < 0 || dy < 0) {
+		// Mixed negative direction cases (FUN_410962 lines 149-240)
+		if (dy < 0) {
+			if (dx < 0) {
+				// Both negative: going up-left
+				if (dx < dy) {
+					// X-major (|dx| > |dy|): read above/below
+					int err = (-dx) >> 1;
+					int steps = 1 - dx;
+					while (steps > 0) {
+						*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+						pixel--;
+						err += dy;
+						steps--;
+						if (err < 0) {
+							err -= dx;
+							pixel -= pitch;
+						}
+					}
+				} else {
+					// Y-major (|dy| > |dx|): read left/right
+					int err = (-dy) >> 1;
+					int steps = 1 - dy;
+					while (steps > 0) {
+						*pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+						pixel -= pitch;
+						err += dx;
+						steps--;
+						if (err < 0) {
+							err -= dy;
+							pixel--;
+						}
+					}
+				}
+			} else {
+				// dx > 0, dy < 0: going right-up
+				if (-dy < dx) {
+					// X-major: read above/below
+					int err = dx >> 1;
+					int steps = dx + 1;
+					while (steps > 0) {
+						*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+						pixel++;
+						err += dy;
+						steps--;
+						if (err < 0) {
+							err += dx;
+							pixel -= pitch;
+						}
+					}
+				} else {
+					// Y-major: read left/right
+					int err = (-dy) >> 1;
+					int steps = 1 - dy;
+					while (steps > 0) {
+						*pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+						pixel -= pitch;
+						err -= dx;
+						steps--;
+						if (err < 0) {
+							err -= dy;
+							pixel++;
+						}
+					}
+				}
+			}
+		} else {
+			// dx < 0, dy > 0: going left-down
+			if (-dx == dy || -dx < dy) {
+				// Y-major: read left/right
+				int err = dy >> 1;
+				int steps = dy + 1;
+				while (steps > 0) {
+					*pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+					pixel += pitch;
+					err += dx;
+					steps--;
+					if (err < 0) {
+						err += dy;
+						pixel--;
+					}
+				}
+			} else {
+				// X-major: read above/below
+				int err = (-dx) >> 1;
+				int steps = 1 - dx;
+				while (steps > 0) {
+					*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+					pixel--;
+					err -= dy;
+					steps--;
+					if (err < 0) {
+						err -= dx;
+						pixel += pitch;
+					}
+				}
+			}
+		}
+	} else {
+		// Both positive: going right-down (FUN_410962 lines 242-270)
+		if (dy < dx) {
+			// X-major: read above/below
+			int err = dx >> 1;
+			int steps = dx + 1;
+			while (steps > 0) {
+				*pixel = _edgeTable[(uint)pixel[pitch] + (uint)pixel[-pitch] * 256];
+				pixel++;
+				err -= dy;
+				steps--;
+				if (err < 0) {
+					err += dx;
+					pixel += pitch;
+				}
+			}
+		} else {
+			// Y-major: read left/right
+			int err = dy >> 1;
+			int steps = dy + 1;
+			while (steps > 0) {
+				*pixel = _edgeTable[(uint)pixel[1] + (uint)pixel[-1] * 256];
+				pixel += pitch;
+				err -= dx;
+				steps--;
+				if (err < 0) {
+					err += dy;
+					pixel++;
+				}
+			}
+		}
+	}
+}
+
 // Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-// This is a direct port of the assembly function
+// This is a direct port of the assembly function.
+// Two-layer rendering:
+//   Layer 1: Textured scanlines (beam body) via drawTexturedSegment()
+//   Layer 2: Edge highlights (glow) via drawEdgeHighlightLine(), gated by _rebelDetailMode >= 0
 //
 // Parameters (matching FUN_0040bbf6):
 //   dst, pitch, width, height: destination buffer info
@@ -946,6 +1269,10 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	// FUN_0040BBF6 line 30: Get texture pixel pointer
 	byte *local_28 = texPixels;
 
+	debug(5, "Rebel2: drawLaserBeam gun(%d,%d) tgt(%d,%d) start(%d,%d) end(%d,%d) anim=%d/%d ws=%d hs=%d th=%d",
+		gunX, gunY, targetX, targetY, startX, startY, endX, endY,
+		animFrame, maxFrames, widthScale, heightScale, thickness);
+
 	// FUN_0040BBF6 lines 31-32: Calculate abs differences (FUN_004356e4 = abs)
 	int iVar2 = abs(startY - endY);  // |dy| of beam
 	int iVar3 = abs(startX - endX);  // |dx| of beam
@@ -960,9 +1287,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 		int16 local_24 = -numLines;
 		int16 halfLines = numLines >> 1;
 
-		// FUN_0040BBF6 lines 39-46: Draw parallel lines
+		// FUN_0040BBF6 lines 39-46: Draw parallel textured scanlines (beam body)
 		for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
-			// Draw one textured segment (vertical offset for this scanline)
 			drawTexturedSegment(dst, pitch, width, height,
 			                    startX, (startY - halfLines) + lineIdx,
 			                    endX, (endY - halfLines) + lineIdx,
@@ -973,6 +1299,16 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 				local_28 += texW;
 			}
 		}
+
+		// FUN_0040BBF6 lines 47-51: Edge highlights along top and bottom beam edges
+		if (_rebelDetailMode >= 0) {
+			drawEdgeHighlightLine(dst, pitch, width, height,
+			                      startX, startY - halfLines,
+			                      endX, endY - halfLines);
+			drawEdgeHighlightLine(dst, pitch, width, height,
+			                      startX, (startY - halfLines) + numLines - 1,
+			                      endX, (endY - halfLines) + numLines - 1);
+		}
 	} else {
 		// Mostly vertical beam - draw horizontal scanlines
 		// FUN_0040BBF6 lines 54-56
@@ -987,9 +1323,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 
 		int16 halfLines = numLines >> 1;
 
-		// FUN_0040BBF6 lines 61-68: Draw parallel lines
+		// FUN_0040BBF6 lines 61-68: Draw parallel textured scanlines (beam body)
 		for (int16 lineIdx = 0; lineIdx < numLines; lineIdx++) {
-			// Draw one textured segment (horizontal offset for this scanline)
 			drawTexturedSegment(dst, pitch, width, height,
 			                    (startX - halfLines) + lineIdx, startY,
 			                    (endX - halfLines) + lineIdx, endY,
@@ -1000,6 +1335,16 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 				local_28 += texW;
 			}
 		}
+
+		// FUN_0040BBF6 lines 69-73: Edge highlights along left and right beam edges
+		if (_rebelDetailMode >= 0) {
+			drawEdgeHighlightLine(dst, pitch, width, height,
+			                      startX - halfLines, startY,
+			                      endX - halfLines, endY);
+			drawEdgeHighlightLine(dst, pitch, width, height,
+			                      (startX - halfLines) + numLines - 1, startY,
+			                      (endX - halfLines) + numLines - 1, endY);
+		}
 	}
 }
 void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
@@ -1793,23 +2138,20 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// background persists. FOBJ sprites (enemies) are then decoded on top by SMUSH.
 	// We do NOT redraw the background here as that would overwrite FOBJ content.
 
-	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
-	// 1. FUN_004288c0: Fill status bar background at Y=0xb4 (180)
-	// 2. FUN_004089ab: Draw turret overlays, targeting reticle, crosshair
-	// 3. FUN_0041c012: Draw status bar sprites LAST (on top)
+	// --- HUD Drawing Order (from FUN_004089ab / FUN_40D836 assembly analysis) ---
+	// Original assembly render order for handler 0x26:
+	//   1. FUN_004288c0: Fill status bar background
+	//   2. FUN_004092d9: Collision/hit processing
+	//   3. FUN_00409fbc: Explosion rendering
+	//   4. FUN_0040ad63: LASER SHOTS (drawn BEFORE cockpit overlays)
+	//   5. FUN_004236e0: Crosshair + cockpit NUT overlays (drawn ON TOP of lasers)
+	//   6. FUN_0041c012: Status bar text/numbers
+	// The cockpit frame covers laser beam edges, giving the appearance
+	// that beams emerge from behind the cockpit.
 
 	// STEP 0: Fill status bar background (FUN_004288c0)
 	renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
 
-	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
-	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
-
-	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
-	renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
-
-	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
-	renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
-
 	// Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
 	debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
 		_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
@@ -1826,12 +2168,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Enemy indicators and destroyed enemy area erase
 	renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
 
-	// Explosion animations (FUN_409FBC)
+	// Explosion animations (FUN_409FBC) — drawn before lasers in original
 	renderExplosions(renderBitmap, pitch, width, height);
 
-	// Laser shot beams and impacts
+	// Laser shot beams — drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges
 	renderLaserShots(renderBitmap, pitch, width, height);
 
+	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
+	// These are cockpit frame, crosshair, and reticle — drawn ON TOP of laser beams
+	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
+
+	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
+	renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
+
+	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
+	renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
+
 	// Damage visual effects — handler-specific per original architecture:
 	//   Handler 8:    FUN_401CCF line 119 → FUN_00420754 (palette flash + screen shake)
 	//   Handler 0x19: FUN_41DB5E line 192 → FUN_00420562 (palette flash only, every frame)
@@ -3260,16 +3612,16 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 		switch (_rebelLevelType) {
 		case 1:
 			// Type 1: 3 guns (triple cannon configuration)
-			// Gun 1: (0x136, 0xaa) = (310, 170)
-			// Gun 2: (0xa0, 0x17c) = (160, 380)
-			// Gun 3: (0x0a, 0xaa) = (10, 170)
+			// Gun 1: (0x136, 0xaa) = (310, 170) - right
+			// Gun 2: (0xa0, 0x17c) = (160, 380) - center bottom (off-screen, clipped)
+			// Gun 3: (0x0a, 0xaa) = (10, 170) - left
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				310 + _viewX, 170 + _viewY, targetX, targetY,
 				progress, maxDuration, 12, 8, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				160 + _viewX, 380 + _viewY, targetX, targetY,
-				progress, maxDuration, 12, 8, 12);
+				progress, maxDuration, 8, 5, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				10 + _viewX, 170 + _viewY, targetX, targetY,
@@ -3281,49 +3633,51 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 			// Type 2/5: 2 guns (wing cannons)
 			// Left: (0x6e, 0xe6) = (110, 230)
 			// Right: (0xd2, 0xe6) = (210, 230)
-			// Assembly uses widthScale=0x19(25) for these types
+			// Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				110 + _viewX, 230 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
+				progress, maxDuration, 25, 8, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				210 + _viewX, 230 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
+				progress, maxDuration, 25, 8, 12);
 			break;
 
 		case 6:
 			// Type 6: 2 guns (offscreen - cinematic effect)
 			// Gun 1: (-100, 0)
 			// Gun 2: (0, 0)
+			// Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				-100 + _viewX, 0 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
+				progress, maxDuration, 25, 8, 12);
 
 			drawLaserBeam(renderBitmap, pitch, width, height,
 				0 + _viewX, 0 + _viewY, targetX, targetY,
-				progress, maxDuration, 25, 8, 25);
+				progress, maxDuration, 25, 8, 12);
 			break;
 
 		default:
 			// Default: 2 guns with alternating pattern based on shot sequence
 			// When seqNum & 1 == 0: Left (10, 50), Right (310, 130)
 			// When seqNum & 1 == 1: Left (310, 50), Right (10, 130)
+			// Assembly: widthScale=0x19(25), heightScale=8, thickness=0xC(12)
 			if ((_turretShots[i].seqNum & 1) == 0) {
 				drawLaserBeam(renderBitmap, pitch, width, height,
 					10 + _viewX, 50 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
+					progress, maxDuration, 25, 8, 12);
 
 				drawLaserBeam(renderBitmap, pitch, width, height,
 					310 + _viewX, 130 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
+					progress, maxDuration, 25, 8, 12);
 			} else {
 				drawLaserBeam(renderBitmap, pitch, width, height,
 					310 + _viewX, 50 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
+					progress, maxDuration, 25, 8, 12);
 
 				drawLaserBeam(renderBitmap, pitch, width, height,
 					10 + _viewX, 130 + _viewY, targetX, targetY,
-					progress, maxDuration, 25, 8, 25);
+					progress, maxDuration, 25, 8, 12);
 			}
 			break;
 		}


Commit: fd61a9b4f3f746282a632d452af0aaf80d918195
    https://github.com/scummvm/scummvm/commit/fd61a9b4f3f746282a632d452af0aaf80d918195
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:15+02:00

Commit Message:
SCUMM: RA2: Guard background copies by buffer pitch

Changed paths:
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 59fdc9ddcdf..21ca7728202 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -52,9 +52,17 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	// positions are erased before new ones are drawn.
 	//
 	// This is called at the start of handleFrame(), before any FOBJ chunks are processed.
+	//
+	// IMPORTANT: Only restore when the render buffer pitch matches the background pitch (320).
+	// Levels like Level 12 (Sewers) use oversized buffers (640x260) where FOBJ/FETCH handles
+	// background restoration. Copying the 320-wide background into a 640-wide buffer with
+	// hardcoded pitch=320 would corrupt the corridor rendering.
 	if (_rebelHandler == 8 && _level2BackgroundLoaded && _level2Background && renderBitmap) {
-		for (int y = 0; y < 200; y++) {
-			memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
+		int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
+		if (bufferPitch == 320) {
+			for (int y = 0; y < 200; y++) {
+				memcpy(renderBitmap + y * 320, _level2Background + y * 320, 320);
+			}
 		}
 	}
 
@@ -1971,12 +1979,21 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
 						_level2BackgroundLoaded = true;
 						foundBackground = true;
 
-						// Copy to render bitmap immediately if provided
+						// Copy to render bitmap immediately if provided.
+						// Only copy when render buffer pitch is 320 (standard screen size).
+						// For oversized buffers (e.g., Level 12's 640x260 corridor),
+						// the FOBJ/FETCH system handles background rendering and copying
+						// 320-wide data into a wider buffer would corrupt the corridor.
 						if (renderBitmap) {
-							for (int by = 0; by < 200; by++) {
-								memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
+							int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
+							if (bufferPitch == 320) {
+								for (int by = 0; by < 200; by++) {
+									memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
+								}
+								debug("Rebel2 loadLevel2Background: Copied to renderBitmap (pitch=%d)", bufferPitch);
+							} else {
+								debug("Rebel2 loadLevel2Background: Skipping renderBitmap copy (pitch=%d != 320)", bufferPitch);
 							}
-							debug("Rebel2 loadLevel2Background: Copied to renderBitmap");
 						}
 					}
 					break;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 7338951cd68..614cba00a93 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -391,6 +391,9 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleStore()");
 	assert(subSize >= 4);
 	_storeFrame = true;
+	if (isRA2()) {
+		debug("SmushPlayer STOR: frame=%d - will store next FOBJ", _frame);
+	}
 }
 
 void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -858,6 +861,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if (_dst == _specialBuffer)
 		pitch = _width;
 
+	// Save original FOBJ dimensions before clipping. Codec 37/47 (delta block/glyph)
+	// decode the full frame into the buffer starting at (0,0) regardless of FOBJ
+	// left/top position. They must use the original un-clipped dimensions.
+	int origWidth = width;
+	int origHeight = height;
+
 	int srcSkipY = 0;
 	if (isRA2()) {
 		ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
@@ -880,19 +889,22 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
 		break;
 	case SMUSH_CODEC_DELTA_BLOCKS:
+		// Codec 37 writes the full frame to dst via memcpy — always uses original
+		// FOBJ dimensions, not position-clipped dimensions.
 		if (!_deltaBlocksCodec)
-			_deltaBlocksCodec = new SmushDeltaBlocksDecoder(width, height);
+			_deltaBlocksCodec = new SmushDeltaBlocksDecoder(origWidth, origHeight);
 		if (_deltaBlocksCodec)
 			_deltaBlocksCodec->decode(_dst, src);
 		break;
 	case SMUSH_CODEC_DELTA_GLYPHS:
+		// Codec 47 also writes the full frame — use original dimensions.
 		if (!_deltaGlyphsCodec)
-			_deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(width, height);
+			_deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(origWidth, origHeight);
 		if (_deltaGlyphsCodec)
 			_deltaGlyphsCodec->decode(_dst, src);
 		break;
 	case SMUSH_CODEC_UNCOMPRESSED:
-		smushDecodeUncompressed(_dst, src, left, top, width, height, _vm->_screenWidth);
+		smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
 		break;
 	default:
 		if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
@@ -905,6 +917,25 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		error("Invalid codec for frame object : %d", codec);
 	}
 
+	// RA2 debug: check buffer fill after decode
+	if (isRA2() && _dst == _specialBuffer && _frame < 3) {
+		int nonZero = 0;
+		int total = _width * _height;
+		for (int i = 0; i < total; i++) {
+			if (_dst[i] != 0) nonZero++;
+		}
+		// Sample bottom half
+		int bottomNonZero = 0;
+		int bottomStart = (_height / 2) * _width;
+		int bottomTotal = total - bottomStart;
+		for (int i = bottomStart; i < total; i++) {
+			if (_dst[i] != 0) bottomNonZero++;
+		}
+		debug("SmushPlayer FOBJ decode done: frame=%d codec=%d buf=%dx%d total=%d nonzero=%d (%d%%) bottomHalf=%d/%d (%d%%)",
+			_frame, codec, _width, _height, total, nonZero, (nonZero * 100) / total,
+			bottomNonZero, bottomTotal, bottomTotal > 0 ? (bottomNonZero * 100) / bottomTotal : 0);
+	}
+
 	if (_storeFrame && !isRA2()) {
 		if (_frameBuffer == nullptr) {
 			_frameBuffer = (byte *)malloc(_width * _height);
@@ -964,6 +995,11 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
 		_frame, codec, left, top, width, height, subSize - 14);
 
+	if (isRA2()) {
+		debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
+			_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
+	}
+
 	int32 chunk_size = subSize - 14;
 	byte *chunk_buffer = (byte *)malloc(chunk_size);
 	assert(chunk_buffer);
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 63a314fd99f..4c003c662e8 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -158,10 +158,15 @@ void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
 
 	// Re-decode stored FOBJ data with current offsets (matching original FUN_004246d0).
 	if (_storedFobjData != nullptr) {
+		debug("SmushPlayer FTCH: Re-decoding stored FOBJ codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
+			_storedFobjCodec, _storedFobjLeft, _storedFobjTop,
+			_storedFobjWidth, _storedFobjHeight, _storedFobjDataSize);
 		decodeFrameObject(_storedFobjCodec, _storedFobjData,
 			_storedFobjLeft, _storedFobjTop,
 			_storedFobjWidth, _storedFobjHeight,
 			_storedFobjDataSize);
+	} else {
+		debug("SmushPlayer FTCH: No stored FOBJ data! (frame=%d)", _frame);
 	}
 }
 
@@ -279,6 +284,9 @@ void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
 			_specialBufferSize = bufSize;
 			_width = width;
 			_height = height;
+			// Zero-fill the new buffer to avoid garbage in areas not written by FOBJ codec
+			memset(_specialBuffer, 0, bufSize);
+			debug("SmushPlayer: Allocated new _specialBuffer %dx%d (%d bytes)", width, height, bufSize);
 		}
 	}
 


Commit: 15c5136ca5f82edd1670ce23f1a1f1fe7b2adf64
    https://github.com/scummvm/scummvm/commit/15c5136ca5f82edd1670ce23f1a1f1fe7b2adf64
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:15+02:00

Commit Message:
SCUMM: RA2: Position ship sprites with viewport offsets

Changed paths:
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 21ca7728202..885ee5d2a59 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -652,8 +652,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 				mouseOffsetY = -127;
 
 			// Calculate target positions using the original formula
+			// Original FUN_00401234 lines 151-166:
+			//   local_18 = ((mouseX * 5 + 0x27b) * 0x40) / 0xfe    -> X target
+			//   local_1c = ((mouseY * 5 + 0x27b) * 0x10) / 0xfe    -> Y target
+			//   _DAT_0043e004 = -local_1c   (stored negated for cursor display)
+			// The interpolation (lines 181-193) uses local_1c (positive), NOT _DAT_0043e004.
+			// So the interpolation target must be the positive formula result.
 			_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
-			_shipTargetY = (int16)(-((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
+			_shipTargetY = (int16)(((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
 
 			// Smooth interpolation toward target (max 50 pixels per frame)
 			const int16 maxStep = 50;  // 0x32 in hex
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 8fe2735945e..14624911434 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -573,9 +573,9 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 			_turretShots[i].seqNum = _turretShotSeqCounter;
 			_turretShotSeqCounter++;
 
-			// Target position is where player clicked (screen coords)
-			_turretShots[i].targetX = x;
-			_turretShots[i].targetY = y;
+			// Target position is where player clicked, in buffer coords
+			_turretShots[i].targetX = x + _viewX;
+			_turretShots[i].targetY = y + _viewY;
 
 			// Compute gun position from GRD002 character sprite.
 			// Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
@@ -625,11 +625,11 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 
 				int drawX;
 				if (shouldMirror) {
-					drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+					drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
 				} else {
-					drawX = _rebelViewOffset2X + spriteXOffset;
+					drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
 				}
-				int drawY = spriteYOffset + _rebelViewOffset2Y;
+				int drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
 
 				// Gun barrel is approximately at the character's hand level:
 				// X: center of sprite ± directional offset toward the target
@@ -638,8 +638,8 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 				_turretShots[i].gunY = drawY + (spriteH * 3) / 5;
 			} else {
 				// Fallback: approximate center-bottom of character area
-				_turretShots[i].gunX = _rebelViewOffset2X + 160;
-				_turretShots[i].gunY = _rebelViewOffset2Y + 140;
+				_turretShots[i].gunX = _rebelViewOffset2X + 160 + _viewX;
+				_turretShots[i].gunY = _rebelViewOffset2Y + 140 + _viewY;
 			}
 
 			debug("Rebel2 Handler25: Spawned shot %d target (%d,%d) gun (%d,%d)",
@@ -2909,60 +2909,82 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
 	// Uses _shipSprite (POV001) with position-based offset
+	//
+	// Original FUN_00401CCF lines 87-94:
+	//   FUN_004236e0(bitmap, param_2,
+	//       (short)(shipPosX - 0xa0) >> 3,    // small X offset
+	//       (short)(shipPosY - 0x28) >> 2,    // small Y offset
+	//       0, DAT_0047e010, param_5 & 1, 1, 0);
+	//
+	// FUN_004236e0 adds the NUT sprite's internal X/Y offsets to the position
+	// parameters. The sprite's built-in offsets encode where it should appear
+	// on screen (e.g., center for Level 2/11, bottom for Level 12 FPS gun).
 
 	if (_rebelHandler != 8 || !_shipSprite || _shipLevelMode == 5)
 		return;
 
-	// Calculate display offset from raw ship position (FUN_00401ccf lines 88-89)
+	// Small position offsets from dampened ship movement (FUN_00401ccf lines 88-89)
 	int16 displayOffsetX = (_shipPosX - 0xa0) >> 3;
 	int16 displayOffsetY = (_shipPosY - 0x28) >> 2;
 
-	// Base screen position (low-res: X=160, Y=105)
-	int shipScreenX = 0xa0 + displayOffsetX;
-	int shipScreenY = 0x69 + displayOffsetY;
-
 	int numSprites = _shipSprite->getNumChars();
-	int spriteIndex = 0;
-
-	// Select sprite based on direction and sprite count
-	if (numSprites >= 35) {
-		spriteIndex = _shipDirectionH * 7 + _shipDirectionV;
-		if (spriteIndex >= numSprites)
-			spriteIndex = numSprites - 1;
-	} else if (numSprites >= 25) {
-		int vDir5 = (_shipDirectionV * 5) / 7;
-		spriteIndex = _shipDirectionH * 5 + vDir5;
-		if (spriteIndex >= numSprites)
-			spriteIndex = numSprites - 1;
-	} else if (numSprites >= 5) {
-		spriteIndex = _shipDirectionH;
-		if (spriteIndex >= numSprites)
-			spriteIndex = numSprites - 1;
-	} else if (numSprites == 2) {
-		spriteIndex = _shipFiring ? 1 : 0;
-	}
-
-	// Center sprite at position
-	int spriteW = _shipSprite->getCharWidth(spriteIndex);
-	int spriteH = _shipSprite->getCharHeight(spriteIndex);
-	int drawX = shipScreenX - spriteW / 2 + _viewX;
-	int drawY = shipScreenY - spriteH / 2 + _viewY;
+	// Original uses param_5 & 1 (firing flag) as sprite index — NOT direction-based
+	int spriteIndex = _shipFiring ? 1 : 0;
+	if (spriteIndex >= numSprites)
+		spriteIndex = 0;
+
+	// FUN_004236e0 adds sprite internal offsets to the x/y parameters.
+	// The internal offsets position the sprite correctly for each level type
+	// (centered for Level 2/11 third-person, bottom for Level 12 FPS).
+	int16 spriteXOffset = _shipSprite->getCharXOffset(spriteIndex);
+	int16 spriteYOffset = _shipSprite->getCharYOffset(spriteIndex);
+	int drawX = displayOffsetX + spriteXOffset + _viewX;
+	int drawY = displayOffsetY + spriteYOffset + _viewY;
 
 	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite, spriteIndex);
 
 	// Shadow sprite (POV004 / DAT_0047e028): drawn at same position as primary ship.
-	// Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index
-	// for both primary and shadow, NOT the direction-based spriteIndex.
+	// Original FUN_401CCF lines 91-92 uses param_5 & 1 (firing flag) as sprite index.
 	if (_shipSprite2) {
 		int shadowIndex = _shipFiring ? 1 : 0;
 		if (shadowIndex < _shipSprite2->getNumChars()) {
-			renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _shipSprite2, shadowIndex);
-		}
-	}
-
-	debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) sprite=%d/%d dir=(%d,%d)",
+			int16 shadowXOff = _shipSprite2->getCharXOffset(shadowIndex);
+			int16 shadowYOff = _shipSprite2->getCharYOffset(shadowIndex);
+			int shadowX = displayOffsetX + shadowXOff + _viewX;
+			int shadowY = displayOffsetY + shadowYOff + _viewY;
+			renderNutSprite(renderBitmap, pitch, width, height, shadowX, shadowY, _shipSprite2, shadowIndex);
+		}
+	}
+
+	// Also render ship overlays (POV002 / POV003) if loaded.
+	// These are weapon/character overlays loaded via IACT opcode 8 par4=6,7.
+	if (_shipOverlay1) {
+		int overlayIdx = _shipFiring ? 1 : 0;
+		if (overlayIdx >= _shipOverlay1->getNumChars())
+			overlayIdx = 0;
+		int16 ovlXOff = _shipOverlay1->getCharXOffset(overlayIdx);
+		int16 ovlYOff = _shipOverlay1->getCharYOffset(overlayIdx);
+		int ovlX = displayOffsetX + ovlXOff + _viewX;
+		int ovlY = displayOffsetY + ovlYOff + _viewY;
+		renderNutSprite(renderBitmap, pitch, width, height, ovlX, ovlY, _shipOverlay1, overlayIdx);
+	}
+	if (_shipOverlay2) {
+		int overlayIdx = _shipFiring ? 1 : 0;
+		if (overlayIdx >= _shipOverlay2->getNumChars())
+			overlayIdx = 0;
+		int16 ovlXOff = _shipOverlay2->getCharXOffset(overlayIdx);
+		int16 ovlYOff = _shipOverlay2->getCharYOffset(overlayIdx);
+		int ovlX = displayOffsetX + ovlXOff + _viewX;
+		int ovlY = displayOffsetY + ovlYOff + _viewY;
+		renderNutSprite(renderBitmap, pitch, width, height, ovlX, ovlY, _shipOverlay2, overlayIdx);
+	}
+
+	int sprW = _shipSprite->getCharWidth(spriteIndex);
+	int sprH = _shipSprite->getCharHeight(spriteIndex);
+	debug("Rebel2 Handler8: Ship at (%d,%d) raw(%d,%d) offset(%d,%d) nutOff(%d,%d) size(%d,%d) bottom=%d view(%d,%d) sprite=%d/%d",
 		drawX, drawY, _shipPosX, _shipPosY, displayOffsetX, displayOffsetY,
-		spriteIndex, numSprites, _shipDirectionH, _shipDirectionV);
+		spriteXOffset, spriteYOffset, sprW, sprH, drawY + sprH - _viewY,
+		_viewX, _viewY, spriteIndex, numSprites);
 }
 
 // Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
@@ -2979,9 +3001,10 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 	if (!_grd001Sprite || _grd001Sprite->getNumChars() <= 0)
 		return;
 
-	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
-	const int clipHeight = 180;
-	int renderHeight = MIN(height, clipHeight);
+	// CRITICAL: Clip height to 180 (0xb4) + viewport Y to avoid drawing over status bar.
+	// For oversized buffers (e.g., Level 12's 640x260), the status bar is at
+	// Y = 180 + _viewY in buffer coordinates.
+	int renderHeight = MIN(height, 180 + _viewY);
 
 	// Draw _grd001Sprite based on _grdSpriteMode (DAT_00457900)
 	// Each mode has specific conditions from FUN_0041db5e:
@@ -3015,8 +3038,12 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 		int16 spriteXOffset = _grd001Sprite->getCharXOffset(0);
 		int16 spriteYOffset = _grd001Sprite->getCharYOffset(0);
 
-		int drawX = _rebelViewOffset2X + spriteXOffset;
-		int drawY = _rebelViewOffset2Y + spriteYOffset;
+		// Add viewport offset so sprite follows the visible area.
+		// For 320x200 buffers (Level 2), _viewX/_viewY are 0 — no change.
+		// For oversized buffers (Level 12's 640x260), the viewport scrolls
+		// and sprites must be drawn at the correct position within it.
+		int drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
+		int drawY = _rebelViewOffset2Y + spriteYOffset + _viewY;
 
 		// Apply width-halving logic from original assembly:
 		// When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
@@ -3053,9 +3080,8 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 	if (_rebelHandler != 25)
 		return;
 
-	// CRITICAL: Clip height to 180 (0xb4) to avoid drawing over status bar
-	const int clipHeight = 180;
-	int renderHeight = MIN(height, clipHeight);
+	// CRITICAL: Clip height to 180 (0xb4) + viewport Y to avoid drawing over status bar.
+	int renderHeight = MIN(height, 180 + _viewY);
 
 	// _grd002Sprite (GRD002) is always drawn if it exists (from FUN_41DB5E line 230)
 	// The sprite index is calculated based on damage level and aiming position
@@ -3167,16 +3193,16 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 		if (shouldMirror) {
 			// Mirrored position: X = DAT_00457910 + (320 - sprite_width - sprite_x_offset)
 			// From assembly lines 240-243
-			drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset);
+			drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
 		} else {
 			// Normal position: X = DAT_00457910 + sprite_internal_x_offset
 			// From assembly line 238
-			drawX = _rebelViewOffset2X + spriteXOffset;
+			drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
 		}
 
 		// Y = sprite_internal_y_offset + DAT_00457912
 		// From assembly line 246
-		drawY = spriteYOffset + _rebelViewOffset2Y;
+		drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
 
 		renderNutSpriteMirrored(renderBitmap, pitch, width, renderHeight, drawX, drawY, _grd002Sprite, spriteIdx, shouldMirror);
 


Commit: 972dba7f591dc585cd5077eadb93432d52bb898c
    https://github.com/scummvm/scummvm/commit/972dba7f591dc585cd5077eadb93432d52bb898c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:15+02:00

Commit Message:
SCUMM: RA2: Fix Escape key bug

Changed paths:
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index d541d1aa47d..934346aae24 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -518,7 +518,8 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 		case Common::KEYCODE_ESCAPE:
 			// ESC handling depends on game state:
 			// - In menus: Select quit option and confirm
-			// - During gameplay/cutscenes: Skip video
+			// - During gameplay: Pause and open ScummVM menu
+			// - During cutscenes/intros: Skip video
 			if (splayer) {
 				if (_menuInputActive && (_gameState == kStateMainMenu ||
 				                          _gameState == kStatePilotSelect ||
@@ -529,10 +530,23 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 					_menuSelection = _menuItemCount - 1;  // Select last item (quit/back)
 					_menuSelectionConfirmed = true;
 					debug("Rebel2: ESC pressed in menu - selecting quit (item %d)", _menuSelection);
+					_vm->_smushVideoShouldFinish = true;
+				} else if (_gameState == kStateGameplay && _rebelHandler != 0) {
+					// During active gameplay (handler != 0): pause and open ScummVM menu.
+					// _rebelHandler is non-zero (7, 8, 0x19, 0x26) only during interactive
+					// gameplay sections, and 0 during intro/cutscene/post videos within a level.
+					debug("Rebel2: ESC pressed during gameplay - opening ScummVM menu");
+					bool wasPaused = splayer->_paused;
+					if (!wasPaused)
+						splayer->pause();
+					_vm->openMainMenuDialog();
+					if (!wasPaused)
+						splayer->unpause();
 				} else {
+					// During cutscenes/intros/mission briefings: skip video
 					debug("Rebel2: ESC pressed - skipping video");
+					_vm->_smushVideoShouldFinish = true;
 				}
-				_vm->_smushVideoShouldFinish = true;
 				return true;  // Consume the event
 			}
 			break;


Commit: ce750db5b1126cb6f6b4c136d2cc9bed06401bde
    https://github.com/scummvm/scummvm/commit/ce750db5b1126cb6f6b4c136d2cc9bed06401bde
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:16+02:00

Commit Message:
SCUMM: RA2: Use a large enough bitmap

Changed paths:
    engines/scumm/insane/insane.cpp
    engines/scumm/insane/insane.h
    engines/scumm/insane/insane_rebel.cpp


diff --git a/engines/scumm/insane/insane.cpp b/engines/scumm/insane/insane.cpp
index e4ea4e0bece..8560e42394a 100644
--- a/engines/scumm/insane/insane.cpp
+++ b/engines/scumm/insane/insane.cpp
@@ -184,7 +184,7 @@ void Insane::initvars() {
 		for (j = 0; j < 9; j++)
 			_enHdlVar[i][j] = 0;
 
-	for (i = 0; i < 0x200; i++)
+	for (i = 0; i < 0x401; i++)
 		_iactBits[i] = 0;
 
 
diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index 85647829a36..d8dddfeb305 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -158,7 +158,7 @@ public:
 	int16 _smush_frameNum2;
 	byte _smush_earlyFluContents[0x31a];
 	int16 _enemyState[10][10];
-	byte _iactBits[0x200];
+	byte _iactBits[0x401];
 	int16 _mainRoadPos;
 	int16 _posBrokenCar;
 	int16 _posBrokenTruck;
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 934346aae24..bf3b1c32463 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -252,7 +252,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 		for (j = 0; j < 9; j++)
 			_enHdlVar[i][j] = 0;
 
-	for (i = 0; i < 0x200; i++)
+	for (i = 0; i < 0x401; i++)
 		_iactBits[i] = 0;
 
 	for (i = 0; i < 512; i++) {
@@ -1124,7 +1124,7 @@ bool InsaneRebel2::isBitSet(int n) {
 	if (n < 1) {
 		return false;
 	}
-	assert (n < 0x200);
+	assert (n < 0x401);
 
 	return (_iactBits[n] != 0);
 }
@@ -1133,11 +1133,11 @@ void InsaneRebel2::setBit(int n) {
 	// FUN_004239b0: When n < 1 (i.e., n == 0 or negative), set ALL bits to 1 (disable all objects)
 	// This is used to disable all enemies/objects at once
 	if (n < 1) {
-		for (int i = 0; i < 0x200; i++)
+		for (int i = 0; i < 0x401; i++)
 			_iactBits[i] = 1;
 		return;
 	}
-	assert (n < 0x200);
+	assert (n < 0x401);
 	_iactBits[n] = 1;
 }
 
@@ -1146,11 +1146,11 @@ void InsaneRebel2::clearBit(int n) {
 	// This is called by FUN_00423880 at the start of video playback to reset the bit table,
 	// ensuring all enemies are visible when a new level/segment starts.
 	if (n < 1) {
-		for (int i = 0; i < 0x200; i++)
+		for (int i = 0; i < 0x401; i++)
 			_iactBits[i] = 0;
 		return;
 	}
-	assert (n < 0x200);
+	assert (n < 0x401);
 	_iactBits[n] = 0;
 }
 


Commit: f72c255c6ad4a20a6a10703e8647d6f2bfa9ed96
    https://github.com/scummvm/scummvm/commit/f72c255c6ad4a20a6a10703e8647d6f2bfa9ed96
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:16+02:00

Commit Message:
SCUMM: RA2: Fix crash in smushDecodeSkipRLE

Changed paths:
    engines/scumm/smush/codec_ra2.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/smush/codec_ra2.cpp b/engines/scumm/smush/codec_ra2.cpp
index 7cda7943b59..7b9309f8d51 100644
--- a/engines/scumm/smush/codec_ra2.cpp
+++ b/engines/scumm/smush/codec_ra2.cpp
@@ -94,26 +94,33 @@ void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int wi
  * Format: Each line has 2-byte size, then (skip, runSize, RLE_data) triplets.
  * Note: Skip regions preserve previous frame content (delta compression).
  */
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
 	dst += top * pitch + left;
+	const byte *srcEnd = src + dataSize;
 
 	for (int row = 0; row < height; row++) {
+		if (src + 2 > srcEnd)
+			break;
 		int lineDataSize = READ_LE_UINT16(src);
 		src += 2;
 		const byte *lineEnd = src + lineDataSize;
+		if (lineEnd > srcEnd)
+			lineEnd = srcEnd;
 		byte *lineDst = dst;
 		int x = 0;
 
-		while (src < lineEnd && x < width) {
+		while (src + 2 <= lineEnd && x < width) {
 			int skip = READ_LE_UINT16(src);
 			src += 2;
 			x += skip;  // Skip preserves previous frame content
-			if (src >= lineEnd || x >= width)
+			if (src + 2 > lineEnd || x >= width)
 				break;
 
 			int runSize = READ_LE_UINT16(src);
 			src += 2;
 			const byte *runEnd = src + runSize;
+			if (runEnd > lineEnd)
+				runEnd = lineEnd;
 
 			// Decode RLE within this run - write ALL colors including 0
 			while (src < runEnd && x < width) {
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 4c003c662e8..913efd064a8 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -46,7 +46,7 @@ bool SmushPlayer::isRA2() const {
 
 // Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
 void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 
 /**
@@ -350,7 +350,7 @@ bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
 		smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
 		return true;
 	case SMUSH_CODEC_SKIP_RLE:
-		smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch);
+		smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch, dataSize);
 		return true;
 	case SMUSH_CODEC_RA2_BOMP:
 		smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);


Commit: 0ef613d4c1c42bfde888ce477999615d43b8a8a7
    https://github.com/scummvm/scummvm/commit/0ef613d4c1c42bfde888ce477999615d43b8a8a7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:16+02:00

Commit Message:
SCUMM: RA2: Update death handling and level progression

Changed paths:
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 14624911434..04e50800ee3 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1963,6 +1963,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	
 	_player->setScrollOffset(_viewX, _viewY);
 
+	// Death check: original game (FUN_417E53 line 25) exits video playback
+	// when DAT_0047a7ec >= 0xff (damage accumulator reaches 255).
+	// Sync _playerShield from _playerDamage and break out of video on death.
+	if (_rebelHandler != 0) {
+		_playerShield = 255 - _playerDamage;
+		if (_playerShield <= 0) {
+			_playerShield = 0;
+			_vm->_smushVideoShouldFinish = true;
+		}
+	}
+
 	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
 	// Based on FUN_004089ab:
 	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index ec0e19e74e2..8f09594eaa2 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2725,20 +2725,33 @@ Common::Error ScummEngine::go() {
 					int selectedLevel = rebel->_selectedChapter + 1;
 					debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
 
-					// Run the complete level (handles BEG, gameplay, END/DIE/RETRY/OVER)
-					int result = rebel->runLevel(selectedLevel);
-
-					// Save pilot progress after level completion
-					if (result == InsaneRebel2::kLevelNextLevel) {
-						rebel->updatePilotProgress(selectedLevel - 1,
-							rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
+					// Level progression loop: on success, advance to next level
+					// Original game chains levels directly (e.g. FUN_0040598c(FUN_00418063,0))
+					while (!shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
+						int result = rebel->runLevel(selectedLevel);
+
+						if (result == InsaneRebel2::kLevelNextLevel) {
+							rebel->updatePilotProgress(selectedLevel - 1,
+								rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
+							selectedLevel++;
+							if (selectedLevel > 15) {
+								// Beat the game — play credits
+								rebel->playCreditsSequence();
+								break;
+							}
+						} else {
+							// kLevelGameOver, kLevelQuit, kLevelReturnToMenu — back to menu
+							if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
+								// Propagate quit to outer loop
+								break;
+							}
+							break;
+						}
 					}
 
-					if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
+					if (shouldQuit()) {
 						break;
 					}
-
-					// After level completion or game over, return to menu
 				}
 				// If kChapterSelectBack, loop back to main menu
 			}


Commit: 825c3c8547af7b2e528d9e535813629d7f91123c
    https://github.com/scummvm/scummvm/commit/825c3c8547af7b2e528d9e535813629d7f91123c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:17+02:00

Commit Message:
SCUMM: RA2: Improve sound handling

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_audio.cpp
    engines/scumm/insane/insane_rebel_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index bf3b1c32463..3c2e245b9f3 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -389,6 +389,12 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	}
 	loadSfx();
 
+	// Initialize auxiliary sound buffers (4 × 30000 bytes, loaded from IACT stream)
+	for (i = 0; i < kRA2NumAuxSfx; i++) {
+		_auxSfxData[i] = (byte *)calloc(kRA2AuxBufSize, 1);
+		_auxSfxSize[i] = 0;
+	}
+
 	// Initialize menu system
 	_gameState = kStateMainMenu;  // Start at main menu
 	_menuSelection = 0;           // First item selected
@@ -1089,15 +1095,19 @@ int32 InsaneRebel2::processMouse() {
 					}
 				}
 
-				// Play explosion sound (FUN_0041189e).
+				// Play enemy death sound.
 				// Pan based on enemy center X position: (screenX - 160) mapped to [-127,127]
 				{
 					int enemyCenterX = (it->rect.left + it->rect.right) / 2 - _viewX;
 					int sfxPan = CLIP((enemyCenterX - 160) * 127 / 160, -127, 127);
 					if (_rebelHandler == 8 && it->type >= 1 && it->type <= 4) {
-						playSfx(0, 127, sfxPan);  // BLAST.SAD for handler 8 types 1-4
+						// Handler 8 soldier types 1-4: play from auxiliary buffer 0
+						// Original: FUN_00411931(0, slot+3, 0x7f, pan, 0)
+						playAuxSfx(0, 127, sfxPan);
 					} else {
-						playSfx(2, 127, sfxPan);  // EXPLODE.SAD for all other enemies
+						// All other enemies: EXPLODE.SAD
+						// Original: FUN_0041189e(2, slot+3, 0x7f, pan, 0)
+						playSfx(2, 127, sfxPan);
 					}
 				}
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 43734d7398b..0649f799e66 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -1134,6 +1134,23 @@ public:
 	// slot: 0-7 (SAD file index), volume: 0-127, pan: -127..+127
 	void playSfx(int slot, int volume, int pan);
 
+	// ========== Auxiliary Sound Buffers ==========
+	// 4 pre-allocated buffers (30000 bytes each) loaded from IACT stream data.
+	// Original: DAT_00480308[0..3], loaded via FUN_004118df, played via FUN_00411931.
+	// Used for embedded sound effects (e.g., soldier death sounds in handler 8 levels).
+	static const int kRA2NumAuxSfx = 4;
+	static const int kRA2AuxBufSize = 30000;
+
+	byte *_auxSfxData[kRA2NumAuxSfx];     // Pre-allocated buffer pointers
+	uint32 _auxSfxSize[kRA2NumAuxSfx];    // Current data size in each buffer
+	Audio::SoundHandle _auxSfxHandles[kRA2NumAuxSfx]; // Mixer handles
+
+	// Load sound data into auxiliary buffer (FUN_004118df equivalent)
+	void loadAuxSfx(int buffer, const byte *data, uint32 size);
+
+	// Play from auxiliary buffer (FUN_00411931 equivalent)
+	void playAuxSfx(int buffer, int volume, int pan);
+
 };
 
 } // End of namespace Insane
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel_audio.cpp
index 01f0be50075..7b971f8eea1 100644
--- a/engines/scumm/insane/insane_rebel_audio.cpp
+++ b/engines/scumm/insane/insane_rebel_audio.cpp
@@ -341,6 +341,12 @@ void InsaneRebel2::freeSfx() {
 		_sfxData[i] = nullptr;
 		_sfxSize[i] = 0;
 	}
+	for (int i = 0; i < kRA2NumAuxSfx; i++) {
+		_vm->_mixer->stopHandle(_auxSfxHandles[i]);
+		free(_auxSfxData[i]);
+		_auxSfxData[i] = nullptr;
+		_auxSfxSize[i] = 0;
+	}
 }
 
 void InsaneRebel2::playSfx(int slot, int volume, int pan) {
@@ -371,4 +377,65 @@ void InsaneRebel2::playSfx(int slot, int volume, int pan) {
 	debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
 }
 
+void InsaneRebel2::loadAuxSfx(int buffer, const byte *data, uint32 size) {
+	if (buffer < 0 || buffer >= kRA2NumAuxSfx || !data || size == 0) {
+		return;
+	}
+	if ((int)size > kRA2AuxBufSize) {
+		debug("InsaneRebel2::loadAuxSfx: buffer %d size %d exceeds max %d, truncating",
+			buffer, size, kRA2AuxBufSize);
+		size = kRA2AuxBufSize;
+	}
+
+	memcpy(_auxSfxData[buffer], data, size);
+	_auxSfxSize[buffer] = size;
+
+	debug(5, "InsaneRebel2::loadAuxSfx: buffer=%d size=%d", buffer, size);
+}
+
+void InsaneRebel2::playAuxSfx(int buffer, int volume, int pan) {
+	if (buffer < 0 || buffer >= kRA2NumAuxSfx || !_auxSfxData[buffer] || _auxSfxSize[buffer] == 0) {
+		return;
+	}
+
+	_vm->_mixer->stopHandle(_auxSfxHandles[buffer]);
+
+	// The auxiliary buffer data goes through FUN_00425fc0 (format dispatch) in the original.
+	// Check if data has SAUD header; if so, extract PCM from SDAT chunk.
+	// Otherwise treat as raw 8-bit unsigned PCM at 11025 Hz.
+	const byte *pcmStart = _auxSfxData[buffer];
+	uint32 pcmSize = _auxSfxSize[buffer];
+
+	if (pcmSize > 8 && READ_BE_UINT32(pcmStart) == MKTAG('S', 'A', 'U', 'D')) {
+		// Parse SAUD container to find SDAT chunk
+		uint32 pos = 8; // Skip SAUD tag + size
+		while (pos + 8 <= pcmSize) {
+			uint32 chunkTag = READ_BE_UINT32(pcmStart + pos);
+			uint32 chunkSize = READ_BE_UINT32(pcmStart + pos + 4);
+			if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
+				pcmStart = pcmStart + pos + 8;
+				pcmSize = MIN(chunkSize, pcmSize - pos - 8);
+				break;
+			}
+			pos += 8 + chunkSize;
+		}
+	}
+
+	byte *pcmCopy = (byte *)malloc(pcmSize);
+	if (!pcmCopy) {
+		return;
+	}
+	memcpy(pcmCopy, pcmStart, pcmSize);
+
+	Audio::SeekableAudioStream *stream = Audio::makeRawStream(
+		pcmCopy, pcmSize, 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+
+	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+
+	_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_auxSfxHandles[buffer],
+		stream, -1, scaledVolume, pan);
+
+	debug(5, "InsaneRebel2::playAuxSfx: buffer=%d vol=%d pan=%d pcmSize=%d", buffer, volume, pan, pcmSize);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 885ee5d2a59..f1d3e192218 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -1515,10 +1515,43 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
-	// ===== Sound Loading (par3 21-47) =====
-	if (par3 >= 21 && par3 <= 47) {
-		debug("Rebel2 Opcode 8: Sound loading subcase %d (not implemented)", par3);
-		// TODO: Implement sound loading via FUN_004118df equivalent
+	// ===== Auxiliary Sound Buffer Loading (par4 20-47) =====
+	// FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) → aux buffer 0
+	// FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) → aux buffer 0,
+	//   0x1f-0x25 (31-37) → aux buffer 1, 0x28 (40) → aux buffer 3,
+	//   0x29-0x2f (41-47) → aux buffer 2
+	// Data layout: offset 14 = uint32 data size, offset 18 = PCM data start.
+	// Stream is at offset 8 (after par1-par4), so data size at +6, PCM at +10.
+	if (par4 >= 20 && par4 <= 47) {
+		int auxBuffer = -1;
+		if (par4 >= 20 && par4 <= 27) {
+			auxBuffer = 0;
+		} else if (par4 >= 31 && par4 <= 37) {
+			auxBuffer = 1;
+		} else if (par4 == 40) {
+			auxBuffer = 3;
+		} else if (par4 >= 41 && par4 <= 47) {
+			auxBuffer = 2;
+		}
+
+		if (auxBuffer >= 0 && remaining >= 10) {
+			b.seek(startPos + 6); // Skip to data size field (byte offset 14 from IACT start)
+			uint32 dataSize = b.readUint32LE();
+			if (dataSize > 0 && remaining >= (int64)(10 + dataSize)) {
+				byte *soundData = (byte *)malloc(dataSize);
+				if (soundData) {
+					b.read(soundData, dataSize);
+					loadAuxSfx(auxBuffer, soundData, dataSize);
+					free(soundData);
+					debug("Rebel2 Opcode 8: Loaded %d bytes into aux sound buffer %d (par4=%d)",
+						dataSize, auxBuffer, par4);
+				}
+			} else {
+				debug("Rebel2 Opcode 8: Aux sound par4=%d dataSize=%d exceeds remaining=%lld",
+					par4, dataSize, (long long)remaining);
+			}
+		}
+		b.seek(startPos);
 		return;
 	}
 


Commit: 589580d5b681e77f9dfbce55121c3790231b2280
    https://github.com/scummvm/scummvm/commit/589580d5b681e77f9dfbce55121c3790231b2280
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:17+02:00

Commit Message:
SCUMM: RA2: Fix handler 25 ship rendering

Changed paths:
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 04e50800ee3..ce964bbb20e 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -35,8 +35,9 @@
 
 namespace Scumm {
 
-// External codec functions from codec1.cpp
+// External codec functions from codec1.cpp / codec_ra2.cpp
 extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
 
@@ -2171,9 +2172,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	renderHandler7Ship(renderBitmap, pitch, width, height);
 	renderHandler8Ship(renderBitmap, pitch, width, height);
-	// GRD001 (wall/cockpit) drawn AFTER FOBJs per original FUN_0041DB5E lines 202-210
-	renderHandler25ShipPre(renderBitmap, pitch, width, height);
-	renderHandler25Ship(renderBitmap, pitch, width, height);
 	renderFallbackShip(renderBitmap, pitch, width, height);
 
 	// Enemy indicators and destroyed enemy area erase
@@ -2185,6 +2183,15 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Laser shot beams — drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges
 	renderLaserShots(renderBitmap, pitch, width, height);
 
+	// Handler 25 GRD sprites drawn AFTER enemies/explosions/lasers per original FUN_0041DB5E:
+	//   Line 193: FUN_0041f29a (enemies)
+	//   Line 194: FUN_0041e7c2 (explosions)
+	//   Line 201: FUN_0041f004 (lasers)
+	//   Lines 202-229: GRD001 (wall, opaque, covers enemies behind wall)
+	//   Lines 230-248: GRD002 (character, transparent, drawn last)
+	renderHandler25ShipPre(renderBitmap, pitch, width, height);
+	renderHandler25Ship(renderBitmap, pitch, width, height);
+
 	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
 	// These are cockpit frame, crosshair, and reticle — drawn ON TOP of laser beams
 	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
@@ -3073,6 +3080,11 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 			}
 		}
 
+		// GRD001 uses transparent rendering (color 0 = transparent).
+		// The original uses flags=0 (opaque) in FUN_004236e0, but the NUT pre-decode
+		// already fills color-0 positions with kDefaultTransparentColor (0).
+		// Using transparent rendering here lets the corridor show through any
+		// color-0 border/padding pixels in GRD001, avoiding black-over-corridor artifacts.
 		renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
 
 		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",


Commit: a97db6f7fdba93e50719f5b759d5addd2d28aa69
    https://github.com/scummvm/scummvm/commit/a97db6f7fdba93e50719f5b759d5addd2d28aa69
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:17+02:00

Commit Message:
SCUMM: RA2: Improve shot beam rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3c2e245b9f3..98f1e4c1580 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -370,6 +370,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_grd001Sprite = nullptr;     // DAT_00482240 - GRD001 primary ship
 	_grd002Sprite = nullptr;     // DAT_00482238 - GRD002 secondary ship
 	_grdSpriteMode = 0;          // DAT_00457900 - sprite mode (1,2,3,4)
+	memset(_grdShotOriginX, 0, sizeof(_grdShotOriginX));
+	memset(_grdShotOriginY, 0, sizeof(_grdShotOriginY));
+	_grdShotOriginTableLoaded = false;
 
 	// Initialize Handler 0x26 turret HUD overlay system
 	_hudOverlayNut = nullptr;    // DAT_0047fe78 - Primary HUD overlay (GRD files, animated)
@@ -605,12 +608,12 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 
 // Per-level difficulty parameters extracted from RA2WIN95.EXE at VA 0x47e0f0
 // Original: 2D table indexed by DAT_0047a7fa (difficulty) * 0x242 + DAT_0047a7f8 (levelType) * 0x22
-// 17 fields per entry (34 bytes), 17 entries per difficulty, 5 difficulties
+// 17 fields per entry (34 bytes), 17 entries per difficulty, 6 difficulties
 // Field names from official Difficulty Editor: {laserDelay, snapDistance, missDamage, dodgeDamage,
 //   shotDamage, specialDamage, shotAccuracy, hitPoints, dodgePoints, timePoints,
 //   levelPoints, specialPoints, flags, rollRate, liftRate, slideRate, driftRate}
 // -1 = not applicable for this level type
-const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17] = {
+const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[6][17] = {
 	// Difficulty 0 (Beginner) - 17 level types
 	{
 		{   5,    3,   15,   -1,    2,   -1,   75,   25,   -1,    2,  500,  250,    8,    5,    5,    6,   -1}, // Lv1
@@ -631,7 +634,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
 		{   5,    8,   -1,   -1,    3,   -1,   75,   25,   -1,    2,  500,   -1,    8,   -1,   -1,   -1,   -1}, // Lv15A
 		{   5,    6,  255,   30,    4,   10,   75,   25,   50,    2,  500,  250,    8,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 1 (Easy) - 17 level types
+	// Difficulty 1 (Novice) - 17 level types
 	{
 		{   6,    1,   25,   -1,    3,   -1,   75,   50,   -1,    4, 1000,  500,   16,    6,    6,    7,   -1}, // Lv1
 		{   4,    2,   -1,   -1,    4,   -1,   40,   50,   -1,    0, 1000,  500,   16,  100,  100,  135,   30}, // Lv2
@@ -651,7 +654,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
 		{   5,    7,   -1,   -1,    4,   -1,   75,   50,   -1,    4, 1000,   -1,   16,   -1,   -1,   -1,   -1}, // Lv15A
 		{   5,    6,  255,   35,    4,   10,   75,   50,  100,    4, 1000,  500,   16,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 2 (Medium) - 17 level types
+	// Difficulty 2 (Standard) - 17 level types
 	{
 		{   7,    0,   35,   -1,    5,   -1,   75,   75,   -1,    6, 1500,  750,    0,    7,    7,    8,   -1}, // Lv1
 		{   4,    1,   -1,   -1,    6,   -1,   40,   75,   -1,    0, 1500,  750,    0,  110,  110,  150,   35}, // Lv2
@@ -671,7 +674,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
 		{   5,    4,   -1,   -1,    7,   -1,   75,   75,   -1,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv15A
 		{   5,    5,  255,   40,    7,   10,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 3 (Hard) - 17 level types
+	// Difficulty 3 (Expert) - 17 level types
 	{
 		{   8,    0,   77,   -1,    7,   -1,   80,  100,   -1,    8, 2000, 1000,    4,    8,    8,    9,   -1}, // Lv1
 		{   4,    0,   -1,   -1,    7,   -1,   40,  100,   -1,    0, 2000, 1000,    4,  120,  120,  165,   40}, // Lv2
@@ -691,7 +694,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
 		{   5,    4,   -1,   -1,   10,   -1,   79,  100,   -1,    8, 2000,   -1,    4,   -1,   -1,   -1,   -1}, // Lv15A
 		{   5,    4,  255,   45,    8,    5,   80,  100,  200,    8, 2000, 1000,    4,   -1,   -1,   -1,   -1}, // Lv15B
 	},
-	// Difficulty 4 (Jedi) — identical to difficulty 2 (Medium) in original data
+	// Difficulty 4 (Custom1) — identical to difficulty 2 (Standard) in original data
 	{
 		{   7,    0,   35,   -1,    5,   -1,   75,   75,   -1,    6, 1500,  750,    0,    7,    7,    8,   -1}, // Lv1
 		{   4,    1,   -1,   -1,    6,   -1,   40,   75,   -1,    0, 1500,  750,    0,  110,  110,  150,   35}, // Lv2
@@ -711,11 +714,54 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[5][17]
 		{   5,    4,   -1,   -1,    7,   -1,   75,   75,   -1,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv15A
 		{   5,    5,  255,   40,    7,   10,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv15B
 	},
+	// Difficulty 5 (Custom2) — same as Custom1 except Lv15B roll/lift/slide/drift are 0
+	{
+		{   7,    0,   35,   -1,    5,   -1,   75,   75,   -1,    6, 1500,  750,    0,    7,    7,    8,   -1}, // Lv1
+		{   4,    1,   -1,   -1,    6,   -1,   40,   75,   -1,    0, 1500,  750,    0,  110,  110,  150,   35}, // Lv2
+		{   6,    1,   20,   38,    7,   12,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv3
+		{   5,    1,  100,   35,    6,   20,   75,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv4
+		{   6,    1,   30,   -1,    4,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv5
+		{   6,    1,   -1,   30,    6,   15,   75,   75,  150,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv6A
+		{   6,    1,  200,   50,   12,   -1,   75,   75,  150,    6, 1500,  750,    0,  160,  160,  160,  105}, // Lv6B
+		{  -1,   -1,   -1,   80,   -1,   -1,   -1,   -1,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv7
+		{   5,    1,   27,   -1,    3,   -1,   60,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv8
+		{   5,    3,   19,   60,    7,   -1,   75,   75,  150,    6, 1500,  750,    0,  110,  110,  110,  150}, // Lv9
+		{   5,    9,   -1,   40,  100,   -1,   85,   75,  150,    6, 1500,  750,    0,   11,    7,    8,   -1}, // Lv10
+		{   4,    1,   20,   -1,    6,   -1,   75,   75,   -1,    0, 1500,  750,    0,    6,    7,    8,    9}, // Lv11
+		{   4,    0,   -1,   -1,   11,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv12
+		{   5,    3,   40,   40,    3,   15,   76,   75,  150,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv13
+		{   5,    0,   38,   -1,    4,   -1,   75,   75,   -1,    6, 1500,  750,    0,   -1,   -1,   -1,   -1}, // Lv14
+		{   5,    4,   -1,   -1,    7,   -1,   75,   75,   -1,    6, 1500,   -1,    0,   -1,   -1,   -1,   -1}, // Lv15A
+		{   5,    5,  255,   40,    7,   10,   75,   75,  150,    6, 1500,  750,    0,    0,    0,    0,    0}, // Lv15B
+	},
 };
 
 InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
-	int diff = CLIP(_difficulty, 0, 4);
-	int lvIdx = CLIP((int)_rebelLevelType, 0, 16);
+	int diff = CLIP(_difficulty, 0, 5);
+	int lvIdx = 0;
+
+	// Retail uses DAT_0047a7f8 as the per-segment difficulty table index.
+	// This is NOT the same as handler 0x26's gun "levelType" from opcode 6.
+	//
+	// Index mapping reconstructed from level handlers:
+	//   Lv1->0, Lv2->1, Lv3->2, Lv4->3, Lv5->4,
+	//   Lv6A->5, Lv6B->6, Lv7->7 ... Lv14->14, Lv15A->15, Lv15B->16.
+	// Our Level 6 phase flow sets _currentPhase to 1/2 accordingly.
+	// Level 15 phase switch to 16 is currently approximated by _currentPhase >= 2.
+	if (_selectedLevel <= 0) {
+		// Fallback during non-gameplay contexts before level selection is initialized.
+		lvIdx = CLIP((int)_rebelLevelType, 0, 16);
+	} else if (_selectedLevel <= 5) {
+		lvIdx = _selectedLevel - 1;
+	} else if (_selectedLevel == 6) {
+		lvIdx = (_currentPhase >= 2) ? 6 : 5;
+	} else if (_selectedLevel <= 14) {
+		lvIdx = _selectedLevel;
+	} else { // _selectedLevel == 15
+		lvIdx = (_currentPhase >= 2) ? 16 : 15;
+	}
+
+	lvIdx = CLIP(lvIdx, 0, 16);
 	return kDifficultyTable[diff][lvIdx];
 }
 
@@ -1004,10 +1050,11 @@ int32 InsaneRebel2::processMouse() {
 		}
 	}
 
-	// Left button: Trigger shot on button press (not hold)
-	// From FUN_0040d836 (Handler 7) line 141: shots only spawn when DAT_004437c0 == 2
-	// From FUN_00401CCF (Handler 8) line 82-84: mode 4 disables shooting
-	if (leftPressed && !leftWasPressed && isShootingAllowed()) {
+	// Shot trigger behavior:
+	// - Handler 25 keeps edge-triggered clicks due cover-toggle/sticky input semantics.
+	// - Other gameplay handlers fire while button is held; slot counters still rate-limit.
+	bool triggerShot = (_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed;
+	if (triggerShot && isShootingAllowed()) {
 		Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
 		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
 			mousePos.x, mousePos.y, _enemies.size());
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 0649f799e66..2c30a1efbe5 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -492,6 +492,10 @@ public:
 	// Load Handler 25 GRD NUT sprites from ANIM data (par4 = sprite type: 1,2)
 	bool loadHandler25GrdSprites(byte *animData, int32 size, int16 par4);
 
+	// Parse Handler 25 shot-origin table text from opcode 8 (par4 = 8).
+	// Retail stores values into DAT_004578a6 / DAT_004578c6 (indices 5..19).
+	bool loadHandler25ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining);
+
 	// Load Level 2 background from embedded ANIM
 	bool loadLevel2Background(byte *animData, int32 size, byte *renderBitmap);
 
@@ -544,7 +548,8 @@ public:
 	// For horizontal-dominant beams, reads pixels above/below the line.
 	// For vertical-dominant beams, reads pixels left/right of the line.
 	void drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
-	                           int16 x0, int16 y0, int16 x1, int16 y1);
+	                           int16 x0, int16 y0, int16 x1, int16 y1,
+	                           int16 clipLeft, int16 clipTop, int16 clipRight, int16 clipBottom);
 
 	// Initialize laser texture from NUT sprite (FUN_0040BAB0)
 	void initLaserTexture(NutRenderer *nut, int spriteIdx);
@@ -797,8 +802,8 @@ public:
 		int16 targetX;     // DAT_0044367e[i] - target X position
 		int16 targetY;     // DAT_00443682[i] - target Y position
 		int16 seqNum;      // DAT_0044368a[i] - shot sequence (for alternating)
-		int16 gunX;        // DAT_0045791c[i] - gun barrel X (Handler 25, screen coords)
-		int16 gunY;        // DAT_00457920[i] - gun barrel Y (Handler 25, screen coords)
+		int16 gunX;        // DAT_0045791c[i] - gun barrel X (Handler 25 base coords; render adds view offset)
+		int16 gunY;        // DAT_00457920[i] - gun barrel Y (Handler 25 base coords; render adds view offset)
 	};
 	TurretShot _turretShots[2];
 	int16 _turretShotSeqCounter;  // DAT_0047fe94 - global sequence counter
@@ -1000,6 +1005,13 @@ public:
 	NutRenderer *_grd001Sprite;      // DAT_00482240 - GRD001 primary ship NUT
 	NutRenderer *_grd002Sprite;      // DAT_00482238 - GRD002 secondary ship NUT
 
+	// Handler 25 shot-origin lookup tables from opcode 8/par4=8 text payload.
+	// Indices 5..19 are filled by the retail "%hd %hd ..." parser (FUN_0041CADB case 6).
+	// Uncovered Level 2 firing uses indices 5..14.
+	int16 _grdShotOriginX[30];       // DAT_004578a6 equivalent
+	int16 _grdShotOriginY[30];       // DAT_004578c6 equivalent
+	bool _grdShotOriginTableLoaded;
+
 	// Handler 25 sprite mode (DAT_00457900) - set by opcode 6 par3
 	// Controls which sprite variant to draw:
 	//   1: Draw _grd001Sprite normally
@@ -1032,13 +1044,13 @@ public:
 	NutRenderer *_hudOverlayNut;     // DAT_0047fe78 - Primary HUD overlay (animated)
 	NutRenderer *_hudOverlay2Nut;    // DAT_0047fe80 - Secondary HUD overlay
 
-	/* Difficulty Level (0-5, from pilot menu; clamped to 0-4 for table lookup) */
+	/* Difficulty Level (0-5, from pilot menu; maps directly to table rows) */
 	int _difficulty;
 	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
 
 	// ======================= Per-Level Difficulty Parameters =======================
 	// Extracted from RA2WIN95.EXE at VA 0x47e0f0
-	// 2D table indexed by difficulty (0-4) × level type (0-16)
+	// 2D table indexed by difficulty (0-5) × level type (0-16)
 	// Original indexing: &DAT_0047e0f0 + chapter * 0x242 + levelType * 0x22
 	// Level type (_rebelLevelType) is set by IACT opcode 6 par3
 	// 17 entries: Lv1-5(0-4), Lv6A/6B(5-6), Lv7-14(7-14), Lv15A/15B(15-16)
@@ -1064,9 +1076,10 @@ public:
 		int16 driftRate;       // +0x20: Ship drift rate (flight controls)
 	};
 
-	// Table: 5 difficulty levels × 17 level types
-	// Difficulty 4 (Jedi) is identical to difficulty 2 (Medium) in the original data
-	static const LevelDifficultyParams kDifficultyTable[5][17];
+	// Table: 6 difficulty levels × 17 level types.
+	// Menu labels in GAME.TRS are: Beginner, Novice, Standard, Expert, Custom1, Custom2.
+	// Custom1 is identical to Standard; Custom2 matches Custom1 except Lv15B drift fields.
+	static const LevelDifficultyParams kDifficultyTable[6][17];
 
 	// Look up difficulty parameters for current _difficulty and _rebelLevelType
 	LevelDifficultyParams getDifficultyParams() const;
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index f1d3e192218..416fb281f2b 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -1555,6 +1555,18 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
+	// ===== Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) =====
+	// FUN_0041CADB case 6 pushes 30 short pointers into sscanf with format at 0x482360:
+	//   "%hd %hd  %hd %hd ... %hd %hd" (15 X/Y pairs).
+	// Parsed values are written into DAT_004578a6 / DAT_004578c6 at indices 5..19.
+	if (_rebelHandler == 25 && par4 == 8) {
+		bool loaded = loadHandler25ShotOriginTable(b, startPos, remaining);
+		if (loaded) {
+			b.seek(startPos);
+			return;
+		}
+	}
+
 	// ===== Scan for embedded ANIM data =====
 	// Remaining handlers require finding ANIM tag in the stream
 	debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
@@ -1693,6 +1705,93 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	b.seek(startPos);
 }
 
+bool InsaneRebel2::loadHandler25ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining) {
+	// IACT layout at this point:
+	// - stream is positioned after par1..par4 (8 bytes consumed by caller)
+	// - retail parser reads from offset +18 relative to IACT start -> startPos + 10
+	// - payload size for this opcode family is at offset +14 -> startPos + 6
+	if (remaining < 12)
+		return false;
+
+	int64 savedPos = b.pos();
+	b.seek(startPos + 6);
+	uint32 textSize = b.readUint32LE();
+
+	int64 textPos = startPos + 10;
+	int64 maxAvail = remaining - 10;
+	if (maxAvail <= 0) {
+		b.seek(savedPos);
+		return false;
+	}
+
+	int64 bytesToRead = maxAvail;
+	if (textSize > 0 && (int64)textSize <= maxAvail) {
+		bytesToRead = textSize;
+	}
+
+	char *buf = new char[(size_t)bytesToRead + 1];
+
+	b.seek(textPos);
+	b.read((byte *)buf, bytesToRead);
+	buf[bytesToRead] = '\0';
+
+	// Parse signed 16-bit integers from the ASCII payload.
+	int16 vals[30];
+	int count = 0;
+	const char *p = buf;
+	const char *end = buf + bytesToRead;
+	while (p < end && count < 30) {
+		while (p < end && *p != '-' && *p != '+' && !Common::isDigit(*p))
+			++p;
+		if (p >= end)
+			break;
+
+		int sign = 1;
+		if (*p == '-' || *p == '+') {
+			if (*p == '-')
+				sign = -1;
+			++p;
+		}
+
+		if (p >= end || !Common::isDigit(*p))
+			continue;
+
+		int value = 0;
+		while (p < end && Common::isDigit(*p)) {
+			value = value * 10 + (*p - '0');
+			if (value > 32768)
+				value = 32768; // Keep accumulation bounded before sign/clamp.
+			++p;
+		}
+
+		vals[count++] = (int16)CLIP<int>(sign * value, -32768, 32767);
+	}
+
+	delete[] buf;
+	b.seek(savedPos);
+
+	if (count < 20) {
+		debug("Rebel2 Opcode 8: Handler25 par4=8 parse failed (count=%d, expected up to 30)", count);
+		return false;
+	}
+
+	// Retail mapping (from FUN_41CADB disassembly):
+	// token1->0x4578b0 (X index 5), token2->0x4578d0 (Y index 5), ...
+	// token29->0x4578cc (X index 19), token30->0x4578ec (Y index 19).
+	for (int i = 0; i < 15; ++i) {
+		int pair = i * 2;
+		if (pair + 1 >= count)
+			break;
+		_grdShotOriginX[5 + i] = vals[pair];
+		_grdShotOriginY[5 + i] = vals[pair + 1];
+	}
+	_grdShotOriginTableLoaded = true;
+
+	debug("Rebel2 Opcode 8: Loaded Handler25 shot-origin table (pairs=%d) idx5=(%d,%d) idx14=(%d,%d)",
+		count / 2, _grdShotOriginX[5], _grdShotOriginY[5], _grdShotOriginX[14], _grdShotOriginY[14]);
+	return true;
+}
+
 // ======================= Opcode 8 Helper Functions =======================
 // These helper functions are extracted from the original monolithic iactRebel2Opcode8
 // to improve code readability and match the retail FUN_* function structure.
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index ce964bbb20e..b13e2a76d48 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -146,8 +146,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 			int testLineSize = READ_LE_UINT16(src + testOffset);
 			// A valid first line size should be: > 0, <= width*2
 			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
-				// Validate by summing line sizes
-				int sumTest = 0;
+				// Validate line-size sequence
 				int linesTest = 0;
 				const byte *testPtr = src + testOffset;
 				bool validSum = true;
@@ -158,7 +157,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 						validSum = false;
 						break;
 					}
-					sumTest += ls + 2;
 					testPtr += ls + 2;
 					linesTest++;
 				}
@@ -530,14 +528,14 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
 			// levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
 			playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
 
-			_turretShots[i].counter = getShotMaxDuration();
-			_turretShots[i].seqNum = _turretShotSeqCounter;
-			_turretShotSeqCounter++;
-			_turretShots[i].targetX = x + _viewX;  // DAT_0044366e in original
-			_turretShots[i].targetY = y + _viewY;  // DAT_00443670 in original
-			break;
+				_turretShots[i].counter = getShotMaxDuration();
+				_turretShots[i].seqNum = _turretShotSeqCounter;
+				_turretShotSeqCounter++;
+				_turretShots[i].targetX = x + _viewX;  // DAT_0044366e in original
+				_turretShots[i].targetY = y + _viewY;  // DAT_00443670 in original
+				break;
+			}
 		}
-	}
 }
 
 // Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
@@ -578,10 +576,12 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 			_turretShots[i].targetX = x + _viewX;
 			_turretShots[i].targetY = y + _viewY;
 
-			// Compute gun position from GRD002 character sprite.
-			// Original uses per-direction lookup tables DAT_004578a6/DAT_004578c6.
-			// We approximate from the NUT sprite center + directional offset.
-			if (_grd002Sprite && _grd002Sprite->getNumChars() > 0) {
+			// Compute gun position from retail lookup tables.
+			// Original (FUN_41DB5E) stores:
+			//   DAT_0045791c[i] = gunXTable[spriteIdx] + DAT_00457910 - DAT_0045790c
+			//   DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
+			// where gunXTable/gunYTable are loaded by opcode 8, par4=8.
+			if (_grdShotOriginTableLoaded) {
 				// Compute current sprite index (same logic as renderHandler25Ship)
 				int spriteIdx;
 				if (_rebelDamageLevel == 0) {
@@ -611,34 +611,23 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 				} else {
 					spriteIdx = (_rebelFlightDir == 0) ? (5 - _rebelDamageLevel) : (25 - _rebelDamageLevel);
 				}
-				int numSprites = _grd002Sprite->getNumChars();
 				if (spriteIdx < 0)
 					spriteIdx = 0;
-				if (spriteIdx >= numSprites)
-					spriteIdx = numSprites - 1;
-
-				// Get sprite rendering position (same as in renderHandler25Ship)
-				int16 spriteXOffset = _grd002Sprite->getCharXOffset(spriteIdx);
-				int16 spriteYOffset = _grd002Sprite->getCharYOffset(spriteIdx);
-				int spriteW = _grd002Sprite->getCharWidth(spriteIdx);
-				int spriteH = _grd002Sprite->getCharHeight(spriteIdx);
-				bool shouldMirror = (_rebelFlightDir != 0 && _rebelDamageLevel == 0);
-
-				int drawX;
-				if (shouldMirror) {
-					drawX = _rebelViewOffset2X + (320 - spriteW - spriteXOffset) + _viewX;
-				} else {
-					drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
+				if (spriteIdx >= ARRAYSIZE(_grdShotOriginX))
+					spriteIdx = ARRAYSIZE(_grdShotOriginX) - 1;
+
+				int16 gunXTable = _grdShotOriginX[spriteIdx];
+				int16 gunYTable = _grdShotOriginY[spriteIdx];
+
+				// Mirrored X when DAT_00457902 != 0 in retail.
+				if (_rebelFlightDir != 0) {
+					gunXTable = 320 - gunXTable;
 				}
-				int drawY = spriteYOffset + _rebelViewOffset2Y + _viewY;
 
-				// Gun barrel is approximately at the character's hand level:
-				// X: center of sprite ± directional offset toward the target
-				// Y: about 60% down the sprite height (hand/arm level)
-				_turretShots[i].gunX = drawX + spriteW / 2;
-				_turretShots[i].gunY = drawY + (spriteH * 3) / 5;
+				_turretShots[i].gunX = gunXTable + _rebelViewOffset2X - _rebelViewOffsetX + _viewX;
+				_turretShots[i].gunY = gunYTable + _rebelViewOffset2Y - _rebelViewOffsetY + _viewY;
 			} else {
-				// Fallback: approximate center-bottom of character area
+				// Fallback when table payload (opcode 8/par4=8) was not loaded.
 				_turretShots[i].gunX = _rebelViewOffset2X + 160 + _viewX;
 				_turretShots[i].gunY = _rebelViewOffset2Y + 140 + _viewY;
 			}
@@ -732,131 +721,344 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 }
 
 // Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-void drawTexturedSegment(byte *dst, int pitch, int width, int height, int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8) {
-	// Ported from FUN_00429360 (decompiled). Only 0 in texture is transparent.
-	int sVar4 = 0;                // left
-	int sVar1 = 0;                // top
-	int sVar7 = width - 1;        // right
-	int sVar10 = height - 1;      // bottom
-
-	int px0 = param_3;
-	int py0 = param_4;
-	int px1 = param_5;
-	int py1 = param_6;
-
-	// Clip against screen bounds (translation of original clipping logic)
-	if (px0 == px1) {
-		if (px0 < sVar4 || px0 > sVar7)
+void drawTexturedSegment(byte *dst, int pitch, int width, int height,
+                         int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8,
+                         int clipLeft, int clipTop, int clipRight, int clipBottom) {
+	// Near-direct port of FUN_00429360.
+	// Only color 0 is transparent.
+	int sVar4 = clipLeft;
+	int sVar1 = clipTop;
+	int sVar7 = clipRight;
+	int sVar10 = clipBottom;
+	int p3 = param_3;
+	int p4 = param_4;
+	int p5 = param_5;
+	int p6 = param_6;
+
+	if (p5 == p3) {
+		if (p5 < sVar4 || sVar7 < p5)
 			return;
 	} else {
-		if (px0 < sVar4) {
-			if (px1 < sVar4)
+		if (p3 < sVar4) {
+			if (p5 < sVar4)
 				return;
-			py0 = py1 + ((py0 - py1) * (sVar4 - px1)) / (px0 - px1);
-			px0 = sVar4;
-		} else if (px0 > sVar7) {
-			if (px1 > sVar7)
+			p4 = p6 + (((p4 - p6) * (sVar4 - p5)) / (p3 - p5));
+			p3 = sVar4;
+		} else if (sVar7 < p3) {
+			if (sVar7 < p5)
 				return;
-			py0 = py1 + ((py0 - py1) * (sVar7 - px1)) / (px0 - px1);
-			px0 = sVar7;
+			p4 = p6 + (((p4 - p6) * (sVar7 - p5)) / (p3 - p5));
+			p3 = sVar7;
 		}
-		if (px1 < sVar4) {
-			py1 = py0 + ((py1 - py0) * (sVar4 - px0)) / (px1 - px0);
-			px1 = sVar4;
-		} else if (px1 > sVar7) {
-			py1 = py0 + ((py1 - py0) * (sVar7 - px0)) / (px1 - px0);
-			px1 = sVar7;
+		if (p5 < sVar4) {
+			p6 = p4 + (((p6 - p4) * (sVar4 - p3)) / (p5 - p3));
+			p5 = sVar4;
+		} else if (sVar7 < p5) {
+			p6 = p4 + (((p6 - p4) * (sVar7 - p3)) / (p5 - p3));
+			p5 = sVar7;
 		}
 	}
 
-	if (py0 == py1) {
-		if (py0 < sVar1 || py0 > sVar10)
+	if (p6 == p4) {
+		if (p6 < sVar1 || sVar10 < p6)
 			return;
 	} else {
-		if (py0 < sVar1) {
-			if (py1 < sVar1)
+		if (p4 < sVar1) {
+			if (p6 < sVar1)
 				return;
-			px0 = px1 + ((px0 - px1) * (sVar1 - py1)) / (py0 - py1);
-			py0 = sVar1;
-		} else if (py0 > sVar10) {
-			if (py1 > sVar10)
+			p3 = p5 + (((p3 - p5) * (sVar1 - p6)) / (p4 - p6));
+			p4 = sVar1;
+		} else if (sVar10 < p4) {
+			if (sVar10 < p6)
 				return;
-			px0 = px1 + ((px0 - px1) * (sVar10 - py1)) / (py0 - py1);
-			py0 = sVar10;
+			p3 = p5 + (((p3 - p5) * (sVar10 - p6)) / (p4 - p6));
+			p4 = sVar10;
 		}
-		if (py1 < sVar1) {
-			px1 = px0 + ((px1 - px0) * (sVar1 - py0)) / (py1 - py0);
-			py1 = sVar1;
-		} else if (py1 > sVar10) {
-			px1 = px0 + ((px1 - px0) * (sVar10 - py0)) / (py1 - py0);
-			py1 = sVar10;
+		if (p6 < sVar1) {
+			p5 = (((p5 - p3) * (sVar1 - p4)) / (p6 - p4)) + p3;
+			p6 = sVar1;
+		} else if (sVar10 < p6) {
+			p5 = (((p5 - p3) * (sVar10 - p4)) / (p6 - p4)) + p3;
+			p6 = sVar10;
 		}
 	}
 
-	int dx = px1 - px0;
-	int dy = py1 - py0;
-	int absdx = dx < 0 ? -dx : dx;
-	int absdy = dy < 0 ? -dy : dy;
-
-	// pointer into destination and texture
-	byte *baseDst = dst;
+	int iVar5 = pitch;
+	int sD = p5 - p3;
+	int sE = p6 - p4;
+	byte *pcVar6 = dst + p4 * iVar5 + p3;
 	const byte *texPtr = param_8;
 
-	if (absdx == 0) {
-		if (absdy == 0) {
+	if (sD == 0) {
+		if (sE == 0) {
 			if (*texPtr != 0)
-				baseDst[py0 * pitch + px0] = *texPtr;
+				*pcVar6 = *texPtr;
+			return;
+		} else if (sE < 1) {
+			int iVar9 = -(sE - 1);
+			if (iVar9 <= 0)
+				return;
+			int iVar11 = iVar9;
+			do {
+				iVar11--;
+				if (*texPtr != 0)
+					*pcVar6 = *texPtr;
+				pcVar6 -= iVar5;
+				for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = iVar9 - (sE - 1))
+					texPtr++;
+			} while (0 < iVar11);
+			return;
+		} else {
+			int iVar9 = sE + 1;
+			if (iVar9 <= 0)
+				return;
+			int iVar8 = iVar9;
+			int iVar11 = iVar9;
+			do {
+				iVar11--;
+				if (*texPtr != 0)
+					*pcVar6 = *texPtr;
+				pcVar6 += iVar5;
+				for (iVar8 = iVar8 - param_7; iVar8 < 0; iVar8 += iVar9)
+					texPtr++;
+			} while (0 < iVar11);
 			return;
 		}
-		// vertical-ish
-		int step = absdy + 1;
-		int curY = py0;
-		int signY = dy > 0 ? 1 : -1;
-		int iVar9 = step; // adv counter
-		for (int i = 0; i < step; i++) {
-			if (*texPtr != 0)
-				baseDst[curY * pitch + px0] = *texPtr;
-			curY += signY;
-			iVar9 -= param_7;
-			while (iVar9 < 0) { texPtr++; iVar9 += step; }
+	} else if (sE == 0) {
+		if (sD < 1) {
+			int iVar5h = -(sD - 1);
+			if (iVar5h <= 0)
+				return;
+			int iVar9 = iVar5h;
+			do {
+				iVar9--;
+				if (*texPtr != 0)
+					*pcVar6 = *texPtr;
+				pcVar6 -= 1;
+				for (iVar5h = iVar5h - param_7; iVar5h < 0; iVar5h = iVar5h - (sD - 1))
+					texPtr++;
+			} while (0 < iVar9);
+			return;
+		} else {
+			int iVar5h = sD + 1;
+			if (iVar5h <= 0)
+				return;
+			int iVar11 = iVar5h;
+			int iVar9 = iVar5h;
+			do {
+				iVar9--;
+				if (*texPtr != 0)
+					*pcVar6 = *texPtr;
+				pcVar6 += 1;
+				for (iVar11 = iVar11 - param_7; iVar11 < 0; iVar11 += iVar5h)
+					texPtr++;
+			} while (0 < iVar9);
+			return;
 		}
-		return;
 	}
 
-	if (absdy == 0) {
-		// horizontal-ish
-		int step = absdx + 1;
-		int curX = px0;
-		int signX = dx > 0 ? 1 : -1;
-		int iVar11 = step;
-		for (int i = 0; i < step; i++) {
-			if (*texPtr != 0)
-				baseDst[py0 * pitch + curX] = *texPtr;
-			curX += signX;
-			iVar11 -= param_7;
-			while (iVar11 < 0) { texPtr++; iVar11 += step; }
+	if (sD < 0) {
+		if (-1 < sE) {
+			int iVar11 = sD;
+			int iVar8 = sE;
+			int iVar9 = -iVar11;
+			if (-iVar8 == iVar11 || iVar9 < iVar8) {
+				iVar9 = sE >> 1;
+				if (iVar8 + 1 < 1)
+					return;
+				int iVar3 = iVar8;
+				int iVar12 = iVar8;
+				do {
+					if (*texPtr != 0)
+						*pcVar6 = *texPtr;
+					pcVar6 += iVar5;
+					iVar9 += iVar11;
+					if (iVar9 < 0) {
+						iVar9 += iVar8;
+						pcVar6 -= 1;
+					}
+					for (iVar12 = iVar12 - param_7; iVar12 < 0; iVar12 = iVar8 + 1 + iVar12)
+						texPtr++;
+					bool bVar2 = 0 < iVar3;
+					iVar3--;
+					if (!bVar2)
+						break;
+				} while (true);
+				return;
+			}
+			int iVar12 = iVar9 >> 1;
+			if (-iVar11 + 1 < 1)
+				return;
+			int iVar3 = -iVar11;
+			do {
+				if (*texPtr != 0)
+					*pcVar6 = *texPtr;
+				pcVar6 -= 1;
+				iVar12 -= iVar8;
+				if (iVar12 < 0) {
+					iVar12 -= iVar11;
+					pcVar6 += iVar5;
+				}
+				for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+					texPtr++;
+				bool bVar2 = 0 < iVar3;
+				iVar3--;
+				if (!bVar2)
+					break;
+			} while (true);
+			return;
 		}
+	} else if (-1 < sE) {
+		if (sD <= sE) {
+			int iVar11 = sE;
+			int iVar9 = sE >> 1;
+			if (iVar11 + 1 < 1)
+				return;
+			int iVar8 = iVar11;
+			int iVar12 = iVar11;
+			do {
+				if (*texPtr != 0)
+					*pcVar6 = *texPtr;
+				pcVar6 += iVar5;
+				iVar9 -= sD;
+				if (iVar9 < 0) {
+					iVar9 += iVar11;
+					pcVar6 += 1;
+				}
+				for (iVar8 = iVar8 - param_7; iVar8 < 0; iVar8 = iVar11 + 1 + iVar8)
+					texPtr++;
+				bool bVar2 = 0 < iVar12;
+				iVar12--;
+				if (!bVar2)
+					break;
+			} while (true);
+			return;
+		}
+		int iVar11 = sD;
+		int iVar9 = sD >> 1;
+		if (iVar11 + 1 < 1)
+			return;
+		int iVar8 = iVar11;
+		int iVar12 = iVar11;
+		do {
+			if (*texPtr != 0)
+				*pcVar6 = *texPtr;
+			pcVar6 += 1;
+			iVar9 -= sE;
+			if (iVar9 < 0) {
+				iVar9 += iVar11;
+				pcVar6 += iVar5;
+			}
+			for (iVar8 = iVar8 - param_7; iVar8 < 0; iVar8 = iVar11 + 1 + iVar8)
+				texPtr++;
+			bool bVar2 = 0 < iVar12;
+			iVar12--;
+			if (!bVar2)
+				break;
+		} while (true);
 		return;
 	}
 
-	// general case
-	int steps = (absdx > absdy) ? absdx + 1 : absdy + 1;
-	int x = px0, y = py0;
-	int sx = dx > 0 ? 1 : -1;
-	int sy = dy > 0 ? 1 : -1;
-	int err = absdx - absdy;
-	int iVar12 = steps - 1; // Original starts at majorAxis, not majorAxis+1
-
-	for (int i = 0; i < steps; i++) {
-		if (x >= 0 && x < width && y >= 0 && y < height) {
-			if (*texPtr != 0)
-				baseDst[y * pitch + x] = *texPtr;
+	if (sD < 0) {
+		if (sD < sE) {
+			int iVar11 = sD;
+			int iVar9 = -iVar11;
+			int iVar8 = iVar9 >> 1;
+			if (0 < -iVar11 + 1) {
+				int iVar12 = -iVar11;
+				do {
+					if (*texPtr != 0)
+						*pcVar6 = *texPtr;
+					pcVar6 -= 1;
+					iVar8 += sE;
+					if (iVar8 < 0) {
+						iVar8 -= iVar11;
+						pcVar6 -= iVar5;
+					}
+					for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+						texPtr++;
+					bool bVar2 = 0 < iVar12;
+					iVar12--;
+					if (!bVar2)
+						break;
+				} while (true);
+				return;
+			}
+		} else {
+			int iVar11 = sE;
+			int iVar9 = -iVar11;
+			int iVar8 = iVar9 >> 1;
+			if (0 < -iVar11 + 1) {
+				int iVar12 = -iVar11;
+				do {
+					if (*texPtr != 0)
+						*pcVar6 = *texPtr;
+					pcVar6 -= iVar5;
+					iVar8 += sD;
+					if (iVar8 < 0) {
+						iVar8 -= iVar11;
+						pcVar6 -= 1;
+					}
+					for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+						texPtr++;
+					bool bVar2 = 0 < iVar12;
+					iVar12--;
+					if (!bVar2)
+						break;
+				} while (true);
+				return;
+			}
+		}
+	} else {
+		int iVar11 = sE;
+		int iVar8 = sD;
+		int iVar9 = -iVar11;
+		if (iVar9 < iVar8) {
+			iVar9 = sD >> 1;
+			if (0 < iVar8 + 1) {
+				int iVar3 = iVar8;
+				int iVar12 = iVar8;
+				do {
+					if (*texPtr != 0)
+						*pcVar6 = *texPtr;
+					pcVar6 += 1;
+					iVar9 += iVar11;
+					if (iVar9 < 0) {
+						iVar9 += iVar8;
+						pcVar6 -= iVar5;
+					}
+					for (iVar12 = iVar12 - param_7; iVar12 < 0; iVar12 = iVar8 + 1 + iVar12)
+						texPtr++;
+					bool bVar2 = 0 < iVar3;
+					iVar3--;
+					if (!bVar2)
+						break;
+				} while (true);
+				return;
+			}
+		} else {
+			int iVar12 = iVar9 >> 1;
+			if (0 < -iVar11 + 1) {
+				int iVar3 = -iVar11;
+				do {
+					if (*texPtr != 0)
+						*pcVar6 = *texPtr;
+					pcVar6 -= iVar5;
+					iVar12 -= iVar8;
+					if (iVar12 < 0) {
+						iVar12 -= iVar11;
+						pcVar6 += 1;
+					}
+					for (iVar9 = iVar9 - param_7; iVar9 < 0; iVar9 = (iVar9 - iVar11) + 1)
+						texPtr++;
+					bool bVar2 = 0 < iVar3;
+					iVar3--;
+					if (!bVar2)
+						break;
+				} while (true);
+				return;
+			}
 		}
-		int e2 = 2 * err;
-		if (e2 > -absdy) { err -= absdy; x += sx; }
-		if (e2 < absdx) { err += absdx; y += sy; }
-		iVar12 -= param_7;
-		while (iVar12 < 0) { texPtr++; iVar12 += steps; }
 	}
 }
 
@@ -887,16 +1089,38 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
 	if (!_laserTexture.pixels)
 		return;
 
-	// Render sprite into buffer (FUN_0040BAB0 lines 23-24)
-	// We copy the sprite data directly since it's already in the right format
+	// FUN_0040BAB0 draws the sprite through the normal NUT blitter (FUN_004236e0),
+	// so we must honor x/y offsets and transparency, not just memcpy glyph rows.
 	const byte *srcData = nut->getCharData(spriteIdx);
-	if (srcData) {
-		int srcHeight = nut->getCharHeight(spriteIdx);
-		int copyHeight = MIN(texHeight, (int16)srcHeight);
-		memcpy(_laserTexture.pixels, srcData, texWidth * copyHeight);
+	const int srcWidth = nut->getCharWidth(spriteIdx);
+	const int srcHeight = nut->getCharHeight(spriteIdx);
+	const int srcXOff = nut->getCharXOffset(spriteIdx);
+	const int srcYOff = nut->getCharYOffset(spriteIdx);
+	if (srcData && srcWidth > 0 && srcHeight > 0) {
+		for (int sy = 0; sy < srcHeight; sy++) {
+			int dy = srcYOff + sy;
+			if (dy < 0 || dy >= texHeight)
+				continue;
+
+			const byte *srcRow = srcData + sy * srcWidth;
+			byte *dstRow = _laserTexture.pixels + dy * texWidth;
+			for (int sx = 0; sx < srcWidth; sx++) {
+				int dx = srcXOff + sx;
+				if (dx < 0 || dx >= texWidth)
+					continue;
+
+				byte px = srcRow[sx];
+				// FUN_00429360 (beam raster) only treats 0 as transparent.
+				// Keep 231 pixels from the source texture to avoid dropping beam sub-segments.
+				if (px != 0) {
+					dstRow[dx] = px;
+				}
+			}
+		}
 	}
 
-	debug("Rebel2: Initialized laser texture %dx%d from sprite %d", texWidth, texHeight, spriteIdx);
+	debug("Rebel2: Initialized laser texture %dx%d from sprite %d (xoff=%d yoff=%d src=%dx%d)",
+	      texWidth, texHeight, spriteIdx, srcXOff, srcYOff, srcWidth, srcHeight);
 
 	// Diagnostic: dump texture pixel stats to verify data is loaded correctly
 	if (_laserTexture.pixels && texWidth > 0 && texHeight > 0) {
@@ -992,12 +1216,15 @@ void InsaneRebel2::initEdgeTable(const byte *data) {
 // param_2 = clip rect (or NULL for full buffer)
 // param_3..param_6 = x0, y0, x1, y1 line endpoints
 void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int height,
-                                          int16 x0, int16 y0, int16 x1, int16 y1) {
-	// Clip region (FUN_410962 lines 19-30, simplified for our buffer layout)
-	int16 clipLeft = 1;
-	int16 clipTop = 1;
-	int16 clipRight = width - 2;
-	int16 clipBottom = height - 2;
+                                          int16 x0, int16 y0, int16 x1, int16 y1,
+                                          int16 clipLeftIn, int16 clipTopIn, int16 clipRightIn, int16 clipBottomIn) {
+	// Clip region (FUN_410962 lines 19-30). Clip is provided by caller (gameplay viewport).
+	int16 clipLeft = CLIP<int16>(clipLeftIn, 1, width - 2);
+	int16 clipTop = CLIP<int16>(clipTopIn, 1, height - 2);
+	int16 clipRight = CLIP<int16>(clipRightIn, 1, width - 2);
+	int16 clipBottom = CLIP<int16>(clipBottomIn, 1, height - 2);
+	if (clipLeft > clipRight || clipTop > clipBottom)
+		return;
 
 	// Clip X endpoints (FUN_410962 lines 35-69)
 	if (x0 == x1) {
@@ -1249,9 +1476,13 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	byte *texPixels = _laserTexture.pixels;
 
 	// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
+	// Tuned beam segment spacing: 60% of original.
+	constexpr int kBeamAnimScaleNumerator = 48;   // 16 * 0.6 * 5
+	constexpr int kBeamAnimScaleDenominator = 5;
 	if (maxFrames == 0)
 		maxFrames = 1;
-	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
+	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * kBeamAnimScaleNumerator) /
+	                      ((int)maxFrames * kBeamAnimScaleDenominator));
 
 	// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
 	int16 dx = targetX - gunX;
@@ -1270,6 +1501,19 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	// FUN_0040BBF6 line 30: Get texture pixel pointer
 	byte *local_28 = texPixels;
 
+	// Original callers pass a clip rect for the gameplay viewport (excluding status bar).
+	// This preserves texture phase at the viewport edge and avoids visibly "chopped" beams.
+	int clipLeft = CLIP<int>(_viewX, 0, width - 1);
+	int clipTop = CLIP<int>(_viewY, 0, height - 1);
+	int clipRight = CLIP<int>(_viewX + 319, 0, width - 1);
+	int clipBottom = CLIP<int>(_viewY + 179, 0, height - 1);
+	if (clipLeft > clipRight || clipTop > clipBottom)
+		return;
+	int edgeClipLeft = CLIP<int>(clipLeft + 1, 1, width - 2);
+	int edgeClipTop = CLIP<int>(clipTop + 1, 1, height - 2);
+	int edgeClipRight = CLIP<int>(clipRight - 1, 1, width - 2);
+	int edgeClipBottom = CLIP<int>(clipBottom - 1, 1, height - 2);
+
 	debug(5, "Rebel2: drawLaserBeam gun(%d,%d) tgt(%d,%d) start(%d,%d) end(%d,%d) anim=%d/%d ws=%d hs=%d th=%d",
 		gunX, gunY, targetX, targetY, startX, startY, endX, endY,
 		animFrame, maxFrames, widthScale, heightScale, thickness);
@@ -1293,7 +1537,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 			drawTexturedSegment(dst, pitch, width, height,
 			                    startX, (startY - halfLines) + lineIdx,
 			                    endX, (endY - halfLines) + lineIdx,
-			                    texW, local_28);
+			                    texW, local_28,
+			                    clipLeft, clipTop, clipRight, clipBottom);
 
 			// Advance texture pointer (step through texture rows)
 			for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
@@ -1302,13 +1547,15 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 		}
 
 		// FUN_0040BBF6 lines 47-51: Edge highlights along top and bottom beam edges
-		if (_rebelDetailMode >= 0) {
+		if (_rebelDetailMode >= 0 && edgeClipLeft <= edgeClipRight && edgeClipTop <= edgeClipBottom) {
 			drawEdgeHighlightLine(dst, pitch, width, height,
 			                      startX, startY - halfLines,
-			                      endX, endY - halfLines);
+			                      endX, endY - halfLines,
+			                      edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
 			drawEdgeHighlightLine(dst, pitch, width, height,
 			                      startX, (startY - halfLines) + numLines - 1,
-			                      endX, (endY - halfLines) + numLines - 1);
+			                      endX, (endY - halfLines) + numLines - 1,
+			                      edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
 		}
 	} else {
 		// Mostly vertical beam - draw horizontal scanlines
@@ -1329,7 +1576,8 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 			drawTexturedSegment(dst, pitch, width, height,
 			                    (startX - halfLines) + lineIdx, startY,
 			                    (endX - halfLines) + lineIdx, endY,
-			                    texW, local_28);
+			                    texW, local_28,
+			                    clipLeft, clipTop, clipRight, clipBottom);
 
 			// Advance texture pointer
 			for (local_24 = texH + local_24; local_24 > 0; local_24 -= numLines) {
@@ -1338,15 +1586,18 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 		}
 
 		// FUN_0040BBF6 lines 69-73: Edge highlights along left and right beam edges
-		if (_rebelDetailMode >= 0) {
+		if (_rebelDetailMode >= 0 && edgeClipLeft <= edgeClipRight && edgeClipTop <= edgeClipBottom) {
 			drawEdgeHighlightLine(dst, pitch, width, height,
 			                      startX - halfLines, startY,
-			                      endX - halfLines, endY);
+			                      endX - halfLines, endY,
+			                      edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
 			drawEdgeHighlightLine(dst, pitch, width, height,
 			                      (startX - halfLines) + numLines - 1, startY,
-			                      (endX - halfLines) + numLines - 1, endY);
+			                      (endX - halfLines) + numLines - 1, endY,
+			                      edgeClipLeft, edgeClipTop, edgeClipRight, edgeClipBottom);
 		}
 	}
+
 }
 void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
 	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
@@ -3842,14 +4093,13 @@ void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int
 		pan = CLIP<int16>(pan, -127, 127);
 		// TODO: Apply panning to sound channel i+1
 
-		// Target position (where player clicked)
-		int16 targetX = _turretShots[i].targetX;
-		int16 targetY = _turretShots[i].targetY;
+		// Retail adds DAT_0045790c/0e at render time to both gun and target.
+		int16 targetX = _turretShots[i].targetX + _rebelViewOffsetX;
+		int16 targetY = _turretShots[i].targetY + _rebelViewOffsetY;
 
-		// Gun position computed at spawn time from GRD002 sprite data
-		// Original: DAT_0045791c[i] + DAT_0045790c, DAT_00457920[i] + DAT_0045790e
-		int16 gunX = _turretShots[i].gunX;
-		int16 gunY = _turretShots[i].gunY;
+		// Gun position computed at spawn time in base coords (DAT_0045791c/20).
+		int16 gunX = _turretShots[i].gunX + _rebelViewOffsetX;
+		int16 gunY = _turretShots[i].gunY + _rebelViewOffsetY;
 
 		int16 progress = maxDuration - _turretShots[i].counter;
 


Commit: 0aacf5d751af287602bd6f27d519592402832aa3
    https://github.com/scummvm/scummvm/commit/0aacf5d751af287602bd6f27d519592402832aa3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:18+02:00

Commit Message:
SCUMM: RA2: Improve level selection

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 614cba00a93..10b62b142ef 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -964,8 +964,8 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
 
 	byte *ptr = fobjBuffer;
 	int codec = READ_LE_UINT16(ptr); ptr += 2;
-	int left = READ_LE_UINT16(ptr); ptr += 2;
-	int top = READ_LE_UINT16(ptr); ptr += 2;
+	int left = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
+	int top = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
 	int width = READ_LE_UINT16(ptr); ptr += 2;
 	int height = READ_LE_UINT16(ptr); ptr += 2;
 
@@ -984,8 +984,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	}
 
 	int codec = b.readUint16LE();
-	int left = b.readUint16LE();
-	int top = b.readUint16LE();
+	int left = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
+	int top = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
 	int width = b.readUint16LE();
 	int height = b.readUint16LE();
 
@@ -1005,6 +1005,10 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	assert(chunk_buffer);
 	b.read(chunk_buffer, chunk_size);
 
+	if (isRA2()) {
+		ra2RememberLastFobj(codec, chunk_buffer, chunk_size, left, top, width, height);
+	}
+
 	if (_storeFrame && isRA2()) {
 		ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
 	}
@@ -1018,6 +1022,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
 	uint8 *audioChunk = nullptr;
 	_skipNext = false;
+	if (isRA2())
+		_hasFrameFobjForGost = false;
 
 	if (_insanity) {
 		_vm->_insane->procPreRendering(_dst);
@@ -1087,11 +1093,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleLoad(subSize, b);
 			break;
 		case MKTAG('G','O','S','T'):
-			// GOST = ghost sprite overlay. Re-renders previous FOBJ data at a new
-			// position with priority/transparency flags. Data: 2-byte priority type
-			// (0/1/2 → 0x2000/0x4000/0x6000), 2-byte x, 2-byte y.
-			// TODO: Implement proper ghost rendering by saving previous FOBJ data
-			// and re-rendering it with modified coordinates and priority flags.
+			if (isRA2())
+				ra2HandleGost(subSize, b);
 			break;
 		default:
 			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 7c12fcfa9ac..cc51ccd96f6 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -166,6 +166,17 @@ private:
 	int _storedFobjWidth;
 	int _storedFobjHeight;
 
+	// RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
+	// to re-render the same sprite payload at a different position.
+	byte *_lastFobjData;
+	int32 _lastFobjDataSize;
+	int _lastFobjCodec;
+	int _lastFobjLeft;
+	int _lastFobjTop;
+	int _lastFobjWidth;
+	int _lastFobjHeight;
+	bool _hasFrameFobjForGost;
+
 	Common::String _seekFile;
 	uint32 _startFrame;
 	uint32 _startTime;
@@ -309,6 +320,9 @@ private:
 						int width, int height, int pitch, int dataSize);
 	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
 						  int left, int top, int width, int height);
+	void ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
+							 int left, int top, int width, int height);
+	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
 	void ra2ResetDeltaPalette();
 	SmushFont *ra2GetFont(int font);
 	void ra2ParseNextFrame();
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 913efd064a8..fc2da674632 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -61,6 +61,14 @@ void SmushPlayer::ra2InitFields() {
 	_storedFobjTop = 0;
 	_storedFobjWidth = 0;
 	_storedFobjHeight = 0;
+	_lastFobjData = nullptr;
+	_lastFobjDataSize = 0;
+	_lastFobjCodec = 0;
+	_lastFobjLeft = 0;
+	_lastFobjTop = 0;
+	_lastFobjWidth = 0;
+	_lastFobjHeight = 0;
+	_hasFrameFobjForGost = false;
 	_skipNext = false;
 	_ra2FastForwarding = false;
 	_fobjOffsetX = 0;
@@ -84,6 +92,8 @@ void SmushPlayer::ra2DestroyFields() {
 	_multiFont = nullptr;
 	free(_storedFobjData);
 	_storedFobjData = nullptr;
+	free(_lastFobjData);
+	_lastFobjData = nullptr;
 	free(_loadBuffer);
 	_loadBuffer = nullptr;
 }
@@ -127,6 +137,10 @@ void SmushPlayer::ra2ReleaseVideo() {
 	free(_storedFobjData);
 	_storedFobjData = nullptr;
 	_storedFobjDataSize = 0;
+	free(_lastFobjData);
+	_lastFobjData = nullptr;
+	_lastFobjDataSize = 0;
+	_hasFrameFobjForGost = false;
 	// Preserve _frameBuffer across videos so that gameplay videos (which have no
 	// background FOBJ) can use the stored background from the previous BEG video.
 }
@@ -377,6 +391,82 @@ void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
 	_storeFrame = false;
 }
 
+/**
+ * Cache the most recent frame FOBJ for GOST re-rendering.
+ */
+void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
+									  int left, int top, int width, int height) {
+	if (dataSize <= 0) {
+		_hasFrameFobjForGost = false;
+		return;
+	}
+
+	byte *newData = (byte *)realloc(_lastFobjData, dataSize);
+	if (newData == nullptr) {
+		warning("SmushPlayer::ra2RememberLastFobj: Failed to allocate %d bytes", dataSize);
+		free(_lastFobjData);
+		_lastFobjData = nullptr;
+		_lastFobjDataSize = 0;
+		_hasFrameFobjForGost = false;
+		return;
+	}
+
+	_lastFobjData = newData;
+	memcpy(_lastFobjData, data, dataSize);
+	_lastFobjDataSize = dataSize;
+	_lastFobjCodec = codec;
+	_lastFobjLeft = left;
+	_lastFobjTop = top;
+	_lastFobjWidth = width;
+	_lastFobjHeight = height;
+	_hasFrameFobjForGost = true;
+}
+
+/**
+ * RA2 GOST chunk handler.
+ * Re-renders the most recent frame FOBJ at the supplied ghost position.
+ */
+void SmushPlayer::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 6) {
+		warning("SmushPlayer::ra2HandleGost: chunk too small (%d bytes)", subSize);
+		return;
+	}
+
+	int16 ghostType = b.readSint16LE();
+	int16 ghostX = b.readSint16LE();
+	int16 ghostY = b.readSint16LE();
+
+	if (!_hasFrameFobjForGost || _lastFobjData == nullptr || _lastFobjDataSize <= 0) {
+		debug("SmushPlayer GOST: frame=%d ignored (no current-frame FOBJ cached)", _frame);
+		return;
+	}
+
+	uint16 priorityFlags = 0;
+	if (ghostType == 0) {
+		priorityFlags = 0x2000;
+	} else if (ghostType == 1) {
+		priorityFlags = 0x4000;
+	} else if (ghostType == 2) {
+		priorityFlags = 0x6000;
+	}
+
+	// Match FUN_0042cba0 default behavior (flags bit 0 clear): GOST coordinates
+	// are relative to the cached FOBJ header position.
+	int left = _lastFobjLeft + ghostX;
+	int top = _lastFobjTop + ghostY;
+
+	debug("SmushPlayer GOST: frame=%d type=%d flags=0x%04x gostPos=(%d,%d) basePos=(%d,%d) finalPos=(%d,%d) size=%dx%d codec=%d",
+		_frame, ghostType, priorityFlags, ghostX, ghostY,
+		_lastFobjLeft, _lastFobjTop, left, top,
+		_lastFobjWidth, _lastFobjHeight, _lastFobjCodec);
+
+	// Priority bits (0x2000/0x4000/0x6000) are currently not modeled in
+	// ScummVM's SMUSH decoders. Coordinate-correct re-decode restores expected
+	// RA2 chapter preview behavior.
+	decodeFrameObject(_lastFobjCodec, _lastFobjData, left, top,
+		_lastFobjWidth, _lastFobjHeight, _lastFobjDataSize);
+}
+
 /**
  * Reset XPAL delta palette from the current base palette.
  * Prevents stale delta values from a previous video corrupting the palette.


Commit: 3653d5839f06a0d169782a50c270e3d38c707c78
    https://github.com/scummvm/scummvm/commit/3653d5839f06a0d169782a50c270e3d38c707c78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:18+02:00

Commit Message:
SCUMM: RA2: Fix score font rendering

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 98f1e4c1580..a33ae7f0511 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -799,11 +799,11 @@ void InsaneRebel2::addScore(int points) {
 
 // Render score text to HUD (part of FUN_0041c012)
 // FUN_0041c012 lines 133-137: calls FUN_00434cb0 with format "%07ld"
-// Position (low-res): X = 0x101 (257), Y = 4 within status bar → screen Y = 184
+// using DAT_00482200 (DISPFONT in low-res mode).
 void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY) {
-	(void)statusBarY;
-
-	if (!_smush_dispfontNut)
+	// In low-res mode, score text shares the same NUT as the status bar sprites.
+	NutRenderer *statusFont = _smush_cockpitNut;
+	if (!statusFont)
 		return;
 
 	char scoreStr[16];
@@ -811,9 +811,9 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 
 	// Score position from FUN_0041c012 assembly (low-res mode):
 	//   X = ((DAT_0047a808 < 2) - 1 & 0x101) + 0x101 = 0x101 = 257
-	//   Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar at Y=180)
+	//   Y = ((DAT_0047a808 < 2) - 1 & 4) + 4 = 4 (within status bar)
 	int scoreX = 257 + _viewX;
-	int scoreY = 180 + 4 + _viewY;
+	int scoreY = statusBarY + 4 + _viewY;
 
 	// Render each digit as a NUT sprite (direct pixel blit with color 0 transparency).
 	// This matches the original's FUN_00434cb0 → FUN_004341a0 text rendering which
@@ -825,11 +825,11 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 	int x = scoreX;
 	for (int i = 0; scoreStr[i] != '\0'; i++) {
 		byte ch = (byte)scoreStr[i];
-		if (ch < _smush_dispfontNut->getNumChars()) {
-			int charX = x + _smush_dispfontNut->getCharXOffset(ch);
-			int charY = scoreY + _smush_dispfontNut->getCharYOffset(ch);
-			renderNutSprite(renderBitmap, pitch, width, height, charX, charY, _smush_dispfontNut, ch);
-			x += _smush_dispfontNut->getCharWidth(ch);
+		if (ch < statusFont->getNumChars()) {
+			int charX = x + statusFont->getCharXOffset(ch);
+			int charY = scoreY + statusFont->getCharYOffset(ch);
+			renderNutSprite(renderBitmap, pitch, width, height, charX, charY, statusFont, ch);
+			x += statusFont->getCharWidth(ch);
 		}
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index b13e2a76d48..fcda6c0c086 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2493,7 +2493,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	renderCrosshair(renderBitmap, pitch, width, height);
 
 	// HUD score/lives rendering (FUN_0041c012)
-	renderScoreHUD(renderBitmap, pitch, width, height, 0);
+	renderScoreHUD(renderBitmap, pitch, width, height, statusBarY);
 
 	// Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
 	if (_player) {


Commit: 8d927d5377d03d5acb32f55004ae99912abff036
    https://github.com/scummvm/scummvm/commit/8d927d5377d03d5acb32f55004ae99912abff036
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:18+02:00

Commit Message:
SCUMM: RA2: Fix clipped sprite decoding

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/insane/insane_rebel_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index a33ae7f0511..c66b660d78c 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -465,6 +465,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_deathFrame = 0;
 	_phaseScore = 0;
 	_phaseMisses = 0;
+	_skipSectionRequested = false;
 
 	// Register as EventObserver to capture input events before ScummEngine consumes them
 	_vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
@@ -577,6 +578,19 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			}
 			break;
 
+		case Common::KEYCODE_s:
+			// Debug shortcut: Shift+S skips the current gameplay section.
+			if (splayer &&
+			    _gameState == kStateGameplay &&
+			    _rebelHandler != 0 &&
+			    event.kbd.hasFlags(Common::KBD_SHIFT)) {
+				_skipSectionRequested = true;
+				debug("Rebel2: Shift+S pressed - requesting gameplay section skip");
+				_vm->_smushVideoShouldFinish = true;
+				return true;  // Consume the event
+			}
+			break;
+
 		default:
 			break;
 		}
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 2c30a1efbe5..807ecc0a8b3 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -367,6 +367,7 @@ public:
 	int _deathFrame;          // Frame number where player died (for death video selection)
 	int _phaseScore;          // Accumulated score from previous phases (preserved on phase retry)
 	int _phaseMisses;         // Accumulated misses from previous phases
+	bool _skipSectionRequested; // Debug shortcut (Shift+S): force current gameplay section to end
 
 	// =============================================================
 
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 7bf13aa5c2c..23acf9645da 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -507,6 +507,7 @@ int InsaneRebel2::runLevel(int levelId) {
 	_currentPhase = 1;
 	_phaseScore = 0;
 	_phaseMisses = 0;
+	_skipSectionRequested = false;
 
 	// Dispatch to per-level handler
 	switch (levelId) {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index fcda6c0c086..f32197b8b40 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2126,6 +2126,58 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
 	renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
 }
 
+static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
+		int clipLeft, int clipTop, int clipRight, int clipBottom,
+		int x, int y, NutRenderer *nut, int spriteIdx) {
+	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
+		return;
+
+	if (clipLeft < 0)
+		clipLeft = 0;
+	if (clipTop < 0)
+		clipTop = 0;
+	if (clipRight > pitch)
+		clipRight = pitch;
+	if (clipBottom > dstH)
+		clipBottom = dstH;
+	if (clipLeft >= clipRight || clipTop >= clipBottom)
+		return;
+
+	int w = nut->getCharWidth(spriteIdx);
+	int h = nut->getCharHeight(spriteIdx);
+	const byte *src = nut->getCharData(spriteIdx);
+	if (!src || w <= 0 || h <= 0)
+		return;
+
+	for (int iy = 0; iy < h; ++iy) {
+		int dstY = y + iy;
+		if (dstY < clipTop || dstY >= clipBottom)
+			continue;
+
+		const byte *s = src + iy * w;
+		byte *d = dst + dstY * pitch;
+
+		int dstStart = x;
+		int dstEnd = x + w;
+		int srcStart = 0;
+		if (dstStart < clipLeft) {
+			srcStart = clipLeft - dstStart;
+			dstStart = clipLeft;
+		}
+		if (dstEnd > clipRight)
+			dstEnd = clipRight;
+		if (dstStart >= dstEnd)
+			continue;
+
+		int copyCount = dstEnd - dstStart;
+		for (int ix = 0; ix < copyCount; ++ix) {
+			byte px = s[srcStart + ix];
+			if (px != 0)
+				d[dstStart + ix] = px;
+		}
+	}
+}
+
 // Render NUT sprite with optional horizontal mirroring
 // Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
 void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
@@ -3314,33 +3366,26 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 		int drawX = _rebelViewOffset2X + spriteXOffset + _viewX;
 		int drawY = _rebelViewOffset2Y + spriteYOffset + _viewY;
 
-		// Apply width-halving logic from original assembly:
-		// When damage==0 (uncovered), the original halves DAT_00482234 (buffer width)
-		// This clips the sprite to only half the screen.
-		int renderWidth = width;
-		byte *dstBitmap = renderBitmap;
-
+		// Apply half-width clipping from FUN_41DB5E:
+		// - mode1 uncovered: left half
+		// - mode4 uncovered: right half
+		int clipLeft = 0;
+		int clipRight = width;
 		if (useHalfWidth) {
-			renderWidth = width / 2;  // Clip to half width (160 pixels)
-
-			if (useRightHalf) {
-				// Mode 4: Draw to right half by offsetting the destination buffer
-				// Original: DAT_00482230 += DAT_00482234 (adds 160 to buffer start)
-				// This makes drawing appear on the right half (pixels 160-319)
-				dstBitmap = renderBitmap + (width / 2);
-			}
+			const int halfWidth = width / 2;
+			clipLeft = useRightHalf ? halfWidth : 0;
+			clipRight = clipLeft + halfWidth;
 		}
 
-		// GRD001 uses transparent rendering (color 0 = transparent).
-		// The original uses flags=0 (opaque) in FUN_004236e0, but the NUT pre-decode
-		// already fills color-0 positions with kDefaultTransparentColor (0).
-		// Using transparent rendering here lets the corridor show through any
-		// color-0 border/padding pixels in GRD001, avoiding black-over-corridor artifacts.
-		renderNutSprite(dstBitmap, pitch, renderWidth, renderHeight, drawX, drawY, _grd001Sprite, 0);
+		// FUN_41DB5E mode-4 uncovered mutates DAT_00482230/34 (clip region),
+		// not DAT_00457910 (draw X). Keep drawX unchanged and clip only.
+		renderNutSpriteClipped(renderBitmap, pitch, renderHeight,
+			clipLeft, 0, clipRight, renderHeight,
+			drawX, drawY, _grd001Sprite, 0);
 
-		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d renderW=%d",
+		debug("Rebel2 Handler25 PRE: GRD001 at (%d,%d) nutOff(%d,%d) viewOff(%d,%d) size(%d,%d) mode=%d dmg=%d halfW=%d rightHalf=%d clip=[%d,%d)",
 			drawX, drawY, spriteXOffset, spriteYOffset, _rebelViewOffset2X, _rebelViewOffset2Y,
-			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, renderWidth);
+			spriteW, spriteH, _grdSpriteMode, _rebelDamageLevel, useHalfWidth ? 1 : 0, useRightHalf ? 1 : 0, clipLeft, clipRight);
 	}
 }
 
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 3a729f4deca..8ee4482d399 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -110,6 +110,16 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 
 	uint16 result = 0;
 
+	// Debug shortcut path: force-end current section when requested via Shift+S.
+	// This returns the same sentinel (0xFFFF) used for section completion/death/quit.
+	if (_skipSectionRequested) {
+		_skipSectionRequested = false;
+		_rebelPhaseState = mask;
+		_rebelWaveState = mask;
+		debug("Rebel2 processWaveEnd: Shift+S skip consumed (mask=0x%x)", (uint16)mask);
+		return 0xFFFF;
+	}
+
 	// Step 1: Wait for video to finish (lines 21-32)
 	// Original loop: while (damage < 0xff && frame < maxFrame-1 && !escPressed)
 	// The SmushPlayer::play() call already blocks until video ends, so this step


Commit: a988ea1aae675b8f9bbd9d775e7a4ce1c739c1de
    https://github.com/scummvm/scummvm/commit/a988ea1aae675b8f9bbd9d775e7a4ce1c739c1de
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00

Commit Message:
SCUMM: RA2: Improve collisions and shadow indicators

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index c66b660d78c..7c5c829c344 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -349,9 +349,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	// Initialize Handler 7 FLY ship system
 	_flyShipSprite = nullptr;    // FLY001 - 35 direction frames
-	_flyLaserSprite = nullptr;   // FLY002 - laser sprites
+	_flyLaserSprite = nullptr;   // FLY002 - effect sprites (danger/overlay cues)
 	_flyTargetSprite = nullptr;  // FLY003 - targeting overlay
 	_flyHiResSprite = nullptr;   // FLY004 - high-res alternative
+	_flyEffectAnimCounter = 0;   // DAT_0047ff1c
+	_flyOverlayRepeatCount = 0;  // DAT_00443b52
 	_flyShipScreenX = 0xd4;      // Start at center (212) - matches DAT_00443708 default
 	_flyShipScreenY = 0x82;      // Start at center (130) - matches DAT_0044370a default
 	_smoothedVelocity = 0;       // DAT_0044370c
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 807ecc0a8b3..3a0d47784a3 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -722,7 +722,8 @@ public:
 	// Mode 0/2: Obstacle collision using secondary zones — inside quad = hit
 	// Mode 1/3: Wall/boundary collision using primary zones — per-edge push-back
 	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords
-	void checkHandler7CollisionZones();
+	// and draws proximity shadow cues for nearby danger zones.
+	void checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
 
 	int16 _playerDamage;  // Legacy damage counter (kept for compatibility/telemetry)
 	int16 _playerShield;  // Shields: 0..255 where 255 = full
@@ -959,16 +960,18 @@ public:
 	//
 	// Based on FUN_0040c3cc and FUN_0040d836 disassembly:
 	// - DAT_0047fee8: Ship direction sprites (FLY001, par3=1, 35 frames)
-	// - DAT_0047fef0: Laser fire sprites (FLY002, par3=3)
+	// - DAT_0047fef0: Ship effect sprites (FLY002, par3=3)
 	// - DAT_0047fef8: Targeting overlay (FLY003, par3=2)
 	// - DAT_0047ff00: High-res alternative (FLY004, par3=11)
 	// - DAT_00443708: Ship X position, DAT_0044370a: Ship Y position
 	// - DAT_0044370c: Smoothed horizontal velocity, DAT_0044370e: Vertical input
 
 	NutRenderer *_flyShipSprite;     // DAT_0047fee8 - FLY001 (35 direction frames)
-	NutRenderer *_flyLaserSprite;    // DAT_0047fef0 - FLY002
+	NutRenderer *_flyLaserSprite;    // DAT_0047fef0 - FLY002 (danger/overlay effects)
 	NutRenderer *_flyTargetSprite;   // DAT_0047fef8 - FLY003
 	NutRenderer *_flyHiResSprite;    // DAT_0047ff00 - FLY004
+	int16 _flyEffectAnimCounter;     // DAT_0047ff1c - animated FLY002 cue counter
+	int16 _flyOverlayRepeatCount;    // DAT_00443b52 - repeats for ship overlay effect
 
 	// Handler 7 ship state (FUN_40C3CC / FUN_0040d836)
 	// Position in game coordinate space [20,404]x[20,240], center=(212,130)
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 416fb281f2b..1ecd8052fa1 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -265,8 +265,9 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 				body0, body1, _corridorRightX);
 			break;
 		case 5:
-			// Flag value
-			debug("Rebel2 Opcode 7 par4=5: flag=%d", body0);
+			// DAT_00443b52: repeats FLY002 ship overlay in FUN_40D836.
+			_flyOverlayRepeatCount = body0;
+			debug("Rebel2 Opcode 7 par4=5: flyOverlayRepeat=%d", _flyOverlayRepeatCount);
 			break;
 		default:
 			debug("Rebel2 Opcode 7 par4=%d: body=(%d,%d) — unknown sub-opcode", par4, body0, body1);
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index f32197b8b40..6bf314d3f40 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1846,7 +1846,7 @@ void InsaneRebel2::checkCollisionZones() {
 	}
 }
 
-void InsaneRebel2::checkHandler7CollisionZones() {
+void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
 	// FUN_40E35E — Handler 7 per-frame collision system.
 	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
 	// Two modes depending on _flyControlMode:
@@ -1855,6 +1855,10 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 
 	// Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
 	// to match the original where the decrement happens during rendering.
+	//
+	// local_c in FUN_40E35E: proximity mask for nearby danger-zone shadow cues.
+	// bit 0=left, bit 1=right, bit 2=top, bit 3=bottom
+	uint16 warningMask = 0;
 
 	if (_flyControlMode == 0 || _flyControlMode == 2) {
 		// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
@@ -1931,10 +1935,20 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 					}
 				}
 			}
+
+			// FUN_40E35E line 104: mark near-danger proximity for shadow cue rendering.
+			// Uses the low byte of zone.filterValue (retail local_1c) to pick direction bits.
+			if (zone.field2 - 13 < zone.field1) {
+				uint32 bit = 4u << ((byte)zone.filterValue & 0x1f);
+				warningMask = (uint16)(warningMask | (uint16)bit);
+			}
 		}
 
-		// Corridor boundary proximity (lines 127-131)
-		// These flags are used for directional indicators (not critical for damage)
+		// Corridor side proximity (FUN_40E35E lines 127-131)
+		if (_flyShipScreenX < _corridorLeftX + 0x28)
+			warningMask |= 1;
+		if (_corridorRightX - 0x28 < _flyShipScreenX)
+			warningMask |= 2;
 
 	} else {
 		// ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
@@ -1967,13 +1981,15 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
-						playSfx(1, 127, 0);  // CRASH.SAD, top wall → center pan
 						debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
 					}
 					_spaceShotDirection = 2;  // Direction: pushed down
 					_flyShipScreenY = edgeY;  // Push-back
+					playSfx(1, 127, 0);  // CRASH.SAD, top wall → center pan (always)
 					initDamageFlash();
+				} else if (_flyShipScreenY < edgeY + 0x28) {
+					warningMask |= 4;
 				}
 			}
 
@@ -1990,13 +2006,15 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
-						playSfx(1, 127, 0);  // CRASH.SAD, bottom wall → center pan
 						debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
 					}
 					_spaceShotDirection = 3;  // Direction: pushed up
 					_flyShipScreenY = edgeY;  // Push-back
+					playSfx(1, 127, 0);  // CRASH.SAD, bottom wall → center pan (always)
 					initDamageFlash();
+				} else if (edgeY - 0x28 < _flyShipScreenY) {
+					warningMask |= 8;
 				}
 			}
 
@@ -2006,6 +2024,12 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 				if (_flyShipScreenX < edgeX) {
 					// Ship left of left wall — push right
 					_flyShipScreenX = edgeX;  // Push-back
+
+					// FUN_40E35E resets horizontal history to force immediate rightward correction.
+					for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
+						_velocityHistory[j] = 127;
+					}
+
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
 						int damage = wallDamage;
 						_playerDamage += damage;
@@ -2013,11 +2037,11 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
-						playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
 						debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
 					}
 					_spaceShotDirection = 0;  // Direction: pushed right
+					playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left (always)
 					initDamageFlash();
 				}
 			}
@@ -2028,6 +2052,12 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 				if (edgeX < _flyShipScreenX) {
 					// Ship right of right wall — push left
 					_flyShipScreenX = edgeX;  // Push-back
+
+					// FUN_40E35E resets horizontal history to force immediate leftward correction.
+					for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
+						_velocityHistory[j] = -127;
+					}
+
 					if (_hitCooldown < 5 && !_rebelInvulnerable) {
 						int damage = wallDamage;
 						_playerDamage += damage;
@@ -2035,16 +2065,40 @@ void InsaneRebel2::checkHandler7CollisionZones() {
 							_playerDamage = 255;
 						_rebelHitCounter++;
 						_hitCooldown = 10;
-						playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
 						debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
 							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
 					}
 					_spaceShotDirection = 1;  // Direction: pushed left
+					playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right (always)
 					initDamageFlash();
 				}
 			}
 		}
 	}
+
+	// FUN_40E35E tail: draw proximity danger shadow cues when enabled by frame/flags.
+	// Note: These are cue sprites (often perceived as "shadows"), not the aiming reticle.
+	LevelDifficultyParams dparams = getDifficultyParams();
+	if ((curFrame & 2) != 0 && (dparams.flags & 8) != 0 && _smush_iconsNut) {
+		int scale = (_vm->_screenWidth > 320 || _vm->_screenHeight > 200) ? 2 : 1;
+
+		if ((warningMask & 1) != 0 && _smush_iconsNut->getNumChars() > 0x2d) {
+			renderNutSprite(renderBitmap, pitch, width, height,
+				0xd7 * scale + _viewX, 0x55 * scale + _viewY, _smush_iconsNut, 0x2d);
+		}
+		if ((warningMask & 2) != 0 && _smush_iconsNut->getNumChars() > 0x2c) {
+			renderNutSprite(renderBitmap, pitch, width, height,
+				0x69 * scale + _viewX, 0x55 * scale + _viewY, _smush_iconsNut, 0x2c);
+		}
+		if ((warningMask & 4) != 0 && _smush_iconsNut->getNumChars() > 0x2b) {
+			renderNutSprite(renderBitmap, pitch, width, height,
+				0xa0 * scale + _viewX, 0x82 * scale + _viewY, _smush_iconsNut, 0x2b);
+		}
+		if ((warningMask & 8) != 0 && _smush_iconsNut->getNumChars() > 0x2a) {
+			renderNutSprite(renderBitmap, pitch, width, height,
+				0xa0 * scale + _viewX, 0x28 * scale + _viewY, _smush_iconsNut, 0x2a);
+		}
+	}
 }
 
 void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
@@ -2533,7 +2587,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (_rebelHandler == 0x26) {
 		checkCollisionZones();
 	} else if (_rebelHandler == 7) {
-		checkHandler7CollisionZones();
+		checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
 	}
 
 	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
@@ -3187,44 +3241,84 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 	if (spriteIndex >= numSprites)
 		spriteIndex = numSprites - 1;
 
-	// Transform game coordinates to screen coordinates (FUN_0041c720 equivalent)
-	// The perspective transform shifts the ship position based on perspective offsets.
-	// Close view: FOBJ offset = (-52 - perspX, -45 - perspY), ship at screen center.
-	// For now, use a simplified perspective: ship position = center + offset from center
-	// scaled by perspective. In the original, FUN_00424510 shifts all FOBJ sprites.
-	//
-	// Screen position for sprite drawing (FUN_0040d836 line 174):
-	//   drawX = transformedX - 0xd4, drawY = transformedY - 0x82
-	// Where transformedX/Y come from FUN_0041c720(shipX, shipY, perspX, perspY, viewShift)
-	//
-	// Simplified: screenX = 160 + (shipX - 212) * perspFactor
-	// With the perspective formula, objects near center barely move, objects at edges move more.
-	int drawX = (_flyShipScreenX - 0xd4) + _perspectiveX;
-	int drawY = (_flyShipScreenY - 0x82) + _perspectiveY;
+	// Simplified FUN_41C720-like transform to current render buffer coordinates.
+	int shipCenterX = (_flyShipScreenX - 0xd4) + _perspectiveX + 160 + _viewX;
+	int shipCenterY = (_flyShipScreenY - 0x82) + _perspectiveY + 100 + _viewY;
+
+	// FUN_40D836 lines 108-136: FLY002 proximity cues near corridor danger.
+	if (_flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
+		const int laserChars = _flyLaserSprite->getNumChars();
+		_flyEffectAnimCounter++;
+
+		if (_flyControlMode == 0) {
+			int16 leftDist = _flyShipScreenX - _corridorLeftX;
+			if (leftDist < 0x32) {
+				int cueIndex = _flyEffectAnimCounter % 10;
+				if (cueIndex >= 0 && cueIndex < laserChars) {
+					int cueX = ((shipCenterX - 0x28) - leftDist) - leftDist / 2;
+					renderNutSprite(renderBitmap, pitch, width, height, cueX, shipCenterY, _flyLaserSprite, cueIndex);
+				}
+			}
+
+			int16 rightDist = _corridorRightX - _flyShipScreenX;
+			if (rightDist < 0x32) {
+				int cueIndex = (_flyEffectAnimCounter % 10) + 10;
+				if (cueIndex >= 0 && cueIndex < laserChars) {
+					int cueX = rightDist / 2 + rightDist + shipCenterX + 0x28;
+					renderNutSprite(renderBitmap, pitch, width, height, cueX, shipCenterY, _flyLaserSprite, cueIndex);
+				}
+			}
+		} else {
+			int16 bottomDist = _corridorBottomY - _flyShipScreenY;
+			int bottomX = shipCenterX;
+			int bottomY = (_corridorBottomY - 0x82) + _perspectiveY + 100 + _viewY;
+
+			if (bottomDist < 0x19) {
+				_flyEffectAnimCounter++;
+				int cueIndex = _flyEffectAnimCounter % 10;
+				if (cueIndex >= 0 && cueIndex < laserChars)
+					renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+			}
+			if (bottomDist < 0x32) {
+				_flyEffectAnimCounter++;
+				int cueIndex = _flyEffectAnimCounter % 10;
+				if (cueIndex >= 0 && cueIndex < laserChars)
+					renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+			}
 
-	// Convert from game-center-relative to screen coordinates
-	// The sprite system expects coordinates relative to the 320x200 frame
-	// Center of frame = (160, 100), so offset = game position - game center
-	drawX += 160 + _viewX;
-	drawY += 100 + _viewY;
+			int cueIndex = _flyEffectAnimCounter % 10;
+			if (cueIndex >= 0 && cueIndex < laserChars)
+				renderNutSprite(renderBitmap, pitch, width, height, bottomX, bottomY, _flyLaserSprite, cueIndex);
+		}
+	}
 
 	// Center the sprite on the position
 	int spriteW = _flyShipSprite->getCharWidth(spriteIndex);
 	int spriteH = _flyShipSprite->getCharHeight(spriteIndex);
-	drawX -= spriteW / 2;
-	drawY -= spriteH / 2;
+	int drawX = shipCenterX - spriteW / 2;
+	int drawY = shipCenterY - spriteH / 2;
 
 	renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyShipSprite, spriteIndex);
 
-	// Laser overlay if firing (same position as ship)
-	if (_shipFiring && _flyLaserSprite && _flyLaserSprite->getNumChars() > 0) {
-		int laserIndex = spriteIndex % _flyLaserSprite->getNumChars();
-		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, laserIndex);
+	// FUN_40D836 lines 176-180: optional FLY002 overlay pass at ship position.
+	if (_flyLaserSprite && _flyOverlayRepeatCount > 0) {
+		int overlayIndex = spriteIndex + 0x14;
+		if (overlayIndex >= 0 && overlayIndex < _flyLaserSprite->getNumChars()) {
+			for (int i = 0; i < _flyOverlayRepeatCount; i++) {
+				renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyLaserSprite, overlayIndex);
+			}
+		}
+	}
+
+	// FUN_40D836 lines 181-183: optional FLY003 overlay in high detail mode.
+	if (_flyTargetSprite && _rebelDetailMode >= 0 &&
+		spriteIndex >= 0 && spriteIndex < _flyTargetSprite->getNumChars()) {
+		renderNutSprite(renderBitmap, pitch, width, height, drawX, drawY, _flyTargetSprite, spriteIndex);
 	}
 
-	debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d",
+	debug("Rebel2 Handler7Ship: draw=(%d,%d) sprite=%d/%d shipPos=(%d,%d) persp=(%d,%d) smoothVel=%d vertIn=%d fxCtr=%d fxRep=%d",
 		drawX, drawY, spriteIndex, numSprites, _flyShipScreenX, _flyShipScreenY,
-		_perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput);
+		_perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput, _flyEffectAnimCounter, _flyOverlayRepeatCount);
 }
 
 void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {


Commit: 8c4dfa645aac621f374efa32fd3b5fd6d1393da9
    https://github.com/scummvm/scummvm/commit/8c4dfa645aac621f374efa32fd3b5fd6d1393da9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00

Commit Message:
SCUMM: RA2: Remove debug code

Changed paths:
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 3a0d47784a3..a37d3347fce 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -699,14 +699,6 @@ public:
 	// Hit cooldown timer (DAT_0044374c) - prevents rapid damage stacking
 	int16 _hitCooldown;
 
-	// Draw collision zone quadrilaterals for visualization/debugging
-	// Called from procPostRendering when collision zones should be visible
-	void drawCollisionZones(byte *dst, int pitch, int width, int height, byte color);
-
-	// Draw a single quadrilateral (4 edges)
-	void drawQuad(byte *dst, int pitch, int width, int height, 
-	              int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color);
-
 	// Register a collision zone from IACT opcode 5 data
 	void registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4);
 
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 6bf314d3f40..a62c1405bbe 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2101,81 +2101,6 @@ void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, in
 	}
 }
 
-void InsaneRebel2::drawQuad(byte *dst, int pitch, int width, int height,
-                            int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, byte color) {
-	// Draw a quadrilateral by connecting its 4 vertices with lines
-	// Vertex order: top-left (1), top-right (2), bottom-right (3), bottom-left (4)
-	drawLine(dst, pitch, width, height, x1, y1, x2, y2, color);  // Top edge
-	drawLine(dst, pitch, width, height, x2, y2, x3, y3, color);  // Right edge
-	drawLine(dst, pitch, width, height, x3, y3, x4, y4, color);  // Bottom edge
-	drawLine(dst, pitch, width, height, x4, y4, x1, y1, color);  // Left edge
-}
-
-void InsaneRebel2::drawCollisionZones(byte *dst, int pitch, int width, int height, byte color) {
-	// Draw all active collision zones as wireframe quadrilaterals for debugging
-	// Uses different colors for primary vs secondary zones
-
-	const byte primaryColor = 44;    // Bright red for primary (obstacle) zones
-	const byte secondaryColor = 47;  // Yellow for secondary (boundary) zones
-
-	// Draw primary zones (sub-opcode 0x0D - obstacles)
-	for (int i = 0; i < _primaryZoneCount; i++) {
-		CollisionZone &zone = _primaryZones[i];
-		if (!zone.active)
-			continue;
-
-		// Apply view offset to convert from video coords to screen coords
-		int x1 = zone.x1 + _viewX;
-		int y1 = zone.y1 + _viewY;
-		int x2 = zone.x2 + _viewX;
-		int y2 = zone.y2 + _viewY;
-		int x3 = zone.x3 + _viewX;
-		int y3 = zone.y3 + _viewY;
-		int x4 = zone.x4 + _viewX;
-		int y4 = zone.y4 + _viewY;
-
-		drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, primaryColor);
-	}
-
-	// Draw secondary zones (sub-opcode 0x0E - boundaries)
-	for (int i = 0; i < _secondaryZoneCount; i++) {
-		CollisionZone &zone = _secondaryZones[i];
-		if (!zone.active)
-			continue;
-
-		// Apply view offset
-		int x1 = zone.x1 + _viewX;
-		int y1 = zone.y1 + _viewY;
-		int x2 = zone.x2 + _viewX;
-		int y2 = zone.y2 + _viewY;
-		int x3 = zone.x3 + _viewX;
-		int y3 = zone.y3 + _viewY;
-		int x4 = zone.x4 + _viewX;
-		int y4 = zone.y4 + _viewY;
-
-		drawQuad(dst, pitch, width, height, x1, y1, x2, y2, x3, y3, x4, y4, secondaryColor);
-	}
-
-	// Draw corridor boundaries as a rectangle (from IACT opcode 7)
-	if (_corridorLeftX != 0 || _corridorRightX != 0x1A8) {
-		const byte corridorColor = 45;  // Cyan for corridor boundaries
-		// Draw vertical lines for left/right boundaries
-		drawLine(dst, pitch, width, height,
-			_corridorLeftX + _viewX, _corridorTopY + _viewY,
-			_corridorLeftX + _viewX, _corridorBottomY + _viewY, corridorColor);
-		drawLine(dst, pitch, width, height,
-			_corridorRightX + _viewX, _corridorTopY + _viewY,
-			_corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
-		// Draw horizontal lines for top/bottom boundaries
-		drawLine(dst, pitch, width, height,
-			_corridorLeftX + _viewX, _corridorTopY + _viewY,
-			_corridorRightX + _viewX, _corridorTopY + _viewY, corridorColor);
-		drawLine(dst, pitch, width, height,
-			_corridorLeftX + _viewX, _corridorBottomY + _viewY,
-			_corridorRightX + _viewX, _corridorBottomY + _viewY, corridorColor);
-	}
-}
-
 void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
 	renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
 }
@@ -2590,11 +2515,6 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
 	}
 
-	// Collision zone visualization (debug - for Handler 7/8 pilot modes)
-	if (_rebelHandler == 7 || _rebelHandler == 8) {
-		drawCollisionZones(renderBitmap, pitch, width, height, 0);
-	}
-
 	// Crosshair/reticle (FUN_004089ab, FUN_0040d836)
 	renderCrosshair(renderBitmap, pitch, width, height);
 


Commit: 25cb578e5f093415331a368cc61c4beaad415c49
    https://github.com/scummvm/scummvm/commit/25cb578e5f093415331a368cc61c4beaad415c49
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00

Commit Message:
SCUMM: RA2: Remove remaining debug code

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 7c5c829c344..cbd915447ee 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -1088,7 +1088,7 @@ int32 InsaneRebel2::processMouse() {
 				it->id, it->active, it->destroyed,
 				it->rect.left, it->rect.top, it->rect.right, it->rect.bottom,
 				it->rect.contains(worldMousePos));
-				
+
 			if (it->active && it->rect.contains(worldMousePos)) {
 				// Enemy hit!
 				it->active = false;
@@ -1097,10 +1097,21 @@ int32 InsaneRebel2::processMouse() {
 					it->id, it->type, mousePos.x, mousePos.y,
 					it->rect.left, it->rect.top, it->rect.right, it->rect.bottom);
 
+				// Explosion scale is handler-specific in retail:
+				// - H8/H7/H26 use object half-width
+				// - H25 uses half-width + snapDistance (and type 100 doubles it)
+				int explosionHalfWidth = it->rect.width() / 2;
+				if (_rebelHandler == 25) {
+					LevelDifficultyParams dparams = getDifficultyParams();
+					explosionHalfWidth += dparams.snapDistance;
+					if (it->type == 100)
+						explosionHalfWidth *= 2;
+				}
+
 				// Spawn visual explosion based on handler, enemy type, and flags.
 				//
 				// Rendering functions (FUN_409FBC, FUN_402696, FUN_40F1C5,
-				// FUN_41F29A) check DAT_0047e108 flags & 1 — when set,
+				// FUN_41F29A) check DAT_0047e108 flags & 1 - when set,
 				// explosion NUT sprites are suppressed. This is checked
 				// during rendering in renderExplosions().
 				//
@@ -1111,17 +1122,17 @@ int32 InsaneRebel2::processMouse() {
 				if (_rebelHandler != 8 && _rebelHandler != 25) {
 					spawnExplosion((it->rect.left + it->rect.right) / 2,
 								   (it->rect.top + it->rect.bottom) / 2,
-								   it->rect.width() / 2);
+								   explosionHalfWidth);
 				} else if (_rebelHandler == 8 && it->type == 0) {
 					spawnExplosion((it->rect.left + it->rect.right) / 2,
 								   (it->rect.top + it->rect.bottom) / 2,
-								   it->rect.width() / 2);
+								   explosionHalfWidth);
 				} else if (_rebelHandler == 25 && it->type > 3) {
 					// Counter is set for timing/sound, but rendering
 					// may be suppressed by flags bit 0
 					spawnExplosion((it->rect.left + it->rect.right) / 2,
 								   (it->rect.top + it->rect.bottom) / 2,
-								   it->rect.width() / 2);
+								   explosionHalfWidth);
 				}
 
 				// Disable self (prevents sprite from rendering via SKIP chunks)
@@ -1138,9 +1149,14 @@ int32 InsaneRebel2::processMouse() {
 				// Increment kill counter (DAT_0047ab88)
 				_rebelKillCounter++;
 
-				// Handle dependencies
+				// Handle dependencies.
+				// Handler 25 (FUN_41E7C2) has two kill paths:
+				// - type <= 3: process dependency tables
+				// - type > 3 (and 100): skip dependency handling
+				// Other handlers always use link-table side effects.
+				bool handleDependencies = !(_rebelHandler == 25 && it->type > 3);
 				int id = it->id;
-				if (id >= 0 && id < 512) {
+				if (handleDependencies && id >= 0 && id < 512) {
 					// Slot 2: Enable (Explosion?)
 					if (_rebelLinks[id][2] != 0) {
 						clearBit(_rebelLinks[id][2]);
@@ -1175,7 +1191,7 @@ int32 InsaneRebel2::processMouse() {
 				}
 
 				// Award score for destroying enemy (FUN_0041bf8d called from FUN_40A2E0)
-				// Score value comes from DAT_0047e0fe indexed by difficulty×level
+				// Score value comes from DAT_0047e0fe indexed by difficulty*level
 				{
 					LevelDifficultyParams dparams = getDifficultyParams();
 					if (dparams.hitPoints > 0) {
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index a37d3347fce..998179cd9ba 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -451,7 +451,7 @@ public:
 	// Draw fallback ship using embedded HUD frame
 	void renderFallbackShip(byte *renderBitmap, int pitch, int width, int height);
 
-	// Draw enemy indicator brackets and erase destroyed enemy areas
+	// Draw per-enemy target indicators from the cockpit icon sheet.
 	void renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth);
 
 	// Draw explosion animations from 5-slot system (dispatcher)
@@ -508,7 +508,6 @@ public:
 	// This is called at the start of each frame, before FOBJ sprites are decoded
 	void procPreRendering(byte *renderBitmap) override;
 
-	void drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color);
 	// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
 	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
 
@@ -1042,7 +1041,6 @@ public:
 
 	/* Difficulty Level (0-5, from pilot menu; maps directly to table rows) */
 	int _difficulty;
-	void drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color);
 
 	// ======================= Per-Level Difficulty Parameters =======================
 	// Extracted from RA2WIN95.EXE at VA 0x47e0f0
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index a62c1405bbe..8fa2fde4c6a 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1599,52 +1599,6 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	}
 
 }
-void InsaneRebel2::drawLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, byte color) {
-	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
-	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
-	int err = dx + dy, e2;
-
-	for (;;) {
-		if (x0 >= 0 && x0 < width && y0 >= 0 && y0 < height) {
-			dst[y0 * pitch + x0] = color;
-		}
-		if (x0 == x1 && y0 == y1)
-			break;
-		e2 = 2 * err;
-		if (e2 >= dy) { err += dy; x0 += sx; }
-		if (e2 <= dx) { err += dx; y0 += sy; }
-	}
-}
-
-void InsaneRebel2::drawCornerBrackets(byte *dst, int pitch, int width, int height, int x, int y, int w, int h, byte color) {
-	// Draw L-shaped brackets at corners of the rect (x,y,w,h)
-	// Bracket size: approx 8 pixels
-	int armLen = 2;
-	if (armLen > w / 2)
-		armLen = w / 2;
-	if (armLen > h / 2)
-		armLen = h / 2;
-
-	int x2 = x + w - 1;
-	int y2 = y + h - 1;
-
-	// Top-Left Corner
-	drawLine(dst, pitch, width, height, x, y, x + armLen, y, color);
-	drawLine(dst, pitch, width, height, x, y, x, y + armLen, color);
-
-	// Top-Right Corner
-	drawLine(dst, pitch, width, height, x2 - armLen, y, x2, y, color);
-	drawLine(dst, pitch, width, height, x2, y, x2, y + armLen, color);
-
-	// Bottom-Left Corner
-	drawLine(dst, pitch, width, height, x, y2, x + armLen, y2, color);
-	drawLine(dst, pitch, width, height, x, y2 - armLen, x, y2, color);
-
-	// Bottom-Right Corner
-	drawLine(dst, pitch, width, height, x2 - armLen, y2, x2, y2, color);
-	drawLine(dst, pitch, width, height, x2, y2 - armLen, x2, y2, color);
-}
-
 // ============================================================
 // COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
 // ============================================================
@@ -2456,7 +2410,7 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	renderHandler8Ship(renderBitmap, pitch, width, height);
 	renderFallbackShip(renderBitmap, pitch, width, height);
 
-	// Enemy indicators and destroyed enemy area erase
+	// Enemy target indicators (handler-specific; sprite-based in turret mode)
 	renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
 
 	// Explosion animations (FUN_409FBC) — drawn before lasers in original
@@ -3611,26 +3565,26 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
 }
 
 void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
-	// Draw enemy indicator brackets for active enemies
+	// Original per-enemy target indicator behavior comes from FUN_40A2E0 (handler 0x26):
+	// - Draws cockpit icon sprites 6..10 at enemy centers.
+	// - Enabled when level flags bit 2 (0x04) is clear.
+	// - Sprite index depends on object half-width bucket.
 	//
-	// NOTE: Do NOT fill destroyed enemy areas with black. The original game does not do this.
-	// When an enemy is destroyed:
-	// 1. setBit(enemy_id) disables the enemy in the bit table
-	// 2. clearBit(dependency_id) enables dependent objects (explosion animations)
-	// 3. SKIP chunks in the video cause enemy FOBJ sprites to be skipped (via procSKIP)
-	// 4. renderExplosions() draws the explosion animation from the 5-slot system
-	// 5. The background video shows through where the enemy was
-
-	// Draw green brackets for active enemies (Easy/Medium difficulty only)
-	if (_difficulty >= 2)
+	// It is not a generic all-handler bracket overlay.
+	if (_rebelHandler != 0x26 || !_smush_iconsNut)
+		return;
+
+	LevelDifficultyParams dparams = getDifficultyParams();
+	if ((dparams.flags & 4) != 0)
 		return;
 
-	// FOBJ sprites are rendered with _fobjOffsetX/Y applied (set from _rebelViewOffsetX/Y
-	// for Handler 25). Brackets must use the same offset so they align with the sprites.
+	// FOBJ sprites are rendered with _fobjOffsetX/Y applied. Use the same offsets
+	// so indicators stay aligned with decoded enemy sprites.
 	int fobjOffX = _player ? _player->_fobjOffsetX : 0;
 	int fobjOffY = _player ? _player->_fobjOffsetY : 0;
 
 	Common::Rect viewRect(_viewX, _viewY, _viewX + videoWidth, _viewY + 200);
+	const int sizeClamp = dparams.specialDamage; // DAT_0047e0fa in FUN_40A2E0
 
 	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
 		if (it->destroyed || !it->active || isBitSet(it->id))
@@ -3642,8 +3596,40 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 		    r.bottom <= viewRect.top || r.top >= viewRect.bottom)
 			continue;
 
-		const byte color = 5;  // Green
-		drawCornerBrackets(renderBitmap, pitch, width, height, r.left, r.top, r.width(), r.height(), color);
+		int halfW = r.width() / 2;
+		int halfH = r.height() / 2;
+		if (halfW <= 0 || halfH <= 0)
+			continue;
+
+		// Match size-bucket selection from FUN_40A2E0:
+		// class 0..4 -> sprite 6..10.
+		int indicatorHalfW = halfW;
+		if (sizeClamp > 0)
+			indicatorHalfW = MIN(indicatorHalfW, sizeClamp / 2);
+
+		int sizeClass;
+		if (indicatorHalfW < 3) {
+			sizeClass = 0;
+		} else if (indicatorHalfW < 6) {
+			sizeClass = 1;
+		} else if (indicatorHalfW < 9) {
+			sizeClass = 2;
+		} else if (indicatorHalfW < 12) {
+			sizeClass = 3;
+		} else {
+			sizeClass = 4;
+		}
+
+		int spriteIndex = sizeClass + 6;
+		if (spriteIndex < 0 || spriteIndex >= _smush_iconsNut->getNumChars())
+			continue;
+
+		int centerX = r.left + halfW;
+		int centerY = r.top + halfH;
+		int iw = _smush_iconsNut->getCharWidth(spriteIndex);
+		int ih = _smush_iconsNut->getCharHeight(spriteIndex);
+		renderNutSprite(renderBitmap, pitch, width, height,
+			centerX - iw / 2, centerY - ih / 2, _smush_iconsNut, spriteIndex);
 	}
 }
 
@@ -3897,6 +3883,9 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
 			continue;
 		}
 
+		// Match FUN_41F29A exactly: decrement first, then select frame.
+		_explosions[i].counter--;
+
 		// FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
 		int baseIndex;
 		if (_explosions[i].scale < 11) {
@@ -3919,8 +3908,6 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
 			renderNutSprite(renderBitmap, pitch, width, height,
 				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
 		}
-
-		_explosions[i].counter--;
 	}
 }
 


Commit: b8518e989ec994cdc84f77a84bbd67130c5c3914
    https://github.com/scummvm/scummvm/commit/b8518e989ec994cdc84f77a84bbd67130c5c3914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:19+02:00

Commit Message:
SCUMM: RA2: Add top pilots screen

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/nut_renderer.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index cbd915447ee..3f0684dfee4 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -459,6 +459,11 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_pilotNameInput = "";
 	_pilotEditIndex = -1;
 
+	// Initialize top pilots display state and ranking table (FUN_0040FF00)
+	_topPilotsFrameCount = 0;
+	_topPilotsMaxFrames = 120;
+	initDefaultRankings();
+
 	// Initialize menu input capture system
 	_menuInputActive = false;
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 998179cd9ba..c356eaa5d95 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -67,6 +67,7 @@ public:
 		kStateGameplay = 4,     // Stage 4: Gameplay (FUN_00416787)
 		kStateCredits = 5,      // Credits sequence
 		kStateQuit = 6,         // Exit game
+		kStateTopPilots = 8,    // Top Pilots score display (FUN_00420116)
 		kStateDifficultySelect = 7 // Difficulty submenu within pilot select
 	};
 
@@ -97,10 +98,13 @@ public:
 	// Process menu input (keyboard/mouse) - returns selected item or -1
 	int processMenuInput();
 
+	// Format-code-aware string rendering (^fNN=font, ^cNNN=color)
+	int getMenuStringWidth(const char *str) const;
+	void drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor = 1);
+	void drawMenuStringCentered(byte *renderBitmap, const char *str, int cx, int y, int defaultColor = 1);
+	void drawMenuStringRight(byte *renderBitmap, const char *str, int rx, int y, int defaultColor = 1);
+
 	// Shared menu item renderer - emulates FUN_0041F5AE
-	// items[0] = title, items[1..numItems] = selectable items, selection = highlighted item
-	// leftAligned=false: param_4==0 (centered, for main menu / pilot select)
-	// leftAligned=true:  param_4==1 (left-aligned, for chapter select)
 	void drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
 	                   const char **items, int numItems, int selection,
 	                   bool leftAligned = false);
@@ -163,6 +167,38 @@ public:
 	// Password table lookup (FUN_0041BCE0)
 	Common::String getChapterPassword(int level, int difficulty);
 
+	// ================= Top Pilots Screen (FUN_00420116) ====================
+	// Shows ranked pilot scores with animated reveal, played over menu video
+	// Original: DAT_00443b58 ranking table, 0x4a-byte records, max 15 entries
+
+	static const int kMaxRankings = 15;
+
+	struct RankingEntry {
+		char name[40];       // +0x04: Pilot name (or "-----" for defaults)
+		int32 score;         // +0x36: Total score
+		int32 rating;        // +0x3a: Total rating (converted to rank medals)
+		int16 difficulty;    // +0x3e: Difficulty tier (0-5), TRS index = difficulty + 155
+		int16 chapter;       // +0x40: Highest chapter completed (1-15)
+	};
+
+	RankingEntry _rankings[kMaxRankings];
+	int _numRankings;
+
+	// Initialize ranking table with defaults (FUN_0040FF00)
+	void initDefaultRankings();
+
+	// Insert pilot score into sorted ranking table (FUN_00410271)
+	void insertRanking(const char *name, int32 score, int32 rating, int16 difficulty, int16 chapter);
+
+	// Run top pilots display - called from main menu "Show Top Pilots"
+	void showTopPilots();
+
+	// Draw top pilots overlay on current frame during video playback
+	void drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width, int height);
+
+	int _topPilotsFrameCount;     // Animation frame counter (pilots revealed one per frame)
+	int _topPilotsMaxFrames;      // Total frames to display (120 or 240)
+
 	// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
 	// Original: 10 pilot slots × 0x118 (280) bytes at DAT_004568A8
 	// Stored via SaveFileManager in a custom save file
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 92166f4a927..290a5b77258 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -433,8 +433,100 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	}
 }
 
+// Format-code-aware string width calculation
+// Handles ^fNN (font switch), ^cNNN (color), ^^ (literal ^)
+int InsaneRebel2::getMenuStringWidth(const char *str) const {
+	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
+	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+	if (!defaultFont)
+		return 0;
+
+	int w = 0;
+	NutRenderer *curFont = defaultFont;
+	while (*str) {
+		if (*str == '^') {
+			const char *p = str + 1;
+			if (*p == '^') { str = p + 1; continue; }
+			if (*p == 'f') {
+				p++;
+				int idx = 0;
+				while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+				curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+				str = p;
+				continue;
+			}
+			if (*p == 'c' || *p == 'l') {
+				p++;
+				while (*p >= '0' && *p <= '9') p++;
+				str = p;
+				continue;
+			}
+		}
+		byte c = (byte)*str++;
+		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+		if (curFont && c < curFont->getNumChars())
+			w += curFont->getCharWidth(c);
+	}
+	return w;
+}
+
+// Format-code-aware string rendering at (x, y)
+void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor) {
+	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
+	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
+	if (!defaultFont)
+		return;
+
+	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	int pitch = _vm->_screenWidth;
+
+	NutRenderer *curFont = defaultFont;
+	int curColor = defaultColor;
+	while (*str) {
+		if (*str == '^') {
+			const char *p = str + 1;
+			if (*p == '^') { str = p + 1; continue; }
+			if (*p == 'f') {
+				p++;
+				int idx = 0;
+				while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+				curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
+				str = p;
+				continue;
+			}
+			if (*p == 'c') {
+				p++;
+				int color = 0;
+				while (*p >= '0' && *p <= '9') color = color * 10 + (*p++ - '0');
+				curColor = color;
+				str = p;
+				continue;
+			}
+			if (*p == 'l') { str = p + 1; continue; }
+		}
+		byte c = (byte)*str++;
+		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+		if (!curFont) continue;
+		if (c >= curFont->getNumChars()) continue;
+		int charW = curFont->getCharWidth(c);
+		if (x >= 0 && y >= 0 && charW > 0)
+			curFont->drawCharV7(renderBitmap, clipRect, x, y, pitch, curColor,
+			                    kStyleAlignLeft, c, false, false);
+		x += charW;
+	}
+}
+
+void InsaneRebel2::drawMenuStringCentered(byte *renderBitmap, const char *str, int cx, int y, int defaultColor) {
+	int w = getMenuStringWidth(str);
+	drawMenuString(renderBitmap, str, cx - w / 2, y, defaultColor);
+}
+
+void InsaneRebel2::drawMenuStringRight(byte *renderBitmap, const char *str, int rx, int y, int defaultColor) {
+	int w = getMenuStringWidth(str);
+	drawMenuString(renderBitmap, str, rx - w, y, defaultColor);
+}
+
 void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// =====================================================================
 	// Main menu renderer - calls shared drawMenuItems()
 	// Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
 	// =====================================================================
@@ -742,9 +834,9 @@ int InsaneRebel2::runMainMenu() {
 			_menuInputActive = true;
 			break;
 
-		case 4:  // Show Top Pilots -> high score display
+		case 4:  // Show Top Pilots -> high score display (FUN_00420116(-1))
 			debug("Rebel2: Show Top Pilots selected");
-			// TODO: Implement high score display (FUN_00420116(-1))
+			showTopPilots();
 			break;
 
 		case 5:  // Show Credits -> play credits video
@@ -1197,20 +1289,14 @@ void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width
 	}
 }
 
-// Rating-to-medal string conversion - emulates FUN_0042001f
-// Converts a rating value (0-50) to a string of medal characters for TALKFONT.NUT:
-//   Every 9 points → big medal (DAT_00482550)
-//   Every 3 points → medium medal (DAT_00482558)
-//   Every 1 point  → small medal (DAT_00482560)
+// Rating to medal string (FUN_0042001f): TALKFONT glyphs 3=big, 2=medium, 1=small
 Common::String InsaneRebel2::getRankString(int rating) {
 	if (rating > 50)
 		rating = 50;
 	Common::String result;
-	// TODO: Medal char bytes are placeholders — verify against actual NUT font glyphs
-	// from DAT_00482550/58/60 in the game binary
-	while (rating >= 9) { result += '\x83'; rating -= 9; }  // big medal
-	while (rating >= 3) { result += '\x82'; rating -= 3; }  // medium medal
-	while (rating >= 1) { result += '\x81'; rating -= 1; }  // small medal
+	while (rating >= 9) { result += (char)3; rating -= 9; }
+	while (rating >= 3) { result += (char)2; rating -= 3; }
+	while (rating >= 1) { result += (char)1; rating -= 1; }
 	return result;
 }
 
@@ -1257,9 +1343,7 @@ Common::String InsaneRebel2::getChapterPassword(int level, int difficulty) {
 	return kPasswordTable[idx];
 }
 
-// Draw score/info line at bottom of chapter select - emulates FUN_00434cb0 calls
-// For unlocked chapters: score display using TRS 80 at (25, 190)
-// For locked chapters: password prompt using TRS 81 at (30, 190)
+// Draw score/info line at bottom of chapter select
 void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height) {
 	if (_chapterSelection < 0 || _chapterSelection >= 16)
 		return;
@@ -1268,74 +1352,6 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 	if (!splayer)
 		return;
 
-	// Font system — same as drawMenuItems()
-	NutRenderer *fonts[3] = {
-		_smush_talkfontNut,
-		_smush_smalfontNut,
-		_smush_titlefontNut
-	};
-	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
-	if (!defaultFont)
-		return;
-
-	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
-	int actualPitch = _vm->_screenWidth;
-
-	// Format code parser — same as drawMenuItems()
-	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
-		if (*str != '^')
-			return -1;
-		const char *p = str + 1;
-		if (*p == '^') { str = p; return -1; }
-		if (*p == 'f') {
-			p++;
-			int fontIdx = 0;
-			while (*p >= '0' && *p <= '9') { fontIdx = fontIdx * 10 + (*p - '0'); p++; }
-			str = p;
-			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
-		}
-		if (*p == 'c') {
-			p++;
-			int color = 0;
-			while (*p >= '0' && *p <= '9') { color = color * 10 + (*p - '0'); p++; }
-			str = p;
-			outColor = color;
-			return -2;
-		}
-		return -1;
-	};
-
-	// String rendering with format codes
-	auto drawString = [&](const char *str, int x, int y) {
-		NutRenderer *curFont = defaultFont;
-		int curColor = 1;
-
-		while (*str) {
-			int fontChange = parseFormatCode(str, curColor);
-			if (fontChange >= 0) {
-				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
-				continue;
-			}
-			if (fontChange == -2)
-				continue;
-
-			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z')
-				c = c - 'a' + 'A';
-			if (!curFont)
-				continue;
-			int numChars = curFont->getNumChars();
-			if (c >= numChars)
-				continue;
-			int charW = curFont->getCharWidth(c);
-			if (x >= 0 && y >= 0 && charW > 0) {
-				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
-				                    kStyleAlignLeft, c, false, false);
-			}
-			x += charW;
-		}
-	};
-
 	if (_chapterUnlocked[_chapterSelection]) {
 		// Unlocked: show score info using TRS 80 at X=25 (0x19), Y=190 (0xbe)
 		// TRS 80 = "^f01^c248Pilots: %hd  Score: %ld  Rank: ^f00%s"
@@ -1358,15 +1374,12 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 		Common::String displayStr = Common::String::format(fmtStr,
 			(short)pilotLives, (long)pilotScore, rankStr.c_str());
 
-		drawString(displayStr.c_str(), 25, 190);
+		drawMenuString(renderBitmap, displayStr.c_str(), 25, 190);
 	} else {
-		// Locked: show password prompt using TRS 81 at X=30 (0x1e), Y=190 (0xbe)
-		// Format: "%s ^c005%s%c" (TRS 81 + green password input + blinking cursor)
 		const char *lockStr = splayer->getString(81);
 		if (!lockStr || !lockStr[0])
 			lockStr = "^f01^c248UNREGISTERED - PASSCODE REQUIRED";
 
-		// Blinking cursor: alternate '_' and ' ' (original uses bit 1 of frame counter)
 		static int cursorCounter = 0;
 		cursorCounter++;
 		char cursor = ((cursorCounter / 8) & 1) ? '_' : ' ';
@@ -1374,7 +1387,7 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 		Common::String displayStr = Common::String::format("%s ^c005%s%c",
 			lockStr, _passwordInput.c_str(), cursor);
 
-		drawString(displayStr.c_str(), 30, 190);
+		drawMenuString(renderBitmap, displayStr.c_str(), 30, 190);
 	}
 }
 
@@ -1789,4 +1802,189 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
 }
 
+// ==================== Top Pilots Screen (FUN_00420116) ====================
+// Displays ranked pilot scores with animated reveal over a menu background video.
+//
+// Original FUN_00420116 at 0x420116:
+// - Plays random menu video as background (FUN_0041fdc8)
+// - Draws title from TRS string 0x96 (150) centered at (152, 10) in low-res
+// - Iterates through ranking table (DAT_00443b58, 0x4a-byte records, max 15)
+// - Each row shows: rank medals, pilot name, difficulty, chapter, total score
+// - Rows appear one per frame (animated reveal up to local_10)
+// - param_1 == -2: 240 frame loop (from options); else 120 frames (from main menu)
+// - Exits on mouse click (DAT_0047a7e4 & 1) or any keypress (DAT_0047a7e8 != 0)
+//
+// Ranking table record (0x4a = 74 bytes, from FUN_00410271):
+//   +0x00 (4): timestamp
+//   +0x04 (40): pilot name (or "-----" for defaults)
+//   +0x36 (4): total score
+//   +0x3a (4): total rating
+//   +0x3e (2): difficulty tier (1-3) — TRS lookup: value + 0x9b (155)
+//   +0x40 (2): highest chapter completed (1-15)
+//
+// Column X positions (low-res 320x200):
+//   Rank medals: 43 (0x2b)  - FUN_004341a0, alignment=1
+//   Name:        88 (0x58)  - FUN_00434cb0, alignment=0 (left), "^f01%s"
+//   Difficulty: 195 (0xc3)  - FUN_00434cb0, alignment=1, "^f01%s" (TRS)
+//   Chapter:    245 (0xf5)  - FUN_00434cb0, alignment=1, "^f01%hd"
+//   Score:      295 (0x127) - FUN_00434cb0, alignment=2, "^f01%ld"
+// Row Y: sVar1 * 10 + 42 (0x2a)
+
+// Initialize ranking table with 15 default entries (FUN_0040FF00)
+// Called when no ranking save file exists. Generates placeholder entries with:
+//   name = "-----", score = (15-i)*1500, rating = (15-i)*2,
+//   difficulty = ((15-i)*3+14)/15, chapter = ((15-i)*15+14)/15
+void InsaneRebel2::initDefaultRankings() {
+	_numRankings = 0;
+	memset(_rankings, 0, sizeof(_rankings));
+	for (int i = 0; i < kMaxRankings; i++) {
+		int k = kMaxRankings - i;  // 15 down to 1
+		RankingEntry &r = _rankings[_numRankings];
+		Common::strlcpy(r.name, "-----", sizeof(r.name));
+		r.score = k * 1500;
+		r.rating = k * 2;
+		r.difficulty = (int16)((k * 3 + 14) / 15);
+		r.chapter = (int16)((k * 15 + 14) / 15);
+		_numRankings++;
+	}
+}
+
+// Insert a new entry into the sorted ranking table (FUN_00410271)
+// Maintains descending score order, max kMaxRankings entries
+void InsaneRebel2::insertRanking(const char *name, int32 score, int32 rating,
+                                  int16 difficulty, int16 chapter) {
+	if (score == 0)
+		return;
+
+	// Find insertion point (first entry with score < new score)
+	int insertPos = 0;
+	while (insertPos < _numRankings && score <= _rankings[insertPos].score) {
+		insertPos++;
+	}
+	if (insertPos > kMaxRankings - 1)
+		return;
+
+	// Remove any existing entry with same name
+	for (int i = 0; i < _numRankings; i++) {
+		if (strcmp(_rankings[i].name, name) == 0) {
+			if (score <= _rankings[i].score)
+				return;  // Existing entry has higher score
+			// Remove old entry by shifting
+			for (int j = i; j < _numRankings - 1; j++)
+				_rankings[j] = _rankings[j + 1];
+			_numRankings--;
+			if (insertPos > i)
+				insertPos--;
+			break;
+		}
+	}
+
+	// Shift entries down to make room
+	int lastIdx = MIN(_numRankings, kMaxRankings - 1);
+	for (int i = lastIdx; i > insertPos; i--)
+		_rankings[i] = _rankings[i - 1];
+
+	// Insert new entry
+	RankingEntry &r = _rankings[insertPos];
+	Common::strlcpy(r.name, name, sizeof(r.name));
+	r.score = score;
+	r.rating = rating;
+	r.difficulty = difficulty;
+	r.chapter = chapter;
+
+	if (_numRankings < kMaxRankings)
+		_numRankings++;
+}
+
+void InsaneRebel2::showTopPilots() {
+	debug("Rebel2: Showing Top Pilots screen (FUN_00420116)");
+
+	_menuInputActive = true;
+	while (!_menuEventQueue.empty())
+		_menuEventQueue.pop();
+
+	// param_1 = -1 from main menu: maxFrames = 120 (0x78)
+	_topPilotsMaxFrames = 120;
+	_topPilotsFrameCount = 0;
+
+	_gameState = kStateTopPilots;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	_vm->_smushVideoShouldFinish = false;
+
+	Common::String menuVideo = getRandomMenuVideo();
+	splayer->setCurVideoFlags(0x20);
+	splayer->play(menuVideo.c_str(), 12);
+
+	_gameState = kStateMainMenu;
+	_menuInputActive = true;
+
+	debug("Rebel2: Top Pilots screen finished");
+}
+
+void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width, int height) {
+	if (_topPilotsFrameCount >= _topPilotsMaxFrames) {
+		_vm->_smushVideoShouldFinish = true;
+		return;
+	}
+
+	while (!_menuEventQueue.empty()) {
+		Common::Event event = _menuEventQueue.pop();
+		if (event.type == Common::EVENT_KEYDOWN ||
+		    event.type == Common::EVENT_LBUTTONDOWN) {
+			_vm->_smushVideoShouldFinish = true;
+			return;
+		}
+	}
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer)
+		return;
+
+	// Title + column headers: TRS 150 centered at X=152, Y=10
+	const char *titleStr = splayer->getString(150);
+	if (!titleStr || !titleStr[0])
+		titleStr = "^f02Top Pilots ^f01^c005      Rank        Name               Difficulty  Chapter  Score";
+	drawMenuStringCentered(renderBitmap, titleStr, 152, 10);
+
+	// Animated reveal: show up to _topPilotsFrameCount entries
+	int showCount = MIN(_topPilotsFrameCount, _numRankings);
+
+	for (int row = 0; row < showCount; row++) {
+		const RankingEntry &r = _rankings[row];
+		int rowY = row * 10 + 42;
+		int color = 244;  // 0xF4
+
+		// Column 1: Rank medals at X=43, centered (font 0 = TALKFONT)
+		Common::String rankStr = getRankString(r.rating);
+		if (!rankStr.empty()) {
+			Common::String rankFmt = Common::String::format("^f00%s", rankStr.c_str());
+			drawMenuStringCentered(renderBitmap, rankFmt.c_str(), 43, rowY, color);
+		}
+
+		// Column 2: Pilot name at X=88, left-aligned (font 1 = SMALFONT)
+		Common::String nameFmt = Common::String::format("^f01%s", r.name);
+		drawMenuString(renderBitmap, nameFmt.c_str(), 88, rowY, color);
+
+		// Column 3: Difficulty at X=195, centered - TRS (difficulty + 155)
+		int trsIdx = CLIP((int)r.difficulty, 0, 5) + 155;
+		const char *diffStr = splayer->getString(trsIdx);
+		if (diffStr && diffStr[0]) {
+			Common::String diffFmt = Common::String::format("^f01%s", diffStr);
+			drawMenuStringCentered(renderBitmap, diffFmt.c_str(), 195, rowY, color);
+		}
+
+		// Column 4: Highest chapter at X=245, centered
+		Common::String chFmt = Common::String::format("^f01%d", (int)r.chapter);
+		drawMenuStringCentered(renderBitmap, chFmt.c_str(), 245, rowY, color);
+
+		// Column 5: Total score at X=295, right-aligned
+		Common::String scoreFmt = Common::String::format("^f01%ld", (long)r.score);
+		drawMenuStringRight(renderBitmap, scoreFmt.c_str(), 295, rowY, color);
+	}
+
+	_topPilotsFrameCount++;
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 8fa2fde4c6a..be5fc25a4ce 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2294,6 +2294,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		return;
 	}
 
+	// Handle Top Pilots screen (FUN_00420116)
+	bool topPilotsMode = (introPlaying && _gameState == kStateTopPilots);
+	if (topPilotsMode) {
+		drawTopPilotsOverlay(renderBitmap, pitch, width, height);
+		return;
+	}
+
 	// Handle menu input and rendering if in menu mode
 	if (menuMode) {
 		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 8d81f1a302c..8fd02c39e1d 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -397,6 +397,8 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
 	if (chr >= _numChars)
 		return 0;
 
+
+
 	if (_direction < 0)
 		x -= _chars[chr].width;
 


Commit: b3a0be791d80623ff57ba9ecf8a7690decc93c94
    https://github.com/scummvm/scummvm/commit/b3a0be791d80623ff57ba9ecf8a7690decc93c94
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:20+02:00

Commit Message:
SCUMM: RA2: Improve top pilots screen

Changed paths:
    engines/scumm/insane/insane_rebel_menu.cpp


diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 290a5b77258..6dd6e15e29c 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -265,6 +265,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
 	// =====================================================================
 	//   ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
+	// Fixed-width format codes: ^fNN (2-digit font), ^cNNN (3-digit color)
 	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
 		if (*str != '^')
 			return -1;
@@ -276,21 +277,16 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 		}
 		if (*p == 'f') {
 			p++;
-			int fontIdx = 0;
-			while (*p >= '0' && *p <= '9') {
-				fontIdx = fontIdx * 10 + (*p - '0');
-				p++;
-			}
+			int fontIdx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
+			fontIdx = fontIdx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
 			str = p;
 			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
 		}
 		if (*p == 'c') {
 			p++;
 			int color = 0;
-			while (*p >= '0' && *p <= '9') {
-				color = color * 10 + (*p - '0');
-				p++;
-			}
+			for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
+				color = color * 10 + (*p++ - '0');
 			str = p;
 			outColor = color;
 			return -2;
@@ -434,7 +430,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 }
 
 // Format-code-aware string width calculation
-// Handles ^fNN (font switch), ^cNNN (color), ^^ (literal ^)
+// Fixed-width codes: ^fNN (2-digit font), ^cNNN (3-digit color), ^^ (literal ^)
 int InsaneRebel2::getMenuStringWidth(const char *str) const {
 	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -449,18 +445,19 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
 			if (*p == '^') { str = p + 1; continue; }
 			if (*p == 'f') {
 				p++;
-				int idx = 0;
-				while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+				int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
+				idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
 				curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
 				str = p;
 				continue;
 			}
-			if (*p == 'c' || *p == 'l') {
+			if (*p == 'c') {
 				p++;
-				while (*p >= '0' && *p <= '9') p++;
+				for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++) p++;
 				str = p;
 				continue;
 			}
+			if (*p == 'l') { str = p + 1; continue; }
 		}
 		byte c = (byte)*str++;
 		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
@@ -488,8 +485,8 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
 			if (*p == '^') { str = p + 1; continue; }
 			if (*p == 'f') {
 				p++;
-				int idx = 0;
-				while (*p >= '0' && *p <= '9') idx = idx * 10 + (*p++ - '0');
+				int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
+				idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
 				curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
 				str = p;
 				continue;
@@ -497,7 +494,8 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
 			if (*p == 'c') {
 				p++;
 				int color = 0;
-				while (*p >= '0' && *p <= '9') color = color * 10 + (*p++ - '0');
+				for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
+					color = color * 10 + (*p++ - '0');
 				curColor = color;
 				str = p;
 				continue;
@@ -1942,11 +1940,17 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
 	if (!splayer)
 		return;
 
-	// Title + column headers: TRS 150 centered at X=152, Y=10
-	const char *titleStr = splayer->getString(150);
-	if (!titleStr || !titleStr[0])
-		titleStr = "^f02Top Pilots ^f01^c005      Rank        Name               Difficulty  Chapter  Score";
-	drawMenuStringCentered(renderBitmap, titleStr, 152, 10);
+	// Title centered at X=152, Y=10 (TITLFONT)
+	drawMenuStringCentered(renderBitmap, "^f02Top Pilots", 152, 10);
+
+	// Column headers at Y=30 (SMALFONT), positioned to match data columns
+	int headerY = 30;
+	int headerColor = 5;
+	drawMenuStringCentered(renderBitmap, "^f01Rank", 43, headerY, headerColor);
+	drawMenuString(renderBitmap, "^f01Name", 88, headerY, headerColor);
+	drawMenuStringCentered(renderBitmap, "^f01Difficulty", 195, headerY, headerColor);
+	drawMenuStringCentered(renderBitmap, "^f01Chapter", 245, headerY, headerColor);
+	drawMenuStringRight(renderBitmap, "^f01Score", 295, headerY, headerColor);
 
 	// Animated reveal: show up to _topPilotsFrameCount entries
 	int showCount = MIN(_topPilotsFrameCount, _numRankings);


Commit: 632c63e17ebb34235e50a43628d3df8a13b9a493
    https://github.com/scummvm/scummvm/commit/632c63e17ebb34235e50a43628d3df8a13b9a493
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:20+02:00

Commit Message:
SCUMM: RA2: Add options menu

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 3f0684dfee4..a5fdd71cbc6 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -23,10 +23,13 @@
 
 #include "engines/engine.h"
 #include "common/system.h"
+#include "common/config-manager.h"
 #include "common/events.h"
 #include "common/savefile.h"
 #include "common/util.h"
 
+#include "audio/mixer.h"
+
 #include "scumm/actor.h"
 #include "scumm/file.h"
 #include "scumm/resource.h"
@@ -464,6 +467,17 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_topPilotsMaxFrames = 120;
 	initDefaultRankings();
 
+	// Initialize options menu state (FUN_004167A6 defaults)
+	_optionsSelection = 0;
+	_optionsItemCount = 9;
+	_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
+	_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
+	_optVoicesEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSpeechSoundType);
+	_optTextEnabled = true;
+	_optControlsFlipped = false;
+	_optRapidFire = false;
+	_optVolumeLevel = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2;
+
 	// Initialize menu input capture system
 	_menuInputActive = false;
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index c356eaa5d95..d16c306e937 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -68,7 +68,8 @@ public:
 		kStateCredits = 5,      // Credits sequence
 		kStateQuit = 6,         // Exit game
 		kStateTopPilots = 8,    // Top Pilots score display (FUN_00420116)
-		kStateDifficultySelect = 7 // Difficulty submenu within pilot select
+		kStateDifficultySelect = 7, // Difficulty submenu within pilot select
+		kStateOptions = 9       // Options menu (FUN_004167A6)
 	};
 
 	// Menu selection results (return values from FUN_004147B2)
@@ -199,6 +200,28 @@ public:
 	int _topPilotsFrameCount;     // Animation frame counter (pilots revealed one per frame)
 	int _topPilotsMaxFrames;      // Total frames to display (120 or 240)
 
+	// ================= Options Menu (FUN_004167A6) ====================
+	// TRS strings: 89 (title), 90-101 (toggle labels), 103 (volume), 107/109 (back)
+	// Original settings array at DAT_00482e20[0..4]:
+	//   [0]=auto control, [1]=music, [2]=voices, [3]=sound
+	// Additional flags: DAT_0047a7fe (text/indicators), DAT_0047a80a (rapid fire/arrows)
+	// Volume: DAT_0047a804 (0-127), SFX vol: DAT_0047a802 (127-768)
+
+	void showOptionsMenu();
+	void drawOptionsOverlay(byte *renderBitmap, int pitch, int width, int height);
+	int processOptionsInput();
+
+	int _optionsSelection;
+	int _optionsItemCount;
+	bool _optionsExitRequested;
+	bool _optMusicEnabled;
+	bool _optSfxEnabled;
+	bool _optVoicesEnabled;
+	bool _optTextEnabled;
+	bool _optControlsFlipped;
+	bool _optRapidFire;
+	int _optVolumeLevel;       // 0-127 (DAT_0047a804)
+
 	// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
 	// Original: 10 pilot slots × 0x118 (280) bytes at DAT_004568A8
 	// Stored via SaveFileManager in a custom save file
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 6dd6e15e29c..0ea75e2821c 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -23,6 +23,8 @@
 #include "common/events.h"
 #include "common/util.h"
 
+#include "audio/mixer.h"
+
 #include "graphics/cursorman.h"
 #include "graphics/wincursor.h"
 
@@ -802,11 +804,9 @@ int InsaneRebel2::runMainMenu() {
 			_menuInputActive = false;
 			return kMenuNewGame;  // Return 2 (kMenuNewGame)
 
-		case 1:  // Options -> show options menu
+		case 1:  // Options -> show options menu (FUN_00416787)
 			debug("Rebel2: Options selected");
-			// TODO: Implement options menu (FUN_004167a6)
-			// Options: Music, Sound, Voices, Auto Control, Indicators,
-			// Arrows, Difficulty (0-5), Music Volume, SFX Volume
+			showOptionsMenu();
 			break;
 
 		case 2:  // Calibrate Joystick
@@ -1991,4 +1991,220 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
 	_topPilotsFrameCount++;
 }
 
+// ======================= Options Menu (FUN_004167A6) =======================
+// Original: FUN_00416787 calls FUN_0041fdc8 (init video) + FUN_004167a6(1)
+// FUN_004167a6 runs a loop over FUN_0041f5ae (drawMenuItems) with dynamic
+// toggle labels and slider items. Settings stored at DAT_00482e20[0..3]
+// (get/set via FUN_00425d30/FUN_00425d40) and DAT_0047a7fe, DAT_0047a80a.
+//
+// Menu items for keyboard mode (9 selectable):
+//   [0] Title:    TRS 89  "Game Options"
+//   [1] Music:    TRS 90/91
+//   [2] SFX:      TRS 92/93
+//   [3] Voices:   TRS 94/95
+//   [4] Text:     TRS 96/97
+//   [5] Controls: TRS 98/99
+//   [6] Rapid Fire: TRS 100/101
+//   [7] Volume:   TRS 103 "Volume Level: %hd%%"  (slider, left/right ±4)
+//   [8] Back:     TRS 107 "Return to Main Menu"
+
+void InsaneRebel2::showOptionsMenu() {
+	debug("Rebel2: Showing Options menu (FUN_00416787)");
+
+	_menuInputActive = true;
+	while (!_menuEventQueue.empty())
+		_menuEventQueue.pop();
+
+	_optionsSelection = 0;
+	_optionsItemCount = 8;
+	_optionsExitRequested = false;
+
+	_gameState = kStateOptions;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Loop videos until user exits options (same pattern as runMainMenu)
+	while (!_vm->shouldQuit() && !_optionsExitRequested) {
+		_vm->_smushVideoShouldFinish = false;
+
+		Common::String menuVideo = getRandomMenuVideo();
+		splayer->setCurVideoFlags(0x20);
+		splayer->play(menuVideo.c_str(), 12);
+	}
+
+	_gameState = kStateMainMenu;
+	_menuInputActive = true;
+
+	debug("Rebel2: Options menu finished");
+}
+
+int InsaneRebel2::processOptionsInput() {
+	while (!_menuEventQueue.empty()) {
+		Common::Event event = _menuEventQueue.pop();
+
+		if (event.type == Common::EVENT_KEYDOWN) {
+			_menuInactivityTimer = 0;
+
+			switch (event.kbd.keycode) {
+			case Common::KEYCODE_UP:
+				_optionsSelection--;
+				if (_optionsSelection < 0)
+					_optionsSelection = _optionsItemCount - 1;
+				return -1;
+
+			case Common::KEYCODE_DOWN:
+				_optionsSelection++;
+				if (_optionsSelection >= _optionsItemCount)
+					_optionsSelection = 0;
+				return -1;
+
+			case Common::KEYCODE_LEFT:
+				// Volume slider: decrease by 4 (original step size)
+				if (_optionsSelection == 6) {
+					_optVolumeLevel = MAX(0, _optVolumeLevel - 4);
+					_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
+					    CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+					_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
+					    CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+				}
+				return -1;
+
+			case Common::KEYCODE_RIGHT:
+				// Volume slider: increase by 4
+				if (_optionsSelection == 6) {
+					_optVolumeLevel = MIN(127, _optVolumeLevel + 4);
+					_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
+					    CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+					_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
+					    CLIP<int>(_optVolumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume));
+				}
+				return -1;
+
+			case Common::KEYCODE_RETURN:
+			case Common::KEYCODE_KP_ENTER:
+				// Toggle items 0-5, back item 7
+				switch (_optionsSelection) {
+				case 0:  // Music toggle
+					_optMusicEnabled = !_optMusicEnabled;
+					_vm->_mixer->muteSoundType(Audio::Mixer::kMusicSoundType, !_optMusicEnabled);
+					break;
+				case 1:  // SFX toggle
+					_optSfxEnabled = !_optSfxEnabled;
+					_vm->_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, !_optSfxEnabled);
+					break;
+				case 2:  // Voices toggle
+					_optVoicesEnabled = !_optVoicesEnabled;
+					_vm->_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, !_optVoicesEnabled);
+					break;
+				case 3:  // Text toggle
+					_optTextEnabled = !_optTextEnabled;
+					break;
+				case 4:  // Controls toggle
+					_optControlsFlipped = !_optControlsFlipped;
+					break;
+				case 5:  // Rapid fire toggle
+					_optRapidFire = !_optRapidFire;
+					break;
+				case 6:  // Volume (handled by left/right)
+					break;
+				case 7:  // Back
+					_optionsExitRequested = true;
+					_vm->_smushVideoShouldFinish = true;
+					return 7;
+				}
+				return _optionsSelection;
+
+			case Common::KEYCODE_ESCAPE:
+				_optionsExitRequested = true;
+				_vm->_smushVideoShouldFinish = true;
+				return -2;
+
+			default:
+				break;
+			}
+		}
+
+		if (event.type == Common::EVENT_LBUTTONDOWN) {
+			// Mouse click on items — match drawMenuItems Y positions
+			int mouseY = event.mouse.y;
+			int baseY = _optionsItemCount * -5 + 0x68;
+			for (int i = 0; i < _optionsItemCount; i++) {
+				int itemY = baseY + i * 10;
+				if (mouseY >= itemY - 1 && mouseY < itemY + 9) {
+					_optionsSelection = i;
+					// Simulate enter for this item
+					Common::Event enterEvent;
+					enterEvent.type = Common::EVENT_KEYDOWN;
+					enterEvent.kbd.keycode = Common::KEYCODE_RETURN;
+					_menuEventQueue.push(enterEvent);
+					return -1;
+				}
+			}
+		}
+	}
+	return -1;
+}
+
+void InsaneRebel2::drawOptionsOverlay(byte *renderBitmap, int pitch, int width, int height) {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer)
+		return;
+
+	// Build items array from TRS strings based on current toggle states
+	// TRS 89: title, 90/91: music, 92/93: sfx, 94/95: voices,
+	// 96/97: text, 98/99: controls, 100/101: rapid fire, 103: volume, 107: back
+	const char *items[10];  // title + up to 9 selectable
+
+	// [0] Title
+	items[0] = splayer->getString(89);
+	if (!items[0] || !items[0][0])
+		items[0] = "^f02Game Options";
+
+	// [1] Music On/Off
+	items[1] = splayer->getString(_optMusicEnabled ? 90 : 91);
+	if (!items[1] || !items[1][0])
+		items[1] = _optMusicEnabled ? "^f01^c005Music is On" : "^f01^c005Music is Off";
+
+	// [2] SFX On/Off
+	items[2] = splayer->getString(_optSfxEnabled ? 92 : 93);
+	if (!items[2] || !items[2][0])
+		items[2] = _optSfxEnabled ? "^f01^c005SFX are On" : "^f01^c005SFX are Off";
+
+	// [3] Voices On/Off
+	items[3] = splayer->getString(_optVoicesEnabled ? 94 : 95);
+	if (!items[3] || !items[3][0])
+		items[3] = _optVoicesEnabled ? "^f01^c005Voices are On" : "^f01^c005Voices are Off";
+
+	// [4] Text On/Off
+	items[4] = splayer->getString(_optTextEnabled ? 96 : 97);
+	if (!items[4] || !items[4][0])
+		items[4] = _optTextEnabled ? "^f01^c005Text is On" : "^f01^c005Text is Off";
+
+	// [5] Controls Normal/Flipped
+	items[5] = splayer->getString(_optControlsFlipped ? 99 : 98);
+	if (!items[5] || !items[5][0])
+		items[5] = _optControlsFlipped ? "^f01^c005Controls are Flipped" : "^f01^c005Controls are Normal";
+
+	// [6] Rapid Fire On/Off
+	items[6] = splayer->getString(_optRapidFire ? 100 : 101);
+	if (!items[6] || !items[6][0])
+		items[6] = _optRapidFire ? "^f01^c005Rapid Fire On" : "^f01^c005Rapid Fire Off";
+
+	// [7] Volume Level (slider) — TRS 103 = "^f01^c005Volume Level: %hd%%"
+	static char volumeBuf[64];
+	const char *volFmt = splayer->getString(103);
+	if (volFmt && volFmt[0])
+		Common::sprintf_s(volumeBuf, volFmt, (short)(_optVolumeLevel * 100 / 127));
+	else
+		Common::sprintf_s(volumeBuf, "^f01^c005Volume Level: %hd%%", (short)(_optVolumeLevel * 100 / 127));
+	items[7] = volumeBuf;
+
+	// [8] Back — TRS 107
+	items[8] = splayer->getString(107);
+	if (!items[8] || !items[8][0])
+		items[8] = "^f01^c240Return to Main Menu";
+
+	drawMenuItems(renderBitmap, pitch, width, height, items, _optionsItemCount, _optionsSelection);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index be5fc25a4ce..71d94ff7f2c 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -2301,6 +2301,14 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		return;
 	}
 
+	// Handle Options menu (FUN_004167A6)
+	bool optionsMode = (introPlaying && _gameState == kStateOptions);
+	if (optionsMode) {
+		processOptionsInput();
+		drawOptionsOverlay(renderBitmap, pitch, width, height);
+		return;
+	}
+
 	// Handle menu input and rendering if in menu mode
 	if (menuMode) {
 		// The original game uses the standard Windows arrow cursor (IDC_ARROW)


Commit: 0f3ad175bc6b864c6ff7073582c1e3d50cb6aff3
    https://github.com/scummvm/scummvm/commit/0f3ad175bc6b864c6ff7073582c1e3d50cb6aff3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:20+02:00

Commit Message:
SCUMM: RA2: Standardize declarations and comments

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_audio.cpp
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/insane/insane_rebel_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index a5fdd71cbc6..ea2dce7a674 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -539,9 +539,8 @@ InsaneRebel2::~InsaneRebel2() {
 	}
 }
 
+// notifyEvent -- EventObserver callback for global input dispatch.
 bool InsaneRebel2::notifyEvent(const Common::Event &event) {
-	// Handle global key events (ESC to skip, SPACE to pause)
-	// These work regardless of menu state
 	if (event.type == Common::EVENT_KEYDOWN) {
 		SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
@@ -771,6 +770,7 @@ const InsaneRebel2::LevelDifficultyParams InsaneRebel2::kDifficultyTable[6][17]
 	},
 };
 
+// getDifficultyParams -- Look up difficulty parameters for current level.
 InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
 	int diff = CLIP(_difficulty, 0, 5);
 	int lvIdx = 0;
@@ -800,8 +800,7 @@ InsaneRebel2::LevelDifficultyParams InsaneRebel2::getDifficultyParams() const {
 	return kDifficultyTable[diff][lvIdx];
 }
 
-// Score system implementation (FUN_0041bf8d equivalent)
-// Adds points to score and awards bonus life when crossing threshold
+// addScore -- Score system with bonus life awards (FUN_0041bf8d).
 void InsaneRebel2::addScore(int points) {
 	// Calculate bonus life threshold based on difficulty (DAT_0047a7fa)
 	// From FUN_0041bf8d:
@@ -869,15 +868,16 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 	}
 }
 
-// ======================= Pilot Data System =======================
-// Save/load pilot profiles using ScummVM's save file system (one pilot per slot).
-// Follows the Hypno/Wetlands pattern: each pilot = one ScummVM save slot.
-// Uses ScummEngine::makeSavegameName() for standard ScummVM file naming.
-// Original: FUN_00411980 (load) / FUN_00411A5D (save)
+// ---------------------------------------------------------------------------
+// Pilot Data System
+// ---------------------------------------------------------------------------
+// Save/load pilot profiles using ScummVM's save file system.
+// Original: FUN_00411980 (load) / FUN_00411A5D (save).
 
 static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
 static const uint16 kPilotSaveVersion = 2;
 
+// loadPilots -- Load all pilot profiles from save files (FUN_00411980).
 bool InsaneRebel2::loadPilots() {
 	_numPilots = 0;
 
@@ -918,6 +918,7 @@ bool InsaneRebel2::loadPilots() {
 	return _numPilots > 0;
 }
 
+// savePilots -- Save all pilot profiles to save files (FUN_00411A5D).
 bool InsaneRebel2::savePilots() {
 	bool ok = true;
 
@@ -1044,6 +1045,7 @@ int InsaneRebel2::getPilotHighestLevel() const {
 	return highest;
 }
 
+// processMouse -- Mouse input with edge detection for buttons.
 int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
 
@@ -1262,10 +1264,8 @@ void InsaneRebel2::clearBit(int n) {
 	_iactBits[n] = 0;
 }
 
-// Check if shooting is allowed based on current handler and control mode
-// From FUN_0040d836 (Handler 7): shooting only allowed when DAT_004437c0 == 2
-// From FUN_00401CCF (Handler 8): mode 4/5 disable shooting
-// From FUN_41DB5E (Handler 25): only shoot when fully uncovered (DAT_0045790a == 0)
+// isShootingAllowed -- Check control mode before spawning shots.
+// Handler 7: only mode 2. Handler 8: not mode 4/5. Handler 25: not damaged.
 bool InsaneRebel2::isShootingAllowed() {
 	// Handler 7 (Third-Person Ship): Only mode 2 allows shooting
 	// FUN_0040d836 line 141: if (DAT_004437c0 == 2) { /* spawn shots, draw crosshair */ }
@@ -1291,18 +1291,9 @@ bool InsaneRebel2::isShootingAllowed() {
 	return (_rebelHandler != 0);
 }
 
+// procSKIP -- Conditional FOBJ/PSAD skip via bit table (FUN_00423A50).
+// RA2 uses SKIP chunks to hide destroyed enemy sprites.
 void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
-	// Rebel Assault 2 uses SKIP chunks to conditionally skip the next FOBJ/PSAD chunk.
-	// The SKIP chunk contains one or two object IDs. If the bit for the object is set
-	// (i.e., the object is disabled/destroyed), skip the next chunk.
-	//
-	// This is the same mechanism as Full Throttle, but RA2 uses it for enemy objects:
-	// - When an enemy is destroyed, setBit(enemy_id) is called
-	// - SKIP chunks in the video contain the enemy ID
-	// - If the bit is set, the next FOBJ (enemy sprite) is skipped
-	// - This prevents destroyed enemy sprites from being rendered
-	//
-	// The original game's FUN_00423A50 chunk reader uses this mechanism.
 
 	int16 par1, par2;
 	_player->_skipNext = false;
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index d16c306e937..f3bced79181 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -57,7 +57,9 @@ public:
 	Common::Queue<Common::Event> _menuEventQueue;
 	bool _menuInputActive;  // True when we're capturing menu input events
 
-	// ======================= Menu System =======================
+	// ---------------------------------------------------------------------------
+	// Menu System
+	// ---------------------------------------------------------------------------
 	// Main game states (emulates retail state machine from FUN_004142BD)
 	enum GameState {
 		kStateIntro = 0,        // Stage 0: Intro/Credits sequence
@@ -119,9 +121,11 @@ public:
 	// Reset menu state for fresh start
 	void resetMenu();
 
-	// ================= Chapter Selection Screen (FUN_00415CF8) ====================
-	// This is the actual level/chapter selection screen with preview thumbnail
-	// Distinct from pilot selection (FUN_00414A41)
+	// ---------------------------------------------------------------------------
+	// Chapter Selection Screen (FUN_00415CF8)
+	// ---------------------------------------------------------------------------
+	// Actual level/chapter selection screen with preview thumbnail.
+	// Distinct from pilot selection (FUN_00414A41).
 
 	enum ChapterSelectResult {
 		kChapterSelectBack = 2,   // Return to main menu (ESC or BACK selected)
@@ -168,9 +172,11 @@ public:
 	// Password table lookup (FUN_0041BCE0)
 	Common::String getChapterPassword(int level, int difficulty);
 
-	// ================= Top Pilots Screen (FUN_00420116) ====================
-	// Shows ranked pilot scores with animated reveal, played over menu video
-	// Original: DAT_00443b58 ranking table, 0x4a-byte records, max 15 entries
+	// ---------------------------------------------------------------------------
+	// Top Pilots Screen (FUN_00420116)
+	// ---------------------------------------------------------------------------
+	// Ranked pilot scores with animated reveal, played over menu video.
+	// Original: DAT_00443b58 ranking table, 0x4a-byte records, max 15 entries.
 
 	static const int kMaxRankings = 15;
 
@@ -200,7 +206,9 @@ public:
 	int _topPilotsFrameCount;     // Animation frame counter (pilots revealed one per frame)
 	int _topPilotsMaxFrames;      // Total frames to display (120 or 240)
 
-	// ================= Options Menu (FUN_004167A6) ====================
+	// ---------------------------------------------------------------------------
+	// Options Menu (FUN_004167A6)
+	// ---------------------------------------------------------------------------
 	// TRS strings: 89 (title), 90-101 (toggle labels), 103 (volume), 107/109 (back)
 	// Original settings array at DAT_00482e20[0..4]:
 	//   [0]=auto control, [1]=music, [2]=voices, [3]=sound
@@ -222,9 +230,11 @@ public:
 	bool _optRapidFire;
 	int _optVolumeLevel;       // 0-127 (DAT_0047a804)
 
-	// ================= Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D) ===========
-	// Original: 10 pilot slots × 0x118 (280) bytes at DAT_004568A8
-	// Stored via SaveFileManager in a custom save file
+	// ---------------------------------------------------------------------------
+	// Pilot Data System (FUN_00411B9A / FUN_00411980 / FUN_00411A5D)
+	// ---------------------------------------------------------------------------
+	// 10 pilot slots × 0x118 (280) bytes at DAT_004568A8.
+	// Stored via SaveFileManager in a custom save file.
 
 	static const int kMaxPilots = 10;
 	static const int kMaxPilotNameLen = 15;
@@ -272,8 +282,10 @@ public:
 	// Get highest unlocked level for active pilot (checks damage[] < 0xFF)
 	int getPilotHighestLevel() const;
 
-	// ================= Pilot Selection Menu (FUN_00414A41) ====================
-	// This is the pilot/save selection menu (separate from chapter selection)
+	// ---------------------------------------------------------------------------
+	// Pilot Selection Menu (FUN_00414A41)
+	// ---------------------------------------------------------------------------
+	// Pilot/save selection menu (separate from chapter selection).
 
 	enum LevelSelectResult {
 		kLevelSelectBack = 0,     // Return to main menu
@@ -307,8 +319,10 @@ public:
 	// Process pilot select input - returns -1 (no action) or action code
 	int processLevelSelectInput();
 
-	// ======================= Level Loading System =======================
-	// Emulates the level handler functions FUN_00417E53 through FUN_0041BBE8
+	// ---------------------------------------------------------------------------
+	// Level Loading System
+	// ---------------------------------------------------------------------------
+	// Emulates the level handler functions FUN_00417E53 through FUN_0041BBE8.
 	// Each level has: BEG (intro), gameplay SANs, END (completion), DIE variants,
 	// RETRY, and OVER (game over) videos.
 
@@ -428,7 +442,9 @@ public:
 	int _phaseMisses;         // Accumulated misses from previous phases
 	bool _skipSectionRequested; // Debug shortcut (Shift+S): force current gameplay section to end
 
-	// =============================================================
+	// ---------------------------------------------------------------------------
+	// Resources and Fonts
+	// ---------------------------------------------------------------------------
 
 	NutRenderer *_smush_cockpitNut;
 
@@ -484,8 +500,10 @@ public:
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) override;
 
-	// ======================= Rendering Helper Functions =======================
-	// These are extracted from procPostRendering for better readability
+	// ---------------------------------------------------------------------------
+	// Rendering Helper Functions
+	// ---------------------------------------------------------------------------
+	// Extracted from procPostRendering for better readability.
 
 	// Fill status bar background area (FUN_004288c0 equivalent)
 	void renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
@@ -525,8 +543,10 @@ public:
 	// Reset enemy active flags and collision zones at frame end
 	void frameEndCleanup();
 
-	// ======================= Opcode 6 Helper Functions =======================
-	// Handler-specific setup extracted from iactRebel2Opcode6
+	// ---------------------------------------------------------------------------
+	// Opcode 6 Helper Functions
+	// ---------------------------------------------------------------------------
+	// Handler-specific setup extracted from iactRebel2Opcode6.
 
 	// Handler 8 (third-person on foot) setup - FUN_00401234 case 4
 	void opcode6Handler8Setup(int16 par3, int16 par4);
@@ -537,8 +557,10 @@ public:
 	// Calculate view offsets based on level type (lines 182-213)
 	void opcode6CalcViewOffsets();
 
-	// ======================= Opcode 8 Helper Functions =======================
-	// Resource loading extracted from iactRebel2Opcode8
+	// ---------------------------------------------------------------------------
+	// Opcode 8 Helper Functions
+	// ---------------------------------------------------------------------------
+	// Resource loading extracted from iactRebel2Opcode8.
 
 	// Load Handler 7 FLY NUT sprites from IACT data
 	bool loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4);
@@ -570,8 +592,10 @@ public:
 	// mask231: when true, color 231 is treated as transparent (legacy sprites). For laser beams set false.
 	void drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231 = true);
 
-	// ======================= Laser Texture Buffer (DAT_0047fee4) =======================
-	// Pre-rendered laser texture used by FUN_0040BBF6
+	// ---------------------------------------------------------------------------
+	// Laser Texture Buffer (DAT_0047fee4)
+	// ---------------------------------------------------------------------------
+	// Pre-rendered laser texture used by FUN_0040BBF6.
 	// Initialized from CPITIMAG.NUT sprite 0 via initLaserTexture() (FUN_0040BAB0)
 	struct LaserTexture {
 		byte *pixels;      // Pixel data (rendered from NUT sprite)
@@ -580,7 +604,9 @@ public:
 	};
 	LaserTexture _laserTexture;  // DAT_0047fee4
 
-	// ======================= Edge Blend Table (DAT_0046a7d0) =======================
+	// ---------------------------------------------------------------------------
+	// Edge Blend Table (DAT_0046a7d0)
+	// ---------------------------------------------------------------------------
 	// 256x256 lookup table used by drawEdgeHighlightLine() (FUN_410962) to compute
 	// glow colors at beam edges. For each pixel on the beam edge, the table maps
 	// [adjacent_pixel_above][adjacent_pixel_below] -> output color, producing a
@@ -678,8 +704,10 @@ public:
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
 	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
 
-	// ======================= Embedded Frame Codec Decoders =======================
-	// These functions decode different codec formats used in embedded ANIM/FOBJ data
+	// ---------------------------------------------------------------------------
+	// Embedded Frame Codec Decoders
+	// ---------------------------------------------------------------------------
+	// Decode different codec formats used in embedded ANIM/FOBJ data.
 	// Based on retail FUN_0042C590 (codec 1), FUN_0042BD60 (codec 21), etc.
 
 	// Decode codec 21/44 (Line Update) - skip/copy pairs per line
@@ -715,8 +743,10 @@ public:
 	Explosion _explosions[5];
 	void spawnExplosion(int x, int y, int objectHalfWidth);
 
-	// ======================= Collision Zone System =======================
-	// For Level 3 "pilot" ship obstacle avoidance (FUN_40E35E, FUN_40C3CC)
+	// ---------------------------------------------------------------------------
+	// Collision Zone System
+	// ---------------------------------------------------------------------------
+	// For Level 3 "pilot" ship obstacle avoidance (FUN_40E35E, FUN_40C3CC).
 	// Collision zones are quadrilaterals defined by IACT Opcode 5
 	// The player's ship position is tested against these zones each frame
 	//
@@ -783,7 +813,9 @@ public:
 	int _viewX;
 	int _viewY;
 
-	// ======================= Damage Visual Effect System =======================
+	// ---------------------------------------------------------------------------
+	// Damage Visual Effect System
+	// ---------------------------------------------------------------------------
 	// Palette flash + screen shake on taking damage.
 	// Original functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B
 	//
@@ -841,8 +873,10 @@ public:
 	int _rebelLastCounter;         // Mirrors DAT_0047ab90 (last updated counter)
 
 
-	// ======================= Handler 0x26 Turret Shot System =======================
-	// Based on FUN_40AD63 disassembly - Turret laser rendering
+	// ---------------------------------------------------------------------------
+	// Handler 0x26 Turret Shot System
+	// ---------------------------------------------------------------------------
+	// Based on FUN_40AD63 disassembly - Turret laser rendering.
 	// DAT_0044367a[2]: Shot duration counter (0=inactive)
 	// DAT_0044367e[2]: Target X position
 	// DAT_00443682[2]: Target Y position
@@ -860,8 +894,10 @@ public:
 	TurretShot _turretShots[2];
 	int16 _turretShotSeqCounter;  // DAT_0047fe94 - global sequence counter
 
-	// ======================= Handler 8 Vehicle Shot System =======================
-	// Based on FUN_402ED0 disassembly - Vehicle laser rendering
+	// ---------------------------------------------------------------------------
+	// Handler 8 Vehicle Shot System
+	// ---------------------------------------------------------------------------
+	// Based on FUN_402ED0 disassembly - Vehicle laser rendering.
 	// DAT_0043e00a[2]: Shot duration counter
 	// DAT_0043e00e[2]: Target X position
 	// DAT_0043e012[2]: Target Y position
@@ -874,8 +910,10 @@ public:
 	};
 	VehicleShot _vehicleShots[2];
 
-	// ======================= Handler 7 Third-Person Ship Shot System =======================
-	// Based on FUN_40FADF disassembly - Third-Person Ship laser rendering
+	// ---------------------------------------------------------------------------
+	// Handler 7 Third-Person Ship Shot System
+	// ---------------------------------------------------------------------------
+	// Based on FUN_40FADF disassembly - Third-Person Ship laser rendering.
 	// DAT_00443750[2]: Shot duration counter
 	// DAT_00443754[2]: Target X position
 	// DAT_00443758[2]: Target Y position
@@ -928,7 +966,9 @@ public:
 	// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
 	int16 getShotMaxDuration();
 
-	// ======================= Handler 8 Ship System =======================
+	// ---------------------------------------------------------------------------
+	// Handler 8 Ship System
+	// ---------------------------------------------------------------------------
 	// For third-person on foot missions (Level 2, 11), the player controls Rookie One
 	// that can turn in different directions. The ship sprite comes from
 	// NUT files loaded via IACT opcode 8.
@@ -1003,7 +1043,9 @@ public:
 	// Helper to load a NUT file from IACT chunk data
 	NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
 
-	// ======================= Handler 7 FLY Ship System =======================
+	// ---------------------------------------------------------------------------
+	// Handler 7 FLY Ship System
+	// ---------------------------------------------------------------------------
 	// For third-person ship missions (Level 3, etc.), Handler 7 uses a 35-frame
 	// direction-based ship sprite system. The ship visually banks and turns
 	// based on player position using a 5x7 grid of sprites.
@@ -1043,7 +1085,9 @@ public:
 	int16 _viewShift;                // DAT_00443710 - Clamped smoothed velocity for view transform
 	bool _facingRight;               // DAT_0047ab8c - Ship facing right of center
 
-	// ======================= Handler 25 (0x19) GRD Ship System =======================
+	// ---------------------------------------------------------------------------
+	// Handler 25 (0x19) GRD Ship System
+	// ---------------------------------------------------------------------------
 	// For mixed mode missions (Level 2 speeder bike, etc.), Handler 25 uses GRD NUT
 	// sprites loaded via IACT opcode 8. The ship is rendered based on DAT_00457900 mode.
 	//
@@ -1080,7 +1124,9 @@ public:
 	void renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height);
 	void renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height);
 
-	// ======================= Handler 0x26 Turret HUD Overlays =======================
+	// ---------------------------------------------------------------------------
+	// Handler 0x26 Turret HUD Overlays
+	// ---------------------------------------------------------------------------
 	// For turret missions (Level 1, etc.), Handler 0x26 uses NUT-based HUD overlays
 	// loaded via IACT opcode 8. These contain animated cockpit panel elements.
 	//
@@ -1101,8 +1147,10 @@ public:
 	/* Difficulty Level (0-5, from pilot menu; maps directly to table rows) */
 	int _difficulty;
 
-	// ======================= Per-Level Difficulty Parameters =======================
-	// Extracted from RA2WIN95.EXE at VA 0x47e0f0
+	// ---------------------------------------------------------------------------
+	// Per-Level Difficulty Parameters
+	// ---------------------------------------------------------------------------
+	// Extracted from RA2WIN95.EXE at VA 0x47e0f0.
 	// 2D table indexed by difficulty (0-5) × level type (0-16)
 	// Original indexing: &DAT_0047e0f0 + chapter * 0x242 + levelType * 0x22
 	// Level type (_rebelLevelType) is set by IACT opcode 6 par3
@@ -1147,16 +1195,20 @@ public:
 	// Render score text to HUD (called from procPostRendering)
 	void renderScoreHUD(byte *renderBitmap, int pitch, int width, int height, int statusBarY);
 
-	// ======================= Pause Overlay =======================
-	// Show pause overlay with dimming effect and "PAUSED" text
+	// ---------------------------------------------------------------------------
+	// Pause Overlay
+	// ---------------------------------------------------------------------------
+	// Show pause overlay with dimming effect and "PAUSED" text.
 	// Emulates FUN_405A21 pause rendering (lines 242-305)
 	void showPauseOverlay();
 
 	// Target lock timer (DAT_00443676) - set to 7 when crosshair is over enemy
 	int _targetLockTimer;
 
-	// ========== Audio Handling ==========
-	// Rebel Assault 2 doesn't use iMUSE - audio is handled directly here
+	// ---------------------------------------------------------------------------
+	// Audio Handling
+	// ---------------------------------------------------------------------------
+	// RA2 doesn't use iMUSE -- audio is handled directly through the mixer.
 
 	static const int kRA2MaxAudioTracks = 4;
 
@@ -1178,7 +1230,9 @@ public:
 	// Queue audio data for playback on a specific track
 	void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
 
-	// ========== Sound Effects (SAD files) ==========
+	// ---------------------------------------------------------------------------
+	// Sound Effects (SAD files)
+	// ---------------------------------------------------------------------------
 	// 8 standalone SAUD files in SYSTM/ loaded at init for one-shot SFX.
 	// Slot mapping (from FUN_0042a3b0 init):
 	//   0=BLAST.SAD   1=CRASH.SAD   2=EXPLODE.SAD  3=ALERT.SAD
@@ -1200,7 +1254,9 @@ public:
 	// slot: 0-7 (SAD file index), volume: 0-127, pan: -127..+127
 	void playSfx(int slot, int volume, int pan);
 
-	// ========== Auxiliary Sound Buffers ==========
+	// ---------------------------------------------------------------------------
+	// Auxiliary Sound Buffers
+	// ---------------------------------------------------------------------------
 	// 4 pre-allocated buffers (30000 bytes each) loaded from IACT stream data.
 	// Original: DAT_00480308[0..3], loaded via FUN_004118df, played via FUN_00411931.
 	// Used for embedded sound effects (e.g., soldier death sounds in handler 8 levels).
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel_audio.cpp
index 7b971f8eea1..a063144f40b 100644
--- a/engines/scumm/insane/insane_rebel_audio.cpp
+++ b/engines/scumm/insane/insane_rebel_audio.cpp
@@ -33,9 +33,12 @@
 
 namespace Scumm {
 
-// ========== Audio Handling for Rebel Assault 2 ==========
-// RA2 doesn't use iMUSE - we handle audio directly through the mixer
+// ---------------------------------------------------------------------------
+// Audio Handling
+// ---------------------------------------------------------------------------
+// RA2 doesn't use iMUSE -- audio is handled directly through the mixer.
 
+// initAudio -- Initialize audio system for RA2.
 void InsaneRebel2::initAudio(int sampleRate) {
 	_audioSampleRate = sampleRate;
 	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
@@ -44,6 +47,7 @@ void InsaneRebel2::initAudio(int sampleRate) {
 	}
 }
 
+// terminateAudio -- Stop all tracks and release audio streams.
 void InsaneRebel2::terminateAudio() {
 	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
 		if (_audioTrackActive[i]) {
@@ -57,6 +61,8 @@ void InsaneRebel2::terminateAudio() {
 	}
 }
 
+// queueAudioData -- Queue raw PCM data for playback on a track.
+// Creates the queuing stream on first use. RA2 audio is 8-bit unsigned mono.
 void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
 	if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
 		debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
@@ -95,6 +101,13 @@ void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int vol
 	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
 }
 
+//
+// processAudioFrame -- Per-frame audio dispatch (replaces iMUSE path)
+//
+// Iterates SmushPlayer audio tracks, handles FADING->PLAYING transitions,
+// and feeds PCM data through queueAudioData. Called from SmushPlayer when
+// iMUSE is null.
+//
 void InsaneRebel2::processAudioFrame(int16 feedSize) {
 	if (!_player) {
 		return;
@@ -256,9 +269,11 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
 	}
 }
 
-// ========== Sound Effects (SAD files) ==========
-// Loads standalone SAUD files from SYSTM/ for one-shot SFX playback.
-// Original game loads these via FUN_0042a3b0 at init into DAT_00456888[0..7].
+// ---------------------------------------------------------------------------
+// Sound Effects (SAD files)
+// ---------------------------------------------------------------------------
+// Standalone SAUD files from SYSTM/ loaded at init for one-shot SFX.
+// Original: FUN_0042a3b0 loads into DAT_00456888[0..7].
 
 static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
 	"SYSTM/BLAST.SAD",    // 0 - Player laser fire
@@ -271,6 +286,7 @@ static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
 	"SYSTM/TBLAST.SAD"    // 7 - TIE blast
 };
 
+// loadSfx -- Load all SAD files from SYSTM/ directory (FUN_0042a3b0).
 void InsaneRebel2::loadSfx() {
 	for (int i = 0; i < kRA2NumSfx; i++) {
 		ScummFile *file = _vm->instantiateScummFile();
@@ -333,6 +349,7 @@ void InsaneRebel2::loadSfx() {
 	}
 }
 
+// freeSfx -- Free all loaded SFX data and auxiliary buffers.
 void InsaneRebel2::freeSfx() {
 	for (int i = 0; i < kRA2NumSfx; i++) {
 		// Stop any playing SFX on this slot
@@ -349,6 +366,7 @@ void InsaneRebel2::freeSfx() {
 	}
 }
 
+// playSfx -- Play a one-shot sound effect (8-bit unsigned mono, 11025 Hz).
 void InsaneRebel2::playSfx(int slot, int volume, int pan) {
 	if (slot < 0 || slot >= kRA2NumSfx || !_sfxData[slot] || _sfxSize[slot] == 0) {
 		return;
@@ -377,6 +395,7 @@ void InsaneRebel2::playSfx(int slot, int volume, int pan) {
 	debug(5, "InsaneRebel2::playSfx: slot=%d vol=%d pan=%d size=%d", slot, volume, pan, _sfxSize[slot]);
 }
 
+// loadAuxSfx -- Load sound data into auxiliary buffer (FUN_004118df).
 void InsaneRebel2::loadAuxSfx(int buffer, const byte *data, uint32 size) {
 	if (buffer < 0 || buffer >= kRA2NumAuxSfx || !data || size == 0) {
 		return;
@@ -393,6 +412,8 @@ void InsaneRebel2::loadAuxSfx(int buffer, const byte *data, uint32 size) {
 	debug(5, "InsaneRebel2::loadAuxSfx: buffer=%d size=%d", buffer, size);
 }
 
+// playAuxSfx -- Play from auxiliary buffer (FUN_00411931).
+// Handles both raw PCM and SAUD-wrapped data.
 void InsaneRebel2::playAuxSfx(int buffer, int volume, int pan) {
 	if (buffer < 0 || buffer >= kRA2NumAuxSfx || !_auxSfxData[buffer] || _auxSfxSize[buffer] == 0) {
 		return;
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 1ecd8052fa1..104d64dc9d1 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -35,8 +35,13 @@ namespace Scumm {
 // External codec functions from codec1.cpp
 extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
+//
+// procPreRendering -- Pre-frame setup: background restore and corridor overlays.
+//
+// Restores Level 2 background before FOBJ decoding (Handler 8) and handles
+// Handler 25 corridor overlay positioning.
+//
 void InsaneRebel2::procPreRendering(byte *renderBitmap) {
-	// Call base class implementation first (handles Full Throttle state machine)
 	Insane::procPreRendering(renderBitmap);
 
 	// Reset opcode 6 init flag at the start of each new video.
@@ -93,10 +98,10 @@ void InsaneRebel2::procPreRendering(byte *renderBitmap) {
 	}
 }
 
+// procIACT -- Main IACT chunk dispatcher (overrides Insane::procIACT).
 void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 					  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 					  int16 par1, int16 par2, int16 par3, int16 par4) {
-	// Debug: Log all IACT opcodes
 	debug("Rebel2 IACT: opcode=%d par2=%d par3=%d par4=%d gameState=%d sceneId=%d",
 		par1, par2, par3, par4, _gameState, _currSceneId);
 
@@ -162,7 +167,7 @@ void InsaneRebel2::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
 		iactRebel2Scene1(renderBitmap, codecparam, setupsan12, setupsan13, b, size, flags, par1, par2, par3, par4);
 }
 
-
+// iactRebel2Scene1 -- Scene 1 IACT dispatcher (FUN_4028C5 / FUN_4033CF).
 void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 				  int16 par1, int16 par2, int16 par3, int16 par4) {
@@ -290,6 +295,8 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 		debug("Rebel2 IACT: Unknown Opcode %d (par2=%d par3=%d par4=%d)", par1, par2, par3, par4);
 	}
 }
+
+// iactRebel2Opcode2 -- Link table and state setup (FUN_00407fcb).
 void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Handle IACT opcode 2 subcases based on par3 (type). Mirrors FUN_00407fcb behavior where relevant.
 	// Keep existing linking behavior (par3 == 4) for compatibility.
@@ -400,6 +407,8 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 		debug("Rebel2 IACT Opcode2: Unhandled par3=%d par4=%d", par3, par4);
 	}
 }
+
+// iactRebel2Opcode3 -- Damage and hit counter processing (FUN_4092D9 / FUN_40E35E / FUN_401234).
 void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// IACT opcode 3 — damage and hit counter processing.
 	// Based on FUN_4092D9 (Handler 0x26), FUN_40E35E (Handler 7), FUN_401234 (Handler 8).
@@ -552,6 +561,13 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 	}
 }
 
+//
+// iactRebel2Opcode6 -- Level setup / mode switch (FUN_41CADB case 4)
+//
+// Per-wave initialization: clears bit table, resets link tables, configures
+// handler mode (ship/turret/corridor), and loads collision zones. Called once
+// per wave video on frame 0.
+//
 void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
 	// Opcode 6: Level setup / mode switch
 	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
@@ -1464,16 +1480,19 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 	}
 }
 
+//
+// iactRebel2Opcode8 -- HUD/Ship resource loading (FUN_0040c3cc / FUN_00401234 / FUN_00407fcb)
+//
+// Decodes embedded ANIM data from IACT chunks and dispatches to
+// handler-specific loaders for NUT sprites, HUD overlays, and backgrounds.
+//
+// Handler-specific routing:
+//   Handler 7  (FLY):  FLY NUT sprites via par4 (1, 2, 3, 11)
+//   Handler 8  (POV):  POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
+//   Handler 0x26 (turret): Turret HUD NUT via par3 (1-4)
+//   Handler 0x19: Mixed turret mode, similar to 0x26
+//
 void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
-	// Opcode 8: HUD/Ship resource loading
-	// Dispatches to handler-specific loading functions based on current handler and parameters.
-	//
-	// Handler-specific routing (based on retail disassembly):
-	//   Handler 7 (FUN_0040c3cc):  FLY NUT sprites via par4 (1, 2, 3, 11)
-	//   Handler 8 (FUN_00401234):  POV NUT sprites via par3 (1, 3, 6, 7) or background via par4=5
-	//   Handler 0x26 (FUN_00407fcb): Turret HUD NUT via par3 (1-4)
-	//   Handler 0x19: Mixed turret mode, similar to 0x26
-	//
 	// Sound loading: par3 in range 21-47
 
 	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
@@ -1706,6 +1725,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	b.seek(startPos);
 }
 
+// loadHandler25ShotOriginTable -- Parse shot origin coordinate pairs from IACT payload.
 bool InsaneRebel2::loadHandler25ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining) {
 	// IACT layout at this point:
 	// - stream is positioned after par1..par4 (8 bytes consumed by caller)
@@ -1793,10 +1813,13 @@ bool InsaneRebel2::loadHandler25ShotOriginTable(Common::SeekableReadStream &b, i
 	return true;
 }
 
-// ======================= Opcode 8 Helper Functions =======================
-// These helper functions are extracted from the original monolithic iactRebel2Opcode8
-// to improve code readability and match the retail FUN_* function structure.
+// ---------------------------------------------------------------------------
+// Opcode 8 Helper Functions
+// ---------------------------------------------------------------------------
+// Extracted from the original monolithic iactRebel2Opcode8 to match
+// the retail FUN_* function structure.
 
+// loadHandler7FlySprites -- Handler 7 FLY NUT loading (FUN_0040c3cc case 6).
 bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 remaining, int16 par4) {
 	// Handler 7 FLY NUT loading - FUN_0040c3cc case 6 (opcode 8)
 	// IACT structure after par1-par4 (we're at offset +8):
@@ -1908,6 +1931,7 @@ bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 r
 	return assigned;
 }
 
+// loadTurretHudOverlay -- Handler 0x26/0x19 turret HUD loading (FUN_00407fcb case 8).
 bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3) {
 	// Handler 0x26/0x19 turret HUD overlay loading - FUN_00407fcb case 8
 	// Resolution-dependent loading:
@@ -1953,6 +1977,7 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
 	return true;
 }
 
+// loadHandler8ShipSprites -- Handler 8 POV NUT loading (FUN_00401234 case 6).
 bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par4) {
 	// Handler 8 ship POV NUT loading - FUN_00401234 case 6 (opcode 8)
 	// par4 values (from IACT data offset +6, NOT par3 which is always 0):
@@ -2005,6 +2030,7 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
 	return true;
 }
 
+// loadHandler25GrdSprites -- Handler 25 GRD NUT loading (FUN_0041cadb case 6).
 bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par4) {
 	// Handler 25 GRD ship NUT loading - FUN_0041cadb case 6 (opcode 8)
 	// par4 values (from IACT data offset +6):
@@ -2049,6 +2075,7 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
 	return true;
 }
 
+// loadLevel2Background -- Decode Level 2 background from embedded ANIM (FUN_00401234 case 5).
 bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *renderBitmap) {
 	// Level 2 background loading from embedded ANIM - FUN_00401234 case 5
 	// par4=5 contains the background image embedded as ANIM with FOBJ codec 3
@@ -2149,6 +2176,13 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
 	return foundBackground;
 }
 
+//
+// iactRebel2Opcode9 -- Text/subtitle display via IACT chunk
+//
+// Handles inline text in IACT chunks. Most RA2 subtitles use TRES chunks
+// (handled by SmushPlayer::handleTextResource); this opcode is less common.
+// Supports multi-line wrapping, centered/shadowed text, and clip regions.
+//
 void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Opcode 9: Text/Subtitle Display via IACT chunk
 	// Note: Most RA2 subtitles use TRES chunks handled by SmushPlayer::handleTextResource()
@@ -2370,6 +2404,7 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
 		posX, posY, textFlags, clipX, clipY, clipW, clipH);
 }
 
+// enemyUpdate -- Opcode 4: update enemy position and state (FUN_004028C5 / FUN_0041E7C2).
 void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
 	// Opcode 4: Enemy position update
 	// Read 5 shorts from the stream (offset +8 through +16)
@@ -2427,6 +2462,7 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 	}
 }
 
+// init_enemyStruct -- Create and append a new enemy entry.
 void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
 	enemy e;
 	e.id = id;
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 23acf9645da..1d5709f9f1a 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -29,34 +29,32 @@
 
 namespace Scumm {
 
-// ======================= Level Loading System =======================
-// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8
+// ---------------------------------------------------------------------------
+// Level Loading System
+// ---------------------------------------------------------------------------
+// Emulates the level handler functions from FUN_00417E53 through FUN_0041BBE8.
 // Based on disassembly analysis of the retail Rebel Assault 2 executable.
 
 Common::String InsaneRebel2::getLevelDir(int levelId) {
-	// Returns directory name like "LEV01" for level 1
 	return Common::String::format("LEV%02d", levelId);
 }
 
 Common::String InsaneRebel2::getLevelPrefix(int levelId) {
-	// Returns file prefix like "01" for level 1
 	return Common::String::format("%02d", levelId);
 }
 
+//
+// playIntroSequence -- Intro sequence (FUN_004142BD case 0)
+//
+// Original flow:
+//   - If 'f','o','x' keys all held: play CREDITS/O_OPEN_C.SAN (Fox logo easter egg)
+//   - If 'b','o','t' keys all held: play CREDITS/O_OPEN_D.SAN (LucasArts logo)
+//   - Else: play OPEN/O_OPEN_A.SAN (main intro)
+//   - If DAT_0047ab45 || DAT_0047ab47: play OPEN/O_OPEN_B.SAN (additional intro)
+//
+// We skip the easter eggs and play both O_OPEN_A + O_OPEN_B unconditionally.
+//
 void InsaneRebel2::playIntroSequence() {
-	// Emulates case 0 in FUN_004142BD
-	//
-	// Original flow:
-	//   - If 'f','o','x' keys all held: play CREDITS/O_OPEN_C.SAN (Fox logo easter egg)
-	//   - If 'b','o','t' keys all held: play CREDITS/O_OPEN_D.SAN (LucasArts logo)
-	//     INSTEAD of the normal intro
-	//   - Else: play OPEN/O_OPEN_A.SAN (main intro - normal path)
-	//   - If DAT_0047ab45 || DAT_0047ab47: play OPEN/O_OPEN_B.SAN (additional intro)
-	//   - Fade out over 10 frames, clear palette, show top pilots, then -> main menu
-	//
-	// We skip the Fox/LucasArts easter eggs (require real-time key state during boot)
-	// and play both O_OPEN_A + O_OPEN_B unconditionally.
-
 	debug("Rebel2: Playing intro sequence");
 
 	_gameState = kStateIntro;
@@ -81,9 +79,8 @@ void InsaneRebel2::playIntroSequence() {
 	splayer->play("OPEN/O_OPEN_B.SAN", 12);
 }
 
+// playMissionBriefing -- Mission briefing screen (FUN_00415CF8).
 void InsaneRebel2::playMissionBriefing() {
-	// Emulates FUN_00415CF8 (partial - just the video)
-	// Plays OPEN/O_LEVEL.SAN which shows the mission briefing screen
 
 	debug("Rebel2: Playing mission briefing");
 
@@ -92,15 +89,9 @@ void InsaneRebel2::playMissionBriefing() {
 	splayer->play("OPEN/O_LEVEL.SAN", 12);
 }
 
+// playCinematic -- Play a cinematic/cutscene video.
+// Resets handler to 0 (no HUD) and sets flags to 0x28 (cinematic + buffer preserve).
 void InsaneRebel2::playCinematic(const char *filename) {
-	// Play a cinematic/cutscene video with proper intro mode setup
-	// This helper ensures:
-	// 1. Handler is reset to 0 (no HUD, no shooting)
-	// 2. Video flags are set to 0x28 (cinematic with buffer preserve)
-	//
-	// Original: All video wrapper functions (FUN_00417168, FUN_004171c5,
-	// FUN_00417ab2, FUN_00417327) add | 8 to the base flags before calling
-	// FUN_0041f4d0, so the 0x08 bit (preserve buffer) is always set.
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during cinematics
 
@@ -109,13 +100,10 @@ void InsaneRebel2::playCinematic(const char *filename) {
 	splayer->play(filename, 12);
 }
 
+// playVideoWithText -- Video with progressive text overlay (FUN_004171c5).
+// Text is progressively revealed during [fadeInFrame, fadeOutFrame).
 void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX, int textY,
                                      int fadeInFrame, int fadeOutFrame) {
-	// Emulates FUN_004171c5: plays video with progressive text overlay
-	// Text string loaded from GAME.TRS via getString(textID)
-	// During frame range [fadeInFrame, fadeOutFrame):
-	//   displayLength = currentFrame + 10 - fadeInFrame, capped at 0xBE (190) chars
-	//   Text rendered at (textX, textY) using FUN_004341a0
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;
@@ -135,12 +123,9 @@ void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX
 	_textOverlayActive = false;
 }
 
+// playLevelBegin -- Level beginning cinematic (LEVXX/XXBEG.SAN).
+// Uses per-level text overlay parameters from GAME.TRS via FUN_004171c5.
 void InsaneRebel2::playLevelBegin(int levelId) {
-	// Play the level beginning cinematic (LEVXX/XXBEG.SAN)
-	// Emulates FUN_004171c5 call in each level handler
-	//
-	// Per-level text overlay parameters from original disassembly:
-	// All levels use FUN_004171c5 with a chapter title overlay from GAME.TRS.
 
 	struct TextOverlayParams {
 		int textID;      // TRS string ID (-1 = no text overlay)
@@ -187,14 +172,13 @@ void InsaneRebel2::playLevelBegin(int levelId) {
 	}
 }
 
+//
+// playLevelGameplay -- Main gameplay video(s) for a level
+//
+// Returns true if level completed (shield > 0), false if died.
+// Structures vary by level: single SAN, multi-part subdirs, or two phases.
+//
 bool InsaneRebel2::playLevelGameplay(int levelId) {
-	// Play the main gameplay video(s) for a level
-	// Returns true if level completed (damage < 0xff), false if died
-	//
-	// Different levels have different gameplay structures:
-	// - Level 1, 4, 5: Single gameplay SAN (XXPXX.SAN or XXPLAY.SAN)
-	// - Level 2: Multiple parts with subdirectories (P1/, P2/, P3/)
-	// - Level 3, 6: Two gameplay phases (XXPLAY1.SAN, XXPLAY2.SAN)
 
 	Common::String dir = getLevelDir(levelId);
 	Common::String prefix = getLevelPrefix(levelId);
@@ -377,9 +361,8 @@ bool InsaneRebel2::playLevelGameplay(int levelId) {
 	return (_playerShield > 0);
 }
 
+// playLevelEnd -- Level completion video (FUN_00417327).
 void InsaneRebel2::playLevelEnd(int levelId) {
-	// Play level completion video (LEVXX/XXEND.SAN)
-	// Emulates FUN_00417327 call
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during end cinematic
@@ -396,10 +379,8 @@ void InsaneRebel2::playLevelEnd(int levelId) {
 	splayer->play(filename.c_str(), 12);
 }
 
+// playLevelDeath -- Death video (LEVXX/XXDIE_X.SAN, FUN_00417168).
 void InsaneRebel2::playLevelDeath(int levelId) {
-	// Play death video (LEVXX/XXDIE_X.SAN)
-	// The variant depends on the frame where player died
-	// For simplicity, we'll play the A variant
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
@@ -423,9 +404,8 @@ void InsaneRebel2::playLevelDeath(int levelId) {
 	splayer->play(filename.c_str(), 12);
 }
 
+// playLevelRetry -- Retry prompt video (LEVXX/XXRETRY.SAN, FUN_00417168).
 void InsaneRebel2::playLevelRetry(int levelId) {
-	// Play retry prompt video (LEVXX/XXRETRY.SAN)
-	// Reset handler state for the retry cinematic
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
@@ -442,9 +422,8 @@ void InsaneRebel2::playLevelRetry(int levelId) {
 	splayer->play(filename.c_str(), 12);
 }
 
+// playLevelGameOver -- Game over video (FUN_00417ab2).
 void InsaneRebel2::playLevelGameOver(int levelId) {
-	// Play game over video (LEVXX/XXOVER.SAN)
-	// Emulates FUN_00417ab2 call
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during game over cinematic
@@ -461,9 +440,8 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
 	splayer->play(filename.c_str(), 12);
 }
 
+// playCreditsSequence -- End credits (OPEN/O_CREDIT.SAN).
 void InsaneRebel2::playCreditsSequence() {
-	// Play the end credits (OPEN/O_CREDIT.SAN)
-	// Individual credits are in CREDITS/CRED_XX.SAN
 
 	debug("Rebel2: Playing credits");
 
@@ -472,9 +450,8 @@ void InsaneRebel2::playCreditsSequence() {
 	splayer->play("OPEN/O_CREDIT.SAN", 12);
 }
 
+// runLevel -- Main level dispatcher, calls per-level handlers.
 int InsaneRebel2::runLevel(int levelId) {
-	// Main level dispatcher - calls per-level handlers
-	// Each level handler emulates its retail counterpart (FUN_00417E53 etc.)
 
 	debug("Rebel2: Starting level %d", levelId);
 
@@ -546,20 +523,23 @@ int InsaneRebel2::runLevel(int levelId) {
 	}
 }
 
-// =============================================================================
-// Helper functions
-// =============================================================================
+// ---------------------------------------------------------------------------
+// Helper Functions
+// ---------------------------------------------------------------------------
 
+// Emulates FUN_004233a0.
 int InsaneRebel2::getRandomVariant(int max) {
-	// Emulates FUN_004233a0 - returns random number 0 to max-1
 	return _vm->_rnd.getRandomNumber(max - 1);
 }
 
+//
+// selectDeathVideoVariant -- Frame-based death video selection
+//
+// Returns variant suffix ("A", "B", "C", etc.) based on level, phase,
+// and the frame where the player died. Emulates the per-level frame
+// threshold tables in the retail level handlers.
+//
 Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int frame) {
-	// Select death video variant based on level, phase, and death frame
-	// Emulates the frame-based death video selection in retail level handlers
-	//
-	// Returns variant suffix: "A", "B", "C", etc.
 
 	switch (levelId) {
 	case 1:
@@ -725,8 +705,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 	}
 }
 
+// playLevelDeathVariant -- Death video with variant selection.
 void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
-	// Play death video with proper variant selection
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
@@ -751,8 +731,8 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 	splayer->play(filename.c_str(), 12);
 }
 
+// playLevelRetryVariant -- Phase-specific retry video.
 void InsaneRebel2::playLevelRetryVariant(int levelId, int phase) {
-	// Play retry video - phase-specific for multi-phase levels
 
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // Reset for retry - will be set by IACT opcode 6 if needed
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 0ea75e2821c..f86c8557a6c 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -37,8 +37,10 @@
 
 namespace Scumm {
 
-// ======================= Menu System Implementation =======================
-// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8
+// ---------------------------------------------------------------------------
+// Menu System Implementation
+// ---------------------------------------------------------------------------
+// Emulates retail menu system from FUN_004147B2 and FUN_0041FDC8.
 
 void InsaneRebel2::resetMenu() {
 	_menuSelection = 0;
@@ -47,9 +49,7 @@ void InsaneRebel2::resetMenu() {
 	_menuSelectionConfirmed = false;
 }
 
-// Unlock all chapters for testing
-// Emulates the debug mode from original FUN_00415CF8 (lines 60-71)
-// where DAT_0047ab34 == 'd' enables level unlock via special codes
+// unlockAllChapters -- Debug mode unlock (FUN_00415CF8).
 void InsaneRebel2::unlockAllChapters() {
 	debug("Rebel2: Unlocking all chapters for testing");
 	for (int i = 0; i < 16; i++) {
@@ -58,16 +58,9 @@ void InsaneRebel2::unlockAllChapters() {
 	}
 }
 
+// getRandomMenuVideo -- Select random menu video variant (FUN_0041FDC8).
+// Always uses O_MENU_X.SAN (A-O) instead of audio-only O_MENU.SAN.
 Common::String InsaneRebel2::getRandomMenuVideo() {
-	// Emulates FUN_0041FDC8 - selects random menu video variant
-	//
-	// NOTE: The original game plays O_MENU.SAN when no progress flags are set,
-	// but that file contains ONLY audio (no FOBJ video frames). The O_MENU_X.SAN
-	// variants (A through O) contain actual 320x200 background images in Frame 0.
-	//
-	// We ALWAYS use a random variant to ensure a proper background is displayed.
-	// The original behavior of showing O_MENU.SAN (audio-only) would result in
-	// a black/undefined background which doesn't match the intended experience.
 
 	// Select random variant (0-14 maps to A-O), ensuring different from last
 	int variant;
@@ -82,18 +75,14 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
 	return Common::String::format("OPEN/O_MENU_%c.SAN", letter);
 }
 
+//
+// processMenuInput -- Menu input handling (FUN_0041f5ae)
+//
+// Returns -1 (no action) or 0-4 (menu item selected).
+// Events captured by notifyEvent() before ScummEngine consumes them.
+// Keyboard: Up/Down navigate, Enter confirms. Mouse: Y maps to selection.
+//
 int InsaneRebel2::processMenuInput() {
-	// Emulates FUN_0041f5ae menu input handling
-	// Returns: -1 = no action, 0-4 = menu item selected
-	//
-	// Events are captured by notifyEvent() (EventObserver) which runs before
-	// ScummEngine::parseEvents() consumes them. This ensures we don't miss
-	// any input events even though we only process them on video frames.
-	//
-	// From FUN_0041f5ae disassembly:
-	// - Keyboard: Up/Down arrows navigate, Enter confirms
-	// - Mouse mode (DAT_0047a806 == 1): Y position maps to selection
-	// - Key codes: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b
 
 	int result = -1;
 
@@ -208,13 +197,12 @@ int InsaneRebel2::processMenuInput() {
 	return result;
 }
 
+//
+// drawMenuItems -- Shared menu item renderer (FUN_0041f5ae)
+//
 void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int height,
                                   const char **items, int numItems, int selection,
                                   bool leftAligned) {
-	// =====================================================================
-	// Shared menu renderer - Emulates FUN_0041f5ae
-	// Address: 0x41F5AE
-	// =====================================================================
 	//
 	// items[0] = title string, items[1..numItems] = selectable items
 	// numItems = number of selectable items (FUN_0041f5ae param_3)
@@ -431,8 +419,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	}
 }
 
-// Format-code-aware string width calculation
-// Fixed-width codes: ^fNN (2-digit font), ^cNNN (3-digit color), ^^ (literal ^)
+// getMenuStringWidth -- Format-code-aware string width (^fNN, ^cNNN, ^^).
 int InsaneRebel2::getMenuStringWidth(const char *str) const {
 	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -561,9 +548,11 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 	drawMenuItems(renderBitmap, pitch, width, height, menuItems, 7, _menuSelection);
 }
 
-// ======================= Pause Overlay =======================
-// Emulates FUN_405A21 pause rendering (lines 242-305)
-// Creates a dimmed overlay effect and displays "PAUSED" text
+// ---------------------------------------------------------------------------
+// Pause Overlay
+// ---------------------------------------------------------------------------
+
+// showPauseOverlay -- Dimmed overlay with "PAUSED" text (FUN_405A21).
 void InsaneRebel2::showPauseOverlay() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	if (!splayer) {
@@ -728,13 +717,9 @@ void InsaneRebel2::showPauseOverlay() {
 	debug("showPauseOverlay: Overlay displayed");
 }
 
+// runMainMenu -- Main menu loop (FUN_004147B2).
+// Returns kMenuNewGame, kMenuContinue, kMenuCredits, or 0 (quit).
 int InsaneRebel2::runMainMenu() {
-	// Main menu loop - emulates FUN_004147B2
-	// Returns:
-	//   kMenuNewGame (2) = Start new game
-	//   kMenuContinue (4) = Continue game (level select)
-	//   kMenuCredits (1) = Show credits then return to menu
-	//   0 = Quit game
 
 	debug("Rebel2: Entering main menu");
 
@@ -863,16 +848,13 @@ int InsaneRebel2::runMainMenu() {
 	return 0;
 }
 
-// ==================== Chapter Selection Screen ====================
-// Emulates FUN_00415CF8 - Chapter selection with preview and password input
-// This is the actual level/chapter selection that players see after pilot select
+// ---------------------------------------------------------------------------
+// Chapter Selection Screen
+// ---------------------------------------------------------------------------
 
+// runChapterSelect -- Chapter selection with preview (FUN_00415CF8).
+// Returns kChapterSelectPlay, kChapterSelectBack, or kChapterSelectQuit.
 int InsaneRebel2::runChapterSelect() {
-	// Chapter selection screen loop - emulates FUN_00415CF8
-	// Returns:
-	//   kChapterSelectPlay (5) = Play selected chapter
-	//   kChapterSelectBack (2) = Return to main menu (ESC or BACK)
-	//   kChapterSelectQuit (0) = Quit game
 
 	debug("Rebel2: Entering chapter selection (FUN_00415CF8)");
 
@@ -1445,16 +1427,14 @@ void InsaneRebel2::drawChapterSelectOverlay(byte *renderBitmap, int pitch, int w
 	drawChapterInfoLine(renderBitmap, pitch, width, height);
 }
 
-// ==================== Pilot Selection Menu (FUN_00414A41) ====================
-// Emulates FUN_00414A41 - Pilot/save selection menu
-// This appears before chapter selection. All options go to chapter selection except MAIN MENU.
+// ---------------------------------------------------------------------------
+// Pilot Selection Menu (FUN_00414A41)
+// ---------------------------------------------------------------------------
+// Pilot/save selection before chapter selection.
 
+// runLevelSelect -- Pilot selection menu (FUN_00414A41).
+// Returns kLevelSelectPlay, kLevelSelectBack, or kLevelSelectQuit.
 int InsaneRebel2::runLevelSelect() {
-	// Pilot selection menu loop - emulates FUN_00414A41
-	// Returns:
-	//   kLevelSelectPlay (1) = Go to chapter selection (pilot selected or NEW+difficulty chosen)
-	//   kLevelSelectBack (0) = Return to main menu (MAIN MENU or ESC)
-	//   kLevelSelectQuit (2) = Quit game
 
 	debug("Rebel2: Entering pilot selection (FUN_00414A41), %d pilots loaded", _numPilots);
 
@@ -1800,38 +1780,14 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
 }
 
-// ==================== Top Pilots Screen (FUN_00420116) ====================
-// Displays ranked pilot scores with animated reveal over a menu background video.
-//
-// Original FUN_00420116 at 0x420116:
-// - Plays random menu video as background (FUN_0041fdc8)
-// - Draws title from TRS string 0x96 (150) centered at (152, 10) in low-res
-// - Iterates through ranking table (DAT_00443b58, 0x4a-byte records, max 15)
-// - Each row shows: rank medals, pilot name, difficulty, chapter, total score
-// - Rows appear one per frame (animated reveal up to local_10)
-// - param_1 == -2: 240 frame loop (from options); else 120 frames (from main menu)
-// - Exits on mouse click (DAT_0047a7e4 & 1) or any keypress (DAT_0047a7e8 != 0)
-//
-// Ranking table record (0x4a = 74 bytes, from FUN_00410271):
-//   +0x00 (4): timestamp
-//   +0x04 (40): pilot name (or "-----" for defaults)
-//   +0x36 (4): total score
-//   +0x3a (4): total rating
-//   +0x3e (2): difficulty tier (1-3) — TRS lookup: value + 0x9b (155)
-//   +0x40 (2): highest chapter completed (1-15)
-//
-// Column X positions (low-res 320x200):
-//   Rank medals: 43 (0x2b)  - FUN_004341a0, alignment=1
-//   Name:        88 (0x58)  - FUN_00434cb0, alignment=0 (left), "^f01%s"
-//   Difficulty: 195 (0xc3)  - FUN_00434cb0, alignment=1, "^f01%s" (TRS)
-//   Chapter:    245 (0xf5)  - FUN_00434cb0, alignment=1, "^f01%hd"
-//   Score:      295 (0x127) - FUN_00434cb0, alignment=2, "^f01%ld"
-// Row Y: sVar1 * 10 + 42 (0x2a)
-
-// Initialize ranking table with 15 default entries (FUN_0040FF00)
-// Called when no ranking save file exists. Generates placeholder entries with:
-//   name = "-----", score = (15-i)*1500, rating = (15-i)*2,
-//   difficulty = ((15-i)*3+14)/15, chapter = ((15-i)*15+14)/15
+// ---------------------------------------------------------------------------
+// Top Pilots Screen (FUN_00420116)
+// ---------------------------------------------------------------------------
+// Ranked pilot scores with animated reveal over a menu background video.
+// 0x4a-byte records (max 15): name, score, rating, difficulty, chapter.
+// Column X positions (low-res): medals=43, name=88, diff=195, ch=245, score=295.
+
+// initDefaultRankings -- Fill ranking table with defaults (FUN_0040FF00).
 void InsaneRebel2::initDefaultRankings() {
 	_numRankings = 0;
 	memset(_rankings, 0, sizeof(_rankings));
@@ -1847,8 +1803,7 @@ void InsaneRebel2::initDefaultRankings() {
 	}
 }
 
-// Insert a new entry into the sorted ranking table (FUN_00410271)
-// Maintains descending score order, max kMaxRankings entries
+// insertRanking -- Insert into sorted ranking table (FUN_00410271).
 void InsaneRebel2::insertRanking(const char *name, int32 score, int32 rating,
                                   int16 difficulty, int16 chapter) {
 	if (score == 0)
@@ -1991,23 +1946,13 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
 	_topPilotsFrameCount++;
 }
 
-// ======================= Options Menu (FUN_004167A6) =======================
-// Original: FUN_00416787 calls FUN_0041fdc8 (init video) + FUN_004167a6(1)
-// FUN_004167a6 runs a loop over FUN_0041f5ae (drawMenuItems) with dynamic
-// toggle labels and slider items. Settings stored at DAT_00482e20[0..3]
-// (get/set via FUN_00425d30/FUN_00425d40) and DAT_0047a7fe, DAT_0047a80a.
-//
-// Menu items for keyboard mode (9 selectable):
-//   [0] Title:    TRS 89  "Game Options"
-//   [1] Music:    TRS 90/91
-//   [2] SFX:      TRS 92/93
-//   [3] Voices:   TRS 94/95
-//   [4] Text:     TRS 96/97
-//   [5] Controls: TRS 98/99
-//   [6] Rapid Fire: TRS 100/101
-//   [7] Volume:   TRS 103 "Volume Level: %hd%%"  (slider, left/right ±4)
-//   [8] Back:     TRS 107 "Return to Main Menu"
+// ---------------------------------------------------------------------------
+// Options Menu (FUN_004167A6)
+// ---------------------------------------------------------------------------
+// Toggle labels and slider items. Settings at DAT_00482e20[0..3].
+// Menu items: Music, SFX, Voices, Text, Controls, Rapid Fire, Volume, Back.
 
+// showOptionsMenu -- Options menu loop (FUN_00416787).
 void InsaneRebel2::showOptionsMenu() {
 	debug("Rebel2: Showing Options menu (FUN_00416787)");
 
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 71d94ff7f2c..e12ab1cd340 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -41,9 +41,12 @@ extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top,
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
 
-// ======================= Embedded Frame Codec Decoders =======================
-// These implement the retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0
+// ---------------------------------------------------------------------------
+// Embedded Frame Codec Decoders
+// ---------------------------------------------------------------------------
+// Retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0.
 
+// decodeCodec21 -- Codec 21/44: line update codec (FUN_0042BD60).
 void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
 	// Codec 21/44: Line Update codec (FUN_0042BD60)
 	// Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
@@ -71,6 +74,7 @@ void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int heig
 	}
 }
 
+// decodeCodec23 -- Codec 23: skip/copy with embedded RLE (FUN_0042BBF0).
 void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
 	// Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
 	// Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
@@ -120,6 +124,7 @@ void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int heig
 	}
 }
 
+// decodeCodec45 -- Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0).
 void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
 	// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
 	// May have a 6-byte sub-header starting with "01 FE"
@@ -246,6 +251,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 		width, height, nonZero, (nonZero * 100) / (width * height));
 }
 
+// renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
 void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
 	// Render the decoded embedded frame to the video buffer
 	// Skip immediate draw for handlers that render HUD during post-processing:
@@ -292,6 +298,12 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
 	debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
 }
 
+//
+// loadEmbeddedSan -- Decode an embedded SAN (ANIM/FOBJ) from IACT opcode 8 data.
+//
+// Parses ANIM container, extracts FOBJ codec data, decodes using codec 21/23/45,
+// and stores the result in _rebelEmbeddedHud[userId] for later rendering.
+//
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 	// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
 	if (userId < 0 || userId > 15 || !animData || size < 32) {
@@ -456,13 +468,9 @@ end_parsing:;
 }
 
 // Spawn explosion into the shared 5-slot system.
-// In the original, each handler has its own spawn logic inside its enemy processing function:
-//   Handler 0x26: FUN_40A2E0 (0x40A2E0) — spawns in slot arrays DAT_0044368e[]
-//   Handler 8:    FUN_4028C5 (0x4028C5) — spawns in slot arrays DAT_0043f854[]
-//   Handler 7:    FUN_40F628 (0x40F628) — spawns in slot arrays DAT_00443770[]
-//   Handler 25:   FUN_41E7C2 (0x41E7C2) — spawns in slot arrays DAT_0045792c[]
-// All share the same logic: find first free slot (counter==0), set counter=10,
-// scale=objectHalfWidth, position=enemy center, velocity=0.
+// spawnExplosion -- Allocate an explosion slot at the given position.
+// Per-handler FUN_40A2E0/FUN_4028C5/FUN_40F628/FUN_41E7C2: find first free
+// slot (counter==0), set counter=10, scale=objectHalfWidth, position=center.
 void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 	for (int i = 0; i < 5; i++) {
 		if (!_explosions[i].active || _explosions[i].counter <= 0) {
@@ -476,10 +484,7 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 	}
 }
 
-// Get max shot duration from level table (DAT_0047e0f0 indexed by DAT_0047a7fa/DAT_0047a7f8)
-// The original reads from a per-level per-difficulty table. The field at offset +0x00
-// (DAT_0047e0f0) is the first field of each record — our laserDelay field.
-// This value is used both as the initial shot counter AND as maxFrames for beam rendering.
+// getShotMaxDuration -- Shot duration from per-level difficulty table (DAT_0047e0f0).
 int16 InsaneRebel2::getShotMaxDuration() {
 	LevelDifficultyParams params = getDifficultyParams();
 	// laserDelay = DAT_0047e0f0 field: shot duration in frames
@@ -490,7 +495,7 @@ int16 InsaneRebel2::getShotMaxDuration() {
 	return duration;
 }
 
-// Dispatcher - calls appropriate spawn function based on current handler
+// spawnShot -- Dispatch to per-handler shot spawn.
 void InsaneRebel2::spawnShot(int x, int y) {
 	switch (_rebelHandler) {
 	case 0x26:  // Turret
@@ -520,7 +525,7 @@ void InsaneRebel2::spawnShot(int x, int y) {
 	}
 }
 
-// Handler 0x26 Turret shot spawn (based on FUN_4089AB lines 127-140)
+// spawnTurretShot -- Handler 0x26 turret shot spawn (FUN_4089AB).
 void InsaneRebel2::spawnTurretShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
 		if (_turretShots[i].counter == 0) {
@@ -538,7 +543,7 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
 		}
 }
 
-// Handler 8 Vehicle shot spawn (based on FUN_401CCF lines 65-69)
+// spawnVehicleShot -- Handler 8 vehicle shot spawn (FUN_401CCF).
 void InsaneRebel2::spawnVehicleShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
 		if (_vehicleShots[i].counter == 0) {
@@ -552,11 +557,8 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
 	}
 }
 
-// Handler 25 on-foot shot spawn (based on FUN_0041db5e lines 170-190)
-// Gun position computed from GRD002 character sprite.
-// Original stores: DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
-//                  DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
-// Render adds view offset back, so screen gun = table[idx] + spriteOffset.
+// spawnHandler25Shot -- Handler 25 on-foot shot spawn (FUN_0041db5e).
+// Gun position computed from GRD002 character sprite offset tables.
 void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 	// Handler 25 can only shoot when uncovered (damage == 0)
 	if (_rebelDamageLevel != 0) {
@@ -640,7 +642,7 @@ void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 	}
 }
 
-// Handler 7 Space combat shot spawn (based on FUN_40D836 lines 146-166)
+// spawnSpaceShot -- Handler 7 space combat shot spawn (FUN_40D836).
 void InsaneRebel2::spawnSpaceShot(int x, int y) {
 	for (int i = 0; i < 2; i++) {
 		if (_spaceShots[i].counter == 0) {
@@ -669,6 +671,7 @@ void InsaneRebel2::spawnSpaceShot(int x, int y) {
 	}
 }
 
+// drawTexturedLine -- Draw a line segment textured from a NUT sprite row.
 void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height, int x0, int y0, int x1, int y1, NutRenderer *nut, int spriteIdx, int v, bool mask231) {
 	if (!nut || spriteIdx >= nut->getNumChars())
 		return;
@@ -720,8 +723,8 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	}
 }
 
-// Helper: draw a textured segment between two points using the game's original routine (FUN_00429360 port)
-void drawTexturedSegment(byte *dst, int pitch, int width, int height,
+// drawTexturedSegment -- Textured segment between two points (FUN_00429360 port).
+static void drawTexturedSegment(byte *dst, int pitch, int width, int height,
                          int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8,
                          int clipLeft, int clipTop, int clipRight, int clipBottom) {
 	// Near-direct port of FUN_00429360.
@@ -1063,8 +1066,7 @@ void drawTexturedSegment(byte *dst, int pitch, int width, int height,
 }
 
 
-// Initialize laser texture buffer from NUT sprite (FUN_0040BAB0)
-// This pre-renders a sprite into a buffer that drawLaserBeam uses
+// initLaserTexture -- Pre-render a NUT sprite into the laser texture buffer (FUN_0040BAB0).
 void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
 	if (!nut || spriteIdx >= nut->getNumChars())
 		return;
@@ -1148,7 +1150,7 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
 	}
 }
 
-// Free laser texture buffer (FUN_0040BBD1)
+// freeLaserTexture -- Emulates FUN_0040BBD1.
 void InsaneRebel2::freeLaserTexture() {
 	free(_laserTexture.pixels);
 	_laserTexture.pixels = nullptr;
@@ -1156,11 +1158,14 @@ void InsaneRebel2::freeLaserTexture() {
 	_laserTexture.height = 0;
 }
 
-// Initialize edge blend tables (FUN_410510)
+//
+// initEdgeTable -- Initialize edge blend tables (FUN_410510).
+//
 // When data is nullptr, fills with default tables:
 //   _edgeTable[a*256+b] = min(a,b) (symmetric identity blend)
 //   _edgeTableAlt[a*256+b] = special blend for hi-res mode
 // When data is non-null, loads the primary table from data+8 (upper triangle, symmetric).
+//
 void InsaneRebel2::initEdgeTable(const byte *data) {
 	if (data == nullptr) {
 		// Default table initialization (FUN_410510 param_1==NULL path, lines 12-36)
@@ -1204,7 +1209,8 @@ void InsaneRebel2::initEdgeTable(const byte *data) {
 	}
 }
 
-// Draw edge highlight line using the edge blend table (FUN_410962)
+//
+// drawEdgeHighlightLine -- Edge highlight line using the blend table (FUN_410962).
 // For each pixel along the line, reads the two adjacent pixels perpendicular to
 // the line direction and uses _edgeTable[above*256+below] as the output color.
 // This creates a glow/blend effect at beam edges.
@@ -1448,8 +1454,9 @@ void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int he
 	}
 }
 
-// Draw laser beam using pre-initialized texture (FUN_0040BBF6)
-// This is a direct port of the assembly function.
+//
+// drawLaserBeam -- Laser beam using pre-initialized texture (FUN_0040BBF6).
+//
 // Two-layer rendering:
 //   Layer 1: Textured scanlines (beam body) via drawTexturedSegment()
 //   Layer 2: Edge highlights (glow) via drawEdgeHighlightLine(), gated by _rebelDetailMode >= 0
@@ -1599,12 +1606,15 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	}
 
 }
-// ============================================================
-// COLLISION ZONE SYSTEM (for Level 3 pilot ship obstacle avoidance)
-// ============================================================
-// Based on FUN_40E35E, FUN_40C3CC disassembly from info.md
-// Zones are quadrilaterals registered via IACT opcode 5
+// ---------------------------------------------------------------------------
+// Collision Zone System
+// ---------------------------------------------------------------------------
+// Level 3 pilot ship obstacle avoidance. Zones are quadrilaterals
+// registered via IACT opcode 5 (FUN_40E35E, FUN_40C3CC).
 
+//
+// registerCollisionZone -- Register a quad zone from IACT opcode 5 (FUN_4092D9 / FUN_4033CF).
+//
 void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 subOpcode, int16 par4) {
 	// IACT Opcode 5 data layout — corrected from FUN_4033CF / FUN_4092D9 analysis:
 	//
@@ -1671,6 +1681,7 @@ void InsaneRebel2::registerCollisionZone(Common::SeekableReadStream &b, int16 su
 	}
 }
 
+// resetCollisionZones -- Clear zone tables at end of frame (FUN_403240).
 void InsaneRebel2::resetCollisionZones() {
 	// Reset zone counters at end of frame (FUN_403240 equivalent)
 	// This clears the zone tables so they can be rebuilt from the next frame's IACT chunks
@@ -1678,6 +1689,9 @@ void InsaneRebel2::resetCollisionZones() {
 	_secondaryZoneCount = 0;
 }
 
+//
+// checkCollisionZones -- Per-frame collision test against primary zones (FUN_4092D9).
+//
 void InsaneRebel2::checkCollisionZones() {
 	// Per-frame collision checking — FUN_4092D9 first loop (lines 39-202).
 	// Tests aim/ship position against primary collision zone quadrilaterals.
@@ -1800,6 +1814,12 @@ void InsaneRebel2::checkCollisionZones() {
 	}
 }
 
+//
+// checkHandler7CollisionZones -- Handler 7 per-frame collision (FUN_40E35E).
+//
+// Two modes: obstacle collision (secondary zones) and wall/boundary
+// collision (primary zones with per-edge push-back).
+//
 void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
 	// FUN_40E35E — Handler 7 per-frame collision system.
 	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
@@ -2055,10 +2075,12 @@ void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, in
 	}
 }
 
+// renderNutSprite -- Draw a NUT sprite with transparency.
 void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
 	renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);
 }
 
+// renderNutSpriteClipped -- Draw a NUT sprite with explicit clip rectangle.
 static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
 		int clipLeft, int clipTop, int clipRight, int clipBottom,
 		int x, int y, NutRenderer *nut, int spriteIdx) {
@@ -2111,8 +2133,7 @@ static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
 	}
 }
 
-// Render NUT sprite with optional horizontal mirroring
-// Based on FUN_004236e0 disassembly - flags=0x2001 triggers horizontal flip
+// renderNutSpriteMirrored -- NUT sprite with optional horizontal flip (FUN_004236e0).
 void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx, bool mirror) {
 	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
 		return;
@@ -2170,6 +2191,12 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
 	}
 }
 
+//
+// procPostRendering -- Post-frame rendering: HUD, ships, enemies, effects, status bar.
+//
+// Called after FOBJ decoding. Dispatches to per-handler rendering functions
+// for ship sprites, laser shots, explosions, crosshair, and damage effects.
+//
 void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
 
@@ -2500,19 +2527,18 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	frameEndCleanup();
 }
 
-// ======================= Damage Visual Effect Functions =======================
+// ---------------------------------------------------------------------------
+// Damage Visual Effect Functions
+// ---------------------------------------------------------------------------
 // Palette flash + screen shake when the player takes damage.
-// Original retail functions: FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501
+// FUN_420515, FUN_420562, FUN_420754, FUN_42073B, FUN_420501.
 
-// FUN_00420501 - Reset palette flash counter.
-// Called at level start / scene transitions to clear any in-progress flash.
+// resetDamageFlash -- Reset palette flash counter (FUN_00420501).
 void InsaneRebel2::resetDamageFlash() {
 	_damageFlashCounter = 0;
 }
 
-// FUN_00420515 - Save current palette and initiate a 5-frame flash.
-// If a flash is already in progress, just resets the counter to 5
-// (the palette was already saved on the first hit).
+// initDamageFlash -- Save palette and initiate 5-frame flash (FUN_00420515).
 void InsaneRebel2::initDamageFlash() {
 	if (_damageFlashCounter == 0) {
 		// Save current SMUSH palette before modifying it
@@ -2521,27 +2547,18 @@ void InsaneRebel2::initDamageFlash() {
 	_damageFlashCounter = 5;
 }
 
-// FUN_0042073B - Trigger both palette flash and screen shake.
-// Called from the damage hit handler when the player takes damage.
+// triggerDamageEffect -- Trigger palette flash and screen shake (FUN_0042073B).
 void InsaneRebel2::triggerDamageEffect() {
 	initDamageFlash();
 	_damageShakeCounter = 10;
 }
 
-// FUN_00420562 - Per-frame palette modification.
 //
-// Two modes determined by _damageHighFlashCounter:
+// updateDamageFlashPalette -- Per-frame palette modification (FUN_00420562).
 //
-//   Normal hit flash (_damageHighFlashCounter == 0 or odd):
-//     Decrements _damageFlashCounter. On even counter values, all 768 palette
-//     bytes (RGB) are blended from inverted toward the saved original:
-//       output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
-//     Counter 5→4(apply)→3(skip)→2(apply)→1(skip)→0(apply=original). The
-//     alternating apply/skip creates a strobe-like flash effect.
+// Normal hit flash: alternating inverted-to-original blend on even counter.
+// High-damage red pulse: R-channel only modification at max damage.
 //
-//   High-damage red pulse (_playerDamage >= 0xFF, even counter):
-//     Only the R channel (every 3rd byte) is modified using the same formula
-//     with _damageHighFlashCounter. Creates a pulsing red tint overlay.
 void InsaneRebel2::updateDamageFlashPalette() {
 	// High-damage mode: persistent red pulsing when damage is maxed out
 	if (_playerDamage < 0xFF) {
@@ -2584,14 +2601,8 @@ void InsaneRebel2::updateDamageFlashPalette() {
 	}
 }
 
-// FUN_00420754 - Per-frame screen shake + palette flash.
-//
-// Screen shake randomly shifts scanlines left or right for visual distortion.
-// The number of affected scanlines decreases each frame (counter * 5),
-// creating a diminishing shake effect over 10 frames.
-//
-// Called every frame from procPostRendering when not in cutscene modes
-// (shipLevelMode != 4 and != 5, matching original: DAT_0043e000 != 4 && != 5).
+// updateDamageEffect -- Per-frame screen shake + palette flash (FUN_00420754).
+// Randomly shifts scanlines for visual distortion, diminishing over 10 frames.
 void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
 	if (_damageShakeCounter != 0) {
 		_damageShakeCounter--;
@@ -2637,9 +2648,12 @@ void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width,
 	updateDamageFlashPalette();
 }
 
-// ======================= Rendering Helper Functions =======================
-// These are extracted from procPostRendering for better readability
+// ---------------------------------------------------------------------------
+// Rendering Helper Functions
+// ---------------------------------------------------------------------------
+// Extracted from procPostRendering for better readability.
 
+// renderTextOverlay -- Progressive chapter title overlay (FUN_004171c5).
 void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, int height, int curFrame) {
 	// Emulates FUN_004171c5 text overlay: progressive chapter title during [fadeIn, fadeOut)
 	if (curFrame < _textOverlayFadeIn || curFrame >= _textOverlayFadeOut)
@@ -2774,6 +2788,7 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
 	}
 }
 
+// renderStatusBarBackground -- Fill status bar area with color 4 (FUN_004288c0).
 void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
 											 int videoWidth, int videoHeight, int statusBarY) {
 	// Fill status bar background (FUN_004288c0 equivalent)
@@ -2794,6 +2809,7 @@ void InsaneRebel2::renderStatusBarBackground(byte *renderBitmap, int pitch, int
 	}
 }
 
+// renderTurretHudOverlays -- NUT-based HUD for Handler 0x26/0x19 (FUN_004089ab).
 void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
 	// Draw NUT-based HUD overlays for Handler 0x26/0x19 (turret modes)
 	// From FUN_004089ab disassembly (lines 195-226):
@@ -2861,6 +2877,7 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
 	}
 }
 
+// renderEmbeddedHudOverlays -- Draw embedded SAN HUD overlays from IACT chunks.
 void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int width, int height) {
 	// Draw embedded SAN HUD overlays (from IACT chunks)
 	// For Handler 7 (Level 3): HUD elements are scattered across the screen
@@ -2974,6 +2991,7 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 	}
 }
 
+// renderStatusBarSprites -- DISPFONT.NUT status bar rendering (FUN_0041c012).
 void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int width, int height,
 										  int statusBarY, int32 curFrame) {
 	// FUN_0041c012 equivalent — renders DISPFONT.NUT status bar sprites.
@@ -3113,6 +3131,7 @@ void InsaneRebel2::renderStatusBarSprites(byte *renderBitmap, int pitch, int wid
 	}
 }
 
+// renderHandler7Ship -- Handler 7 third-person ship rendering (FUN_0040d836).
 void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 7 Ship Rendering (Third-Person Ship - FLY sprites)
 	// Based on FUN_0040d836 lines 173-185:
@@ -3210,6 +3229,7 @@ void InsaneRebel2::renderHandler7Ship(byte *renderBitmap, int pitch, int width,
 		_perspectiveX, _perspectiveY, _smoothedVelocity, _verticalInput, _flyEffectAnimCounter, _flyOverlayRepeatCount);
 }
 
+// renderHandler8Ship -- Handler 8 third-person on-foot rendering (FUN_00401CCF).
 void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 8 Ship Rendering (Third-Person On Foot - POV sprites)
 	// Uses _shipSprite (POV001) with position-based offset
@@ -3292,12 +3312,9 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 }
 
 // Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
-// Per original FUN_0041DB5E, GRD sprites are drawn AFTER FOBJ enemies, before GRD002.
-//
-// From FUN_0041db5e disassembly (lines 202-221):
-// - Mode 1 with damage==0: Width halved (left half only, pixels 0-159)
-// - Mode 4 with damage==0: Width halved AND buffer offset (right half only, pixels 160-319)
-// - All other cases: Full width (320 pixels)
+// renderHandler25ShipPre -- Handler 25 GRD001 pre-rendering (FUN_0041DB5E).
+// GRD sprites are drawn AFTER FOBJ enemies, before GRD002. Mode-based
+// half-width clipping for cover/uncover transitions.
 void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
 	if (_rebelHandler != 25)
 		return;
@@ -3372,6 +3389,7 @@ void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int wid
 	}
 }
 
+// renderHandler25Ship -- Handler 25 GRD002 post-rendering (FUN_0041db5e).
 void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width, int height) {
 	// Handler 25 POST-rendering: Draw GRD002 (character sprite) on top of enemies.
 	// GRD001 (wall/cockpit) is drawn before this via renderHandler25ShipPre().
@@ -3513,6 +3531,7 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 	}
 }
 
+// renderFallbackShip -- Fallback ship rendering using embedded HUD frame.
 void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width, int height) {
 	// Fallback: Use embedded HUD frame as ship sprite (Level 3 style)
 	// userId=11 contains the ship sprite strip
@@ -3579,6 +3598,7 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
 		drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);
 }
 
+// renderEnemyOverlays -- Handler 0x26 target indicator sprites (FUN_40A2E0).
 void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width, int height, int videoWidth) {
 	// Original per-enemy target indicator behavior comes from FUN_40A2E0 (handler 0x26):
 	// - Draws cockpit icon sprites 6..10 at enemy centers.
@@ -3648,11 +3668,7 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 	}
 }
 
-// Dispatcher — calls per-handler explosion render function.
-// Original code has separate functions per handler, each with its own
-// position transformation, scale thresholds, and secondary NUT rendering.
-// Each handler's render function checks DAT_0047e108 flags & 1:
-// when bit 0 is set, explosion NUT sprites are suppressed (counter still ticks).
+// renderExplosions -- Dispatch to per-handler explosion renderer.
 void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	// Check flags bit 0: suppress explosion sprite rendering
 	LevelDifficultyParams dparams = getDifficultyParams();
@@ -3688,11 +3704,7 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
 	}
 }
 
-// FUN_409FBC — Handler 0x26 (Turret/Cockpit) explosion rendering.
-// Position: Uses FUN_0041c720 for 3D→2D projection. At low-res, world coords ≈ screen coords.
-// Scale thresholds: Fixed (<11, <21).
-// Secondary NUT: DAT_0047fe80 (rendered if DAT_0047a7fc >= 0).
-// Hi-res: Coordinates doubled when DAT_0047a808 >= 2.
+// renderTurretExplosions -- Handler 0x26 turret explosion rendering (FUN_409FBC).
 void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3734,10 +3746,7 @@ void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int wid
 	}
 }
 
-// FUN_402696 — Handler 8 (Third-Person On-Foot) explosion rendering.
-// Position: World coords minus camera offset (DAT_0043e006/DAT_0043e008 = _viewX/_viewY).
-// Scale thresholds: Fixed (<11, <21) — same as handler 0x26.
-// Secondary NUT: None (only DAT_0047a828).
+// renderVehicleExplosions -- Handler 8 on-foot explosion rendering (FUN_402696).
 void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3778,10 +3787,7 @@ void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int wi
 	}
 }
 
-// FUN_40F1C5 — Handler 7 (Third-Person Ship) explosion rendering.
-// Position: Uses FUN_0041c720 for 3D→2D projection.
-// Scale thresholds: Resolution-dependent (low-res: <11/<21, high-res: <21/<41).
-// Secondary NUT: DAT_0047ff00 (FLY004, rendered if DAT_0047a7fc >= 0).
+// renderSpaceExplosions -- Handler 7 space explosion rendering (FUN_40F1C5).
 void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3880,11 +3886,7 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 	}
 }
 
-// FUN_41F29A — Handler 25 (FPS/Mixed) explosion rendering.
-// Position: World coords + view offset (DAT_0045790c/DAT_0045790e = _rebelViewOffsetX/_rebelViewOffsetY).
-// Scale thresholds: Resolution-dependent (same formula as Handler 7).
-// Secondary NUT: DAT_00482260 (hi-res HUD alternative, rendered if DAT_0047a7fc >= 0).
-// Note: No per-frame sound panning update (unlike handlers 0x26, 8, 7).
+// renderHandler25Explosions -- Handler 25 FPS explosion rendering (FUN_41F29A).
 void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3926,7 +3928,7 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
 	}
 }
 
-// Dispatcher - calls appropriate render function based on current handler
+// renderLaserShots -- Dispatch to per-handler laser renderer.
 void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	switch (_rebelHandler) {
 	case 0x26:  // Turret - FUN_40AD63
@@ -3947,8 +3949,7 @@ void InsaneRebel2::renderLaserShots(byte *renderBitmap, int pitch, int width, in
 	}
 }
 
-// Handler 0x26 Turret laser rendering (FUN_40AD63)
-// Gun positions depend on _rebelLevelType (DAT_004436de)
+// renderTurretLaserShots -- Handler 0x26 turret laser rendering (FUN_40AD63).
 void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	// Uses pre-initialized _laserTexture from sprite 5 of CPITIMAG.NUT
 
@@ -4047,9 +4048,7 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 	}
 }
 
-// Handler 8 Vehicle laser rendering (FUN_402ED0)
-// In the original, the laser is a short muzzle flash from gun barrel toward ship center,
-// NOT a projectile traveling across the screen. The "hit" effect is handled separately.
+// renderVehicleLaserShots -- Handler 8 vehicle laser rendering (FUN_402ED0).
 void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	// No NUT check needed - uses pre-initialized _laserTexture
 
@@ -4093,8 +4092,7 @@ void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int wi
 	}
 }
 
-// Handler 7 Space combat laser rendering (FUN_40FADF)
-// Dual beams from left and right gun positions
+// renderSpaceLaserShots -- Handler 7 space laser rendering (FUN_40FADF).
 void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	// No NUT check needed - uses pre-initialized _laserTexture
 
@@ -4133,8 +4131,7 @@ void InsaneRebel2::renderSpaceLaserShots(byte *renderBitmap, int pitch, int widt
 	}
 }
 
-// Handler 25 laser rendering (FUN_0041f004)
-// Speeder bike laser shots - draws beam from gun position to target
+// renderHandler25LaserShots -- Handler 25 speeder bike laser rendering (FUN_0041f004).
 void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	// FUN_0041f004 uses turret-style shot slots with view offset adjustment
 	// Only render when player is uncovered (damage == 0)
@@ -4177,6 +4174,7 @@ void InsaneRebel2::renderHandler25LaserShots(byte *renderBitmap, int pitch, int
 	}
 }
 
+// renderCrosshair -- Draw crosshair/reticle at mouse position.
 void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int height) {
 	// From FUN_0040d836 (Handler 7) line 167-168: crosshair only drawn when DAT_004437c0 == 2
 	// Don't draw crosshair when shooting is disabled (flight-only segments)
@@ -4259,6 +4257,7 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 	}
 }
 
+// frameEndCleanup -- Reset enemy flags and collision zones at frame end (FUN_403240).
 void InsaneRebel2::frameEndCleanup() {
 	// Reset enemy active flags and collision zones at frame end
 	// The original game rebuilds lists from scratch each frame
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 8ee4482d399..eee2f72bc61 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -29,10 +29,10 @@
 
 namespace Scumm {
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 1 Handler - FUN_00417E53
 // Single gameplay phase (01P01.SAN)
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel1() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -93,11 +93,11 @@ int InsaneRebel2::runLevel1() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Wave State Management - FUN_00417b61
 // Waits for video completion, accumulates kill state, redistributes kill credits.
 // Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
 	// FUN_00417b61: Core wave management function
@@ -197,11 +197,11 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 	return result;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 2 Handler - FUN_00418063
 // Multiple parts with P1/P2/P3 subdirectories
 // Random animation variants for each part
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel2() {
 	// FUN_00418063: Level 2 "Corellia Star" - Third-person on-foot shooting
@@ -541,11 +541,11 @@ int InsaneRebel2::runLevel2() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 3 Handler - FUN_0041885F
 // Two phases with per-phase retry handling
 // Phase 1: 03PLAY1.SAN, Phase 2: 03PLAY2.SAN
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel3() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -660,10 +660,10 @@ int InsaneRebel2::runLevel3() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 4 Handler
 // Cutscene + single gameplay phase
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel4() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -726,11 +726,11 @@ int InsaneRebel2::runLevel4() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 5 Handler - FUN_00418EC6
 // Single gameplay phase (05PLAY.SAN)
 // Random A/B death video like Level 1
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel5() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -786,11 +786,11 @@ int InsaneRebel2::runLevel5() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 6 Handler - FUN_00419317
 // Two phases with per-phase retry (like Level 3)
 // Phase 1: 06PLAY1.SAN, Phase 2: 06PLAY2.SAN
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel6() {
 	// FUN_004190d6 — Mos Eisley: two-phase on-foot (Handler 8)
@@ -911,12 +911,12 @@ int InsaneRebel2::runLevel6() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 7 Handler - FUN_0041974C
 // "TIE Training" - Canyon flight with fork at frame 1592
 // Single gameplay phase (07PLAY.SAN), optional second segment (07PLAYB.SAN)
 // Death: DAT_0047ab8c-based (fork reached → DIE_B, not reached → DIE_A)
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel7() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -988,12 +988,12 @@ int InsaneRebel2::runLevel7() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 8 Handler - FUN_00419976
 // "Flight to Imdaar" - Y-Wing space battle (single phase)
 // No cutscene (starts with BEG). flags=0x08 for gameplay.
 // Death: random A or B
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel8() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1055,13 +1055,13 @@ int InsaneRebel2::runLevel8() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 9 Handler - FUN_00419B86
 // "The Mine Field" - Navigate through force fields (single phase)
 // No cutscene. Initial phaseState = 0xfffffffe (all bits set except bit 0).
 // Mid-events at frames 0x19f (415) and 0x352 (850): FUN_00407f55 (score checkpoint)
 // Death: DAT_0047ab94-based (0→A, 1→C, else→B)
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel9() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1128,12 +1128,12 @@ int InsaneRebel2::runLevel9() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 10 Handler - FUN_00419E0A
 // "Speeder Bikes" - Forest speeder chase (single phase)
 // Has cutscene. Single death video (10DIE.SAN, no variants).
 // Original plays DIE then RETRY in sequence (no separate check).
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel10() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1199,7 +1199,7 @@ int InsaneRebel2::runLevel10() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 11 Handler - FUN_0041A00C
 // "Inside the Terror" - Three phases + bridge puzzle (Handler 8, on-foot)
 //
@@ -1209,7 +1209,7 @@ int InsaneRebel2::runLevel10() {
 //   Exit when (phaseState & 0x70) == 0x70
 // POST3/POST3B/POST3C bridge cinematics
 // Phase 3 second half: P3/11P03_X (G-L) - after bridge, mask 0x0e
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel11() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1591,7 +1591,7 @@ int InsaneRebel2::runLevel11() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 12 Handler - FUN_0041AA14
 // "Sewers" - Four phases FPS corridor shooting (Handler 25)
 //
@@ -1601,7 +1601,7 @@ int InsaneRebel2::runLevel11() {
 // Phase 3: P3/12P03_X (A,B,C,D,F) mask=6
 // Phase 4: P4/12P04_X (A,B,C,D,E,F) mask=0xe
 // Closing: 12P09.SAN
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel12() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1926,14 +1926,14 @@ int InsaneRebel2::runLevel12() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 13 Handler - FUN_0041B3E1
 // "Escaping the Star Destroyer" - Two-phase flight/escape
 // Phase A: 13PLAY_A.SAN (main flight), transitions to Phase B at maxFrame-10
 // Phase B: 13PLAY_B.SAN (reactor loop, flags 0x468) — plays until
 //   (DAT_0047ab90 == 0 && DAT_0047ab7c == 0) meaning all targets destroyed.
 // Death: frame-based (A/B/C/B/D pattern)
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel13() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -2011,11 +2011,11 @@ int InsaneRebel2::runLevel13() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 14 Handler - FUN_0041B6E8
 // "TIE Attack" - Final space battle (single phase)
 // No cutscene. Single death video (14DIE.SAN, no variants).
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel14() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -2076,14 +2076,14 @@ int InsaneRebel2::runLevel14() {
 	return kLevelQuit;
 }
 
-// =============================================================================
+// ---------------------------------------------------------------------------
 // Level 15 Handler - FUN_0041B8D7
 // "Imdaar Alpha" - Final mission (single long phase with level ID switch)
 // Has cutscene. Mid-level: DAT_0047a7f8 changes from 0xf to 0x10 at frame 0x21e.
 // This represents a transition from the tunnel section to the core section.
 // Death: frame-based (A/B/C/B/C/B/D pattern with 7 thresholds)
 // On completion → FUN_0041BBE8 (credits/end game, not a playable level)
-// =============================================================================
+// ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel15() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;


Commit: c390c411cfdd197a82c3c733a24470ea41f771d0
    https://github.com/scummvm/scummvm/commit/c390c411cfdd197a82c3c733a24470ea41f771d0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:21+02:00

Commit Message:
SCUMM: RA2: Standardize menu and rendering comments

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index ea2dce7a674..42284a4254d 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -540,6 +540,7 @@ InsaneRebel2::~InsaneRebel2() {
 }
 
 // notifyEvent -- EventObserver callback for global input dispatch.
+// Handles ESC (skip) and SPACE (pause) regardless of menu state.
 bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 	if (event.type == Common::EVENT_KEYDOWN) {
 		SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
@@ -1292,7 +1293,9 @@ bool InsaneRebel2::isShootingAllowed() {
 }
 
 // procSKIP -- Conditional FOBJ/PSAD skip via bit table (FUN_00423A50).
-// RA2 uses SKIP chunks to hide destroyed enemy sprites.
+// Same mechanism as Full Throttle, but RA2 uses it for enemy objects:
+// when setBit(enemy_id) is called on destruction, SKIP chunks containing
+// that ID cause the next FOBJ (enemy sprite) to be skipped.
 void InsaneRebel2::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 
 	int16 par1, par2;
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index 1d5709f9f1a..cdb0d1850b5 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -91,6 +91,7 @@ void InsaneRebel2::playMissionBriefing() {
 
 // playCinematic -- Play a cinematic/cutscene video.
 // Resets handler to 0 (no HUD) and sets flags to 0x28 (cinematic + buffer preserve).
+// All wrapper functions (FUN_00417168/4171c5/417ab2/417327) add | 8 before calling FUN_0041f4d0.
 void InsaneRebel2::playCinematic(const char *filename) {
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;  // No status bar during cinematics
@@ -101,7 +102,8 @@ void InsaneRebel2::playCinematic(const char *filename) {
 }
 
 // playVideoWithText -- Video with progressive text overlay (FUN_004171c5).
-// Text is progressively revealed during [fadeInFrame, fadeOutFrame).
+// displayLength = currentFrame + 10 - fadeInFrame, capped at 0xBE (190) chars.
+// Text rendered at (textX, textY) via FUN_004341a0.
 void InsaneRebel2::playVideoWithText(const char *filename, int textID, int textX, int textY,
                                      int fadeInFrame, int fadeOutFrame) {
 
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index f86c8557a6c..336d8209388 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -49,7 +49,7 @@ void InsaneRebel2::resetMenu() {
 	_menuSelectionConfirmed = false;
 }
 
-// unlockAllChapters -- Debug mode unlock (FUN_00415CF8).
+// unlockAllChapters -- Debug mode unlock (FUN_00415CF8 lines 60-71, DAT_0047ab34=='d').
 void InsaneRebel2::unlockAllChapters() {
 	debug("Rebel2: Unlocking all chapters for testing");
 	for (int i = 0; i < 16; i++) {
@@ -59,7 +59,9 @@ void InsaneRebel2::unlockAllChapters() {
 }
 
 // getRandomMenuVideo -- Select random menu video variant (FUN_0041FDC8).
-// Always uses O_MENU_X.SAN (A-O) instead of audio-only O_MENU.SAN.
+// Original plays O_MENU.SAN when no progress flags are set, but that file
+// contains ONLY audio (no FOBJ frames) resulting in a black background.
+// We always use O_MENU_X.SAN (A-O) which have 320x200 background images.
 Common::String InsaneRebel2::getRandomMenuVideo() {
 
 	// Select random variant (0-14 maps to A-O), ensuring different from last
@@ -80,7 +82,8 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
 //
 // Returns -1 (no action) or 0-4 (menu item selected).
 // Events captured by notifyEvent() before ScummEngine consumes them.
-// Keyboard: Up/Down navigate, Enter confirms. Mouse: Y maps to selection.
+// Keyboard: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b.
+// Mouse mode (DAT_0047a806 == 1): Y position maps to selection.
 //
 int InsaneRebel2::processMenuInput() {
 
@@ -1784,10 +1787,16 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 // Top Pilots Screen (FUN_00420116)
 // ---------------------------------------------------------------------------
 // Ranked pilot scores with animated reveal over a menu background video.
-// 0x4a-byte records (max 15): name, score, rating, difficulty, chapter.
+// 0x4a (74) byte records (max 15, from FUN_00410271):
+//   +0x00 (4): timestamp, +0x04 (40): name, +0x36 (4): score,
+//   +0x3a (4): rating, +0x3e (2): difficulty tier (1-3, TRS=value+0x9b),
+//   +0x40 (2): highest chapter (1-15).
 // Column X positions (low-res): medals=43, name=88, diff=195, ch=245, score=295.
+// Row Y: sVar1 * 10 + 42.
 
 // initDefaultRankings -- Fill ranking table with defaults (FUN_0040FF00).
+// Generates 15 placeholder entries: score=(15-i)*1500, rating=(15-i)*2,
+// difficulty=((15-i)*3+14)/15, chapter=((15-i)*15+14)/15.
 void InsaneRebel2::initDefaultRankings() {
 	_numRankings = 0;
 	memset(_rankings, 0, sizeof(_rankings));
@@ -1950,7 +1959,8 @@ void InsaneRebel2::drawTopPilotsOverlay(byte *renderBitmap, int pitch, int width
 // Options Menu (FUN_004167A6)
 // ---------------------------------------------------------------------------
 // Toggle labels and slider items. Settings at DAT_00482e20[0..3].
-// Menu items: Music, SFX, Voices, Text, Controls, Rapid Fire, Volume, Back.
+// TRS IDs: Title=89, Music=90/91, SFX=92/93, Voices=94/95, Text=96/97,
+// Controls=98/99, Rapid Fire=100/101, Volume=103 "%hd%%", Back=107.
 
 // showOptionsMenu -- Options menu loop (FUN_00416787).
 void InsaneRebel2::showOptionsMenu() {
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index e12ab1cd340..62af6905343 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -469,8 +469,9 @@ end_parsing:;
 
 // Spawn explosion into the shared 5-slot system.
 // spawnExplosion -- Allocate an explosion slot at the given position.
-// Per-handler FUN_40A2E0/FUN_4028C5/FUN_40F628/FUN_41E7C2: find first free
-// slot (counter==0), set counter=10, scale=objectHalfWidth, position=center.
+// Per-handler slot arrays: 0x26=DAT_0044368e[], 8=DAT_0043f854[],
+// 7=DAT_00443770[], 25=DAT_0045792c[]. All share same logic: find first
+// free slot (counter==0), set counter=10, scale=objectHalfWidth, position=center.
 void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 	for (int i = 0; i < 5; i++) {
 		if (!_explosions[i].active || _explosions[i].counter <= 0) {
@@ -485,6 +486,7 @@ void InsaneRebel2::spawnExplosion(int x, int y, int objectHalfWidth) {
 }
 
 // getShotMaxDuration -- Shot duration from per-level difficulty table (DAT_0047e0f0).
+// Used both as initial shot counter AND maxFrames for beam rendering.
 int16 InsaneRebel2::getShotMaxDuration() {
 	LevelDifficultyParams params = getDifficultyParams();
 	// laserDelay = DAT_0047e0f0 field: shot duration in frames
@@ -558,7 +560,9 @@ void InsaneRebel2::spawnVehicleShot(int x, int y) {
 }
 
 // spawnHandler25Shot -- Handler 25 on-foot shot spawn (FUN_0041db5e).
-// Gun position computed from GRD002 character sprite offset tables.
+// Gun position from GRD002 offset tables:
+//   DAT_0045791c[i] = gunOffsetTable[spriteIdx] + DAT_00457910 - DAT_0045790c
+//   DAT_00457920[i] = gunYTable[spriteIdx] + DAT_00457912 - DAT_0045790e
 void InsaneRebel2::spawnHandler25Shot(int x, int y) {
 	// Handler 25 can only shoot when uncovered (damage == 0)
 	if (_rebelDamageLevel != 0) {
@@ -2556,8 +2560,12 @@ void InsaneRebel2::triggerDamageEffect() {
 //
 // updateDamageFlashPalette -- Per-frame palette modification (FUN_00420562).
 //
-// Normal hit flash: alternating inverted-to-original blend on even counter.
-// High-damage red pulse: R-channel only modification at max damage.
+// Normal hit flash (_damageHighFlashCounter == 0 or odd):
+//   Blend formula: output[i] = 0xFF - ((0xFF - saved[i]) * (0x10 - counter)) >> 4
+//   Counter 5->4(apply)->3(skip)->2(apply)->1(skip)->0(apply=original) = strobe.
+//
+// High-damage red pulse (_playerDamage >= 0xFF, even counter):
+//   R channel only (every 3rd byte) using same formula with _damageHighFlashCounter.
 //
 void InsaneRebel2::updateDamageFlashPalette() {
 	// High-damage mode: persistent red pulsing when damage is maxed out
@@ -2602,7 +2610,8 @@ void InsaneRebel2::updateDamageFlashPalette() {
 }
 
 // updateDamageEffect -- Per-frame screen shake + palette flash (FUN_00420754).
-// Randomly shifts scanlines for visual distortion, diminishing over 10 frames.
+// Shifts counter*5 random scanlines per frame, diminishing over 10 frames.
+// Only called when not in cutscene modes (DAT_0043e000 != 4 && != 5).
 void InsaneRebel2::updateDamageEffect(byte *renderBitmap, int pitch, int width, int height) {
 	if (_damageShakeCounter != 0) {
 		_damageShakeCounter--;
@@ -3312,9 +3321,11 @@ void InsaneRebel2::renderHandler8Ship(byte *renderBitmap, int pitch, int width,
 }
 
 // Handler 25: Draw GRD001 (wall/cockpit overlay) in procPostRendering.
-// renderHandler25ShipPre -- Handler 25 GRD001 pre-rendering (FUN_0041DB5E).
-// GRD sprites are drawn AFTER FOBJ enemies, before GRD002. Mode-based
-// half-width clipping for cover/uncover transitions.
+// renderHandler25ShipPre -- Handler 25 GRD001 pre-rendering (FUN_0041DB5E lines 202-221).
+// GRD sprites drawn AFTER FOBJ enemies, before GRD002. Mode-based clipping:
+//   Mode 1, damage==0: left half only (pixels 0-159)
+//   Mode 4, damage==0: right half only (pixels 160-319)
+//   All other cases: full width (320 pixels)
 void InsaneRebel2::renderHandler25ShipPre(byte *renderBitmap, int pitch, int width, int height) {
 	if (_rebelHandler != 25)
 		return;
@@ -3669,6 +3680,7 @@ void InsaneRebel2::renderEnemyOverlays(byte *renderBitmap, int pitch, int width,
 }
 
 // renderExplosions -- Dispatch to per-handler explosion renderer.
+// DAT_0047e108 flags & 1: suppress explosion sprites (counters still tick).
 void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	// Check flags bit 0: suppress explosion sprite rendering
 	LevelDifficultyParams dparams = getDifficultyParams();
@@ -3705,6 +3717,8 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
 }
 
 // renderTurretExplosions -- Handler 0x26 turret explosion rendering (FUN_409FBC).
+// Position: FUN_0041c720 3D->2D projection (identity at low-res).
+// Scale thresholds: <11, <21. Secondary NUT: DAT_0047fe80 (if DAT_0047a7fc >= 0).
 void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3747,6 +3761,8 @@ void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int wid
 }
 
 // renderVehicleExplosions -- Handler 8 on-foot explosion rendering (FUN_402696).
+// Position: world coords minus camera offset (DAT_0043e006/08 = _viewX/_viewY).
+// Scale thresholds: <11, <21. No secondary NUT.
 void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3788,6 +3804,9 @@ void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int wi
 }
 
 // renderSpaceExplosions -- Handler 7 space explosion rendering (FUN_40F1C5).
+// Position: FUN_0041c720 3D->2D projection.
+// Scale thresholds: resolution-dependent (low-res: <11/<21, hi-res: <21/<41).
+// Secondary NUT: DAT_0047ff00 (FLY004, if DAT_0047a7fc >= 0).
 void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3887,6 +3906,8 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 }
 
 // renderHandler25Explosions -- Handler 25 FPS explosion rendering (FUN_41F29A).
+// Position: world coords + view offset (DAT_0045790c/0e = _rebelViewOffsetX/Y).
+// Scale thresholds: resolution-dependent (same as Handler 7). No sound panning.
 void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -4049,6 +4070,7 @@ void InsaneRebel2::renderTurretLaserShots(byte *renderBitmap, int pitch, int wid
 }
 
 // renderVehicleLaserShots -- Handler 8 vehicle laser rendering (FUN_402ED0).
+// In the original, the laser is a short muzzle flash, NOT a traveling projectile.
 void InsaneRebel2::renderVehicleLaserShots(byte *renderBitmap, int pitch, int width, int height) {
 	// No NUT check needed - uses pre-initialized _laserTexture
 


Commit: 60c4652e2a03009001ff55d95e020d71354a8914
    https://github.com/scummvm/scummvm/commit/60c4652e2a03009001ff55d95e020d71354a8914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:21+02:00

Commit Message:
SCUMM: RA2: Standardize IACT and runlevel comments

Changed paths:
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index 104d64dc9d1..d53069fdd89 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -622,7 +622,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 		// Skip position calculation for special modes 4 and 5
 		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
-			// ===== Movement Range Transition (Covered vs Shooting) =====
+			// ----- Movement Range Transition (Covered vs Shooting) -----
 			// Based on FUN_00401234 lines 85-120:
 			// Mode 2 = "Covered" state - contract movement range to 41 (0x29)
 			// Other modes = "Shooting" state - expand movement range to 127 (0x7f)
@@ -787,9 +787,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
 		}
 
-		// ============================================================
+		// ------------------------------------------------------------
 		// Ship position update — FUN_40C3CC case 4, lines 49-327
-		// ============================================================
+		// ------------------------------------------------------------
 		// Velocity-based physics with momentum/inertia:
 		//   Mouse offset from center → scaled input [-127,127]
 		//   → velocity history averaging → physics delta (clamped ±12/frame)
@@ -1501,7 +1501,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	int64 startPos = b.pos();
 	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
 
-	// ===== Handler 7: FLY NUT Loading (Third-Person Ship) =====
+	// ----- Handler 7: FLY NUT Loading (Third-Person Ship) -----
 	// FUN_0040c3cc case 6: par4 determines FLY sprite slot
 	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
 	if (isHandler7FLY && remaining >= 14) {
@@ -1512,7 +1512,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		b.seek(startPos);
 	}
 
-	// ===== Edge Blend Table Loading (par4 == 1000) =====
+	// ----- Edge Blend Table Loading (par4 == 1000) -----
 	// FUN_405663: After all handler-specific opcode 8 processing, checks if par4==1000.
 	// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
 	// This table controls the edge glow color of laser beams (e.g. red vs green).
@@ -1535,7 +1535,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
-	// ===== Auxiliary Sound Buffer Loading (par4 20-47) =====
+	// ----- Auxiliary Sound Buffer Loading (par4 20-47) -----
 	// FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) → aux buffer 0
 	// FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) → aux buffer 0,
 	//   0x1f-0x25 (31-37) → aux buffer 1, 0x28 (40) → aux buffer 3,
@@ -1575,7 +1575,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
-	// ===== Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) =====
+	// ----- Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) -----
 	// FUN_0041CADB case 6 pushes 30 short pointers into sscanf with format at 0x482360:
 	//   "%hd %hd  %hd %hd ... %hd %hd" (15 X/Y pairs).
 	// Parsed values are written into DAT_004578a6 / DAT_004578c6 at indices 5..19.
@@ -1587,7 +1587,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// ===== Scan for embedded ANIM data =====
+	// ----- Scan for embedded ANIM data -----
 	// Remaining handlers require finding ANIM tag in the stream
 	debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
 		(long long)startPos, (long long)remaining);
@@ -1643,7 +1643,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 
 	bool handled = false;
 
-	// ===== Handler 0x26/0x19: Turret HUD Overlays =====
+	// ----- Handler 0x26/0x19: Turret HUD Overlays -----
 	// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
 	if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
 		if (par3 >= 1 && par3 <= 4) {
@@ -1651,7 +1651,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// ===== Handler 8: POV Ship Sprites or Background =====
+	// ----- Handler 8: POV Ship Sprites or Background -----
 	// FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
 	// NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
 	if (!handled && _rebelHandler == 8) {
@@ -1665,7 +1665,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// ===== Handler 25 (0x19): Level 2 GRD Ship Sprites and Background =====
+	// ----- Handler 25 (0x19): Level 2 GRD Ship Sprites and Background -----
 	// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
 	//   par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
 	//   par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
@@ -1689,7 +1689,7 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// ===== Fallback: Embedded SAN HUD overlays =====
+	// ----- Fallback: Embedded SAN HUD overlays -----
 	// For other cases, load as embedded SAN frame to HUD overlay slots
 	if (!handled) {
 		// Skip high-res data (par3 == 2, 4)
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 336d8209388..26c574777a9 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -233,9 +233,9 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	const int itemBaseY = numItems * -5 + 0x68;
 	const int itemSpacing = 10;
 
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// Font system - Emulates linked list from FUN_00403bd0
-	// =====================================================================
+	// -------------------------------------------------------------------
 	//   Font 0 (^f00): TALKFONT.NUT
 	//   Font 1 (^f01): SMALFONT.NUT (menu items)
 	//   Font 2 (^f02): TITLFONT.NUT (title)
@@ -254,9 +254,9 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
 	int actualPitch = _vm->_screenWidth;
 
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
-	// =====================================================================
+	// -------------------------------------------------------------------
 	//   ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
 	// Fixed-width format codes: ^fNN (2-digit font), ^cNNN (3-digit color)
 	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
@@ -351,22 +351,22 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 		}
 	};
 
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// Draw title - items[0]
 	// Centered: X = center - titleWidth/2
 	// Left-aligned: X = 40 (0x28)
-	// =====================================================================
+	// -------------------------------------------------------------------
 	{
 		int titleWidth = getStringWidth(items[0]);
 		int titleX = leftAligned ? 40 : (centerX - titleWidth / 2);
 		drawString(items[0], titleX, titleY);
 	}
 
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// Draw selectable items with selection highlight box
 	// Centered: item X = center - textWidth/2, box X = center - bracketWidth/2
 	// Left-aligned: item X = 23 (0x17), box X = 20 (0x14)
-	// =====================================================================
+	// -------------------------------------------------------------------
 	for (int i = 0; i < numItems; i++) {
 		int itemY = itemBaseY + i * itemSpacing;
 		const char *text = items[i + 1];
@@ -519,7 +519,7 @@ void InsaneRebel2::drawMenuStringRight(byte *renderBitmap, const char *str, int
 void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int height) {
 	// Main menu renderer - calls shared drawMenuItems()
 	// Emulates FUN_004147b2 -> FUN_0041f5ae with param_3=7, param_4=0
-	// =====================================================================
+	// -------------------------------------------------------------------
 	//
 	// Menu strings loaded from GAME.TRS (keyboard mode indices 10-17):
 	//   TRS index 10: "^f02Game Main Menu"           -> Title (uses TITLFONT)
@@ -1706,10 +1706,10 @@ int InsaneRebel2::processLevelSelectInput() {
 }
 
 void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int width, int height) {
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// Pilot selection / difficulty submenu renderer
 	// Emulates FUN_00414A41 → FUN_0041f5ae
-	// =====================================================================
+	// -------------------------------------------------------------------
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	if (!splayer) {
@@ -1718,12 +1718,12 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	}
 
 	if (_gameState == kStateDifficultySelect) {
-		// =====================================================================
+		// -------------------------------------------------------------------
 		// Difficulty submenu - LAB_00414ff6
 		// FUN_0041f5ae(0, &DAT_00457458, 6, 0)
 		// DAT_00457458 = 7 entries loaded from TRS 110-116 (FUN_00414073 lines 47-50)
 		// param_3 = 6 → items[0]=title(TRS 110), items[1..6]=selectable(TRS 111-116)
-		// =====================================================================
+		// -------------------------------------------------------------------
 		const char *diffItems[7];
 		for (int i = 0; i < 7; i++) {
 			diffItems[i] = splayer->getString(110 + i);
@@ -1735,9 +1735,9 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 		return;
 	}
 
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// Pilot menu - FUN_0041f5ae(0, &DAT_00457768, N+4, 0)
-	// =====================================================================
+	// -------------------------------------------------------------------
 	// items[0]    = title (TRS 20)
 	// items[1..N] = saved pilots (formatted with ^f01^c005<name>^f00)
 	// items[N+1]  = TRS 21 (ADD NEW PILOT)
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index eee2f72bc61..85fc00054f1 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -261,7 +261,7 @@ int InsaneRebel2::runLevel2() {
 			_rebelLinks[i][2] = 0;
 		}
 
-		// ===== PHASE 1: P1/02P01_X.SAN =====
+		// ----- PHASE 1: P1/02P01_X.SAN -----
 		// FUN_0041c7d0: Reset per-phase counters
 		_rebelKillCounter = 0;
 		_rebelHitCounter = 0;
@@ -331,7 +331,7 @@ int InsaneRebel2::runLevel2() {
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
 
-		// ===== PHASE 2: P2/02P02_X.SAN =====
+		// ----- PHASE 2: P2/02P02_X.SAN -----
 		_currentPhase = 2;
 		_rebelKillCounter = 0;
 		_rebelHitCounter = 0;
@@ -413,7 +413,7 @@ int InsaneRebel2::runLevel2() {
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
 
-		// ===== PHASE 3: P3/02P03_X.SAN =====
+		// ----- PHASE 3: P3/02P03_X.SAN -----
 		_currentPhase = 3;
 		_rebelKillCounter = 0;
 		_rebelHitCounter = 0;
@@ -556,7 +556,7 @@ int InsaneRebel2::runLevel3() {
 	if (_vm->shouldQuit())
 		return kLevelQuit;
 
-	// ===== PHASE 1 retry loop =====
+	// ----- PHASE 1 retry loop -----
 	while (!_vm->shouldQuit()) {
 		_playerShield = 255;
 		_playerDamage = 0;
@@ -611,7 +611,7 @@ int InsaneRebel2::runLevel3() {
 	if (_vm->shouldQuit())
 		return kLevelQuit;
 
-	// ===== PHASE 2 retry loop (preserves phase 1 score) =====
+	// ----- PHASE 2 retry loop (preserves phase 1 score) -----
 	_currentPhase = 2;
 
 	while (!_vm->shouldQuit()) {
@@ -818,7 +818,7 @@ int InsaneRebel2::runLevel6() {
 		// DAT_0047ab9c = 0xffffffff — init phase state
 		_rebelPhaseState = 0xffffffff;
 
-		// ===== PHASE 1 =====
+		// ----- PHASE 1 -----
 		_rebelLevelType = 5;  // DAT_0047a7f8 = 5
 		_currentPhase = 1;
 
@@ -862,7 +862,7 @@ int InsaneRebel2::runLevel6() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== PHASE 2 retry loop (inner while(true) in original) =====
+		// ----- PHASE 2 retry loop (inner while(true) in original) -----
 		while (!_vm->shouldQuit()) {
 			_rebelLevelType = 6;  // DAT_0047a7f8 = 6
 			_currentPhase = 2;
@@ -1255,7 +1255,7 @@ int InsaneRebel2::runLevel11() {
 			_rebelLinks[i][2] = 0;
 		}
 
-		// ===== PHASE 1: P1/11P01_X.SAN =====
+		// ----- PHASE 1: P1/11P01_X.SAN -----
 		_rebelKillCounter = 0;
 		_rebelHitCounter = 0;
 		_rebelPhaseState = 0;
@@ -1321,7 +1321,7 @@ int InsaneRebel2::runLevel11() {
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
 
-		// ===== PHASE 2: P2/11P02_X.SAN =====
+		// ----- PHASE 2: P2/11P02_X.SAN -----
 		_currentPhase = 2;
 		_rebelKillCounter = 0;
 		_rebelHitCounter = 0;
@@ -1384,7 +1384,7 @@ int InsaneRebel2::runLevel11() {
 		totalKills += _rebelKillCounter;
 		totalMisses += _rebelHitCounter;
 
-		// ===== PHASE 3 FIRST HALF: P3/11P03_X (A-F) =====
+		// ----- PHASE 3 FIRST HALF: P3/11P03_X (A-F) -----
 		// Bridge puzzle — exit when (phaseState & 0x70) == 0x70
 		_currentPhase = 3;
 		_rebelKillCounter = 0;
@@ -1455,7 +1455,7 @@ int InsaneRebel2::runLevel11() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== PHASE 3 BRIDGE CINEMATICS =====
+		// ----- PHASE 3 BRIDGE CINEMATICS -----
 		{
 			bool allBasicKilled = (_rebelPhaseState & 0x0e) >= 0x0e;
 			if (!allBasicKilled) {
@@ -1472,7 +1472,7 @@ int InsaneRebel2::runLevel11() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== PHASE 3 SECOND HALF: P3/11P03_X (G-L) =====
+		// ----- PHASE 3 SECOND HALF: P3/11P03_X (G-L) -----
 		// Reset shots/explosions (FUN_0041ca6a equivalent)
 		for (int i = 0; i < 5; i++) {
 			_explosions[i].active = false;
@@ -1542,7 +1542,7 @@ int InsaneRebel2::runLevel11() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== LEVEL COMPLETED =====
+		// ----- LEVEL COMPLETED -----
 		{
 			totalMisses += _rebelHitCounter;
 			int accuracy = 0;
@@ -1640,7 +1640,7 @@ int InsaneRebel2::runLevel12() {
 			_rebelLinks[i][2] = 0;
 		}
 
-		// ===== PHASE 1: 12P05 → P1/12P01_X =====
+		// ----- PHASE 1: 12P05 → P1/12P01_X -----
 		// FUN_00401000: Reset at top of each retry
 		_rebelKillCounter = 0;
 		_rebelHitCounter = 0;
@@ -1695,7 +1695,7 @@ int InsaneRebel2::runLevel12() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== PHASE 2: 12P06 → P2/12P02_X =====
+		// ----- PHASE 2: 12P06 → P2/12P02_X -----
 		_currentPhase = 2;
 		_rebelPhaseState = 0;
 		_rebelWaveState = 0;
@@ -1758,7 +1758,7 @@ int InsaneRebel2::runLevel12() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== PHASE 3: 12P07 → P3/12P03_X =====
+		// ----- PHASE 3: 12P07 → P3/12P03_X -----
 		_currentPhase = 3;
 		_rebelPhaseState = 0;
 		_rebelWaveState = 0;
@@ -1819,7 +1819,7 @@ int InsaneRebel2::runLevel12() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== PHASE 4: 12P08 → P4/12P04_X =====
+		// ----- PHASE 4: 12P08 → P4/12P04_X -----
 		_currentPhase = 4;
 		_rebelPhaseState = 0;
 		_rebelWaveState = 0;
@@ -1879,14 +1879,14 @@ int InsaneRebel2::runLevel12() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// ===== CLOSING: 12P09.SAN =====
+		// ----- CLOSING: 12P09.SAN -----
 		splayer->setCurVideoFlags(0x428);
 		splayer->play("LEV12/12P09.SAN", 12);
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
-		// ===== LEVEL COMPLETED =====
+		// ----- LEVEL COMPLETED -----
 		{
 			int accuracy = 0;
 			if (_rebelKillCounter > 0) {


Commit: 523bd629c6d292bc3fcf4a4e1dd8971d4a9a8679
    https://github.com/scummvm/scummvm/commit/523bd629c6d292bc3fcf4a4e1dd8971d4a9a8679
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:21+02:00

Commit Message:
SCUMM: RA2: Clean up variable names

Changed paths:
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_iact.cpp
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/insane/insane_rebel_render.cpp
    engines/scumm/insane/insane_rebel_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index f3bced79181..8bcb0e23170 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -663,7 +663,7 @@ public:
 		int savedBgHeight;        // Height of saved background
 	};
 
-	void init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type = 0);
+	void initEnemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type = 0);
 	void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	Common::List<enemy> _enemies;
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel_iact.cpp
index d53069fdd89..47f818bd034 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel_iact.cpp
@@ -819,15 +819,15 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			inputY = -127;
 
 		// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
-		// Mouse mode: local_c = (DAT_0047a7e0 * 0x7f) / 0xa0
-		int16 local_c = (int16)((inputX * 127) / 160);
-		int16 local_14 = inputY;  // Y already in [-127, 127]
+		// Mouse mode: scaledInputX = (DAT_0047a7e0 * 0x7f) / 0xa0
+		int16 scaledInputX = (int16)((inputX * 127) / 160);
+		int16 scaledInputY = inputY;  // Y already in [-127, 127]
 
 		// --- Step 3: Velocity history + smoothed average (lines 141-157) ---
 		for (int i = 24; i > 0; i--) {
 			_velocityHistory[i] = _velocityHistory[i - 1];
 		}
-		_velocityHistory[0] = local_c;
+		_velocityHistory[0] = scaledInputX;
 
 		// Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
 		const int smoothWindow = 5;
@@ -868,17 +868,17 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		if (_flyControlMode == 1) {
 			// Mode 1: Full cross-axis coupling (lines 174-186)
 			// Banking: vertical input deflects horizontal movement
-			if (local_c < 1) {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * local_14 - windEffectX) >> 9);
+			if (scaledInputX < 1) {
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
 			} else {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * local_14 - windEffectX) >> 9);
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
 			}
 		} else {
 			// Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
-			if (local_c < 1) {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+			if (scaledInputX < 1) {
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
 			} else {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * local_14 >> 2) - windEffectX) >> 9);
+				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
 			}
 		}
 
@@ -894,7 +894,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// Y delta
 		if (_flyControlMode == 1) {
 			// Mode 1: clamped to ±12 with wind (lines 194-216)
-			int yCalc = levelYSpeed * local_14 - (windEffectY >> 1);
+			int yCalc = levelYSpeed * scaledInputY - (windEffectY >> 1);
 			int yDelta = yCalc >> 10;
 			if (yDelta < -12)
 				yDelta = -12;
@@ -903,11 +903,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			_flyShipScreenY -= (int16)yDelta;
 		} else {
 			// Mode 0/2/3: unclamped (lines 238-241)
-			_flyShipScreenY -= (int16)((levelYSpeed * local_14) >> 10);
+			_flyShipScreenY -= (int16)((levelYSpeed * scaledInputY) >> 10);
 		}
 
 		// Store vertical input for direction sprite (line 243)
-		_verticalInput = local_14;  // DAT_0044370e
+		_verticalInput = scaledInputY;  // DAT_0044370e
 
 		// Ship facing direction (line 244)
 		_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
@@ -2458,12 +2458,12 @@ void InsaneRebel2::enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b
 		}
 	}
 	if (!found) {
-		init_enemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
+		initEnemyStruct(enemyId, x, y, w, h, true, false, -1, par4);
 	}
 }
 
-// init_enemyStruct -- Create and append a new enemy entry.
-void InsaneRebel2::init_enemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
+// initEnemyStruct -- Create and append a new enemy entry.
+void InsaneRebel2::initEnemyStruct(int id, int32 x, int32 y, int32 w, int32 h, bool active, bool destroyed, int32 explosionFrame, int type) {
 	enemy e;
 	e.id = id;
 	e.type = type;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 26c574777a9..82561d88b33 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -452,7 +452,8 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
 			if (*p == 'l') { str = p + 1; continue; }
 		}
 		byte c = (byte)*str++;
-		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+		if (c >= 'a' && c <= 'z')
+			c = c - 'a' + 'A';
 		if (curFont && c < curFont->getNumChars())
 			w += curFont->getCharWidth(c);
 	}
@@ -495,9 +496,12 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
 			if (*p == 'l') { str = p + 1; continue; }
 		}
 		byte c = (byte)*str++;
-		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
-		if (!curFont) continue;
-		if (c >= curFont->getNumChars()) continue;
+		if (c >= 'a' && c <= 'z')
+			c = c - 'a' + 'A';
+		if (!curFont)
+			continue;
+		if (c >= curFont->getNumChars())
+			continue;
 		int charW = curFont->getCharWidth(c);
 		if (x >= 0 && y >= 0 && charW > 0)
 			curFont->drawCharV7(renderBitmap, clipRect, x, y, pitch, curColor,
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel_render.cpp
index 62af6905343..143f72db23c 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel_render.cpp
@@ -1135,9 +1135,12 @@ void InsaneRebel2::initLaserTexture(NutRenderer *nut, int spriteIdx) {
 		for (int row = 0; row < texHeight; row++) {
 			for (int col = 0; col < texWidth; col++) {
 				if (_laserTexture.pixels[row * texWidth + col] != 0) {
-					if (col < third) band1++;
-					else if (col < third * 2) band2++;
-					else band3++;
+					if (col < third)
+						band1++;
+					else if (col < third * 2)
+						band2++;
+					else
+						band3++;
 				}
 			}
 		}
@@ -1242,11 +1245,13 @@ void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int he
 			return;
 	} else {
 		if (x0 < clipLeft) {
-			if (x1 < clipLeft) return;
+			if (x1 < clipLeft)
+				return;
 			y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipLeft - x1)) / (int)(x0 - x1));
 			x0 = clipLeft;
 		} else if (x0 > clipRight) {
-			if (x1 > clipRight) return;
+			if (x1 > clipRight)
+				return;
 			y0 = y1 + (int16)(((int)(y0 - y1) * (int)(clipRight - x1)) / (int)(x0 - x1));
 			x0 = clipRight;
 		}
@@ -1265,11 +1270,13 @@ void InsaneRebel2::drawEdgeHighlightLine(byte *dst, int pitch, int width, int he
 			return;
 	} else {
 		if (y0 < clipTop) {
-			if (y1 < clipTop) return;
+			if (y1 < clipTop)
+				return;
 			x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipTop - y1)) / (int)(y0 - y1));
 			y0 = clipTop;
 		} else if (y0 > clipBottom) {
-			if (y1 > clipBottom) return;
+			if (y1 > clipBottom)
+				return;
 			x0 = x1 + (int16)(((int)(x0 - x1) * (int)(clipBottom - y1)) / (int)(y0 - y1));
 			y0 = clipBottom;
 		}
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index 85fc00054f1..f1365a0024a 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -221,7 +221,7 @@ int InsaneRebel2::runLevel2() {
 	int bonusCount = 0;     // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
 	int totalKills = 0;     // local_c: accumulated kill count across phases
 	int totalMisses = 0;    // Accumulated misses (sVar1 + sVar2 from hit counters)
-	int prevWaveState = 0;  // local_8: previous wave's state for Phase 3 randomization
+	int prevWaveState = 0;  // variantIdx: previous wave's state for Phase 3 randomization
 
 	// Play cutscene (02CUT.SAN)
 	playCinematic("LEV02/02CUT.SAN");
@@ -853,7 +853,7 @@ int InsaneRebel2::runLevel6() {
 		}
 
 		// Phase 1 survived — save score, play POST1
-		totalScore = _playerScore;  // local_8 = DAT_0047ab84
+		totalScore = _playerScore;  // variantIdx = DAT_0047ab84
 
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
@@ -876,7 +876,7 @@ int InsaneRebel2::runLevel6() {
 			if (_vm->shouldQuit())
 				return kLevelQuit;
 
-			// Accumulate score: local_8 = DAT_0047ab84 + local_8
+			// Accumulate score: variantIdx = DAT_0047ab84 + variantIdx
 			totalScore += _playerScore;
 
 			if (_playerShield > 0) {
@@ -1407,7 +1407,7 @@ int InsaneRebel2::runLevel11() {
 
 		{
 			uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
-			int local_8 = 0;  // Tracks variant for randomization threshold
+			int variantIdx = 0;  // Tracks variant for randomization threshold
 
 			// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
 			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
@@ -1421,14 +1421,14 @@ int InsaneRebel2::runLevel11() {
 				prevPhaseState = _rebelPhaseState;
 
 				// Randomization: wider range for first few waves
-				if (local_8 < 3) {
-					local_8 = _vm->_rnd.getRandomNumber(7);  // 0-7
+				if (variantIdx < 3) {
+					variantIdx = _vm->_rnd.getRandomNumber(7);  // 0-7
 				} else {
-					local_8 = _vm->_rnd.getRandomNumber(2);  // 0-2
+					variantIdx = _vm->_rnd.getRandomNumber(2);  // 0-2
 				}
 
 				const char *filename;
-				switch (local_8) {
+				switch (variantIdx) {
 				case 0:  filename = "LEV11/P3/11P03_A.SAN"; break;
 				case 1:  filename = "LEV11/P3/11P03_B.SAN"; break;
 				case 2:  filename = "LEV11/P3/11P03_C.SAN"; break;
@@ -1439,13 +1439,13 @@ int InsaneRebel2::runLevel11() {
 				default: filename = "LEV11/P3/11P03_E.SAN"; break;  // duplicate E
 				}
 
-				debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
 				splayer->setCurVideoFlags(0x428);
 				splayer->play(filename, 12);
 				_deathFrame = splayer->_frame;
 
-				// Threshold only for higher variants (original: (2 < local_8) - 1 & 0x14)
-				int16 threshold = (local_8 > 2) ? 0x14 : 0;
+				// Threshold only for higher variants (original: (2 < variantIdx) - 1 & 0x14)
+				int16 threshold = (variantIdx > 2) ? 0x14 : 0;
 				waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
 			}
 		}
@@ -1498,7 +1498,7 @@ int InsaneRebel2::runLevel11() {
 
 		// Only enter wave loop if not all basic types killed already
 		if ((_rebelPhaseState & 0x0e) < 0x0e) {
-			int local_8 = 0;
+			int variantIdx = 0;
 			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
 
 			while (waveSelect != 0xFFFF) {
@@ -1506,14 +1506,14 @@ int InsaneRebel2::runLevel11() {
 					return kLevelQuit;
 
 				// Wider randomization for first few waves
-				if (local_8 < 4) {
-					local_8 = _vm->_rnd.getRandomNumber(8);  // 0-8
+				if (variantIdx < 4) {
+					variantIdx = _vm->_rnd.getRandomNumber(8);  // 0-8
 				} else {
-					local_8 = _vm->_rnd.getRandomNumber(2);  // 0-2
+					variantIdx = _vm->_rnd.getRandomNumber(2);  // 0-2
 				}
 
 				const char *filename;
-				switch (local_8) {
+				switch (variantIdx) {
 				case 0:  filename = "LEV11/P3/11P03_G.SAN"; break;
 				case 1:  filename = "LEV11/P3/11P03_H.SAN"; break;
 				case 2:  filename = "LEV11/P3/11P03_I.SAN"; break;
@@ -1525,12 +1525,12 @@ int InsaneRebel2::runLevel11() {
 				default: filename = "LEV11/P3/11P03_L.SAN"; break;
 				}
 
-				debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
 				splayer->setCurVideoFlags(0x428);
 				splayer->play(filename, 12);
 				_deathFrame = splayer->_frame;
 
-				int16 threshold = (local_8 > 2) ? 0x14 : 0;
+				int16 threshold = (variantIdx > 2) ? 0x14 : 0;
 				waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
 			}
 		}
@@ -1725,15 +1725,15 @@ int InsaneRebel2::runLevel12() {
 					return kLevelQuit;
 
 				// Variant selection: (waveSelect & 2) controls which set
-				int local_8;
+				int variantIdx;
 				if ((waveSelect & 2) == 0) {
-					local_8 = _vm->_rnd.getRandomNumber(2) + 3;  // 3, 4, or 5
+					variantIdx = _vm->_rnd.getRandomNumber(2) + 3;  // 3, 4, or 5
 				} else {
-					local_8 = _vm->_rnd.getRandomNumber(2);      // 0, 1, or 2
+					variantIdx = _vm->_rnd.getRandomNumber(2);      // 0, 1, or 2
 				}
 
 				const char *filename;
-				switch (local_8) {
+				switch (variantIdx) {
 				case 0:  filename = "LEV12/P2/12P02_A.SAN"; break;
 				case 1:  filename = "LEV12/P2/12P02_B.SAN"; break;
 				case 2:  filename = "LEV12/P2/12P02_E.SAN"; break;
@@ -1742,13 +1742,13 @@ int InsaneRebel2::runLevel12() {
 				default: filename = "LEV12/P2/12P02_F.SAN"; break;
 				}
 
-				debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
 				splayer->setCurVideoFlags(0x428);
 				splayer->play(filename, 12);
 				_deathFrame = splayer->_frame;
 
 				// Variants E(2) and F(5) reset threshold to 0
-				int16 threshold = (local_8 == 2 || local_8 == 5) ? 0 : 0x14;
+				int16 threshold = (variantIdx == 2 || variantIdx == 5) ? 0 : 0x14;
 				waveSelect = processWaveEnd(6, &budget, threshold, 0);
 			}
 		}
@@ -1781,7 +1781,7 @@ int InsaneRebel2::runLevel12() {
 			return kLevelQuit;
 
 		{
-			int local_8 = 0;
+			int variantIdx = 0;
 			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
 
 			while (waveSelect != 0xFFFF) {
@@ -1789,14 +1789,14 @@ int InsaneRebel2::runLevel12() {
 					return kLevelQuit;
 
 				// Wider randomization for first few waves
-				if (local_8 < 4) {
-					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
+				if (variantIdx < 4) {
+					variantIdx = _vm->_rnd.getRandomNumber(5);  // 0-5
 				} else {
-					local_8 = _vm->_rnd.getRandomNumber(3);  // 0-3
+					variantIdx = _vm->_rnd.getRandomNumber(3);  // 0-3
 				}
 
 				const char *filename;
-				switch (local_8) {
+				switch (variantIdx) {
 				case 0:  filename = "LEV12/P3/12P03_C.SAN"; break;
 				case 1:  filename = "LEV12/P3/12P03_D.SAN"; break;
 				case 2:  filename = "LEV12/P3/12P03_A.SAN"; break;
@@ -1805,7 +1805,7 @@ int InsaneRebel2::runLevel12() {
 				default: filename = "LEV12/P3/12P03_F.SAN"; break;  // duplicate F
 				}
 
-				debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
 				splayer->setCurVideoFlags(0x428);
 				splayer->play(filename, 12);
 				_deathFrame = splayer->_frame;
@@ -1842,21 +1842,21 @@ int InsaneRebel2::runLevel12() {
 			return kLevelQuit;
 
 		{
-			int local_8 = 0;
+			int variantIdx = 0;
 			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
 
 			while (waveSelect != 0xFFFF) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
-				if (local_8 < 4) {
-					local_8 = _vm->_rnd.getRandomNumber(5);  // 0-5
+				if (variantIdx < 4) {
+					variantIdx = _vm->_rnd.getRandomNumber(5);  // 0-5
 				} else {
-					local_8 = _vm->_rnd.getRandomNumber(3);  // 0-3
+					variantIdx = _vm->_rnd.getRandomNumber(3);  // 0-3
 				}
 
 				const char *filename;
-				switch (local_8) {
+				switch (variantIdx) {
 				case 0:  filename = "LEV12/P4/12P04_C.SAN"; break;
 				case 1:  filename = "LEV12/P4/12P04_D.SAN"; break;
 				case 2:  filename = "LEV12/P4/12P04_A.SAN"; break;
@@ -1865,7 +1865,7 @@ int InsaneRebel2::runLevel12() {
 				default: filename = "LEV12/P4/12P04_F.SAN"; break;
 				}
 
-				debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x local_8=%d)", filename, _rebelPhaseState, local_8);
+				debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
 				splayer->setCurVideoFlags(0x428);
 				splayer->play(filename, 12);
 				_deathFrame = splayer->_frame;


Commit: 74da7770742ed0c33647dbf25a8a44f95a160e66
    https://github.com/scummvm/scummvm/commit/74da7770742ed0c33647dbf25a8a44f95a160e66
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:22+02:00

Commit Message:
SCUMM: RA1: Add initial proof of concept

Changed paths:
  A engines/scumm/insane/insane_rebel1.cpp
  A engines/scumm/insane/insane_rebel1.h
    engines/scumm/detection.h
    engines/scumm/detection_tables.h
    engines/scumm/module.mk
    engines/scumm/scumm-md5.h
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/detection.h b/engines/scumm/detection.h
index f0bf20c91ac..db1da1ba8df 100644
--- a/engines/scumm/detection.h
+++ b/engines/scumm/detection.h
@@ -223,6 +223,7 @@ enum ScummGameId {
 	GID_CMI,
 	GID_DIG,
 	GID_FT,
+	GID_REBEL1,
 	GID_REBEL2,
 	GID_INDY3,
 	GID_INDY4,
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index 33b26872482..d06d0de36c4 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -73,6 +73,7 @@ static const PlainGameDescriptor gameDescriptions[] = {
 	{ "indyzak", "Indiana Jones and the Last Crusade & Zak McKracken" },
 	{ "zakloom", "Zak McKracken & Loom" },
 	{ "ft", "Full Throttle" },
+	{ "rebel1", "Star Wars: Rebel Assault" },
 	{ "rebel2", "Star Wars: Rebel Assault II: The Hidden Empire" },
 	{ "dig", "The Dig" },
 	{ "comi", "The Curse of Monkey Island" },
@@ -228,6 +229,8 @@ static const GameSettings gameVariantsTable[] = {
 	{"ft",   "Remastered", 0, GID_FT,  7, 0, MDT_NONE, GF_DOUBLEFINE_PAK, UNK, GUIO5(GUIO_NOMIDI, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 	{"ft",   "Demo", 0, GID_FT,  7, 0, MDT_NONE, GF_DEMO, UNK, GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINALGUI, GAMEOPTION_LOWLATENCYAUDIO, GAMEOPTION_TTS)},
 
+	{"rebel1", "", 0, GID_REBEL1, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO1(GUIO_NOMIDI)},
+
 	{"rebel2", "", 0, GID_REBEL2, 7, 0, MDT_NONE, 0, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
 	{"rebel2", "Demo", 0, GID_REBEL2, 7, 0, MDT_NONE, GF_DEMO, Common::kPlatformDOS, GUIO3(GUIO_NOMIDI, GAMEOPTION_REBEL2_HIRES, GAMEOPTION_REBEL2_UNLOCK_ALL)},
 
@@ -499,6 +502,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 	{ "ft", "Vollgas Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, 0 },
 	{ "ft", "Vollgas Demo Data", kGenUnchanged, Common::DE_DEU, Common::kPlatformMacintosh, "Demo" },
 
+	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+
 	{ "rebel2", "RA2START.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 	{ "rebel2", "O_DEMO.SAN", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
 
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
new file mode 100644
index 00000000000..32a63fc0a43
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+	// Null out Insane base class pointers that the default constructor doesn't initialize
+	_smush_roadrashRip = nullptr;
+	_smush_roadrsh2Rip = nullptr;
+	_smush_roadrsh3Rip = nullptr;
+	_smush_goglpaltRip = nullptr;
+	_smush_tovista1Flu = nullptr;
+	_smush_tovista2Flu = nullptr;
+	_smush_toranchFlu = nullptr;
+	_smush_minedrivFlu = nullptr;
+	_smush_minefiteFlu = nullptr;
+	_smush_bensgoggNut = nullptr;
+	_smush_bencutNut = nullptr;
+	_smush_iconsNut = nullptr;
+	_smush_icons2Nut = nullptr;
+}
+
+InsaneRebel1::~InsaneRebel1() {
+}
+
+void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+	// Stub: no pre-rendering for RA1 yet
+}
+
+void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	int32 setupsan13, int32 curFrame, int32 maxFrame) {
+	// Stub: no post-rendering for RA1 yet
+}
+
+void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+	int16 par1, int16 par2, int16 par3, int16 par4) {
+	// RA1 does not use IACT chunks
+}
+
+void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
+	// Stub
+}
+
+void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
+	// GAME chunks contain gameplay data (opcodes for corridor, turret, etc.)
+	// For now just log and skip
+	if (subSize >= 4) {
+		uint32 opcode = b.readUint32BE();
+		debug(5, "InsaneRebel1::handleGameChunk: opcode=0x%02x size=%d", opcode >> 24, subSize);
+	}
+}
+
+void InsaneRebel1::playLevel(int level) {
+	Common::String filename = Common::String::format("LVL%d/L%dPLAY1L.ANM", level, level);
+	debug(1, "InsaneRebel1::playLevel(%d): playing %s", level, filename.c_str());
+
+	SmushPlayer *splayer = _vm->_splayer;
+	splayer->play(filename.c_str(), 12);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
new file mode 100644
index 00000000000..0ae72492f8e
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -0,0 +1,59 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined(SCUMM_INSANE_REBEL1_H) && defined(ENABLE_SCUMM_7_8)
+#define SCUMM_INSANE_REBEL1_H
+
+#include "scumm/insane/insane.h"
+
+namespace Scumm {
+
+class ScummEngine_v7;
+class SmushPlayer;
+
+/**
+ * Minimal stub for Star Wars: Rebel Assault (RA1).
+ * This is a proof-of-concept to load level 1.
+ */
+class InsaneRebel1 : public Insane {
+public:
+	InsaneRebel1(ScummEngine_v7 *scumm);
+	~InsaneRebel1() override;
+
+	void procPreRendering(byte *renderBitmap) override;
+	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+		int32 setupsan13, int32 curFrame, int32 maxFrame) override;
+	void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+		int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+		int16 par1, int16 par2, int16 par3, int16 par4) override;
+	void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
+
+	void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
+
+	void playLevel(int level);
+
+private:
+	ScummEngine_v7 *_vm;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 55a00dd601d..9ab40bf738f 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -134,6 +134,7 @@ MODULE_OBJS += \
 	insane/insane_scenes.o \
 	insane/insane_iact.o \
 	insane/insane_rebel.o \
+	insane/insane_rebel1.o \
 	insane/insane_rebel_audio.o \
 	insane/insane_rebel_iact.o \
 	insane/insane_rebel_levels.o \
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 90088d90b8a..72d64bcb65c 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -602,6 +602,7 @@ static const MD5Table md5table[] = {
 	{ "b886b0a5d909c7158a914e1d7c1c6c65", "loom", "EGA", "EGA", -1, Common::FR_FRA, Common::kPlatformDOS },
 	{ "b8955d7d23b4972229060d1592489fef", "freddicove", "HE 100", "", 41182, Common::NL_NLD, Common::kPlatformUnknown },
 	{ "b95347da0052a623f7a4bb15f425d897", "pajama3", "", "Demo", 13911, Common::IT_ITA, Common::kPlatformWindows },
+	{ "b9ac90e4411da1b6f7dd075cc4b03c2a", "rebel1", "", "", 214435, Common::EN_ANY, Common::kPlatformDOS },
 	{ "b9ba19ce376efc69be78ef3baef8d2b9", "monkey", "Mac", "", 8955, Common::EN_ANY, Common::kPlatformMacintosh },
 	{ "b9bb68c5d2c9b6e2d9c513a29a754a57", "puttmoon", "", "", 7828, Common::EN_ANY, Common::kPlatformDOS },
 	{ "b9fce749c3e9da0d41c78213c179631b", "samnmax", "CD", "Demo", 7505, Common::EN_ANY, Common::kPlatformDOS },
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 8f09594eaa2..102fb518908 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -53,6 +53,7 @@
 #include "scumm/players/player_towns.h"
 #include "scumm/insane/insane.h"
 #include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel1.h"
 #include "scumm/he/animation_he.h"
 #include "scumm/he/font_he.h"
 #include "scumm/he/intern_he.h"
@@ -1158,7 +1159,7 @@ Common::Error ScummEngine::init() {
 
 			_filenamePattern.pattern = "%.2d.LFL";
 			_filenamePattern.genMethod = kGenRoomNum;
-		} else if (_game.id == GID_REBEL2) {
+		} else if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2) {
 			_fileHandle = new ScummFile(this);
 		} else if (_game.platform == Common::kPlatformMacintosh) {
 			// The mac versions of Indy4, Sam&Max, DOTT, FT and The Dig used a
@@ -1507,7 +1508,7 @@ Common::Error ScummEngine::init() {
 
 	setupScumm(macResourceFile);
 
-	if (_game.id == GID_REBEL2) {
+	if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2) {
 		_setupIsComplete = true;
 		return Common::kNoError;
 	}
@@ -1786,6 +1787,38 @@ void ScummEngine::setupScumm(const Common::Path &macResourceFile) {
 
 #ifdef ENABLE_SCUMM_7_8
 void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
+	if (_game.id == GID_REBEL1) {
+		_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
+		initScreens(0, 200);
+
+		_numVariables = 256;
+		_scummVars = (int32 *)calloc(_numVariables, sizeof(int32));
+
+		_numArray = 50;
+		_res->allocResTypeData(rtString, 0, _numArray, kDynamicResTypeMode);
+		_res->allocResTypeData(rtSound, 0, 200, kDynamicResTypeMode);
+		_res->allocResTypeData(rtCostume, 0, 200, kDynamicResTypeMode);
+		_res->allocResTypeData(rtRoom, 0, 20, kDynamicResTypeMode);
+
+		defineArray(0, kIntArray, 0, 1000);
+		_numActors = 0;
+
+		setupScummVars();
+
+		_useOriginalGUI = false;
+
+		_sound = new Sound(this, _mixer, false);
+		_musicEngine = _imuseDigital = nullptr;
+		_insane = new InsaneRebel1(this);
+		_splayer = new SmushPlayer(this, nullptr, _insane);
+
+		_macGui = nullptr;
+		_charset = new CharsetRendererV7(this);
+
+		initBanners();
+		return;
+	}
+
 	if (_game.id == GID_REBEL2) {
 		_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
 		initScreens(0, 200);
@@ -2674,6 +2707,13 @@ int ScummEngine::getTalkSpeed() {
 
 Common::Error ScummEngine::go() {
 #ifdef ENABLE_SCUMM_7_8
+	if (_game.id == GID_REBEL1) {
+		ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
+		InsaneRebel1 *rebel = (InsaneRebel1 *)vm7->getInsane();
+		rebel->playLevel(1);
+		return Common::kNoError;
+	}
+
 	if (_game.id == GID_REBEL2) {
 		ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
 		InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
@@ -4177,7 +4217,7 @@ void ScummEngine_v7::scummLoop_handleSound() {
 		_imuseDigital->refreshScripts();
 	}
 
-	if (_game.id == GID_REBEL2)
+	if (_game.id == GID_REBEL1 || _game.id == GID_REBEL2)
 		return;
 
 	_splayer->setChanFlag(0, VAR(VAR_VOICE_MODE) != 0);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 10b62b142ef..f0df820560a 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -44,6 +44,7 @@
 
 #include "scumm/insane/insane.h"
 #include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel1.h"
 
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
@@ -827,8 +828,161 @@ byte *SmushPlayer::getVideoPalette() {
 
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
 
-void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize) {
+/**
+ * RA1 codec 4/5: Block-based frame decoder.
+ * Adapted from FFmpeg's libavcodec/sanm.c old_codec4() (LGPL 2.1+).
+ *
+ * Two tile tables (c4tbl[2][256][16]):
+ *   Table 0: Generated dither patterns from 16-color sub-palette
+ *   Table 1: Loaded from frame data (4bpp nibble-split tiles)
+ *
+ * Each 4x4 block is decoded by reading a bit flag (from mask bytes)
+ * and an index byte. c4tbl[bit][index] gives the 16-pixel pattern.
+ * Codec 4: index 0x80 means "skip block" (delta frame).
+ * Codec 5: all indices are drawn (keyframe).
+ */
+
+// Persistent tile table state for RA1 codec 4/5
+static uint8 s_ra1C4Tbl[2][256][16];
+static uint16 s_ra1C4Param = 0xFFFF;
+
+static void ra1Codec4GenTiles(uint16 param1) {
+	uint8 *dst = &s_ra1C4Tbl[0][0][0];
+
+	for (int i = 1; i < 16; i += 2) {
+		for (int k = 0; k < 16; k++) {
+			int j = i + param1;
+			int l = k + param1;
+			int m = (j + l) / 2;
+			int n = (j + m) / 2;
+			int o = (l + m) / 2;
+			if (j == m || l == m) {
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
+			} else {
+				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+				*dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
+				*dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
+			}
+		}
+	}
+
+	for (int i = 0; i < 16; i += 2) {
+		for (int k = 0; k < 16; k++) {
+			int j = i + param1;
+			int l = k + param1;
+			int m = (j + l) / 2;
+			int n = (j + m) / 2;
+			int o = (l + m) / 2;
+			if (m == j || m == l) {
+				*dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
+				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
+			} else {
+				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+				*dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
+				*dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
+			}
+		}
+	}
+}
+
+static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
+	uint8 *dst = &s_ra1C4Tbl[1][0][0];
+	int loop = param2 * 8;
+
+	if (param2 > 256 || remaining < loop)
+		return false;
+
+	for (int i = 0; i < loop; i++) {
+		byte c = *src++;
+		remaining--;
+		*dst++ = (c >> 4) + clr;
+		*dst++ = (c & 0xF) + clr;
+	}
+	return true;
+}
+
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+						 int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
+	const int mx = pitch;  // framebuffer width
+	const int my = height; // framebuffer height
+
+	// Generate dither tile table if palette base changed
+	if (s_ra1C4Param != param) {
+		ra1Codec4GenTiles(param);
+		s_ra1C4Param = param;
+	}
+
+	// Load frame-specific tiles from data stream (4bpp nibble-split)
+	int remaining = dataSize;
+	const byte *data = src;
+	if (parm2 > 0) {
+		if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
+			warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
+			return;
+		}
+	}
+
+	// Decode blocks: iterate columns by 4, then rows by 4 (column-major order)
+	for (int j = 0; j < width; j += 4) {
+		byte mask = 0, bits = 0;
+		int x = left + j;
+		for (int i = 0; i < height; i += 4) {
+			int y = top + i;
+			int bit = 0;
+
+			if (parm2 > 0) {
+				if (bits == 0) {
+					if (remaining < 1)
+						return;
+					mask = *data++;
+					remaining--;
+					bits = 8;
+				}
+				bit = !!(mask & 0x80);
+				mask <<= 1;
+				bits--;
+			}
+
+			if (remaining < 1)
+				return;
+			byte idx = *data++;
+			remaining--;
+
+			// Codec 4: index 0x80 = skip (delta). Codec 5: no skip.
+			if (bit == 0 && idx == 0x80 && codec != 5)
+				continue;
+			if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
+				continue;
+
+			const byte *gs = &s_ra1C4Tbl[bit][idx][0];
+			if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
+				// Fast path: fully within bounds
+				for (int k = 0; k < 4; k++, gs += 4)
+					memcpy(dst + x + (y + k) * pitch, gs, 4);
+			} else {
+				// Slow path: clipping
+				for (int k = 0; k < 4; k++) {
+					for (int l = 0; l < 4; l++, gs++) {
+						int yo = y + k, xo = x + l;
+						if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
+							*(dst + yo * pitch + xo) = *gs;
+					}
+				}
+			}
+		}
+	}
+}
+
+void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize, uint8 ra1Param, uint16 ra1Parm2) {
 	if ((height == 242) && (width == 384)) {
 		int bufSize = 242 * 384;
 		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
@@ -903,13 +1057,17 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		if (_deltaGlyphsCodec)
 			_deltaGlyphsCodec->decode(_dst, src);
 		break;
+	case SMUSH_CODEC_RA1_DELTA:
+	case SMUSH_CODEC_RA1_BLOCK:
+		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, ra1Param, ra1Parm2, codec);
+		break;
 	case SMUSH_CODEC_UNCOMPRESSED:
 		smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
 		break;
 	default:
 		if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
 			break;
-		if (isRA2()) {
+		if (isRA1() || isRA2()) {
 			debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
 				codec, left, top, width, height);
 			break;
@@ -984,13 +1142,19 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	}
 
 	int codec = b.readUint16LE();
+	uint8 ra1Param = 0;   // RA1: palette base byte (FOBJ byte[1])
+	uint16 ra1Parm2 = 0;  // RA1: tile count (FOBJ bytes[12-13])
+	if (isRA1()) {
+		ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
+		codec &= 0xFF;                  // byte[0] = actual codec number
+	}
 	int left = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
 	int top = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
 	int width = b.readUint16LE();
 	int height = b.readUint16LE();
 
 	b.readUint16LE();
-	b.readUint16LE();
+	ra1Parm2 = b.readUint16LE();
 
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
 		_frame, codec, left, top, width, height, subSize - 14);
@@ -1013,7 +1177,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 		ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
 	}
 
-	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
+	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
 
 	free(chunk_buffer);
 }
@@ -1096,6 +1260,24 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			if (isRA2())
 				ra2HandleGost(subSize, b);
 			break;
+		// RA1-specific chunk types: skip gracefully
+		case MKTAG('G','A','M','E'):
+			if (isRA1()) {
+				InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+				rebel1->handleGameChunk(subSize, b);
+			}
+			break;
+		case MKTAG('G','A','M','2'):
+		case MKTAG('F','A','D','E'):
+		case MKTAG('S','E','G','A'):
+		case MKTAG('A','D','L',' '):
+		case MKTAG('A','D','L','2'):
+		case MKTAG('S','B','L',' '):
+		case MKTAG('S','B','L','2'):
+		case MKTAG('P','S','D','2'):
+		case MKTAG('P','V','O','C'):
+			debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
+			break;
 		default:
 			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 		}
@@ -1157,8 +1339,15 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			setDirtyColors(0, 255);
 		}
 
-		_width = READ_LE_UINT16(&headerContent[4]);
-		_height = READ_LE_UINT16(&headerContent[6]);
+		if (isRA1()) {
+			// v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6
+			// RA1 resolution is always 384x242, set from first FOBJ
+			_width = 384;
+			_height = 242;
+		} else {
+			_width = READ_LE_UINT16(&headerContent[4]);
+			_height = READ_LE_UINT16(&headerContent[6]);
+		}
 
 		if (isRA2())
 			ra2FixupAnimHeader();
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index cc51ccd96f6..b3914043bea 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -78,6 +78,8 @@ namespace Scumm {
 
 #define SMUSH_CODEC_RLE          1
 #define SMUSH_CODEC_RLE_ALT      3
+#define SMUSH_CODEC_RA1_DELTA    4    // RA1: Delta block codec (skip on idx 0x80)
+#define SMUSH_CODEC_RA1_BLOCK    5    // RA1: Block-based frame codec (no skip)
 #define SMUSH_CODEC_UNCOMPRESSED 20
 #define SMUSH_CODEC_LINE_UPDATE  21   // RA2: Skip/copy with literal pixels
 #define SMUSH_CODEC_SKIP_RLE     23   // RA2: Skip/copy with embedded RLE
@@ -289,7 +291,7 @@ private:
 	void tryCmpFile(const char *filename);
 
 	bool readString(const char *file);
-	void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0);
+	void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0, uint8 ra1Param = 0, uint16 ra1Parm2 = 0);
 	void handleAnimHeader(int32 subSize, Common::SeekableReadStream &);
 	void handleFrame(int32 frameSize, Common::SeekableReadStream &);
 	void handleNewPalette(int32 subSize, Common::SeekableReadStream &);
@@ -304,7 +306,8 @@ private:
 	void handleLoad(int32 subSize, Common::SeekableReadStream &);  // RA2 only (impl in smush_player_ra2.cpp)
 	void readPalette(byte *, Common::SeekableReadStream &);
 
-	// RA2-specific methods (implemented in smush_player_ra2.cpp)
+	// RA1/RA2 identification
+	bool isRA1() const;
 	bool isRA2() const;
 	void ra2InitFields();
 	void ra2DestroyFields();
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index fc2da674632..0bfe0e2ae8b 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -40,6 +40,10 @@
 
 namespace Scumm {
 
+bool SmushPlayer::isRA1() const {
+	return _vm->_game.id == GID_REBEL1;
+}
+
 bool SmushPlayer::isRA2() const {
 	return _vm->_game.id == GID_REBEL2;
 }


Commit: 4c5afc1201bf9200eeb1b05254088b751a32a15c
    https://github.com/scummvm/scummvm/commit/4c5afc1201bf9200eeb1b05254088b751a32a15c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:22+02:00

Commit Message:
SCUMM: RA1: Extend initial proof of concept

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 32a63fc0a43..b621ccf72a4 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -19,13 +19,51 @@
  *
  */
 
+#include "common/system.h"
+#include "common/events.h"
+#include "common/endian.h"
 #include "scumm/scumm_v7.h"
+#include "scumm/scumm.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/insane/insane_rebel1.h"
 
 namespace Scumm {
 
+// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
+// Same algorithm as NutRenderer::codec21 but without palette tracking.
+static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
+	while (height--) {
+		byte *dstNext = dst + pitch;
+		const byte *srcNext = src + 2 + READ_LE_UINT16(src);
+		src += 2;
+		int len = width;
+		byte *d = dst;
+		do {
+			int offs = READ_LE_UINT16(src); src += 2;
+			d += offs;
+			len -= offs;
+			if (len <= 0)
+				break;
+			int w = READ_LE_UINT16(src) + 1; src += 2;
+			len -= w;
+			if (len < 0)
+				w += len;
+			memcpy(d, src, w);
+			src += w;
+			d += w;
+		} while (len > 0);
+		dst = dstNext;
+		src = srcNext;
+	}
+}
+
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+	_shipPosX = 192;
+	_shipPosY = 160;
+	_shipDirIndex = 0;
+	_screenWidth = 384;
+	_screenHeight = 242;
+
 	// Null out Insane base class pointers that the default constructor doesn't initialize
 	_smush_roadrashRip = nullptr;
 	_smush_roadrsh2Rip = nullptr;
@@ -45,39 +83,219 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 InsaneRebel1::~InsaneRebel1() {
 }
 
+// Load an RA1 NUT sprite file (ANIM v1).
+// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
+// FRME containers. This loader handles that padding properly, unlike the
+// shared NutRenderer::loadFont which assumes even-size chunks.
+bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
+	ScummFile *file = _vm->instantiateScummFile();
+	_vm->openFile(*file, filename);
+	if (!file->isOpen()) {
+		warning("InsaneRebel1::loadRA1Nut: can't open %s", filename);
+		delete file;
+		return false;
+	}
+
+	uint32 tag = file->readUint32BE();
+	if (tag != MKTAG('A','N','I','M')) {
+		warning("InsaneRebel1::loadRA1Nut: no ANIM tag in %s", filename);
+		delete file;
+		return false;
+	}
+	uint32 animSize = file->readUint32BE();
+	byte *data = (byte *)malloc(animSize);
+	file->read(data, animSize);
+	file->close();
+	delete file;
+
+	// data[0..3] = AHDR tag, data[4..7] = AHDR size
+	if (READ_BE_UINT32(data) != MKTAG('A','H','D','R')) {
+		warning("InsaneRebel1::loadRA1Nut: no AHDR in %s", filename);
+		free(data);
+		return false;
+	}
+
+	bank.numSprites = READ_LE_UINT16(data + 10);
+	bank.sprites = new RA1Sprite[bank.numSprites];
+
+	// Pass 1: Walk chunks with alignment to compute total decoded size.
+	uint32 offset = 0;
+	uint32 decodedSize = 0;
+	for (int i = 0; i < bank.numSprites; i++) {
+		// Skip current chunk (AHDR or previous FRME)
+		uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
+		offset += chunkSize + 8;
+		if (chunkSize & 1) offset++;  // Word-align
+
+		// Now at FRME; skip its header to reach FOBJ
+		offset += 8;
+		if (offset + 22 > animSize) break;
+
+		uint16 w = READ_LE_UINT16(data + offset + 14);
+		uint16 h = READ_LE_UINT16(data + offset + 16);
+		decodedSize += w * h;
+	}
+
+	bank.decodedData = (byte *)calloc(decodedSize, 1);
+	byte *decPtr = bank.decodedData;
+
+	// Pass 2: Decode sprites.
+	offset = 0;
+	for (int i = 0; i < bank.numSprites; i++) {
+		uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
+		offset += chunkSize + 8;
+		if (chunkSize & 1) offset++;
+
+		offset += 8;  // Skip FRME header → now at FOBJ
+		if (offset + 22 > animSize) break;
+
+		int codec = READ_LE_UINT16(data + offset + 8);
+		bank.sprites[i].xoffs = READ_LE_INT16(data + offset + 10);
+		bank.sprites[i].yoffs = READ_LE_INT16(data + offset + 12);
+		bank.sprites[i].width = READ_LE_UINT16(data + offset + 14);
+		bank.sprites[i].height = READ_LE_UINT16(data + offset + 16);
+		bank.sprites[i].data = decPtr;
+
+		int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
+		const byte *fobjData = data + offset + 22;
+
+		if (codec == 21) {
+			decodeBomp(decPtr, fobjData, bank.sprites[i].width,
+					   bank.sprites[i].height, bank.sprites[i].width);
+		} else {
+			warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
+		}
+
+		decPtr += pixelCount;
+	}
+
+	free(data);
+	debug(1, "InsaneRebel1::loadRA1Nut('%s'): %d sprites, %d bytes decoded",
+		  filename, bank.numSprites, decodedSize);
+	return true;
+}
+
+void InsaneRebel1::loadLevelSprites(int level) {
+	Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
+	loadRA1Nut(filename.c_str(), _shipBank);
+}
+
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
-	// Stub: no pre-rendering for RA1 yet
 }
 
 void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 	int32 setupsan13, int32 curFrame, int32 maxFrame) {
-	// Stub: no post-rendering for RA1 yet
+
+	if (_shipBank.numSprites == 0 || !renderBitmap)
+		return;
+
+	int width = _player->_width;
+	int height = _player->_height;
+	if (width == 0) width = _screenWidth;
+	if (height == 0) height = _screenHeight;
+	int pitch = width;
+
+	// Read mouse position and map to normalized range (-1.0 to 1.0)
+	Common::Point mousePos = g_system->getEventManager()->getMousePos();
+	float normX = CLIP((float)(mousePos.x - width / 2) / (float)(width / 2), -1.0f, 1.0f);
+	float normY = CLIP((float)(mousePos.y - height / 2) / (float)(height / 2), -1.0f, 1.0f);
+
+	// Smooth ship position toward mouse (max 8 pixels/frame horizontal, 6 vertical)
+	int targetX = width / 2 + (int)(normX * width / 4);
+	int targetY = height / 2 + (int)(normY * height / 4);
+	_shipPosX += CLIP(targetX - _shipPosX, -8, 8);
+	_shipPosY += CLIP(targetY - _shipPosY, -6, 6);
+
+	// Map mouse to direction index (5 horizontal x 7 vertical = 35 core sprites).
+	int hZone = CLIP((int)((normX + 1.0f) * 2.5f), 0, 4);
+	int vZone = CLIP((int)((normY + 1.0f) * 3.5f), 0, 6);
+	_shipDirIndex = CLIP(hZone * 7 + vZone, 0, _shipBank.numSprites - 1);
+
+	renderShip(renderBitmap, pitch, width, height);
+}
+
+void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
+	if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
+		return;
+
+	const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+	int drawX = _shipPosX - spr.width / 2;
+	int drawY = _shipPosY - spr.height / 2;
+	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+}
+
+void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
+								int x, int y, const RA1Sprite &spr) {
+	if (!spr.data || spr.width <= 0 || spr.height <= 0)
+		return;
+
+	int drawX = x, drawY = y, drawW = spr.width, drawH = spr.height;
+	int srcOffsetX = 0, srcOffsetY = 0;
+
+	if (drawX < 0) { srcOffsetX = -drawX; drawW += drawX; drawX = 0; }
+	if (drawY < 0) { srcOffsetY = -drawY; drawH += drawY; drawY = 0; }
+	if (drawX + drawW > width) drawW = width - drawX;
+	if (drawY + drawH > height) drawH = height - drawY;
+	if (drawW <= 0 || drawH <= 0)
+		return;
+
+	for (int iy = 0; iy < drawH; iy++) {
+		const byte *s = spr.data + (srcOffsetY + iy) * spr.width + srcOffsetX;
+		byte *d = dst + (drawY + iy) * pitch + drawX;
+		for (int ix = 0; ix < drawW; ix++) {
+			byte px = s[ix];
+			if (px != 0)
+				d[ix] = px;
+		}
+	}
 }
 
 void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 	int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 	int16 par1, int16 par2, int16 par3, int16 par4) {
-	// RA1 does not use IACT chunks
 }
 
 void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
-	// Stub
 }
 
 void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
-	// GAME chunks contain gameplay data (opcodes for corridor, turret, etc.)
-	// For now just log and skip
-	if (subSize >= 4) {
-		uint32 opcode = b.readUint32BE();
-		debug(5, "InsaneRebel1::handleGameChunk: opcode=0x%02x size=%d", opcode >> 24, subSize);
+	if (subSize < 8)
+		return;
+
+	uint32 opcode = b.readUint32BE();
+	uint32 param1 = b.readUint32BE();
+
+	switch (opcode) {
+	case 0x5E:
+		debug(5, "InsaneRebel1: GAME 0x5E (mode) param=%d", param1);
+		break;
+	case 0x5D:
+		debug(5, "InsaneRebel1: GAME 0x5D (link) param=%d", param1);
+		break;
+	case 0x5F:
+		debug(5, "InsaneRebel1: GAME 0x5F (event) param=%d", param1);
+		break;
+	case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B:
+	case 0x19: case 0x1A:
+	case 0x0D: case 0x0E:
+		if (subSize >= 20) {
+			b.readUint32BE(); b.readUint32BE(); b.readUint32BE();
+		}
+		break;
+	default:
+		debug(7, "InsaneRebel1: GAME unknown 0x%02x size=%d", opcode, subSize);
+		break;
 	}
 }
 
 void InsaneRebel1::playLevel(int level) {
+	loadLevelSprites(level);
+
 	Common::String filename = Common::String::format("LVL%d/L%dPLAY1L.ANM", level, level);
 	debug(1, "InsaneRebel1::playLevel(%d): playing %s", level, filename.c_str());
 
 	SmushPlayer *splayer = _vm->_splayer;
+	_player = splayer;
 	splayer->play(filename.c_str(), 12);
 }
 
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0ae72492f8e..0ea0d7b2266 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -29,9 +29,28 @@ namespace Scumm {
 class ScummEngine_v7;
 class SmushPlayer;
 
+// Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
+// Separate from NutRenderer to avoid modifying shared NUT parsing code.
+struct RA1Sprite {
+	int16 xoffs;
+	int16 yoffs;
+	uint16 width;
+	uint16 height;
+	byte *data;  // Decoded pixel data (width * height bytes, 0 = transparent)
+};
+
+struct RA1SpriteBank {
+	int numSprites;
+	RA1Sprite *sprites;
+	byte *decodedData;  // Single allocation for all decoded pixels
+
+	RA1SpriteBank() : numSprites(0), sprites(nullptr), decodedData(nullptr) {}
+	~RA1SpriteBank() { delete[] sprites; free(decodedData); }
+};
+
 /**
- * Minimal stub for Star Wars: Rebel Assault (RA1).
- * This is a proof-of-concept to load level 1.
+ * Proof-of-concept for Star Wars: Rebel Assault (RA1).
+ * Loads level 1 and renders the player ship sprite.
  */
 class InsaneRebel1 : public Insane {
 public:
@@ -51,7 +70,22 @@ public:
 	void playLevel(int level);
 
 private:
+	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
+	void loadLevelSprites(int level);
+	void renderShip(byte *dst, int pitch, int width, int height);
+	void renderSprite(byte *dst, int pitch, int width, int height,
+					  int x, int y, const RA1Sprite &sprite);
+
 	ScummEngine_v7 *_vm;
+
+	RA1SpriteBank _shipBank;
+
+	int _shipPosX;
+	int _shipPosY;
+	int _shipDirIndex;
+
+	int _screenWidth;
+	int _screenHeight;
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index f0df820560a..4a093e53b64 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1004,6 +1004,9 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if ((height == 242) && (width == 384)) {
 		_width = width;
 		_height = height;
+	} else if (isRA1() && _dst == _specialBuffer) {
+		// RA1: small overlay FOBJs (codec 1) should not override dimensions
+		// set by the main 384x242 codec 5 FOBJ
 	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
 		// RA2: preserve _width/_height set during buffer allocation
 	} else {
@@ -1599,7 +1602,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 	// Hide mouse
 	bool oldMouseState = CursorMan.showMouse(false);
-	if (isRA2())
+	if (isRA1() || isRA2())
 		insanity(true);
 
 	// Load the video
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index b3914043bea..3d43b97fb51 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -100,6 +100,7 @@ class Insane;
 
 class SmushPlayer {
 	friend class Insane;
+	friend class InsaneRebel1;
 	friend class InsaneRebel2;
 	friend class SmushMultiFont;
 private:


Commit: 8917d7f0091b0fd3c1ea8721335125b41241fe57
    https://github.com/scummvm/scummvm/commit/8917d7f0091b0fd3c1ea8721335125b41241fe57
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:22+02:00

Commit Message:
SCUMM: RA1: Add initial UI rendering code

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index b621ccf72a4..fdd20e2c2fe 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -29,6 +29,20 @@
 
 namespace Scumm {
 
+// RA1 coordinate constants (scaled from RA2's 424x260 → 384x242)
+// RA2 center: (212, 130), RA1 center: (192, 121)
+// RA2 bounds: [20, 404]x[20, 240], RA1 bounds: [18, 366]x[18, 224]
+static const int16 kCenterX = 192;
+static const int16 kCenterY = 121;
+static const int16 kMinX = 18;
+static const int16 kMaxX = 366;
+static const int16 kMinY = 18;
+static const int16 kMaxY = 224;
+
+// Perspective focal lengths (scaled from RA2: focalX=0x2b, focalY=0x19)
+static const int16 kFocalX = 39;   // 0x2b * 384/424 ≈ 39
+static const int16 kFocalY = 23;   // 0x19 * 242/260 ≈ 23
+
 // Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
 // Same algorithm as NutRenderer::codec21 but without palette tracking.
 static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
@@ -58,12 +72,37 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
 }
 
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
-	_shipPosX = 192;
-	_shipPosY = 160;
-	_shipDirIndex = 0;
 	_screenWidth = 384;
 	_screenHeight = 242;
 
+	_shipPosX = kCenterX;
+	_shipPosY = kCenterY;
+	_shipDirIndex = 17;  // Center of 5x7 grid (2*7 + 3)
+
+	_corridorLeftX = kMinX;
+	_corridorTopY = kMinY;
+	_corridorRightX = kMaxX;
+	_corridorBottomY = kMaxY;
+
+	_smoothedVelocity = 0;
+	_verticalInput = 0;
+	memset(_velocityHistory, 0, sizeof(_velocityHistory));
+	memset(_windHistoryX, 0, sizeof(_windHistoryX));
+	memset(_windHistoryY, 0, sizeof(_windHistoryY));
+	_windParamX = 0;
+	_windParamY = 0;
+
+	_perspectiveX = 0;
+	_perspectiveY = 0;
+	_viewShift = 0;
+
+	_flyControlMode = 0;
+	_hitCooldown = 0;
+
+	_playerDamage = 0;
+	_score = 0;
+	_pilots = 3;
+
 	// Null out Insane base class pointers that the default constructor doesn't initialize
 	_smush_roadrashRip = nullptr;
 	_smush_roadrsh2Rip = nullptr;
@@ -178,6 +217,7 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 void InsaneRebel1::loadLevelSprites(int level) {
 	Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
 	loadRA1Nut(filename.c_str(), _shipBank);
+	loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
 }
 
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
@@ -195,23 +235,164 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _screenHeight;
 	int pitch = width;
 
-	// Read mouse position and map to normalized range (-1.0 to 1.0)
+	updateShipPhysics();
+	renderShip(renderBitmap, pitch, width, height);
+	renderHUD(renderBitmap, pitch, width, height);
+}
+
+// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
+// Mouse input → velocity history averaging → position delta → corridor collision → perspective.
+void InsaneRebel1::updateShipPhysics() {
+	// Decrement hit cooldown
+	if (_hitCooldown > 0)
+		_hitCooldown--;
+
+	// --- Step 1: Mouse input as offset from screen center ---
+	// RA2 uses _vm->_mouse (0-319, 0-199), center (160, 100)
+	// RA1 uses event manager mouse pos, center (192, 121)
 	Common::Point mousePos = g_system->getEventManager()->getMousePos();
-	float normX = CLIP((float)(mousePos.x - width / 2) / (float)(width / 2), -1.0f, 1.0f);
-	float normY = CLIP((float)(mousePos.y - height / 2) / (float)(height / 2), -1.0f, 1.0f);
+	int16 inputX = (int16)(mousePos.x - kCenterX);
+	int16 inputY = (int16)(mousePos.y - kCenterY);
+
+	// Clamp to ±kCenterX horizontal, ±127 vertical
+	inputX = CLIP<int16>(inputX, -kCenterX, kCenterX);
+	inputY = CLIP<int16>(inputY, -127, 127);
+
+	// --- Step 2: Scale to [-127, 127] ---
+	int16 scaledInputX = (int16)((inputX * 127) / kCenterX);
+	int16 scaledInputY = inputY;
+
+	// --- Step 3: Velocity history + smoothed average ---
+	for (int i = 24; i > 0; i--)
+		_velocityHistory[i] = _velocityHistory[i - 1];
+	_velocityHistory[0] = scaledInputX;
+
+	const int smoothWindow = 5;
+	int velSum = 0;
+	for (int i = 0; i < smoothWindow; i++)
+		velSum += _velocityHistory[i];
+	_smoothedVelocity = (int16)(velSum / smoothWindow);
+
+	// --- Step 4: Wind history ---
+	for (int i = 14; i > 0; i--)
+		_windHistoryX[i] = _windHistoryX[i - 1];
+	_windHistoryX[0] = _windParamX;
+
+	for (int i = 14; i > 0; i--)
+		_windHistoryY[i] = _windHistoryY[i - 1];
+	_windHistoryY[0] = _windParamY;
+
+	// Wind effect (multiplier = 0 for now, no level data)
+	int16 windEffectX = 0;
+	int16 windEffectY = 0;
+
+	// --- Step 5: Position delta ---
+	const int16 levelSpeed = 32;
+	const int16 levelYSpeed = 48;
+	int16 absSmoothVel = ABS(_smoothedVelocity);
+	int16 positionDeltaX;
+
+	if (_flyControlMode == 1) {
+		// Mode 1: Full cross-axis coupling
+		if (scaledInputX < 1)
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
+		else
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
+	} else {
+		// Mode 0/2/3: Reduced cross-axis coupling
+		if (scaledInputX < 1)
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+		else
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+	}
+
+	// Clamp X delta to ±12 per frame
+	positionDeltaX = CLIP<int16>(positionDeltaX, -12, 12);
+	_shipPosX += positionDeltaX;
+
+	// Y delta
+	if (_flyControlMode == 1) {
+		int yDelta = (levelYSpeed * scaledInputY - (windEffectY >> 1)) >> 10;
+		yDelta = CLIP(yDelta, -12, 12);
+		_shipPosY -= (int16)yDelta;
+	} else {
+		_shipPosY -= (int16)((levelYSpeed * scaledInputY) >> 10);
+	}
 
-	// Smooth ship position toward mouse (max 8 pixels/frame horizontal, 6 vertical)
-	int targetX = width / 2 + (int)(normX * width / 4);
-	int targetY = height / 2 + (int)(normY * height / 4);
-	_shipPosX += CLIP(targetX - _shipPosX, -8, 8);
-	_shipPosY += CLIP(targetY - _shipPosY, -6, 6);
+	_verticalInput = scaledInputY;
+
+	// --- Step 6: Position clamping ---
+	_shipPosX = CLIP<int16>(_shipPosX, kMinX, kMaxX);
+	_shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
+
+	// --- Step 7: Corridor collision (modes 0 and 2) ---
+	if (_flyControlMode == 0 || _flyControlMode == 2) {
+		if (_corridorRightX < _shipPosX) {
+			_shipPosX = _corridorRightX;
+			if (_hitCooldown < 5) {
+				for (int i = 0; i < 25; i++)
+					_velocityHistory[i] = -127;
+				_hitCooldown = 10;
+			}
+		}
+		if (_shipPosX < _corridorLeftX) {
+			_shipPosX = _corridorLeftX;
+			if (_hitCooldown < 5) {
+				for (int i = 0; i < 25; i++)
+					_velocityHistory[i] = 127;
+				_hitCooldown = 10;
+			}
+		}
+		if (_corridorBottomY < _shipPosY)
+			_shipPosY = _corridorBottomY;
+		if (_shipPosY < _corridorTopY)
+			_shipPosY = _corridorTopY;
+	}
 
-	// Map mouse to direction index (5 horizontal x 7 vertical = 35 core sprites).
-	int hZone = CLIP((int)((normX + 1.0f) * 2.5f), 0, 4);
-	int vZone = CLIP((int)((normY + 1.0f) * 3.5f), 0, 6);
-	_shipDirIndex = CLIP(hZone * 7 + vZone, 0, _shipBank.numSprites - 1);
+	// --- Step 8: Perspective offsets ---
+	{
+		int absOffX = ABS(_shipPosX - kCenterX);
+		if (absOffX > 0)
+			_perspectiveX = (int16)((kFocalX * kCenterX * absOffX) /
+				((kCenterX - kFocalX) * absOffX + kFocalX * kCenterX));
+		else
+			_perspectiveX = 0;
+		if (_shipPosX < kCenterX + 1)
+			_perspectiveX = -_perspectiveX;
+
+		int absOffY = ABS(_shipPosY - kCenterY);
+		if (absOffY > 0)
+			_perspectiveY = (int16)((kFocalY * kCenterY * absOffY) /
+				((kCenterY - kFocalY) * absOffY + kFocalY * kCenterY));
+		else
+			_perspectiveY = 0;
+		if (_shipPosY < kCenterY + 1)
+			_perspectiveY = -_perspectiveY;
+	}
 
-	renderShip(renderBitmap, pitch, width, height);
+	_viewShift = CLIP<int16>(_smoothedVelocity, -127, 127);
+
+	// --- Step 9: Direction sprite (5x7 grid with hysteresis) ---
+	// vDir from vertical input: (0xa0 - verticalInput) >> 6
+	int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
+	vDir = CLIP<int16>(vDir, 0, 4);
+
+	// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
+	int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
+	hDir = CLIP<int16>(hDir, 0, 6);
+
+	// Hysteresis at center positions
+	if (hDir == 3 && ABS(_smoothedVelocity) > 10)
+		hDir = (_smoothedVelocity < 1) ? 4 : 2;
+	if (vDir == 2 && ABS(_verticalInput) > 15)
+		vDir = (_verticalInput < 1) ? 3 : 1;
+
+	_shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
+
+	debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d corridor=[%d,%d]-[%d,%d]",
+		_shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
+		positionDeltaX, _shipDirIndex,
+		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
 void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
@@ -219,11 +400,117 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 		return;
 
 	const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
-	int drawX = _shipPosX - spr.width / 2;
-	int drawY = _shipPosY - spr.height / 2;
+
+	// Position: game coords → screen coords via perspective transform
+	// Adapted from RA2's renderHandler7Ship:
+	//   shipCenterX = (shipX - center) + perspX + screenCenterX
+	int drawX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX - spr.width / 2;
+	int drawY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY - spr.height / 2;
+
 	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 }
 
+// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
+// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
+void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
+	if (_displayBank.numSprites == 0)
+		return;
+
+	const RA1Sprite &bar = _displayBank.sprites[0];
+
+	// DISPLAY.NUT sprite is 320×19 at xoffs=0, yoffs=176 in the original game.
+	// Video FOBJs fill the full 384×242 buffer from (0,0), so use sprite offsets directly.
+	int hudX = bar.xoffs;
+	int hudY = bar.yoffs;
+
+	// Draw the status bar background with transparency (pixel 0 = transparent)
+	if (bar.data && bar.width > 0 && bar.height > 0) {
+		int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
+		int srcOffX = 0, srcOffY = 0;
+		if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
+		if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
+		if (drawX + drawW > width) drawW = width - drawX;
+		if (drawY + drawH > height) drawH = height - drawY;
+
+		for (int iy = 0; iy < drawH; iy++) {
+			const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
+			byte *d = dst + (drawY + iy) * pitch + drawX;
+			for (int ix = 0; ix < drawW; ix++) {
+				byte px = s[ix];
+				if (px != 0)
+					d[ix] = px;
+			}
+		}
+
+		debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
+			hudX, hudY, bar.width, bar.height);
+	}
+
+	// Draw damage bar (green rectangle).
+	// From the screenshot: bar starts around x=56, y=8 within the HUD sprite,
+	// max width ~76 pixels, height ~5 pixels. Green = palette index ~0xA0.
+	{
+		int barMaxW = 76;
+		int barH = 5;
+		int barX = hudX + 56;
+		int barY = hudY + 8;
+		int fillW = barMaxW - (barMaxW * _playerDamage / 255);
+		if (fillW < 0) fillW = 0;
+		if (fillW > barMaxW) fillW = barMaxW;
+
+		// Find a green palette index — use 0xA0 (common green in RA1 palette)
+		byte greenColor = 0xA0;
+
+		for (int iy = 0; iy < barH && barY + iy < height; iy++) {
+			byte *d = dst + (barY + iy) * pitch + barX;
+			for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
+				d[ix] = greenColor;
+			}
+		}
+	}
+
+	// Draw score as decimal digits.
+	// From screenshot: score area starts around x=265 within HUD, using palette text color.
+	// For now, just draw simple 4×7 digit bitmaps.
+	{
+		char scoreStr[8];
+		snprintf(scoreStr, sizeof(scoreStr), "%06d", _score);
+		int digitX = hudX + 265;
+		int digitY = hudY + 7;
+		byte textColor = 0xFF;  // White
+
+		for (int c = 0; c < 6 && scoreStr[c]; c++) {
+			int digit = scoreStr[c] - '0';
+			// Simple 3×5 digit rendering
+			static const uint16 digitPatterns[10] = {
+				0x7B6F, // 0: 111 011 011 011 111
+				0x2492, // 1: 010 010 010 010 010
+				0x73E7, // 2: 111 001 111 100 111
+				0x73CF, // 3: 111 001 111 001 111
+				0x5BC9, // 4: 101 101 111 001 001
+				0x7E3F, // 5: 111 110 111 001 111
+				0x7E7F, // 6: 111 110 111 101 111
+				0x7249, // 7: 111 001 001 001 001
+				0x7FFF, // 8: 111 111 111 111 111  (simplified)
+				0x7FCF, // 9: 111 111 111 001 111
+			};
+			if (digit < 0 || digit > 9) digit = 0;
+			uint16 pat = digitPatterns[digit];
+			for (int py = 0; py < 5; py++) {
+				for (int px = 0; px < 3; px++) {
+					int bit = 14 - (py * 3 + px);
+					if (pat & (1 << bit)) {
+						int sx = digitX + c * 5 + px;
+						int sy = digitY + py;
+						if (sx >= 0 && sx < width && sy >= 0 && sy < height)
+							dst[sy * pitch + sx] = textColor;
+					}
+				}
+			}
+		}
+	}
+}
+
 void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
 								int x, int y, const RA1Sprite &spr) {
 	if (!spr.data || spr.width <= 0 || spr.height <= 0)
@@ -258,6 +545,7 @@ void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
 void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 }
 
+// Parse RA1 GAME chunks.
 void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 8)
 		return;
@@ -267,23 +555,67 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 	switch (opcode) {
 	case 0x5E:
-		debug(5, "InsaneRebel1: GAME 0x5E (mode) param=%d", param1);
+		// Mode control
+		_flyControlMode = (int16)param1;
+		debug(5, "RA1 GAME 0x5E: flyControlMode=%d", _flyControlMode);
 		break;
+
 	case 0x5D:
-		debug(5, "InsaneRebel1: GAME 0x5D (link) param=%d", param1);
+		debug(5, "RA1 GAME 0x5D (link) param=%d", param1);
 		break;
+
 	case 0x5F:
-		debug(5, "InsaneRebel1: GAME 0x5F (event) param=%d", param1);
+		debug(5, "RA1 GAME 0x5F (event) param=%d", param1);
 		break;
-	case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B:
+
+	case 0x07:
+		// Per-frame metadata (param1=counter, param2=constant, param3/4=wind?)
+		if (subSize >= 20) {
+			uint32 param2 = b.readUint32BE();
+			uint32 param3 = b.readUint32BE();
+			uint32 param4 = b.readUint32BE();
+			_windParamX = (int16)param3;
+			_windParamY = (int16)param4;
+			debug(7, "RA1 GAME 0x07: idx=%d val=%d wind=(%d,%d)",
+				param1, param2, _windParamX, _windParamY);
+		}
+		break;
+
+	case 0x0D:
+		// Corridor boundaries: per-frame flight corridor
+		// Raw: 0x0D, left, top, right, bottom (all 32-bit BE)
+		if (subSize >= 20) {
+			_corridorLeftX = (int16)param1;
+			_corridorTopY = (int16)b.readUint32BE();
+			_corridorRightX = (int16)b.readUint32BE();
+			_corridorBottomY = (int16)b.readUint32BE();
+			debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d",
+				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+		}
+		break;
+
+	case 0x0E:
+		// Secondary collision zone
+		if (subSize >= 20) {
+			uint32 param2 = b.readUint32BE();
+			uint32 param3 = b.readUint32BE();
+			uint32 param4 = b.readUint32BE();
+			debug(7, "RA1 GAME 0x0E: params=(%d,%d,%d,%d)", param1, param2, param3, param4);
+		}
+		break;
+
+	case 0x08: case 0x09: case 0x0A: case 0x0B:
 	case 0x19: case 0x1A:
-	case 0x0D: case 0x0E:
 		if (subSize >= 20) {
-			b.readUint32BE(); b.readUint32BE(); b.readUint32BE();
+			uint32 param2 = b.readUint32BE();
+			uint32 param3 = b.readUint32BE();
+			uint32 param4 = b.readUint32BE();
+			debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
 		}
 		break;
+
 	default:
-		debug(7, "InsaneRebel1: GAME unknown 0x%02x size=%d", opcode, subSize);
+		debug(7, "RA1 GAME unknown 0x%02x size=%d", opcode, subSize);
 		break;
 	}
 }
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0ea0d7b2266..6d308a66e1d 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -49,8 +49,8 @@ struct RA1SpriteBank {
 };
 
 /**
- * Proof-of-concept for Star Wars: Rebel Assault (RA1).
- * Loads level 1 and renders the player ship sprite.
+ * Star Wars: Rebel Assault (RA1) game logic.
+ * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
  */
 class InsaneRebel1 : public Insane {
 public:
@@ -72,20 +72,59 @@ public:
 private:
 	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
 	void loadLevelSprites(int level);
+	void updateShipPhysics();
 	void renderShip(byte *dst, int pitch, int width, int height);
+	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
 	ScummEngine_v7 *_vm;
 
 	RA1SpriteBank _shipBank;
+	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
 
-	int _shipPosX;
-	int _shipPosY;
-	int _shipDirIndex;
-
+	// RA1 screen dimensions (384x242)
 	int _screenWidth;
 	int _screenHeight;
+
+	// Ship game-coordinate position (adapted from RA2's [20,404]x[20,240])
+	// RA1 coordinate space: [18,366]x[18,224], center=(192,121)
+	int16 _shipPosX;
+	int16 _shipPosY;
+
+	// Direction sprite index (5x7 grid = 35 sprites, vDir*7 + hDir)
+	int16 _shipDirIndex;
+
+	// Corridor boundaries (set by GAME opcode 0x07)
+	int16 _corridorLeftX;
+	int16 _corridorTopY;
+	int16 _corridorRightX;
+	int16 _corridorBottomY;
+
+	// Physics state (velocity-based movement from RA2 Handler 7)
+	int16 _smoothedVelocity;         // Averaged horizontal velocity
+	int16 _verticalInput;            // Stored vertical input component
+	int16 _velocityHistory[25];      // Horizontal velocity ring buffer
+	int16 _windHistoryX[15];         // Wind X history buffer
+	int16 _windHistoryY[15];         // Wind Y history buffer
+	int16 _windParamX;               // Wind X (from GAME opcode 0x07 sub-opcode 0)
+	int16 _windParamY;               // Wind Y (from GAME opcode 0x07 sub-opcode 0)
+
+	// Perspective view offsets
+	int16 _perspectiveX;
+	int16 _perspectiveY;
+	int16 _viewShift;                // Clamped smoothed velocity for view transform
+
+	// Control mode (from GAME opcode 0x5E)
+	int16 _flyControlMode;
+
+	// Hit cooldown timer
+	int16 _hitCooldown;
+
+	// HUD state
+	int16 _playerDamage;    // 0-255, higher = more damage
+	int _score;
+	int _pilots;            // Lives remaining
 };
 
 } // End of namespace Scumm


Commit: 3ac282b4a831a6ac8528e27ae180f9997de561a4
    https://github.com/scummvm/scummvm/commit/3ac282b4a831a6ac8528e27ae180f9997de561a4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:23+02:00

Commit Message:
SCUMM: RA1: Add mouse handling

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/scumm_v7.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index fdd20e2c2fe..1db60024dc5 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -22,6 +22,8 @@
 #include "common/system.h"
 #include "common/events.h"
 #include "common/endian.h"
+#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/scumm.h"
 #include "scumm/smush/smush_player.h"
@@ -248,18 +250,17 @@ void InsaneRebel1::updateShipPhysics() {
 		_hitCooldown--;
 
 	// --- Step 1: Mouse input as offset from screen center ---
-	// RA2 uses _vm->_mouse (0-319, 0-199), center (160, 100)
-	// RA1 uses event manager mouse pos, center (192, 121)
-	Common::Point mousePos = g_system->getEventManager()->getMousePos();
-	int16 inputX = (int16)(mousePos.x - kCenterX);
-	int16 inputY = (int16)(mousePos.y - kCenterY);
-
-	// Clamp to ±kCenterX horizontal, ±127 vertical
-	inputX = CLIP<int16>(inputX, -kCenterX, kCenterX);
+	// Use _vm->_mouse (0-319, 0-199 virtual screen coords), same as RA2.
+	// Center = (160, 100) in virtual screen space.
+	int16 inputX = (int16)(_vm->_mouse.x - 160);
+	int16 inputY = (int16)(_vm->_mouse.y - 100);
+
+	// Clamp: [-160, 160] horizontal, [-127, 127] vertical (same as RA2)
+	inputX = CLIP<int16>(inputX, -160, 160);
 	inputY = CLIP<int16>(inputY, -127, 127);
 
-	// --- Step 2: Scale to [-127, 127] ---
-	int16 scaledInputX = (int16)((inputX * 127) / kCenterX);
+	// --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
+	int16 scaledInputX = (int16)((inputX * 127) / 160);
 	int16 scaledInputY = inputY;
 
 	// --- Step 3: Velocity history + smoothed average ---
@@ -628,7 +629,15 @@ void InsaneRebel1::playLevel(int level) {
 
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
+
+	// Center mouse, hide cursor, and lock mouse to window (like RA2 flight)
+	smush_warpMouse(160, 100, -1);
+	CursorMan.showMouse(false);
+	g_system->lockMouse(true);
+
 	splayer->play(filename.c_str(), 12);
+
+	g_system->lockMouse(false);
 }
 
 } // End of namespace Scumm
diff --git a/engines/scumm/scumm_v7.h b/engines/scumm/scumm_v7.h
index 4c76060c16b..5e96c695c8c 100644
--- a/engines/scumm/scumm_v7.h
+++ b/engines/scumm/scumm_v7.h
@@ -38,6 +38,7 @@ class TextRenderer_v7;
 class ScummEngine_v7 : public ScummEngine_v6 {
 	friend class SmushPlayer;
 	friend class Insane;
+	friend class InsaneRebel1;
 	friend class InsaneRebel2;
 public:
 	ScummEngine_v7(OSystem *syst, const DetectorResult &dr);


Commit: 73fbf2b8d5291230cfb499ae4e9d64b8d920ab98
    https://github.com/scummvm/scummvm/commit/73fbf2b8d5291230cfb499ae4e9d64b8d920ab98
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:23+02:00

Commit Message:
SCUMM: RA1: Add sound support

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1db60024dc5..1292a858ebe 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -22,6 +22,9 @@
 #include "common/system.h"
 #include "common/events.h"
 #include "common/endian.h"
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
 #include "graphics/cursorman.h"
 #include "graphics/wincursor.h"
 #include "scumm/scumm_v7.h"
@@ -105,6 +108,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_score = 0;
 	_pilots = 3;
 
+	// Audio
+	initAudio(11025);
+
 	// Null out Insane base class pointers that the default constructor doesn't initialize
 	_smush_roadrashRip = nullptr;
 	_smush_roadrsh2Rip = nullptr;
@@ -122,6 +128,179 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 }
 
 InsaneRebel1::~InsaneRebel1() {
+	terminateAudio();
+}
+
+// ---------------------------------------------------------------------------
+// Audio
+// ---------------------------------------------------------------------------
+
+void InsaneRebel1::initAudio(int sampleRate) {
+	_audioSampleRate = sampleRate;
+	for (int i = 0; i < kMaxAudioTracks; i++) {
+		_audioStreams[i] = nullptr;
+		_audioTrackActive[i] = false;
+	}
+}
+
+void InsaneRebel1::terminateAudio() {
+	for (int i = 0; i < kMaxAudioTracks; i++) {
+		if (_audioTrackActive[i]) {
+			_vm->_mixer->stopHandle(_audioHandles[i]);
+			_audioTrackActive[i] = false;
+		}
+		if (_audioStreams[i]) {
+			_audioStreams[i]->finish();
+			_audioStreams[i] = nullptr;
+		}
+	}
+}
+
+void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+	if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
+		return;
+
+	if (!_audioStreams[trackIdx]) {
+		debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+		_audioTrackActive[trackIdx] = true;
+		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+								DisposeAfterUse::NO);
+	}
+
+	byte *audioCopy = (byte *)malloc(size);
+	if (!audioCopy)
+		return;
+	memcpy(audioCopy, data, size);
+
+	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+	int scaledPan = (pan * 127) / 128;
+	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel1::processAudioFrame(int16 feedSize) {
+	if (!_player)
+		return;
+
+	SmushPlayer *sp = _player;
+
+	if (sp->_smushTracksNeedInit) {
+		sp->_smushTracksNeedInit = false;
+		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+			sp->_smushDispatch[i].fadeRemaining = 0;
+			sp->_smushDispatch[i].fadeVolume = 0;
+			sp->_smushDispatch[i].fadeSampleRate = 0;
+			sp->_smushDispatch[i].elapsedAudio = 0;
+			sp->_smushDispatch[i].audioLength = 0;
+		}
+	}
+
+	for (int i = 0; i < sp->_smushNumTracks; i++) {
+		SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
+		SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
+
+		if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
+			continue;
+
+		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
+
+		if (!isPlayableTrack)
+			continue;
+
+		int baseVolume;
+		switch (track.flags & TRK_TYPE_MASK) {
+		case IS_SFX:
+			baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
+			break;
+		case IS_BKG_MUSIC:
+			baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
+			break;
+		case IS_SPEECH:
+			baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
+			break;
+		default:
+			baseVolume = track.volume;
+			break;
+		}
+		int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
+
+		// Handle FADING -> PLAYING transition
+		if (track.state == TRK_STATE_FADING) {
+			dispatch.headerPtr = track.dataBuf;
+			dispatch.dataBuf = track.subChunkPtr;
+			dispatch.dataSize = track.dataSize;
+			dispatch.currentOffset = 0;
+			dispatch.audioLength = 0;
+			track.state = TRK_STATE_PLAYING;
+		}
+
+		if (track.state != TRK_STATE_INACTIVE) {
+			int32 tmpFeedSize = feedSize;
+
+			while (tmpFeedSize > 0) {
+				int32 mixInFrameCount = dispatch.currentOffset;
+
+				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+					if (dispatch.audioRemaining < 0)
+						dispatch.audioRemaining = 0;
+
+					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+					if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
+						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
+						if (mixInFrameCount > maxFrames)
+							mixInFrameCount = maxFrames;
+					}
+
+					if (offset + mixInFrameCount > dispatch.dataSize)
+						mixInFrameCount = dispatch.dataSize - offset;
+
+					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+						if (mixInFrameCount <= 0) {
+							track.state = TRK_STATE_ENDING;
+							break;
+						}
+					}
+
+					if (mixInFrameCount > 0) {
+						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
+							break;
+
+						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+						dispatch.currentOffset -= mixInFrameCount;
+						dispatch.audioRemaining += mixInFrameCount;
+
+						if (dispatch.sampleRate > 0) {
+							int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
+							tmpFeedSize -= consumedFeed;
+						} else {
+							tmpFeedSize -= mixInFrameCount;
+						}
+					}
+				}
+
+				if (dispatch.currentOffset <= 0) {
+					if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
+						break;
+					if (dispatch.currentOffset <= 0)
+						break;
+				} else if (tmpFeedSize <= 0) {
+					break;
+				}
+			}
+		}
+
+		track.audioRemaining = dispatch.audioRemaining;
+		dispatch.state = track.state;
+	}
 }
 
 // Load an RA1 NUT sprite file (ANIM v1).
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 6d308a66e1d..0a475abd7ba 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -22,6 +22,8 @@
 #if !defined(SCUMM_INSANE_REBEL1_H) && defined(ENABLE_SCUMM_7_8)
 #define SCUMM_INSANE_REBEL1_H
 
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
 #include "scumm/insane/insane.h"
 
 namespace Scumm {
@@ -78,6 +80,14 @@ private:
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
+	// Audio
+	void initAudio(int sampleRate);
+	void terminateAudio();
+	void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
+public:
+	void processAudioFrame(int16 feedSize);
+private:
+
 	ScummEngine_v7 *_vm;
 
 	RA1SpriteBank _shipBank;
@@ -125,6 +135,13 @@ private:
 	int16 _playerDamage;    // 0-255, higher = more damage
 	int _score;
 	int _pilots;            // Lives remaining
+
+	// Audio state (same structure as RA2)
+	static const int kMaxAudioTracks = 4;
+	Audio::QueuingAudioStream *_audioStreams[kMaxAudioTracks];
+	Audio::SoundHandle _audioHandles[kMaxAudioTracks];
+	bool _audioTrackActive[kMaxAudioTracks];
+	int _audioSampleRate;
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 4a093e53b64..782c2ffd926 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1486,6 +1486,9 @@ void SmushPlayer::parseNextFrame() {
 
 	if (!_imuseDigital && isRA2())
 		ra2ParseNextFrame();
+
+	if (!_imuseDigital && isRA1())
+		processDispatches(_smushAudioSampleRate / 15);
 }
 
 void SmushPlayer::setPalette(const byte *palette) {
@@ -2061,6 +2064,9 @@ void SmushPlayer::processDispatches(int16 feedSize) {
 		if (isRA2() && _insane) {
 			InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
 			rebel2->processAudioFrame(feedSize);
+		} else if (isRA1() && _insane) {
+			InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+			rebel1->processAudioFrame(feedSize);
 		}
 		return;
 	}


Commit: 4ed9384177f0f9d8b1a961605c434cc29e5135fa
    https://github.com/scummvm/scummvm/commit/4ed9384177f0f9d8b1a961605c434cc29e5135fa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:23+02:00

Commit Message:
SCUMM: RA1: Improve controls

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1292a858ebe..d00b6f25bde 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -92,10 +92,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_smoothedVelocity = 0;
 	_verticalInput = 0;
 	memset(_velocityHistory, 0, sizeof(_velocityHistory));
-	memset(_windHistoryX, 0, sizeof(_windHistoryX));
-	memset(_windHistoryY, 0, sizeof(_windHistoryY));
-	_windParamX = 0;
-	_windParamY = 0;
+	_driftParam = 0;
+	_driftAccum = 0;
 
 	_perspectiveX = 0;
 	_perspectiveY = 0;
@@ -438,6 +436,11 @@ void InsaneRebel1::updateShipPhysics() {
 	inputX = CLIP<int16>(inputX, -160, 160);
 	inputY = CLIP<int16>(inputY, -127, 127);
 
+	// Dead zone: ignore small offsets from center to prevent drift
+	const int16 kDeadZone = 8;
+	if (ABS(inputX) < kDeadZone) inputX = 0;
+	if (ABS(inputY) < kDeadZone) inputY = 0;
+
 	// --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
 	int16 scaledInputX = (int16)((inputX * 127) / 160);
 	int16 scaledInputY = inputY;
@@ -453,20 +456,7 @@ void InsaneRebel1::updateShipPhysics() {
 		velSum += _velocityHistory[i];
 	_smoothedVelocity = (int16)(velSum / smoothWindow);
 
-	// --- Step 4: Wind history ---
-	for (int i = 14; i > 0; i--)
-		_windHistoryX[i] = _windHistoryX[i - 1];
-	_windHistoryX[0] = _windParamX;
-
-	for (int i = 14; i > 0; i--)
-		_windHistoryY[i] = _windHistoryY[i - 1];
-	_windHistoryY[0] = _windParamY;
-
-	// Wind effect (multiplier = 0 for now, no level data)
-	int16 windEffectX = 0;
-	int16 windEffectY = 0;
-
-	// --- Step 5: Position delta ---
+	// --- Step 4: Position delta ---
 	const int16 levelSpeed = 32;
 	const int16 levelYSpeed = 48;
 	int16 absSmoothVel = ABS(_smoothedVelocity);
@@ -475,29 +465,41 @@ void InsaneRebel1::updateShipPhysics() {
 	if (_flyControlMode == 1) {
 		// Mode 1: Full cross-axis coupling
 		if (scaledInputX < 1)
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY) >> 9);
 		else
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY) >> 9);
 	} else {
 		// Mode 0/2/3: Reduced cross-axis coupling
 		if (scaledInputX < 1)
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2)) >> 9);
 		else
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2)) >> 9);
 	}
 
+	// Original asm drift pipeline (0x1e3fc-0x1e5cb):
+	//   xDelta = (random(200) - 100 - driftTuning * _driftParam) / 2
+	//   accumX += xDelta  (32-bit accumulator)
+	//   shipDeltaX = accumX >> 8  (divide by 256 for pixel position)
+	// So drift contributes ~±0.2 px/frame. We approximate by accumulating and shifting.
+	const int16 kDriftTuning = 3; // TODO: load from per-difficulty/level tuning table
+	int16 driftBias = (int16)(_vm->_rnd.getRandomNumber(199) - 100 - kDriftTuning * _driftParam);
+	_driftAccum += driftBias >> 1;
+	_driftAccum = CLIP<int32>(_driftAccum, -0x8200, 0x8200);
+	positionDeltaX += (int16)(_driftAccum >> 8);
+
 	// Clamp X delta to ±12 per frame
 	positionDeltaX = CLIP<int16>(positionDeltaX, -12, 12);
 	_shipPosX += positionDeltaX;
 
-	// Y delta
+	// Y delta (no drift in original assembly — field4 is unused)
+	int16 positionDeltaY;
 	if (_flyControlMode == 1) {
-		int yDelta = (levelYSpeed * scaledInputY - (windEffectY >> 1)) >> 10;
-		yDelta = CLIP(yDelta, -12, 12);
-		_shipPosY -= (int16)yDelta;
+		positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
+		positionDeltaY = CLIP<int16>(positionDeltaY, -12, 12);
 	} else {
-		_shipPosY -= (int16)((levelYSpeed * scaledInputY) >> 10);
+		positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
 	}
+	_shipPosY -= positionDeltaY;
 
 	_verticalInput = scaledInputY;
 
@@ -749,15 +751,14 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		break;
 
 	case 0x07:
-		// Per-frame metadata (param1=counter, param2=constant, param3/4=wind?)
+		// Per-frame corridor data: f1=frame index, f2=constant(788), f3=drift bias, f4=unused
+		// Original asm: drift bias * tuning "drift" param, combined with random turbulence
+		// f4 is never referenced in the original handler function
 		if (subSize >= 20) {
-			uint32 param2 = b.readUint32BE();
-			uint32 param3 = b.readUint32BE();
-			uint32 param4 = b.readUint32BE();
-			_windParamX = (int16)param3;
-			_windParamY = (int16)param4;
-			debug(7, "RA1 GAME 0x07: idx=%d val=%d wind=(%d,%d)",
-				param1, param2, _windParamX, _windParamY);
+			b.readUint32BE(); // f2 (constant 788, unused in physics)
+			_driftParam = (int16)(int32)b.readUint32BE();
+			b.readUint32BE(); // f4 (unused in original assembly)
+			debug(7, "RA1 GAME 0x07: frame=%d driftParam=%d", param1, _driftParam);
 		}
 		break;
 
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0a475abd7ba..2bb88e26688 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -115,10 +115,12 @@ private:
 	int16 _smoothedVelocity;         // Averaged horizontal velocity
 	int16 _verticalInput;            // Stored vertical input component
 	int16 _velocityHistory[25];      // Horizontal velocity ring buffer
-	int16 _windHistoryX[15];         // Wind X history buffer
-	int16 _windHistoryY[15];         // Wind Y history buffer
-	int16 _windParamX;               // Wind X (from GAME opcode 0x07 sub-opcode 0)
-	int16 _windParamY;               // Wind Y (from GAME opcode 0x07 sub-opcode 0)
+
+	// Per-frame drift bias from GAME 0x07 field3 (multiplied by tuning "drift" param)
+	// Original pipeline: xDelta added to 32-bit accumulator, position = accum >> 8
+	// field4 is unused in the original assembly
+	int16 _driftParam;
+	int32 _driftAccum;           // 32-bit drift accumulator (position = accum >> 8)
 
 	// Perspective view offsets
 	int16 _perspectiveX;


Commit: 4c5eda6d074bfd6833531d06e9e0e5cf2ee3c959
    https://github.com/scummvm/scummvm/commit/4c5eda6d074bfd6833531d06e9e0e5cf2ee3c959
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:24+02:00

Commit Message:
SCUMM: RA1: Add basic damage handling

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d00b6f25bde..b9f43dabfa1 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -100,11 +100,15 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_viewShift = 0;
 
 	_flyControlMode = 0;
-	_hitCooldown = 0;
 
-	_playerDamage = 0;
+	_health = kMaxHealth;
+	_lives = 3;
 	_score = 0;
-	_pilots = 3;
+	_damageFlags = 0;
+	_damageCooldown = 0;
+	_deathTimer = 0;
+	_screenFlash = 0;
+	_frameCounter = 0;
 
 	// Audio
 	initAudio(11025);
@@ -422,9 +426,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 // Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
 // Mouse input → velocity history averaging → position delta → corridor collision → perspective.
 void InsaneRebel1::updateShipPhysics() {
-	// Decrement hit cooldown
-	if (_hitCooldown > 0)
-		_hitCooldown--;
+	_frameCounter++;
+
+	// Decrement cooldown
+	if (_damageCooldown > 0)
+		_damageCooldown--;
 
 	// --- Step 1: Mouse input as offset from screen center ---
 	// Use _vm->_mouse (0-319, 0-199 virtual screen coords), same as RA2.
@@ -508,27 +514,33 @@ void InsaneRebel1::updateShipPhysics() {
 	_shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
 
 	// --- Step 7: Corridor collision (modes 0 and 2) ---
+	// From FUN_1C54D: wall contact sets damageFlags bits 0-3 (directional)
+	// and pushes velocity history to bounce away from the wall.
 	if (_flyControlMode == 0 || _flyControlMode == 2) {
 		if (_corridorRightX < _shipPosX) {
 			_shipPosX = _corridorRightX;
-			if (_hitCooldown < 5) {
+			_damageFlags |= 0x02;  // Right wall
+			if (_damageCooldown < 5) {
 				for (int i = 0; i < 25; i++)
 					_velocityHistory[i] = -127;
-				_hitCooldown = 10;
 			}
 		}
 		if (_shipPosX < _corridorLeftX) {
 			_shipPosX = _corridorLeftX;
-			if (_hitCooldown < 5) {
+			_damageFlags |= 0x04;  // Left wall
+			if (_damageCooldown < 5) {
 				for (int i = 0; i < 25; i++)
 					_velocityHistory[i] = 127;
-				_hitCooldown = 10;
 			}
 		}
-		if (_corridorBottomY < _shipPosY)
+		if (_corridorBottomY < _shipPosY) {
 			_shipPosY = _corridorBottomY;
-		if (_shipPosY < _corridorTopY)
+			_damageFlags |= 0x01;  // Bottom wall
+		}
+		if (_shipPosY < _corridorTopY) {
 			_shipPosY = _corridorTopY;
+			_damageFlags |= 0x08;  // Top wall
+		}
 	}
 
 	// --- Step 8: Perspective offsets ---
@@ -571,9 +583,42 @@ void InsaneRebel1::updateShipPhysics() {
 
 	_shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
 
-	debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d corridor=[%d,%d]-[%d,%d]",
+	// --- Step 10: Damage processing (from FUN_1DEB5 decompilation) ---
+	// damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
+	if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
+		_health >= 0 && _deathTimer <= 0) {
+		// Projectile hit (bit 7 = 0x80)
+		if (_damageFlags & 0x80)
+			_health -= kHeavyDamage;
+		// Wall collision (bits 1,2,4 = 0x16)
+		if (_damageFlags & 0x16)
+			_health -= kLightDamage;
+
+		if (_health < 0)
+			_deathTimer = kDeathTimerInit;
+
+		_damageCooldown = kDamageCooldownInit;
+		_screenFlash = 3;
+	}
+
+	// Death animation countdown
+	if (_health < 0 && _deathTimer > 0)
+		_deathTimer--;
+
+	// Health regeneration: +1 every 32 frames (from original asm)
+	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0)
+		_health++;
+
+	// Screen flash decay
+	if (_screenFlash > 0)
+		_screenFlash--;
+
+	// Clear per-frame damage flags
+	_damageFlags = 0;
+
+	debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
 		_shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
-		positionDeltaX, _shipDirIndex,
+		positionDeltaX, _shipDirIndex, _health,
 		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
@@ -628,25 +673,35 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 			hudX, hudY, bar.width, bar.height);
 	}
 
-	// Draw damage bar (green rectangle).
-	// From the screenshot: bar starts around x=56, y=8 within the HUD sprite,
-	// max width ~76 pixels, height ~5 pixels. Green = palette index ~0xA0.
+	// Draw health bar (from FUN_1BBCB decompilation).
+	// Bar starts at x=56, y=8 within HUD, max width ~76px, height ~5px.
+	// Color thresholds: green (>50%), yellow (25-50%), red (<25%).
 	{
 		int barMaxW = 76;
 		int barH = 5;
 		int barX = hudX + 56;
 		int barY = hudY + 8;
-		int fillW = barMaxW - (barMaxW * _playerDamage / 255);
-		if (fillW < 0) fillW = 0;
-		if (fillW > barMaxW) fillW = barMaxW;
+		int damage = kMaxHealth - CLIP<int16>(_health, 0, kMaxHealth);
+		int fillW = barMaxW * damage / kMaxHealth;
+		fillW = CLIP(fillW, 0, barMaxW);
+
+		// Color based on damage level (matching original thresholds from FUN_1BBCB)
+		byte barColor;
+		if (_health > kMaxHealth / 2)
+			barColor = 0xA0;  // Green — low damage
+		else if (_health > kMaxHealth / 4)
+			barColor = 0x2C;  // Yellow — moderate damage
+		else
+			barColor = 0x30;  // Red — critical
 
-		// Find a green palette index — use 0xA0 (common green in RA1 palette)
-		byte greenColor = 0xA0;
+		// Flash effect on damage
+		if (_screenFlash > 0)
+			barColor = 0xFF;  // White flash
 
 		for (int iy = 0; iy < barH && barY + iy < height; iy++) {
 			byte *d = dst + (barY + iy) * pitch + barX;
 			for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
-				d[ix] = greenColor;
+				d[ix] = barColor;
 			}
 		}
 	}
@@ -776,12 +831,18 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		break;
 
 	case 0x0E:
-		// Secondary collision zone
+		// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
 		if (subSize >= 20) {
-			uint32 param2 = b.readUint32BE();
-			uint32 param3 = b.readUint32BE();
-			uint32 param4 = b.readUint32BE();
-			debug(7, "RA1 GAME 0x0E: params=(%d,%d,%d,%d)", param1, param2, param3, param4);
+			int16 zoneLeft = (int16)param1;
+			int16 zoneTop = (int16)b.readUint32BE();
+			int16 zoneRight = (int16)b.readUint32BE();
+			int16 zoneBottom = (int16)b.readUint32BE();
+			if (_shipPosX >= zoneLeft && _shipPosX <= zoneRight &&
+				_shipPosY >= zoneTop && _shipPosY <= zoneBottom) {
+				_damageFlags |= 0x10;
+			}
+			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
+				zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
 		}
 		break;
 
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 2bb88e26688..745ee84c906 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -130,13 +130,23 @@ private:
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
 
-	// Hit cooldown timer
-	int16 _hitCooldown;
-
-	// HUD state
-	int16 _playerDamage;    // 0-255, higher = more damage
-	int _score;
-	int _pilots;            // Lives remaining
+	// Damage system (from Ghidra decompilation of FUN_1DEB5)
+	int16 _health;               // 0x7560: current health (init=98, negative=dead, max=98)
+	int16 _lives;                // 0x7562: remaining extra lives
+	int _score;                  // 0x7564: current score
+	byte _damageFlags;           // 0x74D4: per-frame collision bitmask (cleared each frame)
+	int16 _damageCooldown;       // 0x74D8: invulnerability timer (10 frames after hit)
+	int16 _deathTimer;           // 0x756A: death animation countdown (30 on death)
+	int16 _screenFlash;          // 0x7736: screen flash timer on hit
+	uint32 _frameCounter;        // 0x7740: global frame counter
+
+	static const int16 kMaxHealth = 98;
+	static const int16 kDeathTimerInit = 30;
+	static const int16 kDamageCooldownInit = 10;
+
+	// Tuning damage values (TODO: load from data section per difficulty/level)
+	static const int16 kLightDamage = 5;   // "wham" — wall/zone collision
+	static const int16 kHeavyDamage = 15;  // "shot" — projectile hit
 
 	// Audio state (same structure as RA2)
 	static const int kMaxAudioTracks = 4;


Commit: c8264eba60d169a2c99f0b5eb9271c6d4a21d5dc
    https://github.com/scummvm/scummvm/commit/c8264eba60d169a2c99f0b5eb9271c6d4a21d5dc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:24+02:00

Commit Message:
SCUMM: RA1: Play additional videos

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/nut_renderer.cpp
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index b9f43dabfa1..8c2c3268879 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -109,6 +109,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_deathTimer = 0;
 	_screenFlash = 0;
 	_frameCounter = 0;
+	_interactiveVideoActive = false;
 
 	// Audio
 	initAudio(11025);
@@ -127,12 +128,27 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_smush_bencutNut = nullptr;
 	_smush_iconsNut = nullptr;
 	_smush_icons2Nut = nullptr;
+
+	_vm->_system->getEventManager()->getEventDispatcher()->registerObserver(this, 1, false);
 }
 
 InsaneRebel1::~InsaneRebel1() {
+	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
 	terminateAudio();
 }
 
+bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+		if (_player) {
+			debug("Rebel1: ESC pressed - skipping video");
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		}
+	}
+
+	return false;
+}
+
 // ---------------------------------------------------------------------------
 // Audio
 // ---------------------------------------------------------------------------
@@ -409,7 +425,7 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 	int32 setupsan13, int32 curFrame, int32 maxFrame) {
 
-	if (_shipBank.numSprites == 0 || !renderBitmap)
+	if (!_interactiveVideoActive || _shipBank.numSprites == 0 || !renderBitmap)
 		return;
 
 	int width = _player->_width;
@@ -862,21 +878,243 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	}
 }
 
-void InsaneRebel1::playLevel(int level) {
-	loadLevelSprites(level);
+// ---------------------------------------------------------------------------
+// Game flow (matching original at 0x15597)
+// ---------------------------------------------------------------------------
+
+// Play a passive cinematic (no game callback, skippable).
+// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
+void InsaneRebel1::playCinematic(const char *filename) {
+	debug(1, "InsaneRebel1::playCinematic('%s')", filename);
+	SmushPlayer *splayer = _vm->_splayer;
+	_player = splayer;
+	_interactiveVideoActive = false;
+	_vm->_smushVideoShouldFinish = false;
+	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve
+	splayer->play(filename, 12);
+}
+
+void InsaneRebel1::clearVideoBuffer() {
+	if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
+		return;
+
+	const int pixelCount = _vm->_screenWidth * _vm->_screenHeight;
+	byte *clearBuffer = (byte *)calloc(pixelCount, 1);
+	if (!clearBuffer)
+		return;
+
+	if (_vm->_macScreen) {
+		_vm->mac_drawBufferToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	} else {
+		_vm->_system->copyRectToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	}
+	_vm->_system->updateScreen();
+
+	free(clearBuffer);
+}
+
+// Intro sequence (0x155ef-0x158f8):
+//   1. O1LOGO.ANM — LucasArts logo
+//   2. O1OPEN.ANM — Star Wars opening crawl
+void InsaneRebel1::playIntroSequence() {
+	debug(1, "InsaneRebel1: Playing intro sequence");
+
+	// LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
+	playCinematic("OPEN/O1LOGO.ANM");
+	if (_vm->shouldQuit())
+		return;
+	clearVideoBuffer();
+
+	// Star Wars opening crawl (original: PUSH 0x5800, CALL FUN_1BA32)
+	playCinematic("OPEN/O1OPEN.ANM");
+}
+
+// Main menu on O1OPTION.ANM background (0x15968).
+// Original renders text overlay with 5 menu items via FUN_21F7A.
+// For now, we play the menu video as a passive cinematic (non-interactive)
+// and return "Start New Game" immediately.
+// TODO: Implement interactive menu with keyboard/mouse selection.
+int InsaneRebel1::runMainMenu() {
+	debug(1, "InsaneRebel1: Main menu");
+
+	// Play menu background video
+	playCinematic("OPEN/O1OPTION.ANM");
+
+	if (_vm->shouldQuit())
+		return 5;  // Exit
+
+	// TODO: Render menu items overlay:
+	//   "MAIN MENU"         (x=160, y=30)
+	//   "START NEW GAME"    (x=160, y=60)
+	//   "GAME OPTIONS"      (x=160, y=75)
+	//   "ENTER PASSCODE"    (x=160, y=90)
+	//   "CONTINUE DEMO"     (x=160, y=105)
+	//   "EXIT TO DOS"       (x=160, y=120)
+	// For now, auto-select "Start New Game"
+	return 1;
+}
+
+// Level 1 flow (0x16100-0x16737):
+//   1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
+//   2. L1HANGAR.ANM — Hangar departure cutscene
+//   3. "Chapter 1" text
+//   4. L1CU1.ANM — Pre-flight cutscene
+//   5. L1PLAY1L.ANM — Stage 1 gameplay (left path) — INTERACTIVE
+//   6. L1PLAY1R.ANM — Stage 1 gameplay (right path) — INTERACTIVE
+//   7. L1CU2.ANM — Mid-level cutscene
+//   8. L1PLAY2.ANM — Stage 2 turret — INTERACTIVE
+//   9. L1END.ANM — Level end cutscene
+//   Death: L1CRASHA/B.ANM → L1DEATH.ANM → L1RETRY.ANM → retry from L1NEW
+bool InsaneRebel1::runLevel1() {
+	debug(1, "InsaneRebel1: Running level 1");
+
+	// Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
+	loadLevelSprites(1);
+
+	// L1HANGAR.ANM — Hangar departure intro (original: 0x5918, flags 0x0420)
+	playCinematic("LVL1/L1HANGAR.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	// L1CU1.ANM — Pre-flight cutscene (original: 0x5944, flags 0x0400)
+	playCinematic("LVL1/L1CU1.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	// Retry loop
+	while (!_vm->shouldQuit()) {
+		// Reset health for this attempt
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+
+		// L1PLAY1L.ANM — Stage 1 gameplay (left path, original: 0x5953)
+		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			// Survived stage 1 — continue to right path
+			// L1PLAY1R.ANM (original: 0x5965, flags with seekframe=0x187)
+			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			if (_health >= 0) {
+				// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
+				playCinematic("LVL1/L1CU2.ANM");
+				if (_vm->shouldQuit())
+					return false;
+
+				// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
+				playInteractiveVideo("LVL1/L1PLAY2.ANM");
+				if (_vm->shouldQuit())
+					return false;
+
+				// L1END.ANM — Level complete! (original: 0x59a3)
+				playCinematic("LVL1/L1END.ANM");
+				return true;
+			}
+		}
+
+		// Death sequence (original: 0x165e8-0x16737)
+		// Random crash variant A or B
+		if (_vm->_rnd.getRandomNumber(1) == 0)
+			playCinematic("LVL1/L1CRASHA.ANM");
+		else
+			playCinematic("LVL1/L1CRASHB.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		// L1DEATH.ANM (original: 0x5a4b)
+		playCinematic("LVL1/L1DEATH.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		_lives--;
+		if (_lives <= 0) {
+			// Game over — no more retries
+			debug(1, "InsaneRebel1: Game over (no lives left)");
+			return false;
+		}
+
+		// L1RETRY.ANM — Retry prompt (original: 0x5a5c)
+		// After retry, original jumps back to L1NEW→L1PLAY1L (0x16214→0x16680)
+		playCinematic("LVL1/L1RETRY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		// L1NEW.ANM — Briefing before retry (original: 0x5a3c at retry path 0x16680)
+		playCinematic("LVL1/L1NEW.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		// Loop back to gameplay
+	}
+
+	return false;
+}
+
+// Main game entry point — called from ScummEngine::go().
+// Matches original flow at 0x15597: intro → menu → level.
+void InsaneRebel1::runGame() {
+	// Play intro sequence (logo + opening)
+	playIntroSequence();
+	if (_vm->shouldQuit())
+		return;
+
+	// Main menu → gameplay loop
+	while (!_vm->shouldQuit()) {
+		int menuResult = runMainMenu();
+		if (_vm->shouldQuit())
+			return;
+
+		switch (menuResult) {
+		case 1: {
+			// Start New Game — play L1NEW briefing then level 1
+			playCinematic("LVL1/L1NEW.ANM");
+			if (_vm->shouldQuit())
+				return;
+
+			bool completed = runLevel1();
+			if (completed) {
+				debug(1, "InsaneRebel1: Level 1 completed!");
+				// TODO: Continue to level 2
+			}
+			// Return to menu after level ends
+			break;
+		}
+		case 5:
+			// Exit
+			return;
+		default:
+			// Options, Passcode, Demo — not yet implemented, return to menu
+			break;
+		}
+	}
+}
 
-	Common::String filename = Common::String::format("LVL%d/L%dPLAY1L.ANM", level, level);
-	debug(1, "InsaneRebel1::playLevel(%d): playing %s", level, filename.c_str());
+// Play interactive gameplay video (with ship physics + HUD).
+void InsaneRebel1::playInteractiveVideo(const char *filename) {
+	debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
 
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
+	clearBit(0);
+	_interactiveVideoActive = true;
+	_vm->_smushVideoShouldFinish = false;
+	splayer->setCurVideoFlags(0x28);
 
 	// Center mouse, hide cursor, and lock mouse to window (like RA2 flight)
 	smush_warpMouse(160, 100, -1);
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
 
-	splayer->play(filename.c_str(), 12);
+	splayer->play(filename, 12);
+	_interactiveVideoActive = false;
 
 	g_system->lockMouse(false);
 }
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 745ee84c906..2c07cfb9e31 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -24,6 +24,7 @@
 
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
+#include "common/events.h"
 #include "scumm/insane/insane.h"
 
 namespace Scumm {
@@ -54,11 +55,13 @@ struct RA1SpriteBank {
  * Star Wars: Rebel Assault (RA1) game logic.
  * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
  */
-class InsaneRebel1 : public Insane {
+class InsaneRebel1 : public Insane, public Common::EventObserver {
 public:
 	InsaneRebel1(ScummEngine_v7 *scumm);
 	~InsaneRebel1() override;
 
+	bool notifyEvent(const Common::Event &event) override;
+
 	void procPreRendering(byte *renderBitmap) override;
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 		int32 setupsan13, int32 curFrame, int32 maxFrame) override;
@@ -69,9 +72,27 @@ public:
 
 	void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
 
-	void playLevel(int level);
+	// Game flow (matching original at 0x15597)
+	void runGame();
 
 private:
+	// Intro sequence: O1LOGO → O1OPEN (0x155ef-0x158f8)
+	void playIntroSequence();
+	void clearVideoBuffer();
+
+	// Main menu loop on O1OPTION.ANM background (0x15968)
+	// Returns: 1=Start New Game, 2=Game Options, 3=Enter Passcode, 4=Continue Demo, 5=Exit
+	int runMainMenu();
+
+	// Level 1 flow (0x16100): hangar → CU1 → gameplay → CU2 → turret → end
+	// Returns true if level completed, false if player quit
+	bool runLevel1();
+
+	// Play a passive cinematic (no game callback, skippable)
+	void playCinematic(const char *filename);
+
+	// Play interactive gameplay video (with ship physics + HUD)
+	void playInteractiveVideo(const char *filename);
 	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
@@ -154,6 +175,9 @@ private:
 	Audio::SoundHandle _audioHandles[kMaxAudioTracks];
 	bool _audioTrackActive[kMaxAudioTracks];
 	int _audioSampleRate;
+
+	// True only while an interactive gameplay SMUSH is running.
+	bool _interactiveVideoActive;
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 8fd02c39e1d..8b16fbd99c2 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -143,19 +143,38 @@ void NutRenderer::loadFont(const char *filename) {
 	// whole of the undecoded font file.
 
 	_numChars = READ_LE_UINT16(dataSrc + 10);
-	assert(_numChars <= ARRAYSIZE(_chars));
+	if (_numChars > ARRAYSIZE(_chars)) {
+		warning("NutRenderer::loadFont(%s) numChars (%d) exceeds max, clamping", filename, _numChars);
+		_numChars = ARRAYSIZE(_chars);
+	}
 
 	uint32 offset = 0;
 	uint32 decodedLength = 0;
 	int l;
 
 	for (l = 0; l < _numChars; l++) {
-		offset += READ_BE_UINT32(dataSrc + offset + 4) + 16;
+		if (offset + 8 > length) {
+			warning("NutRenderer::loadFont(%s) truncated before char %d (offset %x), clamping", filename, l, offset);
+			break;
+		}
+		uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+		uint64 nextOffset = (uint64)offset + chunkSize + 16 + (chunkSize & 1);
+		if (nextOffset + 18 > length) {
+			warning("NutRenderer::loadFont(%s) font chunk exceeds file at char %d (offset %x), clamping", filename, l, offset);
+			break;
+		}
+		offset = (uint32)nextOffset;
 		int width = READ_LE_UINT16(dataSrc + offset + 14);
 		_fontHeight = READ_LE_UINT16(dataSrc + offset + 16);
 		decodedLength += width * _fontHeight;
 	}
 
+	if (l < _numChars)
+		_numChars = l;
+
+	if (_numChars <= 0 || decodedLength == 0)
+		error("NutRenderer::loadFont(%s) no decodable characters", filename);
+
 	debug(1, "NutRenderer::loadFont('%s') - decodedLength = %d", filename, decodedLength);
 
 	_decodedData = new byte[decodedLength];
@@ -163,14 +182,28 @@ void NutRenderer::loadFont(const char *filename) {
 
 	offset = 0;
 	for (l = 0; l < _numChars; l++) {
-		offset += READ_BE_UINT32(dataSrc + offset + 4) + 8;
+		if (offset + 8 > length) {
+			warning("NutRenderer::loadFont(%s) invalid font chunk header %d (offset %x), stopping decode", filename, l, offset);
+			break;
+		}
+		uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+		uint64 nextOffset = (uint64)offset + chunkSize + 8 + (chunkSize & 1);
+		if (nextOffset + 8 > length) {
+			warning("NutRenderer::loadFont(%s) FRME chunk exceeds file %d (offset %x), stopping decode", filename, l, offset);
+			break;
+		}
+		offset = (uint32)nextOffset;
 		if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','R','M','E')) {
-			error("NutRenderer::loadFont(%s) there is no FRME chunk %d (offset %x)", filename, l, offset);
+			warning("NutRenderer::loadFont(%s) no FRME chunk %d (offset %x), stopping decode", filename, l, offset);
 			break;
 		}
 		offset += 8;
+		if (offset + 22 > length) {
+			warning("NutRenderer::loadFont(%s) FOBJ chunk exceeds file %d (offset %x), stopping decode", filename, l, offset);
+			break;
+		}
 		if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','O','B','J')) {
-			error("NutRenderer::loadFont(%s) there is no FOBJ chunk in FRME chunk %d (offset %x)", filename, l, offset);
+			warning("NutRenderer::loadFont(%s) no FOBJ chunk in FRME chunk %d (offset %x), stopping decode", filename, l, offset);
 			break;
 		}
 		int codec = READ_LE_UINT16(dataSrc + offset + 8);
@@ -258,9 +291,11 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
 	for (l = 0; l < _numChars; l++) {
 		if (offset + 8 > length)
 			break;
-		offset += READ_BE_UINT32(dataSrc + offset + 4) + 16;
-		if (offset + 18 > length)
+		uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+		uint64 nextOffset = (uint64)offset + chunkSize + 16 + (chunkSize & 1);
+		if (nextOffset + 18 > length)
 			break;
+		offset = (uint32)nextOffset;
 		int width = READ_LE_UINT16(dataSrc + offset + 14);
 		_fontHeight = READ_LE_UINT16(dataSrc + offset + 16);
 		decodedLength += width * _fontHeight;
@@ -275,7 +310,11 @@ void NutRenderer::loadFontFromData(const byte *data, int32 dataSize) {
 	for (l = 0; l < _numChars; l++) {
 		if (offset + 8 > length)
 			break;
-		offset += READ_BE_UINT32(dataSrc + offset + 4) + 8;
+		uint32 chunkSize = READ_BE_UINT32(dataSrc + offset + 4);
+		uint64 nextOffset = (uint64)offset + chunkSize + 8 + (chunkSize & 1);
+		if (nextOffset + 8 > length)
+			break;
+		offset = (uint32)nextOffset;
 		if (offset + 8 > length)
 			break;
 		if (READ_BE_UINT32(dataSrc + offset) != MKTAG('F','R','M','E')) {
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 102fb518908..e609f134749 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1809,6 +1809,9 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 
 		_sound = new Sound(this, _mixer, false);
 		_musicEngine = _imuseDigital = nullptr;
+		_res->allocResTypeData(rtBuffer, 0, 10, kDynamicResTypeMode);
+		initScreens(0, 200);
+
 		_insane = new InsaneRebel1(this);
 		_splayer = new SmushPlayer(this, nullptr, _insane);
 
@@ -2710,7 +2713,7 @@ Common::Error ScummEngine::go() {
 	if (_game.id == GID_REBEL1) {
 		ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
 		InsaneRebel1 *rebel = (InsaneRebel1 *)vm7->getInsane();
-		rebel->playLevel(1);
+		rebel->runGame();
 		return Common::kNoError;
 	}
 
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 782c2ffd926..56fd68776a0 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1025,7 +1025,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	int origHeight = height;
 
 	int srcSkipY = 0;
-	if (isRA2()) {
+	if (isRA1() || isRA2()) {
 		ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
 		if (width <= 0 || height <= 0)
 			return;
@@ -1034,7 +1034,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	// For RLE codecs, skip source rows that were clipped from the top.
 	// Each RLE row has a 2-byte size prefix, so we can advance past them.
 	const uint8 *adjustedSrc = src;
-	if (srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+	if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
 		for (int i = 0; i < srcSkipY; i++) {
 			adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
 		}
@@ -1125,8 +1125,8 @@ void SmushPlayer::handleZlibFrameObject(int32 subSize, Common::SeekableReadStrea
 
 	byte *ptr = fobjBuffer;
 	int codec = READ_LE_UINT16(ptr); ptr += 2;
-	int left = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
-	int top = isRA2() ? (int16)READ_LE_UINT16(ptr) : (int)READ_LE_UINT16(ptr); ptr += 2;
+	int left = (int16)READ_LE_UINT16(ptr); ptr += 2;
+	int top = (int16)READ_LE_UINT16(ptr); ptr += 2;
 	int width = READ_LE_UINT16(ptr); ptr += 2;
 	int height = READ_LE_UINT16(ptr); ptr += 2;
 
@@ -1151,8 +1151,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 		ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
 		codec &= 0xFF;                  // byte[0] = actual codec number
 	}
-	int left = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
-	int top = isRA2() ? (int)b.readSint16LE() : (int)b.readUint16LE();
+	int left = (int)b.readSint16LE();
+	int top = (int)b.readSint16LE();
 	int width = b.readUint16LE();
 	int height = b.readUint16LE();
 
@@ -1343,10 +1343,11 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 		}
 
 		if (isRA1()) {
-			// v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6
-			// RA1 resolution is always 384x242, set from first FOBJ
-			_width = 384;
-			_height = 242;
+			// v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6.
+			// Keep dimensions unset here and let FOBJ decoding select buffer and
+			// effective width/height (screen-sized or 384x242 special buffer).
+			_width = 0;
+			_height = 0;
 		} else {
 			_width = READ_LE_UINT16(&headerContent[4]);
 			_height = READ_LE_UINT16(&headerContent[6]);
@@ -1395,6 +1396,8 @@ SmushFont *SmushPlayer::getFont(int font) {
 		}
 	} else if (isRA2()) {
 		return ra2GetFont(font);
+	} else if (isRA1()) {
+		return ra1GetFont(font);
 	} else {
 		int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
 		assert(font >= 0 && font < numFonts);
@@ -1407,6 +1410,43 @@ SmushFont *SmushPlayer::getFont(int font) {
 	return _sf[font];
 }
 
+SmushFont *SmushPlayer::ra1GetFont(int font) {
+	const char *ra1Fonts[] = {
+		"SYS/TALKFONT.NUT",
+		"SYS/TECHFONT.NUT",
+		"SYS/TITLFONT.NUT",
+		"SYS/DISPLAY.NUT"
+	};
+	const char *ra2FallbackFonts[] = {
+		"SYSTM/TALKFONT.NUT",
+		"SYSTM/SMALFONT.NUT",
+		"SYSTM/TITLFONT.NUT",
+		"SYSTM/SMALFONT.NUT"
+	};
+
+	int numFonts = ARRAYSIZE(ra1Fonts);
+	if (font < 0 || font >= numFonts) {
+		debugC(DEBUG_SMUSH, "SmushPlayer::ra1GetFont: unknown font %d, using TALKFONT", font);
+		font = 0;
+	}
+
+	if (_sf[font])
+		return _sf[font];
+
+	const char *fontPath = ra1Fonts[font];
+	ScummFile *testFile = _vm->instantiateScummFile();
+	bool ok = _vm->openFile(*testFile, Common::Path(fontPath));
+	if (ok)
+		testFile->close();
+	delete testFile;
+
+	if (!ok)
+		fontPath = ra2FallbackFonts[font];
+
+	_sf[font] = new SmushFont(_vm, fontPath, true);
+	return _sf[font];
+}
+
 void SmushPlayer::parseNextFrame() {
 
 	if (_seekPos >= 0) {
@@ -1714,13 +1754,36 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 				if (!skipFrame) {
 					// WORKAROUND for bug #2415: "FT DEMO: assertion triggered
 					// when playing movie". Some frames there are 384 x 224
-					int frameWidth = MIN(_width, _vm->_screenWidth);
-					int frameHeight = MIN(_height, _vm->_screenHeight);
+					int frameWidth;
+					int frameHeight;
+					const byte *dst;
+
+					if (isRA1()) {
+						if (_dst == nullptr || _width <= 0 || _height <= 0) {
+							_updateNeeded = false;
+							continue;
+						}
+
+						const int srcX = CLIP(_scrollX, 0, _width - 1);
+						const int srcY = CLIP(_scrollY, 0, _height - 1);
 
-					const byte *dst = _dst + _scrollY * _width + _scrollX;
+						frameWidth = MIN(_width - srcX, _vm->_screenWidth);
+						frameHeight = MIN(_height - srcY, _vm->_screenHeight);
+						if (frameWidth <= 0 || frameHeight <= 0) {
+							_updateNeeded = false;
+							continue;
+						}
+
+						dst = _dst + srcY * _width + srcX;
+					} else {
+						frameWidth = MIN(_width, _vm->_screenWidth);
+						frameHeight = MIN(_height, _vm->_screenHeight);
+						dst = _dst + _scrollY * _width + _scrollX;
+					}
 
 					if (_vm->_macScreen) {
-						_vm->mac_drawBufferToScreen(dst, frameWidth, 0, 0, frameWidth, frameHeight);
+						int srcPitch = isRA1() ? _width : frameWidth;
+						_vm->mac_drawBufferToScreen(dst, srcPitch, 0, 0, frameWidth, frameHeight);
 					} else {
 						_vm->_system->copyRectToScreen(dst, _width, 0, 0, frameWidth, frameHeight);
 					}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 3d43b97fb51..a2454d93546 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -328,6 +328,7 @@ private:
 							 int left, int top, int width, int height);
 	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
 	void ra2ResetDeltaPalette();
+	SmushFont *ra1GetFont(int font);
 	SmushFont *ra2GetFont(int font);
 	void ra2ParseNextFrame();
 	void ra2FixupAnimHeader();
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 0bfe0e2ae8b..176dc179b42 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -548,8 +548,8 @@ void SmushPlayer::clearMaskedRegions() {
 }
 
 void SmushPlayer::setScrollOffset(int x, int y) {
-	_scrollX = x;
-	_scrollY = y;
+	_scrollX = MAX(0, x);
+	_scrollY = MAX(0, y);
 }
 
 } // End of namespace Scumm


Commit: 3073b37e7988883a21223c3493f3058d407af70b
    https://github.com/scummvm/scummvm/commit/3073b37e7988883a21223c3493f3058d407af70b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:24+02:00

Commit Message:
SCUMM: RA1: Improve UI

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8c2c3268879..3d0abd34496 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -29,6 +29,7 @@
 #include "graphics/wincursor.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/scumm.h"
+#include "scumm/nut_renderer.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/insane/insane_rebel1.h"
 
@@ -105,11 +106,36 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_lives = 3;
 	_score = 0;
 	_damageFlags = 0;
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
 	_damageCooldown = 0;
 	_deathTimer = 0;
 	_screenFlash = 0;
 	_frameCounter = 0;
 	_interactiveVideoActive = false;
+	_hudFont = nullptr;
+	const char *hudFontCandidates[] = {
+		"SYS/TECHFONT.NUT",
+		"SYS/TALKFONT.NUT",
+		"SYS/SMALFONT.NUT",
+		"SYSTM/SMALFONT.NUT"
+	};
+	for (uint i = 0; i < ARRAYSIZE(hudFontCandidates); i++) {
+		ScummFile *fontFile = _vm->instantiateScummFile();
+		_vm->openFile(*fontFile, hudFontCandidates[i]);
+		const bool found = fontFile->isOpen();
+		if (found)
+			fontFile->close();
+		delete fontFile;
+
+		if (found) {
+			_hudFont = new NutRenderer(_vm, hudFontCandidates[i]);
+			debug(1, "InsaneRebel1: HUD font loaded from %s", hudFontCandidates[i]);
+			break;
+		}
+	}
+	if (!_hudFont)
+		warning("InsaneRebel1: no HUD font found (TECHFONT/TALKFONT/SMALFONT), HUD numbers disabled");
 
 	// Audio
 	initAudio(11025);
@@ -134,6 +160,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 InsaneRebel1::~InsaneRebel1() {
 	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
+	delete _hudFont;
 	terminateAudio();
 }
 
@@ -458,11 +485,6 @@ void InsaneRebel1::updateShipPhysics() {
 	inputX = CLIP<int16>(inputX, -160, 160);
 	inputY = CLIP<int16>(inputY, -127, 127);
 
-	// Dead zone: ignore small offsets from center to prevent drift
-	const int16 kDeadZone = 8;
-	if (ABS(inputX) < kDeadZone) inputX = 0;
-	if (ABS(inputY) < kDeadZone) inputY = 0;
-
 	// --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
 	int16 scaledInputX = (int16)((inputX * 127) / 160);
 	int16 scaledInputY = inputY;
@@ -599,7 +621,16 @@ void InsaneRebel1::updateShipPhysics() {
 
 	_shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
 
-	// --- Step 10: Damage processing (from FUN_1DEB5 decompilation) ---
+	// --- Step 10: Damage/event bit synthesis + damage processing ---
+	// RA1 FUN_1B297-style latches from GAME opcodes:
+	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
+	//   0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
+	if (_gameLatch5D == 0xFFFF)
+		_damageFlags |= 0x40;
+	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+		_damageFlags |= 0x80;
+
+	// Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
 	// damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
 	if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
 		_health >= 0 && _deathTimer <= 0) {
@@ -617,13 +648,21 @@ void InsaneRebel1::updateShipPhysics() {
 		_screenFlash = 3;
 	}
 
+	// Latches are per-frame event inputs in the original pipeline.
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+
 	// Death animation countdown
 	if (_health < 0 && _deathTimer > 0)
 		_deathTimer--;
 
 	// Health regeneration: +1 every 32 frames (from original asm)
-	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0)
-		_health++;
+	if ((_frameCounter & 0x1F) == 0) {
+		if (_health >= 0 && _health < kMaxHealth)
+			_health++;
+		if (_health >= 0)
+			_score += 1;
+	}
 
 	// Screen flash decay
 	if (_screenFlash > 0)
@@ -689,23 +728,22 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 			hudX, hudY, bar.width, bar.height);
 	}
 
-	// Draw health bar (from FUN_1BBCB decompilation).
-	// Bar starts at x=56, y=8 within HUD, max width ~76px, height ~5px.
-	// Color thresholds: green (>50%), yellow (25-50%), red (<25%).
+	// Draw health bar from FUN_1BBCB behavior.
+	// Original logic uses current health as fill width and computes x as (0x92 - health),
+	// so the bar is right-anchored and shrinks from left to right as damage increases.
 	{
-		int barMaxW = 76;
+		int barMaxW = kMaxHealth;
 		int barH = 5;
-		int barX = hudX + 56;
+		int healthWidth = CLIP<int16>(_health, 0, kMaxHealth);
+		int barX = hudX + (0x92 - healthWidth);
 		int barY = hudY + 8;
-		int damage = kMaxHealth - CLIP<int16>(_health, 0, kMaxHealth);
-		int fillW = barMaxW * damage / kMaxHealth;
-		fillW = CLIP(fillW, 0, barMaxW);
+		int fillW = CLIP(healthWidth, 0, barMaxW);
 
 		// Color based on damage level (matching original thresholds from FUN_1BBCB)
 		byte barColor;
-		if (_health > kMaxHealth / 2)
+		if (_health > kHeavyDamage * 2)
 			barColor = 0xA0;  // Green — low damage
-		else if (_health > kMaxHealth / 4)
+		else if (_health > kLightDamage * 2)
 			barColor = 0x2C;  // Yellow — moderate damage
 		else
 			barColor = 0x30;  // Red — critical
@@ -722,46 +760,50 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		}
 	}
 
-	// Draw score as decimal digits.
-	// From screenshot: score area starts around x=265 within HUD, using palette text color.
-	// For now, just draw simple 4×7 digit bitmaps.
-	{
-		char scoreStr[8];
-		snprintf(scoreStr, sizeof(scoreStr), "%06d", _score);
-		int digitX = hudX + 265;
-		int digitY = hudY + 7;
-		byte textColor = 0xFF;  // White
-
-		for (int c = 0; c < 6 && scoreStr[c]; c++) {
-			int digit = scoreStr[c] - '0';
-			// Simple 3×5 digit rendering
-			static const uint16 digitPatterns[10] = {
-				0x7B6F, // 0: 111 011 011 011 111
-				0x2492, // 1: 010 010 010 010 010
-				0x73E7, // 2: 111 001 111 100 111
-				0x73CF, // 3: 111 001 111 001 111
-				0x5BC9, // 4: 101 101 111 001 001
-				0x7E3F, // 5: 111 110 111 001 111
-				0x7E7F, // 6: 111 110 111 101 111
-				0x7249, // 7: 111 001 001 001 001
-				0x7FFF, // 8: 111 111 111 111 111  (simplified)
-				0x7FCF, // 9: 111 111 111 001 111
-			};
-			if (digit < 0 || digit > 9) digit = 0;
-			uint16 pat = digitPatterns[digit];
-			for (int py = 0; py < 5; py++) {
-				for (int px = 0; px < 3; px++) {
-					int bit = 14 - (py * 3 + px);
-					if (pat & (1 << bit)) {
-						int sx = digitX + c * 5 + px;
-						int sy = digitY + py;
-						if (sx >= 0 && sx < width && sy >= 0 && sy < height)
-							dst[sy * pitch + sx] = textColor;
+	// Draw lives and score using the RA1 small NUT font (RA2-style glyph blit).
+	if (_hudFont && _hudFont->getNumChars() > 0) {
+		auto drawHudString = [&](const char *text, int x, int y) {
+			for (int i = 0; text[i] != '\0'; i++) {
+				byte ch = (byte)text[i];
+				if (ch >= _hudFont->getNumChars()) {
+					x += 4;
+					continue;
+				}
+
+				const byte *glyph = _hudFont->getCharData(ch);
+				int gw = _hudFont->getCharWidth(ch);
+				int gh = _hudFont->getCharHeight(ch);
+				int gx = x + _hudFont->getCharXOffset(ch);
+				int gy = y + _hudFont->getCharYOffset(ch);
+
+				if (glyph && gw > 0 && gh > 0) {
+					for (int py = 0; py < gh; py++) {
+						int sy = gy + py;
+						if (sy < 0 || sy >= height)
+							continue;
+						for (int px = 0; px < gw; px++) {
+							int sx = gx + px;
+							if (sx < 0 || sx >= width)
+								continue;
+							byte pixel = glyph[py * gw + px];
+							if (pixel != 0)
+								dst[sy * pitch + sx] = pixel;
+						}
 					}
 				}
+				x += gw > 0 ? gw : 4;
 			}
-		}
+		};
+
+		char livesStr[8];
+		Common::sprintf_s(livesStr, "%d", MAX<int>(_lives, 0));
+		drawHudString(livesStr, hudX + 180, hudY + 6);
+
+		char scoreStr[16];
+		Common::sprintf_s(scoreStr, "%07d", MAX<int>(_score, 0));
+		drawHudString(scoreStr, hudX + 257, hudY + 4);
 	}
+
 }
 
 void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
@@ -808,17 +850,39 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 	switch (opcode) {
 	case 0x5E:
-		// Mode control
-		_flyControlMode = (int16)param1;
-		debug(5, "RA1 GAME 0x5E: flyControlMode=%d", _flyControlMode);
+		// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
+		// This is not a pure control-mode assignment.
+		_damageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_driftParam = 0;
+		_driftAccum = 0;
+		_smoothedVelocity = 0;
+		_verticalInput = 0;
+		memset(_velocityHistory, 0, sizeof(_velocityHistory));
+
+		// Field1 == 0 corresponds to baseline recenter behavior in the original.
+		if ((int32)param1 == 0) {
+			_shipPosX = kCenterX;
+			_shipPosY = kCenterY;
+		}
+
+		// Keep a conservative default mode after reset.
+		_flyControlMode = 0;
+		debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
 		break;
 
 	case 0x5D:
-		debug(5, "RA1 GAME 0x5D (link) param=%d", param1);
+		_gameLatch5D = (uint16)param1;
+		debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
 		break;
 
 	case 0x5F:
-		debug(5, "RA1 GAME 0x5F (event) param=%d", param1);
+		_gameLatch5F = (uint16)param1;
+		debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
 		break;
 
 	case 0x07:
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 2c07cfb9e31..9b033ed2689 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -31,6 +31,7 @@ namespace Scumm {
 
 class ScummEngine_v7;
 class SmushPlayer;
+class NutRenderer;
 
 // Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
 // Separate from NutRenderer to avoid modifying shared NUT parsing code.
@@ -113,6 +114,7 @@ private:
 
 	RA1SpriteBank _shipBank;
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
+	NutRenderer *_hudFont;        // SMALFONT.NUT (SYS or SYSTM) — numeric HUD text (score/lives)
 
 	// RA1 screen dimensions (384x242)
 	int _screenWidth;
@@ -156,6 +158,8 @@ private:
 	int16 _lives;                // 0x7562: remaining extra lives
 	int _score;                  // 0x7564: current score
 	byte _damageFlags;           // 0x74D4: per-frame collision bitmask (cleared each frame)
+	uint16 _gameLatch5D;         // 0x75D2: GAME 0x5D latch (scene/obstacle/event trigger)
+	uint16 _gameLatch5F;         // 0x75D4: GAME 0x5F latch (probabilistic hit trigger)
 	int16 _damageCooldown;       // 0x74D8: invulnerability timer (10 frames after hit)
 	int16 _deathTimer;           // 0x756A: death animation countdown (30 on death)
 	int16 _screenFlash;          // 0x7736: screen flash timer on hit


Commit: 7fee7db76e4fac47c3b741057fde8d5fd4bb9e1d
    https://github.com/scummvm/scummvm/commit/7fee7db76e4fac47c3b741057fde8d5fd4bb9e1d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:25+02:00

Commit Message:
SCUMM: RA1: Add menu

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 3d0abd34496..2c998ab7f1a 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -29,7 +29,6 @@
 #include "graphics/wincursor.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/scumm.h"
-#include "scumm/nut_renderer.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/insane/insane_rebel1.h"
 
@@ -113,29 +112,16 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenFlash = 0;
 	_frameCounter = 0;
 	_interactiveVideoActive = false;
-	_hudFont = nullptr;
-	const char *hudFontCandidates[] = {
-		"SYS/TECHFONT.NUT",
-		"SYS/TALKFONT.NUT",
-		"SYS/SMALFONT.NUT",
-		"SYSTM/SMALFONT.NUT"
-	};
-	for (uint i = 0; i < ARRAYSIZE(hudFontCandidates); i++) {
-		ScummFile *fontFile = _vm->instantiateScummFile();
-		_vm->openFile(*fontFile, hudFontCandidates[i]);
-		const bool found = fontFile->isOpen();
-		if (found)
-			fontFile->close();
-		delete fontFile;
-
-		if (found) {
-			_hudFont = new NutRenderer(_vm, hudFontCandidates[i]);
-			debug(1, "InsaneRebel1: HUD font loaded from %s", hudFontCandidates[i]);
-			break;
-		}
+	_menuActive = false;
+	_menuConfirmed = false;
+	_menuSelection = 0;
+	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
+		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
+	} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
+		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TECHFONT.NUT (%d chars)", _hudFontBank.numSprites);
+	} else {
+		warning("InsaneRebel1: failed to load RA1 HUD font bank (TECHFONT/TALKFONT)");
 	}
-	if (!_hudFont)
-		warning("InsaneRebel1: no HUD font found (TECHFONT/TALKFONT/SMALFONT), HUD numbers disabled");
 
 	// Audio
 	initAudio(11025);
@@ -160,11 +146,45 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 InsaneRebel1::~InsaneRebel1() {
 	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
-	delete _hudFont;
 	terminateAudio();
 }
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+	if (_menuActive && event.type == Common::EVENT_KEYDOWN) {
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_UP:
+		case Common::KEYCODE_w:
+			_menuSelection = (_menuSelection + 4) % 5;
+			return true;
+		case Common::KEYCODE_DOWN:
+		case Common::KEYCODE_s:
+			_menuSelection = (_menuSelection + 1) % 5;
+			return true;
+		case Common::KEYCODE_RETURN:
+		case Common::KEYCODE_KP_ENTER:
+		case Common::KEYCODE_SPACE:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_1:
+		case Common::KEYCODE_2:
+		case Common::KEYCODE_3:
+		case Common::KEYCODE_4:
+		case Common::KEYCODE_5:
+			_menuSelection = event.kbd.keycode - Common::KEYCODE_1;
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_ESCAPE:
+			_menuSelection = 4;
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			break;
+		}
+	}
+
 	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 		if (_player) {
 			debug("Rebel1: ESC pressed - skipping video");
@@ -380,63 +400,103 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 		return false;
 	}
 
-	bank.numSprites = READ_LE_UINT16(data + 10);
+	const uint16 expectedSprites = READ_LE_UINT16(data + 10);
+	bank.numSprites = expectedSprites;
 	bank.sprites = new RA1Sprite[bank.numSprites];
+	memset(bank.sprites, 0, sizeof(RA1Sprite) * bank.numSprites);
 
-	// Pass 1: Walk chunks with alignment to compute total decoded size.
-	uint32 offset = 0;
+	uint32 *fobjOffsets = (uint32 *)calloc(expectedSprites, sizeof(uint32));
+	if (!fobjOffsets) {
+		free(data);
+		return false;
+	}
+
+	// Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
 	uint32 decodedSize = 0;
-	for (int i = 0; i < bank.numSprites; i++) {
-		// Skip current chunk (AHDR or previous FRME)
-		uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
-		offset += chunkSize + 8;
-		if (chunkSize & 1) offset++;  // Word-align
-
-		// Now at FRME; skip its header to reach FOBJ
-		offset += 8;
-		if (offset + 22 > animSize) break;
-
-		uint16 w = READ_LE_UINT16(data + offset + 14);
-		uint16 h = READ_LE_UINT16(data + offset + 16);
-		decodedSize += w * h;
+	uint16 foundSprites = 0;
+	uint32 chunkOffset = 0;
+	while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
+		uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
+		uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
+		uint32 chunkDataOffset = chunkOffset + 8;
+		uint32 chunkEnd = chunkDataOffset + chunkSize;
+		if (chunkEnd > animSize)
+			break;
+
+		if (chunkTag == MKTAG('F','R','M','E')) {
+			bool foundFobj = false;
+			uint32 subOffset = chunkDataOffset;
+			while (subOffset + 8 <= chunkEnd) {
+				uint32 subTag = READ_BE_UINT32(data + subOffset);
+				uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
+				uint32 subDataOffset = subOffset + 8;
+				uint32 subEnd = subDataOffset + subSize;
+				if (subEnd > chunkEnd)
+					break;
+
+				if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
+					uint16 w = READ_LE_UINT16(data + subOffset + 14);
+					uint16 h = READ_LE_UINT16(data + subOffset + 16);
+					decodedSize += (uint32)w * (uint32)h;
+					fobjOffsets[foundSprites] = subOffset;
+					foundFobj = true;
+					break;
+				}
+
+				subOffset = subEnd;
+				if (subSize & 1)
+					subOffset++;
+			}
+			// Always increment for every FRME to preserve char-to-glyph alignment.
+			// Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
+			foundSprites++;
+		}
+
+		chunkOffset = chunkEnd;
+		if (chunkSize & 1)
+			chunkOffset++;
 	}
 
-	bank.decodedData = (byte *)calloc(decodedSize, 1);
+	bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
+	bank.decodedSize = decodedSize;
 	byte *decPtr = bank.decodedData;
 
-	// Pass 2: Decode sprites.
-	offset = 0;
-	for (int i = 0; i < bank.numSprites; i++) {
-		uint32 chunkSize = READ_BE_UINT32(data + offset + 4);
-		offset += chunkSize + 8;
-		if (chunkSize & 1) offset++;
-
-		offset += 8;  // Skip FRME header → now at FOBJ
-		if (offset + 22 > animSize) break;
+	// Pass 2: Decode collected FOBJ entries.
+	for (uint16 i = 0; i < foundSprites; i++) {
+		uint32 fobjOffset = fobjOffsets[i];
+		if (fobjOffset == 0) {
+			// Empty FRME (no FOBJ) — leave sprite as blank (zeroed by memset).
+			continue;
+		}
 
-		int codec = READ_LE_UINT16(data + offset + 8);
-		bank.sprites[i].xoffs = READ_LE_INT16(data + offset + 10);
-		bank.sprites[i].yoffs = READ_LE_INT16(data + offset + 12);
-		bank.sprites[i].width = READ_LE_UINT16(data + offset + 14);
-		bank.sprites[i].height = READ_LE_UINT16(data + offset + 16);
-		bank.sprites[i].data = decPtr;
+		int codec = READ_LE_UINT16(data + fobjOffset + 8);
+		bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
+		bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
+		bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
+		bank.sprites[i].height = READ_LE_UINT16(data + fobjOffset + 16);
 
 		int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
-		const byte *fobjData = data + offset + 22;
+		const byte *fobjData = data + fobjOffset + 22;
 
 		if (codec == 21) {
+			bank.sprites[i].data = decPtr;
 			decodeBomp(decPtr, fobjData, bank.sprites[i].width,
 					   bank.sprites[i].height, bank.sprites[i].width);
 		} else {
+			bank.sprites[i].width = 0;
+			bank.sprites[i].height = 0;
+			bank.sprites[i].data = nullptr;
 			warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
 		}
 
 		decPtr += pixelCount;
 	}
 
+	free(fobjOffsets);
+
 	free(data);
-	debug(1, "InsaneRebel1::loadRA1Nut('%s'): %d sprites, %d bytes decoded",
-		  filename, bank.numSprites, decodedSize);
+	debug(1, "InsaneRebel1::loadRA1Nut('%s'): expected=%d found=%d decoded=%d bytes",
+		  filename, expectedSprites, foundSprites, decodedSize);
 	return true;
 }
 
@@ -451,6 +511,14 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 
 void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 	int32 setupsan13, int32 curFrame, int32 maxFrame) {
+	if (_menuActive && renderBitmap) {
+		int width = _player ? _player->_width : 0;
+		int height = _player ? _player->_height : 0;
+		if (width == 0) width = _screenWidth;
+		if (height == 0) height = _screenHeight;
+		int pitch = width;
+		renderMainMenuOverlay(renderBitmap, pitch, width, height);
+	}
 
 	if (!_interactiveVideoActive || _shipBank.numSprites == 0 || !renderBitmap)
 		return;
@@ -466,6 +534,105 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	renderHUD(renderBitmap, pitch, width, height);
 }
 
+void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
+	if (!dst || !text || _hudFontBank.numSprites <= 0)
+		return;
+
+	for (int i = 0; text[i] != '\0'; i++) {
+		const byte ch = (byte)text[i];
+
+		if (ch == ' ') {
+			x += 6;
+			continue;
+		}
+
+		// RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
+		if (ch < 0x21) {
+			x += 4;
+			continue;
+		}
+		const int fontIdx = (int)ch - 0x21;
+		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+			x += 4;
+			continue;
+		}
+
+		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+		const int gw = glyph.width;
+		const int gh = glyph.height;
+		const int gx = x + glyph.xoffs;
+		const int gy = y + glyph.yoffs;
+		const uint64 glyphPixels = (uint64)gw * (uint64)gh;
+		if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
+			x += 4;
+			continue;
+		}
+		if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
+			x += 4;
+			continue;
+		}
+		const byte *bankStart = _hudFontBank.decodedData;
+		const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
+		if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
+			x += 4;
+			continue;
+		}
+
+		for (int py = 0; py < gh; py++) {
+			const int sy = gy + py;
+			if (sy < 0 || sy >= height)
+				continue;
+			for (int px = 0; px < gw; px++) {
+				const int sx = gx + px;
+				if (sx < 0 || sx >= width)
+					continue;
+				const byte pixel = glyph.data[py * gw + px];
+				if (pixel != 0)
+					dst[sy * pitch + sx] = pixel;
+			}
+		}
+
+		x += gw > 0 ? gw : 4;
+	}
+}
+
+void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+	static const char *kMenuItems[5] = {
+		"START NEW GAME",
+		"GAME OPTIONS",
+		"ENTER PASSCODE",
+		"CONTINUE DEMO",
+		"EXIT"
+	};
+
+	const int menuX = 92;
+	const int menuY = 60;
+	const int rowH = 16;
+	const int boxW = 190;
+
+	for (int i = 0; i < 5; i++) {
+		const int y = menuY + i * rowH;
+		if (i == _menuSelection) {
+			for (int yy = 0; yy < 12; yy++) {
+				const int sy = y + yy;
+				if (sy < 0 || sy >= height)
+					continue;
+				for (int xx = 0; xx < boxW; xx++) {
+					const int sx = menuX + xx;
+					if (sx < 0 || sx >= width)
+						continue;
+					if (xx < 2 || yy < 2 || xx >= boxW - 2 || yy >= 10)
+						dst[sy * pitch + sx] = 0xFF;
+				}
+			}
+		}
+
+		drawFontBankString(dst, pitch, width, height, menuX + 10, y + 1, kMenuItems[i]);
+	}
+
+	drawFontBankString(dst, pitch, width, height, 118, 36, "MAIN MENU");
+}
+
 // Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
 // Mouse input → velocity history averaging → position delta → corridor collision → perspective.
 void InsaneRebel1::updateShipPhysics() {
@@ -760,48 +927,15 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		}
 	}
 
-	// Draw lives and score using the RA1 small NUT font (RA2-style glyph blit).
-	if (_hudFont && _hudFont->getNumChars() > 0) {
-		auto drawHudString = [&](const char *text, int x, int y) {
-			for (int i = 0; text[i] != '\0'; i++) {
-				byte ch = (byte)text[i];
-				if (ch >= _hudFont->getNumChars()) {
-					x += 4;
-					continue;
-				}
-
-				const byte *glyph = _hudFont->getCharData(ch);
-				int gw = _hudFont->getCharWidth(ch);
-				int gh = _hudFont->getCharHeight(ch);
-				int gx = x + _hudFont->getCharXOffset(ch);
-				int gy = y + _hudFont->getCharYOffset(ch);
-
-				if (glyph && gw > 0 && gh > 0) {
-					for (int py = 0; py < gh; py++) {
-						int sy = gy + py;
-						if (sy < 0 || sy >= height)
-							continue;
-						for (int px = 0; px < gw; px++) {
-							int sx = gx + px;
-							if (sx < 0 || sx >= width)
-								continue;
-							byte pixel = glyph[py * gw + px];
-							if (pixel != 0)
-								dst[sy * pitch + sx] = pixel;
-						}
-					}
-				}
-				x += gw > 0 ? gw : 4;
-			}
-		};
-
+	// Draw lives and score from DISPLAY.NUT glyphs.
+	if (_hudFontBank.numSprites > 0) {
 		char livesStr[8];
 		Common::sprintf_s(livesStr, "%d", MAX<int>(_lives, 0));
-		drawHudString(livesStr, hudX + 180, hudY + 6);
+		drawFontBankString(dst, pitch, width, height, hudX + 180, hudY + 6, livesStr);
 
 		char scoreStr[16];
 		Common::sprintf_s(scoreStr, "%07d", MAX<int>(_score, 0));
-		drawHudString(scoreStr, hudX + 257, hudY + 4);
+		drawFontBankString(dst, pitch, width, height, hudX + 257, hudY + 4, scoreStr);
 	}
 
 }
@@ -1001,21 +1135,21 @@ void InsaneRebel1::playIntroSequence() {
 int InsaneRebel1::runMainMenu() {
 	debug(1, "InsaneRebel1: Main menu");
 
-	// Play menu background video
-	playCinematic("OPEN/O1OPTION.ANM");
+	_menuSelection = 0;
+	while (!_vm->shouldQuit()) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+
+		if (_vm->shouldQuit())
+			return 5;
 
-	if (_vm->shouldQuit())
-		return 5;  // Exit
-
-	// TODO: Render menu items overlay:
-	//   "MAIN MENU"         (x=160, y=30)
-	//   "START NEW GAME"    (x=160, y=60)
-	//   "GAME OPTIONS"      (x=160, y=75)
-	//   "ENTER PASSCODE"    (x=160, y=90)
-	//   "CONTINUE DEMO"     (x=160, y=105)
-	//   "EXIT TO DOS"       (x=160, y=120)
-	// For now, auto-select "Start New Game"
-	return 1;
+		if (_menuConfirmed)
+			return _menuSelection + 1;
+	}
+
+	return 5;
 }
 
 // Level 1 flow (0x16100-0x16737):
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 9b033ed2689..1be3e662bd6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -31,7 +31,7 @@ namespace Scumm {
 
 class ScummEngine_v7;
 class SmushPlayer;
-class NutRenderer;
+class SmushFont;
 
 // Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
 // Separate from NutRenderer to avoid modifying shared NUT parsing code.
@@ -47,8 +47,9 @@ struct RA1SpriteBank {
 	int numSprites;
 	RA1Sprite *sprites;
 	byte *decodedData;  // Single allocation for all decoded pixels
+	uint32 decodedSize;
 
-	RA1SpriteBank() : numSprites(0), sprites(nullptr), decodedData(nullptr) {}
+	RA1SpriteBank() : numSprites(0), sprites(nullptr), decodedData(nullptr), decodedSize(0) {}
 	~RA1SpriteBank() { delete[] sprites; free(decodedData); }
 };
 
@@ -99,6 +100,8 @@ private:
 	void updateShipPhysics();
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
+	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
+	void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
@@ -114,7 +117,8 @@ private:
 
 	RA1SpriteBank _shipBank;
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
-	NutRenderer *_hudFont;        // SMALFONT.NUT (SYS or SYSTM) — numeric HUD text (score/lives)
+	RA1SpriteBank _hudFontBank;   // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
+	SmushFont *_menuFont;         // Use engine text renderer for correct TALKFONT character mapping
 
 	// RA1 screen dimensions (384x242)
 	int _screenWidth;
@@ -182,6 +186,11 @@ private:
 
 	// True only while an interactive gameplay SMUSH is running.
 	bool _interactiveVideoActive;
+
+	// Main menu state (for O1OPTION interactive overlay)
+	bool _menuActive;
+	bool _menuConfirmed;
+	int _menuSelection; // 0..4 maps to return values 1..5
 };
 
 } // End of namespace Scumm


Commit: 50959ca0d3e40493a11769a3f81e26d54c0979ca
    https://github.com/scummvm/scummvm/commit/50959ca0d3e40493a11769a3f81e26d54c0979ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:25+02:00

Commit Message:
SCUMM: RA1: Improve video decoding

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/smush/codec1.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 2c998ab7f1a..068597e8c32 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -115,6 +115,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_menuActive = false;
 	_menuConfirmed = false;
 	_menuSelection = 0;
+	_menuFrameCounter = 0;
 	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
 		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
 	} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
@@ -596,41 +597,97 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
 	}
 }
 
+// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
+// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
+int InsaneRebel1::getFontBankStringWidth(const char *text) {
+	if (!text || _hudFontBank.numSprites <= 0)
+		return 0;
+
+	int w = 0;
+	for (int i = 0; text[i] != '\0'; i++) {
+		const byte ch = (byte)text[i];
+		if (ch == ' ') {
+			w += 6;
+			continue;
+		}
+		if (ch < 0x21) {
+			w += 4;
+			continue;
+		}
+		const int fontIdx = (int)ch - 0x21;
+		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+			w += 4;
+			continue;
+		}
+		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+		w += glyph.width > 0 ? glyph.width : 4;
+	}
+	return w;
+}
+
+// renderMainMenuOverlay -- Draw menu text and selection highlight box.
+// Original menu strings from assault_data_3.bin at 0x5822.
+// Highlight uses RA2-style flashing border box (FUN_004292d0 pattern).
 void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
 	static const char *kMenuItems[5] = {
 		"START NEW GAME",
 		"GAME OPTIONS",
 		"ENTER PASSCODE",
 		"CONTINUE DEMO",
-		"EXIT"
+		"EXIT TO DOS"
 	};
 
-	const int menuX = 92;
+	_menuFrameCounter++;
+
+	// Center title
+	const int titleW = getFontBankStringWidth("MAIN MENU");
+	const int titleX = (width - titleW) / 2;
+	drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
+
+	// Draw menu items centered horizontally
 	const int menuY = 60;
 	const int rowH = 16;
-	const int boxW = 190;
 
 	for (int i = 0; i < 5; i++) {
+		const int textW = getFontBankStringWidth(kMenuItems[i]);
+		const int textX = (width - textW) / 2;
 		const int y = menuY + i * rowH;
+
+		drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
+
+		// Selection highlight box — flashing border (FUN_004292d0 pattern from RA2)
 		if (i == _menuSelection) {
-			for (int yy = 0; yy < 12; yy++) {
-				const int sy = y + yy;
-				if (sy < 0 || sy >= height)
-					continue;
-				for (int xx = 0; xx < boxW; xx++) {
-					const int sx = menuX + xx;
-					if (sx < 0 || sx >= width)
-						continue;
-					if (xx < 2 || yy < 2 || xx >= boxW - 2 || yy >= 10)
-						dst[sy * pitch + sx] = 0xFF;
-				}
+			// Flash between two palette colors every 8 frames
+			byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+
+			int bracketWidth = textW + 12;
+			int bracketHeight = rowH;
+			int leftX = textX - 6;
+			int rightX = leftX + bracketWidth;
+			int topY = y - 1;
+			int bottomY = y + bracketHeight - 2;
+
+			// Clamp
+			if (leftX < 0) leftX = 0;
+			if (rightX >= width) rightX = width - 1;
+			if (topY < 0) topY = 0;
+			if (bottomY >= height) bottomY = height - 1;
+
+			// Draw rectangle border (4 lines)
+			for (int x = leftX; x <= rightX && x < width; x++) {
+				if (topY >= 0 && topY < height)
+					dst[topY * pitch + x] = highlightColor;
+				if (bottomY >= 0 && bottomY < height)
+					dst[bottomY * pitch + x] = highlightColor;
+			}
+			for (int py = topY; py <= bottomY && py < height; py++) {
+				if (leftX >= 0 && leftX < width)
+					dst[py * pitch + leftX] = highlightColor;
+				if (rightX >= 0 && rightX < width)
+					dst[py * pitch + rightX] = highlightColor;
 			}
 		}
-
-		drawFontBankString(dst, pitch, width, height, menuX + 10, y + 1, kMenuItems[i]);
 	}
-
-	drawFontBankString(dst, pitch, width, height, 118, 36, "MAIN MENU");
 }
 
 // Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
@@ -1139,6 +1196,8 @@ int InsaneRebel1::runMainMenu() {
 	while (!_vm->shouldQuit()) {
 		_menuActive = true;
 		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
 		playCinematic("OPEN/O1OPTION.ANM");
 		_menuActive = false;
 
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 1be3e662bd6..16f1b4c14a5 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -102,6 +102,7 @@ private:
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
 	void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+	int getFontBankStringWidth(const char *text);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
@@ -191,6 +192,7 @@ private:
 	bool _menuActive;
 	bool _menuConfirmed;
 	int _menuSelection; // 0..4 maps to return values 1..5
+	int _menuFrameCounter;
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 087e4d53aaf..4ad3dd455db 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,4 +36,47 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
 	} while (--height);
 }
 
+/**
+ * RA1 codec 1: RLE with transparency on pixel 0.
+ * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
+ * allowing the background (restored via FTCH) to show through.
+ * Matches FFmpeg's old_codec1() with opaque=0.
+ */
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch;
+	do {
+		byte *rowDst = dst + left;
+		const byte *lineData = src + 2;
+		int remaining = width;
+
+		while (remaining > 0) {
+			byte code = *lineData++;
+			byte num = (code >> 1) + 1;
+			if (num > remaining)
+				num = remaining;
+			if (code & 1) {
+				// Fill: repeat single byte
+				byte color = *lineData++;
+				if (color != 0) {
+					memset(rowDst, color, num);
+				}
+				// If color == 0: skip (transparent)
+			} else {
+				// Copy: write each byte, skipping 0
+				for (int j = 0; j < num; j++) {
+					byte c = lineData[j];
+					if (c != 0)
+						rowDst[j] = c;
+				}
+				lineData += num;
+			}
+			rowDst += num;
+			remaining -= num;
+		}
+
+		src += READ_LE_UINT16(src) + 2;
+		dst += pitch;
+	} while (--height);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 56fd68776a0..68ca58682a2 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -395,6 +395,9 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
 	if (isRA2()) {
 		debug("SmushPlayer STOR: frame=%d - will store next FOBJ", _frame);
 	}
+	if (isRA1()) {
+		debug("RA1 STOR: frame=%d", _frame);
+	}
 }
 
 void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -406,6 +409,11 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 		return;
 	}
 
+	if (isRA1()) {
+		debug("RA1 FTCH: frame=%d _frameBuffer=%p _dst=%p _width=%d _height=%d",
+			_frame, (void*)_frameBuffer, (void*)_dst, _width, _height);
+	}
+
 	if (_frameBuffer != nullptr) {
 		memcpy(_dst, _frameBuffer, _width * _height);
 	}
@@ -797,6 +805,10 @@ void SmushPlayer::handleDeltaPalette(int32 subSize, Common::SeekableReadStream &
 			_pal[i] = CLIP<int32>(_shiftedDeltaPal[i] >> 7, 0, 255);
 		}
 
+		if (isRA1()) {
+			_pal[0] = _pal[1] = _pal[2] = 0;
+		}
+
 		setDirtyColors(0, 255);
 	} else {
 		for (int j = 0; j < 768; ++j) {
@@ -819,6 +831,11 @@ void SmushPlayer::handleNewPalette(int32 subSize, Common::SeekableReadStream &b)
 		return;
 
 	readPalette(_pal, b);
+
+	if (isRA1()) {
+		_pal[0] = _pal[1] = _pal[2] = 0;
+	}
+
 	setDirtyColors(0, 255);
 }
 
@@ -827,9 +844,91 @@ byte *SmushPlayer::getVideoPalette() {
 }
 
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
 
+/**
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41).
+ *
+ * Each line: [u16 lineSize] then alternating [u16 skip] [u16 copyLen]
+ * followed by (copyLen+1) literal bytes. Destination advances by pitch
+ * per line, source advances by lineSize+2 per line.
+ */
+static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch + left;
+
+	for (int row = 0; row < height; row++) {
+		uint16 lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + lineSize;
+		byte *dstRow = dst;
+		int remaining = width;
+
+		while (remaining > 0 && lineData < lineEnd) {
+			// Read skip distance
+			if (lineData + 2 > lineEnd)
+				break;
+			uint16 skip = READ_LE_UINT16(lineData);
+			lineData += 2;
+			dstRow += skip;
+			remaining -= skip;
+			if (remaining <= 0)
+				break;
+
+			// Read copy count (+1)
+			if (lineData + 2 > lineEnd)
+				break;
+			uint16 copyLen = READ_LE_UINT16(lineData) + 1;
+			lineData += 2;
+
+			int toCopy = MIN<int>(copyLen, remaining);
+			if (lineData + toCopy > lineEnd)
+				toCopy = (int)(lineEnd - lineData);
+			if (toCopy > 0) {
+				memcpy(dstRow, lineData, toCopy);
+				lineData += toCopy;
+				dstRow += toCopy;
+				remaining -= toCopy;
+			}
+			// If copyLen was clamped by remaining, skip rest of source
+			if (copyLen > toCopy)
+				lineData += (copyLen - toCopy);
+		}
+
+		src += lineSize + 2;
+		dst += pitch;
+	}
+}
+
+/**
+ * RA1 codec 2: Scatter/point draw (FUN_110D7).
+ *
+ * Draws individual pixels at accumulated offsets — used for starfield
+ * backgrounds. Each 4-byte entry: [dx:int16_le, dy:uint8, pixel:uint8].
+ * Position starts at (left, top) and accumulates (dx, dy) per entry.
+ * Pixel is drawn if position is within buffer bounds.
+ */
+static void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+	int curX = left;
+	int curY = top;
+
+	while (dataSize >= 4) {
+		int16 dx = (int16)READ_LE_UINT16(src);
+		uint8 dy = src[2];
+		uint8 pixel = src[3];
+		src += 4;
+		dataSize -= 4;
+
+		curX += dx;
+		curY += dy;
+
+		if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight) {
+			dst[curY * pitch + curX] = pixel;
+		}
+	}
+}
+
 /**
  * RA1 codec 4/5: Block-based frame decoder.
  * Adapted from FFmpeg's libavcodec/sanm.c old_codec4() (LGPL 2.1+).
@@ -991,6 +1090,17 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 			_specialBufferSize = bufSize;
 		}
 		_dst = _specialBuffer;
+	} else if (isRA1()) {
+		// RA1 sub-fullscreen frames (e.g. O1OPTION.ANM uses ~319x196 codec 2
+		// frames for the starfield animation). Render into _specialBuffer at
+		// their (left, top) offset position.
+		int bufSize = 384 * 242;
+		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+			free(_specialBuffer);
+			_specialBuffer = (byte *)calloc(bufSize, 1);
+			_specialBufferSize = bufSize;
+		}
+		_dst = _specialBuffer;
 	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
 		ra2SelectFrameBuffer(width, height);
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
@@ -1005,8 +1115,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		_width = width;
 		_height = height;
 	} else if (isRA1() && _dst == _specialBuffer) {
-		// RA1: small overlay FOBJs (codec 1) should not override dimensions
-		// set by the main 384x242 codec 5 FOBJ
+		// RA1: sub-fullscreen FOBJs should not override the 384x242 dimensions.
+		// Set dimensions on first use if not yet established.
+		if (_width == 0 || _height == 0) {
+			_width = 384;
+			_height = 242;
+		}
 	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
 		// RA2: preserve _width/_height set during buffer allocation
 	} else {
@@ -1018,9 +1132,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if (_dst == _specialBuffer)
 		pitch = _width;
 
-	// Save original FOBJ dimensions before clipping. Codec 37/47 (delta block/glyph)
-	// decode the full frame into the buffer starting at (0,0) regardless of FOBJ
-	// left/top position. They must use the original un-clipped dimensions.
+	// Save original FOBJ position and dimensions before clipping. Codec 37/47
+	// (delta block/glyph) decode the full frame starting at (0,0). Codec 2
+	// (scatter draw) needs the original start position for accumulation.
+	int origLeft = left;
+	int origTop = top;
 	int origWidth = width;
 	int origHeight = height;
 
@@ -1034,7 +1150,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	// For RLE codecs, skip source rows that were clipped from the top.
 	// Each RLE row has a 2-byte size prefix, so we can advance past them.
 	const uint8 *adjustedSrc = src;
-	if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT)) {
+	if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT || codec == SMUSH_CODEC_LINE_UPDATE)) {
 		for (int i = 0; i < srcSkipY; i++) {
 			adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
 		}
@@ -1042,9 +1158,22 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
+		if (isRA1()) {
+			// RA1 codec 1: pixel 0 is transparent (background shows through)
+			smushDecodeRA1Transparent(_dst, adjustedSrc, left, top, width, height, pitch);
+		} else {
+			smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
+		}
+		break;
 	case SMUSH_CODEC_RLE_ALT:
+		// Codec 3: all pixels opaque (pixel 0 is written)
 		smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
 		break;
+	case SMUSH_CODEC_RA1_SCATTER:
+		// Codec 2: Scatter draw uses absolute buffer coords, not clipped FOBJ coords.
+		// Pass full buffer dimensions for clipping (not the FOBJ width/height).
+		smushDecodeRA1Scatter(_dst, src, origLeft, origTop, _width, _height, pitch, dataSize);
+		break;
 	case SMUSH_CODEC_DELTA_BLOCKS:
 		// Codec 37 writes the full frame to dst via memcpy — always uses original
 		// FOBJ dimensions, not position-clipped dimensions.
@@ -1067,6 +1196,12 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	case SMUSH_CODEC_UNCOMPRESSED:
 		smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
 		break;
+	case SMUSH_CODEC_LINE_UPDATE:
+		if (isRA1()) {
+			smushDecodeRA1SkipCopy(_dst, adjustedSrc, left, top, width, height, pitch);
+			break;
+		}
+		// Fall through for RA2
 	default:
 		if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
 			break;
@@ -1166,6 +1301,10 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 		debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
 			_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
 	}
+	if (isRA1()) {
+		debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
+			_frame, codec, left, top, width, height, subSize - 14, _storeFrame);
+	}
 
 	int32 chunk_size = subSize - 14;
 	byte *chunk_buffer = (byte *)malloc(chunk_size);
@@ -1196,6 +1335,15 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		_vm->_insane->procPreRendering(_dst);
 	}
 
+	// RA1: Clear framebuffer before each frame's first draw operation.
+	// Matches FFmpeg's first_fob memset: process_frame_obj() zeroes fbuf
+	// before the first FOBJ (or FTCH re-decode) of each frame. Without this,
+	// codec 1's transparent pixels (pixel 0 = skip) show previous frame
+	// content, causing ghost trails on the Star Wars opening crawl.
+	if (isRA1() && _dst && _width > 0 && _height > 0) {
+		memset(_dst, 0, _width * _height);
+	}
+
 	while (frameSize > 0) {
 		const uint32 subType = b.readUint32BE();
 		const int32 subSize = b.readUint32BE();
@@ -1336,6 +1484,14 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			byte *palettePtr = &headerContent[6];
 			memcpy(_pal, palettePtr, sizeof(_pal));
 
+			if (isRA1()) {
+				// RA1: force palette index 0 to black. Some ANM files store
+				// non-black values (e.g. O1OPEN.ANM has palette[0] = blue)
+				// but the original engine always displays index 0 as black.
+				// XPAL delta animation skips index 0, so it's never corrected.
+				_pal[0] = _pal[1] = _pal[2] = 0;
+			}
+
 			if (isRA2())
 				ra2ResetDeltaPalette();
 
@@ -1343,11 +1499,20 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 		}
 
 		if (isRA1()) {
-			// v1 AHDR: offset 4-5 is not width/height, palette starts at offset 6.
-			// Keep dimensions unset here and let FOBJ decoding select buffer and
-			// effective width/height (screen-sized or 384x242 special buffer).
+			// v1 AHDR: pre-allocate 384x242 special buffer and set _dst so that
+			// procPostRendering has a valid target. Keep _width/_height at 0 so
+			// updateScreen() is NOT called until the first FOBJ sets dimensions —
+			// this avoids blitting an empty buffer with palette[0] (which may be
+			// non-black, e.g. O1OPEN.ANM has palette[0] = blue).
 			_width = 0;
 			_height = 0;
+			int bufSize = 384 * 242;
+			if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+				free(_specialBuffer);
+				_specialBuffer = (byte *)calloc(bufSize, 1);
+				_specialBufferSize = bufSize;
+			}
+			_dst = _specialBuffer;
 		} else {
 			_width = READ_LE_UINT16(&headerContent[4]);
 			_height = READ_LE_UINT16(&headerContent[6]);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index a2454d93546..e2b6427bf90 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -77,6 +77,7 @@ namespace Scumm {
 #define TRK_USERID_SFX    3
 
 #define SMUSH_CODEC_RLE          1
+#define SMUSH_CODEC_RA1_SCATTER  2    // RA1: Scatter/point draw (starfield)
 #define SMUSH_CODEC_RLE_ALT      3
 #define SMUSH_CODEC_RA1_DELTA    4    // RA1: Delta block codec (skip on idx 0x80)
 #define SMUSH_CODEC_RA1_BLOCK    5    // RA1: Block-based frame codec (no skip)


Commit: 591fc6920521410bd8229eef0052f5ccd25ff70c
    https://github.com/scummvm/scummvm/commit/591fc6920521410bd8229eef0052f5ccd25ff70c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:25+02:00

Commit Message:
SCUMM: RA1: Add sound and subtitles

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 16f1b4c14a5..0126587c9db 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -101,8 +101,6 @@ private:
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
-	void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
-	int getFontBankStringWidth(const char *text);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
@@ -111,6 +109,8 @@ private:
 	void terminateAudio();
 	void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
 public:
+	void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+	int getFontBankStringWidth(const char *text);
 	void processAudioFrame(int16 feedSize);
 private:
 
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 68ca58682a2..7da104cfcc7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -618,6 +618,13 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 }
 
 void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
+	// RA1 TEXT chunks have a different format: 2 × BE32 header + text data.
+	// Route to dedicated handler.
+	if (isRA1() && subType == MKTAG('T','E','X','T')) {
+		ra1HandleText(subSize, b);
+		return;
+	}
+
 	int pos_x = b.readSint16LE();
 	int pos_y = b.readSint16LE();
 	int flags = b.readSint16LE();
@@ -1418,6 +1425,19 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 				rebel1->handleGameChunk(subSize, b);
 			}
 			break;
+		case MKTAG('P','V','O','C'):
+			// RA1 voice-over audio: same 12-byte header format as PSAD
+			// (3 × BE32: trackId, seqNum, param) followed by SAUD data.
+			// Feed to audio system identically to PSAD.
+			if (!_compressedFileMode) {
+				audioChunk = (uint8 *)malloc(subSize + 8);
+				b.seek(-8, SEEK_CUR);
+				b.read(audioChunk, subSize + 8);
+				feedAudio(audioChunk, 0, 127, 0, 0);
+				free(audioChunk);
+				audioChunk = nullptr;
+			}
+			break;
 		case MKTAG('G','A','M','2'):
 		case MKTAG('F','A','D','E'):
 		case MKTAG('S','E','G','A'):
@@ -1426,7 +1446,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		case MKTAG('S','B','L',' '):
 		case MKTAG('S','B','L','2'):
 		case MKTAG('P','S','D','2'):
-		case MKTAG('P','V','O','C'):
 			debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
 			break;
 		default:
@@ -1612,6 +1631,80 @@ SmushFont *SmushPlayer::ra1GetFont(int font) {
 	return _sf[font];
 }
 
+/**
+ * RA1 TEXT chunk handler.
+ *
+ * RA1 TEXT format (different from SCUMM7/COMI TRES):
+ *   Bytes 0-3 (BE32): y position on screen
+ *   Bytes 4-7 (BE32): parameter (x hint or style)
+ *   Bytes 8+:         text content with 0x00 as line separator
+ *                     '<' / '>' switch font layers (multi-font markup)
+ *
+ * Text is rendered centered horizontally at the given y position.
+ * The '<' font-switch prefix is stripped before rendering.
+ */
+void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
+		return;
+
+	InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+	if (!rebel1)
+		return;
+
+	int yPos = b.readUint32BE();
+	/*int param =*/ b.readUint32BE();
+
+	int textLen = subSize - 8;
+	if (textLen <= 0)
+		return;
+
+	char *textBuf = (char *)malloc(textLen + 1);
+	b.read(textBuf, textLen);
+	textBuf[textLen] = '\0';
+
+	int pitch = _width;
+
+	// Split on 0x00 line separators and render each line centered.
+	// Strip '<' / '>' font layer markers (multi-font not yet supported).
+	const char *lineStart = textBuf;
+	int lineY = yPos;
+	int lineHeight = 10;
+
+	while (lineStart < textBuf + textLen) {
+		const char *lineEnd = lineStart;
+		while (lineEnd < textBuf + textLen && *lineEnd != '\0')
+			lineEnd++;
+
+		int len = (int)(lineEnd - lineStart);
+		if (len > 0) {
+			const char *cleaned = lineStart;
+			while (cleaned < lineEnd && (*cleaned == '<' || *cleaned == '>'))
+				cleaned++;
+
+			int cleanLen = (int)(lineEnd - cleaned);
+			if (cleanLen > 0 && lineY >= 0 && lineY < _height) {
+				char *line = (char *)malloc(cleanLen + 1);
+				memcpy(line, cleaned, cleanLen);
+				line[cleanLen] = '\0';
+
+				// Center the line horizontally
+				int strWidth = rebel1->getFontBankStringWidth(line);
+				int x = (_width - strWidth) / 2;
+				if (x < 0) x = 0;
+
+				rebel1->drawFontBankString(_dst, pitch, _width, _height, x, lineY, line);
+
+				free(line);
+				lineY += lineHeight;
+			}
+		}
+
+		lineStart = lineEnd + 1;
+	}
+
+	free(textBuf);
+}
+
 void SmushPlayer::parseNextFrame() {
 
 	if (_seekPos >= 0) {
@@ -1693,7 +1786,7 @@ void SmushPlayer::parseNextFrame() {
 		ra2ParseNextFrame();
 
 	if (!_imuseDigital && isRA1())
-		processDispatches(_smushAudioSampleRate / 15);
+		processDispatches(_smushAudioSampleRate / _speed);
 }
 
 void SmushPlayer::setPalette(const byte *palette) {
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index e2b6427bf90..f594df5866f 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -330,6 +330,7 @@ private:
 	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
 	void ra2ResetDeltaPalette();
 	SmushFont *ra1GetFont(int font);
+	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
 	SmushFont *ra2GetFont(int font);
 	void ra2ParseNextFrame();
 	void ra2FixupAnimHeader();


Commit: d1a0b4d9bf9eda1115c76cdf40b5f37d6a392d20
    https://github.com/scummvm/scummvm/commit/d1a0b4d9bf9eda1115c76cdf40b5f37d6a392d20
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:26+02:00

Commit Message:
SCUMM: RA1: Add damage handling

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 068597e8c32..0d8673c20fa 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -105,6 +105,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_lives = 3;
 	_score = 0;
 	_damageFlags = 0;
+	_prevDamageFlags = 0;
 	_gameLatch5D = 0;
 	_gameLatch5F = 0;
 	_damageCooldown = 0;
@@ -505,6 +506,9 @@ void InsaneRebel1::loadLevelSprites(int level) {
 	Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
 	loadRA1Nut(filename.c_str(), _shipBank);
 	loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
+
+	Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
+	loadRA1Nut(bangFile.c_str(), _bangBank);
 }
 
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
@@ -532,6 +536,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	updateShipPhysics();
 	renderShip(renderBitmap, pitch, width, height);
+	renderExplosions(renderBitmap, pitch, width, height);
 	renderHUD(renderBitmap, pitch, width, height);
 }
 
@@ -868,6 +873,7 @@ void InsaneRebel1::updateShipPhysics() {
 		if (_health < 0)
 			_deathTimer = kDeathTimerInit;
 
+		_prevDamageFlags = _damageFlags;
 		_damageCooldown = kDamageCooldownInit;
 		_screenFlash = 3;
 	}
@@ -902,6 +908,11 @@ void InsaneRebel1::updateShipPhysics() {
 }
 
 void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
+	// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
+	// Hidden during last 20 frames of death sequence (deathTimer 20→0)
+	if (_health < 0 && _deathTimer <= 20)
+		return;
+
 	if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
 		return;
 
@@ -916,6 +927,65 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 }
 
+// Render explosion sprites during damage cooldown and death sequence.
+// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
+void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
+	if (_bangBank.numSprites <= 0)
+		return;
+
+	// Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
+	int shipScreenX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX;
+	int shipScreenY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY;
+
+	// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
+	// When dead and deathTimer > 10: random explosion sprites scatter around ship
+	if (_health < 0 && _deathTimer > 10) {
+		int intensity = _deathTimer - 10;  // 20→1 as timer goes 30→11
+		if (intensity > 10)
+			intensity = 20 - intensity;     // Triangle: 0→10→0
+
+		// di = intensity * 4 + 1 (vertical scatter range)
+		// si = -20 + intensity * 4 (horizontal scatter range, DAT_75d8 is 0)
+		int rangeY = intensity * 4 + 1;
+		int rangeX = -20 + intensity * 4;
+		if (rangeX < 1) rangeX = 1;
+
+		for (int i = 0; i < intensity; i++) {
+			// Random sprite from bang bank (FUN_21db0(10))
+			int sprIdx = _vm->_rnd.getRandomNumber(_bangBank.numSprites - 1);
+
+			// Random position around ship (matching assembly random scatter)
+			int randX = (int)_vm->_rnd.getRandomNumber(rangeX * 2) - rangeX;
+			int randY = (int)_vm->_rnd.getRandomNumber(rangeY * 2) - rangeY;
+
+			int drawX = shipScreenX + randX;
+			int drawY = shipScreenY + randY;
+
+			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+			renderSprite(dst, pitch, width, height,
+				drawX - spr.width / 2, drawY - spr.height / 2, spr);
+		}
+		return;
+	}
+
+	// --- Damage hit explosion (FUN_1DEB5 LAB_1e185) ---
+	// When alive, in cooldown, and bang bank loaded
+	if (_health >= 0 && _damageCooldown > 0) {
+		// Sprite index = 10 - damageCooldown (frames 0→9 as cooldown 10→1)
+		int sprIdx = _bangBank.numSprites - _damageCooldown;
+		if (sprIdx < 0 || sprIdx >= _bangBank.numSprites)
+			return;
+
+		// Position at ship center (DAT_75d8 is always 0 in RA1)
+		int drawX = shipScreenX;
+		int drawY = shipScreenY;
+
+		const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+		renderSprite(dst, pitch, width, height,
+			drawX - spr.width / 2, drawY - spr.height / 2, spr);
+	}
+}
+
 // Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
 // Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
 void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
@@ -1044,6 +1114,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
 		// This is not a pure control-mode assignment.
 		_damageFlags = 0;
+		_prevDamageFlags = 0;
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
@@ -1243,6 +1314,7 @@ bool InsaneRebel1::runLevel1() {
 		// Reset health for this attempt
 		_health = kMaxHealth;
 		_damageFlags = 0;
+		_prevDamageFlags = 0;
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0126587c9db..bd10c389489 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -101,6 +101,7 @@ private:
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
+	void renderExplosions(byte *dst, int pitch, int width, int height);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
@@ -119,6 +120,7 @@ private:
 	RA1SpriteBank _shipBank;
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
 	RA1SpriteBank _hudFontBank;   // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
+	RA1SpriteBank _bangBank;      // LxBANG.NUT — impact/explosion sprites (10 frames)
 	SmushFont *_menuFont;         // Use engine text renderer for correct TALKFONT character mapping
 
 	// RA1 screen dimensions (384x242)
@@ -163,6 +165,7 @@ private:
 	int16 _lives;                // 0x7562: remaining extra lives
 	int _score;                  // 0x7564: current score
 	byte _damageFlags;           // 0x74D4: per-frame collision bitmask (cleared each frame)
+	byte _prevDamageFlags;       // 0x74D6: previous frame's damage flags (for explosion direction)
 	uint16 _gameLatch5D;         // 0x75D2: GAME 0x5D latch (scene/obstacle/event trigger)
 	uint16 _gameLatch5F;         // 0x75D4: GAME 0x5F latch (probabilistic hit trigger)
 	int16 _damageCooldown;       // 0x74D8: invulnerability timer (10 frames after hit)


Commit: ecfc1eefad1ae8ed200d87f357353292c6938096
    https://github.com/scummvm/scummvm/commit/ecfc1eefad1ae8ed200d87f357353292c6938096
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:26+02:00

Commit Message:
SCUMM: RA1: Improve level 1 loop

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 0d8673c20fa..c95abc44570 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -1285,13 +1285,13 @@ int InsaneRebel1::runMainMenu() {
 // Level 1 flow (0x16100-0x16737):
 //   1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
 //   2. L1HANGAR.ANM — Hangar departure cutscene
-//   3. "Chapter 1" text
-//   4. L1CU1.ANM — Pre-flight cutscene
-//   5. L1PLAY1L.ANM — Stage 1 gameplay (left path) — INTERACTIVE
-//   6. L1PLAY1R.ANM — Stage 1 gameplay (right path) — INTERACTIVE
-//   7. L1CU2.ANM — Mid-level cutscene
-//   8. L1PLAY2.ANM — Stage 2 turret — INTERACTIVE
-//   9. L1END.ANM — Level end cutscene
+//   3. L1CU1.ANM — Pre-flight cutscene
+//   4. L1PLAY1L.ANM or L1PLAY1R.ANM — Stage 1 flight (alternative paths)
+//      L1PLAY1L = "Hard" left path (788 frames), L1PLAY1R = "Easy" right path (396 frames)
+//      Original branches at frame 394 via nextSceneA/nextSceneB collision zones
+//   5. L1CU2.ANM — Mid-level cutscene
+//   6. L1PLAY2.ANM — Stage 2 turret — INTERACTIVE
+//   7. L1END.ANM — Level end cutscene
 //   Death: L1CRASHA/B.ANM → L1DEATH.ANM → L1RETRY.ANM → retry from L1NEW
 bool InsaneRebel1::runLevel1() {
 	debug(1, "InsaneRebel1: Running level 1");
@@ -1320,33 +1320,28 @@ bool InsaneRebel1::runLevel1() {
 		_screenFlash = 0;
 		_frameCounter = 0;
 
-		// L1PLAY1L.ANM — Stage 1 gameplay (left path, original: 0x5953)
+		// Stage 1 flight — L1PLAY1L (hard/left) or L1PLAY1R (easy/right).
+		// Original selects path via collision zones at frame 394 using
+		// nextSceneA(0x67)/nextSceneB(0x69). For now, always play PLAY1L.
+		// TODO: Implement path branching based on player ship position.
 		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
 		if (_health >= 0) {
-			// Survived stage 1 — continue to right path
-			// L1PLAY1R.ANM (original: 0x5965, flags with seekframe=0x187)
-			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+			// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
+			playCinematic("LVL1/L1CU2.ANM");
 			if (_vm->shouldQuit())
 				return false;
 
-			if (_health >= 0) {
-				// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
-				playCinematic("LVL1/L1CU2.ANM");
-				if (_vm->shouldQuit())
-					return false;
-
-				// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
-				playInteractiveVideo("LVL1/L1PLAY2.ANM");
-				if (_vm->shouldQuit())
-					return false;
+			// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
+			playInteractiveVideo("LVL1/L1PLAY2.ANM");
+			if (_vm->shouldQuit())
+				return false;
 
-				// L1END.ANM — Level complete! (original: 0x59a3)
-				playCinematic("LVL1/L1END.ANM");
-				return true;
-			}
+			// L1END.ANM — Level complete! (original: 0x59a3)
+			playCinematic("LVL1/L1END.ANM");
+			return true;
 		}
 
 		// Death sequence (original: 0x165e8-0x16737)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 7da104cfcc7..429af0d73cf 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -264,6 +264,8 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_compressedFileMode = false;
 	_width = 0;
 	_height = 0;
+	_scrollX = 0;
+	_scrollY = 0;
 
 	ra2InitFields();
 	_IACTpos = 0;
@@ -322,6 +324,8 @@ void SmushPlayer::init(int32 speed) {
 	_storeFrame = false;
 	_fobjOffsetX = 0;
 	_fobjOffsetY = 0;
+	_scrollX = 0;
+	_scrollY = 0;
 
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;


Commit: 01e04961b098668a4b982aa2ee97d2e76f302892
    https://github.com/scummvm/scummvm/commit/01e04961b098668a4b982aa2ee97d2e76f302892
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:26+02:00

Commit Message:
SCUMM: RA1: Add right-path branching in level 1

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index c95abc44570..dad2a5b5e68 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -113,6 +113,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenFlash = 0;
 	_frameCounter = 0;
 	_interactiveVideoActive = false;
+	_pathBranchEnabled = false;
+	_rightPathSelected = false;
 	_menuActive = false;
 	_menuConfirmed = false;
 	_menuSelection = 0;
@@ -901,6 +903,21 @@ void InsaneRebel1::updateShipPhysics() {
 	// Clear per-frame damage flags
 	_damageFlags = 0;
 
+	// --- Path branching detection ---
+	// Original enables branching at frame 394 (nextSceneA/nextSceneB in FUN_1B297).
+	// After this frame, if the ship is on the right side of the screen, the player
+	// has chosen the right/easy path. We signal the video to stop so runLevel1()
+	// can switch to L1PLAY1R.
+	if (_pathBranchEnabled && !_rightPathSelected && _frameCounter > (uint32)kPathBranchFrame) {
+		// Ship past center-right threshold = right path chosen
+		static const int16 kRightPathThreshold = kCenterX + 40;
+		if (_shipPosX > kRightPathThreshold) {
+			_rightPathSelected = true;
+			_vm->_smushVideoShouldFinish = true;
+			debug(1, "RA1: Right path selected at frame %d (shipX=%d)", _frameCounter, _shipPosX);
+		}
+	}
+
 	debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
 		_shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
 		positionDeltaX, _shipDirIndex, _health,
@@ -1210,13 +1227,16 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 // Play a passive cinematic (no game callback, skippable).
 // Reuses RA2's pattern: reset handler, set cinematic flags, play video.
-void InsaneRebel1::playCinematic(const char *filename) {
-	debug(1, "InsaneRebel1::playCinematic('%s')", filename);
+// startFrame > 0: fast-forward (decode without display/audio) to that frame.
+void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
+	debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
 	_interactiveVideoActive = false;
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve
+	if (startFrame > 0)
+		splayer->setFastForwardToFrame(startFrame);
 	splayer->play(filename, 12);
 }
 
@@ -1282,36 +1302,41 @@ int InsaneRebel1::runMainMenu() {
 	return 5;
 }
 
-// Level 1 flow (0x16100-0x16737):
+// Level 1 flow (0x16100-0x167A2, from disassembly):
 //   1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
-//   2. L1HANGAR.ANM — Hangar departure cutscene
-//   3. L1CU1.ANM — Pre-flight cutscene
-//   4. L1PLAY1L.ANM or L1PLAY1R.ANM — Stage 1 flight (alternative paths)
-//      L1PLAY1L = "Hard" left path (788 frames), L1PLAY1R = "Easy" right path (396 frames)
-//      Original branches at frame 394 via nextSceneA/nextSceneB collision zones
+//   2. L1HANGAR.ANM — Full hangar departure cutscene (782 frames, flags 0x0420)
+//   3. L1CU1.ANM — Pre-flight cutscene (flags 0x0400)
+//   4. L1PLAY1L.ANM — Stage 1 flight, hard/left path (788 frames)
+//      At frame 394, if player steers right → L1PLAY1R (easy path, 396 frames)
 //   5. L1CU2.ANM — Mid-level cutscene
-//   6. L1PLAY2.ANM — Stage 2 turret — INTERACTIVE
-//   7. L1END.ANM — Level end cutscene
-//   Death: L1CRASHA/B.ANM → L1DEATH.ANM → L1RETRY.ANM → retry from L1NEW
+//   6. L1PLAY2.ANM — Stage 2 turret
+//      If score < 5 (0x75D0): L1RETRY → retry Stage 2
+//   7. L1END.ANM — Level complete
+//   Death (health<0): L1CRASHA/B → lives check:
+//     lives>0: L1NEW → jump back to Stage 1 (skip L1HANGAR/L1CU1)
+//     lives==0: L1DEATH → return to menu
+
 bool InsaneRebel1::runLevel1() {
 	debug(1, "InsaneRebel1: Running level 1");
 
 	// Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
 	loadLevelSprites(1);
 
-	// L1HANGAR.ANM — Hangar departure intro (original: 0x5918, flags 0x0420)
+	// L1HANGAR.ANM — Hangar departure (original: 0x5918, flags 0x0420)
+	// Plays once at level start, never replayed on retry.
 	playCinematic("LVL1/L1HANGAR.ANM");
 	if (_vm->shouldQuit())
 		return false;
 
 	// L1CU1.ANM — Pre-flight cutscene (original: 0x5944, flags 0x0400)
+	// Plays once at level start, never replayed on retry.
 	playCinematic("LVL1/L1CU1.ANM");
 	if (_vm->shouldQuit())
 		return false;
 
-	// Retry loop
+	// Retry loop — on death with lives, L1NEW plays then jumps back here
 	while (!_vm->shouldQuit()) {
-		// Reset health for this attempt
+		// Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
 		_health = kMaxHealth;
 		_damageFlags = 0;
 		_prevDamageFlags = 0;
@@ -1319,15 +1344,23 @@ bool InsaneRebel1::runLevel1() {
 		_deathTimer = 0;
 		_screenFlash = 0;
 		_frameCounter = 0;
+		_pathBranchEnabled = true;
+		_rightPathSelected = false;
 
-		// Stage 1 flight — L1PLAY1L (hard/left) or L1PLAY1R (easy/right).
-		// Original selects path via collision zones at frame 394 using
-		// nextSceneA(0x67)/nextSceneB(0x69). For now, always play PLAY1L.
-		// TODO: Implement path branching based on player ship position.
+		// Stage 1 flight — L1PLAY1L (hard/left path)
 		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
+		if (_rightPathSelected && _health >= 0) {
+			debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
+			_pathBranchEnabled = false;
+			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+			if (_vm->shouldQuit())
+				return false;
+		}
+		_pathBranchEnabled = false;
+
 		if (_health >= 0) {
 			// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
 			playCinematic("LVL1/L1CU2.ANM");
@@ -1339,12 +1372,15 @@ bool InsaneRebel1::runLevel1() {
 			if (_vm->shouldQuit())
 				return false;
 
+			// TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
+			// If score < 5: L1RETRY → retry Stage 2
+
 			// L1END.ANM — Level complete! (original: 0x59a3)
 			playCinematic("LVL1/L1END.ANM");
 			return true;
 		}
 
-		// Death sequence (original: 0x165e8-0x16737)
+		// Death sequence (original: 0x165dd-0x166bb)
 		// Random crash variant A or B
 		if (_vm->_rnd.getRandomNumber(1) == 0)
 			playCinematic("LVL1/L1CRASHA.ANM");
@@ -1353,30 +1389,21 @@ bool InsaneRebel1::runLevel1() {
 		if (_vm->shouldQuit())
 			return false;
 
-		// L1DEATH.ANM (original: 0x5a4b)
-		playCinematic("LVL1/L1DEATH.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
+		// Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
 		_lives--;
 		if (_lives <= 0) {
-			// Game over — no more retries
+			// Game over — L1DEATH then return (original: 0x166C0)
+			playCinematic("LVL1/L1DEATH.ANM");
 			debug(1, "InsaneRebel1: Game over (no lives left)");
 			return false;
 		}
 
-		// L1RETRY.ANM — Retry prompt (original: 0x5a5c)
-		// After retry, original jumps back to L1NEW→L1PLAY1L (0x16214→0x16680)
-		playCinematic("LVL1/L1RETRY.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
-		// L1NEW.ANM — Briefing before retry (original: 0x5a3c at retry path 0x16680)
+		// Lives remaining — L1NEW briefing then retry (original: 0x16675)
 		playCinematic("LVL1/L1NEW.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
-		// Loop back to gameplay
+		// Loop back to gameplay (original: JMP 0x16214 — health reset + Stage 1)
 	}
 
 	return false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index bd10c389489..82ce46cd084 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -91,7 +91,8 @@ private:
 	bool runLevel1();
 
 	// Play a passive cinematic (no game callback, skippable)
-	void playCinematic(const char *filename);
+	// startFrame > 0: fast-forward (decode without display) to that frame
+	void playCinematic(const char *filename, int32 startFrame = 0);
 
 	// Play interactive gameplay video (with ship physics + HUD)
 	void playInteractiveVideo(const char *filename);
@@ -191,6 +192,13 @@ private:
 	// True only while an interactive gameplay SMUSH is running.
 	bool _interactiveVideoActive;
 
+	// Path branching for levels with left/right alternative videos.
+	// Original sets nextSceneA/nextSceneB at frame 394 to enable branching.
+	// In our implementation, we check ship position after the branch frame.
+	static const int kPathBranchFrame = 394;
+	bool _pathBranchEnabled;     // True after branch frame is reached
+	bool _rightPathSelected;     // True if player chose the right/easy path
+
 	// Main menu state (for O1OPTION interactive overlay)
 	bool _menuActive;
 	bool _menuConfirmed;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 429af0d73cf..fbae471f39d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -326,6 +326,7 @@ void SmushPlayer::init(int32 speed) {
 	_fobjOffsetY = 0;
 	_scrollX = 0;
 	_scrollY = 0;
+	_fastForwardToFrame = 0;
 
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
@@ -1384,7 +1385,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleZlibFrameObject(subSize, b);
 			break;
 		case MKTAG('P','S','A','D'):
-			if (!_compressedFileMode) {
+			if (!_compressedFileMode && _fastForwardToFrame == 0) {
 				audioChunk = (uint8 *)malloc(subSize + 8);
 				b.seek(-8, SEEK_CUR);
 				b.read(audioChunk, subSize + 8);
@@ -1433,7 +1434,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			// RA1 voice-over audio: same 12-byte header format as PSAD
 			// (3 × BE32: trackId, seqNum, param) followed by SAUD data.
 			// Feed to audio system identically to PSAD.
-			if (!_compressedFileMode) {
+			if (!_compressedFileMode && _fastForwardToFrame == 0) {
 				audioChunk = (uint8 *)malloc(subSize + 8);
 				b.seek(-8, SEEK_CUR);
 				b.read(audioChunk, subSize + 8);
@@ -1942,7 +1943,15 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 	for (;;) {
 		bool skipFrame = false;
 
-		if (!_paused) {
+		// RA1 fast-forward: process frames rapidly without display/audio
+		// until reaching the target frame. Used to skip recap sections.
+		bool fastForwarding = (_fastForwardToFrame > 0 && _frame < _fastForwardToFrame);
+
+		if (fastForwarding) {
+			// Process frame immediately without timing
+			timerCallback();
+			_updateNeeded = false;
+		} else if (!_paused) {
 			uint32 now, elapsed;
 
 			if (_insanity) {
@@ -1980,10 +1989,20 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 			}
 		}
 
-		_vm->parseEvents();
-		_vm->processInput();
+		// When fast-forwarding completes, reset timing so playback
+		// starts from the correct point without trying to catch up.
+		if (_fastForwardToFrame > 0 && _frame >= _fastForwardToFrame) {
+			_startFrame = _frame;
+			_startTime = _vm->_system->getMillis();
+			_fastForwardToFrame = 0;
+		}
+
+		if (!fastForwarding) {
+			_vm->parseEvents();
+			_vm->processInput();
+		}
 
-		if (!_paused) {
+		if (!_paused && !fastForwarding) {
 			if (_palDirtyMax >= _palDirtyMin) {
 				// Apply gamma correction for Mac versions
 				if (_vm->_macScreen) {
@@ -2076,7 +2095,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 			_vm->_system->updateScreen();
 		}
 
-		_vm->_system->delayMillis(10);
+		if (!fastForwarding)
+			_vm->_system->delayMillis(10);
 	}
 
 	release();
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index f594df5866f..ae942278dd6 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -190,6 +190,7 @@ private:
 	bool _skipNext;
 	bool _ra2FastForwarding;  // Fast-forwarding RA2 BEG video to establish background
 	uint32 _frame;
+	uint32 _fastForwardToFrame;  // RA1: skip display/audio until this frame (0 = disabled)
 
 	// RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
 	// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
@@ -255,6 +256,7 @@ public:
 	bool isAudioCallbackEnabled();
 	byte *getVideoPalette();
 	void setCurVideoFlags(int16 flags);
+	void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
 
 	// Masked regions - areas where video should not update (e.g., destroyed enemies)
 	// The Insane class can add/remove regions, and decodeFrameObject will restore


Commit: 8074a8c46090a0d45c127efd8e81cf8acd785f33
    https://github.com/scummvm/scummvm/commit/8074a8c46090a0d45c127efd8e81cf8acd785f33
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:27+02:00

Commit Message:
SCUMM: RA1: Refine level 1 flight handling

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index dad2a5b5e68..d90e6e43de7 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -35,18 +35,19 @@
 namespace Scumm {
 
 // RA1 coordinate constants (scaled from RA2's 424x260 → 384x242)
-// RA2 center: (212, 130), RA1 center: (192, 121)
-// RA2 bounds: [20, 404]x[20, 240], RA1 bounds: [18, 366]x[18, 224]
-static const int16 kCenterX = 192;
-static const int16 kCenterY = 121;
-static const int16 kMinX = 18;
-static const int16 kMaxX = 366;
-static const int16 kMinY = 18;
-static const int16 kMaxY = 224;
-
-// Perspective focal lengths (scaled from RA2: focalX=0x2b, focalY=0x19)
-static const int16 kFocalX = 39;   // 0x2b * 384/424 ≈ 39
-static const int16 kFocalY = 23;   // 0x19 * 242/260 ≈ 23
+// Original coordinate space: 320x200 game viewport at (0,0) in the 384x242 buffer.
+// Ship base position in the original: (0xA0, 100) = (160, 100) = center of 320x200.
+// Accumulator range: ±0x82 (~±130), so ship can reach ~(30..290, -30..230).
+static const int16 kCenterX = 160;  // _DAT_74B6 init = 0xA0
+static const int16 kCenterY = 100;  // _DAT_74B8 init = 100
+static const int16 kMinX = 20;
+static const int16 kMaxX = 300;
+static const int16 kMinY = 20;
+static const int16 kMaxY = 180;
+
+// Perspective focal lengths (from original tuning table)
+static const int16 kFocalX = 43;    // 0x2b
+static const int16 kFocalY = 25;    // 0x19
 
 // Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
 // Same algorithm as NutRenderer::codec21 but without palette tracking.
@@ -89,15 +90,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_corridorRightX = kMaxX;
 	_corridorBottomY = kMaxY;
 
-	_smoothedVelocity = 0;
-	_verticalInput = 0;
-	memset(_velocityHistory, 0, sizeof(_velocityHistory));
+	_rollAccum = 0;
+	_liftSmooth = 0;
+	_posAccumX = 0;
+	_posAccumY = 0;
 	_driftParam = 0;
-	_driftAccum = 0;
 
 	_perspectiveX = 0;
 	_perspectiveY = 0;
-	_viewShift = 0;
 
 	_flyControlMode = 0;
 
@@ -119,6 +119,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_menuConfirmed = false;
 	_menuSelection = 0;
 	_menuFrameCounter = 0;
+	_turbulenceEnabled = false;
 	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
 		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
 	} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
@@ -697,8 +698,9 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	}
 }
 
-// Velocity-based ship physics adapted from RA2 Handler 7 (FUN_40C3CC case 4).
-// Mouse input → velocity history averaging → position delta → corridor collision → perspective.
+// Ship physics matching FUN_1DEB5 (accumulator-based position system).
+// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
+// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
 void InsaneRebel1::updateShipPhysics() {
 	_frameCounter++;
 
@@ -707,108 +709,102 @@ void InsaneRebel1::updateShipPhysics() {
 		_damageCooldown--;
 
 	// --- Step 1: Mouse input as offset from screen center ---
-	// Use _vm->_mouse (0-319, 0-199 virtual screen coords), same as RA2.
-	// Center = (160, 100) in virtual screen space.
+	// Original: _DAT_756C (horizontal), _DAT_756E (vertical)
 	int16 inputX = (int16)(_vm->_mouse.x - 160);
 	int16 inputY = (int16)(_vm->_mouse.y - 100);
-
-	// Clamp: [-160, 160] horizontal, [-127, 127] vertical (same as RA2)
-	inputX = CLIP<int16>(inputX, -160, 160);
+	inputX = CLIP<int16>(inputX, -127, 127);
 	inputY = CLIP<int16>(inputY, -127, 127);
 
-	// --- Step 2: Scale to [-127, 127] (same as RA2: scaledInputX = inputX * 127 / 160) ---
-	int16 scaledInputX = (int16)((inputX * 127) / 160);
-	int16 scaledInputY = inputY;
-
-	// --- Step 3: Velocity history + smoothed average ---
-	for (int i = 24; i > 0; i--)
-		_velocityHistory[i] = _velocityHistory[i - 1];
-	_velocityHistory[0] = scaledInputX;
-
-	const int smoothWindow = 5;
-	int velSum = 0;
-	for (int i = 0; i < smoothWindow; i++)
-		velSum += _velocityHistory[i];
-	_smoothedVelocity = (int16)(velSum / smoothWindow);
-
-	// --- Step 4: Position delta ---
-	const int16 levelSpeed = 32;
-	const int16 levelYSpeed = 48;
-	int16 absSmoothVel = ABS(_smoothedVelocity);
-	int16 positionDeltaX;
-
-	if (_flyControlMode == 1) {
-		// Mode 1: Full cross-axis coupling
-		if (scaledInputX < 1)
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY) >> 9);
-		else
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY) >> 9);
-	} else {
-		// Mode 0/2/3: Reduced cross-axis coupling
-		if (scaledInputX < 1)
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2)) >> 9);
-		else
-			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2)) >> 9);
-	}
-
-	// Original asm drift pipeline (0x1e3fc-0x1e5cb):
-	//   xDelta = (random(200) - 100 - driftTuning * _driftParam) / 2
-	//   accumX += xDelta  (32-bit accumulator)
-	//   shipDeltaX = accumX >> 8  (divide by 256 for pixel position)
-	// So drift contributes ~±0.2 px/frame. We approximate by accumulating and shifting.
-	const int16 kDriftTuning = 3; // TODO: load from per-difficulty/level tuning table
-	int16 driftBias = (int16)(_vm->_rnd.getRandomNumber(199) - 100 - kDriftTuning * _driftParam);
-	_driftAccum += driftBias >> 1;
-	_driftAccum = CLIP<int32>(_driftAccum, -0x8200, 0x8200);
-	positionDeltaX += (int16)(_driftAccum >> 8);
-
-	// Clamp X delta to ±12 per frame
-	positionDeltaX = CLIP<int16>(positionDeltaX, -12, 12);
-	_shipPosX += positionDeltaX;
-
-	// Y delta (no drift in original assembly — field4 is unused)
-	int16 positionDeltaY;
-	if (_flyControlMode == 1) {
-		positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
-		positionDeltaY = CLIP<int16>(positionDeltaY, -12, 12);
-	} else {
-		positionDeltaY = (int16)((levelYSpeed * scaledInputY) >> 10);
-	}
-	_shipPosY -= positionDeltaY;
-
-	_verticalInput = scaledInputY;
-
-	// --- Step 6: Position clamping ---
+	// --- Step 2: Roll accumulator (_74CA) ---
+	// Tuning values for Level 1 difficulty 0 (from data section)
+	static const int16 kRoll = 100;   // tuning[0x1B1B]
+	static const int16 kLift = 100;   // tuning[0x1B1D]
+	static const int16 kSlide = 60;   // tuning[0x1B1F]
+	static const int16 kDrift = 110;  // tuning[0x1B21]
+
+	// Normal mode: accumulate; mode 0x10: snap to input
+	_rollAccum += (kRoll * (int32)inputX) >> 5;
+	_rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
+
+	// --- Step 3: Vertical smoothing (_74CE) ---
+	// Exponential decay toward -inputY
+	_liftSmooth += (-_liftSmooth - (int32)inputY) >> 1;
+	_liftSmooth = CLIP<int32>(_liftSmooth, -0x20, 0x20);
+
+	// --- Step 4: Position accumulator deltas ---
+	// X delta: drift + slide coupling - cross-coupling
+	int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100;  // 0-199, centered at 100
+	int32 crossTermX;
+	if (_liftSmooth < 0)
+		crossTermX = (kLift * _liftSmooth * _rollAccum) >> 11;
+	else
+		crossTermX = (kLift * _liftSmooth * _rollAccum) >> 12;
+
+	int32 deltaX = (((rng - 100) - (int32)kDrift * _driftParam) >> 1)
+	             + ((kSlide * _rollAccum) >> 7)
+	             - crossTermX;
+
+	// Y delta: roll magnitude + lift cross-coupling
+	int32 absRoll = ABS(_rollAccum);
+	int32 crossTermY;
+	if (_liftSmooth < 0)
+		crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 12;
+	else
+		crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 13;
+
+	int32 deltaY = (absRoll >> 1) + crossTermY;
+
+	// --- Step 5: Update position accumulators ---
+	_posAccumX += deltaX;
+	_posAccumX = CLIP<int32>(_posAccumX, -0x8200, 0x8200);
+	_posAccumY += deltaY;
+	_posAccumY = CLIP<int32>(_posAccumY, -0x3200, 0x4600);
+
+	// --- Step 6: Derive pixel position from accumulators ---
+	// Original: _74BA = _74C2 >> 8, _74BC = _74C6 >> 8
+	// Ship position = base + offset
+	_shipPosX = kCenterX + (int16)(_posAccumX >> 8);
+	_shipPosY = kCenterY + (int16)(_posAccumY >> 8);
+
+	// Clamp to screen bounds
 	_shipPosX = CLIP<int16>(_shipPosX, kMinX, kMaxX);
 	_shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
 
-	// --- Step 7: Corridor collision (modes 0 and 2) ---
-	// From FUN_1C54D: wall contact sets damageFlags bits 0-3 (directional)
-	// and pushes velocity history to bounce away from the wall.
-	if (_flyControlMode == 0 || _flyControlMode == 2) {
-		if (_corridorRightX < _shipPosX) {
+	// --- Step 7: Corridor collision (FUN_1C54D) ---
+	// Wall contact forces position accumulators to corridor edge and sets
+	// damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
+	{
+		bool hasZoneHit = (_damageFlags & 0x10) != 0;
+
+		if (_shipPosX > _corridorRightX) {
+			_posAccumX = (_corridorRightX - kCenterX) << 8;
 			_shipPosX = _corridorRightX;
-			_damageFlags |= 0x02;  // Right wall
-			if (_damageCooldown < 5) {
-				for (int i = 0; i < 25; i++)
-					_velocityHistory[i] = -127;
+			if (!hasZoneHit) {
+				if (_rollAccum > -0x100)
+					_rollAccum = -0x100;  // Push left
+				_damageFlags |= 0x02;  // Right wall
 			}
 		}
 		if (_shipPosX < _corridorLeftX) {
+			_posAccumX = (_corridorLeftX - kCenterX) << 8;
 			_shipPosX = _corridorLeftX;
-			_damageFlags |= 0x04;  // Left wall
-			if (_damageCooldown < 5) {
-				for (int i = 0; i < 25; i++)
-					_velocityHistory[i] = 127;
+			if (!hasZoneHit) {
+				if (_rollAccum < 0x100)
+					_rollAccum = 0x100;   // Push right
+				_damageFlags |= 0x04;  // Left wall
 			}
 		}
-		if (_corridorBottomY < _shipPosY) {
-			_shipPosY = _corridorBottomY;
-			_damageFlags |= 0x01;  // Bottom wall
-		}
 		if (_shipPosY < _corridorTopY) {
+			_posAccumY = ((_corridorTopY - kCenterY) << 8) + 0x100;
 			_shipPosY = _corridorTopY;
-			_damageFlags |= 0x08;  // Top wall
+			if (!hasZoneHit)
+				_damageFlags |= 0x01;
+		}
+		if (_shipPosY > _corridorBottomY) {
+			_posAccumY = ((_corridorBottomY - kCenterY) << 8) - 0x100;
+			_shipPosY = _corridorBottomY;
+			if (!hasZoneHit)
+				_damageFlags |= 0x08;
 		}
 	}
 
@@ -833,24 +829,25 @@ void InsaneRebel1::updateShipPhysics() {
 			_perspectiveY = -_perspectiveY;
 	}
 
-	_viewShift = CLIP<int16>(_smoothedVelocity, -127, 127);
-
-	// --- Step 9: Direction sprite (5x7 grid with hysteresis) ---
-	// vDir from vertical input: (0xa0 - verticalInput) >> 6
-	int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
-	vDir = CLIP<int16>(vDir, 0, 4);
-
-	// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
-	int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
-	hDir = CLIP<int16>(hDir, 0, 6);
+	// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
+	// Horizontal component from _74CA (rollAccum):
+	//   |rollAccum| <= 0x80: center (0)
+	//   rollAccum > 0x80:  ((rollAccum - 0x80) >> 8) * 5 + 5   (right: 5,10,15,20)
+	//   rollAccum < -0x80: ((abs(rollAccum) - 0x80) >> 8) * 5 + 25 (left: 25,30,35,40)
+	int hComponent;
+	if (_rollAccum > 0x80) {
+		hComponent = ((_rollAccum - 0x80) >> 8) * 5 + 5;
+	} else if (_rollAccum < -0x80) {
+		hComponent = ((-_rollAccum - 0x80) >> 8) * 5 + 25;
+	} else {
+		hComponent = 0;
+	}
 
-	// Hysteresis at center positions
-	if (hDir == 3 && ABS(_smoothedVelocity) > 10)
-		hDir = (_smoothedVelocity < 1) ? 4 : 2;
-	if (vDir == 2 && ABS(_verticalInput) > 15)
-		vDir = (_verticalInput < 1) ? 3 : 1;
+	// Vertical component from _74CE (liftSmooth):
+	//   (_74CE + 0x20) * 5 / 0x41  → 0..4  (5 rows)
+	int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
 
-	_shipDirIndex = CLIP<int16>(vDir * 7 + hDir, 0, _shipBank.numSprites - 1);
+	_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
 
 	// --- Step 10: Damage/event bit synthesis + damage processing ---
 	// RA1 FUN_1B297-style latches from GAME opcodes:
@@ -918,9 +915,9 @@ void InsaneRebel1::updateShipPhysics() {
 		}
 	}
 
-	debug(7, "RA1 ship: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
-		_shipPosX, _shipPosY, _smoothedVelocity, _verticalInput,
-		positionDeltaX, _shipDirIndex, _health,
+	debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
+		_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
+		_posAccumX, _posAccumY, _shipDirIndex, _health,
 		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
@@ -1138,10 +1135,10 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
 		_driftParam = 0;
-		_driftAccum = 0;
-		_smoothedVelocity = 0;
-		_verticalInput = 0;
-		memset(_velocityHistory, 0, sizeof(_velocityHistory));
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
 
 		// Field1 == 0 corresponds to baseline recenter behavior in the original.
 		if ((int32)param1 == 0) {
@@ -1178,26 +1175,33 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 	case 0x0D:
 		// Corridor boundaries: per-frame flight corridor
-		// Raw: 0x0D, left, top, right, bottom (all 32-bit BE)
+		// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
+		// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
 		if (subSize >= 20) {
 			_corridorLeftX = (int16)param1;
 			_corridorTopY = (int16)b.readUint32BE();
-			_corridorRightX = (int16)b.readUint32BE();
-			_corridorBottomY = (int16)b.readUint32BE();
-			debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d",
-				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+			int16 corridorWidth = (int16)b.readUint32BE();
+			int16 corridorHeight = (int16)b.readUint32BE();
+			_corridorRightX = _corridorLeftX + corridorWidth;
+			_corridorBottomY = _corridorTopY + corridorHeight;
+			debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
+				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+				corridorWidth, corridorHeight);
 		}
 		break;
 
 	case 0x0E:
 		// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
+		// Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
 		if (subSize >= 20) {
 			int16 zoneLeft = (int16)param1;
 			int16 zoneTop = (int16)b.readUint32BE();
-			int16 zoneRight = (int16)b.readUint32BE();
-			int16 zoneBottom = (int16)b.readUint32BE();
-			if (_shipPosX >= zoneLeft && _shipPosX <= zoneRight &&
-				_shipPosY >= zoneTop && _shipPosY <= zoneBottom) {
+			int16 zoneWidth = (int16)b.readUint32BE();
+			int16 zoneHeight = (int16)b.readUint32BE();
+			int16 zoneRight = zoneLeft + zoneWidth;
+			int16 zoneBottom = zoneTop + zoneHeight;
+			if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
+				_shipPosY > zoneTop && _shipPosY < zoneBottom) {
 				_damageFlags |= 0x10;
 			}
 			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 82ce46cd084..8c8e0f7f79b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -128,35 +128,35 @@ private:
 	int _screenWidth;
 	int _screenHeight;
 
-	// Ship game-coordinate position (adapted from RA2's [20,404]x[20,240])
-	// RA1 coordinate space: [18,366]x[18,224], center=(192,121)
+	// Ship screen position = kCenter + (accumulator >> 8)
+	// Original: _DAT_74B6/_74B8 (base=160,100) + _DAT_74BA/_74BC (offset)
 	int16 _shipPosX;
 	int16 _shipPosY;
 
 	// Direction sprite index (5x7 grid = 35 sprites, vDir*7 + hDir)
 	int16 _shipDirIndex;
 
-	// Corridor boundaries (set by GAME opcode 0x07)
+	// Corridor boundaries (set by GAME opcode 0x0D, computed as left+width, top+height)
 	int16 _corridorLeftX;
 	int16 _corridorTopY;
 	int16 _corridorRightX;
 	int16 _corridorBottomY;
 
-	// Physics state (velocity-based movement from RA2 Handler 7)
-	int16 _smoothedVelocity;         // Averaged horizontal velocity
-	int16 _verticalInput;            // Stored vertical input component
-	int16 _velocityHistory[25];      // Horizontal velocity ring buffer
+	// Physics state (accumulator-based, matching FUN_1DEB5)
+	// _74CA: horizontal roll accumulator, driven by input * roll_tuning
+	int32 _rollAccum;
+	// _74CE: vertical smoothing, exponential decay toward -inputY
+	int32 _liftSmooth;
+	// _74C2/_74C6: position accumulators (32-bit), pixel offset = accum >> 8
+	int32 _posAccumX;
+	int32 _posAccumY;
 
-	// Per-frame drift bias from GAME 0x07 field3 (multiplied by tuning "drift" param)
-	// Original pipeline: xDelta added to 32-bit accumulator, position = accum >> 8
-	// field4 is unused in the original assembly
+	// Per-frame drift bias from GAME 0x07 field3
 	int16 _driftParam;
-	int32 _driftAccum;           // 32-bit drift accumulator (position = accum >> 8)
 
 	// Perspective view offsets
 	int16 _perspectiveX;
 	int16 _perspectiveY;
-	int16 _viewShift;                // Clamped smoothed velocity for view transform
 
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
@@ -204,6 +204,8 @@ private:
 	bool _menuConfirmed;
 	int _menuSelection; // 0..4 maps to return values 1..5
 	int _menuFrameCounter;
+
+	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
 };
 
 } // End of namespace Scumm


Commit: eca65436dd224d38097e7e3cc75bc4b11ba08591
    https://github.com/scummvm/scummvm/commit/eca65436dd224d38097e7e3cc75bc4b11ba08591
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:27+02:00

Commit Message:
SCUMM: RA1: Add options

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d90e6e43de7..63eba74a924 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -49,6 +49,16 @@ static const int16 kMaxY = 180;
 static const int16 kFocalX = 43;    // 0x2b
 static const int16 kFocalY = 25;    // 0x19
 
+// Per-difficulty tuning tables from assault_data_3.bin
+// Indexed: difficulty * 0x28B + level * 0x1F + offset
+// Level 1 values (level index 0):
+//                                roll  lift  slide drift wham  shot
+static const int16 kTuningLevel1[3][6] = {
+	{ 100, 100,  60, 110,  15,   0 },  // Easy
+	{ 100, 105,  60, 115,  25,   0 },  // Normal
+	{ 105, 110,  65, 120,  30,   0 },  // Hard
+};
+
 // Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
 // Same algorithm as NutRenderer::codec21 but without palette tracking.
 static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
@@ -77,6 +87,20 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
 	}
 }
 
+void InsaneRebel1::loadTuningForLevel(int level) {
+	int d = CLIP(_difficulty, 0, 2);
+	// Currently only Level 1 tuning is hardcoded; extend for other levels later
+	(void)level;
+	_tuning.roll  = kTuningLevel1[d][0];
+	_tuning.lift  = kTuningLevel1[d][1];
+	_tuning.slide = kTuningLevel1[d][2];
+	_tuning.drift = kTuningLevel1[d][3];
+	_tuning.wham  = kTuningLevel1[d][4];
+	_tuning.shot  = kTuningLevel1[d][5];
+	debug(1, "RA1: Loaded tuning for difficulty %d: roll=%d lift=%d slide=%d drift=%d wham=%d shot=%d",
+		d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.wham, _tuning.shot);
+}
+
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenWidth = 384;
 	_screenHeight = 242;
@@ -96,6 +120,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_posAccumY = 0;
 	_driftParam = 0;
 
+	_difficulty = 0;  // Easy by default
+	loadTuningForLevel(0);
+
 	_perspectiveX = 0;
 	_perspectiveY = 0;
 
@@ -119,6 +146,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_menuConfirmed = false;
 	_menuSelection = 0;
 	_menuFrameCounter = 0;
+	_optionsActive = false;
+	_optionsSel = 0;
 	_turbulenceEnabled = false;
 	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
 		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
@@ -155,7 +184,33 @@ InsaneRebel1::~InsaneRebel1() {
 }
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
-	if (_menuActive && event.type == Common::EVENT_KEYDOWN) {
+	if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_UP:
+		case Common::KEYCODE_w:
+			_optionsSel = (_optionsSel + 2) % 3;
+			return true;
+		case Common::KEYCODE_DOWN:
+		case Common::KEYCODE_s:
+			_optionsSel = (_optionsSel + 1) % 3;
+			return true;
+		case Common::KEYCODE_RETURN:
+		case Common::KEYCODE_KP_ENTER:
+		case Common::KEYCODE_SPACE:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_ESCAPE:
+			_optionsSel = 2;  // Back
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			break;
+		}
+	}
+
+	if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_w:
@@ -637,6 +692,51 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
 // Original menu strings from assault_data_3.bin at 0x5822.
 // Highlight uses RA2-style flashing border box (FUN_004292d0 pattern).
 void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+	_menuFrameCounter++;
+
+	if (_optionsActive) {
+		// --- Options submenu ---
+		static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+
+		const int titleW = getFontBankStringWidth("GAME OPTIONS");
+		drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
+
+		// Build dynamic option strings
+		char diffLine[64];
+		snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+		const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
+		const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
+
+		const int menuY = 60;
+		const int rowH = 16;
+
+		for (int i = 0; i < 3; i++) {
+			const int textW = getFontBankStringWidth(kOptionsItems[i]);
+			const int textX = (width - textW) / 2;
+			const int y = menuY + i * rowH;
+			drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
+
+			if (i == _optionsSel) {
+				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+				int bracketWidth = textW + 12;
+				int leftX = CLIP(textX - 6, 0, width - 1);
+				int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
+				int topY = CLIP(y - 1, 0, height - 1);
+				int bottomY = CLIP(y + rowH - 2, 0, height - 1);
+				for (int x = leftX; x <= rightX; x++) {
+					dst[topY * pitch + x] = highlightColor;
+					dst[bottomY * pitch + x] = highlightColor;
+				}
+				for (int py = topY; py <= bottomY; py++) {
+					dst[py * pitch + leftX] = highlightColor;
+					dst[py * pitch + rightX] = highlightColor;
+				}
+			}
+		}
+		return;
+	}
+
+	// --- Main menu ---
 	static const char *kMenuItems[5] = {
 		"START NEW GAME",
 		"GAME OPTIONS",
@@ -645,8 +745,6 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		"EXIT TO DOS"
 	};
 
-	_menuFrameCounter++;
-
 	// Center title
 	const int titleW = getFontBankStringWidth("MAIN MENU");
 	const int titleX = (width - titleW) / 2;
@@ -716,14 +814,8 @@ void InsaneRebel1::updateShipPhysics() {
 	inputY = CLIP<int16>(inputY, -127, 127);
 
 	// --- Step 2: Roll accumulator (_74CA) ---
-	// Tuning values for Level 1 difficulty 0 (from data section)
-	static const int16 kRoll = 100;   // tuning[0x1B1B]
-	static const int16 kLift = 100;   // tuning[0x1B1D]
-	static const int16 kSlide = 60;   // tuning[0x1B1F]
-	static const int16 kDrift = 110;  // tuning[0x1B21]
-
 	// Normal mode: accumulate; mode 0x10: snap to input
-	_rollAccum += (kRoll * (int32)inputX) >> 5;
+	_rollAccum += (_tuning.roll * (int32)inputX) >> 5;
 	_rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
 
 	// --- Step 3: Vertical smoothing (_74CE) ---
@@ -736,21 +828,21 @@ void InsaneRebel1::updateShipPhysics() {
 	int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100;  // 0-199, centered at 100
 	int32 crossTermX;
 	if (_liftSmooth < 0)
-		crossTermX = (kLift * _liftSmooth * _rollAccum) >> 11;
+		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
 	else
-		crossTermX = (kLift * _liftSmooth * _rollAccum) >> 12;
+		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 12;
 
-	int32 deltaX = (((rng - 100) - (int32)kDrift * _driftParam) >> 1)
-	             + ((kSlide * _rollAccum) >> 7)
+	int32 deltaX = (((rng - 100) - (int32)_tuning.drift * _driftParam) >> 1)
+	             + (((int32)_tuning.slide * _rollAccum) >> 7)
 	             - crossTermX;
 
 	// Y delta: roll magnitude + lift cross-coupling
 	int32 absRoll = ABS(_rollAccum);
 	int32 crossTermY;
 	if (_liftSmooth < 0)
-		crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 12;
+		crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 12;
 	else
-		crossTermY = (kLift * (0x7DE - absRoll) * _liftSmooth) >> 13;
+		crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 13;
 
 	int32 deltaY = (absRoll >> 1) + crossTermY;
 
@@ -864,10 +956,10 @@ void InsaneRebel1::updateShipPhysics() {
 		_health >= 0 && _deathTimer <= 0) {
 		// Projectile hit (bit 7 = 0x80)
 		if (_damageFlags & 0x80)
-			_health -= kHeavyDamage;
+			_health -= _tuning.shot;
 		// Wall collision (bits 1,2,4 = 0x16)
 		if (_damageFlags & 0x16)
-			_health -= kLightDamage;
+			_health -= _tuning.wham;
 
 		if (_health < 0)
 			_deathTimer = kDeathTimerInit;
@@ -1049,9 +1141,9 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 
 		// Color based on damage level (matching original thresholds from FUN_1BBCB)
 		byte barColor;
-		if (_health > kHeavyDamage * 2)
+		if (_health > _tuning.shot * 2)
 			barColor = 0xA0;  // Green — low damage
-		else if (_health > kLightDamage * 2)
+		else if (_health > _tuning.wham * 2)
 			barColor = 0x2C;  // Yellow — moderate damage
 		else
 			barColor = 0x30;  // Red — critical
@@ -1306,6 +1398,42 @@ int InsaneRebel1::runMainMenu() {
 	return 5;
 }
 
+void InsaneRebel1::runOptionsMenu() {
+	_optionsSel = 0;
+	_optionsActive = true;
+
+	while (!_vm->shouldQuit()) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+
+		if (_vm->shouldQuit())
+			break;
+
+		if (_menuConfirmed) {
+			switch (_optionsSel) {
+			case 0:
+				// Cycle difficulty
+				_difficulty = (_difficulty + 1) % 3;
+				loadTuningForLevel(0);
+				break;
+			case 1:
+				// Toggle turbulence
+				_turbulenceEnabled = !_turbulenceEnabled;
+				break;
+			case 2:
+				// Back to main menu
+				_optionsActive = false;
+				return;
+			}
+		}
+	}
+	_optionsActive = false;
+}
+
 // Level 1 flow (0x16100-0x167A2, from disassembly):
 //   1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
 //   2. L1HANGAR.ANM — Full hangar departure cutscene (782 frames, flags 0x0420)
@@ -1442,11 +1570,15 @@ void InsaneRebel1::runGame() {
 			// Return to menu after level ends
 			break;
 		}
+		case 2:
+			// Game Options
+			runOptionsMenu();
+			break;
 		case 5:
 			// Exit
 			return;
 		default:
-			// Options, Passcode, Demo — not yet implemented, return to menu
+			// Passcode, Demo — not yet implemented, return to menu
 			break;
 		}
 	}
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 8c8e0f7f79b..ceca0ab629b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -161,6 +161,22 @@ private:
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
 
+	// Difficulty (0=easy, 1=normal, 2=hard) — matches original DAT_22BC
+	int _difficulty;
+
+	// Per-difficulty tuning (from assault_data_3.bin, indexed: difficulty * 0x28B + level * 0x1F)
+	struct TuningParams {
+		int16 roll;    // 0x1B1B: horizontal speed/sensitivity
+		int16 lift;    // 0x1B1D: vertical speed/sensitivity
+		int16 slide;   // 0x1B1F: cross-axis coupling
+		int16 drift;   // 0x1B21: drift/turbulence multiplier
+		int16 wham;    // 0x1B27: light/wall damage
+		int16 shot;    // 0x1B29: heavy/projectile damage
+	};
+	TuningParams _tuning;
+
+	void loadTuningForLevel(int level);
+
 	// Damage system (from Ghidra decompilation of FUN_1DEB5)
 	int16 _health;               // 0x7560: current health (init=98, negative=dead, max=98)
 	int16 _lives;                // 0x7562: remaining extra lives
@@ -178,10 +194,6 @@ private:
 	static const int16 kDeathTimerInit = 30;
 	static const int16 kDamageCooldownInit = 10;
 
-	// Tuning damage values (TODO: load from data section per difficulty/level)
-	static const int16 kLightDamage = 5;   // "wham" — wall/zone collision
-	static const int16 kHeavyDamage = 15;  // "shot" — projectile hit
-
 	// Audio state (same structure as RA2)
 	static const int kMaxAudioTracks = 4;
 	Audio::QueuingAudioStream *_audioStreams[kMaxAudioTracks];
@@ -199,12 +211,17 @@ private:
 	bool _pathBranchEnabled;     // True after branch frame is reached
 	bool _rightPathSelected;     // True if player chose the right/easy path
 
-	// Main menu state (for O1OPTION interactive overlay)
+	// Main menu / options state
+	void runOptionsMenu();
 	bool _menuActive;
 	bool _menuConfirmed;
 	int _menuSelection; // 0..4 maps to return values 1..5
 	int _menuFrameCounter;
 
+	// Options submenu state
+	bool _optionsActive;     // True when showing options instead of main menu
+	int _optionsSel;         // 0=difficulty, 1=turbulence, 2=back
+
 	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
 };
 


Commit: 5a0f9d09a83c41de926638ee58e3c160f315a25c
    https://github.com/scummvm/scummvm/commit/5a0f9d09a83c41de926638ee58e3c160f315a25c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:27+02:00

Commit Message:
SCUMM: RA1: Add branching

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 63eba74a924..ac244829986 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -140,6 +140,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenFlash = 0;
 	_frameCounter = 0;
 	_interactiveVideoActive = false;
+	_gameCounter = 0;
 	_pathBranchEnabled = false;
 	_rightPathSelected = false;
 	_menuActive = false;
@@ -993,17 +994,15 @@ void InsaneRebel1::updateShipPhysics() {
 	_damageFlags = 0;
 
 	// --- Path branching detection ---
-	// Original enables branching at frame 394 (nextSceneA/nextSceneB in FUN_1B297).
-	// After this frame, if the ship is on the right side of the screen, the player
-	// has chosen the right/easy path. We signal the video to stop so runLevel1()
-	// can switch to L1PLAY1R.
-	if (_pathBranchEnabled && !_rightPathSelected && _frameCounter > (uint32)kPathBranchFrame) {
-		// Ship past center-right threshold = right path chosen
-		static const int16 kRightPathThreshold = kCenterX + 40;
-		if (_shipPosX > kRightPathThreshold) {
+	// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
+	// After this point, drift goes strongly negative (pushing ship left for the hard path).
+	// If ship is right of center, player chose the right/easy path → switch to L1PLAY1R.
+	// The check fires once when the game counter first reaches the branch point.
+	if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
+		if (_shipPosX > kCenterX) {
 			_rightPathSelected = true;
 			_vm->_smushVideoShouldFinish = true;
-			debug(1, "RA1: Right path selected at frame %d (shipX=%d)", _frameCounter, _shipPosX);
+			debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
 		}
 	}
 
@@ -1254,14 +1253,15 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		break;
 
 	case 0x07:
-		// Per-frame corridor data: f1=frame index, f2=constant(788), f3=drift bias, f4=unused
-		// Original asm: drift bias * tuning "drift" param, combined with random turbulence
-		// f4 is never referenced in the original handler function
+		// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
+		// f1 is the original's _DAT_7740 (game frame counter)
+		// f3 is the drift/wind parameter combined with tuning table
+		_gameCounter = param1;
 		if (subSize >= 20) {
-			b.readUint32BE(); // f2 (constant 788, unused in physics)
+			b.readUint32BE(); // f2 (max frames, unused in physics)
 			_driftParam = (int16)(int32)b.readUint32BE();
 			b.readUint32BE(); // f4 (unused in original assembly)
-			debug(7, "RA1 GAME 0x07: frame=%d driftParam=%d", param1, _driftParam);
+			debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
 		}
 		break;
 
@@ -1476,10 +1476,13 @@ bool InsaneRebel1::runLevel1() {
 		_deathTimer = 0;
 		_screenFlash = 0;
 		_frameCounter = 0;
+		_gameCounter = 0;
 		_pathBranchEnabled = true;
 		_rightPathSelected = false;
 
 		// Stage 1 flight — L1PLAY1L (hard/left path)
+		// The first 394 frames are the common section. At counter 394, if
+		// ship is right of center, we switch to L1PLAY1R (easy path).
 		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
 		if (_vm->shouldQuit())
 			return false;
@@ -1588,6 +1591,10 @@ void InsaneRebel1::runGame() {
 void InsaneRebel1::playInteractiveVideo(const char *filename) {
 	debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
 
+	// Stop any leftover audio from previous video
+	terminateAudio();
+	initAudio(_audioSampleRate);
+
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
 	clearBit(0);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index ceca0ab629b..3f884d8b894 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -205,10 +205,11 @@ private:
 	bool _interactiveVideoActive;
 
 	// Path branching for levels with left/right alternative videos.
-	// Original sets nextSceneA/nextSceneB at frame 394 to enable branching.
-	// In our implementation, we check ship position after the branch frame.
-	static const int kPathBranchFrame = 394;
-	bool _pathBranchEnabled;     // True after branch frame is reached
+	// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
+	// We check ship position at that counter value to decide left vs right path.
+	static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
+	int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
+	bool _pathBranchEnabled;     // True when branching is active for this video
 	bool _rightPathSelected;     // True if player chose the right/easy path
 
 	// Main menu / options state


Commit: c0146e48e02b067d14d59c199e78dd2dd36decad
    https://github.com/scummvm/scummvm/commit/c0146e48e02b067d14d59c199e78dd2dd36decad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00

Commit Message:
SCUMM: RA1: Add level 2 and HUD features

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index ac244829986..8911ad08a45 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -51,13 +51,22 @@ static const int16 kFocalY = 25;    // 0x19
 
 // Per-difficulty tuning tables from assault_data_3.bin
 // Indexed: difficulty * 0x28B + level * 0x1F + offset
-// Level 1 values (level index 0):
-//                                roll  lift  slide drift wham  shot
-static const int16 kTuningLevel1[3][6] = {
-	{ 100, 100,  60, 110,  15,   0 },  // Easy
-	{ 100, 105,  60, 115,  25,   0 },  // Normal
-	{ 105, 110,  65, 120,  30,   0 },  // Hard
+// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
+static const int16 kTuningTable[2][3][9] = {
+	// Level 1 (Flight Training)
+	{
+		{ 100, 100,  60, 110,   0,   0,  15,   0,   0 },  // Easy
+		{ 100, 105,  60, 115,   0,   0,  25,   0,   0 },  // Normal
+		{ 105, 110,  65, 120,   0,   0,  30,   0,   0 },  // Hard
+	},
+	// Level 2 (Asteroid Field Training)
+	{
+		{ 100,  16, 120,   0,   7,   0,  15,   0,  25 },  // Easy
+		{ 100,  18, 120,   0,   5,   0,  20,   0,  50 },  // Normal
+		{ 100,  20, 150,   0,   1,   0,  25,   0,  75 },  // Hard
+	},
 };
+static const int kNumTunedLevels = 2;
 
 // Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
 // Same algorithm as NutRenderer::codec21 but without palette tracking.
@@ -89,16 +98,19 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
 
 void InsaneRebel1::loadTuningForLevel(int level) {
 	int d = CLIP(_difficulty, 0, 2);
-	// Currently only Level 1 tuning is hardcoded; extend for other levels later
-	(void)level;
-	_tuning.roll  = kTuningLevel1[d][0];
-	_tuning.lift  = kTuningLevel1[d][1];
-	_tuning.slide = kTuningLevel1[d][2];
-	_tuning.drift = kTuningLevel1[d][3];
-	_tuning.wham  = kTuningLevel1[d][4];
-	_tuning.shot  = kTuningLevel1[d][5];
-	debug(1, "RA1: Loaded tuning for difficulty %d: roll=%d lift=%d slide=%d drift=%d wham=%d shot=%d",
-		d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.wham, _tuning.shot);
+	int l = CLIP(level, 0, kNumTunedLevels - 1);
+	_tuning.roll  = kTuningTable[l][d][0];
+	_tuning.lift  = kTuningTable[l][d][1];
+	_tuning.slide = kTuningTable[l][d][2];
+	_tuning.drift = kTuningTable[l][d][3];
+	_tuning.snap  = kTuningTable[l][d][4];
+	_tuning.miss  = kTuningTable[l][d][5];
+	_tuning.wham  = kTuningTable[l][d][6];
+	_tuning.shot  = kTuningTable[l][d][7];
+	_tuning.kill  = kTuningTable[l][d][8];
+	debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d snap=%d miss=%d wham=%d shot=%d kill=%d",
+		level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.snap, _tuning.miss,
+		_tuning.wham, _tuning.shot, _tuning.kill);
 }
 
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
@@ -126,11 +138,20 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_perspectiveX = 0;
 	_perspectiveY = 0;
 
+	memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+	memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+	memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+	memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+	_avgInputX = 0;
+	_avgInputY = 0;
+
+	_currentLevel = 0;
 	_flyControlMode = 0;
 
 	_health = kMaxHealth;
 	_lives = 3;
 	_score = 0;
+	_prevScore = 0;
 	_damageFlags = 0;
 	_prevDamageFlags = 0;
 	_gameLatch5D = 0;
@@ -562,12 +583,19 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 }
 
 void InsaneRebel1::loadLevelSprites(int level) {
-	Common::String filename = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
-	loadRA1Nut(filename.c_str(), _shipBank);
+	// Ship direction bank — not all levels have one (e.g. Level 2 is first-person)
+	Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
+	if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
+		debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
+	}
 	loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
 
+	// Explosion sprites — try BANG first, then EXPLD
 	Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
-	loadRA1Nut(bangFile.c_str(), _bangBank);
+	if (!loadRA1Nut(bangFile.c_str(), _bangBank)) {
+		Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
+		loadRA1Nut(expldFile.c_str(), _bangBank);
+	}
 }
 
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
@@ -584,7 +612,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		renderMainMenuOverlay(renderBitmap, pitch, width, height);
 	}
 
-	if (!_interactiveVideoActive || _shipBank.numSprites == 0 || !renderBitmap)
+	if (!_interactiveVideoActive || !renderBitmap)
 		return;
 
 	int width = _player->_width;
@@ -593,12 +621,66 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _screenHeight;
 	int pitch = width;
 
-	updateShipPhysics();
-	renderShip(renderBitmap, pitch, width, height);
+	if (_currentLevel == 1) {
+		// Level 2: first-person asteroid dodge — no ship sprite, input averaging physics
+		updateAsteroidPhysics();
+	} else {
+		// Level 1 (and others): third-person ship flight with accumulators
+		if (_shipBank.numSprites > 0) {
+			updateShipPhysics();
+			renderShip(renderBitmap, pitch, width, height);
+		}
+	}
 	renderExplosions(renderBitmap, pitch, width, height);
+	renderCrosshair(renderBitmap, pitch, width, height);
 	renderHUD(renderBitmap, pitch, width, height);
 }
 
+// Draw crosshair/pointer at mouse position into the render buffer.
+// The original DOS game uses the INT 33h hardware mouse cursor (a red crosshair).
+// We replicate it by drawing a red cross into the render buffer each frame.
+void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
+	int cx = _vm->_mouse.x;
+	int cy = _vm->_mouse.y;
+
+	// Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
+	// Palette index 15 = (255,255,255) white, used as outline for visibility.
+	const byte colorInner = 119;
+	const byte colorOutline = 15;
+	const int size = 7;  // arm length
+
+	// Helper lambda to draw a pixel with outline
+	auto drawPx = [&](int x, int y, byte c) {
+		if (x >= 0 && x < width && y >= 0 && y < height)
+			dst[y * pitch + x] = c;
+	};
+
+	// Draw outline first (1px border around each arm pixel)
+	for (int d = -size; d <= size; d++) {
+		if (d >= -1 && d <= 1) continue; // skip center area for outline
+		// Horizontal arm outline
+		drawPx(cx + d, cy - 1, colorOutline);
+		drawPx(cx + d, cy + 1, colorOutline);
+		// Vertical arm outline
+		drawPx(cx - 1, cy + d, colorOutline);
+		drawPx(cx + 1, cy + d, colorOutline);
+	}
+	// Arm endpoints
+	drawPx(cx - size - 1, cy, colorOutline);
+	drawPx(cx + size + 1, cy, colorOutline);
+	drawPx(cx, cy - size - 1, colorOutline);
+	drawPx(cx, cy + size + 1, colorOutline);
+
+	// Draw red cross arms
+	for (int d = -size; d <= size; d++) {
+		if (d == 0) continue; // gap at center
+		drawPx(cx + d, cy, colorInner);  // horizontal
+		drawPx(cx, cy + d, colorInner);  // vertical
+	}
+	// Center dot
+	drawPx(cx, cy, colorInner);
+}
+
 void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
 	if (!dst || !text || _hudFontBank.numSprites <= 0)
 		return;
@@ -1012,6 +1094,101 @@ void InsaneRebel1::updateShipPhysics() {
 		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
+// Level 2+ asteroid/surface physics (FUN_1CDA7).
+// Uses 10-frame input history averaging instead of accumulators.
+// Ship position = averaged input + center offset.
+// Viewport = second history buffer for smooth camera scrolling.
+void InsaneRebel1::updateAsteroidPhysics() {
+	// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
+	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
+		_health++;
+	}
+
+	// Damage application (FUN_1CDA7 lines 20-41)
+	// No cooldown — all three damage types can stack each frame
+	if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+		_screenFlash = 5;
+		if (_damageFlags & 0x80)
+			_health -= _tuning.shot;
+		if (_damageFlags & 0x40)
+			_health -= _tuning.miss;
+		if (_damageFlags & 0x20)
+			_health -= _tuning.wham;
+		if (_health < 0) {
+			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
+		}
+		_damageFlags = 0;
+	}
+
+	// Death fade countdown
+	if (_deathTimer > 1 && _health < 0) {
+		_deathTimer--;
+	}
+
+	// Screen flash countdown
+	if (_screenFlash > 0) {
+		_screenFlash--;
+	}
+
+	// Input history shift and averaging (FUN_1CDA7 lines 107-131)
+	// Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
+	for (int i = kInputHistorySize - 1; i > 0; i--) {
+		_inputHistoryX[i] = _inputHistoryX[i - 1];
+		_inputHistoryY[i] = _inputHistoryY[i - 1];
+	}
+
+	// Read current mouse input (mapped to -160..+160 range)
+	int mouseX = 0, mouseY = 0;
+	if (_vm->_mouse.x >= 0) {
+		mouseX = CLIP<int>(_vm->_mouse.x - kCenterX, -kCenterX, kCenterX);
+		mouseY = CLIP<int>(_vm->_mouse.y - kCenterY, -kCenterY, kCenterY);
+	}
+	_inputHistoryX[0] = (int16)mouseX;
+	_inputHistoryY[0] = (int16)mouseY;
+
+	// Average over 10 frames
+	int16 sumX = 0, sumY = 0;
+	for (int i = 0; i < kInputHistorySize; i++) {
+		sumX += _inputHistoryX[i];
+		sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
+	}
+	_avgInputX = (int16)(sumX / kInputHistorySize);
+	_avgInputY = (int16)(sumY / kInputHistorySize);
+
+	// Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
+	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
+	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
+
+	// Ship position = average + center offset
+	// Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
+	_shipPosX = _avgInputX + 0xA0;
+	_shipPosY = _avgInputY + 0x46;
+
+	// Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
+	for (int i = kInputHistorySize - 1; i > 0; i--) {
+		_viewHistoryX[i] = _viewHistoryX[i - 1];
+		_viewHistoryY[i] = _viewHistoryY[i - 1];
+	}
+	_viewHistoryX[0] = _avgInputX;
+	_viewHistoryY[0] = _avgInputY;
+
+	int16 viewSumX = 0, viewSumY = 0;
+	for (int i = 0; i < kInputHistorySize; i++) {
+		viewSumX += _viewHistoryX[i];
+		viewSumY += _viewHistoryY[i];
+	}
+	// Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
+	// Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
+	_perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
+	_perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
+
+	_frameCounter++;
+
+	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
+		_shipPosX, _shipPosY, _avgInputX, _avgInputY,
+		_perspectiveX, _perspectiveY, _health, _screenFlash);
+}
+
 void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 	// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
 	// Hidden during last 20 frames of death sequence (deathTimer 20→0)
@@ -1097,6 +1274,12 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	if (_displayBank.numSprites == 0)
 		return;
 
+	// Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
+	if (_score / 10000 > _prevScore / 10000) {
+		_lives++;
+	}
+	_prevScore = _score;
+
 	const RA1Sprite &bar = _displayBank.sprites[0];
 
 	// DISPLAY.NUT sprite is 320×19 at xoffs=0, yoffs=176 in the original game.
@@ -1139,13 +1322,14 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		int fillW = CLIP(healthWidth, 0, barMaxW);
 
 		// Color based on damage level (matching original thresholds from FUN_1BBCB)
+		// Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
 		byte barColor;
 		if (_health > _tuning.shot * 2)
-			barColor = 0xA0;  // Green — low damage
+			barColor = 0xD5;  // Green (0,192,0) — low damage
 		else if (_health > _tuning.wham * 2)
-			barColor = 0x2C;  // Yellow — moderate damage
+			barColor = 0x63;  // Yellow (255,255,31) — moderate damage
 		else
-			barColor = 0x30;  // Red — critical
+			barColor = 0xDD;  // Red (192,0,0) — critical
 
 		// Flash effect on damage
 		if (_screenFlash > 0)
@@ -1159,15 +1343,29 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		}
 	}
 
-	// Draw lives and score from DISPLAY.NUT glyphs.
-	if (_hudFontBank.numSprites > 0) {
-		char livesStr[8];
-		Common::sprintf_s(livesStr, "%d", MAX<int>(_lives, 0));
-		drawFontBankString(dst, pitch, width, height, hudX + 180, hudY + 6, livesStr);
+	// Lives: black out excess pilot icons embedded in DISPLAY.NUT background.
+	// Original FUN_1BBCB: FUN_21D66(buf, lives*10+186, 6, 51-lives*10, 9, 0, 320)
+	// Icons are 5 slots at x=186..236, each ~10px wide. Cover unused slots with black.
+	if (_lives >= 0 && _lives < 5) {
+		int coverX = hudX + _lives * 10 + 186;
+		int coverY = hudY + 6;
+		int coverW = 51 - _lives * 10;
+		int coverH = 9;
+		if (coverX >= 0 && coverY >= 0) {
+			for (int iy = 0; iy < coverH && coverY + iy < height; iy++) {
+				byte *d = dst + (coverY + iy) * pitch + coverX;
+				for (int ix = 0; ix < coverW && coverX + ix < width; ix++) {
+					d[ix] = 0x00;
+				}
+			}
+		}
+	}
 
+	// Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
+	if (_hudFontBank.numSprites > 0) {
 		char scoreStr[16];
-		Common::sprintf_s(scoreStr, "%07d", MAX<int>(_score, 0));
-		drawFontBankString(dst, pitch, width, height, hudX + 257, hudY + 4, scoreStr);
+		Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
+		drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
 	}
 
 }
@@ -1230,6 +1428,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_liftSmooth = 0;
 		_posAccumX = 0;
 		_posAccumY = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
 
 		// Field1 == 0 corresponds to baseline recenter behavior in the original.
 		if ((int32)param1 == 0) {
@@ -1301,7 +1505,19 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		}
 		break;
 
-	case 0x08: case 0x09: case 0x0A: case 0x0B:
+	case 0x0B:
+		// Asteroid/surface per-frame handler (FUN_1CDA7).
+		// field1 = frame counter, field2 = max frames
+		_gameCounter = param1;
+		if (subSize >= 20) {
+			b.readUint32BE(); // field2 (max frames)
+			b.readUint32BE(); // field3
+			b.readUint32BE(); // field4
+		}
+		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+		break;
+
+	case 0x08: case 0x09: case 0x0A:
 	case 0x19: case 0x1A:
 		if (subSize >= 20) {
 			uint32 param2 = b.readUint32BE();
@@ -1544,6 +1760,69 @@ bool InsaneRebel1::runLevel1() {
 	return false;
 }
 
+// Level 2: Asteroid Field Training
+// Flow: L2NEW → L2INTRO → L2PLAY (interactive) → L2END/L2DEATH
+bool InsaneRebel1::runLevel2() {
+	debug(1, "InsaneRebel1: Running level 2");
+
+	_currentLevel = 1;
+	loadLevelSprites(2);
+	loadTuningForLevel(1);
+
+	// L2INTRO.ANM — intro cutscene (481 frames)
+	playCinematic("LVL2/L2INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	// Retry loop
+	while (!_vm->shouldQuit()) {
+		// Reset state for this attempt
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		// L2PLAY.ANM — asteroid dodge (800 frames, interactive)
+		playInteractiveVideo("LVL2/L2PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			// Level complete!
+			playCinematic("LVL2/L2END.ANM");
+			return true;
+		}
+
+		// Death
+		playCinematic("LVL2/L2DEATH.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		_lives--;
+		if (_lives <= 0) {
+			debug(1, "InsaneRebel1: Game over (no lives left)");
+			return false;
+		}
+
+		// Retry briefing
+		playCinematic("LVL2/L2NEW.ANM");
+		if (_vm->shouldQuit())
+			return false;
+	}
+
+	return false;
+}
+
 // Main game entry point — called from ScummEngine::go().
 // Matches original flow at 0x15597: intro → menu → level.
 void InsaneRebel1::runGame() {
@@ -1560,16 +1839,24 @@ void InsaneRebel1::runGame() {
 
 		switch (menuResult) {
 		case 1: {
+#if 0 // Skip level 1 for testing — jump straight to level 2
 			// Start New Game — play L1NEW briefing then level 1
 			playCinematic("LVL1/L1NEW.ANM");
 			if (_vm->shouldQuit())
 				return;
 
 			bool completed = runLevel1();
-			if (completed) {
-				debug(1, "InsaneRebel1: Level 1 completed!");
-				// TODO: Continue to level 2
+#else
+			bool completed = true;
+#endif
+			if (completed && !_vm->shouldQuit()) {
+				completed = runLevel2();
+				if (completed) {
+					debug(1, "InsaneRebel1: Level 2 completed!");
+					// TODO: Continue to level 3
+				}
 			}
+			_currentLevel = 0;
 			// Return to menu after level ends
 			break;
 		}
@@ -1602,7 +1889,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename) {
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);
 
-	// Center mouse, hide cursor, and lock mouse to window (like RA2 flight)
+	// Center mouse, hide system cursor (we draw our own), lock mouse to window
 	smush_warpMouse(160, 100, -1);
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 3f884d8b894..61e1b28f968 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -90,6 +90,9 @@ private:
 	// Returns true if level completed, false if player quit
 	bool runLevel1();
 
+	// Level 2 flow: NEW → INTRO → PLAY (asteroid dodge) → END/DEATH
+	bool runLevel2();
+
 	// Play a passive cinematic (no game callback, skippable)
 	// startFrame > 0: fast-forward (decode without display) to that frame
 	void playCinematic(const char *filename, int32 startFrame = 0);
@@ -103,6 +106,7 @@ private:
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
 	void renderExplosions(byte *dst, int pitch, int width, int height);
+	void renderCrosshair(byte *dst, int pitch, int width, int height);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
@@ -154,10 +158,25 @@ private:
 	// Per-frame drift bias from GAME 0x07 field3
 	int16 _driftParam;
 
-	// Perspective view offsets
+	// Perspective view offsets (0x74B6/0x74B8: viewport scroll base)
 	int16 _perspectiveX;
 	int16 _perspectiveY;
 
+	// Input history buffers for 0x0B handler (FUN_1CDA7) — 10-frame averaging
+	static const int kInputHistorySize = 10;
+	int16 _inputHistoryX[kInputHistorySize];  // 0x7580: horizontal input history
+	int16 _inputHistoryY[kInputHistorySize];  // 0x7594: vertical input history
+	int16 _viewHistoryX[kInputHistorySize];   // 0x75A8: viewport horizontal history
+	int16 _viewHistoryY[kInputHistorySize];   // 0x75BC: viewport vertical history
+	int16 _avgInputX;    // smoothed horizontal input (clamped to [-0xA0, 0xA0])
+	int16 _avgInputY;    // smoothed vertical input (clamped to [-0x46, 0x41])
+
+	// 0x0B handler physics update (asteroid/surface levels)
+	void updateAsteroidPhysics();
+
+	// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
+	int _currentLevel;
+
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
 
@@ -170,8 +189,11 @@ private:
 		int16 lift;    // 0x1B1D: vertical speed/sensitivity
 		int16 slide;   // 0x1B1F: cross-axis coupling
 		int16 drift;   // 0x1B21: drift/turbulence multiplier
+		int16 snap;    // 0x1B23: hit radius for shooting targets
+		int16 miss;    // 0x1B25: obstacle collision damage (0x0B bit 0x40)
 		int16 wham;    // 0x1B27: light/wall damage
 		int16 shot;    // 0x1B29: heavy/projectile damage
+		int16 kill;    // 0x1B2B: score per target kill
 	};
 	TuningParams _tuning;
 
@@ -181,6 +203,7 @@ private:
 	int16 _health;               // 0x7560: current health (init=98, negative=dead, max=98)
 	int16 _lives;                // 0x7562: remaining extra lives
 	int _score;                  // 0x7564: current score
+	int _prevScore;              // 0x8288: previous score (for extra life bonus at 10k intervals)
 	byte _damageFlags;           // 0x74D4: per-frame collision bitmask (cleared each frame)
 	byte _prevDamageFlags;       // 0x74D6: previous frame's damage flags (for explosion direction)
 	uint16 _gameLatch5D;         // 0x75D2: GAME 0x5D latch (scene/obstacle/event trigger)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index fbae471f39d..717e6c006c7 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -53,6 +53,7 @@
 #include "audio/decoders/vorbis.h"
 
 #include "common/compression/deflate.h"
+#include "common/memstream.h"
 
 namespace Scumm {
 
@@ -1357,9 +1358,31 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	}
 
 	while (frameSize > 0) {
-		const uint32 subType = b.readUint32BE();
-		const int32 subSize = b.readUint32BE();
-		const int32 subOffset = b.pos();
+		const int32 chunkStart = b.pos();
+		uint32 subType = b.readUint32BE();
+		int32 subSize = b.readUint32BE();
+		int32 subOffset = b.pos();
+
+		// RA1 workaround: some frames have missing padding bytes after
+		// odd-sized sub-chunks (authoring tool bug in 4 frames of L2PLAY.ANM).
+		// If the tag looks invalid, back up 1 byte and retry.
+		if (isRA1() && frameSize > 8) {
+			byte b0 = (subType >> 24) & 0xFF;
+			byte b1 = (subType >> 16) & 0xFF;
+			byte b2 = (subType >> 8) & 0xFF;
+			byte b3 = subType & 0xFF;
+			bool validTag = (b0 >= 0x20 && b0 <= 0x7E) && (b1 >= 0x20 && b1 <= 0x7E) &&
+			                (b2 >= 0x20 && b2 <= 0x7E) && (b3 >= 0x00 && b3 <= 0x7E);
+			if (!validTag) {
+				// Try 1 byte earlier (missing padding byte)
+				b.seek(chunkStart - 1, SEEK_SET);
+				subType = b.readUint32BE();
+				subSize = b.readUint32BE();
+				subOffset = b.pos();
+				frameSize++;
+				debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: RA1 realigned to %s at frame %d", tag2str(subType), _frame);
+			}
+		}
 
 		// RA2: When _skipNext is set, skip the NEXT chunk of ANY type
 		if (isRA2() && _skipNext) {
@@ -1453,6 +1476,77 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		case MKTAG('P','S','D','2'):
 			debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
 			break;
+		case MKTAG('O','B','J','\0'):
+			// RA1 object overlay chunk: variable-size header + embedded FOBJ
+			// sprites (including the cockpit overlay), GAME, and PSAD chunks.
+			// The reported size field is unreliable — remaining FRME data after
+			// OBJ\0 contains unstructured data between the reported end and
+			// subsequent sub-chunks. Read ALL remaining FRME data and scan for
+			// embedded sub-chunks, then stop frame parsing.
+			if (isRA1()) {
+				int32 objDataSize = frameSize - 8;
+				if (objDataSize > 0) {
+					byte *objBuf = (byte *)malloc(objDataSize);
+					b.read(objBuf, objDataSize);
+
+					int32 objPos = 0;
+					while (objPos + 8 < objDataSize) {
+						uint32 embTag = READ_BE_UINT32(objBuf + objPos);
+						uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
+						int32 embRemaining = objDataSize - objPos - 8;
+
+						bool recognized = (embTag == MKTAG('F','O','B','J') ||
+						                   embTag == MKTAG('G','A','M','E') ||
+						                   embTag == MKTAG('P','S','A','D'));
+
+						if (!recognized || embSize > (uint32)embRemaining) {
+							// Not a recognized tag or size exceeds remaining data.
+							// Advance byte-by-byte through the OBJ header.
+							objPos++;
+							continue;
+						}
+
+						if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
+							Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+							handleFrameObject(embSize, embStream);
+
+							// Save the largest OBJ embedded FOBJ as the cockpit overlay
+							// to re-render every subsequent frame.
+							if (_ra1ObjOverlayData == nullptr ||
+							    (int32)embSize > _ra1ObjOverlayDataSize) {
+								free(_ra1ObjOverlayData);
+								_ra1ObjOverlayDataSize = embSize;
+								_ra1ObjOverlayData = (byte *)malloc(embSize);
+								memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
+								// Parse FOBJ header for codec/position/size
+								_ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
+								_ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
+								_ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
+								_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
+								_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
+							}
+						} else if (embTag == MKTAG('G','A','M','E')) {
+							Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+							InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+							rebel1->handleGameChunk(embSize, embStream);
+						} else if (embTag == MKTAG('P','S','A','D')) {
+							if (!_compressedFileMode && _fastForwardToFrame == 0) {
+								uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
+								memcpy(audioBuf, objBuf + objPos, embSize + 8);
+								feedAudio(audioBuf, 0, 127, 0, 0);
+								free(audioBuf);
+							}
+						}
+
+						objPos += 8 + embSize;
+						if (embSize & 1) objPos++;
+					}
+					free(objBuf);
+				}
+				frameSize = 0;
+				continue;
+			}
+			break;
 		default:
 			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 		}
@@ -1465,6 +1559,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		}
 	}
 
+	// RA1: Re-render saved OBJ cockpit overlay every frame (drawn once in
+	// frame 0's OBJ chunk, then scene FOBJs overwrite it each frame).
+	if (isRA1() && _ra1ObjOverlayData != nullptr && _frame > 0) {
+		Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
+		handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
+	}
+
 	if (_insanity) {
 		_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
 	}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index ae942278dd6..5b0eaefc674 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -170,6 +170,16 @@ private:
 	int _storedFobjWidth;
 	int _storedFobjHeight;
 
+	// RA1: OBJ\0 embedded cockpit overlay FOBJ — drawn once in frame 0,
+	// saved here and re-rendered every subsequent frame after scene FOBJs.
+	byte *_ra1ObjOverlayData;
+	int32 _ra1ObjOverlayDataSize;
+	int _ra1ObjOverlayCodec;
+	int _ra1ObjOverlayLeft;
+	int _ra1ObjOverlayTop;
+	int _ra1ObjOverlayWidth;
+	int _ra1ObjOverlayHeight;
+
 	// RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
 	// to re-render the same sprite payload at a different position.
 	byte *_lastFobjData;
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 176dc179b42..4dcf9168838 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -73,6 +73,13 @@ void SmushPlayer::ra2InitFields() {
 	_lastFobjWidth = 0;
 	_lastFobjHeight = 0;
 	_hasFrameFobjForGost = false;
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
+	_ra1ObjOverlayCodec = 0;
+	_ra1ObjOverlayLeft = 0;
+	_ra1ObjOverlayTop = 0;
+	_ra1ObjOverlayWidth = 0;
+	_ra1ObjOverlayHeight = 0;
 	_skipNext = false;
 	_ra2FastForwarding = false;
 	_fobjOffsetX = 0;
@@ -100,6 +107,8 @@ void SmushPlayer::ra2DestroyFields() {
 	_lastFobjData = nullptr;
 	free(_loadBuffer);
 	_loadBuffer = nullptr;
+	free(_ra1ObjOverlayData);
+	_ra1ObjOverlayData = nullptr;
 }
 
 /**
@@ -144,6 +153,9 @@ void SmushPlayer::ra2ReleaseVideo() {
 	free(_lastFobjData);
 	_lastFobjData = nullptr;
 	_lastFobjDataSize = 0;
+	free(_ra1ObjOverlayData);
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
 	_hasFrameFobjForGost = false;
 	// Preserve _frameBuffer across videos so that gameplay videos (which have no
 	// background FOBJ) can use the stored background from the previous BEG video.


Commit: c33805415a93c96aa85c23f3b562eda23ad1965c
    https://github.com/scummvm/scummvm/commit/c33805415a93c96aa85c23f3b562eda23ad1965c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00

Commit Message:
SCUMM: RA2: Capture mouse

Changed paths:
    engines/scumm/insane/insane_rebel_levels.cpp


diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index cdb0d1850b5..d30bc2f8ef4 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -21,6 +21,8 @@
 
 #include "common/system.h"
 
+#include "graphics/cursorman.h"
+
 #include "scumm/scumm_v7.h"
 
 #include "scumm/smush/smush_player.h"
@@ -475,6 +477,14 @@ int InsaneRebel2::runLevel(int levelId) {
 	// Set the current level
 	_selectedLevel = levelId;
 
+	// Lock the mouse to the game window during gameplay.
+	// The original hides the cursor (ShowCursor(0)) and relies on Windows confining
+	// the mouse to the game window. Without locking, the cursor can escape the
+	// ScummVM window making the ship uncontrollable.
+	smush_warpMouse(160, 100, -1);
+	CursorMan.showMouse(false);
+	g_system->lockMouse(true);
+
 	// Initialize common player state
 	_playerLives = 3;
 	_playerShield = 255;
@@ -489,40 +499,63 @@ int InsaneRebel2::runLevel(int levelId) {
 	_skipSectionRequested = false;
 
 	// Dispatch to per-level handler
+	int result;
 	switch (levelId) {
 	case 1:
-		return runLevel1();
+		result = runLevel1();
+		break;
 	case 2:
-		return runLevel2();
+		result = runLevel2();
+		break;
 	case 3:
-		return runLevel3();
+		result = runLevel3();
+		break;
 	case 4:
-		return runLevel4();
+		result = runLevel4();
+		break;
 	case 5:
-		return runLevel5();
+		result = runLevel5();
+		break;
 	case 6:
-		return runLevel6();
+		result = runLevel6();
+		break;
 	case 7:
-		return runLevel7();
+		result = runLevel7();
+		break;
 	case 8:
-		return runLevel8();
+		result = runLevel8();
+		break;
 	case 9:
-		return runLevel9();
+		result = runLevel9();
+		break;
 	case 10:
-		return runLevel10();
+		result = runLevel10();
+		break;
 	case 11:
-		return runLevel11();
+		result = runLevel11();
+		break;
 	case 12:
-		return runLevel12();
+		result = runLevel12();
+		break;
 	case 13:
-		return runLevel13();
+		result = runLevel13();
+		break;
 	case 14:
-		return runLevel14();
+		result = runLevel14();
+		break;
 	case 15:
-		return runLevel15();
+		result = runLevel15();
+		break;
 	default:
-		return runLevel1();
+		result = runLevel1();
+		break;
 	}
+
+	// Unlock the mouse when returning to menu
+	g_system->lockMouse(false);
+	CursorMan.showMouse(true);
+
+	return result;
 }
 
 // ---------------------------------------------------------------------------


Commit: 2d99feb58f2e0944f7a0379649764cb3798f33e1
    https://github.com/scummvm/scummvm/commit/2d99feb58f2e0944f7a0379649764cb3798f33e1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00

Commit Message:
SCUMM: RA2: Add level execution helpers

Changed paths:
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_levels.cpp
    engines/scumm/insane/insane_rebel_runlevels.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index 8bcb0e23170..d07ad57d577 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -367,7 +367,10 @@ public:
 	// Play game over video (LEVXX/XXOVER.SAN)
 	void playLevelGameOver(int levelId);
 
-	// Play the ending/credits sequence
+	// Play the full ending sequence: finale + credits + epilogue (FUN_0041bbe8)
+	void playEndingSequence();
+
+	// Play main menu credits video (OPEN/O_CREDIT.SAN)
 	void playCreditsSequence();
 
 	// Get the directory name for a level (e.g., "LEV01" for level 1)
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel_levels.cpp
index d30bc2f8ef4..1b61f13b70a 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel_levels.cpp
@@ -444,10 +444,57 @@ void InsaneRebel2::playLevelGameOver(int levelId) {
 	splayer->play(filename.c_str(), 12);
 }
 
-// playCreditsSequence -- End credits (OPEN/O_CREDIT.SAN).
+// playEndingSequence -- Finale + credits + epilogue (FUN_0041bbe8).
+//
+// Original flow:
+//   1. Play difficulty-dependent finale video:
+//      - Difficulty 2: FINAL/F_FIN_B.SAN
+//      - Difficulty 3: FINAL/F_FIN_C.SAN
+//      - Default:      FINAL/F_FIN_A.SAN
+//   2. Play credits: FINAL/F_CREDIT.SAN
+//   3. Play epilogue: FINAL/F_EPILOG.SAN
+//   4. Return to main menu
+//
+void InsaneRebel2::playEndingSequence() {
+
+	debug("Rebel2: Playing ending sequence (difficulty=%d)", _difficulty);
+
+	// Switch to gameplay state to stop menu overlay rendering
+	_gameState = kStateGameplay;
+	_menuInputActive = false;
+
+	// Clear the screen to remove any leftover menu pixels
+	VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen];
+	memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h);
+	_vm->markRectAsDirty(kMainVirtScreen, 0, vs->w, 0, vs->h);
+
+	// Difficulty-dependent finale video
+	if (_difficulty == 2) {
+		playCinematic("FINAL/F_FIN_B.SAN");
+	} else if (_difficulty == 3) {
+		playCinematic("FINAL/F_FIN_C.SAN");
+	} else {
+		playCinematic("FINAL/F_FIN_A.SAN");
+	}
+
+	if (_vm->shouldQuit())
+		return;
+
+	// Credits
+	playCinematic("FINAL/F_CREDIT.SAN");
+
+	if (_vm->shouldQuit())
+		return;
+
+	// Epilogue
+	playCinematic("FINAL/F_EPILOG.SAN");
+}
+
+// playCreditsSequence -- Main menu credits (OPEN/O_CREDIT.SAN).
+// This is the credits accessible from the main menu, NOT the ending credits.
 void InsaneRebel2::playCreditsSequence() {
 
-	debug("Rebel2: Playing credits");
+	debug("Rebel2: Playing menu credits");
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	splayer->setCurVideoFlags(0x20);
@@ -477,6 +524,16 @@ int InsaneRebel2::runLevel(int levelId) {
 	// Set the current level
 	_selectedLevel = levelId;
 
+	// Set the level type for difficulty table lookup (DAT_0047a7f8).
+	// Each original level function sets this before gameplay starts.
+	// Levels 1-6 use types 0-5, but Level 6 also uses type 6 mid-level.
+	// Levels 7-15 use types 7-15 (gap at type 6 which is Level 6 phase 2).
+	// Level 15 also switches to type 16 mid-level at frame 0x21e.
+	static const int kLevelTypeMap[16] = {
+		-1, 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15
+	};
+	_rebelLevelType = kLevelTypeMap[levelId];
+
 	// Lock the mouse to the game window during gameplay.
 	// The original hides the cursor (ShowCursor(0)) and relies on Windows confining
 	// the mouse to the game window. Without locking, the cursor can escape the
@@ -579,7 +636,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 	switch (levelId) {
 	case 1:
 		// Level 1: Random between A and B
-		return (getRandomVariant(2) == 0) ? "A" : "B";
+		// Original: random!=0 → A (offset 0), random==0 → B (offset 0x14)
+		return (getRandomVariant(2) == 0) ? "B" : "A";
 
 	case 2:
 		// Level 2: Just "DIE" (no variants)
@@ -614,12 +672,13 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 		}
 
 	case 4:
-		// Level 4: Just "DIE" (no variants)
-		return "";
+		// Level 4: Single variant "A" (original plays 04DIE_A.SAN)
+		return "A";
 
 	case 5:
-		// Level 5: Random between A and B (like Level 1)
-		return (getRandomVariant(2) == 0) ? "A" : "B";
+		// Level 5: Random between A and B
+		// Original: random!=0 → A (offset 0), random==0 → B (offset 0x14)
+		return (getRandomVariant(2) == 0) ? "B" : "A";
 
 	case 6:
 		// Level 6 (FUN_004190D6): Phase-based with detailed frame selection
@@ -679,7 +738,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 
 	case 8:
 		// Level 8 (FUN_00419976): Random A or B
-		return (getRandomVariant(2) == 0) ? "A" : "B";
+		// Original: random!=0 → A (offset 0), random==0 → B (offset 0x14)
+		return (getRandomVariant(2) == 0) ? "B" : "A";
 
 	case 9:
 		// Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel_runlevels.cpp
index f1365a0024a..3071ee93dac 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel_runlevels.cpp
@@ -2102,6 +2102,9 @@ int InsaneRebel2::runLevel15() {
 	// FUN_00401000 + FUN_0041c7d0 + FUN_0040c040
 	clearBit(0);
 
+	// Original sets DAT_0047a7f8 = 0xf before the gameplay loop
+	_rebelLevelType = 0xf;
+
 	while (!_vm->shouldQuit()) {
 		_playerShield = 255;
 		_playerDamage = 0;
@@ -2109,11 +2112,10 @@ int InsaneRebel2::runLevel15() {
 
 		clearBit(0);
 
-		// Original: DAT_0047a7f8 = 0xf (level 15) before gameplay
-		// At frame 0x21e (542): DAT_0047a7f8 = 0x10 (switches to "level 16" internally)
-		// After gameplay: DAT_0047a7f8 = 0x10 (stays at 16)
-		// This level ID switch affects which difficulty data is used mid-level.
-		// The IACT callbacks handle gameplay regardless of this ID.
+		// Original: DAT_0047a7f8 = 0xf again at start of each retry
+		// At frame 0x21e (542): switches to 0x10 (affects difficulty lookup mid-level)
+		// The frame-based switch is handled by IACT opcode 6 in the video data.
+		_rebelLevelType = 0xf;
 
 		// Play gameplay (15PLAY.SAN)
 		// Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index e609f134749..f0e8b348048 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2768,6 +2768,11 @@ Common::Error ScummEngine::go() {
 					int selectedLevel = rebel->_selectedChapter + 1;
 					debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
 
+					// Ending selected directly from chapter select (FUN_0041bbe8, case 0xf)
+					if (selectedLevel == 16) {
+						rebel->playEndingSequence();
+					}
+
 					// Level progression loop: on success, advance to next level
 					// Original game chains levels directly (e.g. FUN_0040598c(FUN_00418063,0))
 					while (!shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
@@ -2778,8 +2783,8 @@ Common::Error ScummEngine::go() {
 								rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
 							selectedLevel++;
 							if (selectedLevel > 15) {
-								// Beat the game — play credits
-								rebel->playCreditsSequence();
+								// Beat the game — play ending sequence (FUN_0041bbe8)
+								rebel->playEndingSequence();
 								break;
 							}
 						} else {


Commit: 1969645fbbb4df02611117b66157c9f75fe6d6a3
    https://github.com/scummvm/scummvm/commit/1969645fbbb4df02611117b66157c9f75fe6d6a3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:28+02:00

Commit Message:
SCUMM: RA2: Add credits

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player_ra2.cpp
    engines/scumm/string_v7.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 717e6c006c7..491d1751804 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -71,13 +71,15 @@ private:
 	int _nbStrings;
 	int _lastId;
 	const char *_lastString;
+	bool _preserveNewlines;
 
 public:
 
 	StringResource() :
 		_nbStrings(0),
 		_lastId(-1),
-		_lastString(nullptr) {
+		_lastString(nullptr),
+		_preserveNewlines(false) {
 		for (int i = 0; i < MAX_STRINGS; i++) {
 			_strings[i].id = 0;
 			_strings[i].string = nullptr;
@@ -89,6 +91,8 @@ public:
 		}
 	}
 
+	void setPreserveNewlines(bool preserve) { _preserveNewlines = preserve; }
+
 	bool init(char *buffer, int32 length) {
 		char *def_start = strchr(buffer, '#');
 		while (def_start != nullptr) {
@@ -168,11 +172,20 @@ public:
 				line_start = line_end+1;
 				if (line_start[0] == '/' && line_start[1] == '/') {
 					line_start += 2;
-					if	(line_end[-1] == '\r')
-						line_end[-1] = ' ';
-					else
-						*line_end++ = ' ';
-					memmove(line_end, line_start, strlen(line_start)+1);
+					// RA2: preserve newlines for multi-line TRES text
+					// (credits, cast lists). Other games join with spaces.
+					if (_preserveNewlines) {
+						if (line_end[-1] == '\r')
+							line_end[-1] = '\n';
+						// else line_end already points to '\n'
+						memmove(line_end + 1, line_start, strlen(line_start)+1);
+					} else {
+						if	(line_end[-1] == '\r')
+							line_end[-1] = ' ';
+						else
+							*line_end++ = ' ';
+						memmove(line_end, line_start, strlen(line_start)+1);
+					}
 				}
 			}
 			_strings[_nbStrings].id = id;
@@ -231,6 +244,7 @@ static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_enc
 	}
 	StringResource *sr = new StringResource;
 	assert(sr);
+	sr->setPreserveNewlines(vm->_game.id == GID_REBEL2);
 	sr->init(filebuffer, length);
 	delete[] filebuffer;
 	return sr;
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 4dcf9168838..c93a67fedb3 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -498,11 +498,17 @@ void SmushPlayer::ra2ResetDeltaPalette() {
  * RA2 font path table.
  */
 SmushFont *SmushPlayer::ra2GetFont(int font) {
+	// Font table for low-res mode (320x200). Original exe uses pointer
+	// arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
+	// Font 0: TALKFONT (TKHIFONT hi-res)
+	// Font 1: SMALFONT (SMHIFONT hi-res)
+	// Font 2: TITLFONT (TIHIFONT hi-res)
+	// Font 3: POVFONT  (POHIFONT hi-res)
 	const char *ra2_fonts[] = {
 		"SYSTM/TALKFONT.NUT",
-		"SYSTM/DIHIFONT.NUT",
+		"SYSTM/SMALFONT.NUT",
 		"SYSTM/TITLFONT.NUT",
-		"SYSTM/SMALFONT.NUT"
+		"SYSTM/POVFONT.NUT"
 	};
 	int numFonts = ARRAYSIZE(ra2_fonts);
 	if (font >= 0 && font < numFonts) {
diff --git a/engines/scumm/string_v7.cpp b/engines/scumm/string_v7.cpp
index 9088be0728d..a9fe0c80ab9 100644
--- a/engines/scumm/string_v7.cpp
+++ b/engines/scumm/string_v7.cpp
@@ -129,7 +129,10 @@ int TextRenderer_v7::getStringHeight(const char *str, uint numBytesMax) {
 		}
 
 		if (*str == '\n') {
-			totalHeight += (lineHeight ? lineHeight : _gr->getFontHeight()) + 1;
+			int lh = lineHeight ? lineHeight : _gr->getFontHeight();
+			// RA2: add extra inter-line spacing for larger fonts (credits text)
+			int gap = (_gameId == GID_REBEL2 && lh > 8) ? lh / 2 : 1;
+			totalHeight += lh + gap;
 			lineHeight = 0;
 		} else if (*str != '\r' && *str != _lineBreakMarker) {
 			lineHeight = MAX<int>(lineHeight, _gr->getCharHeight(*str));


Commit: 6e1e672ffdbe0856ba1d0260a4be0b3afc71fcbb
    https://github.com/scummvm/scummvm/commit/6e1e672ffdbe0856ba1d0260a4be0b3afc71fcbb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:29+02:00

Commit Message:
SCUMM: RA2: Improve pause menu layout

Changed paths:
    engines/scumm/insane/insane_rebel.cpp
    engines/scumm/insane/insane_rebel.h
    engines/scumm/insane/insane_rebel_menu.cpp
    engines/scumm/smush/smush_multi_font.cpp
    engines/scumm/smush/smush_multi_font.h
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel.cpp
index 42284a4254d..4496daf950e 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel.cpp
@@ -27,6 +27,7 @@
 #include "common/events.h"
 #include "common/savefile.h"
 #include "common/util.h"
+#include "graphics/paletteman.h"
 
 #include "audio/mixer.h"
 
@@ -116,6 +117,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	// SmushFont for menu text rendering - uses SMALFONT with proper drawString support
 	_menuFont = new SmushFont(_vm, "SYSTM/SMALFONT.NUT", true);
+	_pauseOverlayActive = false;
+	memset(_savedPausePalette, 0, sizeof(_savedPausePalette));
 
 	// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
 	// This is loaded into DAT_0047aba8 and used as a background overlay via FUN_004236e0
@@ -541,10 +544,30 @@ InsaneRebel2::~InsaneRebel2() {
 
 // notifyEvent -- EventObserver callback for global input dispatch.
 // Handles ESC (skip) and SPACE (pause) regardless of menu state.
+// Pause behavior matches original FUN_405A21: SPACE pauses, ANY key unpauses.
 bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 	if (event.type == Common::EVENT_KEYDOWN) {
 		SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
+		// When paused during gameplay, ANY key unpauses (FUN_405A21 line 360-365).
+		// ESC additionally opens the ScummVM menu (original: quit key exits level).
+		if (splayer && splayer->_paused && _gameState == kStateGameplay) {
+			debug("Rebel2: Key pressed while paused - unpausing");
+			// Restore the original palette saved by showPauseOverlay
+			if (_pauseOverlayActive) {
+				_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
+				_pauseOverlayActive = false;
+			}
+			splayer->unpause();
+			if (event.kbd.keycode == Common::KEYCODE_ESCAPE && _rebelHandler != 0) {
+				debug("Rebel2: ESC during pause - opening ScummVM menu");
+				splayer->pause();
+				_vm->openMainMenuDialog();
+				splayer->unpause();
+			}
+			return true;
+		}
+
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_ESCAPE:
 			// ESC handling depends on game state:
@@ -564,8 +587,6 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 					_vm->_smushVideoShouldFinish = true;
 				} else if (_gameState == kStateGameplay && _rebelHandler != 0) {
 					// During active gameplay (handler != 0): pause and open ScummVM menu.
-					// _rebelHandler is non-zero (7, 8, 0x19, 0x26) only during interactive
-					// gameplay sections, and 0 during intro/cutscene/post videos within a level.
 					debug("Rebel2: ESC pressed during gameplay - opening ScummVM menu");
 					bool wasPaused = splayer->_paused;
 					if (!wasPaused)
@@ -583,19 +604,13 @@ bool InsaneRebel2::notifyEvent(const Common::Event &event) {
 			break;
 
 		case Common::KEYCODE_SPACE:
-			// SPACE toggles pause (emulates FUN_405A21 pause handling)
-			// Only allow pausing during gameplay, not in menus
-			if (splayer && _gameState == kStateGameplay) {
-				if (splayer->_paused) {
-					debug("Rebel2: SPACE pressed - unpausing");
-					splayer->unpause();
-				} else {
-					debug("Rebel2: SPACE pressed - pausing");
-					splayer->pause();
-					// Show the pause overlay with dimming effect and "PAUSED" text
-					showPauseOverlay();
-				}
-				return true;  // Consume the event
+			// SPACE pauses during gameplay (FUN_405A21).
+			// Unpausing is handled above (any key while paused).
+			if (splayer && _gameState == kStateGameplay && !splayer->_paused) {
+				debug("Rebel2: SPACE pressed - pausing");
+				splayer->pause();
+				showPauseOverlay();
+				return true;
 			}
 			break;
 
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel.h
index d07ad57d577..88d20f74101 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel.h
@@ -471,6 +471,10 @@ public:
 	// SmushFont for menu text rendering (uses SMALFONT.NUT with proper string drawing)
 	SmushFont *_menuFont;
 
+	// Saved palette for pause overlay restoration (FUN_405A21)
+	byte _savedPausePalette[768];
+	bool _pauseOverlayActive;
+
 	// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
 	// Loaded into DAT_0047aba8 and rendered via FUN_004236e0 as background
 	NutRenderer *_smush_mouseoverNut;
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel_menu.cpp
index 82561d88b33..9bbd6115991 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel_menu.cpp
@@ -22,6 +22,7 @@
 #include "common/system.h"
 #include "common/events.h"
 #include "common/util.h"
+#include "graphics/paletteman.h"
 
 #include "audio/mixer.h"
 
@@ -32,6 +33,7 @@
 
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
 
 #include "scumm/insane/insane_rebel.h"
 
@@ -559,169 +561,120 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 // Pause Overlay
 // ---------------------------------------------------------------------------
 
-// showPauseOverlay -- Dimmed overlay with "PAUSED" text (FUN_405A21).
+// pauseFillRect -- Helper to fill a rectangle in the frame buffer with bounds checking.
+static void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
+	if (x < 0) { w += x; x = 0; }
+	if (y < 0) { h += y; y = 0; }
+	if (x + w > bufW) w = bufW - x;
+	if (y + h > bufH) h = bufH - y;
+	if (w <= 0 || h <= 0) return;
+	for (int row = y; row < y + h; row++)
+		memset(buf + row * bufW + x, color, w);
+}
+
+// showPauseOverlay -- Dimmed overlay with metallic frame and "PAUSED" text.
+// Reproduces FUN_405A21 from the original executable.
+//
+// The original dims pixels using a green-channel formula that produces palette
+// indices 16-79, relying on a built-in grayscale ramp at those positions.
+// We instead dim the system palette (25% brightness) which achieves the same
+// visual effect regardless of palette layout. The pixel buffer is only modified
+// for the frame decorations and text — the game frame pixels stay unchanged.
+// The original palette is saved in _savedPausePalette and restored on unpause.
 void InsaneRebel2::showPauseOverlay() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	if (!splayer) {
-		debug("showPauseOverlay: No SmushPlayer active");
+	if (!splayer)
 		return;
-	}
 
-	// Get frame buffer and palette from SmushPlayer
-	// _dst points to the virtual screen pixels (the actual rendering destination)
-	// _frameBuffer is only used for store/fetch operations, not general rendering
 	byte *frameBuffer = splayer->_dst;
 	byte *palette = splayer->_pal;
 	int width = splayer->_width;
 	int height = splayer->_height;
 
-	if (!frameBuffer || !palette || width <= 0 || height <= 0) {
-		debug("showPauseOverlay: No frame buffer (%p), palette (%p), or invalid dimensions (%dx%d)",
-		      (void*)frameBuffer, (void*)palette, width, height);
+	if (!frameBuffer || !palette || width <= 0 || height <= 0)
 		return;
-	}
-
-	debug("showPauseOverlay: Applying dimming effect to %dx%d buffer", width, height);
-
-	// Apply dimming effect (emulates FUN_405A21 lines 242-251)
-	// Original algorithm:
-	//   For each pixel, take the green component of its palette entry
-	//   and the green component of the previous pixel's palette entry,
-	//   add them, divide by 8, add 16.
-	// This creates a dark dimmed effect.
-	int bufferSize = width * height;
-	byte prevPixel = 0;
-
-	for (int i = 0; i < bufferSize; i++) {
-		byte curPixel = frameBuffer[i];
-
-		// Get green components from palette (offset +1 in RGB triplets)
-		int greenCur = palette[curPixel * 3 + 1];
-		int greenPrev = palette[prevPixel * 3 + 1];
-
-		// Apply dimming formula: (green1 + green2) >> 3 + 0x10
-		byte dimmedValue = ((greenCur + greenPrev) >> 3) + 0x10;
-
-		frameBuffer[i] = dimmedValue;
-		prevPixel = curPixel;
-	}
 
-	// Draw border decorations (simplified version of FUN_405A21 lines 261-283)
-	// Draw horizontal lines at top and bottom of a centered box
-	int boxLeft = 12;
-	int boxRight = width - 12;
-	int boxTop = 23;   // 0x17
-	int boxBottom = height - 23;  // ~175 for 200 height
-
-	byte borderColor = 0x50;  // Gray border color
-
-	// Top and bottom borders
-	for (int x = boxLeft; x < boxRight; x++) {
-		if (boxTop >= 0 && boxTop < height)
-			frameBuffer[boxTop * width + x] = borderColor;
-		if (boxBottom >= 0 && boxBottom < height)
-			frameBuffer[boxBottom * width + x] = borderColor;
+	int screenW = MIN(width, (int)_vm->_screenWidth);
+	int screenH = MIN(height, (int)_vm->_screenHeight);
+
+	// Step 1: Save current palette and create a dimmed version.
+	memcpy(_savedPausePalette, palette, 768);
+	_pauseOverlayActive = true;
+
+	byte dimPal[768];
+	memcpy(dimPal, palette, 768);
+	for (int i = 0; i < 768; i++)
+		dimPal[i] >>= 2;  // 25% brightness
+
+	// Override specific palette entries for UI elements.
+	// Frame bars (0x50): medium gray
+	dimPal[0x50 * 3 + 0] = 80; dimPal[0x50 * 3 + 1] = 80; dimPal[0x50 * 3 + 2] = 80;
+	// Rivet outline (0x51): slightly lighter gray
+	dimPal[0x51 * 3 + 0] = 110; dimPal[0x51 * 3 + 1] = 110; dimPal[0x51 * 3 + 2] = 110;
+	// RA2 NUT fonts use hardcoded palette indices 1-4 for rendering:
+	//   1=body color (remapped to col param), 2=highlight, 3=anti-alias, 4=outline
+	// Index 5 is used by TRS ^c005 escape code as the text body color.
+	dimPal[1 * 3 + 0] = 255; dimPal[1 * 3 + 1] = 255; dimPal[1 * 3 + 2] = 255;
+	dimPal[2 * 3 + 0] = 188; dimPal[2 * 3 + 1] = 188; dimPal[2 * 3 + 2] = 188;
+	dimPal[3 * 3 + 0] = 128; dimPal[3 * 3 + 1] = 128; dimPal[3 * 3 + 2] = 128;
+	dimPal[4 * 3 + 0] = 0;   dimPal[4 * 3 + 1] = 0;   dimPal[4 * 3 + 2] = 0;
+	dimPal[5 * 3 + 0] = 252; dimPal[5 * 3 + 1] = 252; dimPal[5 * 3 + 2] = 252;
+
+	_vm->_system->getPaletteManager()->setPalette(dimPal, 0, 256);
+
+	// Step 2: Draw the metallic frame (FUN_405A21 lines 261-283).
+	// Horizontal border lines: 2px thick at y=23 and y=175
+	pauseFillRect(frameBuffer, width, height, 0, 0x17, 0x140, 2, 0x50);
+	pauseFillRect(frameBuffer, width, height, 0, 0xAF, 0x140, 2, 0x50);
+
+	// Thick side bars: 40px wide on left and right
+	pauseFillRect(frameBuffer, width, height, 0,     0, 0x28, 200, 0x50);
+	pauseFillRect(frameBuffer, width, height, 0x118, 0, 0x28, 200, 0x50);
+
+	// Rivet decorations along left side bar.
+	// Layered rectangles: outer ring (0x51), inner fill (4).
+	for (int i = 0; i < 6; i++) {
+		int yOff = i * 0x24;  // i * 36
+		pauseFillRect(frameBuffer, width, height, 0x0C, yOff,     0x19, 0x11, 0x51);
+		pauseFillRect(frameBuffer, width, height, 0x0B, yOff + 1, 0x1B, 0x0F, 0x51);
+		pauseFillRect(frameBuffer, width, height, 0x0D, yOff,     0x17, 0x11, 4);
+		pauseFillRect(frameBuffer, width, height, 0x0B, yOff + 2, 0x1B, 0x0D, 4);
+		pauseFillRect(frameBuffer, width, height, 0x0C, yOff + 1, 0x19, 0x0F, 4);
 	}
 
-	// Left and right borders
-	for (int y = boxTop; y < boxBottom; y++) {
-		if (boxLeft >= 0 && boxLeft < width)
-			frameBuffer[y * width + boxLeft] = borderColor;
-		if (boxRight >= 0 && boxRight < width)
-			frameBuffer[y * width + boxRight] = borderColor;
+	// Right side bar rivets: same pattern at x=282 (0x11A).
+	for (int i = 0; i < 6; i++) {
+		int yOff = i * 0x24;
+		int xBase = 0x11A;
+		pauseFillRect(frameBuffer, width, height, xBase,     yOff,     0x19, 0x11, 0x51);
+		pauseFillRect(frameBuffer, width, height, xBase - 1, yOff + 1, 0x1B, 0x0F, 0x51);
+		pauseFillRect(frameBuffer, width, height, xBase + 1, yOff,     0x17, 0x11, 4);
+		pauseFillRect(frameBuffer, width, height, xBase - 1, yOff + 2, 0x1B, 0x0D, 4);
+		pauseFillRect(frameBuffer, width, height, xBase,     yOff + 1, 0x19, 0x0F, 4);
 	}
 
-	// Draw corner decorations (simplified)
-	byte cornerColor = 0x51;  // Slightly brighter for corners
-	for (int i = 0; i < 5; i++) {
-		// Top-left corner
-		if (boxTop + i < height && boxLeft + 5 < width)
-			frameBuffer[(boxTop + i) * width + boxLeft + 5] = cornerColor;
-		if (boxTop + 5 < height && boxLeft + i < width)
-			frameBuffer[(boxTop + 5) * width + boxLeft + i] = cornerColor;
-
-		// Top-right corner
-		if (boxTop + i < height && boxRight - 5 >= 0)
-			frameBuffer[(boxTop + i) * width + boxRight - 5] = cornerColor;
-		if (boxTop + 5 < height && boxRight - i >= 0)
-			frameBuffer[(boxTop + 5) * width + boxRight - i] = cornerColor;
-
-		// Bottom-left corner
-		if (boxBottom - i >= 0 && boxLeft + 5 < width)
-			frameBuffer[(boxBottom - i) * width + boxLeft + 5] = cornerColor;
-		if (boxBottom - 5 >= 0 && boxLeft + i < width)
-			frameBuffer[(boxBottom - 5) * width + boxLeft + i] = cornerColor;
-
-		// Bottom-right corner
-		if (boxBottom - i >= 0 && boxRight - 5 >= 0)
-			frameBuffer[(boxBottom - i) * width + boxRight - 5] = cornerColor;
-		if (boxBottom - 5 >= 0 && boxRight - i >= 0)
-			frameBuffer[(boxBottom - 5) * width + boxRight - i] = cornerColor;
+	// Step 3: Draw "Game Paused" text using TRS string 0x78 (120).
+	// Original: FUN_00434cb0(-1, buf, NULL, x=10, y=10, align=1, fg=4, bg=0x10, text)
+	// fg=4 is the foreground color (used by ^cNNN in the TRS string itself).
+	const char *pauseText = splayer->getString(0x78);
+	if (!pauseText || !pauseText[0])
+		pauseText = "Game Paused";
+
+	SmushMultiFont *multiFont = splayer->getMultiFont();
+	if (!multiFont) {
+		splayer->ensureMultiFont();
+		multiFont = splayer->getMultiFont();
 	}
-
-	// Draw "PAUSED" text centered
-	// Try to load from TRS - the exact index may vary by language version
-	// TRS index 80 (0x50) is likely "PAUSED" or equivalent (from DAT_004573f8)
-	// Note: splayer is already defined at the start of this function
-	const char *pauseText = splayer ? splayer->getString(80) : nullptr;
-	if (!pauseText || !pauseText[0]) {
-		// Fallback only if TRS string not available
-		pauseText = "PAUSED";
-	}
-
-	// Draw text using SmushFont if available
-	if (_menuFont) {
-		Common::Rect clipRect(0, 0, width, height);
-
-		// Calculate centered position
-		// Text should be centered horizontally and vertically in the box
-		int textX = width / 2;  // SmushFont handles centering with kStyleAlignCenter
-		int textY = height / 2 - 4;  // Slightly above center
-
-		// Draw with color 4 and background 0x10 (matching original parameters)
-		// FUN_00434cb0 params: x=10, y=10 or 20, color=4, bg=0x10
-		_menuFont->drawString(pauseText, frameBuffer, clipRect, textX, textY, 0x10, kStyleAlignCenter);
-	} else if (_smush_smalfontNut) {
-		// Fallback: draw using NutRenderer directly
-		NutRenderer *font = _smush_smalfontNut;
-		int numFontChars = font->getNumChars();
-		Common::Rect clipRect(0, 0, width, height);
-
-		// Calculate text width
-		int textWidth = 0;
-		const char *p = pauseText;
-		while (*p) {
-			byte c = (byte)*p++;
-			if (c >= 'a' && c <= 'z')
-				c = c - 'a' + 'A';
-			if (c < numFontChars) {
-				textWidth += font->getCharWidth(c);
-			}
-		}
-
-		// Draw centered
-		int textX = (width - textWidth) / 2;
-		int textY = height / 2 - 4;
-
-		p = pauseText;
-		while (*p) {
-			byte c = (byte)*p++;
-			if (c >= 'a' && c <= 'z')
-				c = c - 'a' + 'A';
-			if (c < numFontChars && textX >= 0 && textY >= 0) {
-				font->drawCharV7(frameBuffer, clipRect, textX, textY, width, -1,
-				                 kStyleAlignLeft, c, true, true);
-				textX += font->getCharWidth(c);
-			}
-		}
+	if (multiFont) {
+		Common::Rect clipRect(0, 0, screenW, screenH);
+		// Original uses x=10, y=10 (single-buffer mode) or y=20 (double-buffer mode).
+		// We use single-buffer mode (y=10).
+		multiFont->drawString(pauseText, frameBuffer, clipRect, 10, 10, width, 4, kStyleAlignLeft);
 	}
 
-	// Update the screen to show the pause overlay
-	// SmushPlayer uses copyRectToScreen to transfer the buffer to the display backend
-	_vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, width, height);
+	// Step 4: Push to screen.
+	_vm->_system->copyRectToScreen(frameBuffer, width, 0, 0, screenW, screenH);
 	_vm->_system->updateScreen();
-
-	debug("showPauseOverlay: Overlay displayed");
 }
 
 // runMainMenu -- Main menu loop (FUN_004147B2).
diff --git a/engines/scumm/smush/smush_multi_font.cpp b/engines/scumm/smush/smush_multi_font.cpp
index 7f6bbd603a7..e68871d6f33 100644
--- a/engines/scumm/smush/smush_multi_font.cpp
+++ b/engines/scumm/smush/smush_multi_font.cpp
@@ -53,6 +53,11 @@ void SmushMultiFont::drawString(const char *str, byte *buffer, Common::Rect &cli
 	_textRenderer->drawString(str, buffer, clipRect, x, y, _vm->_screenWidth, col, flags);
 }
 
+void SmushMultiFont::drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags) {
+	_currentFont = _defaultFont;
+	_textRenderer->drawString(str, buffer, clipRect, x, y, pitch, col, flags);
+}
+
 void SmushMultiFont::drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags) {
 	// Reset to default font before drawing
 	_currentFont = _defaultFont;
diff --git a/engines/scumm/smush/smush_multi_font.h b/engines/scumm/smush/smush_multi_font.h
index 1dbb4c6e2e1..6c57acbe57f 100644
--- a/engines/scumm/smush/smush_multi_font.h
+++ b/engines/scumm/smush/smush_multi_font.h
@@ -52,6 +52,7 @@ public:
 
 	// String drawing methods
 	void drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
+	void drawString(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int pitch, int16 col, TextStyleFlags flags);
 	void drawStringWrap(const char *str, byte *buffer, Common::Rect &clipRect, int x, int y, int16 col, TextStyleFlags flags);
 
 	// GlyphRenderer_v7 interface
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 5b0eaefc674..1d63e11295e 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -266,6 +266,8 @@ public:
 	bool isAudioCallbackEnabled();
 	byte *getVideoPalette();
 	void setCurVideoFlags(int16 flags);
+	SmushMultiFont *getMultiFont() const { return _multiFont; }
+	void ensureMultiFont();
 	void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
 
 	// Masked regions - areas where video should not update (e.g., destroyed enemies)
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index c93a67fedb3..07f4a0500dc 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -273,16 +273,19 @@ void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
 	}
 }
 
+void SmushPlayer::ensureMultiFont() {
+	if (!_multiFont) {
+		_multiFont = new SmushMultiFont(_vm, this, true);
+	}
+}
+
 /**
  * RA2-specific text rendering using SmushMultiFont for inline font switching.
  */
 void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
 										int pos_x, int pos_y, int left, int top,
 										int width, int height, TextStyleFlags flg) {
-	// Create multi-font renderer on first use
-	if (!_multiFont) {
-		_multiFont = new SmushMultiFont(_vm, this, true);
-	}
+	ensureMultiFont();
 	_multiFont->setDefaultFont(fontId);
 
 	debug("SmushPlayer::handleTextResource: RA2 TRES frame=%d fontId=%d color=%d flags=0x%x flg=%d pos=(%d,%d) clip=(%d,%d,%d,%d) str=\"%.40s\"",


Commit: 86c9b2d979e1897592938c6d0690a766aca373df
    https://github.com/scummvm/scummvm/commit/86c9b2d979e1897592938c6d0690a766aca373df
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:29+02:00

Commit Message:
SCUMM: RA: Split Rebel Assault engine files

Changed paths:
  A engines/scumm/insane/insane_rebel1_audio.cpp
  A engines/scumm/insane/insane_rebel1_iact.cpp
  A engines/scumm/insane/insane_rebel1_levels.cpp
  A engines/scumm/insane/insane_rebel1_menu.cpp
  A engines/scumm/insane/insane_rebel1_render.cpp
  A engines/scumm/insane/insane_rebel1_runlevels.cpp
  A engines/scumm/insane/insane_rebel2.cpp
  A engines/scumm/insane/insane_rebel2.h
  A engines/scumm/insane/insane_rebel2_audio.cpp
  A engines/scumm/insane/insane_rebel2_iact.cpp
  A engines/scumm/insane/insane_rebel2_levels.cpp
  A engines/scumm/insane/insane_rebel2_menu.cpp
  A engines/scumm/insane/insane_rebel2_render.cpp
  A engines/scumm/insane/insane_rebel2_runlevels.cpp
  R engines/scumm/insane/insane_rebel.cpp
  R engines/scumm/insane/insane_rebel.h
  R engines/scumm/insane/insane_rebel_audio.cpp
  R engines/scumm/insane/insane_rebel_iact.cpp
  R engines/scumm/insane/insane_rebel_levels.cpp
  R engines/scumm/insane/insane_rebel_menu.cpp
  R engines/scumm/insane/insane_rebel_render.cpp
  R engines/scumm/insane/insane_rebel_runlevels.cpp
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/module.mk
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8911ad08a45..0a7f24ecc72 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -22,11 +22,13 @@
 #include "common/system.h"
 #include "common/events.h"
 #include "common/endian.h"
+
 #include "audio/audiostream.h"
-#include "audio/decoders/raw.h"
 #include "audio/mixer.h"
+
 #include "graphics/cursorman.h"
 #include "graphics/wincursor.h"
+
 #include "scumm/scumm_v7.h"
 #include "scumm/scumm.h"
 #include "scumm/smush/smush_player.h"
@@ -34,21 +36,6 @@
 
 namespace Scumm {
 
-// RA1 coordinate constants (scaled from RA2's 424x260 → 384x242)
-// Original coordinate space: 320x200 game viewport at (0,0) in the 384x242 buffer.
-// Ship base position in the original: (0xA0, 100) = (160, 100) = center of 320x200.
-// Accumulator range: ±0x82 (~±130), so ship can reach ~(30..290, -30..230).
-static const int16 kCenterX = 160;  // _DAT_74B6 init = 0xA0
-static const int16 kCenterY = 100;  // _DAT_74B8 init = 100
-static const int16 kMinX = 20;
-static const int16 kMaxX = 300;
-static const int16 kMinY = 20;
-static const int16 kMaxY = 180;
-
-// Perspective focal lengths (from original tuning table)
-static const int16 kFocalX = 43;    // 0x2b
-static const int16 kFocalY = 25;    // 0x19
-
 // Per-difficulty tuning tables from assault_data_3.bin
 // Indexed: difficulty * 0x28B + level * 0x1F + offset
 // Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
@@ -68,33 +55,6 @@ static const int16 kTuningTable[2][3][9] = {
 };
 static const int kNumTunedLevels = 2;
 
-// Decode BOMP RLE (codec 21) sprite data into a flat pixel buffer.
-// Same algorithm as NutRenderer::codec21 but without palette tracking.
-static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
-	while (height--) {
-		byte *dstNext = dst + pitch;
-		const byte *srcNext = src + 2 + READ_LE_UINT16(src);
-		src += 2;
-		int len = width;
-		byte *d = dst;
-		do {
-			int offs = READ_LE_UINT16(src); src += 2;
-			d += offs;
-			len -= offs;
-			if (len <= 0)
-				break;
-			int w = READ_LE_UINT16(src) + 1; src += 2;
-			len -= w;
-			if (len < 0)
-				w += len;
-			memcpy(d, src, w);
-			src += w;
-			d += w;
-		} while (len > 0);
-		dst = dstNext;
-		src = srcNext;
-	}
-}
 
 void InsaneRebel1::loadTuningForLevel(int level) {
 	int d = CLIP(_difficulty, 0, 2);
@@ -117,14 +77,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenWidth = 384;
 	_screenHeight = 242;
 
-	_shipPosX = kCenterX;
-	_shipPosY = kCenterY;
+	_shipPosX = kRA1CenterX;
+	_shipPosY = kRA1CenterY;
 	_shipDirIndex = 17;  // Center of 5x7 grid (2*7 + 3)
 
-	_corridorLeftX = kMinX;
-	_corridorTopY = kMinY;
-	_corridorRightX = kMaxX;
-	_corridorBottomY = kMaxY;
+	_corridorLeftX = kRA1MinX;
+	_corridorTopY = kRA1MinY;
+	_corridorRightX = kRA1MaxX;
+	_corridorBottomY = kRA1MaxY;
 
 	_rollAccum = 0;
 	_liftSmooth = 0;
@@ -205,1699 +165,4 @@ InsaneRebel1::~InsaneRebel1() {
 	terminateAudio();
 }
 
-bool InsaneRebel1::notifyEvent(const Common::Event &event) {
-	if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
-		switch (event.kbd.keycode) {
-		case Common::KEYCODE_UP:
-		case Common::KEYCODE_w:
-			_optionsSel = (_optionsSel + 2) % 3;
-			return true;
-		case Common::KEYCODE_DOWN:
-		case Common::KEYCODE_s:
-			_optionsSel = (_optionsSel + 1) % 3;
-			return true;
-		case Common::KEYCODE_RETURN:
-		case Common::KEYCODE_KP_ENTER:
-		case Common::KEYCODE_SPACE:
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_ESCAPE:
-			_optionsSel = 2;  // Back
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		default:
-			break;
-		}
-	}
-
-	if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
-		switch (event.kbd.keycode) {
-		case Common::KEYCODE_UP:
-		case Common::KEYCODE_w:
-			_menuSelection = (_menuSelection + 4) % 5;
-			return true;
-		case Common::KEYCODE_DOWN:
-		case Common::KEYCODE_s:
-			_menuSelection = (_menuSelection + 1) % 5;
-			return true;
-		case Common::KEYCODE_RETURN:
-		case Common::KEYCODE_KP_ENTER:
-		case Common::KEYCODE_SPACE:
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_1:
-		case Common::KEYCODE_2:
-		case Common::KEYCODE_3:
-		case Common::KEYCODE_4:
-		case Common::KEYCODE_5:
-			_menuSelection = event.kbd.keycode - Common::KEYCODE_1;
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_ESCAPE:
-			_menuSelection = 4;
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		default:
-			break;
-		}
-	}
-
-	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
-		if (_player) {
-			debug("Rebel1: ESC pressed - skipping video");
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		}
-	}
-
-	return false;
-}
-
-// ---------------------------------------------------------------------------
-// Audio
-// ---------------------------------------------------------------------------
-
-void InsaneRebel1::initAudio(int sampleRate) {
-	_audioSampleRate = sampleRate;
-	for (int i = 0; i < kMaxAudioTracks; i++) {
-		_audioStreams[i] = nullptr;
-		_audioTrackActive[i] = false;
-	}
-}
-
-void InsaneRebel1::terminateAudio() {
-	for (int i = 0; i < kMaxAudioTracks; i++) {
-		if (_audioTrackActive[i]) {
-			_vm->_mixer->stopHandle(_audioHandles[i]);
-			_audioTrackActive[i] = false;
-		}
-		if (_audioStreams[i]) {
-			_audioStreams[i]->finish();
-			_audioStreams[i] = nullptr;
-		}
-	}
-}
-
-void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
-	if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
-		return;
-
-	if (!_audioStreams[trackIdx]) {
-		debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
-		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
-		_audioTrackActive[trackIdx] = true;
-		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
-								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
-								DisposeAfterUse::NO);
-	}
-
-	byte *audioCopy = (byte *)malloc(size);
-	if (!audioCopy)
-		return;
-	memcpy(audioCopy, data, size);
-
-	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
-	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
-	int scaledPan = (pan * 127) / 128;
-	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
-	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
-}
-
-void InsaneRebel1::processAudioFrame(int16 feedSize) {
-	if (!_player)
-		return;
-
-	SmushPlayer *sp = _player;
-
-	if (sp->_smushTracksNeedInit) {
-		sp->_smushTracksNeedInit = false;
-		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
-			sp->_smushDispatch[i].fadeRemaining = 0;
-			sp->_smushDispatch[i].fadeVolume = 0;
-			sp->_smushDispatch[i].fadeSampleRate = 0;
-			sp->_smushDispatch[i].elapsedAudio = 0;
-			sp->_smushDispatch[i].audioLength = 0;
-		}
-	}
-
-	for (int i = 0; i < sp->_smushNumTracks; i++) {
-		SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
-		SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
-
-		if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
-			continue;
-
-		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
-
-		if (!isPlayableTrack)
-			continue;
-
-		int baseVolume;
-		switch (track.flags & TRK_TYPE_MASK) {
-		case IS_SFX:
-			baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
-			break;
-		case IS_BKG_MUSIC:
-			baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
-			break;
-		case IS_SPEECH:
-			baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
-			break;
-		default:
-			baseVolume = track.volume;
-			break;
-		}
-		int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
-
-		// Handle FADING -> PLAYING transition
-		if (track.state == TRK_STATE_FADING) {
-			dispatch.headerPtr = track.dataBuf;
-			dispatch.dataBuf = track.subChunkPtr;
-			dispatch.dataSize = track.dataSize;
-			dispatch.currentOffset = 0;
-			dispatch.audioLength = 0;
-			track.state = TRK_STATE_PLAYING;
-		}
-
-		if (track.state != TRK_STATE_INACTIVE) {
-			int32 tmpFeedSize = feedSize;
-
-			while (tmpFeedSize > 0) {
-				int32 mixInFrameCount = dispatch.currentOffset;
-
-				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
-					if (dispatch.audioRemaining < 0)
-						dispatch.audioRemaining = 0;
-
-					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
-					if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
-						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
-						if (mixInFrameCount > maxFrames)
-							mixInFrameCount = maxFrames;
-					}
-
-					if (offset + mixInFrameCount > dispatch.dataSize)
-						mixInFrameCount = dispatch.dataSize - offset;
-
-					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
-						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
-						if (mixInFrameCount <= 0) {
-							track.state = TRK_STATE_ENDING;
-							break;
-						}
-					}
-
-					if (mixInFrameCount > 0) {
-						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
-							break;
-
-						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
-						dispatch.currentOffset -= mixInFrameCount;
-						dispatch.audioRemaining += mixInFrameCount;
-
-						if (dispatch.sampleRate > 0) {
-							int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
-							tmpFeedSize -= consumedFeed;
-						} else {
-							tmpFeedSize -= mixInFrameCount;
-						}
-					}
-				}
-
-				if (dispatch.currentOffset <= 0) {
-					if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
-						break;
-					if (dispatch.currentOffset <= 0)
-						break;
-				} else if (tmpFeedSize <= 0) {
-					break;
-				}
-			}
-		}
-
-		track.audioRemaining = dispatch.audioRemaining;
-		dispatch.state = track.state;
-	}
-}
-
-// Load an RA1 NUT sprite file (ANIM v1).
-// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
-// FRME containers. This loader handles that padding properly, unlike the
-// shared NutRenderer::loadFont which assumes even-size chunks.
-bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
-	ScummFile *file = _vm->instantiateScummFile();
-	_vm->openFile(*file, filename);
-	if (!file->isOpen()) {
-		warning("InsaneRebel1::loadRA1Nut: can't open %s", filename);
-		delete file;
-		return false;
-	}
-
-	uint32 tag = file->readUint32BE();
-	if (tag != MKTAG('A','N','I','M')) {
-		warning("InsaneRebel1::loadRA1Nut: no ANIM tag in %s", filename);
-		delete file;
-		return false;
-	}
-	uint32 animSize = file->readUint32BE();
-	byte *data = (byte *)malloc(animSize);
-	file->read(data, animSize);
-	file->close();
-	delete file;
-
-	// data[0..3] = AHDR tag, data[4..7] = AHDR size
-	if (READ_BE_UINT32(data) != MKTAG('A','H','D','R')) {
-		warning("InsaneRebel1::loadRA1Nut: no AHDR in %s", filename);
-		free(data);
-		return false;
-	}
-
-	const uint16 expectedSprites = READ_LE_UINT16(data + 10);
-	bank.numSprites = expectedSprites;
-	bank.sprites = new RA1Sprite[bank.numSprites];
-	memset(bank.sprites, 0, sizeof(RA1Sprite) * bank.numSprites);
-
-	uint32 *fobjOffsets = (uint32 *)calloc(expectedSprites, sizeof(uint32));
-	if (!fobjOffsets) {
-		free(data);
-		return false;
-	}
-
-	// Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
-	uint32 decodedSize = 0;
-	uint16 foundSprites = 0;
-	uint32 chunkOffset = 0;
-	while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
-		uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
-		uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
-		uint32 chunkDataOffset = chunkOffset + 8;
-		uint32 chunkEnd = chunkDataOffset + chunkSize;
-		if (chunkEnd > animSize)
-			break;
-
-		if (chunkTag == MKTAG('F','R','M','E')) {
-			bool foundFobj = false;
-			uint32 subOffset = chunkDataOffset;
-			while (subOffset + 8 <= chunkEnd) {
-				uint32 subTag = READ_BE_UINT32(data + subOffset);
-				uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
-				uint32 subDataOffset = subOffset + 8;
-				uint32 subEnd = subDataOffset + subSize;
-				if (subEnd > chunkEnd)
-					break;
-
-				if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
-					uint16 w = READ_LE_UINT16(data + subOffset + 14);
-					uint16 h = READ_LE_UINT16(data + subOffset + 16);
-					decodedSize += (uint32)w * (uint32)h;
-					fobjOffsets[foundSprites] = subOffset;
-					foundFobj = true;
-					break;
-				}
-
-				subOffset = subEnd;
-				if (subSize & 1)
-					subOffset++;
-			}
-			// Always increment for every FRME to preserve char-to-glyph alignment.
-			// Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
-			foundSprites++;
-		}
-
-		chunkOffset = chunkEnd;
-		if (chunkSize & 1)
-			chunkOffset++;
-	}
-
-	bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
-	bank.decodedSize = decodedSize;
-	byte *decPtr = bank.decodedData;
-
-	// Pass 2: Decode collected FOBJ entries.
-	for (uint16 i = 0; i < foundSprites; i++) {
-		uint32 fobjOffset = fobjOffsets[i];
-		if (fobjOffset == 0) {
-			// Empty FRME (no FOBJ) — leave sprite as blank (zeroed by memset).
-			continue;
-		}
-
-		int codec = READ_LE_UINT16(data + fobjOffset + 8);
-		bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
-		bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
-		bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
-		bank.sprites[i].height = READ_LE_UINT16(data + fobjOffset + 16);
-
-		int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
-		const byte *fobjData = data + fobjOffset + 22;
-
-		if (codec == 21) {
-			bank.sprites[i].data = decPtr;
-			decodeBomp(decPtr, fobjData, bank.sprites[i].width,
-					   bank.sprites[i].height, bank.sprites[i].width);
-		} else {
-			bank.sprites[i].width = 0;
-			bank.sprites[i].height = 0;
-			bank.sprites[i].data = nullptr;
-			warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
-		}
-
-		decPtr += pixelCount;
-	}
-
-	free(fobjOffsets);
-
-	free(data);
-	debug(1, "InsaneRebel1::loadRA1Nut('%s'): expected=%d found=%d decoded=%d bytes",
-		  filename, expectedSprites, foundSprites, decodedSize);
-	return true;
-}
-
-void InsaneRebel1::loadLevelSprites(int level) {
-	// Ship direction bank — not all levels have one (e.g. Level 2 is first-person)
-	Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
-	if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
-		debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
-	}
-	loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
-
-	// Explosion sprites — try BANG first, then EXPLD
-	Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
-	if (!loadRA1Nut(bangFile.c_str(), _bangBank)) {
-		Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
-		loadRA1Nut(expldFile.c_str(), _bangBank);
-	}
-}
-
-void InsaneRebel1::procPreRendering(byte *renderBitmap) {
-}
-
-void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
-	int32 setupsan13, int32 curFrame, int32 maxFrame) {
-	if (_menuActive && renderBitmap) {
-		int width = _player ? _player->_width : 0;
-		int height = _player ? _player->_height : 0;
-		if (width == 0) width = _screenWidth;
-		if (height == 0) height = _screenHeight;
-		int pitch = width;
-		renderMainMenuOverlay(renderBitmap, pitch, width, height);
-	}
-
-	if (!_interactiveVideoActive || !renderBitmap)
-		return;
-
-	int width = _player->_width;
-	int height = _player->_height;
-	if (width == 0) width = _screenWidth;
-	if (height == 0) height = _screenHeight;
-	int pitch = width;
-
-	if (_currentLevel == 1) {
-		// Level 2: first-person asteroid dodge — no ship sprite, input averaging physics
-		updateAsteroidPhysics();
-	} else {
-		// Level 1 (and others): third-person ship flight with accumulators
-		if (_shipBank.numSprites > 0) {
-			updateShipPhysics();
-			renderShip(renderBitmap, pitch, width, height);
-		}
-	}
-	renderExplosions(renderBitmap, pitch, width, height);
-	renderCrosshair(renderBitmap, pitch, width, height);
-	renderHUD(renderBitmap, pitch, width, height);
-}
-
-// Draw crosshair/pointer at mouse position into the render buffer.
-// The original DOS game uses the INT 33h hardware mouse cursor (a red crosshair).
-// We replicate it by drawing a red cross into the render buffer each frame.
-void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
-	int cx = _vm->_mouse.x;
-	int cy = _vm->_mouse.y;
-
-	// Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
-	// Palette index 15 = (255,255,255) white, used as outline for visibility.
-	const byte colorInner = 119;
-	const byte colorOutline = 15;
-	const int size = 7;  // arm length
-
-	// Helper lambda to draw a pixel with outline
-	auto drawPx = [&](int x, int y, byte c) {
-		if (x >= 0 && x < width && y >= 0 && y < height)
-			dst[y * pitch + x] = c;
-	};
-
-	// Draw outline first (1px border around each arm pixel)
-	for (int d = -size; d <= size; d++) {
-		if (d >= -1 && d <= 1) continue; // skip center area for outline
-		// Horizontal arm outline
-		drawPx(cx + d, cy - 1, colorOutline);
-		drawPx(cx + d, cy + 1, colorOutline);
-		// Vertical arm outline
-		drawPx(cx - 1, cy + d, colorOutline);
-		drawPx(cx + 1, cy + d, colorOutline);
-	}
-	// Arm endpoints
-	drawPx(cx - size - 1, cy, colorOutline);
-	drawPx(cx + size + 1, cy, colorOutline);
-	drawPx(cx, cy - size - 1, colorOutline);
-	drawPx(cx, cy + size + 1, colorOutline);
-
-	// Draw red cross arms
-	for (int d = -size; d <= size; d++) {
-		if (d == 0) continue; // gap at center
-		drawPx(cx + d, cy, colorInner);  // horizontal
-		drawPx(cx, cy + d, colorInner);  // vertical
-	}
-	// Center dot
-	drawPx(cx, cy, colorInner);
-}
-
-void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
-	if (!dst || !text || _hudFontBank.numSprites <= 0)
-		return;
-
-	for (int i = 0; text[i] != '\0'; i++) {
-		const byte ch = (byte)text[i];
-
-		if (ch == ' ') {
-			x += 6;
-			continue;
-		}
-
-		// RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
-		if (ch < 0x21) {
-			x += 4;
-			continue;
-		}
-		const int fontIdx = (int)ch - 0x21;
-		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
-			x += 4;
-			continue;
-		}
-
-		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
-		const int gw = glyph.width;
-		const int gh = glyph.height;
-		const int gx = x + glyph.xoffs;
-		const int gy = y + glyph.yoffs;
-		const uint64 glyphPixels = (uint64)gw * (uint64)gh;
-		if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
-			x += 4;
-			continue;
-		}
-		if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
-			x += 4;
-			continue;
-		}
-		const byte *bankStart = _hudFontBank.decodedData;
-		const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
-		if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
-			x += 4;
-			continue;
-		}
-
-		for (int py = 0; py < gh; py++) {
-			const int sy = gy + py;
-			if (sy < 0 || sy >= height)
-				continue;
-			for (int px = 0; px < gw; px++) {
-				const int sx = gx + px;
-				if (sx < 0 || sx >= width)
-					continue;
-				const byte pixel = glyph.data[py * gw + px];
-				if (pixel != 0)
-					dst[sy * pitch + sx] = pixel;
-			}
-		}
-
-		x += gw > 0 ? gw : 4;
-	}
-}
-
-// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
-// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
-int InsaneRebel1::getFontBankStringWidth(const char *text) {
-	if (!text || _hudFontBank.numSprites <= 0)
-		return 0;
-
-	int w = 0;
-	for (int i = 0; text[i] != '\0'; i++) {
-		const byte ch = (byte)text[i];
-		if (ch == ' ') {
-			w += 6;
-			continue;
-		}
-		if (ch < 0x21) {
-			w += 4;
-			continue;
-		}
-		const int fontIdx = (int)ch - 0x21;
-		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
-			w += 4;
-			continue;
-		}
-		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
-		w += glyph.width > 0 ? glyph.width : 4;
-	}
-	return w;
-}
-
-// renderMainMenuOverlay -- Draw menu text and selection highlight box.
-// Original menu strings from assault_data_3.bin at 0x5822.
-// Highlight uses RA2-style flashing border box (FUN_004292d0 pattern).
-void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
-	_menuFrameCounter++;
-
-	if (_optionsActive) {
-		// --- Options submenu ---
-		static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
-
-		const int titleW = getFontBankStringWidth("GAME OPTIONS");
-		drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
-
-		// Build dynamic option strings
-		char diffLine[64];
-		snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
-		const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
-		const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
-
-		const int menuY = 60;
-		const int rowH = 16;
-
-		for (int i = 0; i < 3; i++) {
-			const int textW = getFontBankStringWidth(kOptionsItems[i]);
-			const int textX = (width - textW) / 2;
-			const int y = menuY + i * rowH;
-			drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
-
-			if (i == _optionsSel) {
-				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-				int bracketWidth = textW + 12;
-				int leftX = CLIP(textX - 6, 0, width - 1);
-				int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
-				int topY = CLIP(y - 1, 0, height - 1);
-				int bottomY = CLIP(y + rowH - 2, 0, height - 1);
-				for (int x = leftX; x <= rightX; x++) {
-					dst[topY * pitch + x] = highlightColor;
-					dst[bottomY * pitch + x] = highlightColor;
-				}
-				for (int py = topY; py <= bottomY; py++) {
-					dst[py * pitch + leftX] = highlightColor;
-					dst[py * pitch + rightX] = highlightColor;
-				}
-			}
-		}
-		return;
-	}
-
-	// --- Main menu ---
-	static const char *kMenuItems[5] = {
-		"START NEW GAME",
-		"GAME OPTIONS",
-		"ENTER PASSCODE",
-		"CONTINUE DEMO",
-		"EXIT TO DOS"
-	};
-
-	// Center title
-	const int titleW = getFontBankStringWidth("MAIN MENU");
-	const int titleX = (width - titleW) / 2;
-	drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
-
-	// Draw menu items centered horizontally
-	const int menuY = 60;
-	const int rowH = 16;
-
-	for (int i = 0; i < 5; i++) {
-		const int textW = getFontBankStringWidth(kMenuItems[i]);
-		const int textX = (width - textW) / 2;
-		const int y = menuY + i * rowH;
-
-		drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
-
-		// Selection highlight box — flashing border (FUN_004292d0 pattern from RA2)
-		if (i == _menuSelection) {
-			// Flash between two palette colors every 8 frames
-			byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-
-			int bracketWidth = textW + 12;
-			int bracketHeight = rowH;
-			int leftX = textX - 6;
-			int rightX = leftX + bracketWidth;
-			int topY = y - 1;
-			int bottomY = y + bracketHeight - 2;
-
-			// Clamp
-			if (leftX < 0) leftX = 0;
-			if (rightX >= width) rightX = width - 1;
-			if (topY < 0) topY = 0;
-			if (bottomY >= height) bottomY = height - 1;
-
-			// Draw rectangle border (4 lines)
-			for (int x = leftX; x <= rightX && x < width; x++) {
-				if (topY >= 0 && topY < height)
-					dst[topY * pitch + x] = highlightColor;
-				if (bottomY >= 0 && bottomY < height)
-					dst[bottomY * pitch + x] = highlightColor;
-			}
-			for (int py = topY; py <= bottomY && py < height; py++) {
-				if (leftX >= 0 && leftX < width)
-					dst[py * pitch + leftX] = highlightColor;
-				if (rightX >= 0 && rightX < width)
-					dst[py * pitch + rightX] = highlightColor;
-			}
-		}
-	}
-}
-
-// Ship physics matching FUN_1DEB5 (accumulator-based position system).
-// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
-// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
-void InsaneRebel1::updateShipPhysics() {
-	_frameCounter++;
-
-	// Decrement cooldown
-	if (_damageCooldown > 0)
-		_damageCooldown--;
-
-	// --- Step 1: Mouse input as offset from screen center ---
-	// Original: _DAT_756C (horizontal), _DAT_756E (vertical)
-	int16 inputX = (int16)(_vm->_mouse.x - 160);
-	int16 inputY = (int16)(_vm->_mouse.y - 100);
-	inputX = CLIP<int16>(inputX, -127, 127);
-	inputY = CLIP<int16>(inputY, -127, 127);
-
-	// --- Step 2: Roll accumulator (_74CA) ---
-	// Normal mode: accumulate; mode 0x10: snap to input
-	_rollAccum += (_tuning.roll * (int32)inputX) >> 5;
-	_rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
-
-	// --- Step 3: Vertical smoothing (_74CE) ---
-	// Exponential decay toward -inputY
-	_liftSmooth += (-_liftSmooth - (int32)inputY) >> 1;
-	_liftSmooth = CLIP<int32>(_liftSmooth, -0x20, 0x20);
-
-	// --- Step 4: Position accumulator deltas ---
-	// X delta: drift + slide coupling - cross-coupling
-	int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100;  // 0-199, centered at 100
-	int32 crossTermX;
-	if (_liftSmooth < 0)
-		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
-	else
-		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 12;
-
-	int32 deltaX = (((rng - 100) - (int32)_tuning.drift * _driftParam) >> 1)
-	             + (((int32)_tuning.slide * _rollAccum) >> 7)
-	             - crossTermX;
-
-	// Y delta: roll magnitude + lift cross-coupling
-	int32 absRoll = ABS(_rollAccum);
-	int32 crossTermY;
-	if (_liftSmooth < 0)
-		crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 12;
-	else
-		crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 13;
-
-	int32 deltaY = (absRoll >> 1) + crossTermY;
-
-	// --- Step 5: Update position accumulators ---
-	_posAccumX += deltaX;
-	_posAccumX = CLIP<int32>(_posAccumX, -0x8200, 0x8200);
-	_posAccumY += deltaY;
-	_posAccumY = CLIP<int32>(_posAccumY, -0x3200, 0x4600);
-
-	// --- Step 6: Derive pixel position from accumulators ---
-	// Original: _74BA = _74C2 >> 8, _74BC = _74C6 >> 8
-	// Ship position = base + offset
-	_shipPosX = kCenterX + (int16)(_posAccumX >> 8);
-	_shipPosY = kCenterY + (int16)(_posAccumY >> 8);
-
-	// Clamp to screen bounds
-	_shipPosX = CLIP<int16>(_shipPosX, kMinX, kMaxX);
-	_shipPosY = CLIP<int16>(_shipPosY, kMinY, kMaxY);
-
-	// --- Step 7: Corridor collision (FUN_1C54D) ---
-	// Wall contact forces position accumulators to corridor edge and sets
-	// damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
-	{
-		bool hasZoneHit = (_damageFlags & 0x10) != 0;
-
-		if (_shipPosX > _corridorRightX) {
-			_posAccumX = (_corridorRightX - kCenterX) << 8;
-			_shipPosX = _corridorRightX;
-			if (!hasZoneHit) {
-				if (_rollAccum > -0x100)
-					_rollAccum = -0x100;  // Push left
-				_damageFlags |= 0x02;  // Right wall
-			}
-		}
-		if (_shipPosX < _corridorLeftX) {
-			_posAccumX = (_corridorLeftX - kCenterX) << 8;
-			_shipPosX = _corridorLeftX;
-			if (!hasZoneHit) {
-				if (_rollAccum < 0x100)
-					_rollAccum = 0x100;   // Push right
-				_damageFlags |= 0x04;  // Left wall
-			}
-		}
-		if (_shipPosY < _corridorTopY) {
-			_posAccumY = ((_corridorTopY - kCenterY) << 8) + 0x100;
-			_shipPosY = _corridorTopY;
-			if (!hasZoneHit)
-				_damageFlags |= 0x01;
-		}
-		if (_shipPosY > _corridorBottomY) {
-			_posAccumY = ((_corridorBottomY - kCenterY) << 8) - 0x100;
-			_shipPosY = _corridorBottomY;
-			if (!hasZoneHit)
-				_damageFlags |= 0x08;
-		}
-	}
-
-	// --- Step 8: Perspective offsets ---
-	{
-		int absOffX = ABS(_shipPosX - kCenterX);
-		if (absOffX > 0)
-			_perspectiveX = (int16)((kFocalX * kCenterX * absOffX) /
-				((kCenterX - kFocalX) * absOffX + kFocalX * kCenterX));
-		else
-			_perspectiveX = 0;
-		if (_shipPosX < kCenterX + 1)
-			_perspectiveX = -_perspectiveX;
-
-		int absOffY = ABS(_shipPosY - kCenterY);
-		if (absOffY > 0)
-			_perspectiveY = (int16)((kFocalY * kCenterY * absOffY) /
-				((kCenterY - kFocalY) * absOffY + kFocalY * kCenterY));
-		else
-			_perspectiveY = 0;
-		if (_shipPosY < kCenterY + 1)
-			_perspectiveY = -_perspectiveY;
-	}
-
-	// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
-	// Horizontal component from _74CA (rollAccum):
-	//   |rollAccum| <= 0x80: center (0)
-	//   rollAccum > 0x80:  ((rollAccum - 0x80) >> 8) * 5 + 5   (right: 5,10,15,20)
-	//   rollAccum < -0x80: ((abs(rollAccum) - 0x80) >> 8) * 5 + 25 (left: 25,30,35,40)
-	int hComponent;
-	if (_rollAccum > 0x80) {
-		hComponent = ((_rollAccum - 0x80) >> 8) * 5 + 5;
-	} else if (_rollAccum < -0x80) {
-		hComponent = ((-_rollAccum - 0x80) >> 8) * 5 + 25;
-	} else {
-		hComponent = 0;
-	}
-
-	// Vertical component from _74CE (liftSmooth):
-	//   (_74CE + 0x20) * 5 / 0x41  → 0..4  (5 rows)
-	int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
-
-	_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
-
-	// --- Step 10: Damage/event bit synthesis + damage processing ---
-	// RA1 FUN_1B297-style latches from GAME opcodes:
-	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
-	//   0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
-	if (_gameLatch5D == 0xFFFF)
-		_damageFlags |= 0x40;
-	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
-		_damageFlags |= 0x80;
-
-	// Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
-	// damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
-	if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
-		_health >= 0 && _deathTimer <= 0) {
-		// Projectile hit (bit 7 = 0x80)
-		if (_damageFlags & 0x80)
-			_health -= _tuning.shot;
-		// Wall collision (bits 1,2,4 = 0x16)
-		if (_damageFlags & 0x16)
-			_health -= _tuning.wham;
-
-		if (_health < 0)
-			_deathTimer = kDeathTimerInit;
-
-		_prevDamageFlags = _damageFlags;
-		_damageCooldown = kDamageCooldownInit;
-		_screenFlash = 3;
-	}
-
-	// Latches are per-frame event inputs in the original pipeline.
-	_gameLatch5D = 0;
-	_gameLatch5F = 0;
-
-	// Death animation countdown
-	if (_health < 0 && _deathTimer > 0)
-		_deathTimer--;
-
-	// Health regeneration: +1 every 32 frames (from original asm)
-	if ((_frameCounter & 0x1F) == 0) {
-		if (_health >= 0 && _health < kMaxHealth)
-			_health++;
-		if (_health >= 0)
-			_score += 1;
-	}
-
-	// Screen flash decay
-	if (_screenFlash > 0)
-		_screenFlash--;
-
-	// Clear per-frame damage flags
-	_damageFlags = 0;
-
-	// --- Path branching detection ---
-	// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
-	// After this point, drift goes strongly negative (pushing ship left for the hard path).
-	// If ship is right of center, player chose the right/easy path → switch to L1PLAY1R.
-	// The check fires once when the game counter first reaches the branch point.
-	if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
-		if (_shipPosX > kCenterX) {
-			_rightPathSelected = true;
-			_vm->_smushVideoShouldFinish = true;
-			debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
-		}
-	}
-
-	debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
-		_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
-		_posAccumX, _posAccumY, _shipDirIndex, _health,
-		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
-}
-
-// Level 2+ asteroid/surface physics (FUN_1CDA7).
-// Uses 10-frame input history averaging instead of accumulators.
-// Ship position = averaged input + center offset.
-// Viewport = second history buffer for smooth camera scrolling.
-void InsaneRebel1::updateAsteroidPhysics() {
-	// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
-	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
-		_health++;
-	}
-
-	// Damage application (FUN_1CDA7 lines 20-41)
-	// No cooldown — all three damage types can stack each frame
-	if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
-		_screenFlash = 5;
-		if (_damageFlags & 0x80)
-			_health -= _tuning.shot;
-		if (_damageFlags & 0x40)
-			_health -= _tuning.miss;
-		if (_damageFlags & 0x20)
-			_health -= _tuning.wham;
-		if (_health < 0) {
-			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
-		}
-		_damageFlags = 0;
-	}
-
-	// Death fade countdown
-	if (_deathTimer > 1 && _health < 0) {
-		_deathTimer--;
-	}
-
-	// Screen flash countdown
-	if (_screenFlash > 0) {
-		_screenFlash--;
-	}
-
-	// Input history shift and averaging (FUN_1CDA7 lines 107-131)
-	// Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
-	for (int i = kInputHistorySize - 1; i > 0; i--) {
-		_inputHistoryX[i] = _inputHistoryX[i - 1];
-		_inputHistoryY[i] = _inputHistoryY[i - 1];
-	}
-
-	// Read current mouse input (mapped to -160..+160 range)
-	int mouseX = 0, mouseY = 0;
-	if (_vm->_mouse.x >= 0) {
-		mouseX = CLIP<int>(_vm->_mouse.x - kCenterX, -kCenterX, kCenterX);
-		mouseY = CLIP<int>(_vm->_mouse.y - kCenterY, -kCenterY, kCenterY);
-	}
-	_inputHistoryX[0] = (int16)mouseX;
-	_inputHistoryY[0] = (int16)mouseY;
-
-	// Average over 10 frames
-	int16 sumX = 0, sumY = 0;
-	for (int i = 0; i < kInputHistorySize; i++) {
-		sumX += _inputHistoryX[i];
-		sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
-	}
-	_avgInputX = (int16)(sumX / kInputHistorySize);
-	_avgInputY = (int16)(sumY / kInputHistorySize);
-
-	// Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
-	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
-	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
-
-	// Ship position = average + center offset
-	// Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
-	_shipPosX = _avgInputX + 0xA0;
-	_shipPosY = _avgInputY + 0x46;
-
-	// Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
-	for (int i = kInputHistorySize - 1; i > 0; i--) {
-		_viewHistoryX[i] = _viewHistoryX[i - 1];
-		_viewHistoryY[i] = _viewHistoryY[i - 1];
-	}
-	_viewHistoryX[0] = _avgInputX;
-	_viewHistoryY[0] = _avgInputY;
-
-	int16 viewSumX = 0, viewSumY = 0;
-	for (int i = 0; i < kInputHistorySize; i++) {
-		viewSumX += _viewHistoryX[i];
-		viewSumY += _viewHistoryY[i];
-	}
-	// Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
-	// Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
-	_perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
-	_perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
-
-	_frameCounter++;
-
-	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
-		_shipPosX, _shipPosY, _avgInputX, _avgInputY,
-		_perspectiveX, _perspectiveY, _health, _screenFlash);
-}
-
-void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
-	// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
-	// Hidden during last 20 frames of death sequence (deathTimer 20→0)
-	if (_health < 0 && _deathTimer <= 20)
-		return;
-
-	if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
-		return;
-
-	const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
-
-	// Position: game coords → screen coords via perspective transform
-	// Adapted from RA2's renderHandler7Ship:
-	//   shipCenterX = (shipX - center) + perspX + screenCenterX
-	int drawX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX - spr.width / 2;
-	int drawY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY - spr.height / 2;
-
-	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
-}
-
-// Render explosion sprites during damage cooldown and death sequence.
-// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
-void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
-	if (_bangBank.numSprites <= 0)
-		return;
-
-	// Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
-	int shipScreenX = (_shipPosX - kCenterX) + _perspectiveX + kCenterX;
-	int shipScreenY = (_shipPosY - kCenterY) + _perspectiveY + kCenterY;
-
-	// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
-	// When dead and deathTimer > 10: random explosion sprites scatter around ship
-	if (_health < 0 && _deathTimer > 10) {
-		int intensity = _deathTimer - 10;  // 20→1 as timer goes 30→11
-		if (intensity > 10)
-			intensity = 20 - intensity;     // Triangle: 0→10→0
-
-		// di = intensity * 4 + 1 (vertical scatter range)
-		// si = -20 + intensity * 4 (horizontal scatter range, DAT_75d8 is 0)
-		int rangeY = intensity * 4 + 1;
-		int rangeX = -20 + intensity * 4;
-		if (rangeX < 1) rangeX = 1;
-
-		for (int i = 0; i < intensity; i++) {
-			// Random sprite from bang bank (FUN_21db0(10))
-			int sprIdx = _vm->_rnd.getRandomNumber(_bangBank.numSprites - 1);
-
-			// Random position around ship (matching assembly random scatter)
-			int randX = (int)_vm->_rnd.getRandomNumber(rangeX * 2) - rangeX;
-			int randY = (int)_vm->_rnd.getRandomNumber(rangeY * 2) - rangeY;
-
-			int drawX = shipScreenX + randX;
-			int drawY = shipScreenY + randY;
-
-			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
-			renderSprite(dst, pitch, width, height,
-				drawX - spr.width / 2, drawY - spr.height / 2, spr);
-		}
-		return;
-	}
-
-	// --- Damage hit explosion (FUN_1DEB5 LAB_1e185) ---
-	// When alive, in cooldown, and bang bank loaded
-	if (_health >= 0 && _damageCooldown > 0) {
-		// Sprite index = 10 - damageCooldown (frames 0→9 as cooldown 10→1)
-		int sprIdx = _bangBank.numSprites - _damageCooldown;
-		if (sprIdx < 0 || sprIdx >= _bangBank.numSprites)
-			return;
-
-		// Position at ship center (DAT_75d8 is always 0 in RA1)
-		int drawX = shipScreenX;
-		int drawY = shipScreenY;
-
-		const RA1Sprite &spr = _bangBank.sprites[sprIdx];
-		renderSprite(dst, pitch, width, height,
-			drawX - spr.width / 2, drawY - spr.height / 2, spr);
-	}
-}
-
-// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
-// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
-void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
-	if (_displayBank.numSprites == 0)
-		return;
-
-	// Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
-	if (_score / 10000 > _prevScore / 10000) {
-		_lives++;
-	}
-	_prevScore = _score;
-
-	const RA1Sprite &bar = _displayBank.sprites[0];
-
-	// DISPLAY.NUT sprite is 320×19 at xoffs=0, yoffs=176 in the original game.
-	// Video FOBJs fill the full 384×242 buffer from (0,0), so use sprite offsets directly.
-	int hudX = bar.xoffs;
-	int hudY = bar.yoffs;
-
-	// Draw the status bar background with transparency (pixel 0 = transparent)
-	if (bar.data && bar.width > 0 && bar.height > 0) {
-		int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
-		int srcOffX = 0, srcOffY = 0;
-		if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
-		if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
-		if (drawX + drawW > width) drawW = width - drawX;
-		if (drawY + drawH > height) drawH = height - drawY;
-
-		for (int iy = 0; iy < drawH; iy++) {
-			const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
-			byte *d = dst + (drawY + iy) * pitch + drawX;
-			for (int ix = 0; ix < drawW; ix++) {
-				byte px = s[ix];
-				if (px != 0)
-					d[ix] = px;
-			}
-		}
-
-		debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
-			hudX, hudY, bar.width, bar.height);
-	}
-
-	// Draw health bar from FUN_1BBCB behavior.
-	// Original logic uses current health as fill width and computes x as (0x92 - health),
-	// so the bar is right-anchored and shrinks from left to right as damage increases.
-	{
-		int barMaxW = kMaxHealth;
-		int barH = 5;
-		int healthWidth = CLIP<int16>(_health, 0, kMaxHealth);
-		int barX = hudX + (0x92 - healthWidth);
-		int barY = hudY + 8;
-		int fillW = CLIP(healthWidth, 0, barMaxW);
-
-		// Color based on damage level (matching original thresholds from FUN_1BBCB)
-		// Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
-		byte barColor;
-		if (_health > _tuning.shot * 2)
-			barColor = 0xD5;  // Green (0,192,0) — low damage
-		else if (_health > _tuning.wham * 2)
-			barColor = 0x63;  // Yellow (255,255,31) — moderate damage
-		else
-			barColor = 0xDD;  // Red (192,0,0) — critical
-
-		// Flash effect on damage
-		if (_screenFlash > 0)
-			barColor = 0xFF;  // White flash
-
-		for (int iy = 0; iy < barH && barY + iy < height; iy++) {
-			byte *d = dst + (barY + iy) * pitch + barX;
-			for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
-				d[ix] = barColor;
-			}
-		}
-	}
-
-	// Lives: black out excess pilot icons embedded in DISPLAY.NUT background.
-	// Original FUN_1BBCB: FUN_21D66(buf, lives*10+186, 6, 51-lives*10, 9, 0, 320)
-	// Icons are 5 slots at x=186..236, each ~10px wide. Cover unused slots with black.
-	if (_lives >= 0 && _lives < 5) {
-		int coverX = hudX + _lives * 10 + 186;
-		int coverY = hudY + 6;
-		int coverW = 51 - _lives * 10;
-		int coverH = 9;
-		if (coverX >= 0 && coverY >= 0) {
-			for (int iy = 0; iy < coverH && coverY + iy < height; iy++) {
-				byte *d = dst + (coverY + iy) * pitch + coverX;
-				for (int ix = 0; ix < coverW && coverX + ix < width; ix++) {
-					d[ix] = 0x00;
-				}
-			}
-		}
-	}
-
-	// Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
-	if (_hudFontBank.numSprites > 0) {
-		char scoreStr[16];
-		Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
-		drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
-	}
-
-}
-
-void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
-								int x, int y, const RA1Sprite &spr) {
-	if (!spr.data || spr.width <= 0 || spr.height <= 0)
-		return;
-
-	int drawX = x, drawY = y, drawW = spr.width, drawH = spr.height;
-	int srcOffsetX = 0, srcOffsetY = 0;
-
-	if (drawX < 0) { srcOffsetX = -drawX; drawW += drawX; drawX = 0; }
-	if (drawY < 0) { srcOffsetY = -drawY; drawH += drawY; drawY = 0; }
-	if (drawX + drawW > width) drawW = width - drawX;
-	if (drawY + drawH > height) drawH = height - drawY;
-	if (drawW <= 0 || drawH <= 0)
-		return;
-
-	for (int iy = 0; iy < drawH; iy++) {
-		const byte *s = spr.data + (srcOffsetY + iy) * spr.width + srcOffsetX;
-		byte *d = dst + (drawY + iy) * pitch + drawX;
-		for (int ix = 0; ix < drawW; ix++) {
-			byte px = s[ix];
-			if (px != 0)
-				d[ix] = px;
-		}
-	}
-}
-
-void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
-	int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
-	int16 par1, int16 par2, int16 par3, int16 par4) {
-}
-
-void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
-}
-
-// Parse RA1 GAME chunks.
-void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
-	if (subSize < 8)
-		return;
-
-	uint32 opcode = b.readUint32BE();
-	uint32 param1 = b.readUint32BE();
-
-	switch (opcode) {
-	case 0x5E:
-		// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
-		// This is not a pure control-mode assignment.
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		_driftParam = 0;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
-
-		// Field1 == 0 corresponds to baseline recenter behavior in the original.
-		if ((int32)param1 == 0) {
-			_shipPosX = kCenterX;
-			_shipPosY = kCenterY;
-		}
-
-		// Keep a conservative default mode after reset.
-		_flyControlMode = 0;
-		debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
-		break;
-
-	case 0x5D:
-		_gameLatch5D = (uint16)param1;
-		debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
-		break;
-
-	case 0x5F:
-		_gameLatch5F = (uint16)param1;
-		debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
-		break;
-
-	case 0x07:
-		// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
-		// f1 is the original's _DAT_7740 (game frame counter)
-		// f3 is the drift/wind parameter combined with tuning table
-		_gameCounter = param1;
-		if (subSize >= 20) {
-			b.readUint32BE(); // f2 (max frames, unused in physics)
-			_driftParam = (int16)(int32)b.readUint32BE();
-			b.readUint32BE(); // f4 (unused in original assembly)
-			debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
-		}
-		break;
-
-	case 0x0D:
-		// Corridor boundaries: per-frame flight corridor
-		// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
-		// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
-		if (subSize >= 20) {
-			_corridorLeftX = (int16)param1;
-			_corridorTopY = (int16)b.readUint32BE();
-			int16 corridorWidth = (int16)b.readUint32BE();
-			int16 corridorHeight = (int16)b.readUint32BE();
-			_corridorRightX = _corridorLeftX + corridorWidth;
-			_corridorBottomY = _corridorTopY + corridorHeight;
-			debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
-				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
-				corridorWidth, corridorHeight);
-		}
-		break;
-
-	case 0x0E:
-		// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
-		// Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
-		if (subSize >= 20) {
-			int16 zoneLeft = (int16)param1;
-			int16 zoneTop = (int16)b.readUint32BE();
-			int16 zoneWidth = (int16)b.readUint32BE();
-			int16 zoneHeight = (int16)b.readUint32BE();
-			int16 zoneRight = zoneLeft + zoneWidth;
-			int16 zoneBottom = zoneTop + zoneHeight;
-			if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
-				_shipPosY > zoneTop && _shipPosY < zoneBottom) {
-				_damageFlags |= 0x10;
-			}
-			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
-				zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
-		}
-		break;
-
-	case 0x0B:
-		// Asteroid/surface per-frame handler (FUN_1CDA7).
-		// field1 = frame counter, field2 = max frames
-		_gameCounter = param1;
-		if (subSize >= 20) {
-			b.readUint32BE(); // field2 (max frames)
-			b.readUint32BE(); // field3
-			b.readUint32BE(); // field4
-		}
-		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
-		break;
-
-	case 0x08: case 0x09: case 0x0A:
-	case 0x19: case 0x1A:
-		if (subSize >= 20) {
-			uint32 param2 = b.readUint32BE();
-			uint32 param3 = b.readUint32BE();
-			uint32 param4 = b.readUint32BE();
-			debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
-		}
-		break;
-
-	default:
-		debug(7, "RA1 GAME unknown 0x%02x size=%d", opcode, subSize);
-		break;
-	}
-}
-
-// ---------------------------------------------------------------------------
-// Game flow (matching original at 0x15597)
-// ---------------------------------------------------------------------------
-
-// Play a passive cinematic (no game callback, skippable).
-// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
-// startFrame > 0: fast-forward (decode without display/audio) to that frame.
-void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
-	debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
-	SmushPlayer *splayer = _vm->_splayer;
-	_player = splayer;
-	_interactiveVideoActive = false;
-	_vm->_smushVideoShouldFinish = false;
-	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve
-	if (startFrame > 0)
-		splayer->setFastForwardToFrame(startFrame);
-	splayer->play(filename, 12);
-}
-
-void InsaneRebel1::clearVideoBuffer() {
-	if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
-		return;
-
-	const int pixelCount = _vm->_screenWidth * _vm->_screenHeight;
-	byte *clearBuffer = (byte *)calloc(pixelCount, 1);
-	if (!clearBuffer)
-		return;
-
-	if (_vm->_macScreen) {
-		_vm->mac_drawBufferToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
-	} else {
-		_vm->_system->copyRectToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
-	}
-	_vm->_system->updateScreen();
-
-	free(clearBuffer);
-}
-
-// Intro sequence (0x155ef-0x158f8):
-//   1. O1LOGO.ANM — LucasArts logo
-//   2. O1OPEN.ANM — Star Wars opening crawl
-void InsaneRebel1::playIntroSequence() {
-	debug(1, "InsaneRebel1: Playing intro sequence");
-
-	// LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
-	playCinematic("OPEN/O1LOGO.ANM");
-	if (_vm->shouldQuit())
-		return;
-	clearVideoBuffer();
-
-	// Star Wars opening crawl (original: PUSH 0x5800, CALL FUN_1BA32)
-	playCinematic("OPEN/O1OPEN.ANM");
-}
-
-// Main menu on O1OPTION.ANM background (0x15968).
-// Original renders text overlay with 5 menu items via FUN_21F7A.
-// For now, we play the menu video as a passive cinematic (non-interactive)
-// and return "Start New Game" immediately.
-// TODO: Implement interactive menu with keyboard/mouse selection.
-int InsaneRebel1::runMainMenu() {
-	debug(1, "InsaneRebel1: Main menu");
-
-	_menuSelection = 0;
-	while (!_vm->shouldQuit()) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
-
-		if (_vm->shouldQuit())
-			return 5;
-
-		if (_menuConfirmed)
-			return _menuSelection + 1;
-	}
-
-	return 5;
-}
-
-void InsaneRebel1::runOptionsMenu() {
-	_optionsSel = 0;
-	_optionsActive = true;
-
-	while (!_vm->shouldQuit()) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
-
-		if (_vm->shouldQuit())
-			break;
-
-		if (_menuConfirmed) {
-			switch (_optionsSel) {
-			case 0:
-				// Cycle difficulty
-				_difficulty = (_difficulty + 1) % 3;
-				loadTuningForLevel(0);
-				break;
-			case 1:
-				// Toggle turbulence
-				_turbulenceEnabled = !_turbulenceEnabled;
-				break;
-			case 2:
-				// Back to main menu
-				_optionsActive = false;
-				return;
-			}
-		}
-	}
-	_optionsActive = false;
-}
-
-// Level 1 flow (0x16100-0x167A2, from disassembly):
-//   1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
-//   2. L1HANGAR.ANM — Full hangar departure cutscene (782 frames, flags 0x0420)
-//   3. L1CU1.ANM — Pre-flight cutscene (flags 0x0400)
-//   4. L1PLAY1L.ANM — Stage 1 flight, hard/left path (788 frames)
-//      At frame 394, if player steers right → L1PLAY1R (easy path, 396 frames)
-//   5. L1CU2.ANM — Mid-level cutscene
-//   6. L1PLAY2.ANM — Stage 2 turret
-//      If score < 5 (0x75D0): L1RETRY → retry Stage 2
-//   7. L1END.ANM — Level complete
-//   Death (health<0): L1CRASHA/B → lives check:
-//     lives>0: L1NEW → jump back to Stage 1 (skip L1HANGAR/L1CU1)
-//     lives==0: L1DEATH → return to menu
-
-bool InsaneRebel1::runLevel1() {
-	debug(1, "InsaneRebel1: Running level 1");
-
-	// Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
-	loadLevelSprites(1);
-
-	// L1HANGAR.ANM — Hangar departure (original: 0x5918, flags 0x0420)
-	// Plays once at level start, never replayed on retry.
-	playCinematic("LVL1/L1HANGAR.ANM");
-	if (_vm->shouldQuit())
-		return false;
-
-	// L1CU1.ANM — Pre-flight cutscene (original: 0x5944, flags 0x0400)
-	// Plays once at level start, never replayed on retry.
-	playCinematic("LVL1/L1CU1.ANM");
-	if (_vm->shouldQuit())
-		return false;
-
-	// Retry loop — on death with lives, L1NEW plays then jumps back here
-	while (!_vm->shouldQuit()) {
-		// Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_pathBranchEnabled = true;
-		_rightPathSelected = false;
-
-		// Stage 1 flight — L1PLAY1L (hard/left path)
-		// The first 394 frames are the common section. At counter 394, if
-		// ship is right of center, we switch to L1PLAY1R (easy path).
-		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
-		if (_rightPathSelected && _health >= 0) {
-			debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
-			_pathBranchEnabled = false;
-			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
-			if (_vm->shouldQuit())
-				return false;
-		}
-		_pathBranchEnabled = false;
-
-		if (_health >= 0) {
-			// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
-			playCinematic("LVL1/L1CU2.ANM");
-			if (_vm->shouldQuit())
-				return false;
-
-			// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
-			playInteractiveVideo("LVL1/L1PLAY2.ANM");
-			if (_vm->shouldQuit())
-				return false;
-
-			// TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
-			// If score < 5: L1RETRY → retry Stage 2
-
-			// L1END.ANM — Level complete! (original: 0x59a3)
-			playCinematic("LVL1/L1END.ANM");
-			return true;
-		}
-
-		// Death sequence (original: 0x165dd-0x166bb)
-		// Random crash variant A or B
-		if (_vm->_rnd.getRandomNumber(1) == 0)
-			playCinematic("LVL1/L1CRASHA.ANM");
-		else
-			playCinematic("LVL1/L1CRASHB.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
-		// Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
-		_lives--;
-		if (_lives <= 0) {
-			// Game over — L1DEATH then return (original: 0x166C0)
-			playCinematic("LVL1/L1DEATH.ANM");
-			debug(1, "InsaneRebel1: Game over (no lives left)");
-			return false;
-		}
-
-		// Lives remaining — L1NEW briefing then retry (original: 0x16675)
-		playCinematic("LVL1/L1NEW.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
-		// Loop back to gameplay (original: JMP 0x16214 — health reset + Stage 1)
-	}
-
-	return false;
-}
-
-// Level 2: Asteroid Field Training
-// Flow: L2NEW → L2INTRO → L2PLAY (interactive) → L2END/L2DEATH
-bool InsaneRebel1::runLevel2() {
-	debug(1, "InsaneRebel1: Running level 2");
-
-	_currentLevel = 1;
-	loadLevelSprites(2);
-	loadTuningForLevel(1);
-
-	// L2INTRO.ANM — intro cutscene (481 frames)
-	playCinematic("LVL2/L2INTRO.ANM");
-	if (_vm->shouldQuit())
-		return false;
-
-	// Retry loop
-	while (!_vm->shouldQuit()) {
-		// Reset state for this attempt
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
-
-		// L2PLAY.ANM — asteroid dodge (800 frames, interactive)
-		playInteractiveVideo("LVL2/L2PLAY.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
-		if (_health >= 0) {
-			// Level complete!
-			playCinematic("LVL2/L2END.ANM");
-			return true;
-		}
-
-		// Death
-		playCinematic("LVL2/L2DEATH.ANM");
-		if (_vm->shouldQuit())
-			return false;
-
-		_lives--;
-		if (_lives <= 0) {
-			debug(1, "InsaneRebel1: Game over (no lives left)");
-			return false;
-		}
-
-		// Retry briefing
-		playCinematic("LVL2/L2NEW.ANM");
-		if (_vm->shouldQuit())
-			return false;
-	}
-
-	return false;
-}
-
-// Main game entry point — called from ScummEngine::go().
-// Matches original flow at 0x15597: intro → menu → level.
-void InsaneRebel1::runGame() {
-	// Play intro sequence (logo + opening)
-	playIntroSequence();
-	if (_vm->shouldQuit())
-		return;
-
-	// Main menu → gameplay loop
-	while (!_vm->shouldQuit()) {
-		int menuResult = runMainMenu();
-		if (_vm->shouldQuit())
-			return;
-
-		switch (menuResult) {
-		case 1: {
-#if 0 // Skip level 1 for testing — jump straight to level 2
-			// Start New Game — play L1NEW briefing then level 1
-			playCinematic("LVL1/L1NEW.ANM");
-			if (_vm->shouldQuit())
-				return;
-
-			bool completed = runLevel1();
-#else
-			bool completed = true;
-#endif
-			if (completed && !_vm->shouldQuit()) {
-				completed = runLevel2();
-				if (completed) {
-					debug(1, "InsaneRebel1: Level 2 completed!");
-					// TODO: Continue to level 3
-				}
-			}
-			_currentLevel = 0;
-			// Return to menu after level ends
-			break;
-		}
-		case 2:
-			// Game Options
-			runOptionsMenu();
-			break;
-		case 5:
-			// Exit
-			return;
-		default:
-			// Passcode, Demo — not yet implemented, return to menu
-			break;
-		}
-	}
-}
-
-// Play interactive gameplay video (with ship physics + HUD).
-void InsaneRebel1::playInteractiveVideo(const char *filename) {
-	debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
-
-	// Stop any leftover audio from previous video
-	terminateAudio();
-	initAudio(_audioSampleRate);
-
-	SmushPlayer *splayer = _vm->_splayer;
-	_player = splayer;
-	clearBit(0);
-	_interactiveVideoActive = true;
-	_vm->_smushVideoShouldFinish = false;
-	splayer->setCurVideoFlags(0x28);
-
-	// Center mouse, hide system cursor (we draw our own), lock mouse to window
-	smush_warpMouse(160, 100, -1);
-	CursorMan.showMouse(false);
-	g_system->lockMouse(true);
-
-	splayer->play(filename, 12);
-	_interactiveVideoActive = false;
-
-	g_system->lockMouse(false);
-}
-
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 61e1b28f968..941f27998ee 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -53,6 +53,16 @@ struct RA1SpriteBank {
 	~RA1SpriteBank() { delete[] sprites; free(decodedData); }
 };
 
+// RA1 coordinate constants (scaled from RA2's 424x260 → 384x242)
+static const int16 kRA1CenterX = 160;
+static const int16 kRA1CenterY = 100;
+static const int16 kRA1MinX = 20;
+static const int16 kRA1MaxX = 300;
+static const int16 kRA1MinY = 20;
+static const int16 kRA1MaxY = 180;
+static const int16 kRA1FocalX = 43;
+static const int16 kRA1FocalY = 25;
+
 /**
  * Star Wars: Rebel Assault (RA1) game logic.
  * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
diff --git a/engines/scumm/insane/insane_rebel1_audio.cpp b/engines/scumm/insane/insane_rebel1_audio.cpp
new file mode 100644
index 00000000000..7ca9e73d7b5
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_audio.cpp
@@ -0,0 +1,205 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+// ---------------------------------------------------------------------------
+// Audio
+// ---------------------------------------------------------------------------
+
+void InsaneRebel1::initAudio(int sampleRate) {
+	_audioSampleRate = sampleRate;
+	for (int i = 0; i < kMaxAudioTracks; i++) {
+		_audioStreams[i] = nullptr;
+		_audioTrackActive[i] = false;
+	}
+}
+
+void InsaneRebel1::terminateAudio() {
+	for (int i = 0; i < kMaxAudioTracks; i++) {
+		if (_audioTrackActive[i]) {
+			_vm->_mixer->stopHandle(_audioHandles[i]);
+			_audioTrackActive[i] = false;
+		}
+		if (_audioStreams[i]) {
+			_audioStreams[i]->finish();
+			_audioStreams[i] = nullptr;
+		}
+	}
+}
+
+void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
+	if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
+		return;
+
+	if (!_audioStreams[trackIdx]) {
+		debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
+		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
+		_audioTrackActive[trackIdx] = true;
+		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
+								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+								DisposeAfterUse::NO);
+	}
+
+	byte *audioCopy = (byte *)malloc(size);
+	if (!audioCopy)
+		return;
+	memcpy(audioCopy, data, size);
+
+	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+	int scaledPan = (pan * 127) / 128;
+	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
+	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+}
+
+void InsaneRebel1::processAudioFrame(int16 feedSize) {
+	if (!_player)
+		return;
+
+	SmushPlayer *sp = _player;
+
+	if (sp->_smushTracksNeedInit) {
+		sp->_smushTracksNeedInit = false;
+		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+			sp->_smushDispatch[i].fadeRemaining = 0;
+			sp->_smushDispatch[i].fadeVolume = 0;
+			sp->_smushDispatch[i].fadeSampleRate = 0;
+			sp->_smushDispatch[i].elapsedAudio = 0;
+			sp->_smushDispatch[i].audioLength = 0;
+		}
+	}
+
+	for (int i = 0; i < sp->_smushNumTracks; i++) {
+		SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
+		SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
+
+		if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
+			continue;
+
+		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
+							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
+
+		if (!isPlayableTrack)
+			continue;
+
+		int baseVolume;
+		switch (track.flags & TRK_TYPE_MASK) {
+		case IS_SFX:
+			baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
+			break;
+		case IS_BKG_MUSIC:
+			baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
+			break;
+		case IS_SPEECH:
+			baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
+			break;
+		default:
+			baseVolume = track.volume;
+			break;
+		}
+		int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
+
+		// Handle FADING -> PLAYING transition
+		if (track.state == TRK_STATE_FADING) {
+			dispatch.headerPtr = track.dataBuf;
+			dispatch.dataBuf = track.subChunkPtr;
+			dispatch.dataSize = track.dataSize;
+			dispatch.currentOffset = 0;
+			dispatch.audioLength = 0;
+			track.state = TRK_STATE_PLAYING;
+		}
+
+		if (track.state != TRK_STATE_INACTIVE) {
+			int32 tmpFeedSize = feedSize;
+
+			while (tmpFeedSize > 0) {
+				int32 mixInFrameCount = dispatch.currentOffset;
+
+				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+					if (dispatch.audioRemaining < 0)
+						dispatch.audioRemaining = 0;
+
+					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+					if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
+						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
+						if (mixInFrameCount > maxFrames)
+							mixInFrameCount = maxFrames;
+					}
+
+					if (offset + mixInFrameCount > dispatch.dataSize)
+						mixInFrameCount = dispatch.dataSize - offset;
+
+					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+						if (mixInFrameCount <= 0) {
+							track.state = TRK_STATE_ENDING;
+							break;
+						}
+					}
+
+					if (mixInFrameCount > 0) {
+						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
+							break;
+
+						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+						dispatch.currentOffset -= mixInFrameCount;
+						dispatch.audioRemaining += mixInFrameCount;
+
+						if (dispatch.sampleRate > 0) {
+							int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
+							tmpFeedSize -= consumedFeed;
+						} else {
+							tmpFeedSize -= mixInFrameCount;
+						}
+					}
+				}
+
+				if (dispatch.currentOffset <= 0) {
+					if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
+						break;
+					if (dispatch.currentOffset <= 0)
+						break;
+				} else if (tmpFeedSize <= 0) {
+					break;
+				}
+			}
+		}
+
+		track.audioRemaining = dispatch.audioRemaining;
+		dispatch.state = track.state;
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
new file mode 100644
index 00000000000..7500706e548
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -0,0 +1,478 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+#include "common/endian.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+// Ship physics matching FUN_1DEB5 (accumulator-based position system).
+// Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
+// driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
+void InsaneRebel1::updateShipPhysics() {
+	_frameCounter++;
+
+	// Decrement cooldown
+	if (_damageCooldown > 0)
+		_damageCooldown--;
+
+	// --- Step 1: Mouse input as offset from screen center ---
+	// Original: _DAT_756C (horizontal), _DAT_756E (vertical)
+	int16 inputX = (int16)(_vm->_mouse.x - 160);
+	int16 inputY = (int16)(_vm->_mouse.y - 100);
+	inputX = CLIP<int16>(inputX, -127, 127);
+	inputY = CLIP<int16>(inputY, -127, 127);
+
+	// --- Step 2: Roll accumulator (_74CA) ---
+	// Normal mode: accumulate; mode 0x10: snap to input
+	_rollAccum += (_tuning.roll * (int32)inputX) >> 5;
+	_rollAccum = CLIP<int32>(_rollAccum, -0x47F, 0x47F);
+
+	// --- Step 3: Vertical smoothing (_74CE) ---
+	// Exponential decay toward -inputY
+	_liftSmooth += (-_liftSmooth - (int32)inputY) >> 1;
+	_liftSmooth = CLIP<int32>(_liftSmooth, -0x20, 0x20);
+
+	// --- Step 4: Position accumulator deltas ---
+	// X delta: drift + slide coupling - cross-coupling
+	int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100;  // 0-199, centered at 100
+	int32 crossTermX;
+	if (_liftSmooth < 0)
+		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
+	else
+		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 12;
+
+	int32 deltaX = (((rng - 100) - (int32)_tuning.drift * _driftParam) >> 1)
+	             + (((int32)_tuning.slide * _rollAccum) >> 7)
+	             - crossTermX;
+
+	// Y delta: roll magnitude + lift cross-coupling
+	int32 absRoll = ABS(_rollAccum);
+	int32 crossTermY;
+	if (_liftSmooth < 0)
+		crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 12;
+	else
+		crossTermY = ((int32)_tuning.lift * (0x7DE - absRoll) * _liftSmooth) >> 13;
+
+	int32 deltaY = (absRoll >> 1) + crossTermY;
+
+	// --- Step 5: Update position accumulators ---
+	_posAccumX += deltaX;
+	_posAccumX = CLIP<int32>(_posAccumX, -0x8200, 0x8200);
+	_posAccumY += deltaY;
+	_posAccumY = CLIP<int32>(_posAccumY, -0x3200, 0x4600);
+
+	// --- Step 6: Derive pixel position from accumulators ---
+	// Original: _74BA = _74C2 >> 8, _74BC = _74C6 >> 8
+	// Ship position = base + offset
+	_shipPosX = kRA1CenterX + (int16)(_posAccumX >> 8);
+	_shipPosY = kRA1CenterY + (int16)(_posAccumY >> 8);
+
+	// Clamp to screen bounds
+	_shipPosX = CLIP<int16>(_shipPosX, kRA1MinX, kRA1MaxX);
+	_shipPosY = CLIP<int16>(_shipPosY, kRA1MinY, kRA1MaxY);
+
+	// --- Step 7: Corridor collision (FUN_1C54D) ---
+	// Wall contact forces position accumulators to corridor edge and sets
+	// damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
+	{
+		bool hasZoneHit = (_damageFlags & 0x10) != 0;
+
+		if (_shipPosX > _corridorRightX) {
+			_posAccumX = (_corridorRightX - kRA1CenterX) << 8;
+			_shipPosX = _corridorRightX;
+			if (!hasZoneHit) {
+				if (_rollAccum > -0x100)
+					_rollAccum = -0x100;  // Push left
+				_damageFlags |= 0x02;  // Right wall
+			}
+		}
+		if (_shipPosX < _corridorLeftX) {
+			_posAccumX = (_corridorLeftX - kRA1CenterX) << 8;
+			_shipPosX = _corridorLeftX;
+			if (!hasZoneHit) {
+				if (_rollAccum < 0x100)
+					_rollAccum = 0x100;   // Push right
+				_damageFlags |= 0x04;  // Left wall
+			}
+		}
+		if (_shipPosY < _corridorTopY) {
+			_posAccumY = ((_corridorTopY - kRA1CenterY) << 8) + 0x100;
+			_shipPosY = _corridorTopY;
+			if (!hasZoneHit)
+				_damageFlags |= 0x01;
+		}
+		if (_shipPosY > _corridorBottomY) {
+			_posAccumY = ((_corridorBottomY - kRA1CenterY) << 8) - 0x100;
+			_shipPosY = _corridorBottomY;
+			if (!hasZoneHit)
+				_damageFlags |= 0x08;
+		}
+	}
+
+	// --- Step 8: Perspective offsets ---
+	{
+		int absOffX = ABS(_shipPosX - kRA1CenterX);
+		if (absOffX > 0)
+			_perspectiveX = (int16)((kRA1FocalX * kRA1CenterX * absOffX) /
+				((kRA1CenterX - kRA1FocalX) * absOffX + kRA1FocalX * kRA1CenterX));
+		else
+			_perspectiveX = 0;
+		if (_shipPosX < kRA1CenterX + 1)
+			_perspectiveX = -_perspectiveX;
+
+		int absOffY = ABS(_shipPosY - kRA1CenterY);
+		if (absOffY > 0)
+			_perspectiveY = (int16)((kRA1FocalY * kRA1CenterY * absOffY) /
+				((kRA1CenterY - kRA1FocalY) * absOffY + kRA1FocalY * kRA1CenterY));
+		else
+			_perspectiveY = 0;
+		if (_shipPosY < kRA1CenterY + 1)
+			_perspectiveY = -_perspectiveY;
+	}
+
+	// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
+	// Horizontal component from _74CA (rollAccum):
+	//   |rollAccum| <= 0x80: center (0)
+	//   rollAccum > 0x80:  ((rollAccum - 0x80) >> 8) * 5 + 5   (right: 5,10,15,20)
+	//   rollAccum < -0x80: ((abs(rollAccum) - 0x80) >> 8) * 5 + 25 (left: 25,30,35,40)
+	int hComponent;
+	if (_rollAccum > 0x80) {
+		hComponent = ((_rollAccum - 0x80) >> 8) * 5 + 5;
+	} else if (_rollAccum < -0x80) {
+		hComponent = ((-_rollAccum - 0x80) >> 8) * 5 + 25;
+	} else {
+		hComponent = 0;
+	}
+
+	// Vertical component from _74CE (liftSmooth):
+	//   (_74CE + 0x20) * 5 / 0x41  → 0..4  (5 rows)
+	int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
+
+	_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
+
+	// --- Step 10: Damage/event bit synthesis + damage processing ---
+	// RA1 FUN_1B297-style latches from GAME opcodes:
+	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
+	//   0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
+	if (_gameLatch5D == 0xFFFF)
+		_damageFlags |= 0x40;
+	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+		_damageFlags |= 0x80;
+
+	// Damage guard/mask from FUN_1DEB5: (_damageFlags & 0x96) != 0
+	// damageFlags & 0x96 = bits 1,2,4,7 = wall collisions (0x16) + projectile hit (0x80)
+	if ((_damageFlags & 0x96) != 0 && _damageCooldown == 0 &&
+		_health >= 0 && _deathTimer <= 0) {
+		// Projectile hit (bit 7 = 0x80)
+		if (_damageFlags & 0x80)
+			_health -= _tuning.shot;
+		// Wall collision (bits 1,2,4 = 0x16)
+		if (_damageFlags & 0x16)
+			_health -= _tuning.wham;
+
+		if (_health < 0)
+			_deathTimer = kDeathTimerInit;
+
+		_prevDamageFlags = _damageFlags;
+		_damageCooldown = kDamageCooldownInit;
+		_screenFlash = 3;
+	}
+
+	// Latches are per-frame event inputs in the original pipeline.
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+
+	// Death animation countdown
+	if (_health < 0 && _deathTimer > 0)
+		_deathTimer--;
+
+	// Health regeneration: +1 every 32 frames (from original asm)
+	if ((_frameCounter & 0x1F) == 0) {
+		if (_health >= 0 && _health < kMaxHealth)
+			_health++;
+		if (_health >= 0)
+			_score += 1;
+	}
+
+	// Screen flash decay
+	if (_screenFlash > 0)
+		_screenFlash--;
+
+	// Clear per-frame damage flags
+	_damageFlags = 0;
+
+	// --- Path branching detection ---
+	// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
+	// After this point, drift goes strongly negative (pushing ship left for the hard path).
+	// If ship is right of center, player chose the right/easy path → switch to L1PLAY1R.
+	// The check fires once when the game counter first reaches the branch point.
+	if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
+		if (_shipPosX > kRA1CenterX) {
+			_rightPathSelected = true;
+			_vm->_smushVideoShouldFinish = true;
+			debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
+		}
+	}
+
+	debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
+		_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
+		_posAccumX, _posAccumY, _shipDirIndex, _health,
+		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+}
+
+// Level 2+ asteroid/surface physics (FUN_1CDA7).
+// Uses 10-frame input history averaging instead of accumulators.
+// Ship position = averaged input + center offset.
+// Viewport = second history buffer for smooth camera scrolling.
+void InsaneRebel1::updateAsteroidPhysics() {
+	// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
+	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
+		_health++;
+	}
+
+	// Damage application (FUN_1CDA7 lines 20-41)
+	// No cooldown — all three damage types can stack each frame
+	if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+		_screenFlash = 5;
+		if (_damageFlags & 0x80)
+			_health -= _tuning.shot;
+		if (_damageFlags & 0x40)
+			_health -= _tuning.miss;
+		if (_damageFlags & 0x20)
+			_health -= _tuning.wham;
+		if (_health < 0) {
+			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
+		}
+		_damageFlags = 0;
+	}
+
+	// Death fade countdown
+	if (_deathTimer > 1 && _health < 0) {
+		_deathTimer--;
+	}
+
+	// Screen flash countdown
+	if (_screenFlash > 0) {
+		_screenFlash--;
+	}
+
+	// Input history shift and averaging (FUN_1CDA7 lines 107-131)
+	// Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
+	for (int i = kInputHistorySize - 1; i > 0; i--) {
+		_inputHistoryX[i] = _inputHistoryX[i - 1];
+		_inputHistoryY[i] = _inputHistoryY[i - 1];
+	}
+
+	// Read current mouse input (mapped to -160..+160 range)
+	int mouseX = 0, mouseY = 0;
+	if (_vm->_mouse.x >= 0) {
+		mouseX = CLIP<int>(_vm->_mouse.x - kRA1CenterX, -kRA1CenterX, kRA1CenterX);
+		mouseY = CLIP<int>(_vm->_mouse.y - kRA1CenterY, -kRA1CenterY, kRA1CenterY);
+	}
+	_inputHistoryX[0] = (int16)mouseX;
+	_inputHistoryY[0] = (int16)mouseY;
+
+	// Average over 10 frames
+	int16 sumX = 0, sumY = 0;
+	for (int i = 0; i < kInputHistorySize; i++) {
+		sumX += _inputHistoryX[i];
+		sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
+	}
+	_avgInputX = (int16)(sumX / kInputHistorySize);
+	_avgInputY = (int16)(sumY / kInputHistorySize);
+
+	// Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
+	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
+	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
+
+	// Ship position = average + center offset
+	// Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
+	_shipPosX = _avgInputX + 0xA0;
+	_shipPosY = _avgInputY + 0x46;
+
+	// Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
+	for (int i = kInputHistorySize - 1; i > 0; i--) {
+		_viewHistoryX[i] = _viewHistoryX[i - 1];
+		_viewHistoryY[i] = _viewHistoryY[i - 1];
+	}
+	_viewHistoryX[0] = _avgInputX;
+	_viewHistoryY[0] = _avgInputY;
+
+	int16 viewSumX = 0, viewSumY = 0;
+	for (int i = 0; i < kInputHistorySize; i++) {
+		viewSumX += _viewHistoryX[i];
+		viewSumY += _viewHistoryY[i];
+	}
+	// Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
+	// Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
+	_perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
+	_perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
+
+	_frameCounter++;
+
+	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
+		_shipPosX, _shipPosY, _avgInputX, _avgInputY,
+		_perspectiveX, _perspectiveY, _health, _screenFlash);
+}
+
+
+void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
+	int16 par1, int16 par2, int16 par3, int16 par4) {
+}
+
+void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
+}
+
+// Parse RA1 GAME chunks.
+void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 8)
+		return;
+
+	uint32 opcode = b.readUint32BE();
+	uint32 param1 = b.readUint32BE();
+
+	switch (opcode) {
+	case 0x5E:
+		// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
+		// This is not a pure control-mode assignment.
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_driftParam = 0;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		// Field1 == 0 corresponds to baseline recenter behavior in the original.
+		if ((int32)param1 == 0) {
+			_shipPosX = kRA1CenterX;
+			_shipPosY = kRA1CenterY;
+		}
+
+		// Keep a conservative default mode after reset.
+		_flyControlMode = 0;
+		debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
+		break;
+
+	case 0x5D:
+		_gameLatch5D = (uint16)param1;
+		debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
+		break;
+
+	case 0x5F:
+		_gameLatch5F = (uint16)param1;
+		debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
+		break;
+
+	case 0x07:
+		// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
+		// f1 is the original's _DAT_7740 (game frame counter)
+		// f3 is the drift/wind parameter combined with tuning table
+		_gameCounter = param1;
+		if (subSize >= 20) {
+			b.readUint32BE(); // f2 (max frames, unused in physics)
+			_driftParam = (int16)(int32)b.readUint32BE();
+			b.readUint32BE(); // f4 (unused in original assembly)
+			debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
+		}
+		break;
+
+	case 0x0D:
+		// Corridor boundaries: per-frame flight corridor
+		// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
+		// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
+		if (subSize >= 20) {
+			_corridorLeftX = (int16)param1;
+			_corridorTopY = (int16)b.readUint32BE();
+			int16 corridorWidth = (int16)b.readUint32BE();
+			int16 corridorHeight = (int16)b.readUint32BE();
+			_corridorRightX = _corridorLeftX + corridorWidth;
+			_corridorBottomY = _corridorTopY + corridorHeight;
+			debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
+				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+				corridorWidth, corridorHeight);
+		}
+		break;
+
+	case 0x0E:
+		// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
+		// Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
+		if (subSize >= 20) {
+			int16 zoneLeft = (int16)param1;
+			int16 zoneTop = (int16)b.readUint32BE();
+			int16 zoneWidth = (int16)b.readUint32BE();
+			int16 zoneHeight = (int16)b.readUint32BE();
+			int16 zoneRight = zoneLeft + zoneWidth;
+			int16 zoneBottom = zoneTop + zoneHeight;
+			if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
+				_shipPosY > zoneTop && _shipPosY < zoneBottom) {
+				_damageFlags |= 0x10;
+			}
+			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
+				zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
+		}
+		break;
+
+	case 0x0B:
+		// Asteroid/surface per-frame handler (FUN_1CDA7).
+		// field1 = frame counter, field2 = max frames
+		_gameCounter = param1;
+		if (subSize >= 20) {
+			b.readUint32BE(); // field2 (max frames)
+			b.readUint32BE(); // field3
+			b.readUint32BE(); // field4
+		}
+		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+		break;
+
+	case 0x08: case 0x09: case 0x0A:
+	case 0x19: case 0x1A:
+		if (subSize >= 20) {
+			uint32 param2 = b.readUint32BE();
+			uint32 param3 = b.readUint32BE();
+			uint32 param4 = b.readUint32BE();
+			debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
+		}
+		break;
+
+	default:
+		debug(7, "RA1 GAME unknown 0x%02x size=%d", opcode, subSize);
+		break;
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
new file mode 100644
index 00000000000..88ccd5afc24
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -0,0 +1,205 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+#include "common/endian.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/file.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
+	while (height--) {
+		byte *dstNext = dst + pitch;
+		const byte *srcNext = src + 2 + READ_LE_UINT16(src);
+		src += 2;
+		int len = width;
+		byte *d = dst;
+		do {
+			int offs = READ_LE_UINT16(src); src += 2;
+			d += offs;
+			len -= offs;
+			if (len <= 0)
+				break;
+			int w = READ_LE_UINT16(src) + 1; src += 2;
+			len -= w;
+			if (len < 0)
+				w += len;
+			memcpy(d, src, w);
+			src += w;
+			d += w;
+		} while (len > 0);
+		dst = dstNext;
+		src = srcNext;
+	}
+}
+
+// Load an RA1 NUT sprite file (ANIM v1).
+// RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
+// FRME containers. This loader handles that padding properly, unlike the
+// shared NutRenderer::loadFont which assumes even-size chunks.
+bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
+	ScummFile *file = _vm->instantiateScummFile();
+	_vm->openFile(*file, filename);
+	if (!file->isOpen()) {
+		warning("InsaneRebel1::loadRA1Nut: can't open %s", filename);
+		delete file;
+		return false;
+	}
+
+	uint32 tag = file->readUint32BE();
+	if (tag != MKTAG('A','N','I','M')) {
+		warning("InsaneRebel1::loadRA1Nut: no ANIM tag in %s", filename);
+		delete file;
+		return false;
+	}
+	uint32 animSize = file->readUint32BE();
+	byte *data = (byte *)malloc(animSize);
+	file->read(data, animSize);
+	file->close();
+	delete file;
+
+	// data[0..3] = AHDR tag, data[4..7] = AHDR size
+	if (READ_BE_UINT32(data) != MKTAG('A','H','D','R')) {
+		warning("InsaneRebel1::loadRA1Nut: no AHDR in %s", filename);
+		free(data);
+		return false;
+	}
+
+	const uint16 expectedSprites = READ_LE_UINT16(data + 10);
+	bank.numSprites = expectedSprites;
+	bank.sprites = new RA1Sprite[bank.numSprites];
+	memset(bank.sprites, 0, sizeof(RA1Sprite) * bank.numSprites);
+
+	uint32 *fobjOffsets = (uint32 *)calloc(expectedSprites, sizeof(uint32));
+	if (!fobjOffsets) {
+		free(data);
+		return false;
+	}
+
+	// Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
+	uint32 decodedSize = 0;
+	uint16 foundSprites = 0;
+	uint32 chunkOffset = 0;
+	while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
+		uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
+		uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
+		uint32 chunkDataOffset = chunkOffset + 8;
+		uint32 chunkEnd = chunkDataOffset + chunkSize;
+		if (chunkEnd > animSize)
+			break;
+
+		if (chunkTag == MKTAG('F','R','M','E')) {
+			bool foundFobj = false;
+			uint32 subOffset = chunkDataOffset;
+			while (subOffset + 8 <= chunkEnd) {
+				uint32 subTag = READ_BE_UINT32(data + subOffset);
+				uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
+				uint32 subDataOffset = subOffset + 8;
+				uint32 subEnd = subDataOffset + subSize;
+				if (subEnd > chunkEnd)
+					break;
+
+				if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
+					uint16 w = READ_LE_UINT16(data + subOffset + 14);
+					uint16 h = READ_LE_UINT16(data + subOffset + 16);
+					decodedSize += (uint32)w * (uint32)h;
+					fobjOffsets[foundSprites] = subOffset;
+					foundFobj = true;
+					break;
+				}
+
+				subOffset = subEnd;
+				if (subSize & 1)
+					subOffset++;
+			}
+			// Always increment for every FRME to preserve char-to-glyph alignment.
+			// Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
+			foundSprites++;
+		}
+
+		chunkOffset = chunkEnd;
+		if (chunkSize & 1)
+			chunkOffset++;
+	}
+
+	bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
+	bank.decodedSize = decodedSize;
+	byte *decPtr = bank.decodedData;
+
+	// Pass 2: Decode collected FOBJ entries.
+	for (uint16 i = 0; i < foundSprites; i++) {
+		uint32 fobjOffset = fobjOffsets[i];
+		if (fobjOffset == 0) {
+			// Empty FRME (no FOBJ) — leave sprite as blank (zeroed by memset).
+			continue;
+		}
+
+		int codec = READ_LE_UINT16(data + fobjOffset + 8);
+		bank.sprites[i].xoffs = READ_LE_INT16(data + fobjOffset + 10);
+		bank.sprites[i].yoffs = READ_LE_INT16(data + fobjOffset + 12);
+		bank.sprites[i].width = READ_LE_UINT16(data + fobjOffset + 14);
+		bank.sprites[i].height = READ_LE_UINT16(data + fobjOffset + 16);
+
+		int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
+		const byte *fobjData = data + fobjOffset + 22;
+
+		if (codec == 21) {
+			bank.sprites[i].data = decPtr;
+			decodeBomp(decPtr, fobjData, bank.sprites[i].width,
+					   bank.sprites[i].height, bank.sprites[i].width);
+		} else {
+			bank.sprites[i].width = 0;
+			bank.sprites[i].height = 0;
+			bank.sprites[i].data = nullptr;
+			warning("InsaneRebel1::loadRA1Nut: unsupported codec %d in sprite %d", codec, i);
+		}
+
+		decPtr += pixelCount;
+	}
+
+	free(fobjOffsets);
+
+	free(data);
+	debug(1, "InsaneRebel1::loadRA1Nut('%s'): expected=%d found=%d decoded=%d bytes",
+		  filename, expectedSprites, foundSprites, decodedSize);
+	return true;
+}
+
+void InsaneRebel1::loadLevelSprites(int level) {
+	// Ship direction bank — not all levels have one (e.g. Level 2 is first-person)
+	Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
+	if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
+		debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
+	}
+	loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
+
+	// Explosion sprites — try BANG first, then EXPLD
+	Common::String bangFile = Common::String::format("LVL%d/L%dBANG.NUT", level, level);
+	if (!loadRA1Nut(bangFile.c_str(), _bangBank)) {
+		Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
+		loadRA1Nut(expldFile.c_str(), _bangBank);
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
new file mode 100644
index 00000000000..241a5fc0801
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -0,0 +1,267 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+#include "common/events.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+	if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_UP:
+		case Common::KEYCODE_w:
+			_optionsSel = (_optionsSel + 2) % 3;
+			return true;
+		case Common::KEYCODE_DOWN:
+		case Common::KEYCODE_s:
+			_optionsSel = (_optionsSel + 1) % 3;
+			return true;
+		case Common::KEYCODE_RETURN:
+		case Common::KEYCODE_KP_ENTER:
+		case Common::KEYCODE_SPACE:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_ESCAPE:
+			_optionsSel = 2;  // Back
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			break;
+		}
+	}
+
+	if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_UP:
+		case Common::KEYCODE_w:
+			_menuSelection = (_menuSelection + 4) % 5;
+			return true;
+		case Common::KEYCODE_DOWN:
+		case Common::KEYCODE_s:
+			_menuSelection = (_menuSelection + 1) % 5;
+			return true;
+		case Common::KEYCODE_RETURN:
+		case Common::KEYCODE_KP_ENTER:
+		case Common::KEYCODE_SPACE:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_1:
+		case Common::KEYCODE_2:
+		case Common::KEYCODE_3:
+		case Common::KEYCODE_4:
+		case Common::KEYCODE_5:
+			_menuSelection = event.kbd.keycode - Common::KEYCODE_1;
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_ESCAPE:
+			_menuSelection = 4;
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			break;
+		}
+	}
+
+	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+		if (_player) {
+			debug("Rebel1: ESC pressed - skipping video");
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+	_menuFrameCounter++;
+
+	if (_optionsActive) {
+		// --- Options submenu ---
+		static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+
+		const int titleW = getFontBankStringWidth("GAME OPTIONS");
+		drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
+
+		// Build dynamic option strings
+		char diffLine[64];
+		snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+		const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
+		const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
+
+		const int menuY = 60;
+		const int rowH = 16;
+
+		for (int i = 0; i < 3; i++) {
+			const int textW = getFontBankStringWidth(kOptionsItems[i]);
+			const int textX = (width - textW) / 2;
+			const int y = menuY + i * rowH;
+			drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
+
+			if (i == _optionsSel) {
+				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+				int bracketWidth = textW + 12;
+				int leftX = CLIP(textX - 6, 0, width - 1);
+				int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
+				int topY = CLIP(y - 1, 0, height - 1);
+				int bottomY = CLIP(y + rowH - 2, 0, height - 1);
+				for (int x = leftX; x <= rightX; x++) {
+					dst[topY * pitch + x] = highlightColor;
+					dst[bottomY * pitch + x] = highlightColor;
+				}
+				for (int py = topY; py <= bottomY; py++) {
+					dst[py * pitch + leftX] = highlightColor;
+					dst[py * pitch + rightX] = highlightColor;
+				}
+			}
+		}
+		return;
+	}
+
+	// --- Main menu ---
+	static const char *kMenuItems[5] = {
+		"START NEW GAME",
+		"GAME OPTIONS",
+		"ENTER PASSCODE",
+		"CONTINUE DEMO",
+		"EXIT TO DOS"
+	};
+
+	// Center title
+	const int titleW = getFontBankStringWidth("MAIN MENU");
+	const int titleX = (width - titleW) / 2;
+	drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
+
+	// Draw menu items centered horizontally
+	const int menuY = 60;
+	const int rowH = 16;
+
+	for (int i = 0; i < 5; i++) {
+		const int textW = getFontBankStringWidth(kMenuItems[i]);
+		const int textX = (width - textW) / 2;
+		const int y = menuY + i * rowH;
+
+		drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
+
+		// Selection highlight box — flashing border (FUN_004292d0 pattern from RA2)
+		if (i == _menuSelection) {
+			// Flash between two palette colors every 8 frames
+			byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+
+			int bracketWidth = textW + 12;
+			int bracketHeight = rowH;
+			int leftX = textX - 6;
+			int rightX = leftX + bracketWidth;
+			int topY = y - 1;
+			int bottomY = y + bracketHeight - 2;
+
+			// Clamp
+			if (leftX < 0) leftX = 0;
+			if (rightX >= width) rightX = width - 1;
+			if (topY < 0) topY = 0;
+			if (bottomY >= height) bottomY = height - 1;
+
+			// Draw rectangle border (4 lines)
+			for (int x = leftX; x <= rightX && x < width; x++) {
+				if (topY >= 0 && topY < height)
+					dst[topY * pitch + x] = highlightColor;
+				if (bottomY >= 0 && bottomY < height)
+					dst[bottomY * pitch + x] = highlightColor;
+			}
+			for (int py = topY; py <= bottomY && py < height; py++) {
+				if (leftX >= 0 && leftX < width)
+					dst[py * pitch + leftX] = highlightColor;
+				if (rightX >= 0 && rightX < width)
+					dst[py * pitch + rightX] = highlightColor;
+			}
+		}
+	}
+}
+
+int InsaneRebel1::runMainMenu() {
+	debug(1, "InsaneRebel1: Main menu");
+
+	_menuSelection = 0;
+	while (!_vm->shouldQuit()) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+
+		if (_vm->shouldQuit())
+			return 5;
+
+		if (_menuConfirmed)
+			return _menuSelection + 1;
+	}
+
+	return 5;
+}
+
+void InsaneRebel1::runOptionsMenu() {
+	_optionsSel = 0;
+	_optionsActive = true;
+
+	while (!_vm->shouldQuit()) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+
+		if (_vm->shouldQuit())
+			break;
+
+		if (_menuConfirmed) {
+			switch (_optionsSel) {
+			case 0:
+				// Cycle difficulty
+				_difficulty = (_difficulty + 1) % 3;
+				loadTuningForLevel(0);
+				break;
+			case 1:
+				// Toggle turbulence
+				_turbulenceEnabled = !_turbulenceEnabled;
+				break;
+			case 2:
+				// Back to main menu
+				_optionsActive = false;
+				return;
+			}
+		}
+	}
+	_optionsActive = false;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
new file mode 100644
index 00000000000..5e71af01770
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -0,0 +1,406 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+}
+
+void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	int32 setupsan13, int32 curFrame, int32 maxFrame) {
+	if (_menuActive && renderBitmap) {
+		int width = _player ? _player->_width : 0;
+		int height = _player ? _player->_height : 0;
+		if (width == 0) width = _screenWidth;
+		if (height == 0) height = _screenHeight;
+		int pitch = width;
+		renderMainMenuOverlay(renderBitmap, pitch, width, height);
+	}
+
+	if (!_interactiveVideoActive || !renderBitmap)
+		return;
+
+	int width = _player->_width;
+	int height = _player->_height;
+	if (width == 0) width = _screenWidth;
+	if (height == 0) height = _screenHeight;
+	int pitch = width;
+
+	if (_currentLevel == 1) {
+		// Level 2: first-person asteroid dodge — no ship sprite, input averaging physics
+		updateAsteroidPhysics();
+	} else {
+		// Level 1 (and others): third-person ship flight with accumulators
+		if (_shipBank.numSprites > 0) {
+			updateShipPhysics();
+			renderShip(renderBitmap, pitch, width, height);
+		}
+	}
+	renderExplosions(renderBitmap, pitch, width, height);
+	renderCrosshair(renderBitmap, pitch, width, height);
+	renderHUD(renderBitmap, pitch, width, height);
+}
+
+void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
+	int cx = _vm->_mouse.x;
+	int cy = _vm->_mouse.y;
+
+	// Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
+	// Palette index 15 = (255,255,255) white, used as outline for visibility.
+	const byte colorInner = 119;
+	const byte colorOutline = 15;
+	const int size = 7;  // arm length
+
+	// Helper lambda to draw a pixel with outline
+	auto drawPx = [&](int x, int y, byte c) {
+		if (x >= 0 && x < width && y >= 0 && y < height)
+			dst[y * pitch + x] = c;
+	};
+
+	// Draw outline first (1px border around each arm pixel)
+	for (int d = -size; d <= size; d++) {
+		if (d >= -1 && d <= 1) continue; // skip center area for outline
+		// Horizontal arm outline
+		drawPx(cx + d, cy - 1, colorOutline);
+		drawPx(cx + d, cy + 1, colorOutline);
+		// Vertical arm outline
+		drawPx(cx - 1, cy + d, colorOutline);
+		drawPx(cx + 1, cy + d, colorOutline);
+	}
+	// Arm endpoints
+	drawPx(cx - size - 1, cy, colorOutline);
+	drawPx(cx + size + 1, cy, colorOutline);
+	drawPx(cx, cy - size - 1, colorOutline);
+	drawPx(cx, cy + size + 1, colorOutline);
+
+	// Draw red cross arms
+	for (int d = -size; d <= size; d++) {
+		if (d == 0) continue; // gap at center
+		drawPx(cx + d, cy, colorInner);  // horizontal
+		drawPx(cx, cy + d, colorInner);  // vertical
+	}
+	// Center dot
+	drawPx(cx, cy, colorInner);
+}
+
+void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
+	if (!dst || !text || _hudFontBank.numSprites <= 0)
+		return;
+
+	for (int i = 0; text[i] != '\0'; i++) {
+		const byte ch = (byte)text[i];
+
+		if (ch == ' ') {
+			x += 6;
+			continue;
+		}
+
+		// RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
+		if (ch < 0x21) {
+			x += 4;
+			continue;
+		}
+		const int fontIdx = (int)ch - 0x21;
+		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+			x += 4;
+			continue;
+		}
+
+		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+		const int gw = glyph.width;
+		const int gh = glyph.height;
+		const int gx = x + glyph.xoffs;
+		const int gy = y + glyph.yoffs;
+		const uint64 glyphPixels = (uint64)gw * (uint64)gh;
+		if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
+			x += 4;
+			continue;
+		}
+		if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
+			x += 4;
+			continue;
+		}
+		const byte *bankStart = _hudFontBank.decodedData;
+		const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
+		if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
+			x += 4;
+			continue;
+		}
+
+		for (int py = 0; py < gh; py++) {
+			const int sy = gy + py;
+			if (sy < 0 || sy >= height)
+				continue;
+			for (int px = 0; px < gw; px++) {
+				const int sx = gx + px;
+				if (sx < 0 || sx >= width)
+					continue;
+				const byte pixel = glyph.data[py * gw + px];
+				if (pixel != 0)
+					dst[sy * pitch + sx] = pixel;
+			}
+		}
+
+		x += gw > 0 ? gw : 4;
+	}
+}
+
+// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
+// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
+int InsaneRebel1::getFontBankStringWidth(const char *text) {
+	if (!text || _hudFontBank.numSprites <= 0)
+		return 0;
+
+	int w = 0;
+	for (int i = 0; text[i] != '\0'; i++) {
+		const byte ch = (byte)text[i];
+		if (ch == ' ') {
+			w += 6;
+			continue;
+		}
+		if (ch < 0x21) {
+			w += 4;
+			continue;
+		}
+		const int fontIdx = (int)ch - 0x21;
+		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
+			w += 4;
+			continue;
+		}
+		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
+		w += glyph.width > 0 ? glyph.width : 4;
+	}
+	return w;
+}
+
+void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
+	// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
+	// Hidden during last 20 frames of death sequence (deathTimer 20→0)
+	if (_health < 0 && _deathTimer <= 20)
+		return;
+
+	if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
+		return;
+
+	const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+
+	// Position: game coords → screen coords via perspective transform
+	// Adapted from RA2's renderHandler7Ship:
+	//   shipCenterX = (shipX - center) + perspX + screenCenterX
+	int drawX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX - spr.width / 2;
+	int drawY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY - spr.height / 2;
+
+	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+}
+
+// Render explosion sprites during damage cooldown and death sequence.
+// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
+void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
+	if (_bangBank.numSprites <= 0)
+		return;
+
+	// Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
+	int shipScreenX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX;
+	int shipScreenY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY;
+
+	// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
+	// When dead and deathTimer > 10: random explosion sprites scatter around ship
+	if (_health < 0 && _deathTimer > 10) {
+		int intensity = _deathTimer - 10;  // 20→1 as timer goes 30→11
+		if (intensity > 10)
+			intensity = 20 - intensity;     // Triangle: 0→10→0
+
+		// di = intensity * 4 + 1 (vertical scatter range)
+		// si = -20 + intensity * 4 (horizontal scatter range, DAT_75d8 is 0)
+		int rangeY = intensity * 4 + 1;
+		int rangeX = -20 + intensity * 4;
+		if (rangeX < 1) rangeX = 1;
+
+		for (int i = 0; i < intensity; i++) {
+			// Random sprite from bang bank (FUN_21db0(10))
+			int sprIdx = _vm->_rnd.getRandomNumber(_bangBank.numSprites - 1);
+
+			// Random position around ship (matching assembly random scatter)
+			int randX = (int)_vm->_rnd.getRandomNumber(rangeX * 2) - rangeX;
+			int randY = (int)_vm->_rnd.getRandomNumber(rangeY * 2) - rangeY;
+
+			int drawX = shipScreenX + randX;
+			int drawY = shipScreenY + randY;
+
+			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+			renderSprite(dst, pitch, width, height,
+				drawX - spr.width / 2, drawY - spr.height / 2, spr);
+		}
+		return;
+	}
+
+	// --- Damage hit explosion (FUN_1DEB5 LAB_1e185) ---
+	// When alive, in cooldown, and bang bank loaded
+	if (_health >= 0 && _damageCooldown > 0) {
+		// Sprite index = 10 - damageCooldown (frames 0→9 as cooldown 10→1)
+		int sprIdx = _bangBank.numSprites - _damageCooldown;
+		if (sprIdx < 0 || sprIdx >= _bangBank.numSprites)
+			return;
+
+		// Position at ship center (DAT_75d8 is always 0 in RA1)
+		int drawX = shipScreenX;
+		int drawY = shipScreenY;
+
+		const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+		renderSprite(dst, pitch, width, height,
+			drawX - spr.width / 2, drawY - spr.height / 2, spr);
+	}
+}
+
+// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
+// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
+void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
+	if (_displayBank.numSprites == 0)
+		return;
+
+	// Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
+	if (_score / 10000 > _prevScore / 10000) {
+		_lives++;
+	}
+	_prevScore = _score;
+
+	const RA1Sprite &bar = _displayBank.sprites[0];
+
+	// DISPLAY.NUT sprite is 320×19 at xoffs=0, yoffs=176 in the original game.
+	// Video FOBJs fill the full 384×242 buffer from (0,0), so use sprite offsets directly.
+	int hudX = bar.xoffs;
+	int hudY = bar.yoffs;
+
+	// Draw the status bar background with transparency (pixel 0 = transparent)
+	if (bar.data && bar.width > 0 && bar.height > 0) {
+		int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
+		int srcOffX = 0, srcOffY = 0;
+		if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
+		if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
+		if (drawX + drawW > width) drawW = width - drawX;
+		if (drawY + drawH > height) drawH = height - drawY;
+
+		for (int iy = 0; iy < drawH; iy++) {
+			const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
+			byte *d = dst + (drawY + iy) * pitch + drawX;
+			for (int ix = 0; ix < drawW; ix++) {
+				byte px = s[ix];
+				if (px != 0)
+					d[ix] = px;
+			}
+		}
+
+		debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
+			hudX, hudY, bar.width, bar.height);
+	}
+
+	// Draw health bar from FUN_1BBCB behavior.
+	// Original logic uses current health as fill width and computes x as (0x92 - health),
+	// so the bar is right-anchored and shrinks from left to right as damage increases.
+	{
+		int barMaxW = kMaxHealth;
+		int barH = 5;
+		int healthWidth = CLIP<int16>(_health, 0, kMaxHealth);
+		int barX = hudX + (0x92 - healthWidth);
+		int barY = hudY + 8;
+		int fillW = CLIP(healthWidth, 0, barMaxW);
+
+		// Color based on damage level (matching original thresholds from FUN_1BBCB)
+		// Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
+		byte barColor;
+		if (_health > _tuning.shot * 2)
+			barColor = 0xD5;  // Green (0,192,0) — low damage
+		else if (_health > _tuning.wham * 2)
+			barColor = 0x63;  // Yellow (255,255,31) — moderate damage
+		else
+			barColor = 0xDD;  // Red (192,0,0) — critical
+
+		// Flash effect on damage
+		if (_screenFlash > 0)
+			barColor = 0xFF;  // White flash
+
+		for (int iy = 0; iy < barH && barY + iy < height; iy++) {
+			byte *d = dst + (barY + iy) * pitch + barX;
+			for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
+				d[ix] = barColor;
+			}
+		}
+	}
+
+	// Lives: black out excess pilot icons embedded in DISPLAY.NUT background.
+	// Original FUN_1BBCB: FUN_21D66(buf, lives*10+186, 6, 51-lives*10, 9, 0, 320)
+	// Icons are 5 slots at x=186..236, each ~10px wide. Cover unused slots with black.
+	if (_lives >= 0 && _lives < 5) {
+		int coverX = hudX + _lives * 10 + 186;
+		int coverY = hudY + 6;
+		int coverW = 51 - _lives * 10;
+		int coverH = 9;
+		if (coverX >= 0 && coverY >= 0) {
+			for (int iy = 0; iy < coverH && coverY + iy < height; iy++) {
+				byte *d = dst + (coverY + iy) * pitch + coverX;
+				for (int ix = 0; ix < coverW && coverX + ix < width; ix++) {
+					d[ix] = 0x00;
+				}
+			}
+		}
+	}
+
+	// Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
+	if (_hudFontBank.numSprites > 0) {
+		char scoreStr[16];
+		Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
+		drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
+	}
+
+}
+
+void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
+								int x, int y, const RA1Sprite &spr) {
+	if (!spr.data || spr.width <= 0 || spr.height <= 0)
+		return;
+
+	int drawX = x, drawY = y, drawW = spr.width, drawH = spr.height;
+	int srcOffsetX = 0, srcOffsetY = 0;
+
+	if (drawX < 0) { srcOffsetX = -drawX; drawW += drawX; drawX = 0; }
+	if (drawY < 0) { srcOffsetY = -drawY; drawH += drawY; drawY = 0; }
+	if (drawX + drawW > width) drawW = width - drawX;
+	if (drawY + drawH > height) drawH = height - drawY;
+	if (drawW <= 0 || drawH <= 0)
+		return;
+
+	for (int iy = 0; iy < drawH; iy++) {
+		const byte *s = spr.data + (srcOffsetY + iy) * spr.width + srcOffsetX;
+		byte *d = dst + (drawY + iy) * pitch + drawX;
+		for (int ix = 0; ix < drawW; ix++) {
+			byte px = s[ix];
+			if (px != 0)
+				d[ix] = px;
+		}
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
new file mode 100644
index 00000000000..a150cd2bddb
--- /dev/null
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -0,0 +1,342 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/system.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+// ---------------------------------------------------------------------------
+// Game flow (matching original at 0x15597)
+// ---------------------------------------------------------------------------
+
+// Play a passive cinematic (no game callback, skippable).
+// Reuses RA2's pattern: reset handler, set cinematic flags, play video.
+// startFrame > 0: fast-forward (decode without display/audio) to that frame.
+void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
+	debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
+	SmushPlayer *splayer = _vm->_splayer;
+	_player = splayer;
+	_interactiveVideoActive = false;
+	_vm->_smushVideoShouldFinish = false;
+	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve
+	if (startFrame > 0)
+		splayer->setFastForwardToFrame(startFrame);
+	splayer->play(filename, 12);
+}
+
+void InsaneRebel1::clearVideoBuffer() {
+	if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
+		return;
+
+	const int pixelCount = _vm->_screenWidth * _vm->_screenHeight;
+	byte *clearBuffer = (byte *)calloc(pixelCount, 1);
+	if (!clearBuffer)
+		return;
+
+	if (_vm->_macScreen) {
+		_vm->mac_drawBufferToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	} else {
+		_vm->_system->copyRectToScreen(clearBuffer, _vm->_screenWidth, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
+	}
+	_vm->_system->updateScreen();
+
+	free(clearBuffer);
+}
+
+// Intro sequence (0x155ef-0x158f8):
+//   1. O1LOGO.ANM — LucasArts logo
+//   2. O1OPEN.ANM — Star Wars opening crawl
+void InsaneRebel1::playIntroSequence() {
+	debug(1, "InsaneRebel1: Playing intro sequence");
+
+	// LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
+	playCinematic("OPEN/O1LOGO.ANM");
+	if (_vm->shouldQuit())
+		return;
+	clearVideoBuffer();
+
+	// Star Wars opening crawl (original: PUSH 0x5800, CALL FUN_1BA32)
+	playCinematic("OPEN/O1OPEN.ANM");
+}
+
+// Main menu on O1OPTION.ANM background (0x15968).
+// Original renders text overlay with 5 menu items via FUN_21F7A.
+// For now, we play the menu video as a passive cinematic (non-interactive)
+// and return "Start New Game" immediately.
+
+// Level 1 flow (0x16100-0x167A2, from disassembly):
+//   1. Load NUTs (L1BANK1, L1BANK2, L1EXPLD, L1BANG, L1LASER)
+//   2. L1HANGAR.ANM — Full hangar departure cutscene (782 frames, flags 0x0420)
+//   3. L1CU1.ANM — Pre-flight cutscene (flags 0x0400)
+//   4. L1PLAY1L.ANM — Stage 1 flight, hard/left path (788 frames)
+//      At frame 394, if player steers right → L1PLAY1R (easy path, 396 frames)
+//   5. L1CU2.ANM — Mid-level cutscene
+//   6. L1PLAY2.ANM — Stage 2 turret
+//      If score < 5 (0x75D0): L1RETRY → retry Stage 2
+//   7. L1END.ANM — Level complete
+//   Death (health<0): L1CRASHA/B → lives check:
+//     lives>0: L1NEW → jump back to Stage 1 (skip L1HANGAR/L1CU1)
+//     lives==0: L1DEATH → return to menu
+
+bool InsaneRebel1::runLevel1() {
+	debug(1, "InsaneRebel1: Running level 1");
+
+	// Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
+	loadLevelSprites(1);
+
+	// L1HANGAR.ANM — Hangar departure (original: 0x5918, flags 0x0420)
+	// Plays once at level start, never replayed on retry.
+	playCinematic("LVL1/L1HANGAR.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	// L1CU1.ANM — Pre-flight cutscene (original: 0x5944, flags 0x0400)
+	// Plays once at level start, never replayed on retry.
+	playCinematic("LVL1/L1CU1.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	// Retry loop — on death with lives, L1NEW plays then jumps back here
+	while (!_vm->shouldQuit()) {
+		// Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_pathBranchEnabled = true;
+		_rightPathSelected = false;
+
+		// Stage 1 flight — L1PLAY1L (hard/left path)
+		// The first 394 frames are the common section. At counter 394, if
+		// ship is right of center, we switch to L1PLAY1R (easy path).
+		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_rightPathSelected && _health >= 0) {
+			debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
+			_pathBranchEnabled = false;
+			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+			if (_vm->shouldQuit())
+				return false;
+		}
+		_pathBranchEnabled = false;
+
+		if (_health >= 0) {
+			// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
+			playCinematic("LVL1/L1CU2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
+			playInteractiveVideo("LVL1/L1PLAY2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			// TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
+			// If score < 5: L1RETRY → retry Stage 2
+
+			// L1END.ANM — Level complete! (original: 0x59a3)
+			playCinematic("LVL1/L1END.ANM");
+			return true;
+		}
+
+		// Death sequence (original: 0x165dd-0x166bb)
+		// Random crash variant A or B
+		if (_vm->_rnd.getRandomNumber(1) == 0)
+			playCinematic("LVL1/L1CRASHA.ANM");
+		else
+			playCinematic("LVL1/L1CRASHB.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		// Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
+		_lives--;
+		if (_lives <= 0) {
+			// Game over — L1DEATH then return (original: 0x166C0)
+			playCinematic("LVL1/L1DEATH.ANM");
+			debug(1, "InsaneRebel1: Game over (no lives left)");
+			return false;
+		}
+
+		// Lives remaining — L1NEW briefing then retry (original: 0x16675)
+		playCinematic("LVL1/L1NEW.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		// Loop back to gameplay (original: JMP 0x16214 — health reset + Stage 1)
+	}
+
+	return false;
+}
+
+// Level 2: Asteroid Field Training
+// Flow: L2NEW → L2INTRO → L2PLAY (interactive) → L2END/L2DEATH
+bool InsaneRebel1::runLevel2() {
+	debug(1, "InsaneRebel1: Running level 2");
+
+	_currentLevel = 1;
+	loadLevelSprites(2);
+	loadTuningForLevel(1);
+
+	// L2INTRO.ANM — intro cutscene (481 frames)
+	playCinematic("LVL2/L2INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	// Retry loop
+	while (!_vm->shouldQuit()) {
+		// Reset state for this attempt
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		// L2PLAY.ANM — asteroid dodge (800 frames, interactive)
+		playInteractiveVideo("LVL2/L2PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			// Level complete!
+			playCinematic("LVL2/L2END.ANM");
+			return true;
+		}
+
+		// Death
+		playCinematic("LVL2/L2DEATH.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		_lives--;
+		if (_lives <= 0) {
+			debug(1, "InsaneRebel1: Game over (no lives left)");
+			return false;
+		}
+
+		// Retry briefing
+		playCinematic("LVL2/L2NEW.ANM");
+		if (_vm->shouldQuit())
+			return false;
+	}
+
+	return false;
+}
+
+// Main game entry point — called from ScummEngine::go().
+// Matches original flow at 0x15597: intro → menu → level.
+void InsaneRebel1::runGame() {
+	// Play intro sequence (logo + opening)
+	playIntroSequence();
+	if (_vm->shouldQuit())
+		return;
+
+	// Main menu → gameplay loop
+	while (!_vm->shouldQuit()) {
+		int menuResult = runMainMenu();
+		if (_vm->shouldQuit())
+			return;
+
+		switch (menuResult) {
+		case 1: {
+#if 0 // Skip level 1 for testing — jump straight to level 2
+			// Start New Game — play L1NEW briefing then level 1
+			playCinematic("LVL1/L1NEW.ANM");
+			if (_vm->shouldQuit())
+				return;
+
+			bool completed = runLevel1();
+#else
+			bool completed = true;
+#endif
+			if (completed && !_vm->shouldQuit()) {
+				completed = runLevel2();
+				if (completed) {
+					debug(1, "InsaneRebel1: Level 2 completed!");
+					// TODO: Continue to level 3
+				}
+			}
+			_currentLevel = 0;
+			// Return to menu after level ends
+			break;
+		}
+		case 2:
+			// Game Options
+			runOptionsMenu();
+			break;
+		case 5:
+			// Exit
+			return;
+		default:
+			// Passcode, Demo — not yet implemented, return to menu
+			break;
+		}
+	}
+}
+
+// Play interactive gameplay video (with ship physics + HUD).
+void InsaneRebel1::playInteractiveVideo(const char *filename) {
+	debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
+
+	// Stop any leftover audio from previous video
+	terminateAudio();
+	initAudio(_audioSampleRate);
+
+	SmushPlayer *splayer = _vm->_splayer;
+	_player = splayer;
+	clearBit(0);
+	_interactiveVideoActive = true;
+	_vm->_smushVideoShouldFinish = false;
+	splayer->setCurVideoFlags(0x28);
+
+	// Center mouse, hide system cursor (we draw our own), lock mouse to window
+	smush_warpMouse(160, 100, -1);
+	CursorMan.showMouse(false);
+	g_system->lockMouse(true);
+
+	splayer->play(filename, 12);
+	_interactiveVideoActive = false;
+
+	g_system->lockMouse(false);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel.cpp b/engines/scumm/insane/insane_rebel2.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel.cpp
rename to engines/scumm/insane/insane_rebel2.cpp
index 4496daf950e..afa0f3ddfcb 100644
--- a/engines/scumm/insane/insane_rebel.cpp
+++ b/engines/scumm/insane/insane_rebel2.cpp
@@ -43,7 +43,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 #include "common/config-manager.h"
 #include "audio/audiostream.h"
diff --git a/engines/scumm/insane/insane_rebel.h b/engines/scumm/insane/insane_rebel2.h
similarity index 99%
rename from engines/scumm/insane/insane_rebel.h
rename to engines/scumm/insane/insane_rebel2.h
index 88d20f74101..a8fac1ae4d5 100644
--- a/engines/scumm/insane/insane_rebel.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -19,8 +19,8 @@
  *
  */
 
-#if !defined(SCUMM_INSANE_REBEL_H) && defined(ENABLE_SCUMM_7_8)
-#define SCUMM_INSANE_REBEL_H
+#if !defined(SCUMM_INSANE_REBEL2_H) && defined(ENABLE_SCUMM_7_8)
+#define SCUMM_INSANE_REBEL2_H
 
 #include "scumm/nut_renderer.h"
 
diff --git a/engines/scumm/insane/insane_rebel_audio.cpp b/engines/scumm/insane/insane_rebel2_audio.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_audio.cpp
rename to engines/scumm/insane/insane_rebel2_audio.cpp
index a063144f40b..3341bc18f72 100644
--- a/engines/scumm/insane/insane_rebel_audio.cpp
+++ b/engines/scumm/insane/insane_rebel2_audio.cpp
@@ -26,7 +26,7 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 #include "audio/audiostream.h"
 #include "audio/decoders/raw.h"
diff --git a/engines/scumm/insane/insane_rebel_iact.cpp b/engines/scumm/insane/insane_rebel2_iact.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_iact.cpp
rename to engines/scumm/insane/insane_rebel2_iact.cpp
index 47f818bd034..28552eb5f28 100644
--- a/engines/scumm/insane/insane_rebel_iact.cpp
+++ b/engines/scumm/insane/insane_rebel2_iact.cpp
@@ -28,7 +28,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel_levels.cpp b/engines/scumm/insane/insane_rebel2_levels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_levels.cpp
rename to engines/scumm/insane/insane_rebel2_levels.cpp
index 1b61f13b70a..83d4f7f03fa 100644
--- a/engines/scumm/insane/insane_rebel_levels.cpp
+++ b/engines/scumm/insane/insane_rebel2_levels.cpp
@@ -27,7 +27,7 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel_menu.cpp b/engines/scumm/insane/insane_rebel2_menu.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_menu.cpp
rename to engines/scumm/insane/insane_rebel2_menu.cpp
index 9bbd6115991..9ffbfa8ce55 100644
--- a/engines/scumm/insane/insane_rebel_menu.cpp
+++ b/engines/scumm/insane/insane_rebel2_menu.cpp
@@ -35,7 +35,7 @@
 #include "scumm/smush/smush_font.h"
 #include "scumm/smush/smush_multi_font.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel_render.cpp b/engines/scumm/insane/insane_rebel2_render.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_render.cpp
rename to engines/scumm/insane/insane_rebel2_render.cpp
index 143f72db23c..77a07000f1d 100644
--- a/engines/scumm/insane/insane_rebel_render.cpp
+++ b/engines/scumm/insane/insane_rebel2_render.cpp
@@ -31,7 +31,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel_runlevels.cpp b/engines/scumm/insane/insane_rebel2_runlevels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel_runlevels.cpp
rename to engines/scumm/insane/insane_rebel2_runlevels.cpp
index 3071ee93dac..a48dbc5b8c7 100644
--- a/engines/scumm/insane/insane_rebel_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel2_runlevels.cpp
@@ -25,7 +25,7 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 9ab40bf738f..2011a5049f7 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,14 +133,20 @@ MODULE_OBJS += \
 	insane/insane_enemy.o \
 	insane/insane_scenes.o \
 	insane/insane_iact.o \
-	insane/insane_rebel.o \
 	insane/insane_rebel1.o \
-	insane/insane_rebel_audio.o \
-	insane/insane_rebel_iact.o \
-	insane/insane_rebel_levels.o \
-	insane/insane_rebel_menu.o \
-	insane/insane_rebel_render.o \
-	insane/insane_rebel_runlevels.o \
+	insane/insane_rebel1_audio.o \
+	insane/insane_rebel1_iact.o \
+	insane/insane_rebel1_levels.o \
+	insane/insane_rebel1_menu.o \
+	insane/insane_rebel1_render.o \
+	insane/insane_rebel1_runlevels.o \
+	insane/insane_rebel2.o \
+	insane/insane_rebel2_audio.o \
+	insane/insane_rebel2_iact.o \
+	insane/insane_rebel2_levels.o \
+	insane/insane_rebel2_menu.o \
+	insane/insane_rebel2_render.o \
+	insane/insane_rebel2_runlevels.o \
 	smush/codec1.o \
 	smush/codec20.o \
 	smush/codec37.o \
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index f0e8b348048..aac942458f6 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -52,7 +52,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/players/player_towns.h"
 #include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 #include "scumm/insane/insane_rebel1.h"
 #include "scumm/he/animation_he.h"
 #include "scumm/he/font_he.h"
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 491d1751804..0321e192c7d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -43,7 +43,7 @@
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 #include "scumm/insane/insane_rebel1.h"
 
 #include "audio/audiostream.h"
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 07f4a0500dc..82a10ae4f13 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -36,7 +36,7 @@
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel.h"
+#include "scumm/insane/insane_rebel2.h"
 
 namespace Scumm {
 


Commit: f9aa62e2bc525c13632455158b51cb37f3c56b3b
    https://github.com/scummvm/scummvm/commit/f9aa62e2bc525c13632455158b51cb37f3c56b3b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00

Commit Message:
SCUMM: RA2: Parse level 2 video chunks correctly

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_levels.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 0a7f24ecc72..3cef6a9895a 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -131,6 +131,21 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_optionsActive = false;
 	_optionsSel = 0;
 	_turbulenceEnabled = false;
+
+	// Shooting/targeting state
+	_playerFired = false;
+	_fireCooldown = 0;
+	memset(_shotSlots, 0, sizeof(_shotSlots));
+	_shotAlternator = 0;
+	_targetProximity = 0;
+	_prevTargetProx = 0;
+	_targetCount = 0;
+	_prevTargetCount = 0;
+	memset(_gostSlots, 0, sizeof(_gostSlots));
+	_gostSlotIdx = 0;
+	_killCount = 0;
+	_lastHitTarget = 0;
+
 	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
 		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
 	} else if (loadRA1Nut("SYS/TECHFONT.NUT", _hudFontBank)) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 941f27998ee..14ceee76413 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -116,10 +116,17 @@ private:
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
 	void renderExplosions(byte *dst, int pitch, int width, int height);
-	void renderCrosshair(byte *dst, int pitch, int width, int height);
+	void renderTargeting(byte *dst, int pitch, int width, int height);
+	void renderGostSlots(byte *dst, int pitch, int width, int height);
+	void renderLaserShots(byte *dst, int pitch, int width, int height);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 
+	// Shooting pipeline — FUN_1CCA0 (0x1CCA0) shot spawner,
+	// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
+	void processShot();
+	void checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
+
 	// Audio
 	void initAudio(int sampleRate);
 	void terminateAudio();
@@ -136,6 +143,7 @@ private:
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
 	RA1SpriteBank _hudFontBank;   // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
 	RA1SpriteBank _bangBank;      // LxBANG.NUT — impact/explosion sprites (10 frames)
+	RA1SpriteBank _laserBank;     // LxLASER.NUT — laser/shot effect sprites
 	SmushFont *_menuFont;         // Use engine text renderer for correct TALKFONT character mapping
 
 	// RA1 screen dimensions (384x242)
@@ -257,6 +265,42 @@ private:
 	int _optionsSel;         // 0=difficulty, 1=turbulence, 2=back
 
 	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
+
+	// Shooting state — FUN_1CCA0 (0x1CCA0)
+	bool _playerFired;       // 0x7570: fire button pressed this frame
+	int16 _fireCooldown;     // 0x757C: frames until next shot allowed
+
+	// Explosion shot slots (2 slots) — FUN_1CCA0 (0x1CCA0)
+	static const int kMaxShotSlots = 2;
+	struct ShotSlot {
+		int16 timer;     // 0x75E6: countdown (5 or 2, 0=inactive)
+		int16 posX;      // 0x75F2: cursor X at time of shot
+		int16 posY;      // 0x75F6: cursor Y at time of shot
+		int16 centerX;   // 0x75EA: perspective-adjusted X
+		int16 centerY;   // 0x75EE: perspective-adjusted Y
+	};
+	ShotSlot _shotSlots[kMaxShotSlots];
+	int16 _shotAlternator;   // 0x241F: alternates between 0/1
+
+	// Targeting state — FUN_1C0EF (0x1C0EF)
+	int16 _targetProximity;  // 0x7558: 0=none, 1=near, 2=on-target
+	int16 _prevTargetProx;   // 0x755A: previous frame's proximity
+	int16 _targetCount;      // 0x7552: active targets this frame
+	int16 _prevTargetCount;  // 0x7554: previous frame target count
+
+	// GOST hit animation slots (10 slots) — FUN_1C9CD (0x1C9CD)
+	static const int kMaxGostSlots = 10;
+	struct GostSlot {
+		int16 targetId;  // 0x23C3: target identifier (0=empty)
+		int16 frame;     // 0x23D7: animation frame (0-9, >=10 = done)
+		int16 posX;      // 0x239B: screen X
+		int16 posY;      // 0x23AF: screen Y
+	};
+	GostSlot _gostSlots[kMaxGostSlots];
+	int16 _gostSlotIdx;      // 0x23EB: next slot to write (circular 0-9)
+
+	int16 _killCount;        // 0x75D0: targets destroyed this stage
+	int16 _lastHitTarget;    // 0x75D6: prevents double-hit on same target
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 7500706e548..9e0acecf064 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,7 +27,7 @@
 
 namespace Scumm {
 
-// Ship physics matching FUN_1DEB5 (accumulator-based position system).
+// updateShipPhysics — FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
 // Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
 // driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
 void InsaneRebel1::updateShipPhysics() {
@@ -242,11 +242,19 @@ void InsaneRebel1::updateShipPhysics() {
 		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
-// Level 2+ asteroid/surface physics (FUN_1CDA7).
+// updateAsteroidPhysics — FUN_1CDA7 (0x1CDA7). Opcode 0x0B handler.
 // Uses 10-frame input history averaging instead of accumulators.
 // Ship position = averaged input + center offset.
 // Viewport = second history buffer for smooth camera scrolling.
 void InsaneRebel1::updateAsteroidPhysics() {
+	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
+	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
+	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
+	if (_gameLatch5D == 0xFFFF)
+		_damageFlags |= 0x40;
+	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+		_damageFlags |= 0x80;
+
 	// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
 	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
 		_health++;
@@ -265,9 +273,14 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		if (_health < 0) {
 			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
 		}
+		_prevDamageFlags = _damageFlags;
 		_damageFlags = 0;
 	}
 
+	// Latches are frame-local event inputs in the original pipeline.
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+
 	// Death fade countdown
 	if (_deathTimer > 1 && _health < 0) {
 		_deathTimer--;
@@ -278,55 +291,27 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		_screenFlash--;
 	}
 
-	// Input history shift and averaging (FUN_1CDA7 lines 107-131)
-	// Shift history: [9] = [8], [8] = [7], ... [1] = [0], [0] = current input
-	for (int i = kInputHistorySize - 1; i > 0; i--) {
-		_inputHistoryX[i] = _inputHistoryX[i - 1];
-		_inputHistoryY[i] = _inputHistoryY[i - 1];
-	}
+	// --- Cursor position: direct mouse mapping (like RA2 first-person levels) ---
+	// RA2 crosshair = _vm->_mouse.x/y directly. RA1's original (FUN_1CDA7) averaged
+	// 10 frames of DOS mouse deltas; with ScummVM absolute coords, use direct mapping.
+	_shipPosX = CLIP<int16>((int16)_vm->_mouse.x, 0, 319);
+	_shipPosY = CLIP<int16>((int16)_vm->_mouse.y, 0, 199);
 
-	// Read current mouse input (mapped to -160..+160 range)
-	int mouseX = 0, mouseY = 0;
-	if (_vm->_mouse.x >= 0) {
-		mouseX = CLIP<int>(_vm->_mouse.x - kRA1CenterX, -kRA1CenterX, kRA1CenterX);
-		mouseY = CLIP<int>(_vm->_mouse.y - kRA1CenterY, -kRA1CenterY, kRA1CenterY);
-	}
-	_inputHistoryX[0] = (int16)mouseX;
-	_inputHistoryY[0] = (int16)mouseY;
-
-	// Average over 10 frames
-	int16 sumX = 0, sumY = 0;
-	for (int i = 0; i < kInputHistorySize; i++) {
-		sumX += _inputHistoryX[i];
-		sumY -= _inputHistoryY[i]; // original negates Y: sVar5 = sVar5 - history[i]
-	}
-	_avgInputX = (int16)(sumX / kInputHistorySize);
-	_avgInputY = (int16)(sumY / kInputHistorySize);
-
-	// Clamp (original: [-0xA0, 0xA0] horizontal, [-0x46, 0x41] vertical)
-	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
-	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
-
-	// Ship position = average + center offset
-	// Original: _74BE = _75A8 + 0xA0, _74C0 = _75BC + 0x46
-	_shipPosX = _avgInputX + 0xA0;
-	_shipPosY = _avgInputY + 0x46;
-
-	// Viewport history shift and averaging (FUN_1CDA7 lines 134-157)
+	// Viewport parallax: smoothed offset from center for background scrolling
+	int16 mouseOffX = (int16)(_vm->_mouse.x - kRA1CenterX);
+	int16 mouseOffY = (int16)(_vm->_mouse.y - kRA1CenterY);
 	for (int i = kInputHistorySize - 1; i > 0; i--) {
 		_viewHistoryX[i] = _viewHistoryX[i - 1];
 		_viewHistoryY[i] = _viewHistoryY[i - 1];
 	}
-	_viewHistoryX[0] = _avgInputX;
-	_viewHistoryY[0] = _avgInputY;
+	_viewHistoryX[0] = mouseOffX;
+	_viewHistoryY[0] = mouseOffY;
 
 	int16 viewSumX = 0, viewSumY = 0;
 	for (int i = 0; i < kInputHistorySize; i++) {
 		viewSumX += _viewHistoryX[i];
 		viewSumY += _viewHistoryY[i];
 	}
-	// Original: _74B6 = (viewAvgX >> 1) + 0x20, clamped [0, 0x40]
-	// Original: _74B8 = (viewAvgY >> 1) + 0x17, clamped [0, 0x2E]
 	_perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
 
@@ -346,7 +331,8 @@ void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
 void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 }
 
-// Parse RA1 GAME chunks.
+// handleGameChunk — FUN_1BE1B (0x1BE1B). Central GAME opcode dispatcher.
+// Reads 7x32-bit BE integers from GAME chunk, routes to per-opcode handlers.
 void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 8)
 		return;
@@ -377,6 +363,20 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_avgInputX = 0;
 		_avgInputY = 0;
 
+		// Shooting/targeting reset
+		_playerFired = false;
+		_fireCooldown = 0;
+		memset(_shotSlots, 0, sizeof(_shotSlots));
+		_shotAlternator = 0;
+		_targetProximity = 0;
+		_prevTargetProx = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		memset(_gostSlots, 0, sizeof(_gostSlots));
+		_gostSlotIdx = 0;
+		_killCount = 0;
+		_lastHitTarget = 0;
+
 		// Field1 == 0 corresponds to baseline recenter behavior in the original.
 		if ((int32)param1 == 0) {
 			_shipPosX = kRA1CenterX;
@@ -452,20 +452,47 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		// field1 = frame counter, field2 = max frames
 		_gameCounter = param1;
 		if (subSize >= 20) {
-			b.readUint32BE(); // field2 (max frames)
+			uint32 maxFrames = b.readUint32BE(); // field2 (max frames)
 			b.readUint32BE(); // field3
 			b.readUint32BE(); // field4
+
+			// RA1 scripts drive progression with GAME counters. In LVL2, finish the
+			// interactive SMUSH once the script counter reaches the terminal frame.
+			// This avoids getting stuck if container/frame parsing continues past the
+			// intended gameplay endpoint.
+			if (_interactiveVideoActive && _currentLevel == 1 && maxFrames > 0 &&
+				_gameCounter >= (int32)maxFrames - 1) {
+				_vm->_smushVideoShouldFinish = true;
+				debug(1, "RA1 L2: finishing interactive video at GAME 0x0B counter=%d/%u", _gameCounter, maxFrames);
+			}
 		}
 		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
 		break;
 
+	case 0x5A:
+		// Target detection — FUN_1C0EF (0x1C0EF). AABB from video stream.
+		// Params: targetIdx, left, top, width, height
+		if (subSize >= 24) {
+			int16 targetIdx = (int16)param1;
+			int16 left = (int16)b.readUint32BE();
+			int16 top = (int16)b.readUint32BE();
+			int16 w = (int16)b.readUint32BE();
+			int16 h = (int16)b.readUint32BE();
+			int16 right = left + w;
+			int16 bottom = top + h;
+			checkTargetHit(targetIdx, left, top, right, bottom);
+			debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
+				targetIdx, left, top, right, bottom, _targetProximity);
+		}
+		break;
+
 	case 0x08: case 0x09: case 0x0A:
 	case 0x19: case 0x1A:
 		if (subSize >= 20) {
 			uint32 param2 = b.readUint32BE();
 			uint32 param3 = b.readUint32BE();
 			uint32 param4 = b.readUint32BE();
-			debug(7, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
+			debug(5, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
 		}
 		break;
 
@@ -475,4 +502,91 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	}
 }
 
+// processShot — FUN_1CCA0 (0x1CCA0). Spawns shot into explosion slot when fired.
+// Called once per frame during interactive rendering.
+void InsaneRebel1::processShot() {
+	if (!_playerFired || _fireCooldown > 0) {
+		_playerFired = false;
+		if (_fireCooldown > 0)
+			_fireCooldown--;
+		return;
+	}
+
+	// Find an available slot (timer <= 0 or timer > 5)
+	int slot = -1;
+	for (int i = 0; i < kMaxShotSlots; i++) {
+		if (_shotSlots[i].timer <= 0 || _shotSlots[i].timer > 5) {
+			slot = i;
+			break;
+		}
+	}
+	if (slot < 0) {
+		_playerFired = false;
+		return;
+	}
+
+	// Record shot at current cursor position
+	_shotSlots[slot].timer = 5;  // 5 frames active (original: uVar1 = 5 or 2 based on DAT_75FF)
+	_shotSlots[slot].posX = _shipPosX;
+	_shotSlots[slot].posY = _shipPosY;
+	_shotSlots[slot].centerX = _perspectiveX + (_shipPosX - kRA1CenterX) + kRA1CenterX;
+	_shotSlots[slot].centerY = _perspectiveY + (_shipPosY - kRA1CenterY) + kRA1CenterY;
+
+	_fireCooldown = 3;  // Minimum frames between shots
+	_playerFired = false;
+
+	debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
+}
+
+// checkTargetHit — FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
+// Called from GAME 0x5A handler. Checks cursor proximity and shot hits.
+void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
+	int16 snap = _tuning.snap;
+	int16 curX = _shipPosX;
+	int16 curY = _shipPosY;
+
+	_targetCount++;
+
+	// Check proximity: cursor within target + snap + 5 margin
+	if (curX > left - snap - 5 && curX < right + snap + 5 &&
+		curY > top - snap - 5 && curY < bottom + snap + 5) {
+		if (_targetProximity == 0)
+			_targetProximity = 1;  // Near
+
+		// Check tight lock: cursor within target + snap (no extra margin)
+		if (curX > left - snap && curX < right + snap &&
+			curY > top - snap && curY < bottom + snap) {
+			_targetProximity = 2;  // On-target
+
+			// Check if any active shot slot hits this target
+			if (_lastHitTarget != targetIdx + 1) {
+				for (int i = 0; i < kMaxShotSlots; i++) {
+					if (_shotSlots[i].timer == 1) {  // Shot in final frame = impact
+						// Hit! Record in GOST slot for explosion animation
+						int gi = _gostSlotIdx;
+						_gostSlots[gi].targetId = targetIdx + 1;
+						_gostSlots[gi].frame = 0;
+						_gostSlots[gi].posX = (left + right) / 2;
+						_gostSlots[gi].posY = (top + bottom) / 2;
+						_gostSlotIdx = (_gostSlotIdx + 1) % kMaxGostSlots;
+
+						_lastHitTarget = targetIdx + 1;
+						_score += _tuning.kill;
+						_killCount++;
+
+						// Snap cursor to target center (original: _DAT_74BE/74C0 = target center)
+						if (snap > 0) {
+							_shipPosX = (left + right) / 2;
+							_shipPosY = (top + bottom) / 2;
+						}
+
+						debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
+						return;
+					}
+				}
+			}
+		}
+	}
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 88ccd5afc24..9fe8e6e58aa 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -110,7 +110,6 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 			break;
 
 		if (chunkTag == MKTAG('F','R','M','E')) {
-			bool foundFobj = false;
 			uint32 subOffset = chunkDataOffset;
 			while (subOffset + 8 <= chunkEnd) {
 				uint32 subTag = READ_BE_UINT32(data + subOffset);
@@ -125,7 +124,6 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 					uint16 h = READ_LE_UINT16(data + subOffset + 16);
 					decodedSize += (uint32)w * (uint32)h;
 					fobjOffsets[foundSprites] = subOffset;
-					foundFobj = true;
 					break;
 				}
 
@@ -200,6 +198,10 @@ void InsaneRebel1::loadLevelSprites(int level) {
 		Common::String expldFile = Common::String::format("LVL%d/L%dEXPLD.NUT", level, level);
 		loadRA1Nut(expldFile.c_str(), _bangBank);
 	}
+
+	// Laser/shot effect sprites
+	Common::String laserFile = Common::String::format("LVL%d/L%dLASER.NUT", level, level);
+	loadRA1Nut(laserFile.c_str(), _laserBank);
 }
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 241a5fc0801..f4e4a034eb3 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -90,6 +90,15 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		}
 	}
 
+	// Shooting: mouse button during interactive gameplay — FUN_1CCA0 (0x1CCA0)
+	if (_interactiveVideoActive && !_menuActive) {
+		if (event.type == Common::EVENT_LBUTTONDOWN) {
+			if (_currentLevel != 1)
+				_playerFired = true;
+			return true;
+		}
+	}
+
 	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 		if (_player) {
 			debug("Rebel1: ESC pressed - skipping video");
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 5e71af01770..131a796c726 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -22,11 +22,21 @@
 #include "common/system.h"
 
 #include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
 #include "scumm/insane/insane_rebel1.h"
 
 namespace Scumm {
 
+// procPreRendering — Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
+// For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+	if (_interactiveVideoActive && _player) {
+		_player->_ra1ViewportOffsetX = _perspectiveX;
+		_player->_ra1ViewportOffsetY = _perspectiveY;
+	} else if (_player) {
+		_player->_ra1ViewportOffsetX = 0;
+		_player->_ra1ViewportOffsetY = 0;
+	}
 }
 
 void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
@@ -59,53 +69,135 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			renderShip(renderBitmap, pitch, width, height);
 		}
 	}
-	renderExplosions(renderBitmap, pitch, width, height);
-	renderCrosshair(renderBitmap, pitch, width, height);
+
+	// Shooting/targeting pipeline applies to ship/turret gameplay.
+	// LVL2 (opcode 0x0B asteroid dodge) is avoidance-only.
+	if (_currentLevel != 1) {
+		processShot();
+		for (int i = 0; i < kMaxShotSlots; i++) {
+			if (_shotSlots[i].timer > 0)
+				_shotSlots[i].timer--;
+		}
+
+		renderLaserShots(renderBitmap, pitch, width, height);
+		renderExplosions(renderBitmap, pitch, width, height);
+		renderGostSlots(renderBitmap, pitch, width, height);
+		renderTargeting(renderBitmap, pitch, width, height);
+	}
 	renderHUD(renderBitmap, pitch, width, height);
 }
 
-void InsaneRebel1::renderCrosshair(byte *dst, int pitch, int width, int height) {
-	int cx = _vm->_mouse.x;
-	int cy = _vm->_mouse.y;
-
-	// Palette index 119 = (255,0,0) pure red in L2PLAY.ANM palette.
-	// Palette index 15 = (255,255,255) white, used as outline for visibility.
-	const byte colorInner = 119;
-	const byte colorOutline = 15;
-	const int size = 7;  // arm length
-
-	// Helper lambda to draw a pixel with outline
-	auto drawPx = [&](int x, int y, byte c) {
-		if (x >= 0 && x < width && y >= 0 && y < height)
-			dst[y * pitch + x] = c;
-	};
-
-	// Draw outline first (1px border around each arm pixel)
-	for (int d = -size; d <= size; d++) {
-		if (d >= -1 && d <= 1) continue; // skip center area for outline
-		// Horizontal arm outline
-		drawPx(cx + d, cy - 1, colorOutline);
-		drawPx(cx + d, cy + 1, colorOutline);
-		// Vertical arm outline
-		drawPx(cx - 1, cy + d, colorOutline);
-		drawPx(cx + 1, cy + d, colorOutline);
+// renderTargeting — FUN_1CB22 (0x1CB22). Targeting/lock-on crosshair at cursor position.
+// Draws a crosshair that changes color based on target proximity:
+//   0 (none) = dim grey, 1 (near) = yellow, 2 (on-target) = bright red
+void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
+	int cx = _shipPosX;
+	int cy = _shipPosY;
+
+	// Color based on proximity state
+	// Palette indices chosen for L2PLAY visibility:
+	//   0xD7 = bright green (0,255,0), 0x63 = yellow (255,255,31), 0xDD = red (192,0,0)
+	byte color;
+	if (_targetProximity >= 2)
+		color = 0xDD;  // Red — on target
+	else if (_targetProximity == 1)
+		color = 0x63;  // Yellow — near
+	else
+		color = 0xD7;  // Green — no target
+
+	// Animated crosshair: size pulses with frame counter
+	int armLen = 6 + ((_frameCounter >> 2) & 3);
+	int gap = 2;  // Gap around center
+
+	// Horizontal arms
+	for (int i = gap; i <= armLen; i++) {
+		int lx = cx - i, rx = cx + i;
+		if (cy >= 0 && cy < height) {
+			if (lx >= 0 && lx < width)
+				dst[cy * pitch + lx] = color;
+			if (rx >= 0 && rx < width)
+				dst[cy * pitch + rx] = color;
+		}
+	}
+	// Vertical arms
+	for (int i = gap; i <= armLen; i++) {
+		int uy = cy - i, dy = cy + i;
+		if (cx >= 0 && cx < width) {
+			if (uy >= 0 && uy < height)
+				dst[uy * pitch + cx] = color;
+			if (dy >= 0 && dy < height)
+				dst[dy * pitch + cx] = color;
+		}
 	}
-	// Arm endpoints
-	drawPx(cx - size - 1, cy, colorOutline);
-	drawPx(cx + size + 1, cy, colorOutline);
-	drawPx(cx, cy - size - 1, colorOutline);
-	drawPx(cx, cy + size + 1, colorOutline);
-
-	// Draw red cross arms
-	for (int d = -size; d <= size; d++) {
-		if (d == 0) continue; // gap at center
-		drawPx(cx + d, cy, colorInner);  // horizontal
-		drawPx(cx, cy + d, colorInner);  // vertical
+
+	// Center dot for on-target
+	if (_targetProximity >= 2 && cx >= 0 && cx < width && cy >= 0 && cy < height) {
+		dst[cy * pitch + cx] = color;
+	}
+
+	// Save previous proximity for next frame
+	_prevTargetProx = _targetProximity;
+	_targetProximity = 0;
+
+	// Reset per-frame target count — FUN_1C940 (0x1C940)
+	_prevTargetCount = _targetCount;
+	_targetCount = 0;
+	_lastHitTarget = 0;
+}
+
+// renderGostSlots — FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
+// Renders explosion sprites from bangBank at each GOST slot's recorded position.
+void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
+	if (_bangBank.numSprites <= 0)
+		return;
+
+	for (int i = 0; i < kMaxGostSlots; i++) {
+		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
+			int sprIdx = _gostSlots[i].frame;
+			if (sprIdx >= _bangBank.numSprites)
+				sprIdx = _bangBank.numSprites - 1;
+
+			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+			int drawX = _gostSlots[i].posX - spr.width / 2;
+			int drawY = _gostSlots[i].posY - spr.height / 2;
+			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+
+			_gostSlots[i].frame++;
+			if (_gostSlots[i].frame >= 10)
+				_gostSlots[i].targetId = 0;  // Animation complete
+		}
+	}
+}
+
+// renderLaserShots — FUN_1CCA0 visual output. Renders laser sprites from _laserBank
+// when shot slots are active (timer > 0). Each set of 5 sprites is a laser animation
+// converging from the ship toward center screen. Timer 5→1 maps to sprite frames 0→4.
+void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
+	if (_laserBank.numSprites <= 0)
+		return;
+
+	int spritesPerSet = 5;
+	for (int i = 0; i < kMaxShotSlots; i++) {
+		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
+			// Frame index: timer 5→1 maps to sprite 0→4
+			int frame = spritesPerSet - _shotSlots[i].timer;
+			// Alternate between sprite sets for left/right shots
+			int setOffset = (i % 3) * spritesPerSet;
+			int sprIdx = setOffset + frame;
+			if (sprIdx >= _laserBank.numSprites)
+				continue;
+
+			const RA1Sprite &spr = _laserBank.sprites[sprIdx];
+			// LASER sprites have embedded positions relative to screen center
+			int drawX = spr.xoffs + _perspectiveX;
+			int drawY = spr.yoffs + _perspectiveY;
+			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+		}
 	}
-	// Center dot
-	drawPx(cx, cy, colorInner);
 }
 
+// drawFontBankString — Simplified version of FUN_221B7 (0x221B7).
+// Original is a multi-font markup-capable renderer; this uses a single font bank.
 void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
 	if (!dst || !text || _hudFontBank.numSprites <= 0)
 		return;
@@ -168,8 +260,7 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
 	}
 }
 
-// getFontBankStringWidth -- Measure pixel width of a string using the HUD font bank.
-// Matches the pre-pass width calculation in the original drawString (FUN_221B7).
+// getFontBankStringWidth — Pre-pass width calculation from FUN_221B7 (0x221B7).
 int InsaneRebel1::getFontBankStringWidth(const char *text) {
 	if (!text || _hudFontBank.numSprites <= 0)
 		return 0;
@@ -196,6 +287,8 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
 	return w;
 }
 
+// renderShip — Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
+// Also used by FUN_1E6A7 (0x1E6A7) turret handler via FUN_20BD3.
 void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 	// From FUN_1DEB5 LAB_1e2b2: ship drawn when health >= 0 OR deathTimer > 20
 	// Hidden during last 20 frames of death sequence (deathTimer 20→0)
@@ -216,8 +309,8 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 }
 
-// Render explosion sprites during damage cooldown and death sequence.
-// From FUN_1DEB5 at LAB_1e185 (damage hit) and LAB_1e0e3 (death shake).
+// renderExplosions — Explosion sprites from FUN_1DEB5 (0x1DEB5) LAB_1e185 (damage hit)
+// and LAB_1e0e3 (death shake). See also FUN_1CCA0 (0x1CCA0) explosion spawner.
 void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height) {
 	if (_bangBank.numSprites <= 0)
 		return;
@@ -275,7 +368,7 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 	}
 }
 
-// Render bottom status bar from DISPLAY.NUT with dynamic damage bar and score.
+// renderHUD — FUN_1BBCB (0x1BBCB). Status bar from DISPLAY.NUT with health bar and score.
 // Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
 void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	if (_displayBank.numSprites == 0)
@@ -377,6 +470,8 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 
 }
 
+// renderSprite — Simplified version of FUN_20BD3 (0x20BD3) glyph/sprite renderer.
+// Original dispatches through full codec pipeline; this does flat pixel blit with transparency.
 void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
 								int x, int y, const RA1Sprite &spr) {
 	if (!spr.data || spr.width <= 0 || spr.height <= 0)
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index a150cd2bddb..ad0ca095540 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -230,6 +230,7 @@ bool InsaneRebel1::runLevel2() {
 		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
 		_avgInputX = 0;
 		_avgInputY = 0;
+		_killCount = 0;
 
 		// L2PLAY.ANM — asteroid dodge (800 frames, interactive)
 		playInteractiveVideo("LVL2/L2PLAY.ANM");
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0321e192c7d..ff01333384f 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -390,6 +390,17 @@ void SmushPlayer::release() {
 	} else {
 		free(_frameBuffer);
 		_frameBuffer = nullptr;
+		if (isRA1()) {
+			free(_storedFobjData);
+			_storedFobjData = nullptr;
+			_storedFobjDataSize = 0;
+			_storedFobjCodec = 0;
+			_storedFobjParm2 = 0;
+			_storedFobjLeft = 0;
+			_storedFobjTop = 0;
+			_storedFobjWidth = 0;
+			_storedFobjHeight = 0;
+		}
 	}
 
 	_IACTstream = nullptr;
@@ -422,7 +433,10 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
 
 void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
-	assert(subSize >= 6);
+	if (isRA1())
+		assert(subSize >= 4);
+	else
+		assert(subSize >= 6);
 
 	if (isRA2()) {
 		ra2HandleFetch(b);
@@ -430,8 +444,37 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	}
 
 	if (isRA1()) {
-		debug("RA1 FTCH: frame=%d _frameBuffer=%p _dst=%p _width=%d _height=%d",
-			_frame, (void*)_frameBuffer, (void*)_dst, _width, _height);
+		// RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
+		// Chunk layout: BE32 id/flags, BE32 fetchX, BE32 fetchY.
+		uint32 fetchId = 0;
+		int32 fetchX = 0;
+		int32 fetchY = 0;
+		if (subSize >= 4)
+			fetchId = b.readUint32BE();
+		if (subSize >= 12) {
+			fetchX = b.readSint32BE();
+			fetchY = b.readSint32BE();
+		}
+
+		if (_storedFobjData != nullptr && _storedFobjDataSize > 0) {
+			const int storedCodec = _storedFobjCodec & 0xFF;
+			const uint8 storedParam = (uint8)((_storedFobjCodec >> 8) & 0xFF);
+			int left = _storedFobjLeft + fetchX;
+			int top = _storedFobjTop + fetchY;
+
+			// Apply the same interactive viewport offset used for regular FOBJ chunks.
+			left -= _ra1ViewportOffsetX;
+			top -= _ra1ViewportOffsetY;
+
+			debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
+				_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
+			decodeFrameObject(storedCodec, _storedFobjData, left, top,
+				_storedFobjWidth, _storedFobjHeight, _storedFobjDataSize,
+				storedParam, _storedFobjParm2);
+		} else {
+			debug("RA1 FTCH: frame=%d id=0x%08x with no stored FOBJ data", _frame, fetchId);
+		}
+		return;
 	}
 
 	if (_frameBuffer != nullptr) {
@@ -1259,7 +1302,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 			bottomNonZero, bottomTotal, bottomTotal > 0 ? (bottomNonZero * 100) / bottomTotal : 0);
 	}
 
-	if (_storeFrame && !isRA2()) {
+	if (_storeFrame && !isRA1() && !isRA2()) {
 		if (_frameBuffer == nullptr) {
 			_frameBuffer = (byte *)malloc(_width * _height);
 		}
@@ -1315,6 +1358,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	}
 	int left = (int)b.readSint16LE();
 	int top = (int)b.readSint16LE();
+	const int rawLeft = left;
+	const int rawTop = top;
 	int width = b.readUint16LE();
 	int height = b.readUint16LE();
 
@@ -1329,6 +1374,14 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 			_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
 	}
 	if (isRA1()) {
+		// Viewport scroll for interactive gameplay (FUN_224FD sets _41A0/_41A2).
+		// Original renders FOBJs at raw positions, then displays a 320x200 window
+		// starting at (perspectiveX, perspectiveY) within the 384x242 buffer.
+		// FUN_22605/FUN_2289D subtract _41A0 from FOBJ X coords during rendering.
+		// We simulate the display window shift by subtracting from decode position.
+		left -= _ra1ViewportOffsetX;
+		top -= _ra1ViewportOffsetY;
+
 		debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
 			_frame, codec, left, top, width, height, subSize - 14, _storeFrame);
 	}
@@ -1345,6 +1398,23 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	if (_storeFrame && isRA2()) {
 		ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
 	}
+	if (_storeFrame && isRA1()) {
+		free(_storedFobjData);
+		_storedFobjData = (byte *)malloc(chunk_size);
+		if (_storedFobjData != nullptr) {
+			memcpy(_storedFobjData, chunk_buffer, chunk_size);
+			_storedFobjDataSize = chunk_size;
+			_storedFobjCodec = codec | ((int)ra1Param << 8);
+			_storedFobjParm2 = ra1Parm2;
+			_storedFobjLeft = rawLeft;
+			_storedFobjTop = rawTop;
+			_storedFobjWidth = width;
+			_storedFobjHeight = height;
+		} else {
+			_storedFobjDataSize = 0;
+		}
+		_storeFrame = false;
+	}
 
 	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
 
@@ -1372,30 +1442,38 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	}
 
 	while (frameSize > 0) {
-		const int32 chunkStart = b.pos();
+		// RA1 parser exits when <=1 byte remains in a frame (FUN_1FDBC).
+		// Treat any tiny tail as frame trailer/padding and stop cleanly.
+		if (isRA1() && frameSize <= 1) {
+			if (frameSize == 1)
+				b.skip(1);
+			break;
+		}
+
+		// RA1: Top-of-loop alignment check matching original assembly FUN_1FDBC:
+		// if ((ptr & 1) && (*ptr == 0)) { ptr++; remaining--; }
+		if (isRA1() && (b.pos() & 1) && frameSize > 0) {
+			byte peek = b.readByte();
+			if (peek == 0) {
+				frameSize--;
+			} else {
+				b.seek(-1, SEEK_CUR);
+			}
+		}
+
+		if (frameSize < 8) {
+			b.skip(frameSize);
+			break;
+		}
+
 		uint32 subType = b.readUint32BE();
 		int32 subSize = b.readUint32BE();
 		int32 subOffset = b.pos();
 
-		// RA1 workaround: some frames have missing padding bytes after
-		// odd-sized sub-chunks (authoring tool bug in 4 frames of L2PLAY.ANM).
-		// If the tag looks invalid, back up 1 byte and retry.
-		if (isRA1() && frameSize > 8) {
-			byte b0 = (subType >> 24) & 0xFF;
-			byte b1 = (subType >> 16) & 0xFF;
-			byte b2 = (subType >> 8) & 0xFF;
-			byte b3 = subType & 0xFF;
-			bool validTag = (b0 >= 0x20 && b0 <= 0x7E) && (b1 >= 0x20 && b1 <= 0x7E) &&
-			                (b2 >= 0x20 && b2 <= 0x7E) && (b3 >= 0x00 && b3 <= 0x7E);
-			if (!validTag) {
-				// Try 1 byte earlier (missing padding byte)
-				b.seek(chunkStart - 1, SEEK_SET);
-				subType = b.readUint32BE();
-				subSize = b.readUint32BE();
-				subOffset = b.pos();
-				frameSize++;
-				debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: RA1 realigned to %s at frame %d", tag2str(subType), _frame);
-			}
+		// Guard against consuming the next frame marker as an in-frame chunk.
+		if (isRA1() && subType == MKTAG('F','R','M','E')) {
+			b.seek(-8, SEEK_CUR);
+			break;
 		}
 
 		// RA2: When _skipNext is set, skip the NEXT chunk of ANY type
@@ -1457,7 +1535,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleLoad(subSize, b);
 			break;
 		case MKTAG('G','O','S','T'):
-			if (isRA2())
+			if (isRA2() || isRA1())
 				ra2HandleGost(subSize, b);
 			break;
 		// RA1-specific chunk types: skip gracefully
@@ -1562,12 +1640,25 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			}
 			break;
 		default:
+			if (isRA1()) {
+				// Original FUN_1FDBC lines 163-168: unknown tag with all uppercase
+				// letters (A-Z) → silently return. Otherwise error.
+				byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
+				byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
+				if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
+				    tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
+					debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
+					frameSize = 0;
+					continue;
+				}
+			}
 			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 		}
 
 		frameSize -= subSize + 8;
 		b.seek(subOffset + subSize, SEEK_SET);
-		if (subSize & 1) {
+		// RA1 uses top-of-loop alignment (matching FUN_1FDBC), not bottom-of-loop padding.
+		if (!isRA1() && (subSize & 1)) {
 			b.skip(1);
 			frameSize--;
 		}
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 1d63e11295e..6a10be52e04 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -165,6 +165,7 @@ private:
 	byte *_storedFobjData;
 	int32 _storedFobjDataSize;
 	int _storedFobjCodec;
+	uint16 _storedFobjParm2; // RA1: FOBJ bytes[12..13] needed by codec 4/5
 	int _storedFobjLeft;
 	int _storedFobjTop;
 	int _storedFobjWidth;
@@ -180,6 +181,11 @@ private:
 	int _ra1ObjOverlayWidth;
 	int _ra1ObjOverlayHeight;
 
+	// RA1: Viewport scroll offset for interactive gameplay (FUN_224FD at 0x224FD).
+	// Set by InsaneRebel1::procPreRendering(), applied to FOBJ decode positions.
+	int _ra1ViewportOffsetX;
+	int _ra1ViewportOffsetY;
+
 	// RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
 	// to re-render the same sprite payload at a different position.
 	byte *_lastFobjData;
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 82a10ae4f13..e19f4b51a76 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -61,6 +61,7 @@ void SmushPlayer::ra2InitFields() {
 	_storedFobjData = nullptr;
 	_storedFobjDataSize = 0;
 	_storedFobjCodec = 0;
+	_storedFobjParm2 = 0;
 	_storedFobjLeft = 0;
 	_storedFobjTop = 0;
 	_storedFobjWidth = 0;
@@ -80,6 +81,8 @@ void SmushPlayer::ra2InitFields() {
 	_ra1ObjOverlayTop = 0;
 	_ra1ObjOverlayWidth = 0;
 	_ra1ObjOverlayHeight = 0;
+	_ra1ViewportOffsetX = 0;
+	_ra1ViewportOffsetY = 0;
 	_skipNext = false;
 	_ra2FastForwarding = false;
 	_fobjOffsetX = 0;
@@ -150,6 +153,7 @@ void SmushPlayer::ra2ReleaseVideo() {
 	free(_storedFobjData);
 	_storedFobjData = nullptr;
 	_storedFobjDataSize = 0;
+	_storedFobjParm2 = 0;
 	free(_lastFobjData);
 	_lastFobjData = nullptr;
 	_lastFobjDataSize = 0;
@@ -403,6 +407,7 @@ void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
 	memcpy(_storedFobjData, data, dataSize);
 	_storedFobjDataSize = dataSize;
 	_storedFobjCodec = codec;
+	_storedFobjParm2 = 0;
 	_storedFobjLeft = left;
 	_storedFobjTop = top;
 	_storedFobjWidth = width;


Commit: 0aa073970104a5bce20ee059ff7644a20735b6e7
    https://github.com/scummvm/scummvm/commit/0aa073970104a5bce20ee059ff7644a20735b6e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00

Commit Message:
SCUMM: RA1: Add level selection and level 2 pointer handling

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 3cef6a9895a..64c5c4a7b6f 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -130,6 +130,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_menuFrameCounter = 0;
 	_optionsActive = false;
 	_optionsSel = 0;
+	_levelSelectActive = false;
+	_levelSelectSel = 0;
+	_startLevel = 1;
 	_turbulenceEnabled = false;
 
 	// Shooting/targeting state
@@ -154,6 +157,16 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		warning("InsaneRebel1: failed to load RA1 HUD font bank (TECHFONT/TALKFONT)");
 	}
 
+	// FUN_1CB22 uses "<<" layer markers that resolve to TECHFONT in the original.
+	// Keep a dedicated TECH font bank for targeting markers/lock indicators.
+	if (loadRA1Nut("SYS/TECHFONT.NUT", _techFontBank)) {
+		debug(1, "InsaneRebel1: targeting glyph font loaded from SYS/TECHFONT.NUT (%d chars)", _techFontBank.numSprites);
+	} else if (loadRA1Nut("SYS/TALKFONT.NUT", _techFontBank)) {
+		debug(1, "InsaneRebel1: targeting glyph font fallback loaded from SYS/TALKFONT.NUT (%d chars)", _techFontBank.numSprites);
+	} else {
+		warning("InsaneRebel1: failed to load targeting font bank (TECHFONT/TALKFONT)");
+	}
+
 	// Audio
 	initAudio(11025);
 
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 14ceee76413..be8cdb994de 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -93,8 +93,9 @@ private:
 	void clearVideoBuffer();
 
 	// Main menu loop on O1OPTION.ANM background (0x15968)
-	// Returns: 1=Start New Game, 2=Game Options, 3=Enter Passcode, 4=Continue Demo, 5=Exit
+	// Returns: 1=Start New Game, 2=Game Options, 3=Level Select, 4=Continue Demo, 5=Exit
 	int runMainMenu();
+	void runLevelSelectMenu();
 
 	// Level 1 flow (0x16100): hangar → CU1 → gameplay → CU2 → turret → end
 	// Returns true if level completed, false if player quit
@@ -142,6 +143,7 @@ private:
 	RA1SpriteBank _shipBank;
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
 	RA1SpriteBank _hudFontBank;   // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
+	RA1SpriteBank _techFontBank;  // SYS/TECHFONT.NUT — targeting glyph layer ("<<" markers)
 	RA1SpriteBank _bangBank;      // LxBANG.NUT — impact/explosion sprites (10 frames)
 	RA1SpriteBank _laserBank;     // LxLASER.NUT — laser/shot effect sprites
 	SmushFont *_menuFont;         // Use engine text renderer for correct TALKFONT character mapping
@@ -263,12 +265,15 @@ private:
 	// Options submenu state
 	bool _optionsActive;     // True when showing options instead of main menu
 	int _optionsSel;         // 0=difficulty, 1=turbulence, 2=back
+	bool _levelSelectActive; // True when showing level-select submenu
+	int _levelSelectSel;     // 0=Level1, 1=Level2, 2=Back
+	int _startLevel;         // 1-based start level for "Start New Game"
 
 	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
 
 	// Shooting state — FUN_1CCA0 (0x1CCA0)
 	bool _playerFired;       // 0x7570: fire button pressed this frame
-	int16 _fireCooldown;     // 0x757C: frames until next shot allowed
+	int16 _fireCooldown;     // 0x757C: button-edge gate in original input pipeline
 
 	// Explosion shot slots (2 slots) — FUN_1CCA0 (0x1CCA0)
 	static const int kMaxShotSlots = 2;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 9e0acecf064..fd52033ab8a 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -38,9 +38,16 @@ void InsaneRebel1::updateShipPhysics() {
 		_damageCooldown--;
 
 	// --- Step 1: Mouse input as offset from screen center ---
-	// Original: _DAT_756C (horizontal), _DAT_756E (vertical)
-	int16 inputX = (int16)(_vm->_mouse.x - 160);
-	int16 inputY = (int16)(_vm->_mouse.y - 100);
+	// Original: _DAT_756C (horizontal), _DAT_756E (vertical) in 320x200 space.
+	int16 mouseX = (int16)_vm->_mouse.x;
+	int16 mouseY = (int16)_vm->_mouse.y;
+	if (_player && _player->_width > 0 && _player->_height > 0 &&
+		(mouseX >= 320 || mouseY >= 200)) {
+		mouseX = (int16)((mouseX * 320) / _player->_width);
+		mouseY = (int16)((mouseY * 200) / _player->_height);
+	}
+	int16 inputX = (int16)(mouseX - 160);
+	int16 inputY = (int16)(mouseY - 100);
 	inputX = CLIP<int16>(inputX, -127, 127);
 	inputY = CLIP<int16>(inputY, -127, 127);
 
@@ -132,25 +139,11 @@ void InsaneRebel1::updateShipPhysics() {
 	}
 
 	// --- Step 8: Perspective offsets ---
-	{
-		int absOffX = ABS(_shipPosX - kRA1CenterX);
-		if (absOffX > 0)
-			_perspectiveX = (int16)((kRA1FocalX * kRA1CenterX * absOffX) /
-				((kRA1CenterX - kRA1FocalX) * absOffX + kRA1FocalX * kRA1CenterX));
-		else
-			_perspectiveX = 0;
-		if (_shipPosX < kRA1CenterX + 1)
-			_perspectiveX = -_perspectiveX;
-
-		int absOffY = ABS(_shipPosY - kRA1CenterY);
-		if (absOffY > 0)
-			_perspectiveY = (int16)((kRA1FocalY * kRA1CenterY * absOffY) /
-				((kRA1CenterY - kRA1FocalY) * absOffY + kRA1FocalY * kRA1CenterY));
-		else
-			_perspectiveY = 0;
-		if (_shipPosY < kRA1CenterY + 1)
-			_perspectiveY = -_perspectiveY;
-	}
+	// FUN_1DEB5 computes these linearly from ship offsets:
+	//   viewX = clamp((_74BA + 0x20), 0, 0x40)
+	//   viewY = clamp((_74BC + 0x17), 0, 0x2E)
+	_perspectiveX = CLIP<int16>((int16)(_shipPosX - kRA1CenterX + 0x20), 0, 0x40);
+	_perspectiveY = CLIP<int16>((int16)(_shipPosY - kRA1CenterY + 0x17), 0, 0x2E);
 
 	// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
 	// Horizontal component from _74CA (rollAccum):
@@ -291,29 +284,60 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		_screenFlash--;
 	}
 
-	// --- Cursor position: direct mouse mapping (like RA2 first-person levels) ---
-	// RA2 crosshair = _vm->_mouse.x/y directly. RA1's original (FUN_1CDA7) averaged
-	// 10 frames of DOS mouse deltas; with ScummVM absolute coords, use direct mapping.
-	_shipPosX = CLIP<int16>((int16)_vm->_mouse.x, 0, 319);
-	_shipPosY = CLIP<int16>((int16)_vm->_mouse.y, 0, 199);
+	// --- Cursor and perspective smoothing (FUN_1CDA7) ---
+	// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
+	int16 mouseX = (int16)_vm->_mouse.x;
+	int16 mouseY = (int16)_vm->_mouse.y;
+	if (_player && _player->_width > 0 && _player->_height > 0 &&
+		(mouseX >= 320 || mouseY >= 200)) {
+		mouseX = (int16)((mouseX * 320) / _player->_width);
+		mouseY = (int16)((mouseY * 200) / _player->_height);
+	}
+	int16 inputX = (int16)(mouseX - kRA1CenterX);
+	int16 inputY = (int16)(mouseY - kRA1CenterY);
+	inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
+	inputY = CLIP<int16>(inputY, -100, 100);
+
+	for (int i = kInputHistorySize - 1; i > 0; i--) {
+		_inputHistoryX[i] = _inputHistoryX[i - 1];
+		_inputHistoryY[i] = _inputHistoryY[i - 1];
+	}
+	_inputHistoryX[0] = inputX;
+	_inputHistoryY[0] = inputY;
+
+	int sumInputX = 0;
+	int sumInputY = 0;
+	for (int i = 0; i < kInputHistorySize; i++) {
+		sumInputX += _inputHistoryX[i];
+		sumInputY += _inputHistoryY[i];
+	}
+
+	_avgInputX = (int16)(sumInputX / kInputHistorySize);
+	_avgInputY = (int16)(-sumInputY / kInputHistorySize);
+	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
+	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
+
+	_shipPosX = _avgInputX + 0xA0;
+	_shipPosY = _avgInputY + 0x46;
 
-	// Viewport parallax: smoothed offset from center for background scrolling
-	int16 mouseOffX = (int16)(_vm->_mouse.x - kRA1CenterX);
-	int16 mouseOffY = (int16)(_vm->_mouse.y - kRA1CenterY);
 	for (int i = kInputHistorySize - 1; i > 0; i--) {
 		_viewHistoryX[i] = _viewHistoryX[i - 1];
 		_viewHistoryY[i] = _viewHistoryY[i - 1];
 	}
-	_viewHistoryX[0] = mouseOffX;
-	_viewHistoryY[0] = mouseOffY;
+	_viewHistoryX[0] = _avgInputX;
+	_viewHistoryY[0] = _avgInputY;
 
-	int16 viewSumX = 0, viewSumY = 0;
+	int sumViewX = 0;
+	int sumViewY = 0;
 	for (int i = 0; i < kInputHistorySize; i++) {
-		viewSumX += _viewHistoryX[i];
-		viewSumY += _viewHistoryY[i];
+		sumViewX += _viewHistoryX[i];
+		sumViewY += _viewHistoryY[i];
 	}
-	_perspectiveX = CLIP<int16>((int16)(viewSumX / kInputHistorySize >> 1) + 0x20, 0, 0x40);
-	_perspectiveY = CLIP<int16>((int16)(viewSumY / kInputHistorySize >> 1) + 0x17, 0, 0x2E);
+
+	int16 avgViewX = (int16)(sumViewX / kInputHistorySize);
+	int16 avgViewY = (int16)(sumViewY / kInputHistorySize);
+	_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
+	_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
 
 	_frameCounter++;
 
@@ -505,14 +529,10 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 // processShot — FUN_1CCA0 (0x1CCA0). Spawns shot into explosion slot when fired.
 // Called once per frame during interactive rendering.
 void InsaneRebel1::processShot() {
-	if (!_playerFired || _fireCooldown > 0) {
-		_playerFired = false;
-		if (_fireCooldown > 0)
-			_fireCooldown--;
+	if (!_playerFired)
 		return;
-	}
 
-	// Find an available slot (timer <= 0 or timer > 5)
+	// Find first available slot (timer < 1 or > 5), matching FUN_1CCA0.
 	int slot = -1;
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer <= 0 || _shotSlots[i].timer > 5) {
@@ -525,14 +545,14 @@ void InsaneRebel1::processShot() {
 		return;
 	}
 
-	// Record shot at current cursor position
-	_shotSlots[slot].timer = 5;  // 5 frames active (original: uVar1 = 5 or 2 based on DAT_75FF)
+	// Record shot at current cursor position.
+	_shotSlots[slot].timer = 5;
 	_shotSlots[slot].posX = _shipPosX;
 	_shotSlots[slot].posY = _shipPosY;
-	_shotSlots[slot].centerX = _perspectiveX + (_shipPosX - kRA1CenterX) + kRA1CenterX;
-	_shotSlots[slot].centerY = _perspectiveY + (_shipPosY - kRA1CenterY) + kRA1CenterY;
+	_shotSlots[slot].centerX = _shipPosX;
+	_shotSlots[slot].centerY = _shipPosY;
+	_shotAlternator = 1 - _shotAlternator;
 
-	_fireCooldown = 3;  // Minimum frames between shots
 	_playerFired = false;
 
 	debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index f4e4a034eb3..2344eaf4964 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,6 +29,32 @@
 namespace Scumm {
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_UP:
+		case Common::KEYCODE_w:
+			_levelSelectSel = (_levelSelectSel + 2) % 3;
+			return true;
+		case Common::KEYCODE_DOWN:
+		case Common::KEYCODE_s:
+			_levelSelectSel = (_levelSelectSel + 1) % 3;
+			return true;
+		case Common::KEYCODE_RETURN:
+		case Common::KEYCODE_KP_ENTER:
+		case Common::KEYCODE_SPACE:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		case Common::KEYCODE_ESCAPE:
+			_levelSelectSel = 2; // Back
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			break;
+		}
+	}
+
 	if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
@@ -55,7 +81,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		}
 	}
 
-	if (_menuActive && !_optionsActive && event.type == Common::EVENT_KEYDOWN) {
+	if (_menuActive && !_optionsActive && !_levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_w:
@@ -93,8 +119,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	// Shooting: mouse button during interactive gameplay — FUN_1CCA0 (0x1CCA0)
 	if (_interactiveVideoActive && !_menuActive) {
 		if (event.type == Common::EVENT_LBUTTONDOWN) {
-			if (_currentLevel != 1)
-				_playerFired = true;
+			_playerFired = true;
 			return true;
 		}
 	}
@@ -155,11 +180,55 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		return;
 	}
 
+	if (_levelSelectActive) {
+		// --- Level select submenu ---
+		const int titleW = getFontBankStringWidth("LEVEL SELECT");
+		drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "LEVEL SELECT");
+
+		const char *kLevelItems[3] = {
+			"LEVEL 1: FLIGHT TRAINING",
+			"LEVEL 2: ASTEROID FIELD",
+			"BACK"
+		};
+
+		const int menuY = 60;
+		const int rowH = 16;
+
+		for (int i = 0; i < 3; i++) {
+			const int textW = getFontBankStringWidth(kLevelItems[i]);
+			const int textX = (width - textW) / 2;
+			const int y = menuY + i * rowH;
+			drawFontBankString(dst, pitch, width, height, textX, y + 1, kLevelItems[i]);
+
+			if (i == _startLevel - 1) {
+				drawFontBankString(dst, pitch, width, height, textX - 12, y + 1, ">");
+			}
+
+			if (i == _levelSelectSel) {
+				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
+				int bracketWidth = textW + 12;
+				int leftX = CLIP(textX - 6, 0, width - 1);
+				int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
+				int topY = CLIP(y - 1, 0, height - 1);
+				int bottomY = CLIP(y + rowH - 2, 0, height - 1);
+				for (int x = leftX; x <= rightX; x++) {
+					dst[topY * pitch + x] = highlightColor;
+					dst[bottomY * pitch + x] = highlightColor;
+				}
+				for (int py = topY; py <= bottomY; py++) {
+					dst[py * pitch + leftX] = highlightColor;
+					dst[py * pitch + rightX] = highlightColor;
+				}
+			}
+		}
+		return;
+	}
+
 	// --- Main menu ---
 	static const char *kMenuItems[5] = {
 		"START NEW GAME",
 		"GAME OPTIONS",
-		"ENTER PASSCODE",
+		"LEVEL SELECT",
 		"CONTINUE DEMO",
 		"EXIT TO DOS"
 	};
@@ -273,4 +342,38 @@ void InsaneRebel1::runOptionsMenu() {
 	_optionsActive = false;
 }
 
+void InsaneRebel1::runLevelSelectMenu() {
+	_levelSelectSel = CLIP(_startLevel - 1, 0, 1);
+	_levelSelectActive = true;
+
+	while (!_vm->shouldQuit()) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+
+		if (_vm->shouldQuit())
+			break;
+
+		if (_menuConfirmed) {
+			switch (_levelSelectSel) {
+			case 0:
+				_startLevel = 1;
+				_levelSelectActive = false;
+				return;
+			case 1:
+				_startLevel = 2;
+				_levelSelectActive = false;
+				return;
+			case 2:
+				_levelSelectActive = false;
+				return;
+			}
+		}
+	}
+	_levelSelectActive = false;
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 131a796c726..6f37737a2ce 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -27,12 +27,105 @@
 
 namespace Scumm {
 
+static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+	int x, int y, const char *text) {
+	if (!dst || !text || bank.numSprites <= 0)
+		return;
+
+	for (int i = 0; text[i] != '\0'; i++) {
+		const byte ch = (byte)text[i];
+
+		if (ch == ' ') {
+			x += 6;
+			continue;
+		}
+
+		// RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
+		if (ch < 0x21) {
+			x += 4;
+			continue;
+		}
+		const int fontIdx = (int)ch - 0x21;
+		if (fontIdx < 0 || fontIdx >= bank.numSprites) {
+			x += 4;
+			continue;
+		}
+
+		const RA1Sprite &glyph = bank.sprites[fontIdx];
+		const int gw = glyph.width;
+		const int gh = glyph.height;
+		const int gx = x + glyph.xoffs;
+		const int gy = y + glyph.yoffs;
+		const uint64 glyphPixels = (uint64)gw * (uint64)gh;
+		if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
+			x += 4;
+			continue;
+		}
+		if (!(bank.decodedData && bank.decodedSize > 0)) {
+			x += 4;
+			continue;
+		}
+		const byte *bankStart = bank.decodedData;
+		const byte *bankEnd = bank.decodedData + bank.decodedSize;
+		if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
+			x += 4;
+			continue;
+		}
+
+		for (int py = 0; py < gh; py++) {
+			const int sy = gy + py;
+			if (sy < 0 || sy >= height)
+				continue;
+			for (int px = 0; px < gw; px++) {
+				const int sx = gx + px;
+				if (sx < 0 || sx >= width)
+					continue;
+				const byte pixel = glyph.data[py * gw + px];
+				if (pixel != 0)
+					dst[sy * pitch + sx] = pixel;
+			}
+		}
+
+		x += gw > 0 ? gw : 4;
+	}
+}
+
+static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
+	if (!text || bank.numSprites <= 0)
+		return 0;
+
+	int w = 0;
+	for (int i = 0; text[i] != '\0'; i++) {
+		const byte ch = (byte)text[i];
+		if (ch == ' ') {
+			w += 6;
+			continue;
+		}
+		if (ch < 0x21) {
+			w += 4;
+			continue;
+		}
+		const int fontIdx = (int)ch - 0x21;
+		if (fontIdx < 0 || fontIdx >= bank.numSprites) {
+			w += 4;
+			continue;
+		}
+		const RA1Sprite &glyph = bank.sprites[fontIdx];
+		w += glyph.width > 0 ? glyph.width : 4;
+	}
+	return w;
+}
+
 // procPreRendering — Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
 // For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	if (_interactiveVideoActive && _player) {
-		_player->_ra1ViewportOffsetX = _perspectiveX;
-		_player->_ra1ViewportOffsetY = _perspectiveY;
+		// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
+		// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
+		// ScummVM presents the full 384x242 frame, so use center-relative delta
+		// to avoid exposing black border over cockpit edges.
+		_player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
+		_player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
 	} else if (_player) {
 		_player->_ra1ViewportOffsetX = 0;
 		_player->_ra1ViewportOffsetY = 0;
@@ -70,69 +163,50 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
-	// Shooting/targeting pipeline applies to ship/turret gameplay.
-	// LVL2 (opcode 0x0B asteroid dodge) is avoidance-only.
-	if (_currentLevel != 1) {
-		processShot();
-		for (int i = 0; i < kMaxShotSlots; i++) {
-			if (_shotSlots[i].timer > 0)
-				_shotSlots[i].timer--;
-		}
-
-		renderLaserShots(renderBitmap, pitch, width, height);
-		renderExplosions(renderBitmap, pitch, width, height);
-		renderGostSlots(renderBitmap, pitch, width, height);
-		renderTargeting(renderBitmap, pitch, width, height);
+	// FUN_1CDA7 (0x0B) still executes targeting/shot overlay pipeline
+	// (FUN_1C940, FUN_1CCA0, FUN_1C9CD, FUN_1CB22) while alive.
+	processShot();
+	for (int i = 0; i < kMaxShotSlots; i++) {
+		if (_shotSlots[i].timer > 0)
+			_shotSlots[i].timer--;
 	}
+
+	renderLaserShots(renderBitmap, pitch, width, height);
+	renderExplosions(renderBitmap, pitch, width, height);
+	renderGostSlots(renderBitmap, pitch, width, height);
+	renderTargeting(renderBitmap, pitch, width, height);
 	renderHUD(renderBitmap, pitch, width, height);
 }
 
-// renderTargeting — FUN_1CB22 (0x1CB22). Targeting/lock-on crosshair at cursor position.
-// Draws a crosshair that changes color based on target proximity:
-//   0 (none) = dim grey, 1 (near) = yellow, 2 (on-target) = bright red
+// renderTargeting — FUN_1CB22 (0x1CB22). Targeting/lock-on indicator.
+// The original does not draw a hardcoded pixel cross; it renders glyph markers
+// whose state depends on _targetProximity.
 void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
-	int cx = _shipPosX;
-	int cy = _shipPosY;
-
-	// Color based on proximity state
-	// Palette indices chosen for L2PLAY visibility:
-	//   0xD7 = bright green (0,255,0), 0x63 = yellow (255,255,31), 0xDD = red (192,0,0)
-	byte color;
-	if (_targetProximity >= 2)
-		color = 0xDD;  // Red — on target
-	else if (_targetProximity == 1)
-		color = 0x63;  // Yellow — near
-	else
-		color = 0xD7;  // Green — no target
-
-	// Animated crosshair: size pulses with frame counter
-	int armLen = 6 + ((_frameCounter >> 2) & 3);
-	int gap = 2;  // Gap around center
-
-	// Horizontal arms
-	for (int i = gap; i <= armLen; i++) {
-		int lx = cx - i, rx = cx + i;
-		if (cy >= 0 && cy < height) {
-			if (lx >= 0 && lx < width)
-				dst[cy * pitch + lx] = color;
-			if (rx >= 0 && rx < width)
-				dst[cy * pitch + rx] = color;
+	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+	if (markerBank.numSprites > 0) {
+		// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
+		// Baseline RA1 targeting uses '^' and animation e..h.
+		const bool altMarkerSet = false;
+
+		// Lock indicator at fixed center positions:
+		// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
+		if (_targetProximity > 0) {
+			drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x78, "]");
+			if (_targetProximity > 1)
+				drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x7E, "a");
 		}
-	}
-	// Vertical arms
-	for (int i = gap; i <= armLen; i++) {
-		int uy = cy - i, dy = cy + i;
-		if (cx >= 0 && cx < width) {
-			if (uy >= 0 && uy < height)
-				dst[uy * pitch + cx] = color;
-			if (dy >= 0 && dy < height)
-				dst[dy * pitch + cx] = color;
+
+		// Pointer glyph at current aim position. Original uses two variants:
+		// default marker ('^' or 'x') and animated lock marker (e..h or y..|).
+		char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
+		if (_targetProximity > 1) {
+			marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_frameCounter & 3));
 		}
-	}
 
-	// Center dot for on-target
-	if (_targetProximity >= 2 && cx >= 0 && cx < width && cy >= 0 && cy < height) {
-		dst[cy * pitch + cx] = color;
+		int cursorX = CLIP<int>(_shipPosX, 0, width - 1);
+		int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
+		int markerW = getBankStringWidth(markerBank, marker);
+		drawBankString(markerBank, dst, pitch, width, height, cursorX - markerW / 2, cursorY, marker);
 	}
 
 	// Save previous proximity for next frame
@@ -179,18 +253,15 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	int spritesPerSet = 5;
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
-			// Frame index: timer 5→1 maps to sprite 0→4
 			int frame = spritesPerSet - _shotSlots[i].timer;
-			// Alternate between sprite sets for left/right shots
-			int setOffset = (i % 3) * spritesPerSet;
-			int sprIdx = setOffset + frame;
+			int sprIdx = frame;
 			if (sprIdx >= _laserBank.numSprites)
 				continue;
 
 			const RA1Sprite &spr = _laserBank.sprites[sprIdx];
-			// LASER sprites have embedded positions relative to screen center
-			int drawX = spr.xoffs + _perspectiveX;
-			int drawY = spr.yoffs + _perspectiveY;
+			// LASER sprite offsets are authored in screen space.
+			int drawX = spr.xoffs;
+			int drawY = spr.yoffs;
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 		}
 	}
@@ -199,92 +270,12 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 // drawFontBankString — Simplified version of FUN_221B7 (0x221B7).
 // Original is a multi-font markup-capable renderer; this uses a single font bank.
 void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
-	if (!dst || !text || _hudFontBank.numSprites <= 0)
-		return;
-
-	for (int i = 0; text[i] != '\0'; i++) {
-		const byte ch = (byte)text[i];
-
-		if (ch == ' ') {
-			x += 6;
-			continue;
-		}
-
-		// RA1 font renderer indexes printable characters from '!' (0x21), not raw ASCII.
-		if (ch < 0x21) {
-			x += 4;
-			continue;
-		}
-		const int fontIdx = (int)ch - 0x21;
-		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
-			x += 4;
-			continue;
-		}
-
-		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
-		const int gw = glyph.width;
-		const int gh = glyph.height;
-		const int gx = x + glyph.xoffs;
-		const int gy = y + glyph.yoffs;
-		const uint64 glyphPixels = (uint64)gw * (uint64)gh;
-		if (!glyph.data || gw <= 0 || gh <= 0 || glyphPixels == 0 || glyphPixels > 0x10000) {
-			x += 4;
-			continue;
-		}
-		if (!(_hudFontBank.decodedData && _hudFontBank.decodedSize > 0)) {
-			x += 4;
-			continue;
-		}
-		const byte *bankStart = _hudFontBank.decodedData;
-		const byte *bankEnd = _hudFontBank.decodedData + _hudFontBank.decodedSize;
-		if (glyph.data < bankStart || glyph.data >= bankEnd || glyph.data + glyphPixels > bankEnd) {
-			x += 4;
-			continue;
-		}
-
-		for (int py = 0; py < gh; py++) {
-			const int sy = gy + py;
-			if (sy < 0 || sy >= height)
-				continue;
-			for (int px = 0; px < gw; px++) {
-				const int sx = gx + px;
-				if (sx < 0 || sx >= width)
-					continue;
-				const byte pixel = glyph.data[py * gw + px];
-				if (pixel != 0)
-					dst[sy * pitch + sx] = pixel;
-			}
-		}
-
-		x += gw > 0 ? gw : 4;
-	}
+	drawBankString(_hudFontBank, dst, pitch, width, height, x, y, text);
 }
 
 // getFontBankStringWidth — Pre-pass width calculation from FUN_221B7 (0x221B7).
 int InsaneRebel1::getFontBankStringWidth(const char *text) {
-	if (!text || _hudFontBank.numSprites <= 0)
-		return 0;
-
-	int w = 0;
-	for (int i = 0; text[i] != '\0'; i++) {
-		const byte ch = (byte)text[i];
-		if (ch == ' ') {
-			w += 6;
-			continue;
-		}
-		if (ch < 0x21) {
-			w += 4;
-			continue;
-		}
-		const int fontIdx = (int)ch - 0x21;
-		if (fontIdx < 0 || fontIdx >= _hudFontBank.numSprites) {
-			w += 4;
-			continue;
-		}
-		const RA1Sprite &glyph = _hudFontBank.sprites[fontIdx];
-		w += glyph.width > 0 ? glyph.width : 4;
-	}
-	return w;
+	return getBankStringWidth(_hudFontBank, text);
 }
 
 // renderShip — Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
@@ -300,11 +291,10 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 
 	const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
 
-	// Position: game coords → screen coords via perspective transform
-	// Adapted from RA2's renderHandler7Ship:
-	//   shipCenterX = (shipX - center) + perspX + screenCenterX
-	int drawX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX - spr.width / 2;
-	int drawY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY - spr.height / 2;
+	// FUN_1DEB5 draws at (_74B6 + _74BA, _74B8 + _74BC).
+	// In the current mapping, _shipPosX/_shipPosY already store that screen position.
+	int drawX = _shipPosX - spr.width / 2;
+	int drawY = _shipPosY - spr.height / 2;
 
 	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 }
@@ -315,9 +305,9 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 	if (_bangBank.numSprites <= 0)
 		return;
 
-	// Ship screen center position (matches assembly: DAT_74b6+DAT_74ba, DAT_74b8+DAT_74bc)
-	int shipScreenX = (_shipPosX - kRA1CenterX) + _perspectiveX + kRA1CenterX;
-	int shipScreenY = (_shipPosY - kRA1CenterY) + _perspectiveY + kRA1CenterY;
+	// Ship screen center position (matches assembly DAT_74B6+DAT_74BA, DAT_74B8+DAT_74BC).
+	int shipScreenX = _shipPosX;
+	int shipScreenY = _shipPosY;
 
 	// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
 	// When dead and deathTimer > 10: random explosion sprites scatter around ship
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index ad0ca095540..92a7c2d02a3 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -279,21 +279,29 @@ void InsaneRebel1::runGame() {
 
 		switch (menuResult) {
 		case 1: {
-#if 0 // Skip level 1 for testing — jump straight to level 2
-			// Start New Game — play L1NEW briefing then level 1
-			playCinematic("LVL1/L1NEW.ANM");
-			if (_vm->shouldQuit())
-				return;
-
-			bool completed = runLevel1();
-#else
-			bool completed = true;
-#endif
-			if (completed && !_vm->shouldQuit()) {
+			bool completed = false;
+			if (_startLevel <= 1) {
+				// Start from Level 1 (default flow)
+				playCinematic("LVL1/L1NEW.ANM");
+				if (_vm->shouldQuit())
+					return;
+
+				completed = runLevel1();
+				if (completed && !_vm->shouldQuit()) {
+					completed = runLevel2();
+					if (completed) {
+						debug(1, "InsaneRebel1: Level 2 completed!");
+						// TODO: Continue to level 3
+					}
+				}
+			} else if (_startLevel == 2) {
+				// Direct Level 2 start from Level Select menu
+				playCinematic("LVL2/L2NEW.ANM");
+				if (_vm->shouldQuit())
+					return;
 				completed = runLevel2();
 				if (completed) {
 					debug(1, "InsaneRebel1: Level 2 completed!");
-					// TODO: Continue to level 3
 				}
 			}
 			_currentLevel = 0;
@@ -304,6 +312,10 @@ void InsaneRebel1::runGame() {
 			// Game Options
 			runOptionsMenu();
 			break;
+		case 3:
+			// Level Select
+			runLevelSelectMenu();
+			break;
 		case 5:
 			// Exit
 			return;


Commit: 3e45c5db23df6d86ca48d494d5359375d6d910f9
    https://github.com/scummvm/scummvm/commit/3e45c5db23df6d86ca48d494d5359375d6d910f9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00

Commit Message:
SCUMM: RA1: Render level 2 lasers

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_levels.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index fd52033ab8a..ada0c41a9c8 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -240,6 +240,10 @@ void InsaneRebel1::updateShipPhysics() {
 // Ship position = averaged input + center offset.
 // Viewport = second history buffer for smooth camera scrolling.
 void InsaneRebel1::updateAsteroidPhysics() {
+	// Control feel tweak: original uses full 10-sample average in FUN_1CDA7.
+	// We keep the same pipeline but average over fewer samples for responsiveness.
+	const int kAsteroidSmoothWindow = 2;
+
 	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
@@ -294,7 +298,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		mouseY = (int16)((mouseY * 200) / _player->_height);
 	}
 	int16 inputX = (int16)(mouseX - kRA1CenterX);
-	int16 inputY = (int16)(mouseY - kRA1CenterY);
+	// Assembly uses an inverted-Y convention in the averaging path.
+	// In ScummVM screen coords (Y grows downward), convert here so moving
+	// mouse up moves the pointer up on screen.
+	int16 inputY = (int16)(kRA1CenterY - mouseY);
 	inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
 	inputY = CLIP<int16>(inputY, -100, 100);
 
@@ -307,13 +314,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	int sumInputX = 0;
 	int sumInputY = 0;
-	for (int i = 0; i < kInputHistorySize; i++) {
+	for (int i = 0; i < kAsteroidSmoothWindow; i++) {
 		sumInputX += _inputHistoryX[i];
 		sumInputY += _inputHistoryY[i];
 	}
 
-	_avgInputX = (int16)(sumInputX / kInputHistorySize);
-	_avgInputY = (int16)(-sumInputY / kInputHistorySize);
+	_avgInputX = (int16)(sumInputX / kAsteroidSmoothWindow);
+	_avgInputY = (int16)(-sumInputY / kAsteroidSmoothWindow);
 	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
 	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
 
@@ -329,13 +336,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	int sumViewX = 0;
 	int sumViewY = 0;
-	for (int i = 0; i < kInputHistorySize; i++) {
+	for (int i = 0; i < kAsteroidSmoothWindow; i++) {
 		sumViewX += _viewHistoryX[i];
 		sumViewY += _viewHistoryY[i];
 	}
 
-	int16 avgViewX = (int16)(sumViewX / kInputHistorySize);
-	int16 avgViewY = (int16)(sumViewY / kInputHistorySize);
+	int16 avgViewX = (int16)(sumViewX / kAsteroidSmoothWindow);
+	int16 avgViewY = (int16)(sumViewY / kAsteroidSmoothWindow);
 	_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
 
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 9fe8e6e58aa..195c7490265 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -28,6 +28,9 @@
 
 namespace Scumm {
 
+// From smush/codec1.cpp
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
 static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
 	while (height--) {
 		byte *dstNext = dst + pitch;
@@ -166,6 +169,12 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 			bank.sprites[i].data = decPtr;
 			decodeBomp(decPtr, fobjData, bank.sprites[i].width,
 					   bank.sprites[i].height, bank.sprites[i].width);
+		} else if (codec == 1) {
+			// RA1 codec 1 in NUTs (e.g. LVL2/L2LASER.NUT): RLE where color 0 is transparent.
+			// Decode into a zero-cleared sprite buffer so skipped pixels stay transparent.
+			bank.sprites[i].data = decPtr;
+			smushDecodeRA1Transparent(decPtr, fobjData, 0, 0,
+				bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width);
 		} else {
 			bank.sprites[i].width = 0;
 			bank.sprites[i].height = 0;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6f37737a2ce..9829bd4f025 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -116,14 +116,111 @@ static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
 	return w;
 }
 
+// FUN_1C794: direction bucket in range -4..4 from two points.
+static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
+	int dx = x2 - x1;
+	int dy = y1 - y2;
+	if (dy < 0) {
+		dy = -dy;
+		dx = -dx;
+	}
+
+	if (dx >= 0) {
+		if (dy > dx * 5)
+			return 0;
+		if (dx * 3 < dy * 2)
+			return 1;
+		if (dx * 2 < dy * 3)
+			return 2;
+		if (dx * 2 < dy * 9)
+			return 3;
+		return 4;
+	}
+
+	const int adx = -dx;
+	if (dy > adx * 5)
+		return 0;
+	if (adx * 3 < dy * 2)
+		return -1;
+	if (adx * 2 < dy * 3)
+		return -2;
+	if (adx * 2 < dy * 9)
+		return -3;
+	return -4;
+}
+
+// FUN_1CDA7 maps abs(FUN_1C794) to sprite base index: <=1 -> 0, ==2 -> 5, else -> 10.
+static int ra1ShotDirectionBucket(int dir) {
+	const int absDir = ABS(dir);
+	if (absDir <= 1)
+		return 0;
+	if (absDir == 2)
+		return 5;
+	return 10;
+}
+
+// Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
+static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
+	int x, int y, const RA1Sprite &spr, uint32 flags) {
+	if (!spr.data || spr.width <= 0 || spr.height <= 0)
+		return;
+
+	int drawX = x;
+	int drawY = y;
+	if ((flags & 0x1) == 0) {
+		drawX += spr.xoffs;
+		drawY += spr.yoffs;
+	}
+	if (flags & 0x2) {
+		drawX -= spr.width / 2;
+		drawY -= spr.height / 2;
+	}
+
+	const bool flipX = (flags & 0x2000) != 0;
+	const bool flipY = (flags & 0x4000) != 0;
+
+	int srcOffsetX = 0;
+	int srcOffsetY = 0;
+	int drawW = spr.width;
+	int drawH = spr.height;
+
+	if (drawX < 0) {
+		srcOffsetX = -drawX;
+		drawW += drawX;
+		drawX = 0;
+	}
+	if (drawY < 0) {
+		srcOffsetY = -drawY;
+		drawH += drawY;
+		drawY = 0;
+	}
+	if (drawX + drawW > width)
+		drawW = width - drawX;
+	if (drawY + drawH > height)
+		drawH = height - drawY;
+	if (drawW <= 0 || drawH <= 0)
+		return;
+
+	for (int iy = 0; iy < drawH; iy++) {
+		const int srcY = flipY ? (spr.height - 1 - (srcOffsetY + iy)) : (srcOffsetY + iy);
+		byte *d = dst + (drawY + iy) * pitch + drawX;
+		for (int ix = 0; ix < drawW; ix++) {
+			const int srcX = flipX ? (spr.width - 1 - (srcOffsetX + ix)) : (srcOffsetX + ix);
+			const byte px = spr.data[srcY * spr.width + srcX];
+			if (px != 0)
+				d[ix] = px;
+		}
+	}
+}
+
 // procPreRendering — Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
 // For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	if (_interactiveVideoActive && _player) {
 		// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
 		// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
-		// ScummVM presents the full 384x242 frame, so use center-relative delta
-		// to avoid exposing black border over cockpit edges.
+		// Apply center-relative deltas so right aim shifts scene left, matching
+		// FUN_223FE/FUN_2245B subtracting _41A0 from world-space X.
 		_player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
 		_player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
 	} else if (_player) {
@@ -243,26 +340,69 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	}
 }
 
-// renderLaserShots — FUN_1CCA0 visual output. Renders laser sprites from _laserBank
-// when shot slots are active (timer > 0). Each set of 5 sprites is a laser animation
-// converging from the ship toward center screen. Timer 5→1 maps to sprite frames 0→4.
+// renderLaserShots — FUN_1CDA7/FUN_1D79C shot visual path:
+// per active slot, compute left/right direction with FUN_1C794, pick one
+// of 3x5 sprite bands, and render interpolated sprite positions via FUN_20BD3.
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
 	if (_laserBank.numSprites <= 0)
 		return;
 
-	int spritesPerSet = 5;
+	// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
+	// Entry 0 unused.
+	static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
+	const int spritesPerSet = 5;
+	const int leftStartX = 0;
+	const int rightStartX = 0x13F; // 319
+
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
-			int frame = spritesPerSet - _shotSlots[i].timer;
-			int sprIdx = frame;
-			if (sprIdx >= _laserBank.numSprites)
-				continue;
+			const int timer = _shotSlots[i].timer;
+			const int lerp = kShotLerpByTimer[timer];
+			const int frame = spritesPerSet - timer;
+			const int targetX = CLIP<int>(_shipPosX, 0, width - 1);
+			const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
+
+			int leftStartY = 0x96;
+			int rightStartY = 0x96;
+			uint32 leftFlags = 0x83;
+			uint32 rightFlags = 0x2083;
+
+			// FUN_1CDA7 special mode branch (_DAT_75E4 == 1): toggles emitter origin
+			// and flip flags via DAT_2423. Keep behavior for parity when mode is used.
+			if (_flyControlMode == 1) {
+				if (_shotAlternator != 0) {
+					leftFlags = 0x4083;
+					leftStartY = 0;
+					rightStartY = 0x96;
+				} else {
+					rightFlags = 0x6083;
+					leftStartY = 0x96;
+					rightStartY = 0;
+				}
+				if (timer == 1)
+					_shotAlternator = 1 - _shotAlternator;
+			}
 
-			const RA1Sprite &spr = _laserBank.sprites[sprIdx];
-			// LASER sprite offsets are authored in screen space.
-			int drawX = spr.xoffs;
-			int drawY = spr.yoffs;
-			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+			const int dirLeft = ra1ShotDirection(targetX, targetY, leftStartX, leftStartY);
+			const int dirRight = ra1ShotDirection(rightStartX, targetY, targetX, rightStartY);
+			const int bucketLeft = ra1ShotDirectionBucket(dirLeft);
+			const int bucketRight = ra1ShotDirectionBucket(dirRight);
+			const int sprIdxLeft = frame + bucketLeft;
+			const int sprIdxRight = frame + bucketRight;
+
+			const int interpLeftX = leftStartX + (((targetX - leftStartX) * lerp) >> 3);
+			const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
+			const int interpRightX = rightStartX + (((targetX - rightStartX) * lerp) >> 3);
+			const int interpRightY = rightStartY + (((targetY - rightStartY) * lerp) >> 3);
+
+			if (sprIdxLeft >= 0 && sprIdxLeft < _laserBank.numSprites) {
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interpLeftX, interpLeftY, _laserBank.sprites[sprIdxLeft], leftFlags);
+			}
+			if (sprIdxRight >= 0 && sprIdxRight < _laserBank.numSprites) {
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interpRightX, interpRightY, _laserBank.sprites[sprIdxRight], rightFlags);
+			}
 		}
 	}
 }
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index ff01333384f..5743f24c858 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -462,10 +462,6 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 			int left = _storedFobjLeft + fetchX;
 			int top = _storedFobjTop + fetchY;
 
-			// Apply the same interactive viewport offset used for regular FOBJ chunks.
-			left -= _ra1ViewportOffsetX;
-			top -= _ra1ViewportOffsetY;
-
 			debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
 				_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
 			decodeFrameObject(storedCodec, _storedFobjData, left, top,


Commit: a854f3fd00ebd4c48c4b05a0e6a65fda3bf2cf6f
    https://github.com/scummvm/scummvm/commit/a854f3fd00ebd4c48c4b05a0e6a65fda3bf2cf6f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:30+02:00

Commit Message:
SCUMM: RA1: Fix level 1 targeting pipeline

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 64c5c4a7b6f..225b0678a09 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -107,6 +107,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 	_currentLevel = 0;
 	_flyControlMode = 0;
+	_activeGameOpcode = 0;
 
 	_health = kMaxHealth;
 	_lives = 3;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index be8cdb994de..3fd9d01cf5d 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -199,6 +199,9 @@ private:
 
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
+	// Last per-frame GAME movement handler opcode (0x07/0x08/0x09/0x0A/0x0B/0x1A).
+	// Used to mirror assembly handler-specific overlay pipeline behavior.
+	uint16 _activeGameOpcode;
 
 	// Difficulty (0=easy, 1=normal, 2=hard) — matches original DAT_22BC
 	int _difficulty;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index ada0c41a9c8..34a52224555 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -106,35 +106,35 @@ void InsaneRebel1::updateShipPhysics() {
 	{
 		bool hasZoneHit = (_damageFlags & 0x10) != 0;
 
-		if (_shipPosX > _corridorRightX) {
-			_posAccumX = (_corridorRightX - kRA1CenterX) << 8;
-			_shipPosX = _corridorRightX;
-			if (!hasZoneHit) {
-				if (_rollAccum > -0x100)
+			if (_shipPosX > _corridorRightX) {
+				_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+				_shipPosX = _corridorRightX;
+				if (!hasZoneHit) {
+					if (_rollAccum > -0x100)
 					_rollAccum = -0x100;  // Push left
 				_damageFlags |= 0x02;  // Right wall
 			}
-		}
-		if (_shipPosX < _corridorLeftX) {
-			_posAccumX = (_corridorLeftX - kRA1CenterX) << 8;
-			_shipPosX = _corridorLeftX;
-			if (!hasZoneHit) {
-				if (_rollAccum < 0x100)
+			}
+			if (_shipPosX < _corridorLeftX) {
+				_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+				_shipPosX = _corridorLeftX;
+				if (!hasZoneHit) {
+					if (_rollAccum < 0x100)
 					_rollAccum = 0x100;   // Push right
 				_damageFlags |= 0x04;  // Left wall
 			}
-		}
-		if (_shipPosY < _corridorTopY) {
-			_posAccumY = ((_corridorTopY - kRA1CenterY) << 8) + 0x100;
-			_shipPosY = _corridorTopY;
-			if (!hasZoneHit)
-				_damageFlags |= 0x01;
-		}
-		if (_shipPosY > _corridorBottomY) {
-			_posAccumY = ((_corridorBottomY - kRA1CenterY) << 8) - 0x100;
-			_shipPosY = _corridorBottomY;
-			if (!hasZoneHit)
-				_damageFlags |= 0x08;
+			}
+			if (_shipPosY < _corridorTopY) {
+				_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+				_shipPosY = _corridorTopY;
+				if (!hasZoneHit)
+					_damageFlags |= 0x01;
+			}
+			if (_shipPosY > _corridorBottomY) {
+				_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+				_shipPosY = _corridorBottomY;
+				if (!hasZoneHit)
+					_damageFlags |= 0x08;
 		}
 	}
 
@@ -416,6 +416,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 		// Keep a conservative default mode after reset.
 		_flyControlMode = 0;
+		_activeGameOpcode = 0;
 		debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
 		break;
 
@@ -430,6 +431,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		break;
 
 	case 0x07:
+		_activeGameOpcode = 0x07;
 		// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
 		// f1 is the original's _DAT_7740 (game frame counter)
 		// f3 is the drift/wind parameter combined with tuning table
@@ -479,6 +481,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		break;
 
 	case 0x0B:
+		_activeGameOpcode = 0x0B;
 		// Asteroid/surface per-frame handler (FUN_1CDA7).
 		// field1 = frame counter, field2 = max frames
 		_gameCounter = param1;
@@ -517,8 +520,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		}
 		break;
 
-	case 0x08: case 0x09: case 0x0A:
-	case 0x19: case 0x1A:
+	case 0x08:
+	case 0x09:
+	case 0x0A:
+	case 0x19:
+	case 0x1A:
+		_activeGameOpcode = (uint16)opcode;
 		if (subSize >= 20) {
 			uint32 param2 = b.readUint32BE();
 			uint32 param3 = b.readUint32BE();
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 9829bd4f025..6d819f1b2f1 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -219,9 +219,12 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	if (_interactiveVideoActive && _player) {
 		// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
 		// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
-		// Apply center-relative deltas so right aim shifts scene left, matching
-		// FUN_223FE/FUN_2245B subtracting _41A0 from world-space X.
-		_player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
+		// Keep L2 on assembly-oriented X mapping used by FUN_223FE/FUN_2245B
+		// emulation; preserve legacy L1 mapping to avoid flight-section regressions.
+		if (_currentLevel == 1)
+			_player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
+		else
+			_player->_ra1ViewportOffsetX = 0x20 - _perspectiveX;
 		_player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
 	} else if (_player) {
 		_player->_ra1ViewportOffsetX = 0;
@@ -260,18 +263,31 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	}
 
-	// FUN_1CDA7 (0x0B) still executes targeting/shot overlay pipeline
-	// (FUN_1C940, FUN_1CCA0, FUN_1C9CD, FUN_1CB22) while alive.
-	processShot();
-	for (int i = 0; i < kMaxShotSlots; i++) {
-		if (_shotSlots[i].timer > 0)
-			_shotSlots[i].timer--;
+	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
+	// in handlers 0x09/0x0A/0x0B/0x1A (FUN_1DABB/FUN_1D79C/FUN_1CDA7/FUN_1D57E).
+	const bool hasTargetingPipeline =
+		(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+		 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
+	if (hasTargetingPipeline) {
+		processShot();
+		for (int i = 0; i < kMaxShotSlots; i++) {
+			if (_shotSlots[i].timer > 0)
+				_shotSlots[i].timer--;
+		}
+		renderLaserShots(renderBitmap, pitch, width, height);
+		renderGostSlots(renderBitmap, pitch, width, height);
+		renderTargeting(renderBitmap, pitch, width, height);
+	} else {
+		// Keep lock/target accumulators quiescent when current handler doesn't
+		// execute FUN_1C940/FUN_1CCA0/FUN_1C9CD/FUN_1CB22.
+		_targetProximity = 0;
+		_prevTargetProx = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
 	}
 
-	renderLaserShots(renderBitmap, pitch, width, height);
 	renderExplosions(renderBitmap, pitch, width, height);
-	renderGostSlots(renderBitmap, pitch, width, height);
-	renderTargeting(renderBitmap, pitch, width, height);
 	renderHUD(renderBitmap, pitch, width, height);
 }
 


Commit: 1f6fdf96c8fb2ffb26893301e7f2098286a583e5
    https://github.com/scummvm/scummvm/commit/1f6fdf96c8fb2ffb26893301e7f2098286a583e5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:31+02:00

Commit Message:
SCUMM: RA1: Fix interactive video ghosting

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 3fd9d01cf5d..1599665b406 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -83,6 +83,8 @@ public:
 	void procSKIP(int32 subSize, Common::SeekableReadStream &b) override;
 
 	void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
+	bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
+	int getCurrentLevel() const { return _currentLevel; }
 
 	// Game flow (matching original at 0x15597)
 	void runGame();
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6d819f1b2f1..9162a81ee09 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -213,19 +213,15 @@ static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 	}
 }
 
-// procPreRendering — Sets viewport scroll offset before FOBJ decoding (FUN_224FD at 0x224FD).
-// For interactive levels, shifts the FOBJ decode position based on mouse-controlled perspective.
+// procPreRendering — Sets viewport window offset (FUN_224FD at 0x224FD).
+// RA1 decodes FOBJs at chunk coordinates, then displays a scrolled 320x200
+// window inside the 384x242 framebuffer.
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	if (_interactiveVideoActive && _player) {
 		// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
 		// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
-		// Keep L2 on assembly-oriented X mapping used by FUN_223FE/FUN_2245B
-		// emulation; preserve legacy L1 mapping to avoid flight-section regressions.
-		if (_currentLevel == 1)
-			_player->_ra1ViewportOffsetX = _perspectiveX - 0x20;
-		else
-			_player->_ra1ViewportOffsetX = 0x20 - _perspectiveX;
-		_player->_ra1ViewportOffsetY = _perspectiveY - 0x17;
+		_player->_ra1ViewportOffsetX = _perspectiveX;
+		_player->_ra1ViewportOffsetY = _perspectiveY;
 	} else if (_player) {
 		_player->_ra1ViewportOffsetX = 0;
 		_player->_ra1ViewportOffsetY = 0;
@@ -529,9 +525,19 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	const RA1Sprite &bar = _displayBank.sprites[0];
 
 	// DISPLAY.NUT sprite is 320×19 at xoffs=0, yoffs=176 in the original game.
-	// Video FOBJs fill the full 384×242 buffer from (0,0), so use sprite offsets directly.
-	int hudX = bar.xoffs;
-	int hudY = bar.yoffs;
+	// FUN_224FD (0x224FD) sets the 320x200 window origin inside the 384x242 buffer.
+	// FUN_1BBCB (0x1BBCB) HUD coordinates are screen-space, so when we emulate
+	// perspective via source-window cropping, anchor HUD at window origin to keep
+	// it fixed on-screen.
+	int hudOriginX = 0;
+	int hudOriginY = 0;
+	if (_interactiveVideoActive && _player) {
+		hudOriginX = _player->_ra1ViewportOffsetX;
+		hudOriginY = _player->_ra1ViewportOffsetY;
+	}
+
+	int hudX = hudOriginX + bar.xoffs;
+	int hudY = hudOriginY + bar.yoffs;
 
 	// Draw the status bar background with transparency (pixel 0 = transparent)
 	if (bar.data && bar.width > 0 && bar.height > 0) {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 5743f24c858..34fadefaf20 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -272,6 +272,9 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_frameBuffer = nullptr;
 	_specialBuffer = nullptr;
 	_specialBufferSize = 0;
+	_ra1CleanFrame = nullptr;
+	_ra1CleanFrameSize = 0;
+	_ra1HasCleanFrame = false;
 
 	_seekPos = -1;
 
@@ -328,6 +331,10 @@ SmushPlayer::~SmushPlayer() {
 	_frameBuffer = nullptr;
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
+	free(_ra1CleanFrame);
+	_ra1CleanFrame = nullptr;
+	_ra1CleanFrameSize = 0;
+	_ra1HasCleanFrame = false;
 }
 
 void SmushPlayer::init(int32 speed) {
@@ -342,6 +349,7 @@ void SmushPlayer::init(int32 speed) {
 	_scrollX = 0;
 	_scrollY = 0;
 	_fastForwardToFrame = 0;
+	_ra1HasCleanFrame = false;
 
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
@@ -384,6 +392,10 @@ void SmushPlayer::release() {
 
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
+	free(_ra1CleanFrame);
+	_ra1CleanFrame = nullptr;
+	_ra1CleanFrameSize = 0;
+	_ra1HasCleanFrame = false;
 
 	if (isRA2()) {
 		ra2ReleaseVideo();
@@ -1370,14 +1382,6 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 			_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
 	}
 	if (isRA1()) {
-		// Viewport scroll for interactive gameplay (FUN_224FD sets _41A0/_41A2).
-		// Original renders FOBJs at raw positions, then displays a 320x200 window
-		// starting at (perspectiveX, perspectiveY) within the 384x242 buffer.
-		// FUN_22605/FUN_2289D subtract _41A0 from FOBJ X coords during rendering.
-		// We simulate the display window shift by subtracting from decode position.
-		left -= _ra1ViewportOffsetX;
-		top -= _ra1ViewportOffsetY;
-
 		debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
 			_frame, codec, left, top, width, height, subSize - 14, _storeFrame);
 	}
@@ -1424,17 +1428,37 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	if (isRA2())
 		_hasFrameFobjForGost = false;
 
+	bool interactiveRA1 = false;
+	bool forceInteractiveClearRA1 = false;
+	if (isRA1() && _insane) {
+		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+		interactiveRA1 = rebel1->isInteractiveVideoActive();
+		// Level 2 asteroid stream composes many partial codec1/2+FTCH layers.
+		// Clear per frame for this mode to avoid stale-trail ghosting.
+		forceInteractiveClearRA1 = interactiveRA1 && (rebel1->getCurrentLevel() == 1);
+	}
+
+	// Keep the previous decoded frame (without post-render overlays) as delta source.
+	// FUN_1FDBC (0x1FDBC) decodes frame data first; gameplay overlays from
+	// FUN_1BBCB/FUN_1CB22/FUN_1CDA7 are presentation-stage effects.
+	if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+		_ra1HasCleanFrame && _ra1CleanFrame &&
+		_dst && _width > 0 && _height > 0) {
+		const int frameBytes = _width * _height;
+		if (_ra1CleanFrameSize >= frameBytes)
+			memcpy(_dst, _ra1CleanFrame, frameBytes);
+	}
+
 	if (_insanity) {
 		_vm->_insane->procPreRendering(_dst);
 	}
 
-	// RA1: Clear framebuffer before each frame's first draw operation.
-	// Matches FFmpeg's first_fob memset: process_frame_obj() zeroes fbuf
-	// before the first FOBJ (or FTCH re-decode) of each frame. Without this,
-	// codec 1's transparent pixels (pixel 0 = skip) show previous frame
-	// content, causing ghost trails on the Star Wars opening crawl.
+	// RA1: gameplay/interactivity relies on previous-frame history (delta codecs),
+	// but passive cinematics in current implementation need a per-frame clear to
+	// avoid trails in intro/text sequences.
 	if (isRA1() && _dst && _width > 0 && _height > 0) {
-		memset(_dst, 0, _width * _height);
+		if (!interactiveRA1 || forceInteractiveClearRA1)
+			memset(_dst, 0, _width * _height);
 	}
 
 	while (frameSize > 0) {
@@ -1667,6 +1691,24 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
 	}
 
+	// Snapshot decoded frame before post-render overlays so next frame starts from
+	// clean video data (avoids ghost trails from moving HUD/cursor overlays).
+	if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+		_dst && _width > 0 && _height > 0) {
+		const int frameBytes = _width * _height;
+		byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
+		if (newClean != nullptr) {
+			_ra1CleanFrame = newClean;
+			_ra1CleanFrameSize = frameBytes;
+			memcpy(_ra1CleanFrame, _dst, frameBytes);
+			_ra1HasCleanFrame = true;
+		} else {
+			_ra1HasCleanFrame = false;
+		}
+	} else if (isRA1() && forceInteractiveClearRA1) {
+		_ra1HasCleanFrame = false;
+	}
+
 	if (_insanity) {
 		_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
 	}
@@ -2247,8 +2289,8 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 							continue;
 						}
 
-						const int srcX = CLIP(_scrollX, 0, _width - 1);
-						const int srcY = CLIP(_scrollY, 0, _height - 1);
+						const int srcX = CLIP(_scrollX + _ra1ViewportOffsetX, 0, _width - 1);
+						const int srcY = CLIP(_scrollY + _ra1ViewportOffsetY, 0, _height - 1);
 
 						frameWidth = MIN(_width - srcX, _vm->_screenWidth);
 						frameHeight = MIN(_height - srcY, _vm->_screenHeight);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 6a10be52e04..4c01847882c 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -156,6 +156,9 @@ private:
 	byte *_frameBuffer;
 	byte *_specialBuffer;
 	int _specialBufferSize;
+	byte *_ra1CleanFrame;
+	int32 _ra1CleanFrameSize;
+	bool _ra1HasCleanFrame;
 
 	// RA2: Raw FOBJ data stored by STOR chunk (matching original DAT_00482c04).
 	// The original stores raw FOBJ chunk data and re-decodes it on FTCH with


Commit: 43a9aeb6ee8b0248372b6204bb55f6141ea78533
    https://github.com/scummvm/scummvm/commit/43a9aeb6ee8b0248372b6204bb55f6141ea78533
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:31+02:00

Commit Message:
SCUMM: RA1: Fix asteroid view offsets

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 1599665b406..eba6366f77b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -85,6 +85,7 @@ public:
 	void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
 	bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
 	int getCurrentLevel() const { return _currentLevel; }
+	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
 
 	// Game flow (matching original at 0x15597)
 	void runGame();
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 9162a81ee09..664adcabcfe 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -533,7 +533,11 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	int hudOriginY = 0;
 	if (_interactiveVideoActive && _player) {
 		hudOriginX = _player->_ra1ViewportOffsetX;
-		hudOriginY = _player->_ra1ViewportOffsetY;
+		// Asteroid path (opcode 0x0B / FUN_1CDA7) applies Y correction through
+		// FUN_223FE coordinate transforms in the original renderer, not as a
+		// simple global framebuffer Y window shift.
+		if (_activeGameOpcode != 0x0B)
+			hudOriginY = _player->_ra1ViewportOffsetY;
 	}
 
 	int hudX = hudOriginX + bar.xoffs;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 34fadefaf20..db8e95be5f1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -2289,8 +2289,19 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 							continue;
 						}
 
-						const int srcX = CLIP(_scrollX + _ra1ViewportOffsetX, 0, _width - 1);
-						const int srcY = CLIP(_scrollY + _ra1ViewportOffsetY, 0, _height - 1);
+						int ra1ViewX = _ra1ViewportOffsetX;
+						int ra1ViewY = _ra1ViewportOffsetY;
+						if (_insane) {
+							InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+							// Opcode 0x0B (FUN_1CDA7) uses FUN_223FE for Y compensation on
+							// transformed objects. A global framebuffer Y window shift causes
+							// floating HUD/perspective artifacts in L2 with current renderer.
+							if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B)
+								ra1ViewY = 0;
+						}
+
+						const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
+						const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
 
 						frameWidth = MIN(_width - srcX, _vm->_screenWidth);
 						frameHeight = MIN(_height - srcY, _vm->_screenHeight);


Commit: 19140d2d213510b2dd7fcbd43f8f19c9376e5f71
    https://github.com/scummvm/scummvm/commit/19140d2d213510b2dd7fcbd43f8f19c9376e5f71
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:31+02:00

Commit Message:
SCUMM: RA1: Clear oversized frame buffers

Changed paths:
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index db8e95be5f1..97ddcc9cdfc 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1433,9 +1433,12 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	if (isRA1() && _insane) {
 		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 		interactiveRA1 = rebel1->isInteractiveVideoActive();
-		// Level 2 asteroid stream composes many partial codec1/2+FTCH layers.
-		// Clear per frame for this mode to avoid stale-trail ghosting.
-		forceInteractiveClearRA1 = interactiveRA1 && (rebel1->getCurrentLevel() == 1);
+		const uint16 activeOpcode = rebel1->getActiveGameOpcode();
+		// Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
+		// (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
+		// Keep a conservative fallback for early L2 frames before first 0x0B arrives.
+		forceInteractiveClearRA1 = interactiveRA1 &&
+			(activeOpcode == 0x0B || (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
 	}
 
 	// Keep the previous decoded frame (without post-render overlays) as delta source.


Commit: 9a2fd5d3e2e40ea52aeab46704ea41b713ef31f0
    https://github.com/scummvm/scummvm/commit/9a2fd5d3e2e40ea52aeab46704ea41b713ef31f0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:32+02:00

Commit Message:
SCUMM: RA1: Fix UI rendering

Changed paths:
    engines/scumm/insane/insane_rebel1_render.cpp


diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 664adcabcfe..8a814695376 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -116,6 +116,15 @@ static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
 	return w;
 }
 
+// Approximate FUN_221B7/FUN_20BD3 space-advance behavior from available NUT glyphs.
+// The original reads per-font space width from metadata tables and caps it to 8.
+static int getBankSpaceAdvance(const RA1SpriteBank &bank) {
+	const int exclWidth = getBankStringWidth(bank, "!");
+	if (exclWidth <= 0)
+		return 6;
+	return MIN(exclWidth, 8);
+}
+
 // FUN_1C794: direction bucket in range -4..4 from two points.
 static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
 	int dx = x2 - x1;
@@ -419,15 +428,84 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	}
 }
 
-// drawFontBankString — Simplified version of FUN_221B7 (0x221B7).
-// Original is a multi-font markup-capable renderer; this uses a single font bank.
+// drawFontBankString — FUN_221B7 (0x221B7), partial parity:
+// supports '<'/'>' layer markup and layer-2 space handling used by RA1 HUD/targeting strings.
 void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
-	drawBankString(_hudFontBank, dst, pitch, width, height, x, y, text);
+	if (!text || !dst)
+		return;
+
+	// Original FUN_221B7 layer mapping is table-driven at DAT_2D56 (0x406-byte entries).
+	// Current RA1 integration mirrors the gameplay-relevant subset:
+	//   layer 0/1 -> HUD font bank
+	//   layer 2+  -> TECH font bank
+	int layer = 0;
+	for (int i = 0; text[i] != '\0'; i++) {
+		char ch = text[i];
+		if (ch == '<') {
+			layer++;
+			continue;
+		}
+		if (ch == '>') {
+			layer = MAX(0, layer - 1);
+			continue;
+		}
+
+		const bool techLayer = (layer >= 2);
+		const RA1SpriteBank &bank =
+			(techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+		if (bank.numSprites <= 0)
+			return;
+
+		// FUN_221B7 special-case: when layer==2 and char is space, remap to '!'.
+		if (ch == ' ') {
+			if (techLayer) {
+				drawBankString(bank, dst, pitch, width, height, x, y, "!");
+				x += getBankStringWidth(bank, "!");
+			} else {
+				x += getBankSpaceAdvance(bank);
+			}
+			continue;
+		}
+
+		char glyph[2] = { ch, '\0' };
+		drawBankString(bank, dst, pitch, width, height, x, y, glyph);
+		x += getBankStringWidth(bank, glyph);
+	}
 }
 
-// getFontBankStringWidth — Pre-pass width calculation from FUN_221B7 (0x221B7).
+// getFontBankStringWidth — Width pre-pass from FUN_221B7 (0x221B7), including '<'/'>' markup.
 int InsaneRebel1::getFontBankStringWidth(const char *text) {
-	return getBankStringWidth(_hudFontBank, text);
+	if (!text)
+		return 0;
+
+	int w = 0;
+	int layer = 0;
+	for (int i = 0; text[i] != '\0'; i++) {
+		char ch = text[i];
+		if (ch == '<') {
+			layer++;
+			continue;
+		}
+		if (ch == '>') {
+			layer = MAX(0, layer - 1);
+			continue;
+		}
+
+		const bool techLayer = (layer >= 2);
+		const RA1SpriteBank &bank =
+			(techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+		if (bank.numSprites <= 0)
+			return w;
+
+		if (ch == ' ') {
+			w += techLayer ? getBankStringWidth(bank, "!") : getBankSpaceAdvance(bank);
+			continue;
+		}
+
+		char glyph[2] = { ch, '\0' };
+		w += getBankStringWidth(bank, glyph);
+	}
+	return w;
 }
 
 // renderShip — Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
@@ -510,18 +588,17 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 	}
 }
 
-// renderHUD — FUN_1BBCB (0x1BBCB). Status bar from DISPLAY.NUT with health bar and score.
-// Original layout (320-wide): DAMAGE [green bar] | PILOTS [3 icons] | SCORE [number]
+// renderHUD — FUN_1BBCB (0x1BBCB). Status bar from DISPLAY.NUT with health/lives/score overlays.
 void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
-	if (_displayBank.numSprites == 0)
-		return;
-
 	// Extra life bonus: every 10,000 points (FUN_1BBCB lines 11-27)
 	if (_score / 10000 > _prevScore / 10000) {
 		_lives++;
 	}
 	_prevScore = _score;
 
+	if (_displayBank.numSprites == 0)
+		return;
+
 	const RA1Sprite &bar = _displayBank.sprites[0];
 
 	// DISPLAY.NUT sprite is 320×19 at xoffs=0, yoffs=176 in the original game.
@@ -566,9 +643,9 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 			hudX, hudY, bar.width, bar.height);
 	}
 
-	// Draw health bar from FUN_1BBCB behavior.
-	// Original logic uses current health as fill width and computes x as (0x92 - health),
-	// so the bar is right-anchored and shrinks from left to right as damage increases.
+	// Draw health bar from FUN_1BBCB (0x1BBCB) + FUN_21D66 (0x21D66):
+	// fill rect at (0x92-health, 8), width=health, height=5, color=0.
+	// This is a black "remaining health" fill over the HUD template.
 	{
 		int barMaxW = kMaxHealth;
 		int barH = 5;
@@ -577,24 +654,10 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		int barY = hudY + 8;
 		int fillW = CLIP(healthWidth, 0, barMaxW);
 
-		// Color based on damage level (matching original thresholds from FUN_1BBCB)
-		// Palette indices: 0xD0-0xD7 = greens, 0x60-0x67 = yellows, 0xD8-0xDF = reds
-		byte barColor;
-		if (_health > _tuning.shot * 2)
-			barColor = 0xD5;  // Green (0,192,0) — low damage
-		else if (_health > _tuning.wham * 2)
-			barColor = 0x63;  // Yellow (255,255,31) — moderate damage
-		else
-			barColor = 0xDD;  // Red (192,0,0) — critical
-
-		// Flash effect on damage
-		if (_screenFlash > 0)
-			barColor = 0xFF;  // White flash
-
 		for (int iy = 0; iy < barH && barY + iy < height; iy++) {
 			byte *d = dst + (barY + iy) * pitch + barX;
 			for (int ix = 0; ix < fillW && barX + ix < width; ix++) {
-				d[ix] = barColor;
+				d[ix] = 0x00;
 			}
 		}
 	}
@@ -617,13 +680,35 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		}
 	}
 
-	// Score: 6-digit zero-padded at x=273, y=5 (original format '<<%%06ld' at 0x111,5)
-	if (_hudFontBank.numSprites > 0) {
-		char scoreStr[16];
-		Common::sprintf_s(scoreStr, "%06d", MAX<int>(_score, 0));
+	// Score: FUN_1BBCB (0x1BBCB) -> FUN_21FAF (0x21FAF) with format at 0x6713: "<<%06ld".
+	// Keep the leading "<<" markup so FUN_221B7-equivalent path selects TECH font layer.
+	if (_hudFontBank.numSprites > 0 || _techFontBank.numSprites > 0) {
+		char scoreStr[24];
+		Common::sprintf_s(scoreStr, "<<%06d", MAX<int>(_score, 0));
 		drawFontBankString(dst, pitch, width, height, hudX + 273, hudY + 5, scoreStr);
 	}
 
+	// Low-health indicator from FUN_1BBCB (0x1BBCB):
+	// if (health < miss*2 || health < wham*2 || health < shot*2) and (frame & 8),
+	// draw warning glyph at (0x49, 0x07). Two variants:
+	//   "<<[" when above critical thresholds, "<<\\" when critical.
+	{
+		const bool lowHealthBand =
+			((_health < _tuning.miss * 2) ||
+			 (_health < _tuning.wham * 2) ||
+			 (_health < _tuning.shot * 2)) &&
+			((_frameCounter & 8) != 0);
+		if (lowHealthBand) {
+			const bool aboveCritical =
+				(_health > _tuning.miss) &&
+				(_health > _tuning.wham) &&
+				(_health > _tuning.shot);
+			// FUN_1BBCB pushes string pointers 0x671b ("<<[") or 0x671f ("<<\\") into FUN_221B7.
+			const char *warningStr = aboveCritical ? "<<[" : "<<\\";
+			drawFontBankString(dst, pitch, width, height, hudX + 0x49, hudY + 0x07, warningStr);
+		}
+	}
+
 }
 
 // renderSprite — Simplified version of FUN_20BD3 (0x20BD3) glyph/sprite renderer.


Commit: 8210b3de837696b0159c138ef1647e220fdd5fd9
    https://github.com/scummvm/scummvm/commit/8210b3de837696b0159c138ef1647e220fdd5fd9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:32+02:00

Commit Message:
SCUMM: RA1: Fix level 1 stage transition

Changed paths:
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 8a814695376..4e87e32d01a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -264,6 +264,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// Level 1 (and others): third-person ship flight with accumulators
 		if (_shipBank.numSprites > 0) {
 			updateShipPhysics();
+			// LVL1 assembly flow exits gameplay loops as soon as health drops below 0
+			// (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
+			// Do not render the in-engine death overlay in this path; finish immediately.
+			if (_currentLevel == 0 && _health < 0) {
+				_vm->_smushVideoShouldFinish = true;
+				return;
+			}
 			renderShip(renderBitmap, pitch, width, height);
 		}
 	}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 92a7c2d02a3..e42c2e33718 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -123,6 +123,8 @@ bool InsaneRebel1::runLevel1() {
 
 	// Retry loop — on death with lives, L1NEW plays then jumps back here
 	while (!_vm->shouldQuit()) {
+		bool stage2Started = false;
+
 		// Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
 		_health = kMaxHealth;
 		_damageFlags = 0;
@@ -158,6 +160,7 @@ bool InsaneRebel1::runLevel1() {
 				return false;
 
 			// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
+			stage2Started = true;
 			playInteractiveVideo("LVL1/L1PLAY2.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -170,17 +173,16 @@ bool InsaneRebel1::runLevel1() {
 			return true;
 		}
 
-		// Death sequence (original: 0x165dd-0x166bb)
-		// Random crash variant A or B
-		if (_vm->_rnd.getRandomNumber(1) == 0)
-			playCinematic("LVL1/L1CRASHA.ANM");
-		else
+		// Death sequence (assembly-verified: 0x165dd / 0x16614):
+		// Stage 1 deaths use L1CRASHA; Stage 2 deaths use L1CRASHB.
+		if (stage2Started)
 			playCinematic("LVL1/L1CRASHB.ANM");
+		else
+			playCinematic("LVL1/L1CRASHA.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
-		// Check lives (original: CMP WORD [0x7562], 0 at 0x1666B)
-		_lives--;
+		// Assembly order (0x1666B): check lives first; decrement only on retry path.
 		if (_lives <= 0) {
 			// Game over — L1DEATH then return (original: 0x166C0)
 			playCinematic("LVL1/L1DEATH.ANM");
@@ -192,6 +194,7 @@ bool InsaneRebel1::runLevel1() {
 		playCinematic("LVL1/L1NEW.ANM");
 		if (_vm->shouldQuit())
 			return false;
+		_lives--;
 
 		// Loop back to gameplay (original: JMP 0x16214 — health reset + Stage 1)
 	}


Commit: 8434efb10b0f194c9227caf5a3dfe3d66e9b4515
    https://github.com/scummvm/scummvm/commit/8434efb10b0f194c9227caf5a3dfe3d66e9b4515
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:32+02:00

Commit Message:
SCUMM: RA1: Fix level 1 retry flow

Changed paths:
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index e42c2e33718..89c8f3de938 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -284,11 +284,9 @@ void InsaneRebel1::runGame() {
 		case 1: {
 			bool completed = false;
 			if (_startLevel <= 1) {
-				// Start from Level 1 (default flow)
-				playCinematic("LVL1/L1NEW.ANM");
-				if (_vm->shouldQuit())
-					return;
-
+				// Start from Level 1 (default flow).
+				// Assembly Level 1 entry (0x16100) starts with L1HANGAR/L1CU1,
+				// not with L1NEW (L1NEW is retry-only at 0x16675).
 				completed = runLevel1();
 				if (completed && !_vm->shouldQuit()) {
 					completed = runLevel2();
@@ -298,10 +296,8 @@ void InsaneRebel1::runGame() {
 					}
 				}
 			} else if (_startLevel == 2) {
-				// Direct Level 2 start from Level Select menu
-				playCinematic("LVL2/L2NEW.ANM");
-				if (_vm->shouldQuit())
-					return;
+				// Direct Level 2 start from Level Select menu.
+				// Assembly Level 2 entry (0x16757) starts at L2INTRO; L2NEW is retry-only.
 				completed = runLevel2();
 				if (completed) {
 					debug(1, "InsaneRebel1: Level 2 completed!");


Commit: da5c1e19e044fb21313412170468f73012042f61
    https://github.com/scummvm/scummvm/commit/da5c1e19e044fb21313412170468f73012042f61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:33+02:00

Commit Message:
SCUMM: RA1: Add subtitle fonts

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 225b0678a09..00630052e1e 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -158,6 +158,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		warning("InsaneRebel1: failed to load RA1 HUD font bank (TECHFONT/TALKFONT)");
 	}
 
+	if (loadRA1Nut("SYS/TITLFONT.NUT", _titleFontBank)) {
+		debug(1, "InsaneRebel1: title glyph font loaded from SYS/TITLFONT.NUT (%d chars)", _titleFontBank.numSprites);
+	} else if (loadRA1Nut("SYS/TALKFONT.NUT", _titleFontBank)) {
+		debug(1, "InsaneRebel1: title glyph font fallback loaded from SYS/TALKFONT.NUT (%d chars)", _titleFontBank.numSprites);
+	} else {
+		warning("InsaneRebel1: failed to load title font bank (TITLFONT/TALKFONT)");
+	}
+
 	// FUN_1CB22 uses "<<" layer markers that resolve to TECHFONT in the original.
 	// Keep a dedicated TECH font bank for targeting markers/lock indicators.
 	if (loadRA1Nut("SYS/TECHFONT.NUT", _techFontBank)) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index eba6366f77b..ac7c6e66d3f 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -145,6 +145,7 @@ private:
 
 	RA1SpriteBank _shipBank;
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
+	RA1SpriteBank _titleFontBank; // SYS/TITLFONT.NUT — default subtitle/title layer
 	RA1SpriteBank _hudFontBank;   // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
 	RA1SpriteBank _techFontBank;  // SYS/TECHFONT.NUT — targeting glyph layer ("<<" markers)
 	RA1SpriteBank _bangBank;      // LxBANG.NUT — impact/explosion sprites (10 frames)
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 4e87e32d01a..6d8733bef9d 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -442,8 +442,9 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
 		return;
 
 	// Original FUN_221B7 layer mapping is table-driven at DAT_2D56 (0x406-byte entries).
-	// Current RA1 integration mirrors the gameplay-relevant subset:
-	//   layer 0/1 -> HUD font bank
+	// Current RA1 integration maps subtitle/HUD-relevant layers as:
+	//   layer 0   -> TITLE font bank
+	//   layer 1   -> TALK/HUD font bank
 	//   layer 2+  -> TECH font bank
 	int layer = 0;
 	for (int i = 0; text[i] != '\0'; i++) {
@@ -458,8 +459,12 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
 		}
 
 		const bool techLayer = (layer >= 2);
-		const RA1SpriteBank &bank =
-			(techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+		const bool talkLayer = (layer == 1);
+		const RA1SpriteBank &bank = techLayer ?
+			((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
+			(talkLayer ?
+				_hudFontBank :
+				((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
 		if (bank.numSprites <= 0)
 			return;
 
@@ -499,8 +504,12 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
 		}
 
 		const bool techLayer = (layer >= 2);
-		const RA1SpriteBank &bank =
-			(techLayer && _techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+		const bool talkLayer = (layer == 1);
+		const RA1SpriteBank &bank = techLayer ?
+			((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
+			(talkLayer ?
+				_hudFontBank :
+				((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
 		if (bank.numSprites <= 0)
 			return w;
 
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 97ddcc9cdfc..1daaee85f17 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1884,74 +1884,68 @@ SmushFont *SmushPlayer::ra1GetFont(int font) {
 }
 
 /**
- * RA1 TEXT chunk handler.
- *
- * RA1 TEXT format (different from SCUMM7/COMI TRES):
- *   Bytes 0-3 (BE32): y position on screen
- *   Bytes 4-7 (BE32): parameter (x hint or style)
- *   Bytes 8+:         text content with 0x00 as line separator
- *                     '<' / '>' switch font layers (multi-font markup)
- *
- * Text is rendered centered horizontally at the given y position.
- * The '<' font-switch prefix is stripped before rendering.
+ * RA1 TEXT chunk handler (assembly parity):
+ *   FUN_1FDBC (0x1FDBC) TEXT path -> FUN_221B7 (0x221B7)
+ *   - payload uses two BE32 fields + text bytes with 0x00 separators
+ *   - if first byte is '.', skip that sentinel before rendering
+ *   - '<'/'>' switch font layers (handled by RA1 font-bank renderer)
  */
 void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
 		return;
 
-	InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
+	InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_vm->_insane);
 	if (!rebel1)
 		return;
 
-	int yPos = b.readUint32BE();
-	/*int param =*/ b.readUint32BE();
+	// Keep RA1 on-screen placement path stable (first BE32 drives baseline Y
+	// in current renderer integration), while preserving original text markers.
+	int16 cursorY = (int16)b.readUint32BE();
+	/*int16 xParam =*/ (void)b.readUint32BE();
 
 	int textLen = subSize - 8;
 	if (textLen <= 0)
 		return;
 
-	char *textBuf = (char *)malloc(textLen + 1);
+	byte *textBuf = (byte *)malloc(textLen);
+	if (!textBuf)
+		return;
 	b.read(textBuf, textLen);
-	textBuf[textLen] = '\0';
-
-	int pitch = _width;
-
-	// Split on 0x00 line separators and render each line centered.
-	// Strip '<' / '>' font layer markers (multi-font not yet supported).
-	const char *lineStart = textBuf;
-	int lineY = yPos;
-	int lineHeight = 10;
 
-	while (lineStart < textBuf + textLen) {
-		const char *lineEnd = lineStart;
-		while (lineEnd < textBuf + textLen && *lineEnd != '\0')
-			lineEnd++;
+	// FUN_1FDBC checks first byte at payload+8 and skips a leading '.'
+	// when present (TEXT-at-0x2E sentinel path).
+	int start = 0;
+	if (textLen > 0 && textBuf[0] == '.')
+		start = 1;
 
-		int len = (int)(lineEnd - lineStart);
-		if (len > 0) {
-			const char *cleaned = lineStart;
-			while (cleaned < lineEnd && (*cleaned == '<' || *cleaned == '>'))
-				cleaned++;
+	int remaining = textLen - start;
 
-			int cleanLen = (int)(lineEnd - cleaned);
-			if (cleanLen > 0 && lineY >= 0 && lineY < _height) {
-				char *line = (char *)malloc(cleanLen + 1);
-				memcpy(line, cleaned, cleanLen);
-				line[cleanLen] = '\0';
+	while (remaining > 0) {
+		int lineLen = 0;
+		while (lineLen < remaining && textBuf[start + lineLen] != 0)
+			lineLen++;
 
-				// Center the line horizontally
-				int strWidth = rebel1->getFontBankStringWidth(line);
-				int x = (_width - strWidth) / 2;
-				if (x < 0) x = 0;
+		if (lineLen > 0) {
+			char *line = (char *)malloc(lineLen + 1);
+			if (line) {
+				memcpy(line, textBuf + start, lineLen);
+				line[lineLen] = '\0';
 
-				rebel1->drawFontBankString(_dst, pitch, _width, _height, x, lineY, line);
+				int drawX = (_width - rebel1->getFontBankStringWidth(line)) / 2;
+				if (drawX < 0)
+					drawX = 0;
 
+				rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
 				free(line);
-				lineY += lineHeight;
 			}
 		}
 
-		lineStart = lineEnd + 1;
+		cursorY += 10;
+		int consumed = lineLen;
+		if (consumed < remaining && textBuf[start + consumed] == 0)
+			consumed++;
+		start += consumed;
+		remaining -= consumed;
 	}
 
 	free(textBuf);


Commit: 360d4911f1b3ea64e4d241ce2c6a4545eba7d3f2
    https://github.com/scummvm/scummvm/commit/360d4911f1b3ea64e4d241ce2c6a4545eba7d3f2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:33+02:00

Commit Message:
SCUMM: RA1: Fix subtitle positioning

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index ac7c6e66d3f..0c1d866a24a 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -138,6 +138,7 @@ private:
 public:
 	void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
 	int getFontBankStringWidth(const char *text);
+	int getFontBankLineAdvance(const char *text);
 	void processAudioFrame(int16 feedSize);
 private:
 
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 2344eaf4964..e1e27853e9a 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -21,6 +21,7 @@
 
 #include "common/system.h"
 #include "common/events.h"
+#include "common/str.h"
 
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_player.h"
@@ -137,13 +138,32 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 
 void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
 	_menuFrameCounter++;
+	auto makeTalkText = [](const char *text) {
+		Common::String out("<");
+		out += text;
+		return out;
+	};
+	auto getTalkTextWidth = [&](const char *text) {
+		Common::String styled = makeTalkText(text);
+		return getFontBankStringWidth(styled.c_str());
+	};
+	auto drawTalkText = [&](int x, int y, const char *text) {
+		Common::String styled = makeTalkText(text);
+		drawFontBankString(dst, pitch, width, height, x, y, styled.c_str());
+	};
+	auto getTitleTextWidth = [&](const char *text) {
+		return getFontBankStringWidth(text);
+	};
+	auto drawTitleText = [&](int x, int y, const char *text) {
+		drawFontBankString(dst, pitch, width, height, x, y, text);
+	};
 
 	if (_optionsActive) {
 		// --- Options submenu ---
 		static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
 
-		const int titleW = getFontBankStringWidth("GAME OPTIONS");
-		drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "GAME OPTIONS");
+		const int titleW = getTalkTextWidth("GAME OPTIONS");
+		drawTalkText((width - titleW) / 2, 36, "GAME OPTIONS");
 
 		// Build dynamic option strings
 		char diffLine[64];
@@ -155,10 +175,10 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		const int rowH = 16;
 
 		for (int i = 0; i < 3; i++) {
-			const int textW = getFontBankStringWidth(kOptionsItems[i]);
+			const int textW = getTalkTextWidth(kOptionsItems[i]);
 			const int textX = (width - textW) / 2;
 			const int y = menuY + i * rowH;
-			drawFontBankString(dst, pitch, width, height, textX, y + 1, kOptionsItems[i]);
+			drawTalkText(textX, y + 1, kOptionsItems[i]);
 
 			if (i == _optionsSel) {
 				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
@@ -182,8 +202,8 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 
 	if (_levelSelectActive) {
 		// --- Level select submenu ---
-		const int titleW = getFontBankStringWidth("LEVEL SELECT");
-		drawFontBankString(dst, pitch, width, height, (width - titleW) / 2, 36, "LEVEL SELECT");
+		const int titleW = getTalkTextWidth("LEVEL SELECT");
+		drawTalkText((width - titleW) / 2, 36, "LEVEL SELECT");
 
 		const char *kLevelItems[3] = {
 			"LEVEL 1: FLIGHT TRAINING",
@@ -195,13 +215,13 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		const int rowH = 16;
 
 		for (int i = 0; i < 3; i++) {
-			const int textW = getFontBankStringWidth(kLevelItems[i]);
+			const int textW = getTalkTextWidth(kLevelItems[i]);
 			const int textX = (width - textW) / 2;
 			const int y = menuY + i * rowH;
-			drawFontBankString(dst, pitch, width, height, textX, y + 1, kLevelItems[i]);
+			drawTalkText(textX, y + 1, kLevelItems[i]);
 
 			if (i == _startLevel - 1) {
-				drawFontBankString(dst, pitch, width, height, textX - 12, y + 1, ">");
+				drawTalkText(textX - 12, y + 1, ">");
 			}
 
 			if (i == _levelSelectSel) {
@@ -234,20 +254,20 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	};
 
 	// Center title
-	const int titleW = getFontBankStringWidth("MAIN MENU");
+	const int titleW = getTitleTextWidth("MAIN MENU");
 	const int titleX = (width - titleW) / 2;
-	drawFontBankString(dst, pitch, width, height, titleX, 36, "MAIN MENU");
+	drawTitleText(titleX, 36, "MAIN MENU");
 
 	// Draw menu items centered horizontally
 	const int menuY = 60;
 	const int rowH = 16;
 
 	for (int i = 0; i < 5; i++) {
-		const int textW = getFontBankStringWidth(kMenuItems[i]);
+		const int textW = getTalkTextWidth(kMenuItems[i]);
 		const int textX = (width - textW) / 2;
 		const int y = menuY + i * rowH;
 
-		drawFontBankString(dst, pitch, width, height, textX, y + 1, kMenuItems[i]);
+		drawTalkText(textX, y + 1, kMenuItems[i]);
 
 		// Selection highlight box — flashing border (FUN_004292d0 pattern from RA2)
 		if (i == _menuSelection) {
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6d8733bef9d..93b97d1ef5f 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -125,6 +125,28 @@ static int getBankSpaceAdvance(const RA1SpriteBank &bank) {
 	return MIN(exclWidth, 8);
 }
 
+static const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
+		const RA1SpriteBank &hudBank, const RA1SpriteBank &techBank, int layer) {
+	const bool techLayer = (layer >= 2);
+	const bool talkLayer = (layer == 1);
+	if (techLayer)
+		return (techBank.numSprites > 0) ? techBank : hudBank;
+	if (talkLayer)
+		return hudBank;
+	return (titleBank.numSprites > 0) ? titleBank : hudBank;
+}
+
+static int getBankSpaceHeight(const RA1SpriteBank &bank) {
+	// In FUN_221B7 line advance is derived from the layer's space-glyph height (+4).
+	// With current NUT decoding we approximate that using the '!' glyph (index 0).
+	if (bank.numSprites > 0) {
+		const RA1Sprite &glyph = bank.sprites[0];
+		if (glyph.height > 0)
+			return glyph.height;
+	}
+	return 8;
+}
+
 // FUN_1C794: direction bucket in range -4..4 from two points.
 static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
 	int dx = x2 - x1;
@@ -459,12 +481,7 @@ void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int heigh
 		}
 
 		const bool techLayer = (layer >= 2);
-		const bool talkLayer = (layer == 1);
-		const RA1SpriteBank &bank = techLayer ?
-			((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
-			(talkLayer ?
-				_hudFontBank :
-				((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
+		const RA1SpriteBank &bank = selectLayerBank(_titleFontBank, _hudFontBank, _techFontBank, layer);
 		if (bank.numSprites <= 0)
 			return;
 
@@ -504,12 +521,7 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
 		}
 
 		const bool techLayer = (layer >= 2);
-		const bool talkLayer = (layer == 1);
-		const RA1SpriteBank &bank = techLayer ?
-			((_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank) :
-			(talkLayer ?
-				_hudFontBank :
-				((_titleFontBank.numSprites > 0) ? _titleFontBank : _hudFontBank));
+		const RA1SpriteBank &bank = selectLayerBank(_titleFontBank, _hudFontBank, _techFontBank, layer);
 		if (bank.numSprites <= 0)
 			return w;
 
@@ -524,6 +536,21 @@ int InsaneRebel1::getFontBankStringWidth(const char *text) {
 	return w;
 }
 
+int InsaneRebel1::getFontBankLineAdvance(const char *text) {
+	int layer = 0;
+	if (text) {
+		for (int i = 0; text[i] != '\0'; i++) {
+			if (text[i] == '<')
+				layer++;
+			else if (text[i] == '>')
+				layer = MAX(layer - 1, 0);
+		}
+	}
+
+	const RA1SpriteBank &bank = selectLayerBank(_titleFontBank, _hudFontBank, _techFontBank, layer);
+	return getBankSpaceHeight(bank) + 4;
+}
+
 // renderShip — Ship sprite rendering from FUN_1DEB5 (0x1DEB5) at LAB_1e2b2.
 // Also used by FUN_1E6A7 (0x1E6A7) turret handler via FUN_20BD3.
 void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 1daaee85f17..074f1df0b20 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1898,10 +1898,10 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 	if (!rebel1)
 		return;
 
-	// Keep RA1 on-screen placement path stable (first BE32 drives baseline Y
-	// in current renderer integration), while preserving original text markers.
-	int16 cursorY = (int16)b.readUint32BE();
-	/*int16 xParam =*/ (void)b.readUint32BE();
+	// FUN_1FDBC TEXT path passes BE32 x/y from payload to FUN_221B7 with 0x200
+	// center-alignment flag, so x is an anchor and y is line baseline.
+	const int textAnchorX = b.readSint32BE();
+	int cursorY = b.readSint32BE();
 
 	int textLen = subSize - 8;
 	if (textLen <= 0)
@@ -1927,20 +1927,20 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 
 		if (lineLen > 0) {
 			char *line = (char *)malloc(lineLen + 1);
-			if (line) {
+			if (!line) {
+				cursorY += 12;
+			} else {
 				memcpy(line, textBuf + start, lineLen);
 				line[lineLen] = '\0';
 
-				int drawX = (_width - rebel1->getFontBankStringWidth(line)) / 2;
-				if (drawX < 0)
-					drawX = 0;
-
+				const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
 				rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
+				cursorY += rebel1->getFontBankLineAdvance(line);
 				free(line);
 			}
+		} else {
+			cursorY += rebel1->getFontBankLineAdvance(nullptr);
 		}
-
-		cursorY += 10;
 		int consumed = lineLen;
 		if (consumed < remaining && textBuf[start + consumed] == 0)
 			consumed++;


Commit: 7cad2386cf927c3cbc7e0b3d198e1545c8737c7c
    https://github.com/scummvm/scummvm/commit/7cad2386cf927c3cbc7e0b3d198e1545c8737c7c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:33+02:00

Commit Message:
SCUMM: RA1: Transform GAME zones with camera offsets

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 34a52224555..b22dc27e1be 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,6 +27,15 @@
 
 namespace Scumm {
 
+// FUN_223FE (0x223FE) coordinate transform used by FUN_1C54D/FUN_1C6E9.
+// The original applies camera X offset directly and a Y term derived from
+// DAT_41A2 (+ curve-table contribution). In current RA1 integration we keep
+// the camera-offset part, which fixes left/right corridor asymmetry.
+static void transformPoint223FE(int16 &x, int16 &y, int16 cameraX, int16 cameraY) {
+	x = (int16)(x - cameraX);
+	y = (int16)(y - cameraY);
+}
+
 // updateShipPhysics — FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
 // Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
 // driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
@@ -449,15 +458,23 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
 		// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
 		if (subSize >= 20) {
-			_corridorLeftX = (int16)param1;
-			_corridorTopY = (int16)b.readUint32BE();
+			int16 corridorLeft = (int16)param1;
+			int16 corridorTop = (int16)b.readUint32BE();
 			int16 corridorWidth = (int16)b.readUint32BE();
 			int16 corridorHeight = (int16)b.readUint32BE();
+
+			int16 centerX = corridorLeft + corridorWidth / 2;
+			int16 centerY = corridorTop + corridorHeight / 2;
+			transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+
+			_corridorLeftX = centerX - corridorWidth / 2;
+			_corridorTopY = centerY - corridorHeight / 2;
 			_corridorRightX = _corridorLeftX + corridorWidth;
 			_corridorBottomY = _corridorTopY + corridorHeight;
-			debug(5, "RA1 GAME 0x0D: corridor left=%d top=%d right=%d bottom=%d (w=%d h=%d)",
-				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
-				corridorWidth, corridorHeight);
+			debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
+				corridorLeft, corridorTop, corridorWidth, corridorHeight,
+				_perspectiveX, _perspectiveY,
+				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 		}
 		break;
 
@@ -469,14 +486,21 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			int16 zoneTop = (int16)b.readUint32BE();
 			int16 zoneWidth = (int16)b.readUint32BE();
 			int16 zoneHeight = (int16)b.readUint32BE();
+
+			int16 centerX = zoneLeft + zoneWidth / 2;
+			int16 centerY = zoneTop + zoneHeight / 2;
+			transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+
+			zoneLeft = centerX - zoneWidth / 2;
+			zoneTop = centerY - zoneHeight / 2;
 			int16 zoneRight = zoneLeft + zoneWidth;
 			int16 zoneBottom = zoneTop + zoneHeight;
 			if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
 				_shipPosY > zoneTop && _shipPosY < zoneBottom) {
 				_damageFlags |= 0x10;
 			}
-			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] flags=0x%02x",
-				zoneLeft, zoneTop, zoneRight, zoneBottom, _damageFlags);
+			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
+				zoneLeft, zoneTop, zoneRight, zoneBottom, _perspectiveX, _perspectiveY, _damageFlags);
 		}
 		break;
 


Commit: 08430ff3f98a32949231a34a6f0e7701c0457366
    https://github.com/scummvm/scummvm/commit/08430ff3f98a32949231a34a6f0e7701c0457366
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:34+02:00

Commit Message:
SCUMM: RA1: Preserve left-path branching

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index b22dc27e1be..54fc33c84f6 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -229,13 +229,16 @@ void InsaneRebel1::updateShipPhysics() {
 	// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
 	// After this point, drift goes strongly negative (pushing ship left for the hard path).
 	// If ship is right of center, player chose the right/easy path → switch to L1PLAY1R.
-	// The check fires once when the game counter first reaches the branch point.
-	if (_pathBranchEnabled && !_rightPathSelected && _gameCounter >= kPathBranchCounter) {
+	// Keep this as a one-shot decision: once threshold is reached, lock path.
+	if (_pathBranchEnabled && _gameCounter >= kPathBranchCounter) {
 		if (_shipPosX > kRA1CenterX) {
 			_rightPathSelected = true;
 			_vm->_smushVideoShouldFinish = true;
 			debug(1, "RA1: Right path selected (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
+		} else {
+			debug(1, "RA1: Left path retained (counter=%d, shipX=%d)", _gameCounter, _shipPosX);
 		}
+		_pathBranchEnabled = false;
 	}
 
 	debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",


Commit: 151bad125b8a374c417701107580e9df205b825b
    https://github.com/scummvm/scummvm/commit/151bad125b8a374c417701107580e9df205b825b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:34+02:00

Commit Message:
SCUMM: RA1: Fix level 1 second phase

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_levels.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 00630052e1e..58eb11105fc 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -107,6 +107,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 	_currentLevel = 0;
 	_flyControlMode = 0;
+	_turretEmitterLeftX = 0;
+	_turretEmitterLeftY = 0;
+	_turretEmitterRightX = 0;
+	_turretEmitterRightY = 0;
 	_activeGameOpcode = 0;
 
 	_health = kMaxHealth;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 0c1d866a24a..f284c053543 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -116,6 +116,7 @@ private:
 	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
+	void updateTurretPhysics();
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -145,6 +146,7 @@ private:
 	ScummEngine_v7 *_vm;
 
 	RA1SpriteBank _shipBank;
+	RA1SpriteBank _shipBankAlt; // Secondary ship bank (e.g. L1BANK2 mode-2 sprites)
 	RA1SpriteBank _displayBank;   // SYS/DISPLAY.NUT — bottom status bar
 	RA1SpriteBank _titleFontBank; // SYS/TITLFONT.NUT — default subtitle/title layer
 	RA1SpriteBank _hudFontBank;   // RA1 HUD text glyphs (TECHFONT/TALKFONT via RA1 loader)
@@ -204,6 +206,11 @@ private:
 
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
+	// Mode-2 emitter offsets used by FUN_1D79C when _DAT_75E4 == 2.
+	int16 _turretEmitterLeftX;
+	int16 _turretEmitterLeftY;
+	int16 _turretEmitterRightX;
+	int16 _turretEmitterRightY;
 	// Last per-frame GAME movement handler opcode (0x07/0x08/0x09/0x0A/0x0B/0x1A).
 	// Used to mirror assembly handler-specific overlay pipeline behavior.
 	uint16 _activeGameOpcode;
@@ -291,6 +298,7 @@ private:
 		int16 posY;      // 0x75F6: cursor Y at time of shot
 		int16 centerX;   // 0x75EA: perspective-adjusted X
 		int16 centerY;   // 0x75EE: perspective-adjusted Y
+		int16 variant;   // 0x75FA: emitter table selector (DAT_241F snapshot)
 	};
 	ShotSlot _shotSlots[kMaxShotSlots];
 	int16 _shotAlternator;   // 0x241F: alternates between 0/1
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 54fc33c84f6..244580cf7a0 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -247,6 +247,148 @@ void InsaneRebel1::updateShipPhysics() {
 		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
+// updateTurretPhysics — FUN_1E6A7 (0x1E6A7), opcode 0x08 path.
+// Stage-2 cockpit mode uses different smoothing/clamps than FUN_1DEB5.
+void InsaneRebel1::updateTurretPhysics() {
+	_frameCounter++;
+
+	// FUN_1E6A7 consumes GAME field1 as frame counter (arg6 in dispatcher call).
+	// The 0x10/0x40 gates come from dispatcher arg4 (callback control bits),
+	// not from GAME payload fields.
+	const int32 counter = _gameCounter;
+	const byte modeFlags = 0;
+
+	// RA1 latches consumed by handler family in FUN_1B297.
+	if (_gameLatch5D == 0xFFFF)
+		_damageFlags |= 0x40;
+	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+		_damageFlags |= 0x80;
+
+	if (counter == 0) {
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+	}
+
+	// Damage gate from FUN_1E6A7.
+	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
+		if (_damageFlags == 0x80)
+			_health -= _tuning.shot;
+		else
+			_health -= _tuning.wham;
+
+		if (_health < 0)
+			_deathTimer = kDeathTimerInit;
+
+		_prevDamageFlags = _damageFlags;
+		_damageCooldown = kDamageCooldownInit;
+		_screenFlash = 3;
+	}
+
+	if (_damageCooldown > 0)
+		_damageCooldown--;
+
+	if (_health < 0 && _deathTimer > 0)
+		_deathTimer--;
+
+	// FUN_1E6A7 movement gate: counter > 8 or flags bit 0x40.
+	if (counter > 8 || (modeFlags & 0x40)) {
+		int16 mouseX = (int16)_vm->_mouse.x;
+		int16 mouseY = (int16)_vm->_mouse.y;
+		if (_player && _player->_width > 0 && _player->_height > 0 &&
+			(mouseX >= 320 || mouseY >= 200)) {
+			mouseX = (int16)((mouseX * 320) / _player->_width);
+			mouseY = (int16)((mouseY * 200) / _player->_height);
+		}
+
+		int16 inputX = CLIP<int16>((int16)(mouseX - 160), -127, 127);
+		int16 inputY = CLIP<int16>((int16)(mouseY - 100), -127, 127);
+
+		_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
+		_rollAccum = (_rollAccum * 3) >> 2;
+		_rollAccum = CLIP<int32>(_rollAccum, -0x480, 0x480);
+
+		_liftSmooth += (((int32)_liftSmooth - (int32)inputY) * (int32)_tuning.lift) >> 8;
+		_liftSmooth = (_liftSmooth * 3) >> 2;
+		_liftSmooth = CLIP<int32>(_liftSmooth, -0x32, 0x32);
+
+		if ((modeFlags & 0x10) == 0) {
+			_posAccumX += ((int32)_tuning.slide * _rollAccum) >> 6;
+			_posAccumY -= ((int32)_liftSmooth * 64);
+			_posAccumX = CLIP<int32>(_posAccumX, -0x8C00, 0x8C00);
+			_posAccumY = CLIP<int32>(_posAccumY, -0x4600, 0x3C00);
+		}
+	}
+
+	const int16 offsetX = (int16)(_posAccumX >> 8);
+	const int16 offsetY = (int16)(_posAccumY >> 8);
+
+	// FUN_1D79C tail sets pointer center from offsets:
+	//   _74BE = _74B6 + _74BA
+	//   _74C0 = (_74B8 + _74BC - 0x23) - (_74BC >> 3)
+	_shipPosX = CLIP<int16>((int16)(kRA1CenterX + offsetX), kRA1MinX, kRA1MaxX);
+	_shipPosY = CLIP<int16>((int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3)),
+		kRA1MinY, kRA1MaxY);
+
+	_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
+	_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
+
+	// Direction bucket synthesis from FUN_1E6A7.
+	int dir = 0;
+	if (_flyControlMode == 2) {
+		if (_rollAccum > 0x380) dir = 4;
+		else if (_rollAccum > 0x280) dir = 3;
+		else if (_rollAccum > 0x180) dir = 2;
+		else if (_rollAccum > 0x80) dir = 1;
+		else if (_rollAccum > -0x80) dir = 0;
+		else if (_rollAccum > -0x180) dir = 5;
+		else if (_rollAccum > -0x280) dir = 6;
+		else if (_rollAccum > -0x380) dir = 7;
+		else dir = 8;
+	} else {
+		if (_rollAccum > 0x380) dir = 8;
+		else if (_rollAccum > 0x280) dir = 7;
+		else if (_rollAccum > 0x180) dir = 6;
+		else if (_rollAccum > 0x80) dir = 5;
+		else if (_rollAccum > -0x80) dir = 4;
+		else if (_rollAccum > -0x180) dir = 3;
+		else if (_rollAccum > -0x280) dir = 2;
+		else if (_rollAccum > -0x380) dir = 1;
+		else dir = 0;
+
+		if (offsetY < -0x1E)
+			dir += 0x12;
+		else if (offsetY < 0x1E)
+			dir += 9;
+	}
+	const RA1SpriteBank *shipBank = &_shipBank;
+	if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
+		shipBank = &_shipBankAlt;
+	if (shipBank->numSprites > 0)
+		_shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
+
+	// Regeneration via FUN_1BB0E call in this path.
+	if ((_frameCounter & 0x1F) == 0) {
+		if (_health >= 0 && _health < kMaxHealth)
+			_health++;
+		if (_health >= 0)
+			_score += 1;
+	}
+
+	if (_screenFlash > 0)
+		_screenFlash--;
+
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+	_damageFlags = 0;
+}
+
 // updateAsteroidPhysics — FUN_1CDA7 (0x1CDA7). Opcode 0x0B handler.
 // Uses 10-frame input history averaging instead of accumulators.
 // Ship position = averaged input + center offset.
@@ -426,10 +568,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			_shipPosY = kRA1CenterY;
 		}
 
-		// Keep a conservative default mode after reset.
-		_flyControlMode = 0;
 		_activeGameOpcode = 0;
-		debug(5, "RA1 GAME 0x5E: reset state field1=%d", (int32)param1);
+		debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
 		break;
 
 	case 0x5D:
@@ -553,11 +693,13 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	case 0x19:
 	case 0x1A:
 		_activeGameOpcode = (uint16)opcode;
+		_gameCounter = param1;
 		if (subSize >= 20) {
 			uint32 param2 = b.readUint32BE();
 			uint32 param3 = b.readUint32BE();
 			uint32 param4 = b.readUint32BE();
-			debug(5, "RA1 GAME 0x%02x: params=(%d,%d,%d,%d)", opcode, param1, param2, param3, param4);
+			debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
+				opcode, _gameCounter, param2, param3, param4);
 		}
 		break;
 
@@ -587,11 +729,16 @@ void InsaneRebel1::processShot() {
 	}
 
 	// Record shot at current cursor position.
+	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+	const int16 shipCenterX = turretMode ? (int16)(kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
+	const int16 shipCenterY = turretMode ? (int16)(kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
+
 	_shotSlots[slot].timer = 5;
 	_shotSlots[slot].posX = _shipPosX;
 	_shotSlots[slot].posY = _shipPosY;
-	_shotSlots[slot].centerX = _shipPosX;
-	_shotSlots[slot].centerY = _shipPosY;
+	_shotSlots[slot].centerX = shipCenterX;
+	_shotSlots[slot].centerY = shipCenterY;
+	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
 
 	_playerFired = false;
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 195c7490265..0128443f89c 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -57,11 +57,22 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
 	}
 }
 
+static void resetSpriteBank(RA1SpriteBank &bank) {
+	delete[] bank.sprites;
+	bank.sprites = nullptr;
+	free(bank.decodedData);
+	bank.decodedData = nullptr;
+	bank.numSprites = 0;
+	bank.decodedSize = 0;
+}
+
 // Load an RA1 NUT sprite file (ANIM v1).
 // RA1 NUTs can have odd-size FOBJ chunks padded to 2-byte alignment within
 // FRME containers. This loader handles that padding properly, unlike the
 // shared NutRenderer::loadFont which assumes even-size chunks.
 bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
+	resetSpriteBank(bank);
+
 	ScummFile *file = _vm->instantiateScummFile();
 	_vm->openFile(*file, filename);
 	if (!file->isOpen()) {
@@ -199,6 +210,13 @@ void InsaneRebel1::loadLevelSprites(int level) {
 	if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
 		debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
 	}
+
+	// Secondary ship bank used by some level-specific handlers (e.g. LVL1 mode-2).
+	Common::String bankFileAlt = Common::String::format("LVL%d/L%dBANK2.NUT", level, level);
+	if (!loadRA1Nut(bankFileAlt.c_str(), _shipBankAlt)) {
+		debug(1, "InsaneRebel1: No BANK2 for level %d", level);
+	}
+
 	loadRA1Nut("SYS/DISPLAY.NUT", _displayBank);
 
 	// Explosion sprites — try BANG first, then EXPLD
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 93b97d1ef5f..3cd918f3165 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -190,6 +190,32 @@ static int ra1ShotDirectionBucket(int dir) {
 	return 10;
 }
 
+struct RA1ShotEmitterPair {
+	int16 x1;
+	int16 y1;
+	int16 x2;
+	int16 y2;
+};
+
+// DAT_244A and DAT_251A in ASSAULT.EXE data section, used by FUN_1D79C.
+static const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
+	{ 11, -11, -11, 0 }, { 16, -9, -16, -1 }, { 20, -6, -19, -3 }, { 20, -5, -21, -4 }, { -20, -6, 20, -5 },
+	{ -18, -9, 16, -1 }, { -13, -11, 13, 0 }, { -7, -13, 8, 2 }, { 1, -10, 3, 2 }, { 11, -16, -11, 4 },
+	{ 16, -14, -15, 1 }, { 19, -10, -19, -2 }, { 20, -5, -20, -4 }, { -20, -8, 19, -2 }, { -17, -11, 17, 1 },
+	{ -12, -15, 14, 3 }, { -7, -17, 8, 3 }, { 0, -18, 3, 0 }, { 10, -17, -10, 8 }, { 15, -14, -15, 5 },
+	{ 18, -10, -19, 1 }, { 20, -8, -19, -3 }, { -19, -8, 18, -6 }, { -16, -12, 17, 1 }, { -12, -16, 12, 3 },
+	{ -5, -18, 9, 6 }, { -1, -11, -3, -6 }
+};
+
+static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
+	{ -1, -11, -3, -6 }, { 7, -12, -8, 1 }, { 14, -11, -12, 0 }, { 18, -9, -17, -1 }, { 21, -7, -19, -4 },
+	{ -20, -6, 21, -5 }, { -18, -8, 19, -2 }, { -16, -10, 16, -1 }, { -11, -12, 11, 0 }, { 1, -18, -2, -1 },
+	{ 8, -17, -5, 1 }, { 13, -15, -12, 2 }, { 17, -13, -15, 0 }, { 21, -8, -19, -2 }, { -19, -6, 21, -4 },
+	{ -18, -10, 19, -3 }, { -15, -14, 17, 1 }, { -10, -15, 11, 4 }, { 1, -19, -2, 6 }, { 7, -18, -7, 8 },
+	{ 13, -16, -11, 5 }, { 18, -12, -14, 3 }, { 19, -8, -18, -2 }, { -17, -7, 20, -3 }, { -17, -10, 19, 1 },
+	{ -15, -14, 16, 5 }, { 0, -38, -14, 37 }
+};
+
 // Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
 static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 	int x, int y, const RA1Sprite &spr, uint32 flags) {
@@ -280,27 +306,40 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	int pitch = width;
 
 	if (_currentLevel == 1) {
-		// Level 2: first-person asteroid dodge — no ship sprite, input averaging physics
+		// Level 2: first-person asteroid dodge — opcode 0x0B (FUN_1CDA7).
 		updateAsteroidPhysics();
 	} else {
-		// Level 1 (and others): third-person ship flight with accumulators
-		if (_shipBank.numSprites > 0) {
+		const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+		const bool flightMode = (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
+								 _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+
+		// Dispatch movement path by GAME handler family:
+		//   0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
+		//   0x07/0x09/0x19/0x1A -> flight-family handlers
+		if (turretMode) {
+			updateTurretPhysics();
+		} else if (_shipBank.numSprites > 0 && flightMode) {
 			updateShipPhysics();
-			// LVL1 assembly flow exits gameplay loops as soon as health drops below 0
-			// (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
-			// Do not render the in-engine death overlay in this path; finish immediately.
-			if (_currentLevel == 0 && _health < 0) {
-				_vm->_smushVideoShouldFinish = true;
-				return;
-			}
-			renderShip(renderBitmap, pitch, width, height);
 		}
+
+		// LVL1 assembly flow exits gameplay loops as soon as health drops below 0
+		// (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
+		// Do not render the in-engine death overlay in this path; finish immediately.
+		if (_currentLevel == 0 && _health < 0) {
+			_vm->_smushVideoShouldFinish = true;
+			return;
+		}
+
+		// Ship sprite is present in both flight (0x07 family) and 0x08 turret path.
+		if (flightMode || turretMode)
+			renderShip(renderBitmap, pitch, width, height);
 	}
 
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
-	// in handlers 0x09/0x0A/0x0B/0x1A (FUN_1DABB/FUN_1D79C/FUN_1CDA7/FUN_1D57E).
+	// in handlers 0x09/0x0A/0x0B/0x1A. In LVL1 stage-2 samples, 0x08 drives
+	// turret mode and needs the same targeting overlays enabled.
 	const bool hasTargetingPipeline =
-		(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+		(_activeGameOpcode == 0x08 || _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
 		 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
 	if (hasTargetingPipeline) {
 		processShot();
@@ -390,9 +429,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	}
 }
 
-// renderLaserShots — FUN_1CDA7/FUN_1D79C shot visual path:
-// per active slot, compute left/right direction with FUN_1C794, pick one
-// of 3x5 sprite bands, and render interpolated sprite positions via FUN_20BD3.
+// renderLaserShots — FUN_1CDA7/FUN_1D79C shot visual path.
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
 	if (_laserBank.numSprites <= 0)
 		return;
@@ -403,6 +440,9 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	const int spritesPerSet = 5;
 	const int leftStartX = 0;
 	const int rightStartX = 0x13F; // 319
+	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+	const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
+	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
 
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
@@ -412,13 +452,59 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 			const int targetX = CLIP<int>(_shipPosX, 0, width - 1);
 			const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
 
+			if (turretMode) {
+				// FUN_1D79C chooses emitters in two ways:
+				// - DAT_75E4 == 2: use DAT_75DC..DAT_75E2 fixed offsets
+				// - otherwise: table path DAT_244A/DAT_251A keyed by DAT_74D2
+				int start1X = 0;
+				int start1Y = 0;
+				int start2X = 0;
+				int start2Y = 0;
+				bool haveEmitters = false;
+				if (_flyControlMode == 2) {
+					start1X = shipBaseX - _turretEmitterLeftX;
+					start1Y = shipBaseY + _turretEmitterLeftY;
+					start2X = shipBaseX + _turretEmitterRightX;
+					start2Y = shipBaseY + _turretEmitterRightY;
+					haveEmitters = true;
+				} else if (_shipDirIndex >= 0 && _shipDirIndex < 27) {
+					const RA1ShotEmitterPair &emit =
+						(_shotSlots[i].variant != 0) ? kRA1ShotEmitters244A[_shipDirIndex]
+													 : kRA1ShotEmitters251A[_shipDirIndex];
+					start1X = shipBaseX + emit.x1;
+					start1Y = shipBaseY + emit.y1;
+					start2X = shipBaseX + emit.x2;
+					start2Y = shipBaseY + emit.y2;
+					haveEmitters = true;
+				}
+
+				if (!haveEmitters)
+					continue;
+
+				const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
+				const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
+				const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
+				const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
+				const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
+				const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
+				const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
+				const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
+				const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
+				const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
+
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+				continue;
+			}
+
+			// Fallback for non-turret handlers that still run shot overlays.
 			int leftStartY = 0x96;
 			int rightStartY = 0x96;
 			uint32 leftFlags = 0x83;
 			uint32 rightFlags = 0x2083;
 
-			// FUN_1CDA7 special mode branch (_DAT_75E4 == 1): toggles emitter origin
-			// and flip flags via DAT_2423. Keep behavior for parity when mode is used.
 			if (_flyControlMode == 1) {
 				if (_shotAlternator != 0) {
 					leftFlags = 0x4083;
@@ -439,7 +525,6 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 			const int bucketRight = ra1ShotDirectionBucket(dirRight);
 			const int sprIdxLeft = frame + bucketLeft;
 			const int sprIdxRight = frame + bucketRight;
-
 			const int interpLeftX = leftStartX + (((targetX - leftStartX) * lerp) >> 3);
 			const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
 			const int interpRightX = rightStartX + (((targetX - rightStartX) * lerp) >> 3);
@@ -559,15 +644,26 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 	if (_health < 0 && _deathTimer <= 20)
 		return;
 
-	if (_shipDirIndex < 0 || _shipDirIndex >= _shipBank.numSprites)
+	const RA1SpriteBank *shipBank = &_shipBank;
+	if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
+		shipBank = &_shipBankAlt;
+
+	if (_shipDirIndex < 0 || _shipDirIndex >= shipBank->numSprites)
 		return;
 
-	const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+	const RA1Sprite &spr = shipBank->sprites[_shipDirIndex];
+
+	// In 0x08/0x0A turret handlers, _shipPos holds pointer center (_74BE/_74C0),
+	// while ship sprite center is still (_74B6+_74BA, _74B8+_74BC).
+	int shipScreenX = _shipPosX;
+	int shipScreenY = _shipPosY;
+	if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
+		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+	}
 
-	// FUN_1DEB5 draws at (_74B6 + _74BA, _74B8 + _74BC).
-	// In the current mapping, _shipPosX/_shipPosY already store that screen position.
-	int drawX = _shipPosX - spr.width / 2;
-	int drawY = _shipPosY - spr.height / 2;
+	int drawX = shipScreenX - spr.width / 2;
+	int drawY = shipScreenY - spr.height / 2;
 
 	renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 }
@@ -578,9 +674,14 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 	if (_bangBank.numSprites <= 0)
 		return;
 
-	// Ship screen center position (matches assembly DAT_74B6+DAT_74BA, DAT_74B8+DAT_74BC).
+	// In 0x08/0x0A turret handlers, explosion anchors use ship center
+	// (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
 	int shipScreenX = _shipPosX;
 	int shipScreenY = _shipPosY;
+	if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
+		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+	}
 
 	// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---
 	// When dead and deathTimer > 10: random explosion sprites scatter around ship
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 89c8f3de938..01ff651e813 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -136,6 +136,7 @@ bool InsaneRebel1::runLevel1() {
 		_gameCounter = 0;
 		_pathBranchEnabled = true;
 		_rightPathSelected = false;
+		_flyControlMode = 1;
 
 		// Stage 1 flight — L1PLAY1L (hard/left path)
 		// The first 394 frames are the common section. At counter 394, if
@@ -147,6 +148,7 @@ bool InsaneRebel1::runLevel1() {
 		if (_rightPathSelected && _health >= 0) {
 			debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
 			_pathBranchEnabled = false;
+			_flyControlMode = 1;
 			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -160,6 +162,13 @@ bool InsaneRebel1::runLevel1() {
 				return false;
 
 			// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
+			// Assembly @0x16396..0x163E5 switches to mode 2 and sets
+			// FUN_1D79C emitter offsets in DAT_75DC..DAT_75E2.
+			_flyControlMode = 2;
+			_turretEmitterLeftX = 10;
+			_turretEmitterLeftY = -5;
+			_turretEmitterRightX = 10;
+			_turretEmitterRightY = -5;
 			stage2Started = true;
 			playInteractiveVideo("LVL1/L1PLAY2.ANM");
 			if (_vm->shouldQuit())
@@ -208,6 +217,7 @@ bool InsaneRebel1::runLevel2() {
 	debug(1, "InsaneRebel1: Running level 2");
 
 	_currentLevel = 1;
+	_flyControlMode = 1;
 	loadLevelSprites(2);
 	loadTuningForLevel(1);
 
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 074f1df0b20..479d2242317 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -351,6 +351,17 @@ void SmushPlayer::init(int32 speed) {
 	_fastForwardToFrame = 0;
 	_ra1HasCleanFrame = false;
 
+	// RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
+	// new ANM so data from a previous segment isn't re-applied.
+	free(_ra1ObjOverlayData);
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
+	_ra1ObjOverlayCodec = 0;
+	_ra1ObjOverlayLeft = 0;
+	_ra1ObjOverlayTop = 0;
+	_ra1ObjOverlayWidth = 0;
+	_ra1ObjOverlayHeight = 0;
+
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
 


Commit: fe8658365701272b8c801be3f57217519820119e
    https://github.com/scummvm/scummvm/commit/fe8658365701272b8c801be3f57217519820119e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:34+02:00

Commit Message:
SCUMM: RA1: Refine level 1 second phase

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 244580cf7a0..6f76afb2a91 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,6 +27,13 @@
 
 namespace Scumm {
 
+// LVL1 stage-2 0x5D damage/event codes observed in L1PLAY2.ANM.
+// Original DOS loop uses table/mask-driven latch routing before FUN_1E6A7;
+// in ScummVM's direct GAME dispatch path, map this known range explicitly.
+static inline bool isL1Stage2DamageLatch(uint16 code) {
+	return code >= 6 && code <= 18;
+}
+
 // FUN_223FE (0x223FE) coordinate transform used by FUN_1C54D/FUN_1C6E9.
 // The original applies camera X offset directly and a Y term derived from
 // DAT_41A2 (+ curve-table contribution). In current RA1 integration we keep
@@ -178,7 +185,8 @@ void InsaneRebel1::updateShipPhysics() {
 	// RA1 FUN_1B297-style latches from GAME opcodes:
 	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
-	if (_gameLatch5D == 0xFFFF)
+	if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
+		isL1Stage2DamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
 	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
 		_damageFlags |= 0x80;
@@ -259,7 +267,8 @@ void InsaneRebel1::updateTurretPhysics() {
 	const byte modeFlags = 0;
 
 	// RA1 latches consumed by handler family in FUN_1B297.
-	if (_gameLatch5D == 0xFFFF)
+	if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
+		isL1Stage2DamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
 	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
 		_damageFlags |= 0x80;
@@ -307,7 +316,11 @@ void InsaneRebel1::updateTurretPhysics() {
 			mouseY = (int16)((mouseY * 200) / _player->_height);
 		}
 
-		int16 inputX = CLIP<int16>((int16)(mouseX - 160), -127, 127);
+		// FUN_1F3F8/FUN_231BE preprocesses mouse deltas before FUN_1E6A7 consumes
+		// DAT_756C/DAT_756E. Keep X in 320-space then scale to ±127 (RA2 parity),
+		// Y clamped to ±127.
+		int16 rawInputX = CLIP<int16>((int16)(mouseX - 160), -160, 160);
+		int16 inputX = (int16)((rawInputX * 127) / 160);
 		int16 inputY = CLIP<int16>((int16)(mouseY - 100), -127, 127);
 
 		_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
@@ -332,9 +345,8 @@ void InsaneRebel1::updateTurretPhysics() {
 	// FUN_1D79C tail sets pointer center from offsets:
 	//   _74BE = _74B6 + _74BA
 	//   _74C0 = (_74B8 + _74BC - 0x23) - (_74BC >> 3)
-	_shipPosX = CLIP<int16>((int16)(kRA1CenterX + offsetX), kRA1MinX, kRA1MaxX);
-	_shipPosY = CLIP<int16>((int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3)),
-		kRA1MinY, kRA1MaxY);
+	_shipPosX = (int16)(kRA1CenterX + offsetX);
+	_shipPosY = (int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3));
 
 	_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);


Commit: a5dd30066d934ec92644ca9d15b09d42292b5e67
    https://github.com/scummvm/scummvm/commit/a5dd30066d934ec92644ca9d15b09d42292b5e67
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:35+02:00

Commit Message:
SCUMM: RA1: Improve level 2 perspective and movement

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 58eb11105fc..d6e80d6a921 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -104,6 +104,14 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
 	_avgInputX = 0;
 	_avgInputY = 0;
+	_mouseOffsetX = 0;
+	_mouseOffsetY = 0;
+	_mouseBiasX = 0;
+	_mouseBiasY = 0;
+	_mousePrevBiasX = 0;
+	_mousePrevBiasY = 0;
+	_mouseBiasLatch = false;
+	_mouseRecentering = false;
 
 	_currentLevel = 0;
 	_flyControlMode = 0;
@@ -145,10 +153,15 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_fireCooldown = 0;
 	memset(_shotSlots, 0, sizeof(_shotSlots));
 	_shotAlternator = 0;
+	_shotSideToggle = false;
 	_targetProximity = 0;
 	_prevTargetProx = 0;
+	_targetAnimCounter = 0;
 	_targetCount = 0;
 	_prevTargetCount = 0;
+	memset(_targetBoxX, 0, sizeof(_targetBoxX));
+	memset(_targetBoxY, 0, sizeof(_targetBoxY));
+	memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
 	memset(_gostSlots, 0, sizeof(_gostSlots));
 	_gostSlotIdx = 0;
 	_killCount = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index f284c053543..a49d0676215 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -86,6 +86,7 @@ public:
 	bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
 	int getCurrentLevel() const { return _currentLevel; }
 	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
+	int16 getPerspectiveX() const { return _perspectiveX; }
 
 	// Game flow (matching original at 0x15597)
 	void runGame();
@@ -117,10 +118,12 @@ private:
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
 	void updateTurretPhysics();
+	void preprocessMouseAxes(int16 &inputX, int16 &inputY);
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
 	void renderExplosions(byte *dst, int pitch, int width, int height);
+	void renderTargetBoxes(byte *dst, int pitch, int width, int height);
 	void renderTargeting(byte *dst, int pitch, int width, int height);
 	void renderGostSlots(byte *dst, int pitch, int width, int height);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
@@ -197,6 +200,14 @@ private:
 	int16 _viewHistoryY[kInputHistorySize];   // 0x75BC: viewport vertical history
 	int16 _avgInputX;    // smoothed horizontal input (clamped to [-0xA0, 0xA0])
 	int16 _avgInputY;    // smoothed vertical input (clamped to [-0x46, 0x41])
+	int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
+	int16 _mouseOffsetY; // 0x9760-style accumulated recenter offset in DOS 200-space
+	int16 _mouseBiasX;   // 0x9774: current preprocessed horizontal bias
+	int16 _mouseBiasY;   // 0x9772: current preprocessed vertical bias
+	int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
+	int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
+	bool _mouseBiasLatch;  // 0x4486: one-frame large-jump latch
+	bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
 
 	// 0x0B handler physics update (asteroid/surface levels)
 	void updateAsteroidPhysics();
@@ -302,12 +313,18 @@ private:
 	};
 	ShotSlot _shotSlots[kMaxShotSlots];
 	int16 _shotAlternator;   // 0x241F: alternates between 0/1
+	bool _shotSideToggle;    // 0x2423: 0x0B side-toggle for mode-1 beam emitters
 
 	// Targeting state — FUN_1C0EF (0x1C0EF)
 	int16 _targetProximity;  // 0x7558: 0=none, 1=near, 2=on-target
 	int16 _prevTargetProx;   // 0x755A: previous frame's proximity
+	int16 _targetAnimCounter; // 0x755C: lock-marker animation counter
 	int16 _targetCount;      // 0x7552: active targets this frame
 	int16 _prevTargetCount;  // 0x7554: previous frame target count
+	static const int kMaxTargetBoxes = 20;
+	int16 _targetBoxX[kMaxTargetBoxes];       // 0x74DA: per-target overlay X
+	int16 _targetBoxY[kMaxTargetBoxes];       // 0x7502: per-target overlay Y
+	int16 _targetBoxVariant[kMaxTargetBoxes]; // 0x752A: size/near bucket ('i' + bucket)
 
 	// GOST hit animation slots (10 slots) — FUN_1C9CD (0x1C9CD)
 	static const int kMaxGostSlots = 10;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 6f76afb2a91..623fb05b4af 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -43,6 +43,42 @@ static void transformPoint223FE(int16 &x, int16 &y, int16 cameraX, int16 cameraY
 	y = (int16)(y - cameraY);
 }
 
+// preprocessMouseAxes — FUN_231BE (0x231BE) centered-axis output law, adapted to
+// ScummVM's absolute 320x200 mouse space.
+// The original DOS path combines a full-scale centered axis term with an extra
+// mouse-bias term `(rawX-0x140)>>2`, `(rawY-100)>>1` maintained by
+// FUN_23115/FUN_231BE recenter globals. In ScummVM, mirroring the DOS recenter
+// box makes LVL2 control unusably constrained, so keep the FUN_231BE output law
+// (normalized axis + bias) but feed it directly from logical mouse coordinates.
+// That preserves the handler's expected `_DAT_756C/_DAT_756E` amplitude without
+// introducing the DOS safe-window box or extra latency.
+void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
+	int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
+	int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
+	const int16 rawX = (int16)(logicalX << 1);
+	const int16 rawY = logicalY;
+	const int16 deltaX = (int16)(logicalX - kRA1CenterX);
+	const int16 deltaY = (int16)(logicalY - kRA1CenterY);
+	const int16 normX = (int16)(((int32)deltaX * 127) / 160);
+	const int16 normY = (int16)(((int32)deltaY * 127) / 100);
+	const int16 biasX = (int16)((rawX - 0x140) >> 2);
+	const int16 biasY = (int16)((rawY - 100) >> 1);
+	const int16 scaledX = (int16)(normX + biasX);
+	const int16 scaledY = (int16)(normY + biasY);
+
+	_mouseBiasX = scaledX;
+	_mouseBiasY = scaledY;
+	_mousePrevBiasX = scaledX;
+	_mousePrevBiasY = scaledY;
+	_mouseBiasLatch = false;
+	_mouseRecentering = false;
+	_mouseOffsetX = 0;
+	_mouseOffsetY = 0;
+
+	inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
+	inputY = CLIP<int16>(scaledY, -127, 127);
+}
+
 // updateShipPhysics — FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
 // Roll accumulator (_74CA) driven by input, position accumulators (_74C2/_74C6)
 // driven by roll + drift + cross-coupling. Ship position = base + accum >> 8.
@@ -456,18 +492,9 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	// --- Cursor and perspective smoothing (FUN_1CDA7) ---
 	// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
-	int16 mouseX = (int16)_vm->_mouse.x;
-	int16 mouseY = (int16)_vm->_mouse.y;
-	if (_player && _player->_width > 0 && _player->_height > 0 &&
-		(mouseX >= 320 || mouseY >= 200)) {
-		mouseX = (int16)((mouseX * 320) / _player->_width);
-		mouseY = (int16)((mouseY * 200) / _player->_height);
-	}
-	int16 inputX = (int16)(mouseX - kRA1CenterX);
-	// Assembly uses an inverted-Y convention in the averaging path.
-	// In ScummVM screen coords (Y grows downward), convert here so moving
-	// mouse up moves the pointer up on screen.
-	int16 inputY = (int16)(kRA1CenterY - mouseY);
+	int16 inputX = 0;
+	int16 inputY = 0;
+	preprocessMouseAxes(inputX, inputY);
 	inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
 	inputY = CLIP<int16>(inputY, -100, 100);
 
@@ -559,16 +586,29 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
 		_avgInputX = 0;
 		_avgInputY = 0;
+		_mouseOffsetX = 0;
+		_mouseOffsetY = 0;
+		_mouseBiasX = 0;
+		_mouseBiasY = 0;
+		_mousePrevBiasX = 0;
+		_mousePrevBiasY = 0;
+		_mouseBiasLatch = false;
+		_mouseRecentering = false;
 
 		// Shooting/targeting reset
 		_playerFired = false;
 		_fireCooldown = 0;
 		memset(_shotSlots, 0, sizeof(_shotSlots));
 		_shotAlternator = 0;
+		_shotSideToggle = false;
 		_targetProximity = 0;
 		_prevTargetProx = 0;
+		_targetAnimCounter = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
+		memset(_targetBoxX, 0, sizeof(_targetBoxX));
+		memset(_targetBoxY, 0, sizeof(_targetBoxY));
+		memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
 		memset(_gostSlots, 0, sizeof(_gostSlots));
 		_gostSlotIdx = 0;
 		_killCount = 0;
@@ -764,6 +804,20 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 	int16 snap = _tuning.snap;
 	int16 curX = _shipPosX;
 	int16 curY = _shipPosY;
+	const int slot = _targetCount;
+
+	if (slot < kMaxTargetBoxes) {
+		_targetBoxX[slot] = (int16)((left + right) / 2);
+		_targetBoxY[slot] = (int16)((top + bottom) / 2);
+
+		const int height = bottom - top;
+		int16 glyphVariant = 0;
+		if (height >= 0x10)
+			glyphVariant = 2;
+		else if (height > 3)
+			glyphVariant = 1;
+		_targetBoxVariant[slot] = glyphVariant;
+	}
 
 	_targetCount++;
 
@@ -772,6 +826,8 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 		curY > top - snap - 5 && curY < bottom + snap + 5) {
 		if (_targetProximity == 0)
 			_targetProximity = 1;  // Near
+		if (slot < kMaxTargetBoxes)
+			_targetBoxVariant[slot] = CLIP<int16>((int16)(_targetBoxVariant[slot] + 3), 0, 5);
 
 		// Check tight lock: cursor within target + snap (no extra margin)
 		if (curX > left - snap && curX < right + snap &&
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 3cd918f3165..ea9fdcb5f7a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -27,6 +27,16 @@
 
 namespace Scumm {
 
+static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
+	if (!rebel1 || !rebel1->isInteractiveVideoActive())
+		return 0;
+
+	// In opcode 0x0B (FUN_1CDA7), marker/shot coordinates are in the gameplay
+	// window. Under ScummVM's FUN_224FD crop emulation, shift them into the
+	// 384-wide source buffer so they stay aligned after the source-window crop.
+	return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
+}
+
 static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
 	int x, int y, const char *text) {
 	if (!dst || !text || bank.numSprites <= 0)
@@ -335,6 +345,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			renderShip(renderBitmap, pitch, width, height);
 	}
 
+	// GAME handlers in the original update FUN_224FD during the same frame that
+	// the new control state is computed. Sync the current frame's viewport window
+	// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
+	if (_player) {
+		_player->_ra1ViewportOffsetX = _perspectiveX;
+		_player->_ra1ViewportOffsetY = (_activeGameOpcode == 0x0B) ? 0 : _perspectiveY;
+	}
+
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
 	// in handlers 0x09/0x0A/0x0B/0x1A. In LVL1 stage-2 samples, 0x08 drives
 	// turret mode and needs the same targeting overlays enabled.
@@ -342,6 +360,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		(_activeGameOpcode == 0x08 || _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
 		 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
 	if (hasTargetingPipeline) {
+		renderTargetBoxes(renderBitmap, pitch, width, height);
 		processShot();
 		for (int i = 0; i < kMaxShotSlots; i++) {
 			if (_shotSlots[i].timer > 0)
@@ -364,11 +383,28 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	renderHUD(renderBitmap, pitch, width, height);
 }
 
+// renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
+void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
+	const int overlayX = ra1OverlayViewOffsetX(this);
+
+	for (int i = _targetCount - 1; i >= 0; --i) {
+		if (i >= kMaxTargetBoxes)
+			continue;
+
+		char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
+		drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
+	}
+
+	_prevTargetCount = _targetCount;
+	_targetCount = 0;
+}
+
 // renderTargeting — FUN_1CB22 (0x1CB22). Targeting/lock-on indicator.
 // The original does not draw a hardcoded pixel cross; it renders glyph markers
 // whose state depends on _targetProximity.
 void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+	const int overlayX = ra1OverlayViewOffsetX(this);
 	if (markerBank.numSprites > 0) {
 		// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
 		// Baseline RA1 targeting uses '^' and animation e..h.
@@ -377,19 +413,20 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 		// Lock indicator at fixed center positions:
 		// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
 		if (_targetProximity > 0) {
-			drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x78, "]");
+			drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, "]");
 			if (_targetProximity > 1)
-				drawBankString(markerBank, dst, pitch, width, height, 0xA0, 0x7E, "a");
+				drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, "a");
 		}
 
 		// Pointer glyph at current aim position. Original uses two variants:
 		// default marker ('^' or 'x') and animated lock marker (e..h or y..|).
 		char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
 		if (_targetProximity > 1) {
-			marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_frameCounter & 3));
+			_targetAnimCounter++;
+			marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
 		}
 
-		int cursorX = CLIP<int>(_shipPosX, 0, width - 1);
+		int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
 		int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
 		int markerW = getBankStringWidth(markerBank, marker);
 		drawBankString(markerBank, dst, pitch, width, height, cursorX - markerW / 2, cursorY, marker);
@@ -398,10 +435,6 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 	// Save previous proximity for next frame
 	_prevTargetProx = _targetProximity;
 	_targetProximity = 0;
-
-	// Reset per-frame target count — FUN_1C940 (0x1C940)
-	_prevTargetCount = _targetCount;
-	_targetCount = 0;
 	_lastHitTarget = 0;
 }
 
@@ -411,6 +444,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	if (_bangBank.numSprites <= 0)
 		return;
 
+	const int overlayX = ra1OverlayViewOffsetX(this);
 	for (int i = 0; i < kMaxGostSlots; i++) {
 		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
 			int sprIdx = _gostSlots[i].frame;
@@ -418,7 +452,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 				sprIdx = _bangBank.numSprites - 1;
 
 			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
-			int drawX = _gostSlots[i].posX - spr.width / 2;
+			int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
 			int drawY = _gostSlots[i].posY - spr.height / 2;
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 
@@ -438,6 +472,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	// Entry 0 unused.
 	static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
 	const int spritesPerSet = 5;
+	const int overlayX = ra1OverlayViewOffsetX(this);
 	const int leftStartX = 0;
 	const int rightStartX = 0x13F; // 319
 	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
@@ -449,7 +484,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 			const int timer = _shotSlots[i].timer;
 			const int lerp = kShotLerpByTimer[timer];
 			const int frame = spritesPerSet - timer;
-			const int targetX = CLIP<int>(_shipPosX, 0, width - 1);
+			const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
 			const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
 
 			if (turretMode) {
@@ -506,7 +541,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 			uint32 rightFlags = 0x2083;
 
 			if (_flyControlMode == 1) {
-				if (_shotAlternator != 0) {
+				if (_shotSideToggle) {
 					leftFlags = 0x4083;
 					leftStartY = 0;
 					rightStartY = 0x96;
@@ -516,18 +551,20 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 					rightStartY = 0;
 				}
 				if (timer == 1)
-					_shotAlternator = 1 - _shotAlternator;
+					_shotSideToggle = !_shotSideToggle;
 			}
 
-			const int dirLeft = ra1ShotDirection(targetX, targetY, leftStartX, leftStartY);
-			const int dirRight = ra1ShotDirection(rightStartX, targetY, targetX, rightStartY);
+			const int startLeftX = overlayX + leftStartX;
+			const int startRightX = overlayX + rightStartX;
+			const int dirLeft = ra1ShotDirection((int16)startLeftX, (int16)leftStartY, (int16)targetX, (int16)targetY);
+			const int dirRight = ra1ShotDirection((int16)startRightX, (int16)rightStartY, (int16)targetX, (int16)targetY);
 			const int bucketLeft = ra1ShotDirectionBucket(dirLeft);
 			const int bucketRight = ra1ShotDirectionBucket(dirRight);
 			const int sprIdxLeft = frame + bucketLeft;
 			const int sprIdxRight = frame + bucketRight;
-			const int interpLeftX = leftStartX + (((targetX - leftStartX) * lerp) >> 3);
+			const int interpLeftX = startLeftX + (((targetX - startLeftX) * lerp) >> 3);
 			const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
-			const int interpRightX = rightStartX + (((targetX - rightStartX) * lerp) >> 3);
+			const int interpRightX = startRightX + (((targetX - startRightX) * lerp) >> 3);
 			const int interpRightY = rightStartY + (((targetY - rightStartY) * lerp) >> 3);
 
 			if (sprIdxLeft >= 0 && sprIdxLeft < _laserBank.numSprites) {
@@ -674,9 +711,10 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 	if (_bangBank.numSprites <= 0)
 		return;
 
+	const int overlayX = ra1OverlayViewOffsetX(this);
 	// In 0x08/0x0A turret handlers, explosion anchors use ship center
 	// (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
-	int shipScreenX = _shipPosX;
+	int shipScreenX = overlayX + _shipPosX;
 	int shipScreenY = _shipPosY;
 	if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
 		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 479d2242317..46e5184c6d3 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -485,6 +485,19 @@ void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 			int left = _storedFobjLeft + fetchX;
 			int top = _storedFobjTop + fetchY;
 
+			if (_insane) {
+				InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+				// FUN_1FDBC routes FTCH through FUN_28D0A, which forces FUN_20D43
+				// flag 0x8 instead of replaying a plain raw FOBJ blit. In LVL2,
+				// the stored object is the 320-wide gameplay-window patch, so under
+				// ScummVM's 384-buffer crop emulation it must be anchored to the
+				// current window origin rather than left in raw chunk space.
+				if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
+					_storedFobjWidth == _vm->_screenWidth) {
+					left += _ra1ViewportOffsetX;
+				}
+			}
+
 			debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
 				_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
 			decodeFrameObject(storedCodec, _storedFobjData, left, top,


Commit: a17bfbcf4b9de1a05f6d5fbb84424794efb28ff1
    https://github.com/scummvm/scummvm/commit/a17bfbcf4b9de1a05f6d5fbb84424794efb28ff1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:35+02:00

Commit Message:
SCUMM: RA1: Refactor SMUSH integration

Changed paths:
  A engines/scumm/smush/smush_player_ra1.cpp
    engines/scumm/module.mk
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 2011a5049f7..b7fe661ee74 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -154,6 +154,7 @@ MODULE_OBJS += \
 	smush/codec_ra2.o \
 	smush/smush_multi_font.o \
 	smush/smush_player.o \
+	smush/smush_player_ra1.o \
 	smush/smush_player_ra2.o
 
 ifdef USE_ARM_SMUSH_ASM
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index aac942458f6..424b222793e 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1813,7 +1813,7 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 		initScreens(0, 200);
 
 		_insane = new InsaneRebel1(this);
-		_splayer = new SmushPlayer(this, nullptr, _insane);
+		_splayer = new SmushPlayerRebel1(this, nullptr, _insane);
 
 		_macGui = nullptr;
 		_charset = new CharsetRendererV7(this);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 46e5184c6d3..b86c86f4f31 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -350,17 +350,9 @@ void SmushPlayer::init(int32 speed) {
 	_scrollY = 0;
 	_fastForwardToFrame = 0;
 	_ra1HasCleanFrame = false;
-
 	// RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
 	// new ANM so data from a previous segment isn't re-applied.
-	free(_ra1ObjOverlayData);
-	_ra1ObjOverlayData = nullptr;
-	_ra1ObjOverlayDataSize = 0;
-	_ra1ObjOverlayCodec = 0;
-	_ra1ObjOverlayLeft = 0;
-	_ra1ObjOverlayTop = 0;
-	_ra1ObjOverlayWidth = 0;
-	_ra1ObjOverlayHeight = 0;
+	resetGameVideoState();
 
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
@@ -413,18 +405,8 @@ void SmushPlayer::release() {
 	} else {
 		free(_frameBuffer);
 		_frameBuffer = nullptr;
-		if (isRA1()) {
-			free(_storedFobjData);
-			_storedFobjData = nullptr;
-			_storedFobjDataSize = 0;
-			_storedFobjCodec = 0;
-			_storedFobjParm2 = 0;
-			_storedFobjLeft = 0;
-			_storedFobjTop = 0;
-			_storedFobjWidth = 0;
-			_storedFobjHeight = 0;
-		}
 	}
+	releaseGameVideoState();
 
 	_IACTstream = nullptr;
 
@@ -456,58 +438,16 @@ void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
 
 void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
-	if (isRA1())
-		assert(subSize >= 4);
-	else
-		assert(subSize >= 6);
+	assert(subSize >= (isRA1() ? 4 : 6));
 
 	if (isRA2()) {
 		ra2HandleFetch(b);
 		return;
 	}
 
-	if (isRA1()) {
-		// RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
-		// Chunk layout: BE32 id/flags, BE32 fetchX, BE32 fetchY.
-		uint32 fetchId = 0;
-		int32 fetchX = 0;
-		int32 fetchY = 0;
-		if (subSize >= 4)
-			fetchId = b.readUint32BE();
-		if (subSize >= 12) {
-			fetchX = b.readSint32BE();
-			fetchY = b.readSint32BE();
-		}
-
-		if (_storedFobjData != nullptr && _storedFobjDataSize > 0) {
-			const int storedCodec = _storedFobjCodec & 0xFF;
-			const uint8 storedParam = (uint8)((_storedFobjCodec >> 8) & 0xFF);
-			int left = _storedFobjLeft + fetchX;
-			int top = _storedFobjTop + fetchY;
-
-			if (_insane) {
-				InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-				// FUN_1FDBC routes FTCH through FUN_28D0A, which forces FUN_20D43
-				// flag 0x8 instead of replaying a plain raw FOBJ blit. In LVL2,
-				// the stored object is the 320-wide gameplay-window patch, so under
-				// ScummVM's 384-buffer crop emulation it must be anchored to the
-				// current window origin rather than left in raw chunk space.
-				if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
-					_storedFobjWidth == _vm->_screenWidth) {
-					left += _ra1ViewportOffsetX;
-				}
-			}
-
-			debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
-				_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
-			decodeFrameObject(storedCodec, _storedFobjData, left, top,
-				_storedFobjWidth, _storedFobjHeight, _storedFobjDataSize,
-				storedParam, _storedFobjParm2);
-		} else {
-			debug("RA1 FTCH: frame=%d id=0x%08x with no stored FOBJ data", _frame, fetchId);
-		}
+	// RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
+	if (handleGameFetch(subSize, b))
 		return;
-	}
 
 	if (_frameBuffer != nullptr) {
 		memcpy(_dst, _frameBuffer, _width * _height);
@@ -715,10 +655,8 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
 	// RA1 TEXT chunks have a different format: 2 × BE32 header + text data.
 	// Route to dedicated handler.
-	if (isRA1() && subType == MKTAG('T','E','X','T')) {
-		ra1HandleText(subSize, b);
+	if (handleGameTextResource(subType, subSize, b))
 		return;
-	}
 
 	int pos_x = b.readSint16LE();
 	int pos_y = b.readSint16LE();
@@ -907,9 +845,7 @@ void SmushPlayer::handleDeltaPalette(int32 subSize, Common::SeekableReadStream &
 			_pal[i] = CLIP<int32>(_shiftedDeltaPal[i] >> 7, 0, 255);
 		}
 
-		if (isRA1()) {
-			_pal[0] = _pal[1] = _pal[2] = 0;
-		}
+		adjustGamePalette();
 
 		setDirtyColors(0, 255);
 	} else {
@@ -933,10 +869,7 @@ void SmushPlayer::handleNewPalette(int32 subSize, Common::SeekableReadStream &b)
 		return;
 
 	readPalette(_pal, b);
-
-	if (isRA1()) {
-		_pal[0] = _pal[1] = _pal[2] = 0;
-	}
+	adjustGamePalette();
 
 	setDirtyColors(0, 255);
 }
@@ -1678,7 +1611,8 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 						}
 
 						objPos += 8 + embSize;
-						if (embSize & 1) objPos++;
+						if (embSize & 1)
+							objPos++;
 					}
 					free(objBuf);
 				}
@@ -1711,15 +1645,11 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		}
 	}
 
-	// RA1: Re-render saved OBJ cockpit overlay every frame (drawn once in
-	// frame 0's OBJ chunk, then scene FOBJs overwrite it each frame).
 	if (isRA1() && _ra1ObjOverlayData != nullptr && _frame > 0) {
 		Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
 		handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
 	}
 
-	// Snapshot decoded frame before post-render overlays so next frame starts from
-	// clean video data (avoids ghost trails from moving HUD/cursor overlays).
 	if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
 		_dst && _width > 0 && _height > 0) {
 		const int frameBytes = _width * _height;
@@ -1778,14 +1708,7 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 		if (!_skipPalette) {
 			byte *palettePtr = &headerContent[6];
 			memcpy(_pal, palettePtr, sizeof(_pal));
-
-			if (isRA1()) {
-				// RA1: force palette index 0 to black. Some ANM files store
-				// non-black values (e.g. O1OPEN.ANM has palette[0] = blue)
-				// but the original engine always displays index 0 as black.
-				// XPAL delta animation skips index 0, so it's never corrected.
-				_pal[0] = _pal[1] = _pal[2] = 0;
-			}
+			adjustGamePalette();
 
 			if (isRA2())
 				ra2ResetDeltaPalette();
@@ -1793,22 +1716,7 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			setDirtyColors(0, 255);
 		}
 
-		if (isRA1()) {
-			// v1 AHDR: pre-allocate 384x242 special buffer and set _dst so that
-			// procPostRendering has a valid target. Keep _width/_height at 0 so
-			// updateScreen() is NOT called until the first FOBJ sets dimensions —
-			// this avoids blitting an empty buffer with palette[0] (which may be
-			// non-black, e.g. O1OPEN.ANM has palette[0] = blue).
-			_width = 0;
-			_height = 0;
-			int bufSize = 384 * 242;
-			if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
-				free(_specialBuffer);
-				_specialBuffer = (byte *)calloc(bufSize, 1);
-				_specialBufferSize = bufSize;
-			}
-			_dst = _specialBuffer;
-		} else {
+		if (!handleGameAnimHeader(headerContent)) {
 			_width = READ_LE_UINT16(&headerContent[4]);
 			_height = READ_LE_UINT16(&headerContent[6]);
 		}
@@ -1841,6 +1749,9 @@ SmushFont *SmushPlayer::getFont(int font) {
 	if (_sf[font])
 		return _sf[font];
 
+	if (SmushFont *gameFont = getGameFont(font))
+		return gameFont;
+
 	if (_vm->_game.id == GID_FT) {
 		if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
 			const char *ft_fonts[] = {
@@ -1856,8 +1767,6 @@ SmushFont *SmushPlayer::getFont(int font) {
 		}
 	} else if (isRA2()) {
 		return ra2GetFont(font);
-	} else if (isRA1()) {
-		return ra1GetFont(font);
 	} else {
 		int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
 		assert(font >= 0 && font < numFonts);
@@ -1907,23 +1816,14 @@ SmushFont *SmushPlayer::ra1GetFont(int font) {
 	return _sf[font];
 }
 
-/**
- * RA1 TEXT chunk handler (assembly parity):
- *   FUN_1FDBC (0x1FDBC) TEXT path -> FUN_221B7 (0x221B7)
- *   - payload uses two BE32 fields + text bytes with 0x00 separators
- *   - if first byte is '.', skip that sentinel before rendering
- *   - '<'/'>' switch font layers (handled by RA1 font-bank renderer)
- */
 void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
 		return;
 
-	InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_vm->_insane);
+	InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 	if (!rebel1)
 		return;
 
-	// FUN_1FDBC TEXT path passes BE32 x/y from payload to FUN_221B7 with 0x200
-	// center-alignment flag, so x is an anchor and y is line baseline.
 	const int textAnchorX = b.readSint32BE();
 	int cursorY = b.readSint32BE();
 
@@ -1936,14 +1836,11 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 		return;
 	b.read(textBuf, textLen);
 
-	// FUN_1FDBC checks first byte at payload+8 and skips a leading '.'
-	// when present (TEXT-at-0x2E sentinel path).
 	int start = 0;
 	if (textLen > 0 && textBuf[0] == '.')
 		start = 1;
 
 	int remaining = textLen - start;
-
 	while (remaining > 0) {
 		int lineLen = 0;
 		while (lineLen < remaining && textBuf[start + lineLen] != 0)
@@ -1956,7 +1853,6 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 			} else {
 				memcpy(line, textBuf + start, lineLen);
 				line[lineLen] = '\0';
-
 				const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
 				rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
 				cursorY += rebel1->getFontBankLineAdvance(line);
@@ -1965,6 +1861,7 @@ void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
 		} else {
 			cursorY += rebel1->getFontBankLineAdvance(nullptr);
 		}
+
 		int consumed = lineLen;
 		if (consumed < remaining && textBuf[start + consumed] == 0)
 			consumed++;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 4c01847882c..de3093cfe59 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -98,11 +98,13 @@ class SmushDeltaBlocksDecoder;
 class SmushDeltaGlyphsDecoder;
 class IMuseDigital;
 class Insane;
+class SmushPlayerRebel1;
 
 class SmushPlayer {
 	friend class Insane;
 	friend class InsaneRebel1;
 	friend class InsaneRebel2;
+	friend class SmushPlayerRebel1;
 	friend class SmushMultiFont;
 private:
 	struct SmushAudioDispatch {
@@ -261,7 +263,7 @@ private:
 
 public:
 	SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *_imuseDigital, Insane *insane);
-	~SmushPlayer();
+	virtual ~SmushPlayer();
 
 	void pause();
 	void unpause();
@@ -306,6 +308,15 @@ protected:
 	void setScrollOffset(int x, int y);
 	void seekSan(const char *file, int32 pos, int32 contFrame);
 	const char *getString(int id);
+	virtual void initGamePlayerFields() {}
+	virtual void destroyGamePlayerFields() {}
+	virtual void resetGameVideoState() {}
+	virtual void releaseGameVideoState() {}
+	virtual bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) { return false; }
+	virtual bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
+	virtual SmushFont *getGameFont(int font) { return nullptr; }
+	virtual void adjustGamePalette() {}
+	virtual bool handleGameAnimHeader(byte *headerContent) { return false; }
 
 private:
 	SmushFont *getFont(int font);
@@ -380,6 +391,23 @@ private:
 	void timerCallback();
 };
 
+class SmushPlayerRebel1 : public SmushPlayer {
+public:
+	SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+	~SmushPlayerRebel1() override;
+
+protected:
+	void initGamePlayerFields() override;
+	void destroyGamePlayerFields() override;
+	void resetGameVideoState() override;
+	void releaseGameVideoState() override;
+	bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+	bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+	SmushFont *getGameFont(int font) override;
+	void adjustGamePalette() override;
+	bool handleGameAnimHeader(byte *headerContent) override;
+};
+
 } // End of namespace Scumm
 
 #endif
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
new file mode 100644
index 00000000000..83a78367bfd
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -0,0 +1,162 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 1 specific SmushPlayer methods.
+//
+// Keep these in a dedicated file so the shared smush_player.cpp stays close
+// to upstream while RA1 behavior is isolated in one place.
+
+#include "common/endian.h"
+
+#include "scumm/file.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_player.h"
+
+#include "scumm/insane/insane_rebel1.h"
+
+namespace Scumm {
+
+SmushPlayerRebel1::SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
+	: SmushPlayer(scumm, imuseDigital, insane) {
+	initGamePlayerFields();
+}
+
+SmushPlayerRebel1::~SmushPlayerRebel1() {
+	destroyGamePlayerFields();
+}
+
+void SmushPlayerRebel1::initGamePlayerFields() {
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
+	_ra1ObjOverlayCodec = 0;
+	_ra1ObjOverlayLeft = 0;
+	_ra1ObjOverlayTop = 0;
+	_ra1ObjOverlayWidth = 0;
+	_ra1ObjOverlayHeight = 0;
+	_ra1ViewportOffsetX = 0;
+	_ra1ViewportOffsetY = 0;
+}
+
+void SmushPlayerRebel1::destroyGamePlayerFields() {
+	free(_ra1ObjOverlayData);
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
+}
+
+void SmushPlayerRebel1::resetGameVideoState() {
+	free(_ra1ObjOverlayData);
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
+	_ra1ObjOverlayCodec = 0;
+	_ra1ObjOverlayLeft = 0;
+	_ra1ObjOverlayTop = 0;
+	_ra1ObjOverlayWidth = 0;
+	_ra1ObjOverlayHeight = 0;
+	_ra1ViewportOffsetX = 0;
+	_ra1ViewportOffsetY = 0;
+}
+
+void SmushPlayerRebel1::releaseGameVideoState() {
+	free(_storedFobjData);
+	_storedFobjData = nullptr;
+	_storedFobjDataSize = 0;
+	_storedFobjCodec = 0;
+	_storedFobjParm2 = 0;
+	_storedFobjLeft = 0;
+	_storedFobjTop = 0;
+	_storedFobjWidth = 0;
+	_storedFobjHeight = 0;
+
+	free(_ra1ObjOverlayData);
+	_ra1ObjOverlayData = nullptr;
+	_ra1ObjOverlayDataSize = 0;
+}
+
+bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 4)
+		return false;
+
+	uint32 fetchId = b.readUint32BE();
+	int32 fetchX = 0;
+	int32 fetchY = 0;
+	if (subSize >= 12) {
+		fetchX = b.readSint32BE();
+		fetchY = b.readSint32BE();
+	}
+
+	if (_storedFobjData != nullptr && _storedFobjDataSize > 0) {
+		const int storedCodec = _storedFobjCodec & 0xFF;
+		const uint8 storedParam = (uint8)((_storedFobjCodec >> 8) & 0xFF);
+		int left = _storedFobjLeft + fetchX;
+		int top = _storedFobjTop + fetchY;
+
+		if (_insane) {
+			InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+			if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
+				_storedFobjWidth == _vm->_screenWidth) {
+				left += _ra1ViewportOffsetX;
+			}
+		}
+
+		debug("RA1 FTCH: frame=%d id=0x%08x pos=(%d,%d) using stored FOBJ codec=%d size=%dx%d",
+			_frame, fetchId, left, top, storedCodec, _storedFobjWidth, _storedFobjHeight);
+		decodeFrameObject(storedCodec, _storedFobjData, left, top,
+			_storedFobjWidth, _storedFobjHeight, _storedFobjDataSize,
+			storedParam, _storedFobjParm2);
+	} else {
+		debug("RA1 FTCH: frame=%d id=0x%08x with no stored FOBJ data", _frame, fetchId);
+	}
+
+	return true;
+}
+
+bool SmushPlayerRebel1::handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
+	if (subType != MKTAG('T','E','X','T'))
+		return false;
+
+	ra1HandleText(subSize, b);
+	return true;
+}
+
+SmushFont *SmushPlayerRebel1::getGameFont(int font) {
+	return ra1GetFont(font);
+}
+
+void SmushPlayerRebel1::adjustGamePalette() {
+	_pal[0] = _pal[1] = _pal[2] = 0;
+}
+
+bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
+	(void)headerContent;
+	_width = 0;
+	_height = 0;
+	const int bufSize = 384 * 242;
+	if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+		free(_specialBuffer);
+		_specialBuffer = (byte *)calloc(bufSize, 1);
+		_specialBufferSize = bufSize;
+	}
+	_dst = _specialBuffer;
+	return true;
+}
+
+} // End of namespace Scumm


Commit: 133d39acac8ff0cb663dc0e4e09164db014fd046
    https://github.com/scummvm/scummvm/commit/133d39acac8ff0cb663dc0e4e09164db014fd046
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:35+02:00

Commit Message:
SCUMM: RA: Add additional level handlers

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_levels.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d6e80d6a921..d396aa04263 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -39,7 +39,7 @@ namespace Scumm {
 // Per-difficulty tuning tables from assault_data_3.bin
 // Indexed: difficulty * 0x28B + level * 0x1F + offset
 // Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
-static const int16 kTuningTable[2][3][9] = {
+static const int16 kTuningTable[6][3][9] = {
 	// Level 1 (Flight Training)
 	{
 		{ 100, 100,  60, 110,   0,   0,  15,   0,   0 },  // Easy
@@ -52,8 +52,32 @@ static const int16 kTuningTable[2][3][9] = {
 		{ 100,  18, 120,   0,   5,   0,  20,   0,  50 },  // Normal
 		{ 100,  20, 150,   0,   1,   0,  25,   0,  75 },  // Hard
 	},
+	// Level 3 (Planet Kolaador)
+	{
+		{   0,   0,   0,   0,   4,  15,  25,   0,  25 },  // Easy
+		{   0,   0,   0,   0,   2,  18,  30,   0,  50 },  // Normal
+		{   0,   0,   0,   0,   0,  20,  35,   0,  75 },  // Hard
+	},
+	// Level 4 (Star Destroyer Attack)
+	{
+		{  70, 100, 150,  90,   0,   0,  20,   0,   0 },  // Easy
+		{  72, 105, 155, 105,   0,   0,  25,   0,   0 },  // Normal
+		{  75, 110, 160, 110,   0,   0,  28,   0,   0 },  // Hard
+	},
+	// Level 5 (Tatooine Attack)
+	{
+		{   0,   0,   0,   0,   2,  11,   0,   4,  25 },  // Easy
+		{   0,   0,   0,   0,   1,  25,   0,   6,  50 },  // Normal
+		{   0,   0,   0,   0,   1,  28,   0,   6,  75 },  // Hard
+	},
+	// Level 6 (Asteroid Field Chase)
+	{
+		{   0,   0,   0,   0,   3,  20,   0,   2,  50 },  // Easy
+		{   0,   0,   0,   0,   1,  25,   0,   5, 100 },  // Normal
+		{   0,   0,   0,   0,   1,  28,   0,   6, 200 },  // Hard
+	},
 };
-static const int kNumTunedLevels = 2;
+static const int kNumTunedLevels = 6;
 
 
 void InsaneRebel1::loadTuningForLevel(int level) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index a49d0676215..e38bc1e3a07 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -107,13 +107,17 @@ private:
 
 	// Level 2 flow: NEW → INTRO → PLAY (asteroid dodge) → END/DEATH
 	bool runLevel2();
+	bool runLevel3();
+	bool runLevel4();
+	bool runLevel5();
+	bool runLevel6();
 
 	// Play a passive cinematic (no game callback, skippable)
 	// startFrame > 0: fast-forward (decode without display) to that frame
 	void playCinematic(const char *filename, int32 startFrame = 0);
 
 	// Play interactive gameplay video (with ship physics + HUD)
-	void playInteractiveVideo(const char *filename);
+	void playInteractiveVideo(const char *filename, int32 startFrame = 0);
 	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 623fb05b4af..8747a05ea0c 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -709,14 +709,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			b.readUint32BE(); // field3
 			b.readUint32BE(); // field4
 
-			// RA1 scripts drive progression with GAME counters. In LVL2, finish the
-			// interactive SMUSH once the script counter reaches the terminal frame.
-			// This avoids getting stuck if container/frame parsing continues past the
-			// intended gameplay endpoint.
-			if (_interactiveVideoActive && _currentLevel == 1 && maxFrames > 0 &&
+			// RA1 scripts drive progression with GAME counters. Finish 0x0B-driven
+			// interactive videos once the script counter reaches the terminal frame.
+			if (_interactiveVideoActive && maxFrames > 0 &&
 				_gameCounter >= (int32)maxFrames - 1) {
 				_vm->_smushVideoShouldFinish = true;
-				debug(1, "RA1 L2: finishing interactive video at GAME 0x0B counter=%d/%u", _gameCounter, maxFrames);
+				debug(1, "RA1: finishing 0x0B interactive video at counter=%d/%u", _gameCounter, maxFrames);
 			}
 		}
 		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 0128443f89c..1defb3d8cae 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -208,7 +208,9 @@ void InsaneRebel1::loadLevelSprites(int level) {
 	// Ship direction bank — not all levels have one (e.g. Level 2 is first-person)
 	Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
 	if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
-		debug(1, "InsaneRebel1: No BANK1 for level %d (first-person level)", level);
+		Common::String legacyBankFile = Common::String::format("LVL%d/L%dBANK.NUT", level, level);
+		if (!loadRA1Nut(legacyBankFile.c_str(), _shipBank))
+			debug(1, "InsaneRebel1: No BANK1/BANK for level %d", level);
 	}
 
 	// Secondary ship bank used by some level-specific handlers (e.g. LVL1 mode-2).
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index e1e27853e9a..99f30618d78 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,16 +29,19 @@
 
 namespace Scumm {
 
+static const int kRA1LevelSelectItemCount = 7;
+static const int kRA1LastImplementedLevel = 6;
+
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_w:
-			_levelSelectSel = (_levelSelectSel + 2) % 3;
+			_levelSelectSel = (_levelSelectSel + kRA1LevelSelectItemCount - 1) % kRA1LevelSelectItemCount;
 			return true;
 		case Common::KEYCODE_DOWN:
 		case Common::KEYCODE_s:
-			_levelSelectSel = (_levelSelectSel + 1) % 3;
+			_levelSelectSel = (_levelSelectSel + 1) % kRA1LevelSelectItemCount;
 			return true;
 		case Common::KEYCODE_RETURN:
 		case Common::KEYCODE_KP_ENTER:
@@ -47,7 +50,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 			_vm->_smushVideoShouldFinish = true;
 			return true;
 		case Common::KEYCODE_ESCAPE:
-			_levelSelectSel = 2; // Back
+			_levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
@@ -205,16 +208,20 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		const int titleW = getTalkTextWidth("LEVEL SELECT");
 		drawTalkText((width - titleW) / 2, 36, "LEVEL SELECT");
 
-		const char *kLevelItems[3] = {
+		const char *kLevelItems[kRA1LevelSelectItemCount] = {
 			"LEVEL 1: FLIGHT TRAINING",
 			"LEVEL 2: ASTEROID FIELD",
+			"LEVEL 3: PLANET KOLAADOR",
+			"LEVEL 4: STAR DESTROYER",
+			"LEVEL 5: TATOOINE ATTACK",
+			"LEVEL 6: ASTEROID CHASE",
 			"BACK"
 		};
 
 		const int menuY = 60;
 		const int rowH = 16;
 
-		for (int i = 0; i < 3; i++) {
+		for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
 			const int textW = getTalkTextWidth(kLevelItems[i]);
 			const int textX = (width - textW) / 2;
 			const int y = menuY + i * rowH;
@@ -363,7 +370,7 @@ void InsaneRebel1::runOptionsMenu() {
 }
 
 void InsaneRebel1::runLevelSelectMenu() {
-	_levelSelectSel = CLIP(_startLevel - 1, 0, 1);
+	_levelSelectSel = CLIP(_startLevel - 1, 0, kRA1LastImplementedLevel - 1);
 	_levelSelectActive = true;
 
 	while (!_vm->shouldQuit()) {
@@ -378,19 +385,14 @@ void InsaneRebel1::runLevelSelectMenu() {
 			break;
 
 		if (_menuConfirmed) {
-			switch (_levelSelectSel) {
-			case 0:
-				_startLevel = 1;
-				_levelSelectActive = false;
-				return;
-			case 1:
-				_startLevel = 2;
-				_levelSelectActive = false;
-				return;
-			case 2:
+			if (_levelSelectSel < kRA1LastImplementedLevel) {
+				_startLevel = _levelSelectSel + 1;
 				_levelSelectActive = false;
 				return;
 			}
+
+			_levelSelectActive = false;
+			return;
 		}
 	}
 	_levelSelectActive = false;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ea9fdcb5f7a..f814e0ee6b5 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -315,8 +315,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _screenHeight;
 	int pitch = width;
 
-	if (_currentLevel == 1) {
-		// Level 2: first-person asteroid dodge — opcode 0x0B (FUN_1CDA7).
+	const bool asteroidMode = (_activeGameOpcode == 0x0B);
+	if (asteroidMode) {
+		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
 		updateAsteroidPhysics();
 	} else {
 		const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
@@ -328,7 +329,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		//   0x07/0x09/0x19/0x1A -> flight-family handlers
 		if (turretMode) {
 			updateTurretPhysics();
-		} else if (_shipBank.numSprites > 0 && flightMode) {
+		} else if (flightMode) {
 			updateShipPhysics();
 		}
 
@@ -350,7 +351,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
 	if (_player) {
 		_player->_ra1ViewportOffsetX = _perspectiveX;
-		_player->_ra1ViewportOffsetY = (_activeGameOpcode == 0x0B) ? 0 : _perspectiveY;
+		_player->_ra1ViewportOffsetY = asteroidMode ? 0 : _perspectiveY;
 	}
 
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 01ff651e813..99399530357 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -106,26 +106,21 @@ void InsaneRebel1::playIntroSequence() {
 bool InsaneRebel1::runLevel1() {
 	debug(1, "InsaneRebel1: Running level 1");
 
-	// Load level sprites (original: pushes L1BANK1..L1BANG NUT filenames)
+	_currentLevel = 0;
+	loadTuningForLevel(0);
 	loadLevelSprites(1);
 
-	// L1HANGAR.ANM — Hangar departure (original: 0x5918, flags 0x0420)
-	// Plays once at level start, never replayed on retry.
 	playCinematic("LVL1/L1HANGAR.ANM");
 	if (_vm->shouldQuit())
 		return false;
 
-	// L1CU1.ANM — Pre-flight cutscene (original: 0x5944, flags 0x0400)
-	// Plays once at level start, never replayed on retry.
 	playCinematic("LVL1/L1CU1.ANM");
 	if (_vm->shouldQuit())
 		return false;
 
-	// Retry loop — on death with lives, L1NEW plays then jumps back here
 	while (!_vm->shouldQuit()) {
 		bool stage2Started = false;
 
-		// Reset health for this attempt (original: MOV WORD [0x7560], 98 at 0x16214)
 		_health = kMaxHealth;
 		_damageFlags = 0;
 		_prevDamageFlags = 0;
@@ -134,56 +129,74 @@ bool InsaneRebel1::runLevel1() {
 		_screenFlash = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
 		_pathBranchEnabled = true;
 		_rightPathSelected = false;
 		_flyControlMode = 1;
 
-		// Stage 1 flight — L1PLAY1L (hard/left path)
-		// The first 394 frames are the common section. At counter 394, if
-		// ship is right of center, we switch to L1PLAY1R (easy path).
 		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
 		if (_rightPathSelected && _health >= 0) {
-			debug(1, "InsaneRebel1: Switching to right path (L1PLAY1R)");
 			_pathBranchEnabled = false;
 			_flyControlMode = 1;
-			playInteractiveVideo("LVL1/L1PLAY1R.ANM");
+			playInteractiveVideo("LVL1/L1PLAY1R.ANM", 0x187);
 			if (_vm->shouldQuit())
 				return false;
 		}
 		_pathBranchEnabled = false;
 
 		if (_health >= 0) {
-			// L1CU2.ANM — Mid-level cutscene (original: 0x5977)
 			playCinematic("LVL1/L1CU2.ANM");
 			if (_vm->shouldQuit())
 				return false;
 
-			// L1PLAY2.ANM — Stage 2 turret (original: 0x5986)
-			// Assembly @0x16396..0x163E5 switches to mode 2 and sets
-			// FUN_1D79C emitter offsets in DAT_75DC..DAT_75E2.
-			_flyControlMode = 2;
-			_turretEmitterLeftX = 10;
-			_turretEmitterLeftY = -5;
-			_turretEmitterRightX = 10;
-			_turretEmitterRightY = -5;
-			stage2Started = true;
-			playInteractiveVideo("LVL1/L1PLAY2.ANM");
-			if (_vm->shouldQuit())
-				return false;
-
-			// TODO: Check score threshold (original: CMP WORD [0x75D0], 5)
-			// If score < 5: L1RETRY → retry Stage 2
+			while (!_vm->shouldQuit()) {
+				_flyControlMode = 2;
+				_turretEmitterLeftX = 10;
+				_turretEmitterLeftY = -5;
+				_turretEmitterRightX = 10;
+				_turretEmitterRightY = -5;
+				_activeGameOpcode = 0;
+				_gameLatch5D = 0;
+				_gameLatch5F = 0;
+				_killCount = 0;
+				stage2Started = true;
+
+				playInteractiveVideo("LVL1/L1PLAY2.ANM");
+				if (_vm->shouldQuit())
+					return false;
+
+				if (_health < 0)
+					break;
+
+				if (_killCount > 4) {
+					playCinematic("LVL1/L1END.ANM");
+					return !_vm->shouldQuit();
+				}
 
-			// L1END.ANM — Level complete! (original: 0x59a3)
-			playCinematic("LVL1/L1END.ANM");
-			return true;
+				playCinematic("LVL1/L1RETRY.ANM");
+				if (_vm->shouldQuit())
+					return false;
+			}
 		}
 
-		// Death sequence (assembly-verified: 0x165dd / 0x16614):
-		// Stage 1 deaths use L1CRASHA; Stage 2 deaths use L1CRASHB.
 		if (stage2Started)
 			playCinematic("LVL1/L1CRASHB.ANM");
 		else
@@ -191,44 +204,33 @@ bool InsaneRebel1::runLevel1() {
 		if (_vm->shouldQuit())
 			return false;
 
-		// Assembly order (0x1666B): check lives first; decrement only on retry path.
 		if (_lives <= 0) {
-			// Game over — L1DEATH then return (original: 0x166C0)
 			playCinematic("LVL1/L1DEATH.ANM");
-			debug(1, "InsaneRebel1: Game over (no lives left)");
 			return false;
 		}
 
-		// Lives remaining — L1NEW briefing then retry (original: 0x16675)
 		playCinematic("LVL1/L1NEW.ANM");
 		if (_vm->shouldQuit())
 			return false;
 		_lives--;
-
-		// Loop back to gameplay (original: JMP 0x16214 — health reset + Stage 1)
 	}
 
 	return false;
 }
 
-// Level 2: Asteroid Field Training
-// Flow: L2NEW → L2INTRO → L2PLAY (interactive) → L2END/L2DEATH
 bool InsaneRebel1::runLevel2() {
 	debug(1, "InsaneRebel1: Running level 2");
 
 	_currentLevel = 1;
-	_flyControlMode = 1;
 	loadLevelSprites(2);
 	loadTuningForLevel(1);
 
-	// L2INTRO.ANM — intro cutscene (481 frames)
 	playCinematic("LVL2/L2INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
 
-	// Retry loop
 	while (!_vm->shouldQuit()) {
-		// Reset state for this attempt
+		_flyControlMode = 0;
 		_health = kMaxHealth;
 		_damageFlags = 0;
 		_prevDamageFlags = 0;
@@ -244,33 +246,297 @@ bool InsaneRebel1::runLevel2() {
 		_avgInputX = 0;
 		_avgInputY = 0;
 		_killCount = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
 
-		// L2PLAY.ANM — asteroid dodge (800 frames, interactive)
 		playInteractiveVideo("LVL2/L2PLAY.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
 		if (_health >= 0) {
-			// Level complete!
 			playCinematic("LVL2/L2END.ANM");
-			return true;
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL2/L2NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
 		}
 
-		// Death
 		playCinematic("LVL2/L2DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel3() {
+	debug(1, "InsaneRebel1: Running level 3");
+
+	_currentLevel = 2;
+	loadLevelSprites(3);
+	loadTuningForLevel(2);
+
+	playCinematic("LVL3/L3INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		playInteractiveVideo("LVL3/L3PLAY.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
-		_lives--;
-		if (_lives <= 0) {
-			debug(1, "InsaneRebel1: Game over (no lives left)");
+		if (_health >= 0) {
+			playCinematic("LVL3/L3END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL3/L3NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL3/L3DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel4() {
+	debug(1, "InsaneRebel1: Running level 4");
+
+	_currentLevel = 3;
+	loadLevelSprites(4);
+	loadTuningForLevel(3);
+
+	playCinematic("LVL4/L4INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+
+		playInteractiveVideo("LVL4/L4PLAY1.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			_activeGameOpcode = 0;
+			_gameLatch5D = 0;
+			_gameLatch5F = 0;
+			_killCount = 0;
+			playInteractiveVideo("LVL4/L4PLAY2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+		}
+
+		if (_health >= 0) {
+			playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL4/L4NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL4/L4DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel5() {
+	debug(1, "InsaneRebel1: Running level 5");
+
+	_currentLevel = 4;
+	loadLevelSprites(5);
+	loadTuningForLevel(4);
+
+	playCinematic("LVL5/L5INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		playInteractiveVideo("LVL5/L5PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health < 0) {
+			if (_lives > 0) {
+				playCinematic("LVL5/L5NEW.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				_lives--;
+				continue;
+			}
+
+			playCinematic("LVL5/L5DEATH.ANM");
 			return false;
 		}
 
-		// Retry briefing
-		playCinematic("LVL2/L2NEW.ANM");
+		if (_killCount <= 2) {
+			if (_lives > 0) {
+				playCinematic("LVL5/L5RETRY.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				_lives--;
+				continue;
+			}
+
+			playCinematic("LVL5/L5DEATH2.ANM");
+			return false;
+		}
+
+		playCinematic("LVL5/L5BINTRO.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		loadRA1Nut("LVL5/L5LASER2.NUT", _laserBank);
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		playInteractiveVideo("LVL5/L5PLAY2.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			playCinematic("LVL5/L5END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL5/L5NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL5/L5DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel6() {
+	debug(1, "InsaneRebel1: Running level 6");
+
+	_currentLevel = 5;
+	loadLevelSprites(6);
+	loadTuningForLevel(5);
+
+	playCinematic("LVL6/L6INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		playInteractiveVideo("LVL6/L6PLAY.ANM");
 		if (_vm->shouldQuit())
 			return false;
+
+		if (_health >= 0) {
+			playCinematic("LVL6/L6END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL6/L6NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL6/L6DEATH.ANM");
+		return false;
 	}
 
 	return false;
@@ -279,6 +545,16 @@ bool InsaneRebel1::runLevel2() {
 // Main game entry point — called from ScummEngine::go().
 // Matches original flow at 0x15597: intro → menu → level.
 void InsaneRebel1::runGame() {
+	typedef bool (InsaneRebel1::*RunLevelMethod)();
+	static const RunLevelMethod kLevelRunners[] = {
+		&InsaneRebel1::runLevel1,
+		&InsaneRebel1::runLevel2,
+		&InsaneRebel1::runLevel3,
+		&InsaneRebel1::runLevel4,
+		&InsaneRebel1::runLevel5,
+		&InsaneRebel1::runLevel6
+	};
+
 	// Play intro sequence (logo + opening)
 	playIntroSequence();
 	if (_vm->shouldQuit())
@@ -292,29 +568,15 @@ void InsaneRebel1::runGame() {
 
 		switch (menuResult) {
 		case 1: {
-			bool completed = false;
-			if (_startLevel <= 1) {
-				// Start from Level 1 (default flow).
-				// Assembly Level 1 entry (0x16100) starts with L1HANGAR/L1CU1,
-				// not with L1NEW (L1NEW is retry-only at 0x16675).
-				completed = runLevel1();
-				if (completed && !_vm->shouldQuit()) {
-					completed = runLevel2();
-					if (completed) {
-						debug(1, "InsaneRebel1: Level 2 completed!");
-						// TODO: Continue to level 3
-					}
-				}
-			} else if (_startLevel == 2) {
-				// Direct Level 2 start from Level Select menu.
-				// Assembly Level 2 entry (0x16757) starts at L2INTRO; L2NEW is retry-only.
-				completed = runLevel2();
-				if (completed) {
-					debug(1, "InsaneRebel1: Level 2 completed!");
-				}
+			const int startLevel = CLIP<int>(_startLevel, 1, sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+			bool completed = true;
+
+			for (int level = startLevel;
+				 level <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0])) && completed && !_vm->shouldQuit();
+				 ++level) {
+				completed = (this->*kLevelRunners[level - 1])();
 			}
 			_currentLevel = 0;
-			// Return to menu after level ends
 			break;
 		}
 		case 2:
@@ -336,8 +598,8 @@ void InsaneRebel1::runGame() {
 }
 
 // Play interactive gameplay video (with ship physics + HUD).
-void InsaneRebel1::playInteractiveVideo(const char *filename) {
-	debug(1, "InsaneRebel1::playInteractiveVideo('%s')", filename);
+void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
+	debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
 
 	// Stop any leftover audio from previous video
 	terminateAudio();
@@ -349,6 +611,8 @@ void InsaneRebel1::playInteractiveVideo(const char *filename) {
 	_interactiveVideoActive = true;
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);
+	if (startFrame > 0)
+		splayer->setFastForwardToFrame(startFrame);
 
 	// Center mouse, hide system cursor (we draw our own), lock mouse to window
 	smush_warpMouse(160, 100, -1);


Commit: 26a309a37aaa4702f7cc4c5184a03544d89cbb1b
    https://github.com/scummvm/scummvm/commit/26a309a37aaa4702f7cc4c5184a03544d89cbb1b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:36+02:00

Commit Message:
SCUMM: RA1: Add projection table for gameplay points

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d396aa04263..8424460425d 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -121,6 +121,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 	_perspectiveX = 0;
 	_perspectiveY = 0;
+	_projectionCurveExtent = 1;
+	memset(_projectionTable, 0, sizeof(_projectionTable));
 
 	memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 	memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index e38bc1e3a07..aa7d1255a91 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -87,6 +87,8 @@ public:
 	int getCurrentLevel() const { return _currentLevel; }
 	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
 	int16 getPerspectiveX() const { return _perspectiveX; }
+	void projectGameplayPoint(int16 &x, int16 &y) const;
+	void unprojectGameplayPoint(int16 &x, int16 &y) const;
 
 	// Game flow (matching original at 0x15597)
 	void runGame();
@@ -123,6 +125,8 @@ private:
 	void updateShipPhysics();
 	void updateTurretPhysics();
 	void preprocessMouseAxes(int16 &inputX, int16 &inputY);
+	void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
+	void resetProjectionTable();
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -195,6 +199,9 @@ private:
 	// Perspective view offsets (0x74B6/0x74B8: viewport scroll base)
 	int16 _perspectiveX;
 	int16 _perspectiveY;
+	static const int kProjectionTableSize = 80;
+	int16 _projectionCurveExtent;
+	int16 _projectionTable[kProjectionTableSize];
 
 	// Input history buffers for 0x0B handler (FUN_1CDA7) — 10-frame averaging
 	static const int kInputHistorySize = 10;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 8747a05ea0c..1b53e5473a0 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -34,13 +34,55 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
 	return code >= 6 && code <= 18;
 }
 
-// FUN_223FE (0x223FE) coordinate transform used by FUN_1C54D/FUN_1C6E9.
-// The original applies camera X offset directly and a Y term derived from
-// DAT_41A2 (+ curve-table contribution). In current RA1 integration we keep
-// the camera-offset part, which fixes left/right corridor asymmetry.
-static void transformPoint223FE(int16 &x, int16 &y, int16 cameraX, int16 cameraY) {
-	x = (int16)(x - cameraX);
-	y = (int16)(y - cameraY);
+void InsaneRebel1::rebuildProjectionTable(int16 curveStep, int16 curveExtent) {
+	_projectionCurveExtent = curveExtent;
+
+	int step = curveStep >> 1;
+	int accum = 0;
+	int threshold = 0;
+	int value = step;
+
+	for (int i = 0; i < kProjectionTableSize / 2; i++) {
+		if (step < 0) {
+			accum -= step;
+			if (threshold < accum) {
+				threshold += kProjectionTableSize / 2;
+				value += 1;
+			}
+		} else {
+			accum += step;
+			if (threshold < accum) {
+				threshold += kProjectionTableSize / 2;
+				value -= 1;
+			}
+		}
+
+		_projectionTable[i] = (int16)value;
+		_projectionTable[kProjectionTableSize - 1 - i] = (int16)-value;
+	}
+}
+
+void InsaneRebel1::resetProjectionTable() {
+	rebuildProjectionTable(0, 1);
+}
+
+void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
+	x = (int16)(x - _perspectiveX);
+
+	int curveIndex = 0x4F - (x >> 2);
+	curveIndex = CLIP<int>(curveIndex, 0, kProjectionTableSize - 1);
+
+	const int yCompensation = (_perspectiveY + (_projectionCurveExtent >> 1)) - _projectionTable[curveIndex];
+	y = (int16)(y - yCompensation);
+}
+
+void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
+	int curveIndex = 0x4F - (x >> 2);
+	curveIndex = CLIP<int>(curveIndex, 0, kProjectionTableSize - 1);
+
+	const int yCompensation = (_perspectiveY + (_projectionCurveExtent >> 1)) - _projectionTable[curveIndex];
+	y = (int16)(y + yCompensation);
+	x = (int16)(x + _perspectiveX);
 }
 
 // preprocessMouseAxes — FUN_231BE (0x231BE) centered-axis output law, adapted to
@@ -197,6 +239,11 @@ void InsaneRebel1::updateShipPhysics() {
 	_perspectiveX = CLIP<int16>((int16)(_shipPosX - kRA1CenterX + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)(_shipPosY - kRA1CenterY + 0x17), 0, 0x2E);
 
+	// FUN_1DEB5 updates the curve table via FUN_22549 after SetCameraOffset.
+	// The full DOS path blends a few roll-history terms; use the current roll
+	// accumulator so side-looking still bends the gameplay projection.
+	rebuildProjectionTable(CLIP<int16>((int16)(-(_rollAccum >> 7)), -0x1A, 0x1A), 0x1A);
+
 	// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
 	// Horizontal component from _74CA (rollAccum):
 	//   |rollAccum| <= 0x80: center (0)
@@ -387,6 +434,10 @@ void InsaneRebel1::updateTurretPhysics() {
 	_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
 
+	// FUN_1E6A7 rebuilds the side-look curve with a shallower table than the
+	// main flight handler, derived directly from roll.
+	rebuildProjectionTable((int16)(-(_rollAccum >> 9)), 0x0D);
+
 	// Direction bucket synthesis from FUN_1E6A7.
 	int dir = 0;
 	if (_flyControlMode == 2) {
@@ -538,6 +589,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	int16 avgViewY = (int16)(sumViewY / kAsteroidSmoothWindow);
 	_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
+	resetProjectionTable();
 
 	_frameCounter++;
 
@@ -621,6 +673,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		}
 
 		_activeGameOpcode = 0;
+		resetProjectionTable();
 		debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
 		break;
 
@@ -660,7 +713,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 			int16 centerX = corridorLeft + corridorWidth / 2;
 			int16 centerY = corridorTop + corridorHeight / 2;
-			transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+			projectGameplayPoint(centerX, centerY);
 
 			_corridorLeftX = centerX - corridorWidth / 2;
 			_corridorTopY = centerY - corridorHeight / 2;
@@ -684,7 +737,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 			int16 centerX = zoneLeft + zoneWidth / 2;
 			int16 centerY = zoneTop + zoneHeight / 2;
-			transformPoint223FE(centerX, centerY, _perspectiveX, _perspectiveY);
+			projectGameplayPoint(centerX, centerY);
 
 			zoneLeft = centerX - zoneWidth / 2;
 			zoneTop = centerY - zoneHeight / 2;
@@ -804,6 +857,8 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 	int16 curY = _shipPosY;
 	const int slot = _targetCount;
 
+	unprojectGameplayPoint(curX, curY);
+
 	if (slot < kMaxTargetBoxes) {
 		_targetBoxX[slot] = (int16)((left + right) / 2);
 		_targetBoxY[slot] = (int16)((top + bottom) / 2);
@@ -848,10 +903,13 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						_score += _tuning.kill;
 						_killCount++;
 
-						// Snap cursor to target center (original: _DAT_74BE/74C0 = target center)
+						// Snap cursor to target center and re-project it, matching FUN_1C0EF.
 						if (snap > 0) {
-							_shipPosX = (left + right) / 2;
-							_shipPosY = (top + bottom) / 2;
+							int16 lockX = (left + right) / 2;
+							int16 lockY = (top + bottom) / 2;
+							projectGameplayPoint(lockX, lockY);
+							_shipPosX = lockX;
+							_shipPosY = lockY;
 						}
 
 						debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index f814e0ee6b5..840e355a9d8 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -386,14 +386,15 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 // renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
 void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
-	const int overlayX = ra1OverlayViewOffsetX(this);
-
 	for (int i = _targetCount - 1; i >= 0; --i) {
 		if (i >= kMaxTargetBoxes)
 			continue;
 
 		char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
-		drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
+		int16 drawX = _targetBoxX[i];
+		int16 drawY = _targetBoxY[i];
+		projectGameplayPoint(drawX, drawY);
+		drawFontBankString(dst, pitch, width, height, drawX, drawY, box);
 	}
 
 	_prevTargetCount = _targetCount;
@@ -445,7 +446,6 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	if (_bangBank.numSprites <= 0)
 		return;
 
-	const int overlayX = ra1OverlayViewOffsetX(this);
 	for (int i = 0; i < kMaxGostSlots; i++) {
 		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
 			int sprIdx = _gostSlots[i].frame;
@@ -453,8 +453,11 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 				sprIdx = _bangBank.numSprites - 1;
 
 			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
-			int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
-			int drawY = _gostSlots[i].posY - spr.height / 2;
+			int16 drawX = _gostSlots[i].posX;
+			int16 drawY = _gostSlots[i].posY;
+			projectGameplayPoint(drawX, drawY);
+			drawX -= spr.width / 2;
+			drawY -= spr.height / 2;
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 
 			_gostSlots[i].frame++;


Commit: 4ceacc55a2a2637974229657924f38f72e5939b4
    https://github.com/scummvm/scummvm/commit/4ceacc55a2a2637974229657924f38f72e5939b4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:36+02:00

Commit Message:
SCUMM: RA1: Track frame object hit state

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8424460425d..8c1c7d38e93 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -192,6 +192,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_gostSlotIdx = 0;
 	_killCount = 0;
 	_lastHitTarget = 0;
+	resetFrameObjectState();
 
 	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
 		debug(1, "InsaneRebel1: HUD/menu glyph font loaded from SYS/TALKFONT.NUT (%d chars)", _hudFontBank.numSprites);
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index aa7d1255a91..101c5f6b50b 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -89,6 +89,9 @@ public:
 	int16 getPerspectiveX() const { return _perspectiveX; }
 	void projectGameplayPoint(int16 &x, int16 &y) const;
 	void unprojectGameplayPoint(int16 &x, int16 &y) const;
+	bool handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
+		int codec, uint8 &ra1Param);
+	void resetFrameObjectState();
 
 	// Game flow (matching original at 0x15597)
 	void runGame();
@@ -137,6 +140,8 @@ private:
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
+	void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
+	void applyFrameObjectHitState(int16 targetIdx);
 
 	// Shooting pipeline — FUN_1CCA0 (0x1CCA0) shot spawner,
 	// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
@@ -350,6 +355,9 @@ private:
 
 	int16 _killCount;        // 0x75D0: targets destroyed this stage
 	int16 _lastHitTarget;    // 0x75D6: prevents double-hit on same target
+
+	static const int kFrameObjectStateBytes = 300;
+	byte _frameObjectState[kFrameObjectStateBytes];
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 1b53e5473a0..d48edddae88 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -34,6 +34,80 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
 	return code >= 6 && code <= 18;
 }
 
+void InsaneRebel1::resetFrameObjectState() {
+	memset(_frameObjectState, 0, sizeof(_frameObjectState));
+	for (int i = 0x50; i < 0x96; i++)
+		_frameObjectState[i] = 0xFF;
+}
+
+void InsaneRebel1::updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
+	const int16 targetKey = targetIdx + 1;
+	for (int i = 0; i < kMaxGostSlots; i++) {
+		if (_gostSlots[i].targetId == targetKey && _gostSlots[i].frame < 10) {
+			_gostSlots[i].posX = (left + right) / 2;
+			_gostSlots[i].posY = (top + bottom) / 2;
+		}
+	}
+}
+
+void InsaneRebel1::applyFrameObjectHitState(int16 targetIdx) {
+	if (targetIdx < 0)
+		return;
+
+	const int byteIndex = targetIdx >> 3;
+	if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+		return;
+
+	const byte bit = (byte)(0x80 >> (targetIdx & 7));
+	const int altIndex = byteIndex + 0x96;
+	if (altIndex >= kFrameObjectStateBytes)
+		return;
+
+	if ((_frameObjectState[altIndex] & bit) == 0)
+		_frameObjectState[byteIndex] |= bit;
+	else
+		_frameObjectState[altIndex] &= ~bit;
+}
+
+bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
+		int codec, uint8 &ra1Param) {
+	if (!_interactiveVideoActive)
+		return true;
+
+	int absObjectId = (objectId < 0) ? -objectId : objectId;
+	if (absObjectId == 0)
+		return true;
+
+	const int bitIndex = absObjectId - 1;
+	const int byteIndex = bitIndex >> 3;
+	if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+		return true;
+
+	const byte bit = (byte)(0x80 >> (bitIndex & 7));
+	const int altIndex = byteIndex + 0x96;
+	const bool primarySet = (_frameObjectState[byteIndex] & bit) != 0;
+	const bool secondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
+	const int16 right = left + width;
+	const int16 bottom = top + height;
+
+	if (objectId > 0 && objectId < 0x280) {
+		if (!primarySet || secondarySet)
+			checkTargetHit(objectId - 1, left, top, right, bottom);
+		else
+			updateGostSlotPosition(objectId - 1, left, top, right, bottom);
+	}
+
+	const bool updatedPrimarySet = (_frameObjectState[byteIndex] & bit) != 0;
+	const bool updatedSecondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
+	if (updatedPrimarySet)
+		return false;
+
+	if (updatedSecondarySet && codec == 0x17)
+		ra1Param = (uint8)(ra1Param - 0x10);
+
+	return true;
+}
+
 void InsaneRebel1::rebuildProjectionTable(int16 curveStep, int16 curveExtent) {
 	_projectionCurveExtent = curveExtent;
 
@@ -850,14 +924,14 @@ void InsaneRebel1::processShot() {
 }
 
 // checkTargetHit — FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
-// Called from GAME 0x5A handler. Checks cursor proximity and shot hits.
+// The original compares target bounds against the cursor after
+// UnprojectScreenPoint(), then reprojects the snapped cursor center after a hit.
 void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
 	int16 snap = _tuning.snap;
 	int16 curX = _shipPosX;
 	int16 curY = _shipPosY;
-	const int slot = _targetCount;
-
 	unprojectGameplayPoint(curX, curY);
+	const int slot = _targetCount;
 
 	if (slot < kMaxTargetBoxes) {
 		_targetBoxX[slot] = (int16)((left + right) / 2);
@@ -902,14 +976,14 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						_lastHitTarget = targetIdx + 1;
 						_score += _tuning.kill;
 						_killCount++;
+						applyFrameObjectHitState(targetIdx);
 
-						// Snap cursor to target center and re-project it, matching FUN_1C0EF.
+						// Match FUN_1C0EF: snap in unprojected space, then project back
+						// into the current gameplay window before rendering the pointer.
 						if (snap > 0) {
-							int16 lockX = (left + right) / 2;
-							int16 lockY = (top + bottom) / 2;
-							projectGameplayPoint(lockX, lockY);
-							_shipPosX = lockX;
-							_shipPosY = lockY;
+							_shipPosX = (left + right) / 2;
+							_shipPosY = (top + bottom) / 2;
+							projectGameplayPoint(_shipPosX, _shipPosY);
 						}
 
 						debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 840e355a9d8..f814e0ee6b5 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -386,15 +386,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 // renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
 void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
+	const int overlayX = ra1OverlayViewOffsetX(this);
+
 	for (int i = _targetCount - 1; i >= 0; --i) {
 		if (i >= kMaxTargetBoxes)
 			continue;
 
 		char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
-		int16 drawX = _targetBoxX[i];
-		int16 drawY = _targetBoxY[i];
-		projectGameplayPoint(drawX, drawY);
-		drawFontBankString(dst, pitch, width, height, drawX, drawY, box);
+		drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
 	}
 
 	_prevTargetCount = _targetCount;
@@ -446,6 +445,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	if (_bangBank.numSprites <= 0)
 		return;
 
+	const int overlayX = ra1OverlayViewOffsetX(this);
 	for (int i = 0; i < kMaxGostSlots; i++) {
 		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
 			int sprIdx = _gostSlots[i].frame;
@@ -453,11 +453,8 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 				sprIdx = _bangBank.numSprites - 1;
 
 			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
-			int16 drawX = _gostSlots[i].posX;
-			int16 drawY = _gostSlots[i].posY;
-			projectGameplayPoint(drawX, drawY);
-			drawX -= spr.width / 2;
-			drawY -= spr.height / 2;
+			int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
+			int drawY = _gostSlots[i].posY - spr.height / 2;
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 
 			_gostSlots[i].frame++;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 99399530357..b84b0cdd16a 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -609,6 +609,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	_player = splayer;
 	clearBit(0);
 	_interactiveVideoActive = true;
+	resetFrameObjectState();
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);
 	if (startFrame > 0)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index b86c86f4f31..0c8ad49928d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1316,6 +1316,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 
 	int codec = b.readUint16LE();
 	uint8 ra1Param = 0;   // RA1: palette base byte (FOBJ byte[1])
+	uint16 ra1ObjectId = 0; // RA1: object id / event target id (FOBJ bytes[10-11])
 	uint16 ra1Parm2 = 0;  // RA1: tile count (FOBJ bytes[12-13])
 	if (isRA1()) {
 		ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
@@ -1328,7 +1329,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	int width = b.readUint16LE();
 	int height = b.readUint16LE();
 
-	b.readUint16LE();
+	ra1ObjectId = b.readUint16LE();
 	ra1Parm2 = b.readUint16LE();
 
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
@@ -1339,8 +1340,8 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 			_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
 	}
 	if (isRA1()) {
-		debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
-			_frame, codec, left, top, width, height, subSize - 14, _storeFrame);
+		debug("RA1 FOBJ: frame=%d codec=%d object=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
+			_frame, codec, ra1ObjectId, left, top, width, height, subSize - 14, _storeFrame);
 	}
 
 	int32 chunk_size = subSize - 14;
@@ -1373,6 +1374,15 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 		_storeFrame = false;
 	}
 
+	if (isRA1() && _insane) {
+		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+		if (!rebel1->handleFrameObjectTarget((int16)ra1ObjectId, (int16)rawLeft, (int16)rawTop,
+				(int16)width, (int16)height, codec, ra1Param)) {
+			free(chunk_buffer);
+			return;
+		}
+	}
+
 	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
 
 	free(chunk_buffer);


Commit: 851bb8fcba206f2e5c78ef739d7108e03f125800
    https://github.com/scummvm/scummvm/commit/851bb8fcba206f2e5c78ef739d7108e03f125800
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:36+02:00

Commit Message:
SCUMM: RA1: Add hitboxes

Changed paths:
    engines/scumm/insane/insane_rebel1_render.cpp


diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index f814e0ee6b5..58460e65103 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -100,6 +100,40 @@ static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int
 	}
 }
 
+static const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
+	if (bank.numSprites <= 0)
+		return nullptr;
+	if ((byte)ch < 0x21)
+		return nullptr;
+
+	const int fontIdx = (int)(byte)ch - 0x21;
+	if (fontIdx < 0 || fontIdx >= bank.numSprites)
+		return nullptr;
+
+	const RA1Sprite &glyph = bank.sprites[fontIdx];
+	if (!glyph.data || glyph.width <= 0 || glyph.height <= 0)
+		return nullptr;
+
+	return &glyph;
+}
+
+// Glyph markers in FUN_1C940/FUN_1CB22 go through DrawStringEx(..., flags=3),
+// which centers the glyph and ignores the NUT x/y offsets. Use the same anchor
+// rules here instead of the generic left-anchored text path.
+static void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+	int centerX, int centerY, char ch) {
+	char glyphStr[2] = { ch, '\0' };
+	const RA1Sprite *glyph = lookupBankGlyph(bank, ch);
+	if (!glyph) {
+		drawBankString(bank, dst, pitch, width, height, centerX, centerY, glyphStr);
+		return;
+	}
+
+	const int drawX = centerX - glyph->xoffs - (int)glyph->width / 2;
+	const int drawY = centerY - glyph->yoffs - (int)glyph->height / 2;
+	drawBankString(bank, dst, pitch, width, height, drawX, drawY, glyphStr);
+}
+
 static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
 	if (!text || bank.numSprites <= 0)
 		return 0;
@@ -387,13 +421,21 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 // renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
 void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
 	const int overlayX = ra1OverlayViewOffsetX(this);
+	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
+	const bool projectTargetMarkers = (_activeGameOpcode == 0x0B);
 
 	for (int i = _targetCount - 1; i >= 0; --i) {
 		if (i >= kMaxTargetBoxes)
 			continue;
 
-		char box[4] = { '<', '<', (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5)), '\0' };
-		drawFontBankString(dst, pitch, width, height, overlayX + _targetBoxX[i], _targetBoxY[i], box);
+		int16 drawX = _targetBoxX[i];
+		int16 drawY = _targetBoxY[i];
+		if (projectTargetMarkers)
+			drawX = (int16)(drawX - _perspectiveX);
+
+		const char boxGlyph = (char)('i' + CLIP<int16>(_targetBoxVariant[i], 0, 5));
+		drawCenteredBankGlyph(markerBank, dst, pitch, width, height,
+			overlayX + drawX, drawY, boxGlyph);
 	}
 
 	_prevTargetCount = _targetCount;
@@ -414,9 +456,9 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 		// Lock indicator at fixed center positions:
 		// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
 		if (_targetProximity > 0) {
-			drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, "]");
+			drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, ']');
 			if (_targetProximity > 1)
-				drawBankString(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, "a");
+				drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, 'a');
 		}
 
 		// Pointer glyph at current aim position. Original uses two variants:
@@ -429,8 +471,7 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 
 		int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
 		int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
-		int markerW = getBankStringWidth(markerBank, marker);
-		drawBankString(markerBank, dst, pitch, width, height, cursorX - markerW / 2, cursorY, marker);
+		drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
 	}
 
 	// Save previous proximity for next frame
@@ -446,6 +487,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 		return;
 
 	const int overlayX = ra1OverlayViewOffsetX(this);
+	const bool projectGostMarkers = (_activeGameOpcode == 0x0B);
 	for (int i = 0; i < kMaxGostSlots; i++) {
 		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
 			int sprIdx = _gostSlots[i].frame;
@@ -453,8 +495,13 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 				sprIdx = _bangBank.numSprites - 1;
 
 			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
-			int drawX = overlayX + _gostSlots[i].posX - spr.width / 2;
-			int drawY = _gostSlots[i].posY - spr.height / 2;
+			int16 centerX = _gostSlots[i].posX;
+			int16 centerY = _gostSlots[i].posY;
+			if (projectGostMarkers)
+				centerX = (int16)(centerX - _perspectiveX);
+
+			int drawX = overlayX + centerX - spr.width / 2;
+			int drawY = centerY - spr.height / 2;
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 
 			_gostSlots[i].frame++;


Commit: cecedef4f75dc5948a72297a8cb400015bf3f06b
    https://github.com/scummvm/scummvm/commit/cecedef4f75dc5948a72297a8cb400015bf3f06b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:37+02:00

Commit Message:
SCUMM: RA1: Apply overlay Y offsets

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 101c5f6b50b..6d3aa92d591 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -87,6 +87,7 @@ public:
 	int getCurrentLevel() const { return _currentLevel; }
 	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
 	int16 getPerspectiveX() const { return _perspectiveX; }
+	int16 getPerspectiveY() const { return _perspectiveY; }
 	void projectGameplayPoint(int16 &x, int16 &y) const;
 	void unprojectGameplayPoint(int16 &x, int16 &y) const;
 	bool handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 58460e65103..ca56df82c2f 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -37,6 +37,13 @@ static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
 	return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
 }
 
+static inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
+	if (!rebel1 || !rebel1->isInteractiveVideoActive())
+		return 0;
+
+	return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
+}
+
 static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
 	int x, int y, const char *text) {
 	if (!dst || !text || bank.numSprites <= 0)
@@ -385,7 +392,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
 	if (_player) {
 		_player->_ra1ViewportOffsetX = _perspectiveX;
-		_player->_ra1ViewportOffsetY = asteroidMode ? 0 : _perspectiveY;
+		_player->_ra1ViewportOffsetY = _perspectiveY;
 	}
 
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
@@ -448,6 +455,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
 void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
 	const int overlayX = ra1OverlayViewOffsetX(this);
+	const int overlayY = ra1OverlayViewOffsetY(this);
 	if (markerBank.numSprites > 0) {
 		// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
 		// Baseline RA1 targeting uses '^' and animation e..h.
@@ -456,9 +464,9 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 		// Lock indicator at fixed center positions:
 		// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
 		if (_targetProximity > 0) {
-			drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x78, ']');
+			drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x78, ']');
 			if (_targetProximity > 1)
-				drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, 0x7E, 'a');
+				drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x7E, 'a');
 		}
 
 		// Pointer glyph at current aim position. Original uses two variants:
@@ -470,7 +478,7 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 		}
 
 		int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
-		int cursorY = CLIP<int>(_shipPosY, 0, height - 1);
+		int cursorY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
 		drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
 	}
 
@@ -521,11 +529,12 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
 	const int spritesPerSet = 5;
 	const int overlayX = ra1OverlayViewOffsetX(this);
+	const int overlayY = ra1OverlayViewOffsetY(this);
 	const int leftStartX = 0;
 	const int rightStartX = 0x13F; // 319
 	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
 	const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
-	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
+	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : (overlayY + _shipPosY);
 
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
@@ -533,7 +542,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 			const int lerp = kShotLerpByTimer[timer];
 			const int frame = spritesPerSet - timer;
 			const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
-			const int targetY = CLIP<int>(_shipPosY, 0, height - 1);
+			const int targetY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
 
 			if (turretMode) {
 				// FUN_1D79C chooses emitters in two ways:
@@ -583,20 +592,20 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 			}
 
 			// Fallback for non-turret handlers that still run shot overlays.
-			int leftStartY = 0x96;
-			int rightStartY = 0x96;
+			int leftStartY = overlayY + 0x96;
+			int rightStartY = overlayY + 0x96;
 			uint32 leftFlags = 0x83;
 			uint32 rightFlags = 0x2083;
 
 			if (_flyControlMode == 1) {
 				if (_shotSideToggle) {
 					leftFlags = 0x4083;
-					leftStartY = 0;
-					rightStartY = 0x96;
+					leftStartY = overlayY;
+					rightStartY = overlayY + 0x96;
 				} else {
 					rightFlags = 0x6083;
-					leftStartY = 0x96;
-					rightStartY = 0;
+					leftStartY = overlayY + 0x96;
+					rightStartY = overlayY;
 				}
 				if (timer == 1)
 					_shotSideToggle = !_shotSideToggle;
@@ -760,10 +769,11 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 		return;
 
 	const int overlayX = ra1OverlayViewOffsetX(this);
+	const int overlayY = ra1OverlayViewOffsetY(this);
 	// In 0x08/0x0A turret handlers, explosion anchors use ship center
 	// (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
 	int shipScreenX = overlayX + _shipPosX;
-	int shipScreenY = _shipPosY;
+	int shipScreenY = overlayY + _shipPosY;
 	if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
 		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
 		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
@@ -840,11 +850,7 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	int hudOriginY = 0;
 	if (_interactiveVideoActive && _player) {
 		hudOriginX = _player->_ra1ViewportOffsetX;
-		// Asteroid path (opcode 0x0B / FUN_1CDA7) applies Y correction through
-		// FUN_223FE coordinate transforms in the original renderer, not as a
-		// simple global framebuffer Y window shift.
-		if (_activeGameOpcode != 0x0B)
-			hudOriginY = _player->_ra1ViewportOffsetY;
+		hudOriginY = _player->_ra1ViewportOffsetY;
 	}
 
 	int hudX = hudOriginX + bar.xoffs;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0c8ad49928d..e1301126370 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -2219,14 +2219,6 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 						int ra1ViewX = _ra1ViewportOffsetX;
 						int ra1ViewY = _ra1ViewportOffsetY;
-						if (_insane) {
-							InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-							// Opcode 0x0B (FUN_1CDA7) uses FUN_223FE for Y compensation on
-							// transformed objects. A global framebuffer Y window shift causes
-							// floating HUD/perspective artifacts in L2 with current renderer.
-							if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B)
-								ra1ViewY = 0;
-						}
 
 						const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
 						const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);


Commit: c5b5b86f8d97274e2fd102873b4628ea8c01f588
    https://github.com/scummvm/scummvm/commit/c5b5b86f8d97274e2fd102873b4628ea8c01f588
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:37+02:00

Commit Message:
SCUMM: RA1: Add level 7 through 10 handlers

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 8c1c7d38e93..e7d9441590d 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -39,7 +39,7 @@ namespace Scumm {
 // Per-difficulty tuning tables from assault_data_3.bin
 // Indexed: difficulty * 0x28B + level * 0x1F + offset
 // Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
-static const int16 kTuningTable[6][3][9] = {
+static const int16 kTuningTable[10][3][9] = {
 	// Level 1 (Flight Training)
 	{
 		{ 100, 100,  60, 110,   0,   0,  15,   0,   0 },  // Easy
@@ -76,8 +76,32 @@ static const int16 kTuningTable[6][3][9] = {
 		{   0,   0,   0,   0,   1,  25,   0,   5, 100 },  // Normal
 		{   0,   0,   0,   0,   1,  28,   0,   6, 200 },  // Hard
 	},
+	// Level 7 (Imperial Probe Droids)
+	{
+		{  70, 150,  50,  25,  10,   0,  20,   0,  25 },  // Easy
+		{  72, 165, 155,  30,   8,   0,  30,   0,  50 },  // Normal
+		{ 110, 190,  55,  65,   3,   0,  33,   0,  75 },  // Hard
+	},
+	// Level 8 (Imperial Walkers)
+	{
+		{   0,   0,   0,   0,   5,   0,   0,   2,  25 },  // Easy
+		{   0,   0,   0,   0,   3,   0,   0,   5,  50 },  // Normal
+		{   0,   0,   0,   0,   1,   0,   0,   6,  75 },  // Hard
+	},
+	// Level 9 (Stormtroopers)
+	{
+		{   0,   0,   0,   0,   2,  20,  20,   0,  25 },  // Easy
+		{   0,   0,   0,   0,   1,  25,  30,   0,  50 },  // Normal
+		{   0,   0,   0,   0,   0,  28,  33,   0,  75 },  // Hard
+	},
+	// Level 10 (Protect Rebel Transport)
+	{
+		{ 100, 150, 150,  25,   7,   0,  12,   2,  50 },  // Easy
+		{ 100, 160, 200,  35,   4,   0,  30,   4, 100 },  // Normal
+		{ 100, 180, 250,  50,   3,   0,  33,   5, 100 },  // Hard
+	},
 };
-static const int kNumTunedLevels = 6;
+static const int kNumTunedLevels = 10;
 
 
 void InsaneRebel1::loadTuningForLevel(int level) {
@@ -163,6 +187,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_gameCounter = 0;
 	_pathBranchEnabled = false;
 	_rightPathSelected = false;
+	_levelRouteIndex = -1;
+	_pendingRouteIndex = -1;
 	_menuActive = false;
 	_menuConfirmed = false;
 	_menuSelection = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 6d3aa92d591..5a2a9d7b7b0 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -117,6 +117,10 @@ private:
 	bool runLevel4();
 	bool runLevel5();
 	bool runLevel6();
+	bool runLevel7();
+	bool runLevel8();
+	bool runLevel9();
+	bool runLevel10();
 
 	// Play a passive cinematic (no game callback, skippable)
 	// startFrame > 0: fast-forward (decode without display) to that frame
@@ -131,6 +135,7 @@ private:
 	void preprocessMouseAxes(int16 &inputX, int16 &inputY);
 	void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
 	void resetProjectionTable();
+	void checkDynamicLevelBranch();
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -297,6 +302,8 @@ private:
 	int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
 	bool _pathBranchEnabled;     // True when branching is active for this video
 	bool _rightPathSelected;     // True if player chose the right/easy path
+	int _levelRouteIndex;        // Current mid-level route/segment for branching levels
+	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
 
 	// Main menu / options state
 	void runOptionsMenu();
@@ -309,7 +316,7 @@ private:
 	bool _optionsActive;     // True when showing options instead of main menu
 	int _optionsSel;         // 0=difficulty, 1=turbulence, 2=back
 	bool _levelSelectActive; // True when showing level-select submenu
-	int _levelSelectSel;     // 0=Level1, 1=Level2, 2=Back
+	int _levelSelectSel;     // 0=Level1 ... N-1=Back
 	int _startLevel;         // 1-based start level for "Start New Game"
 
 	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index d48edddae88..7d3badbfc12 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -34,6 +34,29 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
 	return code >= 6 && code <= 18;
 }
 
+static const int16 kLevel7BranchFrames[6][6] = {
+	{ -1,  78, 267, 398, 556, 630 },
+	{ -1, 187, 376, 507, 665, 739 },
+	{ -1, 187, 376, 507, 665, 739 },
+	{ -1,  -1,  -1, 284, 442, 516 },
+	{ -1,  -1,  -1, 143, 301, 375 },
+	{ -1, 112, 301, 432, 590, 664 }
+};
+
+static const int16 kLevel7BranchDir[6] = {
+	0, 1, 1, -1, 1, 1
+};
+
+static const int16 kLevel7BranchThreshold[6] = {
+	0, 170, 170, 160, 160, 160
+};
+
+static const int16 kLevel8BranchFrames[3][3] = {
+	{ 2588, 1709,  262 },
+	{ 2323, 1444,   -2 },
+	{  877,   -2,   -2 }
+};
+
 void InsaneRebel1::resetFrameObjectState() {
 	memset(_frameObjectState, 0, sizeof(_frameObjectState));
 	for (int i = 0x50; i < 0x96; i++)
@@ -140,6 +163,61 @@ void InsaneRebel1::resetProjectionTable() {
 	rebuildProjectionTable(0, 1);
 }
 
+void InsaneRebel1::checkDynamicLevelBranch() {
+	if (!_interactiveVideoActive || _levelRouteIndex < 0 || _pendingRouteIndex >= 0 || _vm->_smushVideoShouldFinish)
+		return;
+
+	if (_currentLevel == 6) {
+		const int route = CLIP<int>(_levelRouteIndex, 0, 5);
+		for (int nextRoute = 1; nextRoute < 6; ++nextRoute) {
+			const int triggerFrame = kLevel7BranchFrames[route][nextRoute];
+			if (triggerFrame <= 0 || nextRoute == route || _frameCounter != (uint32)(triggerFrame - 1))
+				continue;
+
+			const bool takeBranch = (kLevel7BranchDir[nextRoute] > 0)
+				? (_shipPosX > kLevel7BranchThreshold[nextRoute])
+				: (_shipPosX < kLevel7BranchThreshold[nextRoute]);
+			if (!takeBranch)
+				continue;
+
+			_pendingRouteIndex = nextRoute;
+			_vm->_smushVideoShouldFinish = true;
+			debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d",
+				route, nextRoute, (unsigned)_frameCounter, _shipPosX);
+			return;
+		}
+	}
+
+	if (_currentLevel == 7) {
+		const int route = CLIP<int>(_levelRouteIndex, 0, 2);
+		const int frame = (int)_frameCounter;
+		const int leftBlockedFrame = kLevel8BranchFrames[route][2];
+		const int rightBlockedFrame = kLevel8BranchFrames[route][1];
+		int nextRoute = -1;
+
+		for (int i = 0; i < 3; ++i) {
+			const int triggerFrame = kLevel8BranchFrames[route][i];
+			if (triggerFrame >= 0 && frame == triggerFrame) {
+				if (_shipPosX < kRA1CenterX) {
+					if (frame != leftBlockedFrame)
+						nextRoute = 1;
+				} else {
+					if (frame != rightBlockedFrame)
+						nextRoute = 2;
+				}
+				break;
+			}
+		}
+
+		if (nextRoute >= 0 && nextRoute != route) {
+			_pendingRouteIndex = nextRoute;
+			_vm->_smushVideoShouldFinish = true;
+			debug(1, "RA1 L8 branch: route=%d -> %d at frame=%u shipX=%d",
+				route, nextRoute, (unsigned)_frameCounter, _shipPosX);
+		}
+	}
+}
+
 void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
 	x = (int16)(x - _perspectiveX);
 
@@ -406,6 +484,8 @@ void InsaneRebel1::updateShipPhysics() {
 		_pathBranchEnabled = false;
 	}
 
+	checkDynamicLevelBranch();
+
 	debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
 		_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
 		_posAccumX, _posAccumY, _shipDirIndex, _health,
@@ -666,6 +746,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	resetProjectionTable();
 
 	_frameCounter++;
+	checkDynamicLevelBranch();
 
 	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
 		_shipPosX, _shipPosY, _avgInputX, _avgInputY,
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 99f30618d78..36436309bf0 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,8 +29,8 @@
 
 namespace Scumm {
 
-static const int kRA1LevelSelectItemCount = 7;
-static const int kRA1LastImplementedLevel = 6;
+static const int kRA1LevelSelectItemCount = 11;
+static const int kRA1LastImplementedLevel = 10;
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
@@ -215,6 +215,10 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 			"LEVEL 4: STAR DESTROYER",
 			"LEVEL 5: TATOOINE ATTACK",
 			"LEVEL 6: ASTEROID CHASE",
+			"LEVEL 7: PROBE DROIDS",
+			"LEVEL 8: IMPERIAL WALKERS",
+			"LEVEL 9: STORMTROOPERS",
+			"LEVEL 10: REBEL TRANSPORT",
 			"BACK"
 		};
 
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index b84b0cdd16a..9e526320822 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -542,6 +542,436 @@ bool InsaneRebel1::runLevel6() {
 	return false;
 }
 
+bool InsaneRebel1::runLevel7() {
+	debug(1, "InsaneRebel1: Running level 7");
+
+	static const char *const kLevel7Segments[] = {
+		"LVL7/L7PLAY1.ANM",
+		"LVL7/L7PLAY2.ANM",
+		"LVL7/L7PLAY3.ANM",
+		"LVL7/L7PLAY4.ANM",
+		"LVL7/L7PLAY5.ANM",
+		"LVL7/L7PLAY6.ANM"
+	};
+
+	_currentLevel = 6;
+	loadLevelSprites(7);
+	loadTuningForLevel(6);
+
+	playCinematic("LVL7/L7INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 3;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		int route = 0;
+		while (!_vm->shouldQuit()) {
+			_levelRouteIndex = route;
+			_pendingRouteIndex = -1;
+			playInteractiveVideo(kLevel7Segments[route]);
+			if (_vm->shouldQuit())
+				return false;
+
+			if (_health < 0)
+				break;
+
+			if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
+				break;
+
+			route = _pendingRouteIndex;
+		}
+
+		_levelRouteIndex = -1;
+		_pendingRouteIndex = -1;
+
+		if (_health >= 0) {
+			playCinematic("LVL7/L7END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL7/L7NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL7/L7DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel8() {
+	debug(1, "InsaneRebel1: Running level 8");
+
+	static const char *const kLevel8Routes[] = {
+		"LVL8/L8PLAY.ANM",
+		"LVL8/L8PLAY2.ANM",
+		"LVL8/L8PLAY3.ANM"
+	};
+
+	_currentLevel = 7;
+	loadLevelSprites(8);
+	loadTuningForLevel(7);
+
+	playCinematic("LVL8/L8INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 3;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		int route = 0;
+		while (!_vm->shouldQuit()) {
+			_levelRouteIndex = route;
+			_pendingRouteIndex = -1;
+			playInteractiveVideo(kLevel8Routes[route]);
+			if (_vm->shouldQuit())
+				return false;
+
+			if (_health < 0)
+				break;
+
+			if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
+				break;
+
+			route = _pendingRouteIndex;
+		}
+
+		_levelRouteIndex = -1;
+		_pendingRouteIndex = -1;
+
+		if (_health >= 0) {
+			playCinematic("LVL8/L8END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL8/L8NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL8/L8DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel9() {
+	debug(1, "InsaneRebel1: Running level 9");
+
+	const int randPath1 = _vm->_rnd.getRandomNumber(1);
+	const int randPath2 = _vm->_rnd.getRandomNumber(1);
+	const int randPath3 = _vm->_rnd.getRandomNumber(1);
+
+	_currentLevel = 8;
+	loadLevelSprites(9);
+	loadTuningForLevel(8);
+
+	playCinematic("LVL9/L9INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 0;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		while (!_vm->shouldQuit()) {
+			playInteractiveVideo("LVL9/L9PLAY1.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			if (_health < 0)
+				break;
+
+			playCinematic("LVL9/L9CUT1.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			_shipPosX = kRA1CenterX;
+			_posAccumX = 0;
+			playInteractiveVideo("LVL9/L9PLAY2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			if (_health < 0)
+				break;
+
+			const int side1 = (_shipPosX < kRA1CenterX) ? 0 : 1;
+			playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			if (side1 == randPath1) {
+				playCinematic("LVL9/L9CUT2A.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				continue;
+			}
+
+			playCinematic("LVL9/L9CUT2B.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			playInteractiveVideo("LVL9/L9PLAY3A.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			if (_health < 0)
+				break;
+
+			if (_killCount < 15) {
+				playInteractiveVideo("LVL9/L9PLAY3B.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				if (_health < 0)
+					break;
+			}
+
+			playCinematic("LVL9/L9CUT3.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			_shipPosX = kRA1CenterX;
+			_posAccumX = 0;
+			playInteractiveVideo("LVL9/L9PLAY4.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			if (_health < 0)
+				break;
+
+			const int side2 = (_shipPosX < kRA1CenterX) ? 0 : 1;
+			playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			if (side2 == randPath2) {
+				playCinematic(side2 == 0 ? "LVL9/L9CUT4AX.ANM" : "LVL9/L9CUT4B.ANM");
+				if (_vm->shouldQuit())
+					return false;
+
+				playInteractiveVideo("LVL9/L9PLAY5.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				if (_health < 0)
+					break;
+
+				playCinematic("LVL9/L9CUT5.ANM");
+				if (_vm->shouldQuit())
+					return false;
+
+				_shipPosX = kRA1CenterX;
+				_posAccumX = 0;
+				playInteractiveVideo("LVL9/L9PLAY6.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				if (_health < 0)
+					break;
+
+				const int side3 = (_shipPosX < kRA1CenterX) ? 0 : 1;
+				if (side3 == randPath3) {
+					playCinematic("LVL9/L9CUT6A.ANM");
+					if (_vm->shouldQuit())
+						return false;
+					continue;
+				}
+
+				playCinematic("LVL9/L9CUT6B.ANM");
+				if (_vm->shouldQuit())
+					return false;
+			} else {
+				playCinematic(side2 == 0 ? "LVL9/L9CUT4A.ANM" : "LVL9/L9CUT4BX.ANM");
+				if (_vm->shouldQuit())
+					return false;
+			}
+
+			playInteractiveVideo("LVL9/L9PLAY7.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			if (_health < 0)
+				break;
+
+			playCinematic("LVL9/L9END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL9/L9NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL9/L9DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+bool InsaneRebel1::runLevel10() {
+	debug(1, "InsaneRebel1: Running level 10");
+
+	_currentLevel = 9;
+	loadLevelSprites(10);
+	loadTuningForLevel(9);
+
+	playCinematic("LVL10/L10INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		playInteractiveVideo("LVL10/L10PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			playCinematic("LVL10/L10END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL10/L10NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL10/L10DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
 // Main game entry point — called from ScummEngine::go().
 // Matches original flow at 0x15597: intro → menu → level.
 void InsaneRebel1::runGame() {
@@ -552,7 +982,11 @@ void InsaneRebel1::runGame() {
 		&InsaneRebel1::runLevel3,
 		&InsaneRebel1::runLevel4,
 		&InsaneRebel1::runLevel5,
-		&InsaneRebel1::runLevel6
+		&InsaneRebel1::runLevel6,
+		&InsaneRebel1::runLevel7,
+		&InsaneRebel1::runLevel8,
+		&InsaneRebel1::runLevel9,
+		&InsaneRebel1::runLevel10
 	};
 
 	// Play intro sequence (logo + opening)


Commit: 560d6f9ca5bb45cf5b4295e72cc3be864aacdfcd
    https://github.com/scummvm/scummvm/commit/560d6f9ca5bb45cf5b4295e72cc3be864aacdfcd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:37+02:00

Commit Message:
SCUMM: RA1: Center interactive FTCH placement

Changed paths:
    engines/scumm/smush/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 83a78367bfd..dcf820eb29c 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -35,6 +35,22 @@
 
 namespace Scumm {
 
+static void ra1ApplyCenteredFetchPlacement(InsaneRebel1 *rebel1, int width, int height, int &left, int &top) {
+	int16 centerX = (int16)(left + (width >> 1));
+	int16 centerY = (int16)(top + (height >> 1));
+
+	rebel1->projectGameplayPoint(centerX, centerY);
+
+	const int projectedLeft = (int)centerX - (width >> 1);
+	const int projectedTop = (int)centerY - (height >> 1);
+
+	// RestoreStoredFramePatch routes FTCH through DispatchFobjCodec with flag 0x800.
+	// That path applies ProjectPointToScreen() to the center point, then only moves
+	// a quarter of the projected delta before decoding the stored FOBJ.
+	left -= ((projectedLeft - left) >> 2);
+	top -= ((projectedTop - top) >> 2);
+}
+
 SmushPlayerRebel1::SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
 	: SmushPlayer(scumm, imuseDigital, insane) {
 	initGamePlayerFields();
@@ -111,9 +127,18 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 
 		if (_insane) {
 			InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-			if (rebel1->isInteractiveVideoActive() && rebel1->getActiveGameOpcode() == 0x0B &&
-				_storedFobjWidth == _vm->_screenWidth) {
-				left += _ra1ViewportOffsetX;
+			if (rebel1->isInteractiveVideoActive()) {
+				if (rebel1->getActiveGameOpcode() == 0x0B && _storedFobjWidth == _vm->_screenWidth) {
+					left += _ra1ViewportOffsetX;
+				} else {
+					ra1ApplyCenteredFetchPlacement(rebel1, _storedFobjWidth, _storedFobjHeight, left, top);
+					// ScummVM currently emulates the RA1 camera with a source-window crop
+					// for interactive scenes. FTCH placement from the original executable
+					// is computed in fixed presentation space, so convert it back into the
+					// cropped buffer space used by the current renderer.
+					left += _ra1ViewportOffsetX;
+					top += _ra1ViewportOffsetY;
+				}
 			}
 		}
 


Commit: 2a4d224a45dc9e55d8da89659048c7aba7aaf2db
    https://github.com/scummvm/scummvm/commit/2a4d224a45dc9e55d8da89659048c7aba7aaf2db
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:38+02:00

Commit Message:
SCUMM: RA1: Render HUD plate opaquely

Changed paths:
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ca56df82c2f..ee10d2ce5f5 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -856,7 +856,9 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	int hudX = hudOriginX + bar.xoffs;
 	int hudY = hudOriginY + bar.yoffs;
 
-	// Draw the status bar background with transparency (pixel 0 = transparent)
+	// DOS RA1 draws the HUD plate through DrawFobjGlyph(..., flags=0x181),
+	// which selects the opaque blit path. Keep zero-valued pixels black instead
+	// of treating them as transparent.
 	if (bar.data && bar.width > 0 && bar.height > 0) {
 		int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
 		int srcOffX = 0, srcOffY = 0;
@@ -868,11 +870,7 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		for (int iy = 0; iy < drawH; iy++) {
 			const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
 			byte *d = dst + (drawY + iy) * pitch + drawX;
-			for (int ix = 0; ix < drawW; ix++) {
-				byte px = s[ix];
-				if (px != 0)
-					d[ix] = px;
-			}
+			memcpy(d, s, drawW);
 		}
 
 		debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index dcf820eb29c..1188ee84f3b 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -130,6 +130,7 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 			if (rebel1->isInteractiveVideoActive()) {
 				if (rebel1->getActiveGameOpcode() == 0x0B && _storedFobjWidth == _vm->_screenWidth) {
 					left += _ra1ViewportOffsetX;
+					top += _ra1ViewportOffsetY;
 				} else {
 					ra1ApplyCenteredFetchPlacement(rebel1, _storedFobjWidth, _storedFobjHeight, left, top);
 					// ScummVM currently emulates the RA1 camera with a source-window crop


Commit: ad1713032f7e4590ac3e35213bf8926b5c603658
    https://github.com/scummvm/scummvm/commit/ad1713032f7e4590ac3e35213bf8926b5c603658
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:38+02:00

Commit Message:
SCUMM: RA1: Enable controls in level 3

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 7d3badbfc12..bfbc1f70b95 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -279,21 +279,35 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
 void InsaneRebel1::updateShipPhysics() {
 	_frameCounter++;
 
+	// HandleGameOp07_ShipFlight resets the ship accumulators and camera when
+	// the GAME 0x07 frame counter enters at 0. Level 1 happened to work because
+	// its runlevel code pre-initialized the same state, but later 0x07-driven
+	// stages like L3 rely on the handler to do this reset itself.
+	if (_gameCounter == 0) {
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_perspectiveX = 0x20;
+		_perspectiveY = 0x17;
+		resetProjectionTable();
+	}
+
 	// Decrement cooldown
 	if (_damageCooldown > 0)
 		_damageCooldown--;
 
-	// --- Step 1: Mouse input as offset from screen center ---
-	// Original: _DAT_756C (horizontal), _DAT_756E (vertical) in 320x200 space.
-	int16 mouseX = (int16)_vm->_mouse.x;
-	int16 mouseY = (int16)_vm->_mouse.y;
-	if (_player && _player->_width > 0 && _player->_height > 0 &&
-		(mouseX >= 320 || mouseY >= 200)) {
-		mouseX = (int16)((mouseX * 320) / _player->_width);
-		mouseY = (int16)((mouseY * 200) / _player->_height);
-	}
-	int16 inputX = (int16)(mouseX - 160);
-	int16 inputY = (int16)(mouseY - 100);
+	// --- Step 1: Gameplay axes from FUN_231BE ---
+	// HandleGameOp07_ShipFlight consumes the preprocessed axes in DAT_756C/756E,
+	// not raw mouse coordinates. Reuse the same centered-axis law here.
+	int16 inputX = 0;
+	int16 inputY = 0;
+	preprocessMouseAxes(inputX, inputY);
 	inputX = CLIP<int16>(inputX, -127, 127);
 	inputY = CLIP<int16>(inputY, -127, 127);
 
@@ -545,20 +559,13 @@ void InsaneRebel1::updateTurretPhysics() {
 
 	// FUN_1E6A7 movement gate: counter > 8 or flags bit 0x40.
 	if (counter > 8 || (modeFlags & 0x40)) {
-		int16 mouseX = (int16)_vm->_mouse.x;
-		int16 mouseY = (int16)_vm->_mouse.y;
-		if (_player && _player->_width > 0 && _player->_height > 0 &&
-			(mouseX >= 320 || mouseY >= 200)) {
-			mouseX = (int16)((mouseX * 320) / _player->_width);
-			mouseY = (int16)((mouseY * 200) / _player->_height);
-		}
-
-		// FUN_1F3F8/FUN_231BE preprocesses mouse deltas before FUN_1E6A7 consumes
-		// DAT_756C/DAT_756E. Keep X in 320-space then scale to ±127 (RA2 parity),
-		// Y clamped to ±127.
-		int16 rawInputX = CLIP<int16>((int16)(mouseX - 160), -160, 160);
-		int16 inputX = (int16)((rawInputX * 127) / 160);
-		int16 inputY = CLIP<int16>((int16)(mouseY - 100), -127, 127);
+		// FUN_1E6A7 consumes DAT_756C/DAT_756E from the shared input bridge,
+		// not raw mouse coordinates.
+		int16 inputX = 0;
+		int16 inputY = 0;
+		preprocessMouseAxes(inputX, inputY);
+		inputX = CLIP<int16>(inputX, -127, 127);
+		inputY = CLIP<int16>(inputY, -127, 127);
 
 		_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
 		_rollAccum = (_rollAccum * 3) >> 2;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 9e526320822..562d30d09b3 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -223,7 +223,8 @@ bool InsaneRebel1::runLevel2() {
 
 	_currentLevel = 1;
 	loadLevelSprites(2);
-	loadTuningForLevel(1);
+	// DOS RunLevel2Flow launches L2PLAY.ANM with gameplay selector 2.
+	loadTuningForLevel(2);
 
 	playCinematic("LVL2/L2INTRO.ANM");
 	if (_vm->shouldQuit())
@@ -279,7 +280,8 @@ bool InsaneRebel1::runLevel3() {
 
 	_currentLevel = 2;
 	loadLevelSprites(3);
-	loadTuningForLevel(2);
+	// DOS RunLevel3Flow launches L3PLAY.ANM with gameplay selector 3.
+	loadTuningForLevel(3);
 
 	playCinematic("LVL3/L3INTRO.ANM");
 	if (_vm->shouldQuit())
@@ -335,7 +337,8 @@ bool InsaneRebel1::runLevel4() {
 
 	_currentLevel = 3;
 	loadLevelSprites(4);
-	loadTuningForLevel(3);
+	// DOS RunLevel4Flow launches L4PLAY1/2.ANM with gameplay selector 4.
+	loadTuningForLevel(4);
 
 	playCinematic("LVL4/L4INTRO.ANM");
 	if (_vm->shouldQuit())


Commit: ce45d205dd279258a2e8340fe03ba08bbfe82b80
    https://github.com/scummvm/scummvm/commit/ce45d205dd279258a2e8340fe03ba08bbfe82b80
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:38+02:00

Commit Message:
SCUMM: RA1: Refine mouse and frame opcode state

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_render.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index e7d9441590d..47aecc90d06 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -170,6 +170,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_turretEmitterRightX = 0;
 	_turretEmitterRightY = 0;
 	_activeGameOpcode = 0;
+	_frameGameOpcodeMask = 0;
+	_frameDispatchFlags = 0;
 
 	_health = kMaxHealth;
 	_lives = 3;
@@ -203,6 +205,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	// Shooting/targeting state
 	_playerFired = false;
 	_fireCooldown = 0;
+	_gameplayFlags75fe = 0;
+	_gameplayFlags75ff = 0;
 	memset(_shotSlots, 0, sizeof(_shotSlots));
 	_shotAlternator = 0;
 	_shotSideToggle = false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 5a2a9d7b7b0..240467be46f 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -86,6 +86,9 @@ public:
 	bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
 	int getCurrentLevel() const { return _currentLevel; }
 	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
+	bool hasFrameGameOpcode(uint16 opcode) const {
+		return opcode < 32 && (_frameGameOpcodeMask & (1u << opcode)) != 0;
+	}
 	int16 getPerspectiveX() const { return _perspectiveX; }
 	int16 getPerspectiveY() const { return _perspectiveY; }
 	void projectGameplayPoint(int16 &x, int16 &y) const;
@@ -244,9 +247,11 @@ private:
 	int16 _turretEmitterLeftY;
 	int16 _turretEmitterRightX;
 	int16 _turretEmitterRightY;
-	// Last per-frame GAME movement handler opcode (0x07/0x08/0x09/0x0A/0x0B/0x1A).
-	// Used to mirror assembly handler-specific overlay pipeline behavior.
+	// Last per-frame GAME opcode observed in the current playback stream.
+	// Kept for legacy call sites; frame-accurate dispatch uses _frameGameOpcodeMask.
 	uint16 _activeGameOpcode;
+	uint32 _frameGameOpcodeMask;
+	uint16 _frameDispatchFlags;
 
 	// Difficulty (0=easy, 1=normal, 2=hard) — matches original DAT_22BC
 	int _difficulty;
@@ -322,8 +327,10 @@ private:
 	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
 
 	// Shooting state — FUN_1CCA0 (0x1CCA0)
-	bool _playerFired;       // 0x7570: fire button pressed this frame
-	int16 _fireCooldown;     // 0x757C: button-edge gate in original input pipeline
+	bool _playerFired;       // 0x7570: current fire-button state
+	int16 _fireCooldown;     // 0x757C: previous-frame fire-button state (edge gate)
+	uint16 _gameplayFlags75fe; // 0x75FE: gameplay mode flags
+	uint16 _gameplayFlags75ff; // 0x75FF: targeting / shot-style flags
 
 	// Explosion shot slots (2 slots) — FUN_1CCA0 (0x1CCA0)
 	static const int kMaxShotSlots = 2;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index bfbc1f70b95..34571abf24d 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -27,11 +27,53 @@
 
 namespace Scumm {
 
-// LVL1 stage-2 0x5D damage/event codes observed in L1PLAY2.ANM.
-// Original DOS loop uses table/mask-driven latch routing before FUN_1E6A7;
-// in ScummVM's direct GAME dispatch path, map this known range explicitly.
+// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
+// (6..18), while the recovered outer loop compares the post-latch state against the
+// later translated values seen in the executable. Accept both representations.
 static inline bool isL1Stage2DamageLatch(uint16 code) {
-	return code >= 6 && code <= 18;
+	switch (code) {
+	case 0x0006:
+	case 0x0007:
+	case 0x0008:
+	case 0x0009:
+	case 0x000A:
+	case 0x000B:
+	case 0x000C:
+	case 0x000D:
+	case 0x000E:
+	case 0x000F:
+	case 0x0010:
+	case 0x0011:
+	case 0x0012:
+	case 0x0049:
+	case 0x004B:
+	case 0x004E:
+	case 0x0051:
+	case 0x0054:
+	case 0x005C:
+	case 0x005E:
+	case 0x0060:
+	case 0x0062:
+	case 0x0064:
+		return true;
+	default:
+		return false;
+	}
+}
+
+static inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
+	switch (frameCounter) {
+	case 0x0034:
+	case 0x00ED:
+	case 0x0173:
+		return perspectiveX <= 0x28;
+	case 0x0088:
+	case 0x00FA:
+	case 0x0151:
+		return perspectiveX >= 0x18;
+	default:
+		return false;
+	}
 }
 
 static const int16 kLevel7BranchFrames[6][6] = {
@@ -239,14 +281,13 @@ void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
 
 // preprocessMouseAxes — FUN_231BE (0x231BE) centered-axis output law, adapted to
 // ScummVM's absolute 320x200 mouse space.
-// The original DOS path combines a full-scale centered axis term with an extra
-// mouse-bias term `(rawX-0x140)>>2`, `(rawY-100)>>1` maintained by
-// FUN_23115/FUN_231BE recenter globals. In ScummVM, mirroring the DOS recenter
-// box makes LVL2 control unusably constrained, so keep the FUN_231BE output law
-// (normalized axis + bias) but feed it directly from logical mouse coordinates.
-// That preserves the handler's expected `_DAT_756C/_DAT_756E` amplitude without
-// introducing the DOS safe-window box or extra latency.
+// Preserve the DOS bias/offset persistence and one-frame jump latch from
+// FUN_231BE, but avoid hard recentring the host mouse into the DOS safe window.
+// The actual frame-averaging behavior stays untouched.
 void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
+	if (_mouseRecentering)
+		return;
+
 	int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
 	int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
 	const int16 rawX = (int16)(logicalX << 1);
@@ -255,19 +296,77 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
 	const int16 deltaY = (int16)(logicalY - kRA1CenterY);
 	const int16 normX = (int16)(((int32)deltaX * 127) / 160);
 	const int16 normY = (int16)(((int32)deltaY * 127) / 100);
-	const int16 biasX = (int16)((rawX - 0x140) >> 2);
-	const int16 biasY = (int16)((rawY - 100) >> 1);
+	int16 biasX = (int16)((rawX + _mouseOffsetX - 0x140) >> 2);
+	int16 biasY = (int16)((rawY + _mouseOffsetY - 100) >> 1);
+
+	if (biasY < 0x65) {
+		const bool largeJump =
+			(_mousePrevBiasX + 0x14 < biasX) ||
+			(_mousePrevBiasY + 0x14 < biasY) ||
+			(biasX < _mousePrevBiasX - 0x14) ||
+			(biasY < _mousePrevBiasY - 0x14);
+		if (largeJump) {
+			if (!_mouseBiasLatch) {
+				biasX = _mousePrevBiasX;
+				biasY = _mousePrevBiasY;
+				_mouseBiasLatch = true;
+			}
+		} else {
+			_mouseBiasLatch = false;
+		}
+	} else {
+		biasX = _mousePrevBiasX;
+		biasY = _mousePrevBiasY;
+		_mouseBiasLatch = true;
+	}
+
 	const int16 scaledX = (int16)(normX + biasX);
 	const int16 scaledY = (int16)(normY + biasY);
 
-	_mouseBiasX = scaledX;
-	_mouseBiasY = scaledY;
-	_mousePrevBiasX = scaledX;
-	_mousePrevBiasY = scaledY;
-	_mouseBiasLatch = false;
-	_mouseRecentering = false;
-	_mouseOffsetX = 0;
-	_mouseOffsetY = 0;
+	_mouseBiasX = biasX;
+	_mouseBiasY = biasY;
+	_mousePrevBiasX = biasX;
+	_mousePrevBiasY = biasY;
+
+	int accumX = rawX + _mouseOffsetX;
+	if (accumX < 0xC0)
+		_mouseOffsetX = (int16)(0xC0 - rawX);
+	else if (accumX > 0x1C0)
+		_mouseOffsetX = (int16)(0x1C0 - rawX);
+
+	int accumY = rawY + _mouseOffsetY;
+	if (accumY < -0x1C)
+		_mouseOffsetY = (int16)(-0x1C - rawY);
+	else if (accumY > 0xE4)
+		_mouseOffsetY = (int16)(0xE4 - rawY);
+
+	accumX = rawX + _mouseOffsetX;
+	if (accumX < 0x145) {
+		if (accumX < 0x142) {
+			if (accumX < 0x13C)
+				_mouseOffsetX += 4;
+			else if (accumX < 0x13F)
+				_mouseOffsetX += 1;
+		} else {
+			_mouseOffsetX -= 1;
+		}
+	} else {
+		_mouseOffsetX -= 4;
+	}
+
+	accumY = rawY + _mouseOffsetY;
+	if (accumY < 0x69) {
+		if (accumY < 0x66) {
+			if (accumY < 0x60)
+				_mouseOffsetY += 4;
+			else if (accumY < 99)
+				_mouseOffsetY += 1;
+		} else {
+			_mouseOffsetY -= 1;
+		}
+	} else {
+		_mouseOffsetY -= 4;
+	}
 
 	inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
 	inputY = CLIP<int16>(scaledY, -127, 127);
@@ -515,13 +614,18 @@ void InsaneRebel1::updateTurretPhysics() {
 	// The 0x10/0x40 gates come from dispatcher arg4 (callback control bits),
 	// not from GAME payload fields.
 	const int32 counter = _gameCounter;
-	const byte modeFlags = 0;
+	const uint16 modeFlags = _frameDispatchFlags;
 
 	// RA1 latches consumed by handler family in FUN_1B297.
+	if (_currentLevel == 0 && _flyControlMode == 2 && isL1Stage2SweepDamage((uint16)counter, _perspectiveX))
+		_damageFlags |= 0x20;
 	if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
 		isL1Stage2DamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
-	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+	if (_gameLatch5F != 0 &&
+		((_currentLevel == 0 && _flyControlMode == 2)
+			? (_vm->_rnd.getRandomNumber(2) == 0)
+			: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
 		_damageFlags |= 0x80;
 
 	if (counter == 0) {
@@ -529,8 +633,6 @@ void InsaneRebel1::updateTurretPhysics() {
 		_posAccumY = 0;
 		_rollAccum = 0;
 		_liftSmooth = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
 		_damageFlags = 0;
 		_prevDamageFlags = 0;
 		_damageCooldown = 0;
@@ -586,12 +688,6 @@ void InsaneRebel1::updateTurretPhysics() {
 	const int16 offsetX = (int16)(_posAccumX >> 8);
 	const int16 offsetY = (int16)(_posAccumY >> 8);
 
-	// FUN_1D79C tail sets pointer center from offsets:
-	//   _74BE = _74B6 + _74BA
-	//   _74C0 = (_74B8 + _74BC - 0x23) - (_74BC >> 3)
-	_shipPosX = (int16)(kRA1CenterX + offsetX);
-	_shipPosY = (int16)((kRA1CenterY + offsetY - 0x23) - (offsetY >> 3));
-
 	_perspectiveX = CLIP<int16>((int16)(offsetX + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)(offsetY + 0x17), 0, 0x2E);
 
@@ -835,22 +931,41 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		}
 
 		_activeGameOpcode = 0;
+		_frameGameOpcodeMask = 0;
+		_frameDispatchFlags = 0;
 		resetProjectionTable();
 		debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
 		break;
 
 	case 0x5D:
-		_gameLatch5D = (uint16)param1;
+		if ((uint16)param1 == 0xFFFF) {
+			_gameLatch5D = 0xFFFF;
+		} else if (param1 > 0) {
+			const int bitIndex = (int)param1 - 1;
+			const int byteIndex = bitIndex >> 3;
+			if (byteIndex >= 0 && byteIndex < 0x96 &&
+				(_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+				_gameLatch5D = (uint16)param1;
+			}
+		}
 		debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
 		break;
 
 	case 0x5F:
-		_gameLatch5F = (uint16)param1;
+		if (param1 > 0) {
+			const int bitIndex = (int)param1 - 1;
+			const int byteIndex = bitIndex >> 3;
+			if (byteIndex >= 0 && byteIndex < 0x96 &&
+				(_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+				_gameLatch5F = (uint16)param1;
+			}
+		}
 		debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
 		break;
 
 	case 0x07:
 		_activeGameOpcode = 0x07;
+		_frameGameOpcodeMask |= (1u << 0x07);
 		// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
 		// f1 is the original's _DAT_7740 (game frame counter)
 		// f3 is the drift/wind parameter combined with tuning table
@@ -916,6 +1031,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 	case 0x0B:
 		_activeGameOpcode = 0x0B;
+		_frameGameOpcodeMask |= (1u << 0x0B);
 		// Asteroid/surface per-frame handler (FUN_1CDA7).
 		// field1 = frame counter, field2 = max frames
 		_gameCounter = param1;
@@ -958,6 +1074,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	case 0x19:
 	case 0x1A:
 		_activeGameOpcode = (uint16)opcode;
+		_frameGameOpcodeMask |= (1u << opcode);
 		_gameCounter = param1;
 		if (subSize >= 20) {
 			uint32 param2 = b.readUint32BE();
@@ -977,7 +1094,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 // processShot — FUN_1CCA0 (0x1CCA0). Spawns shot into explosion slot when fired.
 // Called once per frame during interactive rendering.
 void InsaneRebel1::processShot() {
-	if (!_playerFired)
+	if (!_playerFired || _fireCooldown != 0)
 		return;
 
 	// Find first available slot (timer < 1 or > 5), matching FUN_1CCA0.
@@ -989,7 +1106,6 @@ void InsaneRebel1::processShot() {
 		}
 	}
 	if (slot < 0) {
-		_playerFired = false;
 		return;
 	}
 
@@ -998,7 +1114,7 @@ void InsaneRebel1::processShot() {
 	const int16 shipCenterX = turretMode ? (int16)(kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
 	const int16 shipCenterY = turretMode ? (int16)(kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
 
-	_shotSlots[slot].timer = 5;
+	_shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
 	_shotSlots[slot].posX = _shipPosX;
 	_shotSlots[slot].posY = _shipPosY;
 	_shotSlots[slot].centerX = shipCenterX;
@@ -1006,8 +1122,6 @@ void InsaneRebel1::processShot() {
 	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
 
-	_playerFired = false;
-
 	debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
 }
 
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 36436309bf0..59ca5357f74 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -126,6 +126,10 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 			_playerFired = true;
 			return true;
 		}
+		if (event.type == Common::EVENT_LBUTTONUP) {
+			_playerFired = false;
+			return true;
+		}
 	}
 
 	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ee10d2ce5f5..ce61d86921a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -325,6 +325,9 @@ static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 // RA1 decodes FOBJs at chunk coordinates, then displays a scrolled 320x200
 // window inside the 384x242 framebuffer.
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
+	_frameGameOpcodeMask = 0;
+	_frameDispatchFlags = 0;
+
 	if (_interactiveVideoActive && _player) {
 		// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
 		// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
@@ -356,14 +359,20 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _screenHeight;
 	int pitch = width;
 
-	const bool asteroidMode = (_activeGameOpcode == 0x0B);
+	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
+	const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
+		(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
 	if (asteroidMode) {
 		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
 		updateAsteroidPhysics();
 	} else {
-		const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
-		const bool flightMode = (_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
-								 _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+		const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
+			(!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
+		const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
+			hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
+			(!haveFrameGameOpcodes &&
+				(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
+				 _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
 
 		// Dispatch movement path by GAME handler family:
 		//   0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
@@ -378,6 +387,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
 		// Do not render the in-engine death overlay in this path; finish immediately.
 		if (_currentLevel == 0 && _health < 0) {
+			_fireCooldown = _playerFired ? 1 : 0;
 			_vm->_smushVideoShouldFinish = true;
 			return;
 		}
@@ -396,12 +406,18 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
-	// in handlers 0x09/0x0A/0x0B/0x1A. In LVL1 stage-2 samples, 0x08 drives
-	// turret mode and needs the same targeting overlays enabled.
+	// in handlers 0x09/0x0A/0x0B/0x1A. LVL1 stage-2 works because the stream emits
+	// both 0x0A and 0x08 in the same frame, not because 0x08 owns the overlay path.
 	const bool hasTargetingPipeline =
-		(_activeGameOpcode == 0x08 || _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
-		 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A);
+		hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
+		hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
+		(!haveFrameGameOpcodes &&
+			(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+			 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
 	if (hasTargetingPipeline) {
+		const bool turretTargetingMode =
+			hasFrameGameOpcode(0x0A) ||
+			(!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
 		renderTargetBoxes(renderBitmap, pitch, width, height);
 		processShot();
 		for (int i = 0; i < kMaxShotSlots; i++) {
@@ -411,6 +427,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		renderLaserShots(renderBitmap, pitch, width, height);
 		renderGostSlots(renderBitmap, pitch, width, height);
 		renderTargeting(renderBitmap, pitch, width, height);
+
+		// FUN_1D79C (GAME 0x0A) owns the cursor center in stage-2 turret mode.
+		// The preceding overlay/shot pass uses the previous frame's cursor; the
+		// handler then publishes the next cursor position from the current
+		// ship-offset and camera state.
+		if (turretTargetingMode) {
+			const int16 shipOffsetX = (int16)(_posAccumX >> 8);
+			const int16 shipOffsetY = (int16)(_posAccumY >> 8);
+			_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
+			_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
+		}
 	} else {
 		// Keep lock/target accumulators quiescent when current handler doesn't
 		// execute FUN_1C940/FUN_1CCA0/FUN_1C9CD/FUN_1CB22.
@@ -423,6 +450,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	renderExplosions(renderBitmap, pitch, width, height);
 	renderHUD(renderBitmap, pitch, width, height);
+	_fireCooldown = _playerFired ? 1 : 0;
 }
 
 // renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
@@ -459,7 +487,7 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 	if (markerBank.numSprites > 0) {
 		// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
 		// Baseline RA1 targeting uses '^' and animation e..h.
-		const bool altMarkerSet = false;
+		const bool altMarkerSet = (_gameplayFlags75ff & 0x2) != 0;
 
 		// Lock indicator at fixed center positions:
 		// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).


Commit: 2a7ffb4168924d14ee45b1b38c800c6f336ff405
    https://github.com/scummvm/scummvm/commit/2a7ffb4168924d14ee45b1b38c800c6f336ff405
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:39+02:00

Commit Message:
SCUMM: RA1: Trigger death video immediately

Changed paths:
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ce61d86921a..6d669ca12e6 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -383,10 +383,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			updateShipPhysics();
 		}
 
-		// LVL1 assembly flow exits gameplay loops as soon as health drops below 0
-		// (see 0x1626E/0x162EE -> 0x165DD and 0x1640B -> 0x16614), then plays crash video.
-		// Do not render the in-engine death overlay in this path; finish immediately.
-		if (_currentLevel == 0 && _health < 0) {
+		// Most RA1 gameplay loops exit the current interactive movie as soon as
+		// health drops below 0, then dispatch to the level-specific retry/death
+		// clip. LVL9's on-foot flow is the main exception: it keeps pumping while
+		// deathTimer > 1 before branching out of the current segment.
+		if (_health < 0 && _currentLevel != 8) {
 			_fireCooldown = _playerFired ? 1 : 0;
 			_vm->_smushVideoShouldFinish = true;
 			return;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 562d30d09b3..1b7eb1e788d 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -445,10 +445,10 @@ bool InsaneRebel1::runLevel5() {
 
 		if (_killCount <= 2) {
 			if (_lives > 0) {
+				_lives--;
 				playCinematic("LVL5/L5RETRY.ANM");
 				if (_vm->shouldQuit())
 					return false;
-				_lives--;
 				continue;
 			}
 


Commit: a6ab490d52fb6773025f7811aa2a30f955fcd921
    https://github.com/scummvm/scummvm/commit/a6ab490d52fb6773025f7811aa2a30f955fcd921
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:39+02:00

Commit Message:
SCUMM: RA1: Add additional level implementations

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 47aecc90d06..d6d1e3ee582 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -187,11 +187,13 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_frameCounter = 0;
 	_interactiveVideoActive = false;
 	_gameCounter = 0;
-	_pathBranchEnabled = false;
-	_rightPathSelected = false;
-	_levelRouteIndex = -1;
-	_pendingRouteIndex = -1;
-	_menuActive = false;
+		_pathBranchEnabled = false;
+		_rightPathSelected = false;
+		_levelRouteIndex = -1;
+		_pendingRouteIndex = -1;
+		_levelRouteChoice = 0;
+		_levelGameplayPhase = 0;
+		_menuActive = false;
 	_menuConfirmed = false;
 	_menuSelection = 0;
 	_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 240467be46f..fa6456355ad 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -303,12 +303,14 @@ private:
 	// Path branching for levels with left/right alternative videos.
 	// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
 	// We check ship position at that counter value to decide left vs right path.
-	static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
-	int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
-	bool _pathBranchEnabled;     // True when branching is active for this video
-	bool _rightPathSelected;     // True if player chose the right/easy path
-	int _levelRouteIndex;        // Current mid-level route/segment for branching levels
-	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
+		static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
+		int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
+		bool _pathBranchEnabled;     // True when branching is active for this video
+		bool _rightPathSelected;     // True if player chose the right/easy path
+		int _levelRouteIndex;        // Current mid-level route/segment for branching levels
+		int _pendingRouteIndex;      // Next route requested by original frame-branch logic
+		int _levelRouteChoice;       // Level-local pending branch choice (0=none, 1=left, 2=right)
+		int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
 
 	// Main menu / options state
 	void runOptionsMenu();
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 34571abf24d..c01f5175bbd 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -99,6 +99,259 @@ static const int16 kLevel8BranchFrames[3][3] = {
 	{  877,   -2,   -2 }
 };
 
+static inline bool isLevel4DamageLatch(uint16 code) {
+	switch (code) {
+	case 0x0008:
+	case 0x000A:
+	case 0x000C:
+	case 0x0010:
+	case 0x0018:
+	case 0x001C:
+	case 0x001E:
+	case 0x0024:
+	case 0x0026:
+	case 0x0028:
+	case 0x002D:
+		return true;
+	default:
+		return false;
+	}
+}
+
+static inline bool isLevel6DamageLatch(uint16 code) {
+	switch (code) {
+	case 0x0003:
+	case 0x0008:
+	case 0x0009:
+	case 0x000D:
+	case 0x000F:
+	case 0x0028:
+		return true;
+	default:
+		return false;
+	}
+}
+
+static inline bool isLevel10DamageLatch(uint16 code) {
+	if (code < 0x7F) {
+		if (code > 0x3D) {
+			if (code > 0x3E) {
+				if (code < 0x5C) {
+					if (code < 0x54) {
+						if (code < 0x44)
+							return false;
+						if (code > 0x44)
+							return code == 0x4F;
+					} else if (code > 0x54) {
+						if (code < 0x58)
+							return code == 0x56;
+						if (code < 0x59)
+							return true;
+						return code == 0x5A;
+					}
+				} else if (code > 0x5C) {
+					if (code < 0x73) {
+						if (code < 0x6A)
+							return false;
+						if (code > 0x6A)
+							return code == 0x6F;
+					} else if (code > 0x73) {
+						if (code < 0x77)
+							return code == 0x75;
+						if (code < 0x78)
+							return true;
+						return code == 0x7B;
+					}
+				}
+			}
+			return true;
+		}
+
+		if (code > 0x1F) {
+			if (code > 0x20) {
+				if (code < 0x2E) {
+					if (code < 0x22)
+						return false;
+					if (code > 0x22)
+						return code == 0x24;
+				} else if (code > 0x2E) {
+					if (code < 0x35)
+						return code == 0x32;
+					if (code < 0x36)
+						return false;
+					return code == 0x3C;
+				}
+			}
+			return true;
+		}
+
+		if (code > 0x0E) {
+			if (code > 0x0F) {
+				if (code < 0x14)
+					return false;
+				if (code > 0x14)
+					return code == 0x1A;
+			}
+			return true;
+		}
+
+		return code > 3 && (code < 5 || code == 6);
+	}
+
+	if (code > 0x7F) {
+		if (code < 0xC7) {
+			if (code < 0xA2) {
+				if (code < 0x95) {
+					if (code < 0x83)
+						return false;
+					if (code > 0x83)
+						return code == 0x90;
+				} else if (code > 0x95) {
+					if (code < 0x9E)
+						return false;
+					if (code > 0x9E)
+						return code == 0xA0;
+				}
+			} else if (code > 0xA2) {
+				if (code < 0xB7) {
+					if (code < 0xA5)
+						return false;
+					if (code > 0xA5)
+						return code == 0xAD;
+				} else if (code > 0xB7) {
+					if (code < 0xBB)
+						return code == 0xB9;
+					if (code < 0xBC)
+						return true;
+					return code == 0xBE;
+				}
+			}
+			return true;
+		}
+
+		if (code > 0xC7) {
+			if (code < 0xE6) {
+				if (code < 0xD7) {
+					if (code < 0xC9)
+						return false;
+					if (code > 0xC9)
+						return code == 0xCF;
+				} else if (code > 0xD9) {
+					if (code < 0xDD)
+						return code == 0xDB;
+					if (code < 0xDE)
+						return true;
+					return code == 0xDF;
+				}
+			} else if (code > 0xE6) {
+				if (code < 0xF5) {
+					if (code < 0xF0)
+						return false;
+					if (code > 0xF0)
+						return code == 0xF3;
+				} else if (code > 0xF5) {
+					if (code < 0xFF)
+						return code == 0xFD;
+					if (code < 0x100)
+						return true;
+					return code == 0x103;
+				}
+			}
+		}
+	}
+
+	return true;
+}
+
+static inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+	switch (frame) {
+	case 0x006A:
+	case 0x00FD:
+	case 0x011C:
+	case 0x0563:
+		return perspectiveX < 0x18;
+	case 0x0144:
+		return perspectiveX < 0x29 && perspectiveY < 0x20;
+	case 0x016F:
+	case 0x0222:
+	case 0x02EB:
+		return perspectiveX < 0x29;
+	case 0x01DE:
+	case 0x0492:
+		return perspectiveX < 8;
+	case 0x024A:
+		return perspectiveY < 0x27;
+	case 0x0318:
+		return perspectiveY < 0x0F;
+	case 0x0397:
+		return perspectiveX < 0x18 && perspectiveY < 0x20;
+	case 0x0405:
+		return perspectiveX > 0x28;
+	case 0x0462:
+		return perspectiveY < 0x20;
+	case 0x04F1:
+		return perspectiveX < 0x29 && perspectiveY >= 0x0F;
+	case 0x04FF:
+		return perspectiveX < 0x29 && perspectiveY < 0x20;
+	case 0x0617:
+		return !(perspectiveX > 7 && perspectiveX < 0x29 &&
+			perspectiveY > 0x0E && perspectiveY < 0x20);
+	default:
+		return false;
+	}
+}
+
+static inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+	switch (frame) {
+	case 0x00CD:
+		return perspectiveX < 0x29;
+	case 0x00EF:
+		return perspectiveY < 0x0F;
+	case 0x0294:
+	case 0x04BE:
+	case 0x076C:
+		return perspectiveX < 0x29 && perspectiveY < 0x20;
+	case 0x03A2:
+		return perspectiveX < 0x18;
+	case 0x05C9:
+	case 0x085A:
+	case 0x096F:
+		return perspectiveY < 0x20;
+	default:
+		return false;
+	}
+}
+
+static inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+	switch (frame) {
+	case 0x0189:
+		return perspectiveY < 0x0F;
+	case 0x0297:
+		return perspectiveX < 0x18;
+	case 0x03B3:
+	case 0x0661:
+		return perspectiveX < 0x29 && perspectiveY < 0x20;
+	case 0x04BE:
+	case 0x074F:
+	case 0x0864:
+		return perspectiveY < 0x20;
+	default:
+		return false;
+	}
+}
+
+static inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+	switch (frame) {
+	case 0x00BB:
+		return perspectiveX < 0x29 && perspectiveY < 0x20;
+	case 0x01A9:
+	case 0x02BE:
+		return perspectiveY < 0x20;
+	default:
+		return false;
+	}
+}
+
 void InsaneRebel1::resetFrameObjectState() {
 	memset(_frameObjectState, 0, sizeof(_frameObjectState));
 	for (int i = 0x50; i < 0x96; i++)
@@ -235,20 +488,31 @@ void InsaneRebel1::checkDynamicLevelBranch() {
 		const int frame = (int)_frameCounter;
 		const int leftBlockedFrame = kLevel8BranchFrames[route][2];
 		const int rightBlockedFrame = kLevel8BranchFrames[route][1];
+		const bool shotEdge = _playerFired && _fireCooldown == 0;
 		int nextRoute = -1;
 
 		for (int i = 0; i < 3; ++i) {
 			const int triggerFrame = kLevel8BranchFrames[route][i];
-			if (triggerFrame >= 0 && frame == triggerFrame) {
-				if (_shipPosX < kRA1CenterX) {
-					if (frame != leftBlockedFrame)
-						nextRoute = 1;
-				} else {
-					if (frame != rightBlockedFrame)
-						nextRoute = 2;
-				}
-				break;
+			if (triggerFrame < 0)
+				continue;
+
+			if (shotEdge && frame > triggerFrame - 0x32 && frame <= triggerFrame)
+				_levelRouteChoice = (_shipPosX < kRA1CenterX) ? 1 : 2;
+
+			if (frame != triggerFrame)
+				continue;
+
+			const bool chooseLeft = (_levelRouteChoice == 1) ||
+				((_shipPosX < kRA1CenterX) && (_levelRouteChoice != 2));
+			if (chooseLeft) {
+				if (frame != leftBlockedFrame)
+					nextRoute = 1;
+			} else {
+				if (frame != rightBlockedFrame)
+					nextRoute = 2;
 			}
+			_levelRouteChoice = 0;
+			break;
 		}
 
 		if (nextRoute >= 0 && nextRoute != route) {
@@ -757,11 +1021,38 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
-	if (_gameLatch5D == 0xFFFF)
+	if (_gameLatch5D == 0xFFFF ||
+		(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
+		(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
+		(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
-	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+	if (_gameLatch5F != 0 &&
+		((_currentLevel == 3 || _currentLevel == 9)
+			? (_vm->_rnd.getRandomNumber(2) == 0)
+			: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
 		_damageFlags |= 0x80;
 
+	if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
+		_damageFlags |= 0x20;
+
+	if (_currentLevel == 7) {
+		bool walkerHazard = false;
+		switch (CLIP<int>(_levelRouteIndex, 0, 2)) {
+		case 0:
+			walkerHazard = hasLevel8PerspectiveHazardRoute0((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+			break;
+		case 1:
+			walkerHazard = hasLevel8PerspectiveHazardRoute1((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+			break;
+		case 2:
+			walkerHazard = hasLevel8PerspectiveHazardRoute2((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+			break;
+		}
+
+		if (walkerHazard)
+			_damageFlags |= 0x20;
+	}
+
 	// Health regeneration (FUN_1BB0E): +1 every 32 frames when alive
 	if (_health >= 0 && _health < kMaxHealth && (_frameCounter & 0x1F) == 0) {
 		_health++;
@@ -849,6 +1140,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	resetProjectionTable();
 
 	_frameCounter++;
+
+	if (_currentLevel == 3 && _levelGameplayPhase == 2 && _frameCounter == 0x3E)
+		_gameplayFlags75ff |= 2;
+
 	checkDynamicLevelBranch();
 
 	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 1b7eb1e788d..5264bf83624 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -300,7 +300,9 @@ bool InsaneRebel1::runLevel3() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -357,8 +359,11 @@ bool InsaneRebel1::runLevel4() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
+		_levelGameplayPhase = 0;
 
+		_levelGameplayPhase = 1;
 		playInteractiveVideo("LVL4/L4PLAY1.ANM");
 		if (_vm->shouldQuit())
 			return false;
@@ -367,7 +372,9 @@ bool InsaneRebel1::runLevel4() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
+			_gameplayFlags75ff = 0;
 			_killCount = 0;
+			_levelGameplayPhase = 2;
 			playInteractiveVideo("LVL4/L4PLAY2.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -418,7 +425,9 @@ bool InsaneRebel1::runLevel5() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -513,7 +522,9 @@ bool InsaneRebel1::runLevel6() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -578,6 +589,7 @@ bool InsaneRebel1::runLevel7() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -591,6 +603,7 @@ bool InsaneRebel1::runLevel7() {
 		_posAccumY = 0;
 		_perspectiveX = 0;
 		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -668,6 +681,7 @@ bool InsaneRebel1::runLevel8() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -681,6 +695,7 @@ bool InsaneRebel1::runLevel8() {
 		_posAccumY = 0;
 		_perspectiveX = 0;
 		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -756,6 +771,7 @@ bool InsaneRebel1::runLevel9() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -769,6 +785,7 @@ bool InsaneRebel1::runLevel9() {
 		_posAccumY = 0;
 		_perspectiveX = 0;
 		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -931,6 +948,7 @@ bool InsaneRebel1::runLevel10() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -944,6 +962,7 @@ bool InsaneRebel1::runLevel10() {
 		_posAccumY = 0;
 		_perspectiveX = 0;
 		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -1046,6 +1065,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	_player = splayer;
 	clearBit(0);
 	_interactiveVideoActive = true;
+	_levelRouteChoice = 0;
 	resetFrameObjectState();
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);


Commit: 53bc68ed7be6d27e5f2f083ae821847fe3a5df82
    https://github.com/scummvm/scummvm/commit/53bc68ed7be6d27e5f2f083ae821847fe3a5df82
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:39+02:00

Commit Message:
SCUMM: RA1: Add missing sound effects

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_audio.cpp
    engines/scumm/insane/insane_rebel1_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d6d1e3ee582..2f4a7462949 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -254,6 +254,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 	// Audio
 	initAudio(11025);
+	memset(_sfxData, 0, sizeof(_sfxData));
+	memset(_sfxSize, 0, sizeof(_sfxSize));
+	loadSfx();
 
 	// Null out Insane base class pointers that the default constructor doesn't initialize
 	_smush_roadrashRip = nullptr;
@@ -276,6 +279,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 InsaneRebel1::~InsaneRebel1() {
 	_vm->_system->getEventManager()->getEventDispatcher()->unregisterObserver(this);
 	terminateAudio();
+	freeSfx();
 }
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index fa6456355ad..eeb6f6bb4bf 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -160,6 +160,9 @@ private:
 	// Audio
 	void initAudio(int sampleRate);
 	void terminateAudio();
+	void loadSfx();
+	void freeSfx();
+	void playSfx(int slot, int volume, int pan);
 	void queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan);
 public:
 	void drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
@@ -296,6 +299,20 @@ private:
 	Audio::SoundHandle _audioHandles[kMaxAudioTracks];
 	bool _audioTrackActive[kMaxAudioTracks];
 	int _audioSampleRate;
+	static const int kNumSfx = 8;
+	enum SfxSlot {
+		kSfxLaserShot = 0,
+		kSfxExplode = 1,
+		kSfxBoom = 2,
+		kSfxKlaxon = 3,
+		kSfxLockOn = 4,
+		kSfxAlert = 5,
+		kSfxBonus = 6,
+		kSfxBlast = 7
+	};
+	byte *_sfxData[kNumSfx];
+	uint32 _sfxSize[kNumSfx];
+	Audio::SoundHandle _sfxHandles[kNumSfx];
 
 	// True only while an interactive gameplay SMUSH is running.
 	bool _interactiveVideoActive;
diff --git a/engines/scumm/insane/insane_rebel1_audio.cpp b/engines/scumm/insane/insane_rebel1_audio.cpp
index 7ca9e73d7b5..5801d0dce6f 100644
--- a/engines/scumm/insane/insane_rebel1_audio.cpp
+++ b/engines/scumm/insane/insane_rebel1_audio.cpp
@@ -21,6 +21,8 @@
 
 #include "common/system.h"
 
+#include "scumm/file.h"
+
 #include "audio/audiostream.h"
 #include "audio/decoders/raw.h"
 #include "audio/mixer.h"
@@ -30,6 +32,17 @@
 
 namespace Scumm {
 
+static const char *const kRA1SfxFiles[8] = {
+	"SYS/LASRSHOT.SAD",
+	"SYS/EXPLODE.SAD",
+	"SYS/BOOM.SAD",
+	"SYS/KLAXON.SAD",
+	"SYS/LOCKON.SAD",
+	"SYS/ALERT.SAD",
+	"SYS/BONUS.SAD",
+	"SYS/BLAST.SAD"
+};
+
 // ---------------------------------------------------------------------------
 // Audio
 // ---------------------------------------------------------------------------
@@ -53,6 +66,10 @@ void InsaneRebel1::terminateAudio() {
 			_audioStreams[i] = nullptr;
 		}
 	}
+
+	for (int i = 0; i < kNumSfx; i++) {
+		_vm->_mixer->stopHandle(_sfxHandles[i]);
+	}
 }
 
 void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
@@ -202,4 +219,97 @@ void InsaneRebel1::processAudioFrame(int16 feedSize) {
 	}
 }
 
+void InsaneRebel1::loadSfx() {
+	for (int i = 0; i < kNumSfx; i++) {
+		if (_sfxData[i] || _sfxSize[i] != 0)
+			continue;
+
+		ScummFile *file = _vm->instantiateScummFile();
+		_vm->openFile(*file, kRA1SfxFiles[i]);
+		if (!file->isOpen()) {
+			debug("InsaneRebel1::loadSfx: could not open %s", kRA1SfxFiles[i]);
+			delete file;
+			continue;
+		}
+
+		const uint32 fileSize = file->size();
+		if (fileSize < 16) {
+			debug("InsaneRebel1::loadSfx: %s too small (%u bytes)", kRA1SfxFiles[i], fileSize);
+			file->close();
+			delete file;
+			continue;
+		}
+
+		const uint32 tag = file->readUint32BE();
+		if (tag != MKTAG('S', 'A', 'U', 'D')) {
+			debug("InsaneRebel1::loadSfx: %s not a SAUD file (tag=0x%08x)", kRA1SfxFiles[i], tag);
+			file->close();
+			delete file;
+			continue;
+		}
+		file->readUint32BE();
+
+		bool foundSdat = false;
+		while (file->pos() + 8 <= (int64)fileSize) {
+			const uint32 chunkTag = file->readUint32BE();
+			const uint32 chunkSize = file->readUint32BE();
+			if (chunkTag == MKTAG('S', 'D', 'A', 'T')) {
+				const uint32 pcmSize = MIN(chunkSize, fileSize - (uint32)file->pos());
+				byte *pcm = (byte *)malloc(pcmSize);
+				if (pcm) {
+					file->read(pcm, pcmSize);
+					_sfxData[i] = pcm;
+					_sfxSize[i] = pcmSize;
+					debug("InsaneRebel1::loadSfx: loaded %s (%u bytes PCM)", kRA1SfxFiles[i], pcmSize);
+				}
+				foundSdat = true;
+				break;
+			}
+			file->seek(chunkSize, SEEK_CUR);
+		}
+
+		if (!foundSdat)
+			debug("InsaneRebel1::loadSfx: no SDAT chunk in %s", kRA1SfxFiles[i]);
+
+		file->close();
+		delete file;
+	}
+}
+
+void InsaneRebel1::freeSfx() {
+	for (int i = 0; i < kNumSfx; i++) {
+		_vm->_mixer->stopHandle(_sfxHandles[i]);
+		free(_sfxData[i]);
+		_sfxData[i] = nullptr;
+		_sfxSize[i] = 0;
+	}
+}
+
+void InsaneRebel1::playSfx(int slot, int volume, int pan) {
+	if (slot < 0 || slot >= kNumSfx || !_sfxData[slot] || _sfxSize[slot] == 0)
+		return;
+	if (_player && !_player->isChanActive(CHN_OTHER))
+		return;
+
+	_vm->_mixer->stopHandle(_sfxHandles[slot]);
+
+	byte *pcmCopy = (byte *)malloc(_sfxSize[slot]);
+	if (!pcmCopy)
+		return;
+	memcpy(pcmCopy, _sfxData[slot], _sfxSize[slot]);
+
+	Audio::SeekableAudioStream *stream = Audio::makeRawStream(
+		pcmCopy, _sfxSize[slot], 11025, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+	int mixVolume = CLIP(volume, 0, 127);
+	if (_player) {
+		const int baseVolume = (_player->_smushTrackVols[1] * mixVolume) >> 7;
+		mixVolume = (baseVolume * _player->_smushTrackVols[0]) / 127;
+	}
+	const int scaledVolume = (mixVolume * Audio::Mixer::kMaxChannelVolume) / 127;
+	const int clampedPan = CLIP(pan, -127, 127);
+
+	_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_sfxHandles[slot],
+		stream, -1, scaledVolume, clampedPan);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index c01f5175bbd..611413d556a 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -1416,6 +1416,7 @@ void InsaneRebel1::processShot() {
 	_shotSlots[slot].centerY = shipCenterY;
 	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
+	playSfx(kSfxLaserShot, 127, 0);
 
 	debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
 }
@@ -1474,6 +1475,11 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						_score += _tuning.kill;
 						_killCount++;
 						applyFrameObjectHitState(targetIdx);
+						int16 hitCenterX = (left + right) / 2;
+						int16 hitCenterY = (top + bottom) / 2;
+						projectGameplayPoint(hitCenterX, hitCenterY);
+						const int sfxPan = CLIP((hitCenterX - kRA1CenterX) * 127 / kRA1CenterX, -127, 127);
+						playSfx(kSfxExplode, 127, sfxPan);
 
 						// Match FUN_1C0EF: snap in unprojected space, then project back
 						// into the current gameplay window before rendering the pointer.


Commit: 26edb60399be48831890751ef01326bd5ba918ad
    https://github.com/scummvm/scummvm/commit/26edb60399be48831890751ef01326bd5ba918ad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00

Commit Message:
SCUMM: RA1: Use named parameters

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 2f4a7462949..b0bf2382788 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -36,89 +36,161 @@
 
 namespace Scumm {
 
-// Per-difficulty tuning tables from assault_data_3.bin
-// Indexed: difficulty * 0x28B + level * 0x1F + offset
-// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill
-static const int16 kTuningTable[10][3][9] = {
-	// Level 1 (Flight Training)
+// Per-difficulty tuning tables from assault_data_3.bin (also loadable from C:\rebltune.txt)
+// 21 sub-levels x 3 difficulties x 13 fields
+// Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
+static const int16 kTuningTable[21][3][13] = {
+	// Sub-level 0: "1A" (Flight Training - canyon flight)
 	{
-		{ 100, 100,  60, 110,   0,   0,  15,   0,   0 },  // Easy
-		{ 100, 105,  60, 115,   0,   0,  25,   0,   0 },  // Normal
-		{ 105, 110,  65, 120,   0,   0,  30,   0,   0 },  // Hard
+		{ 100, 100,  60, 110,   0,   0,  15,   0,   0,   5,  500,  100, 2048 },  // Easy
+		{ 100, 105,  60, 115,   0,   0,  25,   0,   0,   5, 1000,  200, 2048 },  // Normal
+		{ 105, 110,  65, 120,   0,   0,  30,   0,   0,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Level 2 (Asteroid Field Training)
+	// Sub-level 1: "1B" (Flight Training - asteroid flight)
 	{
-		{ 100,  16, 120,   0,   7,   0,  15,   0,  25 },  // Easy
-		{ 100,  18, 120,   0,   5,   0,  20,   0,  50 },  // Normal
-		{ 100,  20, 150,   0,   1,   0,  25,   0,  75 },  // Hard
+		{ 100,  16, 120,   0,   7,   0,  15,   0,  25,   5,  500,  100, 3072 },  // Easy
+		{ 100,  18, 120,   0,   5,   0,  20,   0,  50,   5, 1000,  200, 3072 },  // Normal
+		{ 100,  20, 150,   0,   1,   0,  25,   0,  75,  10, 1500,  500, 3074 },  // Hard
 	},
-	// Level 3 (Planet Kolaador)
+	// Sub-level 2: "2" (Planet Kolaador)
 	{
-		{   0,   0,   0,   0,   4,  15,  25,   0,  25 },  // Easy
-		{   0,   0,   0,   0,   2,  18,  30,   0,  50 },  // Normal
-		{   0,   0,   0,   0,   0,  20,  35,   0,  75 },  // Hard
+		{   0,   0,   0,   0,   4,  15,  25,   0,  25,  10,  500,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   2,  18,  30,   0,  50,  10, 1000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   0,  20,  35,   0,  75,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Level 4 (Star Destroyer Attack)
+	// Sub-level 3: "3" (Star Destroyer Attack)
 	{
-		{  70, 100, 150,  90,   0,   0,  20,   0,   0 },  // Easy
-		{  72, 105, 155, 105,   0,   0,  25,   0,   0 },  // Normal
-		{  75, 110, 160, 110,   0,   0,  28,   0,   0 },  // Hard
+		{  70, 100, 150,  90,   0,   0,  20,   0,   0,   5, 1000,  100, 2048 },  // Easy
+		{  72, 105, 155, 105,   0,   0,  25,   0,   0,   5, 2000,  200, 2048 },  // Normal
+		{  75, 110, 160, 110,   0,   0,  28,   0,   0,  10, 3000,  500, 2050 },  // Hard
 	},
-	// Level 5 (Tatooine Attack)
+	// Sub-level 4: "4A" (Tatooine Attack)
 	{
-		{   0,   0,   0,   0,   2,  11,   0,   4,  25 },  // Easy
-		{   0,   0,   0,   0,   1,  25,   0,   6,  50 },  // Normal
-		{   0,   0,   0,   0,   1,  28,   0,   6,  75 },  // Hard
+		{   0,   0,   0,   0,   2,  11,   0,   4,  25,   5,  500,  750, 2048 },  // Easy
+		{   0,   0,   0,   0,   1,  25,   0,   6,  50,   5, 1000, 1500, 2048 },  // Normal
+		{   0,   0,   0,   0,   1,  28,   0,   6,  75,  10, 1500, 2000, 2050 },  // Hard
 	},
-	// Level 6 (Asteroid Field Chase)
+	// Sub-level 5: "4B" (Tatooine Attack part 2)
 	{
-		{   0,   0,   0,   0,   3,  20,   0,   2,  50 },  // Easy
-		{   0,   0,   0,   0,   1,  25,   0,   5, 100 },  // Normal
-		{   0,   0,   0,   0,   1,  28,   0,   6, 200 },  // Hard
+		{   0,   0,   0,   0,   3,  20,   0,   2,  50,   5,  500,  750, 2064 },  // Easy
+		{   0,   0,   0,   0,   1,  25,   0,   5, 100,   5, 1000, 1500, 2064 },  // Normal
+		{   0,   0,   0,   0,   1,  28,   0,   6, 200,  10, 1500, 2000, 2064 },  // Hard
 	},
-	// Level 7 (Imperial Probe Droids)
+	// Sub-level 6: "5A" (Imperial Probe Droids - speeder)
 	{
-		{  70, 150,  50,  25,  10,   0,  20,   0,  25 },  // Easy
-		{  72, 165, 155,  30,   8,   0,  30,   0,  50 },  // Normal
-		{ 110, 190,  55,  65,   3,   0,  33,   0,  75 },  // Hard
+		{  70, 150,  50,  25,  10,   0,  20,   0,  25,   5,  500,   15, 3072 },  // Easy
+		{  72, 165, 155,  30,   8,   0,  30,   0,  50,   5, 1000,   30, 3072 },  // Normal
+		{ 110, 190,  55,  65,   3,   0,  33,   0,  75,  10, 1500,   75, 3074 },  // Hard
 	},
-	// Level 8 (Imperial Walkers)
+	// Sub-level 7: "5B" (Imperial Walkers)
 	{
-		{   0,   0,   0,   0,   5,   0,   0,   2,  25 },  // Easy
-		{   0,   0,   0,   0,   3,   0,   0,   5,  50 },  // Normal
-		{   0,   0,   0,   0,   1,   0,   0,   6,  75 },  // Hard
+		{   0,   0,   0,   0,   5,   0,   0,   2,  25,   0,  500,   15, 2048 },  // Easy
+		{   0,   0,   0,   0,   3,   0,   0,   5,  50,   5, 1000,   30, 2048 },  // Normal
+		{   0,   0,   0,   0,   1,   0,   0,   6,  75,  10, 1500,   75, 2050 },  // Hard
 	},
-	// Level 9 (Stormtroopers)
+	// Sub-level 8: "6" (Stormtroopers)
 	{
-		{   0,   0,   0,   0,   2,  20,  20,   0,  25 },  // Easy
-		{   0,   0,   0,   0,   1,  25,  30,   0,  50 },  // Normal
-		{   0,   0,   0,   0,   0,  28,  33,   0,  75 },  // Hard
+		{   0,   0,   0,   0,   2,  20,  20,   0,  25,   5,  500,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   1,  25,  30,   0,  50,   5, 1000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   0,  28,  33,   0,  75,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Level 10 (Protect Rebel Transport)
+	// Sub-level 9: "7" (Protect Rebel Transport)
 	{
-		{ 100, 150, 150,  25,   7,   0,  12,   2,  50 },  // Easy
-		{ 100, 160, 200,  35,   4,   0,  30,   4, 100 },  // Normal
-		{ 100, 180, 250,  50,   3,   0,  33,   5, 100 },  // Hard
+		{ 100, 150, 150,  25,   7,   0,  12,   2,  50,   5,  500,  100, 3072 },  // Easy
+		{ 100, 160, 200,  35,   4,   0,  30,   4, 100,   5, 1000,  200, 3072 },  // Normal
+		{ 100, 180, 250,  50,   3,   0,  33,   5, 100,  10, 1500,  500, 3074 },  // Hard
+	},
+	// Sub-level 10: "8" (Death Star surface)
+	{
+		{   0,   0,   0,   0,   0,   0,  30,   0,  25,   0, 1000,  100, 3074 },  // Easy
+		{   0,   0,   0,   0,   0,   0,  36,   0,  50,   0, 2000,  200, 3074 },  // Normal
+		{   0,   0,   0,   0,   0,   0,  39,   0,  75,   0, 3000,  500, 3074 },  // Hard
+	},
+	// Sub-level 11: "9A" (Death Star turrets part 1)
+	{
+		{   0,   0,   0,   0,   4,   0,   0,  15,  25,   0, 1000,  100, 3074 },  // Easy
+		{   0,   0,   0,   0,   2,   0,   0,  25,  50,   0, 2000,  200, 3078 },  // Normal
+		{   0,   0,   0,   0,   0,   0,   0,  30,  75,   0, 3000,  500, 3078 },  // Hard
+	},
+	// Sub-level 12: "9B" (Death Star turrets part 2)
+	{
+		{   0,   0,   0,   0,   0,   0,   0,  15,  25,   0, 1000,  100, 3098 },  // Easy
+		{   0,   0,   0,   0,   0,   0,   0,  25,  50,   0, 2000,  200, 3098 },  // Normal
+		{   0,   0,   0,   0,   0,   0,   0,  30,  75,   0, 3000,  500, 3098 },  // Hard
+	},
+	// Sub-level 13: "10" (Death Star trench approach)
+	{
+		{   0,   0,   0,   0,   3,  10,   0,   5,  25,   5,  500,  200, 2048 },  // Easy
+		{   0,   0,   0,   0,   1,  16,   0,   5,  50,   5, 1000,  400, 2048 },  // Normal
+		{   0,   0,   0,   0,   0,  18,   0,   7,  75,  10, 1500, 1000, 2050 },  // Hard
+	},
+	// Sub-level 14: "11" (Death Star trench - speeder)
+	{
+		{  70, 150, 150,  25,  12,   0,  30,   0,  50,   5,  500,  200, 3072 },  // Easy
+		{  72, 165, 155,  30,   7,   0,  36,   0,  50,   5, 1000,  400, 3072 },  // Normal
+		{  75, 170, 160,  33,   3,   0,  39,   0,  75,  10, 1500, 1000, 3074 },  // Hard
+	},
+	// Sub-level 15: "12" (Death Star trench run)
+	{
+		{   0,   0,   0,   0,   4,  13,   0,   5,  25,   5,  500,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   2,  20,   0,   5,  50,   5, 1000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   0,  23,   0,   5,  75,  10, 1500,  500, 2050 },  // Hard
+	},
+	// Sub-level 16: "13" (Asteroid belt chase)
+	{
+		{ 100,  16, 120,   0,  20,   0,  35,   8,  75,   5,  500,  100, 3072 },  // Easy
+		{ 100,  18, 120,   0,  18,   0,  36,  10, 100,   5, 1000,  200, 3072 },  // Normal
+		{ 100,  20, 150,   0,  15,   0,  39,  12, 200,  10, 1500,  500, 3074 },  // Hard
+	},
+	// Sub-level 17: "14A" (Star Destroyer attack 2 part 1)
+	{
+		{   0,   0,   0,   0,   0,  20,  35,   8,  25,   0, 1000,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   0,  27,  36,  12,  50,   0, 2000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   0,  28,  39,  12,  75,   0, 3000,  500, 2050 },  // Hard
+	},
+	// Sub-level 18: "14B" (Star Destroyer attack 2 part 2)
+	{
+		{   0,   0,   0,   0,  10,  20,  35,   8,  25,   0, 1000,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   5,  25,  36,  10,  50,   0, 2000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   4,  28,  39,  12,  75,   0, 3000,  500, 2050 },  // Hard
+	},
+	// Sub-level 19: "15A" (Death Star trench final part 1)
+	{
+		{   0,   0,   0,   0,   4,   0,  28,   3,  25,   5,  500,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   3,   0,  36,   3,  50,   5, 1000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   3,   0,  39,   4,  75,  10, 1500,  500, 2050 },  // Hard
+	},
+	// Sub-level 20: "15B" (Death Star trench final part 2)
+	{
+		{   0,   0,   0,   0,   4,  10,  30,   3,  25,   5,  500,  100, 2048 },  // Easy
+		{   0,   0,   0,   0,   3,  20,  34,   3,  50,   5, 1000,  200, 2048 },  // Normal
+		{   0,   0,   0,   0,   2,  22,  35,   4,  75,  10, 1500,  500, 2050 },  // Hard
 	},
 };
-static const int kNumTunedLevels = 10;
+static const int kNumTunedLevels = 21;
 
 
 void InsaneRebel1::loadTuningForLevel(int level) {
 	int d = CLIP(_difficulty, 0, 2);
 	int l = CLIP(level, 0, kNumTunedLevels - 1);
-	_tuning.roll  = kTuningTable[l][d][0];
-	_tuning.lift  = kTuningTable[l][d][1];
-	_tuning.slide = kTuningTable[l][d][2];
-	_tuning.drift = kTuningTable[l][d][3];
-	_tuning.snap  = kTuningTable[l][d][4];
-	_tuning.miss  = kTuningTable[l][d][5];
-	_tuning.wham  = kTuningTable[l][d][6];
-	_tuning.shot  = kTuningTable[l][d][7];
-	_tuning.kill  = kTuningTable[l][d][8];
-	debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d snap=%d miss=%d wham=%d shot=%d kill=%d",
-		level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.snap, _tuning.miss,
-		_tuning.wham, _tuning.shot, _tuning.kill);
+	_tuning.roll     = kTuningTable[l][d][0];
+	_tuning.lift     = kTuningTable[l][d][1];
+	_tuning.slide    = kTuningTable[l][d][2];
+	_tuning.drift    = kTuningTable[l][d][3];
+	_tuning.snap     = kTuningTable[l][d][4];
+	_tuning.miss     = kTuningTable[l][d][5];
+	_tuning.wham     = kTuningTable[l][d][6];
+	_tuning.shot     = kTuningTable[l][d][7];
+	_tuning.kill     = kTuningTable[l][d][8];
+	_tuning.time     = kTuningTable[l][d][9];
+	_tuning.levelPts = kTuningTable[l][d][10];
+	_tuning.bonus    = kTuningTable[l][d][11];
+	_tuning.flags    = kTuningTable[l][d][12];
+	debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d drift=%d snap=%d "
+		"miss=%d wham=%d shot=%d kill=%d time=%d levelPts=%d bonus=%d flags=0x%x",
+		level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.snap,
+		_tuning.miss, _tuning.wham, _tuning.shot, _tuning.kill,
+		_tuning.time, _tuning.levelPts, _tuning.bonus, _tuning.flags);
 }
 
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index eeb6f6bb4bf..9b36288eb1d 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -260,16 +260,22 @@ private:
 	int _difficulty;
 
 	// Per-difficulty tuning (from assault_data_3.bin, indexed: difficulty * 0x28B + level * 0x1F)
+	// Original game loads from C:\rebltune.txt; ScummVM uses hardcoded table.
+	// 21 sub-levels (1A,1B,2,3,4A,4B,5A,5B,6,7,8,9A,9B,10,11,12,13,14A,14B,15A,15B)
 	struct TuningParams {
-		int16 roll;    // 0x1B1B: horizontal speed/sensitivity
-		int16 lift;    // 0x1B1D: vertical speed/sensitivity
-		int16 slide;   // 0x1B1F: cross-axis coupling
-		int16 drift;   // 0x1B21: drift/turbulence multiplier
-		int16 snap;    // 0x1B23: hit radius for shooting targets
-		int16 miss;    // 0x1B25: obstacle collision damage (0x0B bit 0x40)
-		int16 wham;    // 0x1B27: light/wall damage
-		int16 shot;    // 0x1B29: heavy/projectile damage
-		int16 kill;    // 0x1B2B: score per target kill
+		int16 roll;      // +0x05: horizontal speed/sensitivity
+		int16 lift;      // +0x07: vertical speed/sensitivity
+		int16 slide;     // +0x09: cross-axis coupling
+		int16 drift;     // +0x0B: drift/turbulence multiplier
+		int16 snap;      // +0x0D: hit radius for shooting targets
+		int16 miss;      // +0x0F: obstacle collision damage (0x0B bit 0x40)
+		int16 wham;      // +0x11: light/wall damage
+		int16 shot;      // +0x13: heavy/projectile damage
+		int16 kill;      // +0x15: score per target kill
+		int16 time;      // +0x17: survival bonus (added every 32 frames)
+		int16 levelPts;  // +0x19: chapter completion bonus (RunChapterCompleteSummaryScreen)
+		int16 bonus;     // +0x1B: per-level bonus multiplier for kills/accuracy
+		int16 flags;     // +0x1D: level behavior flags (loaded into DAT_75FE at level init)
 	};
 	TuningParams _tuning;
 
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 611413d556a..9fd9b39f2b1 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -830,12 +830,12 @@ void InsaneRebel1::updateShipPhysics() {
 	if (_health < 0 && _deathTimer > 0)
 		_deathTimer--;
 
-	// Health regeneration: +1 every 32 frames (from original asm)
+	// Health regeneration + survival bonus every 32 frames (UpdatePeriodicScoreAndHealth)
 	if ((_frameCounter & 0x1F) == 0) {
 		if (_health >= 0 && _health < kMaxHealth)
 			_health++;
 		if (_health >= 0)
-			_score += 1;
+			_score += _tuning.time;
 	}
 
 	// Screen flash decay
@@ -993,12 +993,12 @@ void InsaneRebel1::updateTurretPhysics() {
 	if (shipBank->numSprites > 0)
 		_shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
 
-	// Regeneration via FUN_1BB0E call in this path.
+	// Regeneration + survival bonus via FUN_1BB0E call in this path.
 	if ((_frameCounter & 0x1F) == 0) {
 		if (_health >= 0 && _health < kMaxHealth)
 			_health++;
 		if (_health >= 0)
-			_score += 1;
+			_score += _tuning.time;
 	}
 
 	if (_screenFlash > 0)


Commit: 0d84e60b0b78543e3dd1191fe49b20461e1e6fae
    https://github.com/scummvm/scummvm/commit/0d84e60b0b78543e3dd1191fe49b20461e1e6fae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00

Commit Message:
SCUMM: RA1: Add chapter names in intro

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index b0bf2382788..d8b3d53264b 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -266,6 +266,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		_levelRouteChoice = 0;
 		_levelGameplayPhase = 0;
 		_menuActive = false;
+	_introTextActive = false;
+	_introTextStartFrame = 0;
+	_introTextEndFrame = 0;
+	_introTextLevel = 0;
 	_menuConfirmed = false;
 	_menuSelection = 0;
 	_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 9b36288eb1d..bf06bbf2ef2 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -243,6 +243,14 @@ private:
 	// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
 	int _currentLevel;
 
+	// Intro title overlay (RunTwoLineTextSplash from original)
+	bool _introTextActive;
+	int32 _introTextStartFrame;  // revealBaseY: frame at which text begins appearing
+	int32 _introTextEndFrame;    // stopY: frame after which text stops
+	int _introTextLevel;         // index into kLevelTitles
+	void beginLevelTitleOverlay(int level);
+	void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
+
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
 	// Mode-2 emitter offsets used by FUN_1D79C when _DAT_75E4 == 2.
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 6d669ca12e6..ffb050cd18a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -350,6 +350,15 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		renderMainMenuOverlay(renderBitmap, pitch, width, height);
 	}
 
+	// Intro title overlay (RunTwoLineTextSplash) — drawn during intro cinematics
+	if (_introTextActive && renderBitmap) {
+		int w = _player ? _player->_width : _screenWidth;
+		int h = _player ? _player->_height : _screenHeight;
+		if (w == 0) w = _screenWidth;
+		if (h == 0) h = _screenHeight;
+		drawLevelTitleOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
+	}
+
 	if (!_interactiveVideoActive || !renderBitmap)
 		return;
 
@@ -665,6 +674,73 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	}
 }
 
+// Level intro title data (from RunTwoLineTextSplash calls in original binary).
+// titleText is drawn at y=10, subtitleText at y=25 (matching original DrawUiString positions).
+// revealStartFrame/revealEndFrame control the frame range during which text is visible.
+static const struct {
+	const char *titleText;      // Top line (chapter number)
+	const char *subtitleText;   // Bottom line (level name)
+	int16 revealStartFrame;     // Frame at which text begins appearing
+	int16 revealEndFrame;       // Frame after which text stops
+} kLevelTitles[] = {
+	{ "Chapter 1",  "Flight Training",        -80, -30 },  // Original computes totalFrames-0x50..totalFrames-0x1e
+	{ "Chapter 2",  "Asteroid Field Training",  40, 100 },
+	{ "Chapter 3",  "Planet Kolaador",           0,  50 },
+	{ "Chapter 4",  "Star Destroyer Attack",     5,  50 },
+	{ "Chapter 5",  "Tatooine Attack",         346, 390 },
+	{ "Chapter 6",  "Asteroid Field Chase",     30,  80 },
+	{ "Chapter 7",  "Imperial Probe Droids",   166, 230 },
+	{ "Chapter 8",  "Imperial Walkers",         30,  75 },
+	{ "Chapter 9",  "Stormtroopers",             2,  45 },
+	{ "Chapter 10", "Protect Rebel Transport",  10,  75 },
+	{ "Chapter 11", "Yavin Training",            1,  45 },
+	{ "Chapter 12", "Tie Attack",               15,  70 },
+	{ "Chapter 13", "Death Star Surface",       10,  60 },
+	{ "Chapter 14", "Surface Cannon",          122, 175 },
+	{ "Chapter 15", "Death Star Trench",         1,  49 },
+};
+
+void InsaneRebel1::beginLevelTitleOverlay(int level) {
+	if (level < 0 || level >= ARRAYSIZE(kLevelTitles)) {
+		_introTextActive = false;
+		return;
+	}
+	_introTextActive = true;
+	_introTextLevel = level;
+	_introTextStartFrame = kLevelTitles[level].revealStartFrame;
+	_introTextEndFrame = kLevelTitles[level].revealEndFrame;
+}
+
+void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame) {
+	if (!_introTextActive || _introTextLevel < 0 || _introTextLevel >= ARRAYSIZE(kLevelTitles))
+		return;
+
+	// Resolve negative frame values (relative to end of video, e.g. Chapter 1)
+	if (_introTextStartFrame < 0)
+		_introTextStartFrame = maxFrame + _introTextStartFrame;
+	if (_introTextEndFrame < 0)
+		_introTextEndFrame = maxFrame + _introTextEndFrame;
+
+	// Only draw within the frame window
+	if (curFrame < _introTextStartFrame || curFrame >= _introTextEndFrame) {
+		if (curFrame >= _introTextEndFrame)
+			_introTextActive = false;
+		return;
+	}
+
+	const char *title = kLevelTitles[_introTextLevel].titleText;
+	const char *subtitle = kLevelTitles[_introTextLevel].subtitleText;
+
+	// Center horizontally (matching original DrawUiString x_center=0xA0=160)
+	int titleW = getFontBankStringWidth(title);
+	int subtitleW = getFontBankStringWidth(subtitle);
+	int centerX = width / 2;
+
+	// Original positions: title at y=10, subtitle at y=25 (0x19)
+	drawFontBankString(dst, pitch, width, height, centerX - titleW / 2, 10, title);
+	drawFontBankString(dst, pitch, width, height, centerX - subtitleW / 2, 25, subtitle);
+}
+
 // drawFontBankString — FUN_221B7 (0x221B7), partial parity:
 // supports '<'/'>' layer markup and layer-2 space handling used by RA1 HUD/targeting strings.
 void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 5264bf83624..420895d56d0 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -110,6 +110,7 @@ bool InsaneRebel1::runLevel1() {
 	loadTuningForLevel(0);
 	loadLevelSprites(1);
 
+	beginLevelTitleOverlay(0);
 	playCinematic("LVL1/L1HANGAR.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -226,6 +227,7 @@ bool InsaneRebel1::runLevel2() {
 	// DOS RunLevel2Flow launches L2PLAY.ANM with gameplay selector 2.
 	loadTuningForLevel(2);
 
+	beginLevelTitleOverlay(1);
 	playCinematic("LVL2/L2INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -283,6 +285,7 @@ bool InsaneRebel1::runLevel3() {
 	// DOS RunLevel3Flow launches L3PLAY.ANM with gameplay selector 3.
 	loadTuningForLevel(3);
 
+	beginLevelTitleOverlay(2);
 	playCinematic("LVL3/L3INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -342,6 +345,7 @@ bool InsaneRebel1::runLevel4() {
 	// DOS RunLevel4Flow launches L4PLAY1/2.ANM with gameplay selector 4.
 	loadTuningForLevel(4);
 
+	beginLevelTitleOverlay(3);
 	playCinematic("LVL4/L4INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -407,6 +411,7 @@ bool InsaneRebel1::runLevel5() {
 	loadLevelSprites(5);
 	loadTuningForLevel(4);
 
+	beginLevelTitleOverlay(4);
 	playCinematic("LVL5/L5INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -505,6 +510,7 @@ bool InsaneRebel1::runLevel6() {
 	loadLevelSprites(6);
 	loadTuningForLevel(5);
 
+	beginLevelTitleOverlay(5);
 	playCinematic("LVL6/L6INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -572,6 +578,7 @@ bool InsaneRebel1::runLevel7() {
 	loadLevelSprites(7);
 	loadTuningForLevel(6);
 
+	beginLevelTitleOverlay(6);
 	playCinematic("LVL7/L7INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -664,6 +671,7 @@ bool InsaneRebel1::runLevel8() {
 	loadLevelSprites(8);
 	loadTuningForLevel(7);
 
+	beginLevelTitleOverlay(7);
 	playCinematic("LVL8/L8INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -754,6 +762,7 @@ bool InsaneRebel1::runLevel9() {
 	loadLevelSprites(9);
 	loadTuningForLevel(8);
 
+	beginLevelTitleOverlay(8);
 	playCinematic("LVL9/L9INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;
@@ -931,6 +940,7 @@ bool InsaneRebel1::runLevel10() {
 	loadLevelSprites(10);
 	loadTuningForLevel(9);
 
+	beginLevelTitleOverlay(9);
 	playCinematic("LVL10/L10INTRO.ANM");
 	if (_vm->shouldQuit())
 		return false;


Commit: cf25b976b06f998d527e3fbc5c29b4e82a99689c
    https://github.com/scummvm/scummvm/commit/cf25b976b06f998d527e3fbc5c29b4e82a99689c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00

Commit Message:
SCUMM: RA1: Improve level selection menu navigation

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index bf06bbf2ef2..8c81e8ed0a3 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -108,7 +108,7 @@ private:
 	// Main menu loop on O1OPTION.ANM background (0x15968)
 	// Returns: 1=Start New Game, 2=Game Options, 3=Level Select, 4=Continue Demo, 5=Exit
 	int runMainMenu();
-	void runLevelSelectMenu();
+	int runLevelSelectMenu();
 
 	// Level 1 flow (0x16100): hangar → CU1 → gameplay → CU2 → turret → end
 	// Returns true if level completed, false if player quit
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 59ca5357f74..2ab05d280dd 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -29,19 +29,34 @@
 
 namespace Scumm {
 
-static const int kRA1LevelSelectItemCount = 11;
-static const int kRA1LastImplementedLevel = 10;
+static const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
+static const int kRA1LevelSelectRowsPerCol = 8;
+static const int kRA1NumLevels = 15;
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
+		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
+		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_w:
-			_levelSelectSel = (_levelSelectSel + kRA1LevelSelectItemCount - 1) % kRA1LevelSelectItemCount;
+			row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
 			return true;
 		case Common::KEYCODE_DOWN:
 		case Common::KEYCODE_s:
-			_levelSelectSel = (_levelSelectSel + 1) % kRA1LevelSelectItemCount;
+			row = (row + 1) % kRA1LevelSelectRowsPerCol;
+			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+			return true;
+		case Common::KEYCODE_LEFT:
+		case Common::KEYCODE_a:
+			if (col > 0)
+				_levelSelectSel -= kRA1LevelSelectRowsPerCol;
+			return true;
+		case Common::KEYCODE_RIGHT:
+		case Common::KEYCODE_d:
+			if (col < 1)
+				_levelSelectSel += kRA1LevelSelectRowsPerCol;
 			return true;
 		case Common::KEYCODE_RETURN:
 		case Common::KEYCODE_KP_ENTER:
@@ -208,36 +223,42 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	}
 
 	if (_levelSelectActive) {
-		// --- Level select submenu ---
+		// --- Level select submenu (two-column layout) ---
 		const int titleW = getTalkTextWidth("LEVEL SELECT");
-		drawTalkText((width - titleW) / 2, 36, "LEVEL SELECT");
-
-		const char *kLevelItems[kRA1LevelSelectItemCount] = {
-			"LEVEL 1: FLIGHT TRAINING",
-			"LEVEL 2: ASTEROID FIELD",
-			"LEVEL 3: PLANET KOLAADOR",
-			"LEVEL 4: STAR DESTROYER",
-			"LEVEL 5: TATOOINE ATTACK",
-			"LEVEL 6: ASTEROID CHASE",
-			"LEVEL 7: PROBE DROIDS",
-			"LEVEL 8: IMPERIAL WALKERS",
-			"LEVEL 9: STORMTROOPERS",
-			"LEVEL 10: REBEL TRANSPORT",
+		drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
+
+		static const char *kLevelItems[kRA1LevelSelectItemCount] = {
+			" 1  FLIGHT TRAINING",
+			" 2  ASTEROID FIELD",
+			" 3  PLANET KOLAADOR",
+			" 4  STAR DESTROYER",
+			" 5  TATOOINE ATTACK",
+			" 6  ASTEROID CHASE",
+			" 7  PROBE DROIDS",
+			" 8  IMPERIAL WALKERS",
+			" 9  STORMTROOPERS",
+			"10  REBEL TRANSPORT",
+			"11  YAVIN TRAINING",
+			"12  TIE ATTACK",
+			"13  DEATH STAR SURFACE",
+			"14  SURFACE CANNON",
+			"15  DEATH STAR TRENCH",
 			"BACK"
 		};
 
-		const int menuY = 60;
-		const int rowH = 16;
+		const int menuY = 50;
+		const int rowH = 14;
+		const int leftColX = 8;
+		const int rightColX = width / 2 + 4;
 
 		for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
+			const int col = i / kRA1LevelSelectRowsPerCol;
+			const int row = i % kRA1LevelSelectRowsPerCol;
+			const int textX = (col == 0) ? leftColX : rightColX;
+			const int y = menuY + row * rowH;
 			const int textW = getTalkTextWidth(kLevelItems[i]);
-			const int textX = (width - textW) / 2;
-			const int y = menuY + i * rowH;
-			drawTalkText(textX, y + 1, kLevelItems[i]);
 
-			if (i == _startLevel - 1) {
-				drawTalkText(textX - 12, y + 1, ">");
-			}
+			drawTalkText(textX, y + 1, kLevelItems[i]);
 
 			if (i == _levelSelectSel) {
 				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
@@ -377,8 +398,8 @@ void InsaneRebel1::runOptionsMenu() {
 	_optionsActive = false;
 }
 
-void InsaneRebel1::runLevelSelectMenu() {
-	_levelSelectSel = CLIP(_startLevel - 1, 0, kRA1LastImplementedLevel - 1);
+int InsaneRebel1::runLevelSelectMenu() {
+	_levelSelectSel = CLIP(_startLevel - 1, 0, kRA1NumLevels - 1);
 	_levelSelectActive = true;
 
 	while (!_vm->shouldQuit()) {
@@ -393,17 +414,17 @@ void InsaneRebel1::runLevelSelectMenu() {
 			break;
 
 		if (_menuConfirmed) {
-			if (_levelSelectSel < kRA1LastImplementedLevel) {
-				_startLevel = _levelSelectSel + 1;
+			if (_levelSelectSel < kRA1NumLevels) {
 				_levelSelectActive = false;
-				return;
+				return _levelSelectSel + 1;  // 1-based level number
 			}
-
+			// BACK
 			_levelSelectActive = false;
-			return;
+			return 0;
 		}
 	}
 	_levelSelectActive = false;
+	return 0;
 }
 
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 420895d56d0..ab15110e0dc 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -1049,10 +1049,16 @@ void InsaneRebel1::runGame() {
 			// Game Options
 			runOptionsMenu();
 			break;
-		case 3:
-			// Level Select
-			runLevelSelectMenu();
+		case 3: {
+			// Level Select — launch directly
+			int selectedLevel = runLevelSelectMenu();
+			if (selectedLevel >= 1 && selectedLevel <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]))) {
+				_startLevel = selectedLevel;
+				(this->*kLevelRunners[selectedLevel - 1])();
+				_currentLevel = 0;
+			}
 			break;
+		}
 		case 5:
 			// Exit
 			return;


Commit: 595f2a1fcb515ba2855919378399bf774c858736
    https://github.com/scummvm/scummvm/commit/595f2a1fcb515ba2855919378399bf774c858736
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:40+02:00

Commit Message:
SCUMM: RA1: Improve level 9 rendering

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_levels.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp
    engines/scumm/smush/codec1.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index d8b3d53264b..a31b45bdcc3 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -218,6 +218,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_perspectiveX = 0;
 	_perspectiveY = 0;
 	_projectionCurveExtent = 1;
+	_onFootCharX = 0;
+	_onFootCharY = 0;
+	_onFootAnimCounter = 0;
+	_onFootInitialized = false;
 	memset(_projectionTable, 0, sizeof(_projectionTable));
 
 	memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 8c81e8ed0a3..e4fde7cbff6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -240,6 +240,13 @@ private:
 	// 0x0B handler physics update (asteroid/surface levels)
 	void updateAsteroidPhysics();
 
+	// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
+	void updateOnFootPhysics();
+	int16 _onFootCharX;      // Character draw X (g_shipOffsetX in original)
+	int16 _onFootCharY;      // Character draw Y (g_shipOffsetY in original)
+	int16 _onFootAnimCounter; // DAT_0000828a: fire animation counter
+	bool _onFootInitialized;  // First-frame init flag for 0x19
+
 	// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
 	int _currentLevel;
 
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 9fd9b39f2b1..9f3c4492a65 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -791,7 +791,8 @@ void InsaneRebel1::updateShipPhysics() {
 	//   (_74CE + 0x20) * 5 / 0x41  → 0..4  (5 rows)
 	int vComponent = (_liftSmooth + 0x20) * 5 / 0x41;
 
-	_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
+	if (_shipBank.numSprites > 0)
+		_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
 
 	// --- Step 10: Damage/event bit synthesis + damage processing ---
 	// RA1 FUN_1B297-style latches from GAME opcodes:
@@ -1152,6 +1153,120 @@ void InsaneRebel1::updateAsteroidPhysics() {
 }
 
 
+// updateOnFootPhysics — HandleGameOp19_OnFootSequence (0x19) + HandleGameOp1A_OnFootVariant (0x1A).
+// On-foot handler for Level 9 (Stormtroopers). Character walks left/right, crosshair tracks mouse.
+//
+// Original has TWO separate variable pairs:
+//   DAT_000041a0/41a2 = camera offset (SetCameraOffset, ProjectPointToScreen)
+//   g_perspectiveX/Y  = crosshair center (on-foot targeting)
+// Our _perspectiveX/_perspectiveY maps to the camera offset (DAT_000041a0/41a2).
+// The crosshair center (0xA3, 0x82) is a separate constant for on-foot mode.
+static const int16 kOnFootCenterX = 0xA3;  // g_perspectiveX in HandleGameOp19
+static const int16 kOnFootCenterY = 0x82;  // g_perspectiveY in HandleGameOp19
+
+void InsaneRebel1::updateOnFootPhysics() {
+	// --- First-frame initialization (0x19 counter==0) ---
+	if (!_onFootInitialized) {
+		_onFootInitialized = true;
+		_shipDirIndex = 15;       // Center facing
+		_onFootAnimCounter = 0;
+		_onFootCharX = 0;
+		_onFootCharY = 0;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+
+		// SetCameraOffset(0,0) — no viewport crop for on-foot levels
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		resetProjectionTable();
+	}
+
+	// --- 0x19: Character walk animation + damage ---
+	// Track fire button for animation
+	if (!_playerFired)
+		_onFootAnimCounter = 0;
+	else
+		_onFootAnimCounter++;
+
+	// Walk direction state machine (from HandleGameOp19)
+	if (_shipDirIndex == 0) {
+		// Left edge: snap to center, step character left
+		_shipDirIndex = 15;
+		_onFootCharX -= 0x3A;
+	} else if (_shipDirIndex < 5) {
+		_shipDirIndex--;
+	} else if (_shipDirIndex < 10) {
+		_shipDirIndex++;
+	} else if (_shipDirIndex == 10) {
+		// Right edge: snap to center, step character right
+		_shipDirIndex = 15;
+		_onFootCharX += 0x3A;
+	} else if (_onFootAnimCounter < 5 && !_playerFired) {
+		// Aim direction from crosshair toward character (QuantizeDirection8Way)
+		int16 dx = _shipPosX - (_onFootCharX + kOnFootCenterX);
+		int16 aimDir = 0;
+		if (dx > 30)
+			aimDir = 4;
+		else if (dx > 10)
+			aimDir = 2;
+		else if (dx < -30)
+			aimDir = -4;
+		else if (dx < -10)
+			aimDir = -2;
+		_shipDirIndex = CLIP<int16>(aimDir + 15, 11, 19);
+	} else {
+		// Walking based on mouse input direction
+		int16 inputX = 0, inputY = 0;
+		preprocessMouseAxes(inputX, inputY);
+		if (inputX > 0x1E && _onFootCharX < 0x72)
+			_shipDirIndex = 6;  // Walk right
+		else if (inputX < -0x1E && _onFootCharX > -0x72)
+			_shipDirIndex = 4;  // Walk left
+	}
+
+	// --- 0x1A: Crosshair positioning (HandleGameOp1A_OnFootVariant) ---
+	// shipPosX/Y = mouse_input + crosshair_center + character_offset
+	int16 inputX = 0, inputY = 0;
+	preprocessMouseAxes(inputX, inputY);
+	inputX = CLIP<int16>(inputX, -100, 100);
+	int16 inputYNeg = CLIP<int16>((int16)(-inputY), -0x4B, 0x0F);
+	_shipPosX = inputX + kOnFootCenterX + _onFootCharX;
+	_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
+
+	// --- Damage handling (from 0x19) ---
+	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
+		_health -= _tuning.miss;  // On-foot uses DAT_00001b29 offset = miss tuning
+		if (_health < 0)
+			_deathTimer = 15;
+		_prevDamageFlags = _damageFlags;
+		_damageCooldown = 3;
+		_screenFlash = 5;
+	}
+
+	if (_damageCooldown > 0)
+		_damageCooldown--;
+
+	// Health regen + survival bonus (UpdatePeriodicScoreAndHealth)
+	if ((_frameCounter & 0x1F) == 0) {
+		if (_health >= 0 && _health < kMaxHealth)
+			_health++;
+		if (_health >= 0)
+			_score += _tuning.time;
+	}
+
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+
+	if (_deathTimer > 1 && _health < 0)
+		_deathTimer--;
+	if (_screenFlash > 0)
+		_screenFlash--;
+
+	_damageFlags = 0;
+	_frameCounter++;
+}
+
 void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 	int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 	int16 par1, int16 par2, int16 par3, int16 par4) {
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/insane_rebel1_levels.cpp
index 1defb3d8cae..c51577b751c 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/insane_rebel1_levels.cpp
@@ -205,12 +205,15 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 }
 
 void InsaneRebel1::loadLevelSprites(int level) {
-	// Ship direction bank — not all levels have one (e.g. Level 2 is first-person)
+	// Ship/character direction bank — try BANK1, BANK, then PILOT (Level 9 on-foot)
 	Common::String bankFile = Common::String::format("LVL%d/L%dBANK1.NUT", level, level);
 	if (!loadRA1Nut(bankFile.c_str(), _shipBank)) {
 		Common::String legacyBankFile = Common::String::format("LVL%d/L%dBANK.NUT", level, level);
-		if (!loadRA1Nut(legacyBankFile.c_str(), _shipBank))
-			debug(1, "InsaneRebel1: No BANK1/BANK for level %d", level);
+		if (!loadRA1Nut(legacyBankFile.c_str(), _shipBank)) {
+			Common::String pilotFile = Common::String::format("LVL%d/L%dPILOT.NUT", level, level);
+			if (!loadRA1Nut(pilotFile.c_str(), _shipBank))
+				debug(1, "InsaneRebel1: No BANK1/BANK/PILOT for level %d", level);
+		}
 	}
 
 	// Secondary ship bank used by some level-specific handlers (e.g. LVL1 mode-2).
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ffb050cd18a..ab5a2c6f5b4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -371,21 +371,40 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
 	const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
 		(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
+	const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
+		(!haveFrameGameOpcodes &&
+			(_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
 	if (asteroidMode) {
 		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
 		updateAsteroidPhysics();
+	} else if (onFootMode) {
+		// On-foot handler — opcodes 0x19/0x1A (Level 9 Stormtroopers)
+		updateOnFootPhysics();
+
+		if (_health < 0 && _deathTimer < 2) {
+			_fireCooldown = _playerFired ? 1 : 0;
+			_vm->_smushVideoShouldFinish = true;
+			return;
+		}
+
+		// Draw character sprite at on-foot position (DrawFobjGlyph with flag 0x80)
+		if (_shipBank.numSprites > 0 && _shipDirIndex >= 0 &&
+			_shipDirIndex < _shipBank.numSprites) {
+			const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
+			int drawX = _onFootCharX + spr.xoffs;
+			int drawY = _onFootCharY + spr.yoffs;
+			renderSprite(renderBitmap, pitch, width, height, drawX, drawY, spr);
+		}
 	} else {
 		const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
 			(!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
 		const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
-			hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
 			(!haveFrameGameOpcodes &&
-				(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09 ||
-				 _activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
+				(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
 
 		// Dispatch movement path by GAME handler family:
 		//   0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
-		//   0x07/0x09/0x19/0x1A -> flight-family handlers
+		//   0x07/0x09 -> flight-family handlers
 		if (turretMode) {
 			updateTurretPhysics();
 		} else if (flightMode) {
@@ -394,9 +413,8 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 		// Most RA1 gameplay loops exit the current interactive movie as soon as
 		// health drops below 0, then dispatch to the level-specific retry/death
-		// clip. LVL9's on-foot flow is the main exception: it keeps pumping while
-		// deathTimer > 1 before branching out of the current segment.
-		if (_health < 0 && _currentLevel != 8) {
+		// clip.
+		if (_health < 0) {
 			_fireCooldown = _playerFired ? 1 : 0;
 			_vm->_smushVideoShouldFinish = true;
 			return;
@@ -410,9 +428,15 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// GAME handlers in the original update FUN_224FD during the same frame that
 	// the new control state is computed. Sync the current frame's viewport window
 	// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
+	// On-foot mode uses SetCameraOffset(0,0) — no viewport crop.
 	if (_player) {
-		_player->_ra1ViewportOffsetX = _perspectiveX;
-		_player->_ra1ViewportOffsetY = _perspectiveY;
+		if (onFootMode) {
+			_player->_ra1ViewportOffsetX = 0;
+			_player->_ra1ViewportOffsetY = 0;
+		} else {
+			_player->_ra1ViewportOffsetX = _perspectiveX;
+			_player->_ra1ViewportOffsetY = _perspectiveY;
+		}
 	}
 
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index ab15110e0dc..7b05e2f0814 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -787,7 +787,7 @@ bool InsaneRebel1::runLevel9() {
 		_lastHitTarget = 0;
 		_shipPosX = kRA1CenterX;
 		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
+		_shipDirIndex = 15;  // On-foot center direction
 		_rollAccum = 0;
 		_liftSmooth = 0;
 		_posAccumX = 0;
@@ -795,6 +795,10 @@ bool InsaneRebel1::runLevel9() {
 		_perspectiveX = 0;
 		_perspectiveY = 0;
 		_levelGameplayPhase = 0;
+		_onFootCharX = 0;
+		_onFootCharY = 0;
+		_onFootAnimCounter = 0;
+		_onFootInitialized = false;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -821,6 +825,7 @@ bool InsaneRebel1::runLevel9() {
 			if (_health < 0)
 				break;
 
+			_gameplayFlags75fe |= 4;
 			const int side1 = (_shipPosX < kRA1CenterX) ? 0 : 1;
 			playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
 			if (_vm->shouldQuit())
@@ -863,6 +868,7 @@ bool InsaneRebel1::runLevel9() {
 			if (_health < 0)
 				break;
 
+			_gameplayFlags75fe |= 4;
 			const int side2 = (_shipPosX < kRA1CenterX) ? 0 : 1;
 			playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
 			if (_vm->shouldQuit())
@@ -891,6 +897,7 @@ bool InsaneRebel1::runLevel9() {
 				if (_health < 0)
 					break;
 
+				_gameplayFlags75fe |= 4;
 				const int side3 = (_shipPosX < kRA1CenterX) ? 0 : 1;
 				if (side3 == randPath3) {
 					playCinematic("LVL9/L9CUT6A.ANM");
diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 4ad3dd455db..7c6efb9bd61 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,6 +36,7 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
 	} while (--height);
 }
 
+
 /**
  * RA1 codec 1: RLE with transparency on pixel 0.
  * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index e1301126370..f20a05d3357 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -879,6 +879,7 @@ byte *SmushPlayer::getVideoPalette() {
 }
 
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
@@ -1201,8 +1202,13 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 		}
 		break;
 	case SMUSH_CODEC_RLE_ALT:
-		// Codec 3: all pixels opaque (pixel 0 is written)
-		smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
+		// Codec 3: all pixels opaque (pixel 0 is written).
+		// Original FUN_00010916 writes every byte including value 0.
+		if (isRA1()) {
+			smushDecodeRLEOpaque(_dst, adjustedSrc, left, top, width, height, pitch);
+		} else {
+			smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
+		}
 		break;
 	case SMUSH_CODEC_RA1_SCATTER:
 		// Codec 2: Scatter draw uses absolute buffer coords, not clipped FOBJ coords.
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 1188ee84f3b..698c64e831e 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -128,7 +128,11 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 		if (_insane) {
 			InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 			if (rebel1->isInteractiveVideoActive()) {
-				if (rebel1->getActiveGameOpcode() == 0x0B && _storedFobjWidth == _vm->_screenWidth) {
+				const uint16 gameOp = rebel1->getActiveGameOpcode();
+				// 0x0B (asteroid/surface) and 0x19/0x1A (on-foot) use SetCameraOffset
+				// directly — no projection-based FTCH placement.
+				if ((gameOp == 0x0B && _storedFobjWidth == _vm->_screenWidth) ||
+					gameOp == 0x19 || gameOp == 0x1A) {
 					left += _ra1ViewportOffsetX;
 					top += _ra1ViewportOffsetY;
 				} else {


Commit: 0235c711e158600470cb838f1ce2aa83b5a63268
    https://github.com/scummvm/scummvm/commit/0235c711e158600470cb838f1ce2aa83b5a63268
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:41+02:00

Commit Message:
SCUMM: RA1: Improve on-foot level rendering

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 9f3c4492a65..2ac8f46b999 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -1234,9 +1234,18 @@ void InsaneRebel1::updateOnFootPhysics() {
 	_shipPosX = inputX + kOnFootCenterX + _onFootCharX;
 	_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
 
-	// --- Damage handling (from 0x19) ---
+	// --- Scripted damage latches → damageFlags (matching FUN_1B297 pattern) ---
+	// GAME 0x5D/0x5F set latches; convert to damage flags before the check.
+	if (_gameLatch5D == 0xFFFF)
+		_damageFlags |= 0x40;
+	if (_gameLatch5F != 0 &&
+		_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
+		_damageFlags |= 0x80;
+
+	// --- Damage handling (from HandleGameOp19_OnFootSequence) ---
+	// On-foot uses single tuning value (DAT_00001b29 offset = miss) for all damage types.
 	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
-		_health -= _tuning.miss;  // On-foot uses DAT_00001b29 offset = miss tuning
+		_health -= _tuning.miss;
 		if (_health < 0)
 			_deathTimer = 15;
 		_prevDamageFlags = _damageFlags;
@@ -1462,8 +1471,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		break;
 
 	case 0x5A:
-		// Target detection — FUN_1C0EF (0x1C0EF). AABB from video stream.
-		// Params: targetIdx, left, top, width, height
+		// Target detection — HandleGameOp5A (0x1C0EF). AABB from video stream.
+		// Original checks event mask: if target already killed, skip to GOST update.
 		if (subSize >= 24) {
 			int16 targetIdx = (int16)param1;
 			int16 left = (int16)b.readUint32BE();
@@ -1472,7 +1481,25 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			int16 h = (int16)b.readUint32BE();
 			int16 right = left + w;
 			int16 bottom = top + h;
-			checkTargetHit(targetIdx, left, top, right, bottom);
+
+			if (targetIdx >= 0) {
+				const int byteIdx = targetIdx >> 3;
+				if (byteIdx >= 0 && byteIdx < 0x96 && byteIdx < kFrameObjectStateBytes) {
+					const byte bit = (byte)(0x80 >> (targetIdx & 7));
+					const int altIdx = byteIdx + 0x96;
+					const bool primarySet = (_frameObjectState[byteIdx] & bit) != 0;
+					const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
+						((_frameObjectState[altIdx] & bit) != 0);
+
+					if (!primarySet || secondarySet) {
+						checkTargetHit(targetIdx, left, top, right, bottom);
+					} else {
+						updateGostSlotPosition(targetIdx, left, top, right, bottom);
+					}
+				} else {
+					checkTargetHit(targetIdx, left, top, right, bottom);
+				}
+			}
 			debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
 				targetIdx, left, top, right, bottom, _targetProximity);
 		}
@@ -1507,6 +1534,14 @@ void InsaneRebel1::processShot() {
 	if (!_playerFired || _fireCooldown != 0)
 		return;
 
+	// On-foot mode: only spawn when in aiming stance (dirIndex 11-19) or flags force it.
+	// Original: if (((10 < g_shipDirIndex) && (g_shipDirIndex < 0x14)) || ((DAT_000075fe & 8) != 0))
+	const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+	if (onFootMode) {
+		if (!(_shipDirIndex > 10 && _shipDirIndex < 20) && !(_gameplayFlags75fe & 8))
+			return;
+	}
+
 	// Find first available slot (timer < 1 or > 5), matching FUN_1CCA0.
 	int slot = -1;
 	for (int i = 0; i < kMaxShotSlots; i++) {
@@ -1519,21 +1554,34 @@ void InsaneRebel1::processShot() {
 		return;
 	}
 
-	// Record shot at current cursor position.
+	// Shot origin depends on game mode:
+	// On-foot: character position (g_shipOffsetX + g_perspectiveX)
+	// Turret: perspective-adjusted center
+	// Flight: cursor position
 	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
-	const int16 shipCenterX = turretMode ? (int16)(kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
-	const int16 shipCenterY = turretMode ? (int16)(kRA1CenterY + (_perspectiveY - 0x17)) : _shipPosY;
+	int16 originX, originY;
+	if (onFootMode) {
+		originX = _onFootCharX + kOnFootCenterX;
+		originY = _onFootCharY + kOnFootCenterY;
+	} else if (turretMode) {
+		originX = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
+		originY = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+	} else {
+		originX = _shipPosX;
+		originY = _shipPosY;
+	}
 
 	_shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
 	_shotSlots[slot].posX = _shipPosX;
 	_shotSlots[slot].posY = _shipPosY;
-	_shotSlots[slot].centerX = shipCenterX;
-	_shotSlots[slot].centerY = shipCenterY;
+	_shotSlots[slot].centerX = originX;
+	_shotSlots[slot].centerY = originY;
 	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
 	playSfx(kSfxLaserShot, 127, 0);
 
-	debug(5, "RA1 shot: slot=%d pos=(%d,%d)", slot, _shotSlots[slot].posX, _shotSlots[slot].posY);
+	debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
+		_shotSlots[slot].posX, _shotSlots[slot].posY, originX, originY);
 }
 
 // checkTargetHit — FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
@@ -1604,7 +1652,9 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 							projectGameplayPoint(_shipPosX, _shipPosY);
 						}
 
-						debug(5, "RA1 HIT: target=%d score=%d kills=%d", targetIdx, _score, _killCount);
+						debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
+							targetIdx, gi, _gostSlots[gi].posX, _gostSlots[gi].posY,
+							_score, _killCount, _bangBank.numSprites);
 						return;
 					}
 				}
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index ab5a2c6f5b4..311841d370d 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -454,11 +454,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			(!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
 		renderTargetBoxes(renderBitmap, pitch, width, height);
 		processShot();
+		renderLaserShots(renderBitmap, pitch, width, height);
+		// Timer decrement AFTER rendering (original decrements inside the render loop).
+		// This ensures timer==5 first frame is rendered with gun barrel offset and lerp=1.
 		for (int i = 0; i < kMaxShotSlots; i++) {
 			if (_shotSlots[i].timer > 0)
 				_shotSlots[i].timer--;
 		}
-		renderLaserShots(renderBitmap, pitch, width, height);
 		renderGostSlots(renderBitmap, pitch, width, height);
 		renderTargeting(renderBitmap, pitch, width, height);
 
@@ -553,8 +555,17 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 // renderGostSlots — FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
 // Renders explosion sprites from bangBank at each GOST slot's recorded position.
 void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
-	if (_bangBank.numSprites <= 0)
+	if (_bangBank.numSprites <= 0) {
+		// Warn if there are active GOST slots but no bang sprites to render
+		for (int i = 0; i < kMaxGostSlots; i++) {
+			if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
+				debug(1, "RA1 renderGostSlots: bangBank empty but gost slot %d active (target=%d frame=%d)",
+					i, _gostSlots[i].targetId, _gostSlots[i].frame);
+				_gostSlots[i].targetId = 0;  // Clear stale slot
+			}
+		}
 		return;
+	}
 
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const bool projectGostMarkers = (_activeGameOpcode == 0x0B);
@@ -581,7 +592,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	}
 }
 
-// renderLaserShots — FUN_1CDA7/FUN_1D79C shot visual path.
+// renderLaserShots — FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
 	if (_laserBank.numSprites <= 0)
 		return;
@@ -589,11 +600,24 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
 	// Entry 0 unused.
 	static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
+	// DAT_2413: on-foot lerp table (timer 5 = 1, not 0 like flight mode).
+	static const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
+	// DAT_240e: gun barrel X offset indexed by shipDirIndex (for timer==5 first frame).
+	static const int16 kOnFootGunBarrelX[20] = {
+		4, 0, 8, 8, 7, 6, 4, 1, 0, 0,
+		0, -56, -47, -23, -13, 0, 13, 30, 54, 59
+	};
+	// DAT_2420: gun barrel Y offset indexed by shipDirIndex (for timer==5 first frame).
+	static const int16 kOnFootGunBarrelY[20] = {
+		0, 0, -56, -47, -23, -13, 0, 13, 30, 54,
+		59, -3, -19, -24, -30, -28, -30, -29, -20, -5
+	};
 	const int spritesPerSet = 5;
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const int overlayY = ra1OverlayViewOffsetY(this);
 	const int leftStartX = 0;
 	const int rightStartX = 0x13F; // 319
+	const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
 	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
 	const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
 	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : (overlayY + _shipPosY);
@@ -601,11 +625,41 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
 			const int timer = _shotSlots[i].timer;
-			const int lerp = kShotLerpByTimer[timer];
 			const int frame = spritesPerSet - timer;
 			const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
 			const int targetY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
 
+			if (onFootMode) {
+				// HandleGameOp1A_OnFootVariant: single beam from character to crosshair.
+				// Gun barrel offset on first frame (timer==5, dirIndex in aiming range).
+				if (timer == 5 && _shipDirIndex > 10 && _shipDirIndex < 20) {
+					_shotSlots[i].centerX += kOnFootGunBarrelX[_shipDirIndex];
+					_shotSlots[i].centerY += kOnFootGunBarrelY[_shipDirIndex];
+				}
+
+				// Skip visible rendering when flags indicate invisible shots (auto-fire).
+				if ((_gameplayFlags75fe & 8) != 0)
+					continue;
+
+				const int srcX = _shotSlots[i].centerX;
+				const int srcY = _shotSlots[i].centerY;
+				const int dstX = _shotSlots[i].posX;
+				const int dstY = _shotSlots[i].posY;
+				const int dir = ra1ShotDirection((int16)srcX, (int16)srcY,
+					(int16)dstX, (int16)dstY);
+				const int sprIdx = MIN<int>(ABS(dir), _laserBank.numSprites - 1);
+				const uint32 flags = 0x83 | ((dir < 0) ? 0x2000 : 0);
+				const int onFootLerp = kOnFootShotLerp[timer];
+				const int interpX = srcX + (((dstX - srcX) * onFootLerp) >> 3);
+				const int interpY = srcY + (((dstY - srcY) * onFootLerp) >> 3);
+
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interpX, interpY, _laserBank.sprites[sprIdx], flags);
+				continue;
+			}
+
+			const int lerp = kShotLerpByTimer[timer];
+
 			if (turretMode) {
 				// FUN_1D79C chooses emitters in two ways:
 				// - DAT_75E4 == 2: use DAT_75DC..DAT_75E2 fixed offsets
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 7b05e2f0814..b19e957206f 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -1089,6 +1089,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	clearBit(0);
 	_interactiveVideoActive = true;
 	_levelRouteChoice = 0;
+	_onFootInitialized = false;  // Reset so each segment triggers counter==0 init
 	resetFrameObjectState();
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);


Commit: 4dcc5d8d2df2e07a5f572b7ad67119132ca6657a
    https://github.com/scummvm/scummvm/commit/4dcc5d8d2df2e07a5f572b7ad67119132ca6657a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:41+02:00

Commit Message:
SCUMM: RA1: Improve walker level rendering

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index a31b45bdcc3..fbca7de4e95 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -186,6 +186,9 @@ void InsaneRebel1::loadTuningForLevel(int level) {
 	_tuning.levelPts = kTuningTable[l][d][10];
 	_tuning.bonus    = kTuningTable[l][d][11];
 	_tuning.flags    = kTuningTable[l][d][12];
+	// initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE).
+	_gameplayFlags75fe = (uint16)_tuning.flags;
+
 	debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d drift=%d snap=%d "
 		"miss=%d wham=%d shot=%d kill=%d time=%d levelPts=%d bonus=%d flags=0x%x",
 		level, d, _tuning.roll, _tuning.lift, _tuning.slide, _tuning.drift, _tuning.snap,
@@ -304,6 +307,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_gostSlotIdx = 0;
 	_killCount = 0;
 	_lastHitTarget = 0;
+	_walkerHealth = 100;
+	_walkerTimer = 0;
+	_walkerBranchChoice = 0;
 	resetFrameObjectState();
 
 	if (loadRA1Nut("SYS/TALKFONT.NUT", _hudFontBank)) {
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index e4fde7cbff6..21c65deea64 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -146,7 +146,11 @@ private:
 	void renderTargetBoxes(byte *dst, int pitch, int width, int height);
 	void renderTargeting(byte *dst, int pitch, int width, int height);
 	void renderGostSlots(byte *dst, int pitch, int width, int height);
+	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
+							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
+	void renderLevel8Overlay(byte *dst, int pitch, int width, int height);
+	void updateLevel8WalkerState();
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 	void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
@@ -411,6 +415,31 @@ private:
 	int16 _killCount;        // 0x75D0: targets destroyed this stage
 	int16 _lastHitTarget;    // 0x75D6: prevents double-hit on same target
 
+	// Level 8 walker-specific state — RunLevel8Flow (0x18546)
+	int16 _walkerHealth;     // Walker health percentage (0-100), init=100
+	int16 _walkerTimer;      // Attack window countdown (100→0)
+	int16 _walkerBranchChoice; // Directional choice: 0=none, 1=left, 2=right
+
+	// Attack window frame numbers per route (3 routes × 3 windows)
+	// Route 0: 2588/1709/262, Route 1: 2323/1444/-2, Route 2: 877/-2/-2
+	// -2 = disabled (no window at that slot for this route)
+	static const int16 kWalkerAttackWindow1[3];
+	static const int16 kWalkerAttackWindow2[3];
+	static const int16 kWalkerAttackWindow3[3];
+
+	// Per-route damage frame tables — FUN_12fe1/FUN_130c9/FUN_13195
+	// Each entry: {frameNumber, hitboxType} where hitboxType determines the check
+	struct WalkerDamageFrame {
+		int16 frame;
+		int16 type;    // 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
+	};
+	static const WalkerDamageFrame kWalkerDamageRoute0[];
+	static const WalkerDamageFrame kWalkerDamageRoute1[];
+	static const WalkerDamageFrame kWalkerDamageRoute2[];
+	static const int kWalkerDamageRoute0Count;
+	static const int kWalkerDamageRoute1Count;
+	static const int kWalkerDamageRoute2Count;
+
 	static const int kFrameObjectStateBytes = 300;
 	byte _frameObjectState[kFrameObjectStateBytes];
 };
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 311841d370d..c498bc388a4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -437,6 +437,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			_player->_ra1ViewportOffsetX = _perspectiveX;
 			_player->_ra1ViewportOffsetY = _perspectiveY;
 		}
+
+		// Screen shake — SetCameraOffset (0x224FD): random [-2,+2] jitter when
+		// _screenFlash > 0. Original uses RandScaleByte(5) - 2 for each axis.
+		if (_screenFlash > 0) {
+			_player->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+			_player->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+		}
 	}
 
 	// Assembly dispatch (FUN_1BE1B) only runs the targeting/shot overlay pipeline
@@ -485,12 +492,27 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	}
 
 	renderExplosions(renderBitmap, pitch, width, height);
+
+	// Level 8 (Imperial Walkers) — walker-specific state update + UI overlay.
+	// In the original, RunLevel8Flow runs the walker logic inline in the per-frame
+	// game loop. We call it from procPostRendering when _currentLevel == 7.
+	if (_currentLevel == 7) {
+		updateLevel8WalkerState();
+		renderLevel8Overlay(renderBitmap, pitch, width, height);
+	}
+
 	renderHUD(renderBitmap, pitch, width, height);
 	_fireCooldown = _playerFired ? 1 : 0;
 }
 
 // renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
+// Original gates on g_hudDisableFlags (0x75FE) bit 1: skip when set (Hard difficulty).
 void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
+	if (_gameplayFlags75fe & 2) {
+		_prevTargetCount = _targetCount;
+		_targetCount = 0;
+		return;
+	}
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
 	const bool projectTargetMarkers = (_activeGameOpcode == 0x0B);
@@ -552,8 +574,32 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 	_lastHitTarget = 0;
 }
 
+// renderGostScorePopup — Per-kill score glyph from RenderGostOverlaySlots (0x1C9CD).
+// Maps kill score to tech-font glyph and draws it rising upward from the kill position.
+void InsaneRebel1::renderGostScorePopup(byte *dst, int pitch, int width, int height,
+										int16 centerX, int16 centerY, int16 frame) {
+	// Score-to-glyph mapping from original (0x1CA5D-0x1CACB)
+	char glyphChar = '\0';
+	uint16 scoreValue = (uint16)_tuning.kill;
+	if (scoreValue == 10)       glyphChar = 0x72;  // 'r'
+	else if (scoreValue == 25)  glyphChar = 0x74;  // 't'
+	else if (scoreValue == 50)  glyphChar = 0x73;  // 's'
+	else if (scoreValue == 100) glyphChar = 0x6F;  // 'o'
+	else if (scoreValue == 200) glyphChar = 0x70;  // 'p'
+	else if (scoreValue == 500) glyphChar = 0x71;  // 'q'
+
+	if (glyphChar == '\0')
+		return;
+
+	// Original: DrawStringEx(buf, colorMap, centerX-4, centerY-frame, 1, 100, 3, scoreText)
+	// "<<{glyph}" string selects tech font layer via the << markup
+	char scoreText[4] = { '<', '<', glyphChar, '\0' };
+	drawFontBankString(dst, pitch, width, height,
+					   centerX - 4, centerY - frame, scoreText);
+}
+
 // renderGostSlots — FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
-// Renders explosion sprites from bangBank at each GOST slot's recorded position.
+// Renders explosion sprites from bangBank + per-kill score popup glyphs.
 void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
 	if (_bangBank.numSprites <= 0) {
 		// Warn if there are active GOST slots but no bang sprites to render
@@ -585,6 +631,13 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 			int drawY = centerY - spr.height / 2;
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 
+			// Per-kill score popup glyph — RenderGostOverlaySlots (0x1CA35)
+			// Suppressed in on-foot mode (combatModeFlags & 8)
+			if ((_gameplayFlags75fe & 8) == 0) {
+				renderGostScorePopup(dst, pitch, width, height,
+									overlayX + centerX, centerY, _gostSlots[i].frame);
+			}
+
 			_gostSlots[i].frame++;
 			if (_gostSlots[i].frame >= 10)
 				_gostSlots[i].targetId = 0;  // Animation complete
@@ -1128,6 +1181,253 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 
 }
 
+// Attack window frame tables — RunLevel8Flow (0x18546), data at 0x236D/0x2373/0x2379.
+// Each route has up to 3 attack windows. -2 means disabled.
+const int16 InsaneRebel1::kWalkerAttackWindow1[3] = { 2588, 2323, 877 };
+const int16 InsaneRebel1::kWalkerAttackWindow2[3] = { 1709, 1444, -2 };
+const int16 InsaneRebel1::kWalkerAttackWindow3[3] = { 262, -2, -2 };
+
+// Per-route walker damage frame tables — FUN_12fe1/FUN_130c9/FUN_13195.
+// type: 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
+const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute0[] = {
+	{0x00CD, 2}, {0x00EF, 2}, {0x0294, 1}, {0x03A2, 2},
+	{0x04BE, 0}, {0x05C9, 2}, {0x076C, 0}, {0x085A, 4}, {0x096F, 2}
+};
+const int InsaneRebel1::kWalkerDamageRoute0Count = ARRAYSIZE(kWalkerDamageRoute0);
+
+const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute1[] = {
+	{0x0189, 1}, {0x0297, 2}, {0x03B3, 0}, {0x04BE, 4},
+	{0x0661, 0}, {0x074F, 4}, {0x0864, 4}
+};
+const int InsaneRebel1::kWalkerDamageRoute1Count = ARRAYSIZE(kWalkerDamageRoute1);
+
+const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute2[] = {
+	{0x00BB, 0}, {0x01A9, 4}, {0x02BE, 4}
+};
+const int InsaneRebel1::kWalkerDamageRoute2Count = ARRAYSIZE(kWalkerDamageRoute2);
+
+// updateLevel8WalkerState — Per-frame walker health + attack window + damage logic.
+// Called from procPostRendering when _currentLevel == 7.
+void InsaneRebel1::updateLevel8WalkerState() {
+	// Walker health computation — RunLevel8Flow (0x18634-0x18655)
+	if (_walkerHealth >= 11) {
+		_walkerHealth = (int16)(100 - (_killCount + (_killCount >> 2)));
+	} else if (_walkerHealth > 0 && (_frameCounter & 3) == 0) {
+		_walkerHealth--;
+	}
+
+	// Walker destroyed — exit interactive video (original loop: `while (sVar6 != 0)`)
+	if (_walkerHealth <= 0) {
+		_vm->_smushVideoShouldFinish = true;
+		return;
+	}
+
+	// Per-route damage check — FUN_12fe1/FUN_130c9/FUN_13195
+	int route = CLIP(_levelRouteIndex, 0, 2);
+	const WalkerDamageFrame *table;
+	int tableCount;
+	switch (route) {
+	case 0:  table = kWalkerDamageRoute0; tableCount = kWalkerDamageRoute0Count; break;
+	case 1:  table = kWalkerDamageRoute1; tableCount = kWalkerDamageRoute1Count; break;
+	default: table = kWalkerDamageRoute2; tableCount = kWalkerDamageRoute2Count; break;
+	}
+
+	uint16 fc = (uint16)_frameCounter;
+	for (int i = 0; i < tableCount; i++) {
+		if (fc != (uint16)table[i].frame)
+			continue;
+
+		int16 dx = _shipPosX - kRA1CenterX;  // distance from center
+		int16 dy = _shipPosY - kRA1CenterY;
+		if (dx < 0) dx = -dx;
+		if (dy < 0) dy = -dy;
+
+		bool hit = false;
+		switch (table[i].type) {
+		case 0: hit = (dx < 0x29 && dy < 0x20); break;  // proximity(41,32)
+		case 1: hit = (dy < 0x0F); break;                // offsetY(15)
+		case 2: hit = (dx < 0x18); break;                // offsetX(24)
+		case 3: hit = (dx < 0x28 && dy < 0x1F); break;   // proximity(40,31)
+		case 4: hit = (dy < 0x1F); break;                // offsetY(31)
+		default: break;
+		}
+
+		if (hit)
+			_damageFlags |= 0x20;
+		break;
+	}
+
+	// Attack window logic — RunLevel8Flow (0x18778-0x18B4A)
+	const int16 *windows[3] = {
+		&kWalkerAttackWindow1[route],
+		&kWalkerAttackWindow2[route],
+		&kWalkerAttackWindow3[route]
+	};
+	int16 frameNum = (int16)fc;
+
+	// Check if we're inside any attack window (window-100 < frame <= window)
+	bool inWindow = false;
+	for (int w = 0; w < 3; w++) {
+		int16 windowEnd = *windows[w];
+		if (windowEnd < 0) continue;
+		if (frameNum > windowEnd - 100 && frameNum <= windowEnd) {
+			inWindow = true;
+
+			// Reset timer at window start (first frame of window)
+			if (frameNum == windowEnd - 99)
+				_walkerTimer = 100;
+			break;
+		}
+	}
+
+	if (inWindow && _walkerBranchChoice == 0) {
+		_walkerTimer--;
+
+		// Check if we're in the directional phase (last 50 frames)
+		bool inDirectionalPhase = false;
+		for (int w = 0; w < 3; w++) {
+			int16 windowEnd = *windows[w];
+			if (windowEnd < 0) continue;
+			if (frameNum > windowEnd - 0x32 && frameNum <= windowEnd) {
+				inDirectionalPhase = true;
+				break;
+			}
+		}
+
+		if (inDirectionalPhase) {
+			// Player can choose direction during last 50 frames
+			if (_playerFired && _avgInputX == 0) {
+				_walkerBranchChoice = (_shipPosX < 0xA0) ? 1 : 2;
+			}
+		} else {
+			// Torpedo sound every 8 frames during targeting phase
+			if ((_frameCounter & 7) == 0)
+				playSfx(kSfxLockOn, 127, 0);
+		}
+	}
+
+	// At window boundary: decide route branch — RunLevel8Flow (0x18A7F-0x18B4A)
+	// Original: left branches unless at window3, right branches unless at window2.
+	for (int w = 0; w < 3; w++) {
+		int16 windowEnd = *windows[w];
+		if (windowEnd < 0) continue;
+		if (fc != (uint16)windowEnd) continue;
+
+		int newRoute = 0;
+		bool goLeft = (_walkerBranchChoice == 1) ||
+					  (_shipPosX < 0xA0 && _walkerBranchChoice != 2);
+
+		if (goLeft) {
+			// Left: branch to route 1 unless at window3 (w==2)
+			if (w != 2)
+				newRoute = 1;
+		} else {
+			// Right: branch to route 2 unless at window2 (w==1)
+			if (w != 1)
+				newRoute = 2;
+		}
+
+		if (newRoute != 0) {
+			_pendingRouteIndex = newRoute;
+			_vm->_smushVideoShouldFinish = true;
+		}
+		_walkerBranchChoice = 0;
+		break;
+	}
+}
+
+// renderLevel8Overlay — Walker-specific UI from RunLevel8Flow (0x18660-0x18A7E).
+// Draws walker health %, attack timer, directional arrows, and target reticle.
+// Original draws via DrawStringEx/FormatAndDrawText in screen-space (within viewport).
+// We draw into 384x242 buffer, so add viewport offset to convert screen→buffer coords.
+void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int height) {
+	if (_currentLevel != 7)
+		return;
+
+	// Viewport offset: screen-space → buffer-space (same approach as renderHUD)
+	int viewX = _player ? _player->_ra1ViewportOffsetX : _perspectiveX;
+	int viewY = _player ? _player->_ra1ViewportOffsetY : _perspectiveY;
+
+	// Walker health display — "<<WALKER %d%%" at projected (0x61, 0x8D)
+	// Blinks when health < 16: only drawn when (frameCounter & 2) != 0
+	if (_walkerHealth > 0 && (_walkerHealth >= 16 || (_frameCounter & 2) != 0)) {
+		int16 projX = 0x61, projY = 0x8D;
+		projectGameplayPoint(projX, projY);
+		// Apply 1/4 parallax compensation (original: 0x61 - (projX - 0x61) >> 2)
+		projX = (int16)(0x61 - ((projX - 0x61) >> 2));
+		projY = (int16)(0x8D - ((projY - 0x8D) >> 2));
+
+		char walkerStr[24];
+		Common::sprintf_s(walkerStr, "<<WALKER %d%%%%", (int)_walkerHealth);
+		drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, walkerStr);
+	}
+
+	// Attack window overlay (timer + arrows/reticle)
+	int route = CLIP(_levelRouteIndex, 0, 2);
+	const int16 *windows[3] = {
+		&kWalkerAttackWindow1[route],
+		&kWalkerAttackWindow2[route],
+		&kWalkerAttackWindow3[route]
+	};
+	int16 frameNum = (int16)(uint16)_frameCounter;
+
+	bool inWindow = false;
+	bool inDirectionalPhase = false;
+	for (int w = 0; w < 3; w++) {
+		int16 windowEnd = *windows[w];
+		if (windowEnd < 0) continue;
+		if (frameNum > windowEnd - 100 && frameNum <= windowEnd) {
+			inWindow = true;
+			if (frameNum > windowEnd - 0x32 && frameNum <= windowEnd)
+				inDirectionalPhase = true;
+			break;
+		}
+	}
+
+	if (!inWindow || _walkerBranchChoice != 0)
+		return;
+
+	// Timer countdown — "<<TIME %d" at projected (0x62, 0x9C)
+	{
+		int16 projX = 0x62, projY = 0x9C;
+		projectGameplayPoint(projX, projY);
+		projX = (int16)(0x62 - ((projX - 0x62) >> 2));
+		projY = (int16)(0x9C - ((projY - 0x9C) >> 2));
+
+		char timerStr[16];
+		Common::sprintf_s(timerStr, "<<TIME %d", (int)_walkerTimer);
+		drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, timerStr);
+	}
+
+	if (inDirectionalPhase) {
+		// Directional arrows — projected from (0,0) with parallax compensation
+		int16 projX = 0, projY = 0;
+		projectGameplayPoint(projX, projY);
+		int16 px = (int16)(-(projX >> 2));
+		int16 py = (int16)(-(projY >> 2));
+
+		if (_shipPosX < 0xA0) {
+			// Left arrow "<<v"
+			drawFontBankString(dst, pitch, width, height,
+				viewX + 0xA6 + px, viewY + 0x92 + py, "<<v");
+		} else {
+			// Right arrow "<<u"
+			drawFontBankString(dst, pitch, width, height,
+				viewX + 0xA8 - px, viewY + 0x93 - py, "<<u");
+		}
+	} else {
+		// Target reticle — "<<w" at projected (0xA9, 0x9A), blinks on (frame & 4)
+		if ((_frameCounter & 4) == 0) {
+			int16 projX = 0, projY = 0;
+			projectGameplayPoint(projX, projY);
+			int16 drawX = (int16)(0xA9 - (projX >> 2));
+			int16 drawY = (int16)(0x9A - (projY >> 2));
+			drawFontBankString(dst, pitch, width, height,
+				viewX + drawX, viewY + drawY, "<<w");
+		}
+	}
+}
+
 // renderSprite — Simplified version of FUN_20BD3 (0x20BD3) glyph/sprite renderer.
 // Original dispatches through full codec pipeline; this does flat pixel blit with transparency.
 void InsaneRebel1::renderSprite(byte *dst, int pitch, int width, int height,
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index b19e957206f..2027d684974 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -711,6 +711,11 @@ bool InsaneRebel1::runLevel8() {
 		_avgInputX = 0;
 		_avgInputY = 0;
 
+		// Walker-specific state — RunLevel8Flow (0x18546)
+		_walkerHealth = 100;
+		_walkerTimer = 0;
+		_walkerBranchChoice = 0;
+
 		int route = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;


Commit: 403d9675a91748fd2a325d100516d91c2979b819
    https://github.com/scummvm/scummvm/commit/403d9675a91748fd2a325d100516d91c2979b819
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:41+02:00

Commit Message:
SCUMM: RA1: Complete menu handling

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index fbca7de4e95..1291a7b90f7 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -287,6 +287,27 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_startLevel = 1;
 	_turbulenceEnabled = false;
 
+	// Options — read initial state from ScummVM mixer
+	_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
+	_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
+	_optTextEnabled = true;
+	_optControlsYFlip = false;
+	_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
+
+	// Default high scores — from DS:0x1D0/0x298/0x2C0
+	static const struct { const char *name; int32 score; byte gender; } kDefaultScores[kHighScoreCount] = {
+		{"Vince",   10000, 2}, {"Tamlynn",  9000, 2}, {"Chip",    8000, 2},
+		{"Brett",    7000, 1}, {"Casey",    6000, 1}, {"Justin",  5000, 1},
+		{"Bill",     4000, 0}, {"Aaron",    3000, 0}, {"Mary",    2000, 0},
+		{"Ron",      1000, 0}
+	};
+	for (int i = 0; i < kHighScoreCount; i++) {
+		Common::sprintf_s(_highScores[i].name, "<%s", kDefaultScores[i].name);
+		_highScores[i].score = kDefaultScores[i].score;
+		_highScores[i].gender = kDefaultScores[i].gender;
+	}
+	_highScoresActive = false;
+
 	// Shooting/targeting state
 	_playerFired = false;
 	_fireCooldown = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 21c65deea64..f42f612e441 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -361,15 +361,33 @@ private:
 	int _menuSelection; // 0..4 maps to return values 1..5
 	int _menuFrameCounter;
 
-	// Options submenu state
+	// Options submenu state — RunGameOptionsMenu (0x14B42)
+	static const int kOptionsItemCount = 9; // 8 options + BACK
 	bool _optionsActive;     // True when showing options instead of main menu
-	int _optionsSel;         // 0=difficulty, 1=turbulence, 2=back
+	int _optionsSel;         // 0..8 selected option row
 	bool _levelSelectActive; // True when showing level-select submenu
 	int _levelSelectSel;     // 0=Level1 ... N-1=Back
 	int _startLevel;         // 1-based start level for "Start New Game"
 
+	// Per-option state (matching original RunGameOptionsMenu globals)
+	bool _optMusicEnabled;    // DAT_22b7: music on/off
+	bool _optSfxEnabled;      // DAT_22b8: sfx+voice on/off
+	bool _optTextEnabled;     // DAT_22b9: dialogue text on/off
+	bool _optControlsYFlip;   // DAT_22be: Y-axis inversion
+	int  _optVolume;          // DAT_22c1: master volume 0..127
 	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
 
+	// High scores / TOP PILOTS display — data at DS:0x1D0
+	static const int kHighScoreCount = 10;
+	struct HighScoreEntry {
+		char name[20];   // 0x14 bytes per entry (includes '<' prefix)
+		int32 score;
+		byte gender;     // 0/1/2 → tech font glyph '{','|','}'
+	};
+	HighScoreEntry _highScores[kHighScoreCount];
+	bool _highScoresActive;  // True when showing TOP PILOTS overlay
+	void showHighScores();
+
 	// Shooting state — FUN_1CCA0 (0x1CCA0)
 	bool _playerFired;       // 0x7570: current fire-button state
 	int16 _fireCooldown;     // 0x757C: previous-frame fire-button state (edge gate)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 2ac8f46b999..699c181a71a 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -634,6 +634,10 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
 
 	inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
 	inputY = CLIP<int16>(scaledY, -127, 127);
+
+	// Controls Y-flip option (DAT_22be in original)
+	if (_optControlsYFlip)
+		inputY = -inputY;
 }
 
 // updateShipPhysics — FUN_1DEB5 (0x1DEB5). Accumulator-based position system.
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 2ab05d280dd..231b94fdac8 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -20,9 +20,12 @@
  */
 
 #include "common/system.h"
+#include "common/config-manager.h"
 #include "common/events.h"
 #include "common/str.h"
 
+#include "audio/mixer.h"
+
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/insane/insane_rebel1.h"
@@ -78,11 +81,35 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_w:
-			_optionsSel = (_optionsSel + 2) % 3;
+			_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
 			return true;
 		case Common::KEYCODE_DOWN:
 		case Common::KEYCODE_s:
-			_optionsSel = (_optionsSel + 1) % 3;
+			_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
+			return true;
+		case Common::KEYCODE_LEFT:
+		case Common::KEYCODE_a:
+			// Volume down when on volume row (row 6)
+			if (_optionsSel == 6) {
+				_optVolume = MAX(0, _optVolume - 5);
+				_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+					(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+				ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+				ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+				ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+			}
+			return true;
+		case Common::KEYCODE_RIGHT:
+		case Common::KEYCODE_d:
+			// Volume up when on volume row (row 6)
+			if (_optionsSel == 6) {
+				_optVolume = MIN(127, _optVolume + 5);
+				_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+					(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+				ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+				ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+				ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+			}
 			return true;
 		case Common::KEYCODE_RETURN:
 		case Common::KEYCODE_KP_ENTER:
@@ -91,7 +118,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 			_vm->_smushVideoShouldFinish = true;
 			return true;
 		case Common::KEYCODE_ESCAPE:
-			_optionsSel = 2;  // Back
+			_optionsSel = kOptionsItemCount - 1;  // Back
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
@@ -147,6 +174,13 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		}
 	}
 
+	// High scores: any key or click dismisses
+	if (_highScoresActive && (event.type == Common::EVENT_KEYDOWN ||
+		event.type == Common::EVENT_LBUTTONDOWN)) {
+		_vm->_smushVideoShouldFinish = true;
+		return true;
+	}
+
 	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 		if (_player) {
 			debug("Rebel1: ESC pressed - skipping video");
@@ -180,27 +214,57 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		drawFontBankString(dst, pitch, width, height, x, y, text);
 	};
 
+	if (_highScoresActive) {
+		// --- TOP PILOTS high score display ---
+		// Original renders over O1SCORE.ANM. Title appears after frame 20,
+		// entries fade in one per frame. We show all immediately.
+		const int titleW = getTalkTextWidth("TOP PILOTS");
+		drawTalkText((width - titleW) / 2, 10, "TOP PILOTS");
+
+		for (int i = 0; i < kHighScoreCount; i++) {
+			const int y = 25 + i * 14;
+			// Name (left side)
+			drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
+			// Score (right side)
+			char scoreLine[32];
+			Common::sprintf_s(scoreLine, "<%ld", (long)_highScores[i].score);
+			drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
+		}
+		return;
+	}
+
 	if (_optionsActive) {
-		// --- Options submenu ---
+		// --- Options submenu (matching original RunGameOptionsMenu) ---
 		static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
 
 		const int titleW = getTalkTextWidth("GAME OPTIONS");
-		drawTalkText((width - titleW) / 2, 36, "GAME OPTIONS");
-
-		// Build dynamic option strings
-		char diffLine[64];
-		snprintf(diffLine, sizeof(diffLine), "DIFFICULTY: %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
-		const char *turbLine = _turbulenceEnabled ? "TURBULENCE: ON" : "TURBULENCE: OFF";
-		const char *kOptionsItems[3] = { diffLine, turbLine, "BACK" };
+		drawTalkText((width - titleW) / 2, 30, "GAME OPTIONS");
+
+		// Build dynamic option strings for each row
+		char diffLine[64], volLine[64];
+		Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+		Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
+
+		const char *optItems[kOptionsItemCount] = {
+			"EXIT MENU",
+			_optMusicEnabled  ? "MUSIC IS ON"             : "MUSIC IS OFF",
+			_optSfxEnabled    ? "SFX AND VOICE ARE ON"    : "SFX AND VOICE ARE OFF",
+			_optTextEnabled   ? "DIALOGUE TEXT IS ON"      : "DIALOGUE TEXT IS OFF",
+			_optControlsYFlip ? "CONTROLS ARE Y-FLIPPED"  : "CONTROLS ARE NORMAL",
+			_turbulenceEnabled ? "TURBULENCE IS ON"        : "TURBULENCE IS OFF",
+			volLine,
+			diffLine,
+			"BACK"
+		};
 
-		const int menuY = 60;
-		const int rowH = 16;
+		const int menuY = 44;
+		const int rowH = 14;
 
-		for (int i = 0; i < 3; i++) {
-			const int textW = getTalkTextWidth(kOptionsItems[i]);
+		for (int i = 0; i < kOptionsItemCount; i++) {
+			const int textW = getTalkTextWidth(optItems[i]);
 			const int textX = (width - textW) / 2;
 			const int y = menuY + i * rowH;
-			drawTalkText(textX, y + 1, kOptionsItems[i]);
+			drawTalkText(textX, y + 1, optItems[i]);
 
 			if (i == _optionsSel) {
 				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
@@ -228,21 +292,21 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
 
 		static const char *kLevelItems[kRA1LevelSelectItemCount] = {
-			" 1  FLIGHT TRAINING",
-			" 2  ASTEROID FIELD",
-			" 3  PLANET KOLAADOR",
-			" 4  STAR DESTROYER",
-			" 5  TATOOINE ATTACK",
-			" 6  ASTEROID CHASE",
-			" 7  PROBE DROIDS",
-			" 8  IMPERIAL WALKERS",
-			" 9  STORMTROOPERS",
-			"10  REBEL TRANSPORT",
-			"11  YAVIN TRAINING",
-			"12  TIE ATTACK",
-			"13  DEATH STAR SURFACE",
-			"14  SURFACE CANNON",
-			"15  DEATH STAR TRENCH",
+			" 1 TRAINING",
+			" 2 ASTEROIDS",
+			" 3 KOLAADOR",
+			" 4 STAR DESTR",
+			" 5 TATOOINE",
+			" 6 AST CHASE",
+			" 7 PROBES",
+			" 8 WALKERS",
+			" 9 TROOPERS",
+			"10 TRANSPORT",
+			"11 YAVIN",
+			"12 TIE ATK",
+			"13 DS SURFACE",
+			"14 CANNON",
+			"15 DS TRENCH",
 			"BACK"
 		};
 
@@ -379,17 +443,35 @@ void InsaneRebel1::runOptionsMenu() {
 
 		if (_menuConfirmed) {
 			switch (_optionsSel) {
-			case 0:
-				// Cycle difficulty
-				_difficulty = (_difficulty + 1) % 3;
-				loadTuningForLevel(0);
+			case 0: // EXIT MENU (same as BACK)
+				_optionsActive = false;
+				return;
+			case 1: // Toggle music
+				_optMusicEnabled = !_optMusicEnabled;
+				_vm->_mixer->muteSoundType(Audio::Mixer::kMusicSoundType, !_optMusicEnabled);
+				break;
+			case 2: // Toggle SFX + Voice
+				_optSfxEnabled = !_optSfxEnabled;
+				_vm->_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, !_optSfxEnabled);
+				_vm->_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, !_optSfxEnabled);
 				break;
-			case 1:
-				// Toggle turbulence
+			case 3: // Toggle dialogue text
+				_optTextEnabled = !_optTextEnabled;
+				ConfMan.setBool("subtitles", _optTextEnabled);
+				break;
+			case 4: // Toggle Y-flip controls
+				_optControlsYFlip = !_optControlsYFlip;
+				break;
+			case 5: // Toggle turbulence
 				_turbulenceEnabled = !_turbulenceEnabled;
 				break;
-			case 2:
-				// Back to main menu
+			case 6: // Volume — adjusted via left/right in notifyEvent
+				break;
+			case 7: // Cycle difficulty
+				_difficulty = (_difficulty + 1) % 3;
+				loadTuningForLevel(0);
+				break;
+			case 8: // BACK
 				_optionsActive = false;
 				return;
 			}
@@ -427,4 +509,15 @@ int InsaneRebel1::runLevelSelectMenu() {
 	return 0;
 }
 
+void InsaneRebel1::showHighScores() {
+	// Original plays O1SCORE.ANM with TOP PILOTS overlay, dismissable by any key.
+	_highScoresActive = true;
+	_menuActive = true;
+	_menuFrameCounter = 0;
+	clearVideoBuffer();
+	playCinematic("OPEN/O1SCORE.ANM");
+	_menuActive = false;
+	_highScoresActive = false;
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 2027d684974..29aaadb1f2e 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -1046,13 +1046,17 @@ void InsaneRebel1::runGame() {
 
 		switch (menuResult) {
 		case 1: {
-			const int startLevel = CLIP<int>(_startLevel, 1, sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+			// START NEW GAME — sequential play from _startLevel
+			const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+			const int startLevel = CLIP<int>(_startLevel, 1, numLevels);
 			bool completed = true;
 
 			for (int level = startLevel;
-				 level <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0])) && completed && !_vm->shouldQuit();
+				 level <= numLevels && completed && !_vm->shouldQuit();
 				 ++level) {
 				completed = (this->*kLevelRunners[level - 1])();
+				if (completed && level < numLevels)
+					_startLevel = level + 1;
 			}
 			_currentLevel = 0;
 			break;
@@ -1071,11 +1075,17 @@ void InsaneRebel1::runGame() {
 			}
 			break;
 		}
+		case 4:
+			// CONTINUE DEMO — attract mode.
+			// Original shows TOP PILOTS (O1SCORE.ANM) then loops O1OPEN.ANM.
+			showHighScores();
+			if (!_vm->shouldQuit())
+				playCinematic("OPEN/O1OPEN.ANM");
+			break;
 		case 5:
 			// Exit
 			return;
 		default:
-			// Passcode, Demo — not yet implemented, return to menu
 			break;
 		}
 	}


Commit: 3ec6f05b036171e0c78850cc21e322dfe2c677b1
    https://github.com/scummvm/scummvm/commit/3ec6f05b036171e0c78850cc21e322dfe2c677b1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:42+02:00

Commit Message:
SCUMM: RA1: Add level 11 through 15 handlers

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1291a7b90f7..c49dfceb683 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -186,8 +186,11 @@ void InsaneRebel1::loadTuningForLevel(int level) {
 	_tuning.levelPts = kTuningTable[l][d][10];
 	_tuning.bonus    = kTuningTable[l][d][11];
 	_tuning.flags    = kTuningTable[l][d][12];
-	// initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE).
+	// initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE)
+	// and clears protected targets (DAT_00007732/7734).
 	_gameplayFlags75fe = (uint16)_tuning.flags;
+	_protectedTargetA = 0;
+	_protectedTargetB = 0;
 
 	debug(1, "RA1: Loaded tuning level=%d diff=%d: roll=%d lift=%d slide=%d drift=%d snap=%d "
 		"miss=%d wham=%d shot=%d kill=%d time=%d levelPts=%d bonus=%d flags=0x%x",
@@ -295,7 +298,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
 
 	// Default high scores — from DS:0x1D0/0x298/0x2C0
-	static const struct { const char *name; int32 score; byte gender; } kDefaultScores[kHighScoreCount] = {
+	static const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
 		{"Vince",   10000, 2}, {"Tamlynn",  9000, 2}, {"Chip",    8000, 2},
 		{"Brett",    7000, 1}, {"Casey",    6000, 1}, {"Justin",  5000, 1},
 		{"Bill",     4000, 0}, {"Aaron",    3000, 0}, {"Mary",    2000, 0},
@@ -304,7 +307,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	for (int i = 0; i < kHighScoreCount; i++) {
 		Common::sprintf_s(_highScores[i].name, "<%s", kDefaultScores[i].name);
 		_highScores[i].score = kDefaultScores[i].score;
-		_highScores[i].gender = kDefaultScores[i].gender;
+		_highScores[i].difficulty = kDefaultScores[i].difficulty;
 	}
 	_highScoresActive = false;
 
@@ -328,6 +331,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_gostSlotIdx = 0;
 	_killCount = 0;
 	_lastHitTarget = 0;
+	_protectedTargetA = 0;
+	_protectedTargetB = 0;
+	_shieldGenHitsA = 0;
+	_shieldGenHitsB = 0;
+	_torpedoFired = false;
 	_walkerHealth = 100;
 	_walkerTimer = 0;
 	_walkerBranchChoice = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index f42f612e441..66a7a74ec62 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -124,6 +124,11 @@ private:
 	bool runLevel8();
 	bool runLevel9();
 	bool runLevel10();
+	bool runLevel11();
+	bool runLevel12();
+	bool runLevel13();
+	bool runLevel14();
+	bool runLevel15();
 
 	// Play a passive cinematic (no game callback, skippable)
 	// startFrame > 0: fast-forward (decode without display) to that frame
@@ -382,7 +387,7 @@ private:
 	struct HighScoreEntry {
 		char name[20];   // 0x14 bytes per entry (includes '<' prefix)
 		int32 score;
-		byte gender;     // 0/1/2 → tech font glyph '{','|','}'
+		byte difficulty;  // 0/1/2 → tech font glyph '{','|','}' (easy/normal/hard)
 	};
 	HighScoreEntry _highScores[kHighScoreCount];
 	bool _highScoresActive;  // True when showing TOP PILOTS overlay
@@ -433,6 +438,19 @@ private:
 	int16 _killCount;        // 0x75D0: targets destroyed this stage
 	int16 _lastHitTarget;    // 0x75D6: prevents double-hit on same target
 
+	// Protected target IDs — 0x7732/0x7734 in original
+	// Targets listed here can be hit repeatedly (no event mask toggle).
+	// Used by Level 4 (shield generators) and Level 15 (torpedo targets).
+	int16 _protectedTargetA; // 0x7732: protected objectId+1 (0=none)
+	int16 _protectedTargetB; // 0x7734: protected objectId+1 (0=none)
+
+	// Per-target hit counters for shield generator tracking (Level 4)
+	int16 _shieldGenHitsA;   // Hits on _protectedTargetA
+	int16 _shieldGenHitsB;   // Hits on _protectedTargetB
+
+	// Torpedo fired flag — set when torpedo hits in Phase 2 (Level 15)
+	bool _torpedoFired;      // 0x7602 bit 1 in original
+
 	// Level 8 walker-specific state — RunLevel8Flow (0x18546)
 	int16 _walkerHealth;     // Walker health percentage (0-100), init=100
 	int16 _walkerTimer;      // Attack window countdown (100→0)
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 699c181a71a..1ecda9e047b 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -372,6 +372,12 @@ void InsaneRebel1::applyFrameObjectHitState(int16 targetIdx) {
 	if (targetIdx < 0)
 		return;
 
+	// Protected targets (shield generators in Level 4, etc.) can be hit
+	// repeatedly — skip event mask toggle. Original: DAT_00007732/7734 check
+	// in HandleGameOp5A.
+	if (targetIdx + 1 == _protectedTargetA || targetIdx + 1 == _protectedTargetB)
+		return;
+
 	const int byteIndex = targetIdx >> 3;
 	if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
 		return;
@@ -1146,9 +1152,45 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	_frameCounter++;
 
+	// Level 4 Phase 2: enable torpedo mode at frame 0x3E
 	if (_currentLevel == 3 && _levelGameplayPhase == 2 && _frameCounter == 0x3E)
 		_gameplayFlags75ff |= 2;
 
+	// Level 4 Phase 1: track shield generator hits per frame.
+	// Original (RunLevel4Flow): g_recentKillObjectIdPlus1 checked every frame.
+	// When enough hits accumulated (>0x30), generator is "destroyed" (clear protectedTarget).
+	// When both destroyed for 60 frames, phase ends.
+	if (_currentLevel == 3 && _levelGameplayPhase == 1) {
+		if (_lastHitTarget == _protectedTargetA && _protectedTargetA != 0)
+			_shieldGenHitsA++;
+		if (_lastHitTarget == _protectedTargetB && _protectedTargetB != 0)
+			_shieldGenHitsB++;
+
+		if (_shieldGenHitsA > 0x30)
+			_protectedTargetA = 0;
+		if (_shieldGenHitsB > 0x30)
+			_protectedTargetB = 0;
+
+		// Both destroyed: count down 60 frames then end phase
+		if (_protectedTargetA == 0 && _protectedTargetB == 0 &&
+			_shieldGenHitsA > 0x30 && _shieldGenHitsB > 0x30) {
+			_shieldGenHitsA++;  // reuse as countdown
+			if (_shieldGenHitsA > 0x30 + 0x3C)
+				_vm->_smushVideoShouldFinish = true;
+		}
+	}
+
+	// Level 15 Phase 2: enable torpedo and end on hit.
+	// Original (RunLevel1GameLoop): torpedo at frame 0x18A, ends when DAT_00007602 & 2.
+	if (_currentLevel == 14 && _levelGameplayPhase == 2) {
+		if (_frameCounter == 0x18A)
+			_gameplayFlags75ff |= 2;
+		if (_killCount > 0) {
+			_torpedoFired = true;
+			_vm->_smushVideoShouldFinish = true;
+		}
+	}
+
 	checkDynamicLevelBranch();
 
 	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 231b94fdac8..bcb9da88cda 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -225,9 +225,12 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 			const int y = 25 + i * 14;
 			// Name (left side)
 			drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
-			// Score (right side)
+			// Score + difficulty glyph (right side) — original format "<%ld %c"
+			// Difficulty byte 0/1/2 + 0x7B = '{','|','}' tech font glyphs (easy/normal/hard)
 			char scoreLine[32];
-			Common::sprintf_s(scoreLine, "<%ld", (long)_highScores[i].score);
+			Common::sprintf_s(scoreLine, "<%ld %c",
+				(long)_highScores[i].score,
+				(char)(_highScores[i].difficulty + 0x7B));
 			drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
 		}
 		return;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 29aaadb1f2e..76a66edbdca 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -367,12 +367,23 @@ bool InsaneRebel1::runLevel4() {
 		_killCount = 0;
 		_levelGameplayPhase = 0;
 
+		// Phase 1: Destroy two shield generators.
+		// Original sets DAT_00007732=0x39, DAT_00007734=0x3A — protected target IDs
+		// that can be hit repeatedly without event mask toggle.
+		_protectedTargetA = 0x39;
+		_protectedTargetB = 0x3A;
+		_shieldGenHitsA = 0;
+		_shieldGenHitsB = 0;
 		_levelGameplayPhase = 1;
 		playInteractiveVideo("LVL4/L4PLAY1.ANM");
+		_protectedTargetA = 0;
+		_protectedTargetB = 0;
 		if (_vm->shouldQuit())
 			return false;
 
 		if (_health >= 0) {
+			// Phase 2: Torpedo run — torpedo enabled at frame 0x3E by IACT handler.
+			// killCount > 0 = torpedo hit, killCount == 0 = missed.
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
@@ -385,6 +396,7 @@ bool InsaneRebel1::runLevel4() {
 		}
 
 		if (_health >= 0) {
+			// L4END1 = torpedo hit, L4END2 = torpedo missed
 			playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
 			return !_vm->shouldQuit();
 		}
@@ -1016,6 +1028,431 @@ bool InsaneRebel1::runLevel10() {
 	return false;
 }
 
+// Level 11 flow (RunLevel11Flow, 0x19F67): Yavin Training
+// Turret-style level. Single interactive phase with kill-count retry.
+// Original: L11INTRO → L11PLAY (turret, killCount>4 to pass) → L11RETRY → retry/L11END
+bool InsaneRebel1::runLevel11() {
+	debug(1, "InsaneRebel1: Running level 11");
+
+	_currentLevel = 10;
+	loadLevelSprites(11);
+	loadTuningForLevel(10);
+
+	beginLevelTitleOverlay(10);
+	playCinematic("LVL11/L11INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+		_turretEmitterLeftX = 25;
+		_turretEmitterLeftY = 15;
+
+		while (!_vm->shouldQuit()) {
+			playInteractiveVideo("LVL11/L11PLAY.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			if (_health < 0)
+				break;
+
+			// Original: killCount > 4 means pass
+			if (_killCount > 4)
+				break;
+
+			// Not enough kills — retry
+			playCinematic("LVL11/L11RETRY.ANM");
+			if (_vm->shouldQuit())
+				return false;
+		}
+
+		if (_health >= 0) {
+			playCinematic("LVL11/L11END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL11/L11NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL11/L11DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+// Level 12 flow (RunLevel12Flow, 0x1A2DD): TIE Attack
+// Single interactive phase with mid-level retry mechanism.
+// Original: L12INTRO → L12PLAY → (retry at specific frame) → L12END
+bool InsaneRebel1::runLevel12() {
+	debug(1, "InsaneRebel1: Running level 12");
+
+	_currentLevel = 11;
+	loadLevelSprites(12);
+	loadTuningForLevel(11);
+
+	beginLevelTitleOverlay(11);
+	playCinematic("LVL12/L12INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		playInteractiveVideo("LVL12/L12PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			playCinematic("LVL12/L12END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL12/L12NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL12/L12DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+// Level 13 flow (RunLevel13Flow, 0x1A6E3): Death Star Surface
+// Flight level with enemy projectile system (original has 5-slot projectile tracking).
+// Original: L13INTRO → L13PLAY → L13END/L13NEW/L13DEATH
+bool InsaneRebel1::runLevel13() {
+	debug(1, "InsaneRebel1: Running level 13");
+
+	_currentLevel = 12;
+	loadLevelSprites(13);
+	loadTuningForLevel(12);
+
+	beginLevelTitleOverlay(12);
+	playCinematic("LVL13/L13INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		playInteractiveVideo("LVL13/L13PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			playCinematic("LVL13/L13END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL13/L13NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL13/L13DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+// Level 14 flow (RunLevel14Flow, 0x1ACB0): Surface Cannon
+// Two interactive phases: L14PLAY (targeting cannons) + L14PLAY2 (exhaust port approach).
+// Original: L14INTRO → L14PLAY → L14PLAY2 → L14END/L14NEW/L14DEATH
+bool InsaneRebel1::runLevel14() {
+	debug(1, "InsaneRebel1: Running level 14");
+
+	_currentLevel = 13;
+	loadLevelSprites(14);
+	loadTuningForLevel(13);
+
+	beginLevelTitleOverlay(13);
+	playCinematic("LVL14/L14INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		// Phase 1: targeting surface cannons
+		playInteractiveVideo("LVL14/L14PLAY.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			// Phase 2: exhaust port approach
+			_activeGameOpcode = 0;
+			_gameLatch5D = 0;
+			_gameLatch5F = 0;
+			_gameplayFlags75ff = 0;
+			_killCount = 0;
+
+			playInteractiveVideo("LVL14/L14PLAY2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+		}
+
+		if (_health >= 0) {
+			playCinematic("LVL14/L14END.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL14/L14NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL14/L14DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
+// Level 15 flow (RunLevel1GameLoop, 0x1B283): Death Star Trench
+// Two interactive phases with mid-level cutscene.
+// Original: L15INTRO → L15PLAY1 (trench run) → L15INTR2 (torpedo lock cutscene)
+//   → L15PLAY2 (final approach + torpedo) → L15END1/L15NEW/L15DEATH
+bool InsaneRebel1::runLevel15() {
+	debug(1, "InsaneRebel1: Running level 15");
+
+	_currentLevel = 14;
+	loadLevelSprites(15);
+	loadTuningForLevel(14);
+
+	beginLevelTitleOverlay(14);
+	playCinematic("LVL15/L15INTRO.ANM");
+	if (_vm->shouldQuit())
+		return false;
+
+	while (!_vm->shouldQuit()) {
+		_flyControlMode = 1;
+		_health = kMaxHealth;
+		_damageFlags = 0;
+		_prevDamageFlags = 0;
+		_damageCooldown = 0;
+		_deathTimer = 0;
+		_screenFlash = 0;
+		_frameCounter = 0;
+		_gameCounter = 0;
+		_activeGameOpcode = 0;
+		_gameLatch5D = 0;
+		_gameLatch5F = 0;
+		_gameplayFlags75ff = 0;
+		_killCount = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		_levelGameplayPhase = 0;
+		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_avgInputX = 0;
+		_avgInputY = 0;
+
+		// Phase 1: trench run
+		_levelGameplayPhase = 1;
+		playInteractiveVideo("LVL15/L15PLAY1.ANM");
+		if (_vm->shouldQuit())
+			return false;
+
+		if (_health >= 0) {
+			// Torpedo lock cutscene
+			playCinematic("LVL15/L15INTR2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			// Phase 2: final approach and torpedo shot.
+			// Original: torpedo enabled at frame 0x18A via _gameplayFlags75ff |= 2.
+			// _torpedoFired set by IACT handler when killCount > 0 (torpedo hits exhaust port).
+			_activeGameOpcode = 0;
+			_gameLatch5D = 0;
+			_gameLatch5F = 0;
+			_gameplayFlags75ff = 0;
+			_killCount = 0;
+			_torpedoFired = false;
+			_levelGameplayPhase = 2;
+
+			playInteractiveVideo("LVL15/L15PLAY2.ANM");
+			if (_vm->shouldQuit())
+				return false;
+		}
+
+		if (_health >= 0) {
+			playCinematic("LVL15/L15END1.ANM");
+			return !_vm->shouldQuit();
+		}
+
+		if (_lives > 0) {
+			playCinematic("LVL15/L15NEW.ANM");
+			if (_vm->shouldQuit())
+				return false;
+			_lives--;
+			continue;
+		}
+
+		playCinematic("LVL15/L15DEATH.ANM");
+		return false;
+	}
+
+	return false;
+}
+
 // Main game entry point — called from ScummEngine::go().
 // Matches original flow at 0x15597: intro → menu → level.
 void InsaneRebel1::runGame() {
@@ -1030,7 +1467,12 @@ void InsaneRebel1::runGame() {
 		&InsaneRebel1::runLevel7,
 		&InsaneRebel1::runLevel8,
 		&InsaneRebel1::runLevel9,
-		&InsaneRebel1::runLevel10
+		&InsaneRebel1::runLevel10,
+		&InsaneRebel1::runLevel11,
+		&InsaneRebel1::runLevel12,
+		&InsaneRebel1::runLevel13,
+		&InsaneRebel1::runLevel14,
+		&InsaneRebel1::runLevel15
 	};
 
 	// Play intro sequence (logo + opening)


Commit: 730896f1c30379ad671f078d397c0b3731450f78
    https://github.com/scummvm/scummvm/commit/730896f1c30379ad671f078d397c0b3731450f78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:42+02:00

Commit Message:
SCUMM: RA1: Add missing globals

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index c49dfceb683..4d4793f4c3c 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -267,6 +267,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_deathTimer = 0;
 	_screenFlash = 0;
 	_frameCounter = 0;
+	_screenShakeEnabled = false;
+	_deathCauseIndicator = 0;
+	_hudRenderFlag = 0;
+	_hudDirtyFlag = 0;
+	_maxChapterUnlocked = 0;
 	_interactiveVideoActive = false;
 	_gameCounter = 0;
 		_pathBranchEnabled = false;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 66a7a74ec62..93b0601d0c5 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -318,6 +318,11 @@ private:
 	int16 _deathTimer;           // 0x756A: death animation countdown (30 on death)
 	int16 _screenFlash;          // 0x7736: screen flash timer on hit
 	uint32 _frameCounter;        // 0x7740: global frame counter
+	bool _screenShakeEnabled;    // 0x41AC: when true, SetCameraOffset adds ±2 random jitter
+	byte _deathCauseIndicator;   // 0x772E: non-zero = player died; selects death animation variant
+	byte _hudRenderFlag;         // 0x7600: 0xFF when HUD should render (set by combat mode handlers)
+	byte _hudDirtyFlag;          // 0x7601: 0xFF after HUD redraw (set by renderHUD)
+	int16 _maxChapterUnlocked;   // 0x7730: highest chapter with valid passcode (0=none, set on completion)
 
 	static const int16 kMaxHealth = 98;
 	static const int16 kDeathTimerInit = 30;
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 1ecda9e047b..0a9ec942e9e 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -771,12 +771,17 @@ void InsaneRebel1::updateShipPhysics() {
 		}
 	}
 
-	// --- Step 8: Perspective offsets ---
+	// --- Step 8: Perspective offsets (SetCameraOffset) ---
 	// FUN_1DEB5 computes these linearly from ship offsets:
 	//   viewX = clamp((_74BA + 0x20), 0, 0x40)
 	//   viewY = clamp((_74BC + 0x17), 0, 0x2E)
 	_perspectiveX = CLIP<int16>((int16)(_shipPosX - kRA1CenterX + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)(_shipPosY - kRA1CenterY + 0x17), 0, 0x2E);
+	// Screen shake: when enabled, add random ±2 jitter (original SetCameraOffset at 0x22514)
+	if (_screenShakeEnabled) {
+		_perspectiveX = CLIP<int16>((int16)(_perspectiveX + (_vm->_rnd.getRandomNumber(4) - 2)), 0, 0x40);
+		_perspectiveY = CLIP<int16>((int16)(_perspectiveY + (_vm->_rnd.getRandomNumber(4) - 2)), 0, 0x2E);
+	}
 
 	// FUN_1DEB5 updates the curve table via FUN_22549 after SetCameraOffset.
 	// The full DOS path blends a few roll-history terms; use the current roll
@@ -825,8 +830,14 @@ void InsaneRebel1::updateShipPhysics() {
 		if (_damageFlags & 0x16)
 			_health -= _tuning.wham;
 
-		if (_health < 0)
+		if (_health < 0) {
 			_deathTimer = kDeathTimerInit;
+			// g_deathCauseIndicator (0x772E) — set based on damage source
+			if (_damageFlags & 0x80)
+				_deathCauseIndicator = 2;  // Projectile hit death
+			else
+				_deathCauseIndicator = 1;  // Collision death
+		}
 
 		_prevDamageFlags = _damageFlags;
 		_damageCooldown = kDamageCooldownInit;
@@ -849,9 +860,11 @@ void InsaneRebel1::updateShipPhysics() {
 			_score += _tuning.time;
 	}
 
-	// Screen flash decay
-	if (_screenFlash > 0)
+	// Screen flash decay — screen shake follows flash (EnableScreenShake/DisableScreenShake at 0x224ED)
+	if (_screenFlash > 0) {
 		_screenFlash--;
+		_screenShakeEnabled = (_screenFlash > 0);
+	}
 
 	// Clear per-frame damage flags
 	_damageFlags = 0;
@@ -920,8 +933,13 @@ void InsaneRebel1::updateTurretPhysics() {
 		else
 			_health -= _tuning.wham;
 
-		if (_health < 0)
+		if (_health < 0) {
 			_deathTimer = kDeathTimerInit;
+			if (_damageFlags & 0x80)
+				_deathCauseIndicator = 2;
+			else
+				_deathCauseIndicator = 1;
+		}
 
 		_prevDamageFlags = _damageFlags;
 		_damageCooldown = kDamageCooldownInit;
@@ -1012,8 +1030,10 @@ void InsaneRebel1::updateTurretPhysics() {
 			_score += _tuning.time;
 	}
 
-	if (_screenFlash > 0)
+	if (_screenFlash > 0) {
 		_screenFlash--;
+		_screenShakeEnabled = (_screenFlash > 0);
+	}
 
 	_gameLatch5D = 0;
 	_gameLatch5F = 0;
@@ -1081,6 +1101,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
 			_health -= _tuning.wham;
 		if (_health < 0) {
 			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
+			if (_damageFlags & 0x80)
+				_deathCauseIndicator = 2;
+			else
+				_deathCauseIndicator = 1;
 		}
 		_prevDamageFlags = _damageFlags;
 		_damageFlags = 0;
@@ -1095,9 +1119,10 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		_deathTimer--;
 	}
 
-	// Screen flash countdown
+	// Screen flash countdown — screen shake follows flash
 	if (_screenFlash > 0) {
 		_screenFlash--;
+		_screenShakeEnabled = (_screenFlash > 0);
 	}
 
 	// --- Cursor and perspective smoothing (FUN_1CDA7) ---
@@ -1292,8 +1317,10 @@ void InsaneRebel1::updateOnFootPhysics() {
 	// On-foot uses single tuning value (DAT_00001b29 offset = miss) for all damage types.
 	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
 		_health -= _tuning.miss;
-		if (_health < 0)
+		if (_health < 0) {
 			_deathTimer = 15;
+			_deathCauseIndicator = (_damageFlags & 0x80) ? 2 : 1;
+		}
 		_prevDamageFlags = _damageFlags;
 		_damageCooldown = 3;
 		_screenFlash = 5;
@@ -1315,8 +1342,10 @@ void InsaneRebel1::updateOnFootPhysics() {
 
 	if (_deathTimer > 1 && _health < 0)
 		_deathTimer--;
-	if (_screenFlash > 0)
+	if (_screenFlash > 0) {
 		_screenFlash--;
+		_screenShakeEnabled = (_screenFlash > 0);
+	}
 
 	_damageFlags = 0;
 	_frameCounter++;
@@ -1348,6 +1377,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
 		_driftParam = 0;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index c498bc388a4..49283a5e2a4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -368,6 +368,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _screenHeight;
 	int pitch = width;
 
+	// Set HUD render flag when interactive gameplay is active (0x7600)
+	_hudRenderFlag = 0xFF;
+
 	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
 	const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
 		(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
@@ -1179,6 +1182,7 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		}
 	}
 
+	_hudDirtyFlag = 0xFF;  // Mark HUD as freshly drawn (0x7601)
 }
 
 // Attack window frame tables — RunLevel8Flow (0x18546), data at 0x236D/0x2373/0x2379.
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 76a66edbdca..6e88dd22332 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -128,6 +128,8 @@ bool InsaneRebel1::runLevel1() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -189,6 +191,7 @@ bool InsaneRebel1::runLevel1() {
 
 				if (_killCount > 4) {
 					playCinematic("LVL1/L1END.ANM");
+					_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)1);
 					return !_vm->shouldQuit();
 				}
 
@@ -240,6 +243,8 @@ bool InsaneRebel1::runLevel2() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
@@ -259,6 +264,7 @@ bool InsaneRebel1::runLevel2() {
 
 		if (_health >= 0) {
 			playCinematic("LVL2/L2END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)2);
 			return !_vm->shouldQuit();
 		}
 
@@ -298,6 +304,8 @@ bool InsaneRebel1::runLevel3() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -319,6 +327,7 @@ bool InsaneRebel1::runLevel3() {
 
 		if (_health >= 0) {
 			playCinematic("LVL3/L3END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)3);
 			return !_vm->shouldQuit();
 		}
 
@@ -358,6 +367,8 @@ bool InsaneRebel1::runLevel4() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -398,6 +409,7 @@ bool InsaneRebel1::runLevel4() {
 		if (_health >= 0) {
 			// L4END1 = torpedo hit, L4END2 = torpedo missed
 			playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)4);
 			return !_vm->shouldQuit();
 		}
 
@@ -437,6 +449,8 @@ bool InsaneRebel1::runLevel5() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -497,6 +511,7 @@ bool InsaneRebel1::runLevel5() {
 
 		if (_health >= 0) {
 			playCinematic("LVL5/L5END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)5);
 			return !_vm->shouldQuit();
 		}
 
@@ -535,6 +550,8 @@ bool InsaneRebel1::runLevel6() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -556,6 +573,7 @@ bool InsaneRebel1::runLevel6() {
 
 		if (_health >= 0) {
 			playCinematic("LVL6/L6END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)6);
 			return !_vm->shouldQuit();
 		}
 
@@ -603,6 +621,8 @@ bool InsaneRebel1::runLevel7() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -652,6 +672,7 @@ bool InsaneRebel1::runLevel7() {
 
 		if (_health >= 0) {
 			playCinematic("LVL7/L7END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)7);
 			return !_vm->shouldQuit();
 		}
 
@@ -696,6 +717,8 @@ bool InsaneRebel1::runLevel8() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -728,6 +751,11 @@ bool InsaneRebel1::runLevel8() {
 		_walkerTimer = 0;
 		_walkerBranchChoice = 0;
 
+		// g_level8HitboxBuffer (0x7698) = _frameObjectState[150..299] filled with 0xFF.
+		// This enables all frame object event masks in the secondary half of the array,
+		// which the IACT 0x5A handler uses to gate walker-related frame objects.
+		memset(_frameObjectState + 150, 0xFF, 150);
+
 		int route = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;
@@ -750,6 +778,7 @@ bool InsaneRebel1::runLevel8() {
 
 		if (_health >= 0) {
 			playCinematic("LVL8/L8END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)8);
 			return !_vm->shouldQuit();
 		}
 
@@ -792,6 +821,8 @@ bool InsaneRebel1::runLevel9() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -939,6 +970,7 @@ bool InsaneRebel1::runLevel9() {
 				break;
 
 			playCinematic("LVL9/L9END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)9);
 			return !_vm->shouldQuit();
 		}
 
@@ -977,6 +1009,8 @@ bool InsaneRebel1::runLevel10() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -1010,6 +1044,7 @@ bool InsaneRebel1::runLevel10() {
 
 		if (_health >= 0) {
 			playCinematic("LVL10/L10END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)10);
 			return !_vm->shouldQuit();
 		}
 
@@ -1051,6 +1086,8 @@ bool InsaneRebel1::runLevel11() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -1100,6 +1137,7 @@ bool InsaneRebel1::runLevel11() {
 
 		if (_health >= 0) {
 			playCinematic("LVL11/L11END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)11);
 			return !_vm->shouldQuit();
 		}
 
@@ -1141,6 +1179,8 @@ bool InsaneRebel1::runLevel12() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -1174,6 +1214,7 @@ bool InsaneRebel1::runLevel12() {
 
 		if (_health >= 0) {
 			playCinematic("LVL12/L12END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)12);
 			return !_vm->shouldQuit();
 		}
 
@@ -1215,6 +1256,8 @@ bool InsaneRebel1::runLevel13() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -1248,6 +1291,7 @@ bool InsaneRebel1::runLevel13() {
 
 		if (_health >= 0) {
 			playCinematic("LVL13/L13END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)13);
 			return !_vm->shouldQuit();
 		}
 
@@ -1289,6 +1333,8 @@ bool InsaneRebel1::runLevel14() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -1336,6 +1382,7 @@ bool InsaneRebel1::runLevel14() {
 
 		if (_health >= 0) {
 			playCinematic("LVL14/L14END.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)14);
 			return !_vm->shouldQuit();
 		}
 
@@ -1378,6 +1425,8 @@ bool InsaneRebel1::runLevel15() {
 		_damageCooldown = 0;
 		_deathTimer = 0;
 		_screenFlash = 0;
+		_screenShakeEnabled = false;
+		_deathCauseIndicator = 0;
 		_frameCounter = 0;
 		_gameCounter = 0;
 		_activeGameOpcode = 0;
@@ -1435,6 +1484,7 @@ bool InsaneRebel1::runLevel15() {
 
 		if (_health >= 0) {
 			playCinematic("LVL15/L15END1.ANM");
+			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
 			return !_vm->shouldQuit();
 		}
 


Commit: a8581ef1f387eb6515daa186ad13d1fe5c9d9e78
    https://github.com/scummvm/scummvm/commit/a8581ef1f387eb6515daa186ad13d1fe5c9d9e78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:42+02:00

Commit Message:
SCUMM: RA: Add basic joystick support

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel2.cpp
    engines/scumm/insane/insane_rebel2.h
    engines/scumm/insane/insane_rebel2_iact.cpp
    engines/scumm/insane/insane_rebel2_menu.cpp
    engines/scumm/insane/insane_rebel2_render.cpp
    engines/scumm/metaengine.cpp
    engines/scumm/scumm.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 4d4793f4c3c..1984745d429 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -244,6 +244,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_mousePrevBiasY = 0;
 	_mouseBiasLatch = false;
 	_mouseRecentering = false;
+	_joystickAxisX = 0;
+	_joystickAxisY = 0;
+	_level2JoystickFilteredX = 0;
+	_level2JoystickFilteredY = 0;
+	_activeInputSource = kInputSourceMouse;
 
 	_currentLevel = 0;
 	_flyControlMode = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 93b0601d0c5..338aee4ccc6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -140,7 +140,7 @@ private:
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
 	void updateTurretPhysics();
-	void preprocessMouseAxes(int16 &inputX, int16 &inputY);
+	void preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick = nullptr);
 	void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
 	void resetProjectionTable();
 	void checkDynamicLevelBranch();
@@ -240,14 +240,24 @@ private:
 	int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
 	int16 _mouseOffsetY; // 0x9760-style accumulated recenter offset in DOS 200-space
 	int16 _mouseBiasX;   // 0x9774: current preprocessed horizontal bias
-	int16 _mouseBiasY;   // 0x9772: current preprocessed vertical bias
-	int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
-	int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
-	bool _mouseBiasLatch;  // 0x4486: one-frame large-jump latch
-	bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
-
-	// 0x0B handler physics update (asteroid/surface levels)
-	void updateAsteroidPhysics();
+		int16 _mouseBiasY;   // 0x9772: current preprocessed vertical bias
+		int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
+		int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
+		bool _mouseBiasLatch;  // 0x4486: one-frame large-jump latch
+		bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
+		int16 _joystickAxisX;   // Rebel-specific left-stick X captured from keymapper axis events
+		int16 _joystickAxisY;   // Rebel-specific left-stick Y captured from keymapper axis events
+		int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
+		int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
+		enum InputSource {
+			kInputSourceMouse,
+			kInputSourceJoystickAnalog,
+			kInputSourceJoystickDigital
+		};
+		InputSource _activeInputSource;
+
+		// 0x0B handler physics update (asteroid/surface levels)
+		void updateAsteroidPhysics();
 
 	// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
 	void updateOnFootPhysics();
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 0a9ec942e9e..c5069953a82 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/system.h"
+#include "common/config-manager.h"
 #include "common/endian.h"
 
 #include "scumm/scumm_v7.h"
@@ -27,6 +28,22 @@
 
 namespace Scumm {
 
+static inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
+	const int deadZone = MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000;
+	return (ABS(axisValue) <= deadZone) ? 0 : axisValue;
+}
+
+static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
+	const int delta = (int)inputValue - (int)filteredValue;
+	int step = delta / 10;
+
+	if (step == 0 && delta != 0)
+		step = (delta > 0) ? 1 : -1;
+
+	filteredValue = CLIP<int>(filteredValue + step, -axisMax, axisMax);
+	return filteredValue;
+}
+
 // LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
 // (6..18), while the recovered outer loop compares the post-latch state against the
 // later translated values seen in the executable. Accept both representations.
@@ -554,10 +571,53 @@ void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
 // Preserve the DOS bias/offset persistence and one-frame jump latch from
 // FUN_231BE, but avoid hard recentring the host mouse into the DOS safe window.
 // The actual frame-averaging behavior stays untouched.
-void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY) {
+void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick) {
+	if (usedJoystick)
+		*usedJoystick = false;
+
 	if (_mouseRecentering)
 		return;
 
+	const int16 analogAxisX = applyRebel1AnalogDeadzone(_joystickAxisX);
+	const int16 analogAxisY = applyRebel1AnalogDeadzone(_joystickAxisY);
+	const int joyX =
+		(_vm->getActionState(kScummActionInsaneRight) ? 1 : 0) -
+		(_vm->getActionState(kScummActionInsaneLeft) ? 1 : 0);
+	const int joyY =
+		(_vm->getActionState(kScummActionInsaneUp) ? 1 : 0) -
+		(_vm->getActionState(kScummActionInsaneDown) ? 1 : 0);
+
+	if (_activeInputSource == kInputSourceJoystickAnalog) {
+		if (usedJoystick)
+			*usedJoystick = true;
+
+		if (analogAxisX != 0 || analogAxisY != 0) {
+			inputX = CLIP<int32>(((int32)analogAxisX * 127) / Common::JOYAXIS_MAX, -127, 127);
+			inputY = CLIP<int32>(((int32)analogAxisY * 127) / Common::JOYAXIS_MAX, -127, 127);
+		} else {
+			inputX = 0;
+			inputY = 0;
+		}
+
+		if (_optControlsYFlip)
+			inputY = -inputY;
+
+		return;
+	}
+
+	if (_activeInputSource == kInputSourceJoystickDigital || joyX != 0 || joyY != 0) {
+		if (usedJoystick)
+			*usedJoystick = true;
+
+		inputX = joyX * 127;
+		inputY = joyY * 127;
+
+		if (_optControlsYFlip)
+			inputY = -inputY;
+
+		return;
+	}
+
 	int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
 	int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
 	const int16 rawX = (int16)(logicalX << 1);
@@ -958,9 +1018,29 @@ void InsaneRebel1::updateTurretPhysics() {
 		// not raw mouse coordinates.
 		int16 inputX = 0;
 		int16 inputY = 0;
-		preprocessMouseAxes(inputX, inputY);
+		bool usedJoystick = false;
+		preprocessMouseAxes(inputX, inputY, &usedJoystick);
 		inputX = CLIP<int16>(inputX, -127, 127);
 		inputY = CLIP<int16>(inputY, -127, 127);
+		const int16 rawInputX = inputX;
+		const int16 rawInputY = inputY;
+
+		if (usedJoystick) {
+			// First-person turret/cockpit stages are noticeably more sensitive on
+			// joystick than on mouse, so damp only the joystick-driven input here.
+			inputX /= 2;
+			inputY /= 2;
+		}
+
+		debug("RA1 turret input: source=%s mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d mode=%d opcode=0x%X",
+			usedJoystick ? "joystick-actions" : "mouse-path",
+			_vm->_mouse.x, _vm->_mouse.y,
+			_vm->getActionState(kScummActionInsaneLeft),
+			_vm->getActionState(kScummActionInsaneRight),
+			_vm->getActionState(kScummActionInsaneUp),
+			_vm->getActionState(kScummActionInsaneDown),
+			rawInputX, rawInputY, inputX, inputY,
+			_currentLevel, _flyControlMode, _activeGameOpcode);
 
 		_rollAccum += (_tuning.roll * (int32)inputX) >> 4;
 		_rollAccum = (_rollAccum * 3) >> 2;
@@ -1129,9 +1209,48 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
 	int16 inputX = 0;
 	int16 inputY = 0;
-	preprocessMouseAxes(inputX, inputY);
+	bool usedJoystick = false;
+	preprocessMouseAxes(inputX, inputY, &usedJoystick);
 	inputX = CLIP<int16>(inputX, -0xA0, 0xA0);
 	inputY = CLIP<int16>(inputY, -100, 100);
+	const int16 rawInputX = inputX;
+	const int16 rawInputY = inputY;
+	const bool op0BAnalogSmoothing = (_activeInputSource == kInputSourceJoystickAnalog);
+	const char *inputSourceName = "mouse-path";
+
+	if (_activeInputSource == kInputSourceJoystickAnalog)
+		inputSourceName = "joystick-analog";
+	else if (_activeInputSource == kInputSourceJoystickDigital)
+		inputSourceName = "joystick-dpad";
+
+	if (usedJoystick) {
+		// The 0x0B first-person handler is shared by multiple RA1 stages. Smooth
+		// analog stick input over time so these sections keep full reach without
+		// feeling hyper-sensitive, while leaving mouse behavior untouched.
+		if (op0BAnalogSmoothing) {
+			inputX = smoothRebel1Op0BAnalogInput(inputX, _level2JoystickFilteredX, 127);
+			inputY = smoothRebel1Op0BAnalogInput(inputY, _level2JoystickFilteredY, 100);
+		} else {
+			_level2JoystickFilteredX = 0;
+			_level2JoystickFilteredY = 0;
+			inputX /= 2;
+			inputY /= 2;
+		}
+	} else {
+		_level2JoystickFilteredX = 0;
+		_level2JoystickFilteredY = 0;
+	}
+
+	debug("RA1 asteroid input: source=%s axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+		inputSourceName,
+		_joystickAxisX, _joystickAxisY,
+		_vm->_mouse.x, _vm->_mouse.y,
+		_vm->getActionState(kScummActionInsaneLeft),
+		_vm->getActionState(kScummActionInsaneRight),
+		_vm->getActionState(kScummActionInsaneUp),
+		_vm->getActionState(kScummActionInsaneDown),
+		rawInputX, rawInputY, inputX, inputY,
+		_currentLevel, _activeGameOpcode);
 
 	for (int i = kInputHistorySize - 1; i > 0; i--) {
 		_inputHistoryX[i] = _inputHistoryX[i - 1];
@@ -1399,6 +1518,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_mousePrevBiasY = 0;
 		_mouseBiasLatch = false;
 		_mouseRecentering = false;
+		_level2JoystickFilteredX = 0;
+		_level2JoystickFilteredY = 0;
 
 		// Shooting/targeting reset
 		_playerFired = false;
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index bcb9da88cda..0723a76928a 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -37,6 +37,148 @@ static const int kRA1LevelSelectRowsPerCol = 8;
 static const int kRA1NumLevels = 15;
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+	if (event.type == Common::EVENT_MOUSEMOVE && !_mouseRecentering) {
+		_activeInputSource = kInputSourceMouse;
+	}
+
+	if (event.type == Common::EVENT_CUSTOM_BACKEND_ACTION_AXIS) {
+		_activeInputSource = kInputSourceJoystickAnalog;
+
+		switch (event.customType) {
+		case kScummBackendActionRebel1AxisUp:
+			if (event.joystick.position == 0 && _joystickAxisY < 0)
+				return true;
+			_joystickAxisY = event.joystick.position;
+			return true;
+		case kScummBackendActionRebel1AxisDown:
+			if (event.joystick.position == 0 && _joystickAxisY > 0)
+				return true;
+			_joystickAxisY = -event.joystick.position;
+			return true;
+		case kScummBackendActionRebel1AxisLeft:
+			if (event.joystick.position == 0 && _joystickAxisX > 0)
+				return true;
+			_joystickAxisX = -event.joystick.position;
+			return true;
+		case kScummBackendActionRebel1AxisRight:
+			if (event.joystick.position == 0 && _joystickAxisX < 0)
+				return true;
+			_joystickAxisX = event.joystick.position;
+			return true;
+		default:
+			break;
+		}
+	}
+
+	if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START ||
+		event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
+		const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+
+		if (pressed &&
+			(event.customType == kScummActionInsaneUp ||
+			 event.customType == kScummActionInsaneDown ||
+			 event.customType == kScummActionInsaneLeft ||
+			 event.customType == kScummActionInsaneRight)) {
+			_activeInputSource = kInputSourceJoystickDigital;
+		}
+
+		if (_highScoresActive && pressed && event.customType == kScummActionInsaneAttack) {
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		}
+
+		if (_menuActive && !_highScoresActive && pressed) {
+			if (_levelSelectActive) {
+				int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
+				int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
+
+				switch (event.customType) {
+				case kScummActionInsaneUp:
+					row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+					_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+					return true;
+				case kScummActionInsaneDown:
+					row = (row + 1) % kRA1LevelSelectRowsPerCol;
+					_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+					return true;
+				case kScummActionInsaneLeft:
+					if (col > 0)
+						_levelSelectSel -= kRA1LevelSelectRowsPerCol;
+					return true;
+				case kScummActionInsaneRight:
+					if (col < 1)
+						_levelSelectSel += kRA1LevelSelectRowsPerCol;
+					return true;
+				case kScummActionInsaneAttack:
+					_menuConfirmed = true;
+					_vm->_smushVideoShouldFinish = true;
+					return true;
+				default:
+					break;
+				}
+			}
+
+			if (_optionsActive) {
+				switch (event.customType) {
+				case kScummActionInsaneUp:
+					_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
+					return true;
+				case kScummActionInsaneDown:
+					_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
+					return true;
+				case kScummActionInsaneLeft:
+					if (_optionsSel == 6) {
+						_optVolume = MAX(0, _optVolume - 5);
+						_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+							(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+						ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+						ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+						ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+					}
+					return true;
+				case kScummActionInsaneRight:
+					if (_optionsSel == 6) {
+						_optVolume = MIN(127, _optVolume + 5);
+						_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+							(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
+						ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
+						ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
+						ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
+					}
+					return true;
+				case kScummActionInsaneAttack:
+					_menuConfirmed = true;
+					_vm->_smushVideoShouldFinish = true;
+					return true;
+				default:
+					break;
+				}
+			}
+
+			if (!_optionsActive && !_levelSelectActive) {
+				switch (event.customType) {
+				case kScummActionInsaneUp:
+					_menuSelection = (_menuSelection + 4) % 5;
+					return true;
+				case kScummActionInsaneDown:
+					_menuSelection = (_menuSelection + 1) % 5;
+					return true;
+				case kScummActionInsaneAttack:
+					_menuConfirmed = true;
+					_vm->_smushVideoShouldFinish = true;
+					return true;
+				default:
+					break;
+				}
+			}
+		}
+
+		if (_interactiveVideoActive && !_menuActive && event.customType == kScummActionInsaneAttack) {
+			_playerFired = pressed;
+			return true;
+		}
+	}
+
 	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
 		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
 		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
diff --git a/engines/scumm/insane/insane_rebel2.cpp b/engines/scumm/insane/insane_rebel2.cpp
index afa0f3ddfcb..ad54fabdcc7 100644
--- a/engines/scumm/insane/insane_rebel2.cpp
+++ b/engines/scumm/insane/insane_rebel2.cpp
@@ -546,9 +546,84 @@ InsaneRebel2::~InsaneRebel2() {
 // Handles ESC (skip) and SPACE (pause) regardless of menu state.
 // Pause behavior matches original FUN_405A21: SPACE pauses, ANY key unpauses.
 bool InsaneRebel2::notifyEvent(const Common::Event &event) {
-	if (event.type == Common::EVENT_KEYDOWN) {
-		SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START ||
+		event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
+		const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+
+		if (pressed && splayer && splayer->_paused && _gameState == kStateGameplay) {
+			debug("Rebel2: Joystick action while paused - unpausing");
+			if (_pauseOverlayActive) {
+				_vm->_system->getPaletteManager()->setPalette(_savedPausePalette, 0, 256);
+				_pauseOverlayActive = false;
+			}
+			splayer->unpause();
+			return true;
+		}
+
+		if (_menuInputActive && pressed) {
+			Common::KeyCode keycode = Common::KEYCODE_INVALID;
+
+			switch (_gameState) {
+			case kStateMainMenu:
+			case kStatePilotSelect:
+			case kStateDifficultySelect:
+			case kStateChapterSelect:
+			case kStateOptions:
+				switch (event.customType) {
+				case kScummActionInsaneUp:
+					keycode = Common::KEYCODE_UP;
+					break;
+				case kScummActionInsaneDown:
+					keycode = Common::KEYCODE_DOWN;
+					break;
+				case kScummActionInsaneLeft:
+					keycode = Common::KEYCODE_LEFT;
+					break;
+				case kScummActionInsaneRight:
+					keycode = Common::KEYCODE_RIGHT;
+					break;
+				case kScummActionInsaneAttack:
+					keycode = Common::KEYCODE_RETURN;
+					break;
+				case kScummActionInsaneSwitch:
+					keycode = Common::KEYCODE_ESCAPE;
+					break;
+				default:
+					break;
+				}
+				break;
+
+			case kStateTopPilots:
+				if (event.customType == kScummActionInsaneAttack)
+					keycode = Common::KEYCODE_RETURN;
+				else if (event.customType == kScummActionInsaneSwitch)
+					keycode = Common::KEYCODE_ESCAPE;
+				break;
+
+			default:
+				break;
+			}
 
+			if (keycode != Common::KEYCODE_INVALID) {
+				Common::Event syntheticEvent = Common::Event();
+				syntheticEvent.type = Common::EVENT_KEYDOWN;
+				syntheticEvent.kbd.keycode = keycode;
+				syntheticEvent.kbd.ascii = (keycode == Common::KEYCODE_RETURN) ? '\r' :
+					(keycode == Common::KEYCODE_ESCAPE) ? Common::ASCII_ESCAPE : 0;
+				_menuEventQueue.push(syntheticEvent);
+				return true;
+			}
+		}
+
+		if (event.customType == kScummActionInsaneAttack ||
+			event.customType == kScummActionInsaneSwitch) {
+			return true;
+		}
+	}
+
+	if (event.type == Common::EVENT_KEYDOWN) {
 		// When paused during gameplay, ANY key unpauses (FUN_405A21 line 360-365).
 		// ESC additionally opens the ScummVM menu (original: quit key exits level).
 		if (splayer && splayer->_paused && _gameState == kStateGameplay) {
@@ -1068,6 +1143,10 @@ int32 InsaneRebel2::processMouse() {
 	// Get button state directly from event manager (SCUMM VARs aren't updated during SMUSH)
 	// Bit 0 = left button, Bit 1 = right button, Bit 2 = middle button
 	uint32 currentButtons = _vm->_system->getEventManager()->getButtonState();
+	if (_vm->getActionState(kScummActionInsaneAttack))
+		currentButtons |= 1;
+	if (_vm->getActionState(kScummActionInsaneSwitch))
+		currentButtons |= 2;
 
 	// Edge detection for buttons
 	bool leftPressed = (currentButtons & 1) != 0;
@@ -1108,7 +1187,7 @@ int32 InsaneRebel2::processMouse() {
 	// - Other gameplay handlers fire while button is held; slot counters still rate-limit.
 	bool triggerShot = (_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed;
 	if (triggerShot && isShootingAllowed()) {
-		Common::Point mousePos(_vm->_mouse.x, _vm->_mouse.y);
+		Common::Point mousePos = getGameplayAimPoint();
 		debug("Rebel2 Click: Mouse=(%d,%d) Enemies=%d",
 			mousePos.x, mousePos.y, _enemies.size());
 
@@ -1244,6 +1323,32 @@ int32 InsaneRebel2::processMouse() {
 	return buttons;
 }
 
+Common::Point InsaneRebel2::getGameplayAimPoint() {
+	Common::Point aimPos(_vm->_mouse.x, _vm->_mouse.y);
+
+	if (_menuInputActive || _gameState != kStateGameplay)
+		return aimPos;
+
+	int dx = 0;
+	int dy = 0;
+
+	if (_vm->getActionState(kScummActionInsaneLeft))
+		dx--;
+	if (_vm->getActionState(kScummActionInsaneRight))
+		dx++;
+	if (_vm->getActionState(kScummActionInsaneUp))
+		dy--;
+	if (_vm->getActionState(kScummActionInsaneDown))
+		dy++;
+
+	if (dx || dy) {
+		aimPos.x = (dx < 0) ? 0 : (dx > 0) ? 319 : 160;
+		aimPos.y = (dy < 0) ? 0 : (dy > 0) ? 199 : 100;
+	}
+
+	return aimPos;
+}
+
 bool InsaneRebel2::isBitSet(int n) {
 	// FUN_00423970: When param_1 < 1 (0 or negative), the bounds check fails and returns false.
 	// This means ID 0 or negative IDs are always treated as "enabled" (not skipped).
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/insane_rebel2.h
index a8fac1ae4d5..f123efb5bb7 100644
--- a/engines/scumm/insane/insane_rebel2.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -483,6 +483,7 @@ public:
 	
 
 	int32 processMouse() override;
+	Common::Point getGameplayAimPoint();
 	bool isBitSet(int n) override;
 	void setBit(int n) override;
 
diff --git a/engines/scumm/insane/insane_rebel2_iact.cpp b/engines/scumm/insane/insane_rebel2_iact.cpp
index 28552eb5f28..161c89192f3 100644
--- a/engines/scumm/insane/insane_rebel2_iact.cpp
+++ b/engines/scumm/insane/insane_rebel2_iact.cpp
@@ -651,10 +651,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			// local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
 			// local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
 
-			// Map mouse position (-127 to 127 range) to ship target
-			// Mouse is 0-320, center is 160. Map to -127 to 127 range
-			int16 mouseOffsetX = (int16)((_vm->_mouse.x - 160) * 127 / 160);
-			int16 mouseOffsetY = (int16)((_vm->_mouse.y - 100) * 127 / 100);
+			// Map the effective aim position (-127 to 127 range) to the ship target.
+			Common::Point aimPos = getGameplayAimPoint();
+			int16 mouseOffsetX = (int16)((aimPos.x - 160) * 127 / 160);
+			int16 mouseOffsetY = (int16)((aimPos.y - 100) * 127 / 100);
 
 			// Clamp X offset to movement range limit (covered/shooting state)
 			// Based on FUN_00401234 lines 119-136
@@ -698,8 +698,8 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 			// Calculate ship direction indices for sprite selection
 			// Map mouse position to 5x7 direction grid (like Handler 7)
-			int16 mouseX = _vm->_mouse.x;
-			int16 mouseY = _vm->_mouse.y;
+			int16 mouseX = aimPos.x;
+			int16 mouseY = aimPos.y;
 
 			// Scale mouse if video is larger than 320x200
 			if (_player && _player->_width > 320) {
@@ -740,12 +740,13 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
 		}
 
-		// Update firing state from mouse button
+		// Update firing state from mouse button or joystick fire action
 		// Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
 		if (_shipLevelMode == 4) {
 			_shipFiring = false;
 		} else {
-			_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+			_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+				_vm->getActionState(kScummActionInsaneAttack);
 		}
 
 		debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
@@ -805,8 +806,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
 		// _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
 		// consistent with handler 8 which uses _vm->_mouse.x directly.
-		int16 inputX = (int16)(_vm->_mouse.x - 160);  // DAT_0047a7e0
-		int16 inputY = (int16)(_vm->_mouse.y - 100);  // DAT_0047a7e2
+		Common::Point aimPos = getGameplayAimPoint();
+		int16 inputX = (int16)(aimPos.x - 160);  // DAT_0047a7e0
+		int16 inputY = (int16)(aimPos.y - 100);  // DAT_0047a7e2
 
 		// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
 		if (inputX > 160)
@@ -1038,7 +1040,9 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		if (_shipDirectionIndex > 34)
 			_shipDirectionIndex = 34;
 
-		_shipFiring = (_flyControlMode == 2) && (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0);
+		_shipFiring = (_flyControlMode == 2) &&
+			((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+			 _vm->getActionState(kScummActionInsaneAttack));
 
 		debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
 			_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
@@ -1151,7 +1155,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			if (_rebelDamageLevel == 5) {
 				// At max damage, check for direction change input
 				// For now, use mouse X position to determine direction
-				int16 mouseX = _vm->_mouse.x;
+				int16 mouseX = getGameplayAimPoint().x;
 				if (_player && _player->_width > 320) {
 					mouseX = (mouseX * 320) / _player->_width;
 				}
@@ -1371,10 +1375,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		if (_rebelDamageLevel == 5) {
 			// Check for joystick/key input to change direction
 			// Simplified: use mouse position
-			if (_vm->_mouse.x > 75) {
+			int16 mouseX = getGameplayAimPoint().x;
+			if (mouseX > 235) {
 				_rebelFlightDir = 1;
 			}
-			if (_vm->_mouse.x < -75) {
+			if (mouseX < 85) {
 				_rebelFlightDir = 0;
 			}
 		}
diff --git a/engines/scumm/insane/insane_rebel2_menu.cpp b/engines/scumm/insane/insane_rebel2_menu.cpp
index 9ffbfa8ce55..15151d30504 100644
--- a/engines/scumm/insane/insane_rebel2_menu.cpp
+++ b/engines/scumm/insane/insane_rebel2_menu.cpp
@@ -755,9 +755,9 @@ int InsaneRebel2::runMainMenu() {
 			break;
 
 		case 2:  // Calibrate Joystick
-			debug("Rebel2: Calibrate Joystick selected");
-			// TODO: Implement joystick calibration (FUN_00425820)
-			// Plays O_CALIB.SAN with joystick calibration prompts
+			debug("Rebel2: Calibrate Joystick selected - no-op for modern joystick support");
+			// Modern controller support uses live keymapper actions; no explicit
+			// joystick calibration flow is required here.
 			break;
 
 		case 3:  // Continue Intro -> replay intro videos
@@ -1594,9 +1594,6 @@ int InsaneRebel2::processLevelSelectInput() {
 	int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
 	int itemCount = isDifficultyMode ? 6 : _levelItemCount;
 
-	// Mouse hit Y positions — must match drawMenuItems() formula
-	const int baseY = itemCount * -5 + 0x68;
-
 	while (!_menuEventQueue.empty()) {
 		Common::Event event = _menuEventQueue.pop();
 		switch (event.type) {
diff --git a/engines/scumm/insane/insane_rebel2_render.cpp b/engines/scumm/insane/insane_rebel2_render.cpp
index 77a07000f1d..72ad585eed4 100644
--- a/engines/scumm/insane/insane_rebel2_render.cpp
+++ b/engines/scumm/insane/insane_rebel2_render.cpp
@@ -1723,8 +1723,9 @@ void InsaneRebel2::checkCollisionZones() {
 	// Calculate aim position in centered coordinates.
 	// Original: local_10 = mouseOffset + 0xa0, then smoothed and clamped to [-0x34..0x34]
 	// Simplified mapping: mouse 0..320 → [-52..52], mouse 0..200 → [-45..45]
-	int16 aimX = (int16)((_vm->_mouse.x - 160) * 52 / 160);
-	int16 aimY = (int16)((100 - _vm->_mouse.y) * 45 / 100);
+	Common::Point aimPos = getGameplayAimPoint();
+	int16 aimX = (int16)((aimPos.x - 160) * 52 / 160);
+	int16 aimY = (int16)((100 - aimPos.y) * 45 / 100);
 
 	// Clamp to original ranges (DAT_0047a7fc < 1 path)
 	if (aimX > 0x34)
@@ -2233,8 +2234,9 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		maxScrollY = 0;
 	
 	// Simple linear mapping: Center of screen corresponds to center of buffer
-	_viewX = (_vm->_mouse.x * maxScrollX) / _vm->_screenWidth;
-	_viewY = (_vm->_mouse.y * maxScrollY) / _vm->_screenHeight;
+	Common::Point aimPos = getGameplayAimPoint();
+	_viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
+	_viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
 	
 	_player->setScrollOffset(_viewX, _viewY);
 
@@ -2839,8 +2841,9 @@ void InsaneRebel2::renderTurretHudOverlays(byte *renderBitmap, int pitch, int wi
 		return;
 
 	// Calculate mouse offset (clamped to -127..127)
-	int mouseOffsetX = (_vm->_mouse.x - 160);
-	int mouseOffsetY = (_vm->_mouse.y - 100);
+	Common::Point aimPos = getGameplayAimPoint();
+	int mouseOffsetX = (aimPos.x - 160);
+	int mouseOffsetY = (aimPos.y - 100);
 	if (mouseOffsetX > 127)
 		mouseOffsetX = 127;
 	if (mouseOffsetX < -127)
@@ -3451,8 +3454,9 @@ void InsaneRebel2::renderHandler25Ship(byte *renderBitmap, int pitch, int width,
 			int16 areaBottom = (_corridorBottomY > 0) ? _corridorBottomY : 180;
 
 			// Get crosshair position (using mouse position scaled to game coords)
-			int16 crosshairX = _vm->_mouse.x;
-			int16 crosshairY = _vm->_mouse.y;
+			Common::Point aimPos = getGameplayAimPoint();
+			int16 crosshairX = aimPos.x;
+			int16 crosshairY = aimPos.y;
 			if (_player && _player->_width > 320) {
 				crosshairX = (crosshairX * 320) / _player->_width;
 				crosshairY = (crosshairY * 200) / _player->_height;
@@ -4220,7 +4224,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 	// Update target lock state and draw crosshair/reticle
 
 	// Target lock detection (DAT_00443676 equivalent)
-	Common::Point worldMousePos(_vm->_mouse.x + _viewX, _vm->_mouse.y + _viewY);
+	Common::Point aimPos = getGameplayAimPoint();
+	Common::Point worldMousePos(aimPos.x + _viewX, aimPos.y + _viewY);
 	bool targetLocked = false;
 
 	for (Common::List<enemy>::iterator it = _enemies.begin(); it != _enemies.end(); ++it) {
@@ -4270,8 +4275,8 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		int ch = _smush_iconsNut->getCharHeight(reticleIndex);
 
 		// Calculate crosshair position
-		int crosshairX = _vm->_mouse.x - cw / 2 + _viewX;
-		int crosshairY = _vm->_mouse.y - ch / 2 + _viewY;
+		int crosshairX = aimPos.x - cw / 2 + _viewX;
+		int crosshairY = aimPos.y - ch / 2 + _viewY;
 
 		// Handler 25 (0x19): Add view offset to crosshair position
 		// From FUN_41DB5E lines 198-199: X = DAT_00457914 + DAT_0045790c, Y = DAT_00457916 + DAT_0045790e
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 0d9479e420b..5cb8d9c1b88 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1102,6 +1102,110 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
 		keymaps.push_back(insaneKeymap);
 	}
 
+	if (gameId == "rebel1") {
+		Keymap *rebel1Keymap = new Keymap(Keymap::kKeymapTypeGame, "scumm-rebel1", _("Rebel Assault controls"));
+
+		act = new Action("RA1UP", _("Aim up / menu up"));
+		act->setCustomEngineActionEvent(kScummActionInsaneUp);
+		act->addDefaultInputMapping("JOY_UP");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1DOWN", _("Aim down / menu down"));
+		act->setCustomEngineActionEvent(kScummActionInsaneDown);
+		act->addDefaultInputMapping("JOY_DOWN");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1LEFT", _("Aim left / menu left"));
+		act->setCustomEngineActionEvent(kScummActionInsaneLeft);
+		act->addDefaultInputMapping("JOY_LEFT");
+		rebel1Keymap->addAction(act);
+
+			act = new Action("RA1RIGHT", _("Aim right / menu right"));
+			act->setCustomEngineActionEvent(kScummActionInsaneRight);
+			act->addDefaultInputMapping("JOY_RIGHT");
+			rebel1Keymap->addAction(act);
+
+			act = new Action("RA1STICKUP", _("Stick up"));
+			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
+			act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+			rebel1Keymap->addAction(act);
+
+			act = new Action("RA1STICKDOWN", _("Stick down"));
+			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
+			act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+			rebel1Keymap->addAction(act);
+
+			act = new Action("RA1STICKLEFT", _("Stick left"));
+			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
+			act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+			rebel1Keymap->addAction(act);
+
+			act = new Action("RA1STICKRIGHT", _("Stick right"));
+			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
+			act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+			rebel1Keymap->addAction(act);
+
+			act = new Action("RA1FIRE", _("Fire / select"));
+			act->setCustomEngineActionEvent(kScummActionInsaneAttack);
+			act->addDefaultInputMapping("JOY_A");
+			rebel1Keymap->addAction(act);
+
+		act = new Action("RA1BACK", _("Back / skip"));
+		act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));
+		act->addDefaultInputMapping("JOY_B");
+		act->addDefaultInputMapping("JOY_Y");
+		act->addDefaultInputMapping("JOY_START");
+		rebel1Keymap->addAction(act);
+
+		keymaps.push_back(rebel1Keymap);
+	}
+
+	if (gameId == "rebel2") {
+		Keymap *rebel2Keymap = new Keymap(Keymap::kKeymapTypeGame, "scumm-rebel2", _("Rebel Assault II controls"));
+
+		act = new Action("RA2UP", _("Aim up / menu up"));
+		act->setCustomEngineActionEvent(kScummActionInsaneUp);
+		act->addDefaultInputMapping("JOY_UP");
+		act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2DOWN", _("Aim down / menu down"));
+		act->setCustomEngineActionEvent(kScummActionInsaneDown);
+		act->addDefaultInputMapping("JOY_DOWN");
+		act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2LEFT", _("Aim left / menu left"));
+		act->setCustomEngineActionEvent(kScummActionInsaneLeft);
+		act->addDefaultInputMapping("JOY_LEFT");
+		act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2RIGHT", _("Aim right / menu right"));
+		act->setCustomEngineActionEvent(kScummActionInsaneRight);
+		act->addDefaultInputMapping("JOY_RIGHT");
+		act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2FIRE", _("Fire / select"));
+		act->setCustomEngineActionEvent(kScummActionInsaneAttack);
+		act->addDefaultInputMapping("JOY_A");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2COVER", _("Cover / back"));
+		act->setCustomEngineActionEvent(kScummActionInsaneSwitch);
+		act->addDefaultInputMapping("JOY_B");
+		rebel2Keymap->addAction(act);
+
+		act = new Action("RA2BACK", _("Skip / menu"));
+		act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));
+		act->addDefaultInputMapping("JOY_Y");
+		act->addDefaultInputMapping("JOY_START");
+		rebel2Keymap->addAction(act);
+
+		keymaps.push_back(rebel2Keymap);
+	}
+
 	return keymaps;
 }
 
diff --git a/engines/scumm/scumm.h b/engines/scumm/scumm.h
index 928cebd9349..4a7b88a8421 100644
--- a/engines/scumm/scumm.h
+++ b/engines/scumm/scumm.h
@@ -503,6 +503,13 @@ enum ScummAction {
 	kScummActionCount
 };
 
+enum ScummBackendAction {
+	kScummBackendActionRebel1AxisUp = 11000,
+	kScummBackendActionRebel1AxisDown,
+	kScummBackendActionRebel1AxisLeft,
+	kScummBackendActionRebel1AxisRight
+};
+
 extern const char *const insaneKeymapId;
 
 /**


Commit: 51052ab299bf2f63bf0eb95b78704875173fcf2a
    https://github.com/scummvm/scummvm/commit/51052ab299bf2f63bf0eb95b78704875173fcf2a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:43+02:00

Commit Message:
SCUMM: RA1: Improve torpedo handling

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 338aee4ccc6..d1b865ab1f6 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -160,6 +160,7 @@ private:
 					  int x, int y, const RA1Sprite &sprite);
 	void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
 	void applyFrameObjectHitState(int16 targetIdx);
+	bool isFrameObjectPrimarySet(int16 objectId) const;
 
 	// Shooting pipeline — FUN_1CCA0 (0x1CCA0) shot spawner,
 	// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
@@ -463,8 +464,9 @@ private:
 	int16 _shieldGenHitsA;   // Hits on _protectedTargetA
 	int16 _shieldGenHitsB;   // Hits on _protectedTargetB
 
-	// Torpedo fired flag — set when torpedo hits in Phase 2 (Level 15)
-	bool _torpedoFired;      // 0x7602 bit 1 in original
+	// Level 15 torpedo success latch. The original derives this from
+	// g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
+	bool _torpedoFired;
 
 	// Level 8 walker-specific state — RunLevel8Flow (0x18546)
 	int16 _walkerHealth;     // Walker health percentage (0-100), init=100
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index c5069953a82..59e02601943 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -410,6 +410,19 @@ void InsaneRebel1::applyFrameObjectHitState(int16 targetIdx) {
 		_frameObjectState[altIndex] &= ~bit;
 }
 
+bool InsaneRebel1::isFrameObjectPrimarySet(int16 objectId) const {
+	if (objectId <= 0)
+		return false;
+
+	const int bitIndex = objectId - 1;
+	const int byteIndex = bitIndex >> 3;
+	if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+		return false;
+
+	const byte bit = (byte)(0x80 >> (bitIndex & 7));
+	return (_frameObjectState[byteIndex] & bit) != 0;
+}
+
 bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
 		int codec, uint8 &ra1Param) {
 	if (!_interactiveVideoActive)
@@ -1296,9 +1309,14 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	_frameCounter++;
 
-	// Level 4 Phase 2: enable torpedo mode at frame 0x3E
-	if (_currentLevel == 3 && _levelGameplayPhase == 2 && _frameCounter == 0x3E)
-		_gameplayFlags75ff |= 2;
+	// Level 4 Phase 2: enable torpedo mode at frame 0x3E and finish as
+	// soon as the torpedo registers a hit. The DOS loop exits on killCount.
+	if (_currentLevel == 3 && _levelGameplayPhase == 2) {
+		if (_frameCounter == 0x3E)
+			_gameplayFlags75ff |= 2;
+		if (_killCount > 0)
+			_vm->_smushVideoShouldFinish = true;
+	}
 
 	// Level 4 Phase 1: track shield generator hits per frame.
 	// Original (RunLevel4Flow): g_recentKillObjectIdPlus1 checked every frame.
@@ -1324,12 +1342,16 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		}
 	}
 
-	// Level 15 Phase 2: enable torpedo and end on hit.
-	// Original (RunLevel1GameLoop): torpedo at frame 0x18A, ends when DAT_00007602 & 2.
+	// Level 15 Phase 2: enable torpedo at frame 0x18A, expose the protected
+	// target IDs used by the original flow, and finish when object-state bit
+	// 0x7602 & 2 becomes set.
 	if (_currentLevel == 14 && _levelGameplayPhase == 2) {
-		if (_frameCounter == 0x18A)
+		if (_frameCounter == 0x18A) {
 			_gameplayFlags75ff |= 2;
-		if (_killCount > 0) {
+			_protectedTargetA = 0x67;
+			_protectedTargetB = 0x69;
+		}
+		if (isFrameObjectPrimarySet(7)) {
 			_torpedoFired = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
@@ -1775,7 +1797,7 @@ void InsaneRebel1::processShot() {
 	_shotSlots[slot].centerY = originY;
 	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
-	playSfx(kSfxLaserShot, 127, 0);
+	playSfx((_gameplayFlags75ff & 0x2) ? kSfxAlert : kSfxLaserShot, 127, 0);
 
 	debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
 		_shotSlots[slot].posX, _shotSlots[slot].posY, originX, originY);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 49283a5e2a4..23b287561fc 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -542,6 +542,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
 // The original does not draw a hardcoded pixel cross; it renders glyph markers
 // whose state depends on _targetProximity.
 void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
+	static const char kRA1TorpedoIndicator[] = "<d";
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const int overlayY = ra1OverlayViewOffsetY(this);
@@ -569,6 +570,12 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 		int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
 		int cursorY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
 		drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
+
+		if (altMarkerSet) {
+			const int indicatorWidth = getFontBankStringWidth(kRA1TorpedoIndicator);
+			drawFontBankString(dst, pitch, width, height,
+				overlayX + 0xA0 - indicatorWidth / 2, overlayY + 0x6E, kRA1TorpedoIndicator);
+		}
 	}
 
 	// Save previous proximity for next frame
@@ -650,7 +657,11 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 
 // renderLaserShots — FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
-	if (_laserBank.numSprites <= 0)
+	static const char kRA1TorpedoTrailLeft[] = "<<&";
+	static const char kRA1TorpedoTrailRight[] = "<<'";
+	const bool torpedoMode = (_gameplayFlags75ff & 0x2) != 0;
+
+	if (_laserBank.numSprites <= 0 && !torpedoMode)
 		return;
 
 	// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
@@ -763,6 +774,19 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 				continue;
 			}
 
+			if (torpedoMode) {
+				const int trailY = overlayY + _shipPosY;
+				const int leftTrailX = overlayX + _shipPosX - 0x14 - (timer << 3);
+				const int rightTrailX = overlayX + _shipPosX + 0x14 + (timer << 3);
+				const int leftWidth = getFontBankStringWidth(kRA1TorpedoTrailLeft);
+				const int rightWidth = getFontBankStringWidth(kRA1TorpedoTrailRight);
+				drawFontBankString(dst, pitch, width, height,
+					leftTrailX - leftWidth / 2, trailY, kRA1TorpedoTrailLeft);
+				drawFontBankString(dst, pitch, width, height,
+					rightTrailX - rightWidth / 2, trailY, kRA1TorpedoTrailRight);
+				continue;
+			}
+
 			// Fallback for non-turret handlers that still run shot overlays.
 			int leftStartY = overlayY + 0x96;
 			int rightStartY = overlayY + 0x96;
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 6e88dd22332..a089917c017 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -393,8 +393,8 @@ bool InsaneRebel1::runLevel4() {
 			return false;
 
 		if (_health >= 0) {
-			// Phase 2: Torpedo run — torpedo enabled at frame 0x3E by IACT handler.
-			// killCount > 0 = torpedo hit, killCount == 0 = missed.
+			// Phase 2: torpedo run. The DOS loop enables torpedo mode at frame
+			// 0x3E and exits early as soon as killCount becomes nonzero.
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
@@ -1466,9 +1466,9 @@ bool InsaneRebel1::runLevel15() {
 			if (_vm->shouldQuit())
 				return false;
 
-			// Phase 2: final approach and torpedo shot.
-			// Original: torpedo enabled at frame 0x18A via _gameplayFlags75ff |= 2.
-			// _torpedoFired set by IACT handler when killCount > 0 (torpedo hits exhaust port).
+			// Phase 2: final approach and torpedo shot. The DOS flow enables
+			// torpedo mode at frame 0x18A and completes only after object-state
+			// bit 0x7602 & 2 is set by the exhaust-port hit.
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
@@ -1482,6 +1482,11 @@ bool InsaneRebel1::runLevel15() {
 				return false;
 		}
 
+		if (_health >= 0 && !_torpedoFired) {
+			debug(1, "InsaneRebel1: Level 15 torpedo run ended without exhaust-port hit");
+			return false;
+		}
+
 		if (_health >= 0) {
 			playCinematic("LVL15/L15END1.ANM");
 			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);


Commit: ad7549ed3c6bebaa15fb347f84005f3789d57321
    https://github.com/scummvm/scummvm/commit/ad7549ed3c6bebaa15fb347f84005f3789d57321
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:43+02:00

Commit Message:
SCUMM: RA1: Improve Escape key handling

Changed paths:
    engines/scumm/insane/insane_rebel1_menu.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/insane_rebel1_menu.cpp
index 0723a76928a..7d3857c673c 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/insane_rebel1_menu.cpp
@@ -323,9 +323,29 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		return true;
 	}
 
-	if (event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
-		if (_player) {
-			debug("Rebel1: ESC pressed - skipping video");
+	if (event.type == Common::EVENT_KEYDOWN && _player) {
+		if (_interactiveVideoActive && !_menuActive &&
+			event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+			debug("Rebel1: ESC pressed during gameplay - opening ScummVM menu");
+			const bool wasPaused = _player->_paused;
+			if (!wasPaused)
+				_player->pause();
+			_vm->openMainMenuDialog();
+			if (!wasPaused)
+				_player->unpause();
+			return true;
+		}
+
+		if (_interactiveVideoActive && !_menuActive &&
+			event.kbd.keycode == Common::KEYCODE_s &&
+			event.kbd.hasFlags(Common::KBD_SHIFT)) {
+			debug("Rebel1: Shift+S pressed - skipping gameplay section");
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		}
+
+		if (!_interactiveVideoActive && event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+			debug("Rebel1: ESC pressed - skipping cinematic");
 			_vm->_smushVideoShouldFinish = true;
 			return true;
 		}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index a089917c017..44920dec6b3 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -47,6 +47,11 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
 	if (startFrame > 0)
 		splayer->setFastForwardToFrame(startFrame);
 	splayer->play(filename, 12);
+
+	// Level-title text is only meant for the intro cinematic that armed it.
+	// Clear it even when the movie ended through ESC, so it cannot leak into
+	// the next cutscene or gameplay segment.
+	_introTextActive = false;
 }
 
 void InsaneRebel1::clearVideoBuffer() {


Commit: 326181870b0deef0548380538ab06b0ec69ceb65
    https://github.com/scummvm/scummvm/commit/326181870b0deef0548380538ab06b0ec69ceb65
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:43+02:00

Commit Message:
SCUMM: RA1: Implement level 5

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1984745d429..9c77eb506a6 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -205,6 +205,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 
 	_shipPosX = kRA1CenterX;
 	_shipPosY = kRA1CenterY;
+	_flightAimX = kRA1CenterX;
+	_flightAimY = kRA1CenterY;
 	_shipDirIndex = 17;  // Center of 5x7 grid (2*7 + 3)
 
 	_corridorLeftX = kRA1MinX;
@@ -285,6 +287,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		_pendingRouteIndex = -1;
 		_levelRouteChoice = 0;
 		_levelGameplayPhase = 0;
+		_level5SuccessFramesRemaining = 0;
 		_menuActive = false;
 	_introTextActive = false;
 	_introTextStartFrame = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index d1b865ab1f6..c7e970975d9 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -86,6 +86,7 @@ public:
 	bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
 	int getCurrentLevel() const { return _currentLevel; }
 	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
+	uint16 getEffectiveGameOpcode() const;
 	bool hasFrameGameOpcode(uint16 opcode) const {
 		return opcode < 32 && (_frameGameOpcodeMask & (1u << opcode)) != 0;
 	}
@@ -93,6 +94,10 @@ public:
 	int16 getPerspectiveY() const { return _perspectiveY; }
 	void projectGameplayPoint(int16 &x, int16 &y) const;
 	void unprojectGameplayPoint(int16 &x, int16 &y) const;
+	int16 getGameplayCursorX() const;
+	int16 getGameplayCursorY() const;
+	void setGameplayCursor(int16 x, int16 y);
+	void updateFlightVariantCursor();
 	bool handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
 		int codec, uint8 &ra1Param);
 	void resetFrameObjectState();
@@ -201,6 +206,9 @@ private:
 	// Original: _DAT_74B6/_74B8 (base=160,100) + _DAT_74BA/_74BC (offset)
 	int16 _shipPosX;
 	int16 _shipPosY;
+	// GAME opcode 0x09 uses a separate aim cursor; other handlers target via _shipPos.
+	int16 _flightAimX;
+	int16 _flightAimY;
 
 	// Direction sprite index (5x7 grid = 35 sprites, vDir*7 + hDir)
 	int16 _shipDirIndex;
@@ -463,6 +471,7 @@ private:
 	// Per-target hit counters for shield generator tracking (Level 4)
 	int16 _shieldGenHitsA;   // Hits on _protectedTargetA
 	int16 _shieldGenHitsB;   // Hits on _protectedTargetB
+	int16 _level5SuccessFramesRemaining; // DOS RunLevel5Flow: 20-frame hold after the third kill
 
 	// Level 15 torpedo success latch. The original derives this from
 	// g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 59e02601943..8a809478938 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -44,6 +44,9 @@ static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filtere
 	return filteredValue;
 }
 
+static const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
+static const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
+
 // LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
 // (6..18), while the recovered outer loop compares the post-latch state against the
 // later translated values seen in the executable. Accept both representations.
@@ -579,6 +582,70 @@ void InsaneRebel1::unprojectGameplayPoint(int16 &x, int16 &y) const {
 	x = (int16)(x + _perspectiveX);
 }
 
+uint16 InsaneRebel1::getEffectiveGameOpcode() const {
+	if (hasFrameGameOpcode(0x1A))
+		return 0x1A;
+	if (hasFrameGameOpcode(0x19))
+		return 0x19;
+	if (hasFrameGameOpcode(0x0B))
+		return 0x0B;
+	if (hasFrameGameOpcode(0x0A))
+		return 0x0A;
+	if (hasFrameGameOpcode(0x09))
+		return 0x09;
+	if (hasFrameGameOpcode(0x08))
+		return 0x08;
+	if (hasFrameGameOpcode(0x07))
+		return 0x07;
+
+	return _activeGameOpcode;
+}
+
+int16 InsaneRebel1::getGameplayCursorX() const {
+	return (getEffectiveGameOpcode() == 0x09) ? _flightAimX : _shipPosX;
+}
+
+int16 InsaneRebel1::getGameplayCursorY() const {
+	return (getEffectiveGameOpcode() == 0x09) ? _flightAimY : _shipPosY;
+}
+
+void InsaneRebel1::setGameplayCursor(int16 x, int16 y) {
+	if (getEffectiveGameOpcode() == 0x09) {
+		_flightAimX = x;
+		_flightAimY = y;
+	} else {
+		_shipPosX = x;
+		_shipPosY = y;
+	}
+}
+
+void InsaneRebel1::updateFlightVariantCursor() {
+	if (getEffectiveGameOpcode() != 0x09)
+		return;
+
+	const int bucket = CLIP<int>(ABS(_rollAccum) >> 8, 0, ARRAYSIZE(kRA1Op09AimXScale) - 1);
+	int32 xScale = kRA1Op09AimXScale[bucket];
+	if (_rollAccum > 0)
+		xScale = -xScale;
+
+	// Assembly-verified 0x09 layout:
+	//   ship sprite center = (_74B6 + _74BA, _74B8 + _74BC)
+	//   cursor center      = (_74BE, _74C0)
+	// In ScummVM the flight sprite center already lives in _shipPos.
+	const int16 shipBaseX = _shipPosX;
+	const int16 shipBaseY = _shipPosY;
+	const int32 liftTerm = (int32)_liftSmooth - 0x0F;
+	_flightAimX = CLIP<int32>(shipBaseX + ((liftTerm * xScale) >> 8), kRA1MinX, kRA1MaxX);
+	_flightAimY = CLIP<int32>(shipBaseY + ((liftTerm * kRA1Op09AimYScale[bucket]) >> 8),
+		kRA1MinY, kRA1MaxY);
+
+	if (_currentLevel == 4) {
+		debug(1, "RA1 op09 cursor: frame=%d shipBase=(%d,%d) shipPos=(%d,%d) aim=(%d,%d) roll=%d lift=%d bucket=%d dir=%d persp=(%d,%d)",
+			_gameCounter, shipBaseX, shipBaseY, _shipPosX, _shipPosY, _flightAimX, _flightAimY,
+			_rollAccum, _liftSmooth, bucket, _shipDirIndex, _perspectiveX, _perspectiveY);
+	}
+}
+
 // preprocessMouseAxes — FUN_231BE (0x231BE) centered-axis output law, adapted to
 // ScummVM's absolute 320x200 mouse space.
 // Preserve the DOS bias/offset persistence and one-frame jump latch from
@@ -736,6 +803,8 @@ void InsaneRebel1::updateShipPhysics() {
 		_liftSmooth = 0;
 		_shipPosX = kRA1CenterX;
 		_shipPosY = kRA1CenterY;
+		_flightAimX = kRA1CenterX;
+		_flightAimY = kRA1CenterY;
 		_damageFlags = 0;
 		_prevDamageFlags = 0;
 		_damageCooldown = 0;
@@ -1342,6 +1411,16 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		}
 	}
 
+	// Level 5 Phase 1: DOS RunLevel5Flow exits L5PLAY only after killCount stays
+	// above 2 for 20 frontend frames. That countdown is carried by the runlevel,
+	// not by opcode 0x07 itself.
+	if (_currentLevel == 4 && _levelGameplayPhase == 1 &&
+		_level5SuccessFramesRemaining > 0 && _killCount > 2 && !_vm->_smushVideoShouldFinish) {
+		_level5SuccessFramesRemaining--;
+		if (_level5SuccessFramesRemaining == 0)
+			_vm->_smushVideoShouldFinish = true;
+	}
+
 	// Level 15 Phase 2: enable torpedo at frame 0x18A, expose the protected
 	// target IDs used by the original flow, and finish when object-state bit
 	// 0x7602 & 2 becomes set.
@@ -1736,8 +1815,13 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			uint32 param2 = b.readUint32BE();
 			uint32 param3 = b.readUint32BE();
 			uint32 param4 = b.readUint32BE();
-			debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
-				opcode, _gameCounter, param2, param3, param4);
+			if (opcode == 0x09 && _currentLevel == 4) {
+				debug(1, "RA1 GAME 0x09: counter=%d params=(%d,%d,%d) opcodeMask=0x%08x",
+					_gameCounter, param2, param3, param4, _frameGameOpcodeMask);
+			} else {
+				debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
+					opcode, _gameCounter, param2, param3, param4);
+			}
 		}
 		break;
 
@@ -1755,7 +1839,8 @@ void InsaneRebel1::processShot() {
 
 	// On-foot mode: only spawn when in aiming stance (dirIndex 11-19) or flags force it.
 	// Original: if (((10 < g_shipDirIndex) && (g_shipDirIndex < 0x14)) || ((DAT_000075fe & 8) != 0))
-	const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
+	const uint16 effectiveOpcode = getEffectiveGameOpcode();
+	const bool onFootMode = (effectiveOpcode == 0x19 || effectiveOpcode == 0x1A);
 	if (onFootMode) {
 		if (!(_shipDirIndex > 10 && _shipDirIndex < 20) && !(_gameplayFlags75fe & 8))
 			return;
@@ -1777,7 +1862,7 @@ void InsaneRebel1::processShot() {
 	// On-foot: character position (g_shipOffsetX + g_perspectiveX)
 	// Turret: perspective-adjusted center
 	// Flight: cursor position
-	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+	const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
 	int16 originX, originY;
 	if (onFootMode) {
 		originX = _onFootCharX + kOnFootCenterX;
@@ -1790,17 +1875,25 @@ void InsaneRebel1::processShot() {
 		originY = _shipPosY;
 	}
 
+	const int16 cursorX = getGameplayCursorX();
+	const int16 cursorY = getGameplayCursorY();
 	_shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
-	_shotSlots[slot].posX = _shipPosX;
-	_shotSlots[slot].posY = _shipPosY;
+	_shotSlots[slot].posX = cursorX;
+	_shotSlots[slot].posY = cursorY;
 	_shotSlots[slot].centerX = originX;
 	_shotSlots[slot].centerY = originY;
 	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
 	playSfx((_gameplayFlags75ff & 0x2) ? kSfxAlert : kSfxLaserShot, 127, 0);
 
-	debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
-		_shotSlots[slot].posX, _shotSlots[slot].posY, originX, originY);
+	if (effectiveOpcode == 0x09 || _currentLevel == 4) {
+		debug(1, "RA1 shot: opcode=0x%02x frame=%d slot=%d cursor=(%d,%d) origin=(%d,%d) dir=%d mode=%d",
+			effectiveOpcode, _gameCounter, slot, cursorX, cursorY, originX, originY,
+			_shipDirIndex, _flyControlMode);
+	} else {
+		debug(5, "RA1 shot: slot=%d pos=(%d,%d) origin=(%d,%d)", slot,
+			cursorX, cursorY, originX, originY);
+	}
 }
 
 // checkTargetHit — FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
@@ -1808,8 +1901,8 @@ void InsaneRebel1::processShot() {
 // UnprojectScreenPoint(), then reprojects the snapped cursor center after a hit.
 void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
 	int16 snap = _tuning.snap;
-	int16 curX = _shipPosX;
-	int16 curY = _shipPosY;
+	int16 curX = getGameplayCursorX();
+	int16 curY = getGameplayCursorY();
 	unprojectGameplayPoint(curX, curY);
 	const int slot = _targetCount;
 
@@ -1866,9 +1959,10 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						// Match FUN_1C0EF: snap in unprojected space, then project back
 						// into the current gameplay window before rendering the pointer.
 						if (snap > 0) {
-							_shipPosX = (left + right) / 2;
-							_shipPosY = (top + bottom) / 2;
-							projectGameplayPoint(_shipPosX, _shipPosY);
+							int16 snappedX = (left + right) / 2;
+							int16 snappedY = (top + bottom) / 2;
+							projectGameplayPoint(snappedX, snappedY);
+							setGameplayCursor(snappedX, snappedY);
 						}
 
 						debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 23b287561fc..e634893131a 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -34,14 +34,14 @@ static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
 	// In opcode 0x0B (FUN_1CDA7), marker/shot coordinates are in the gameplay
 	// window. Under ScummVM's FUN_224FD crop emulation, shift them into the
 	// 384-wide source buffer so they stay aligned after the source-window crop.
-	return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
+	return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
 }
 
 static inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
 	if (!rebel1 || !rebel1->isInteractiveVideoActive())
 		return 0;
 
-	return (rebel1->getActiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
+	return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
 }
 
 static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
@@ -267,6 +267,44 @@ static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
 	{ -15, -14, 16, 5 }, { 0, -38, -14, 37 }
 };
 
+// DAT_25EC/DAT_25F0 and DAT_28BC in ASSAULT.EXE. GAME opcode 0x09 uses these
+// emitter offsets instead of the generic edge-beam fallback.
+static const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
+	{ -38, -14, 37, 6 }, { 37, -14, 37, 7 }, { 42, -11, -40, 11 }, { -37, -6, 38, 14 }, { -37, -5, 38, 15 },
+	{ -35, -19, 36, 11 }, { -35, -18, 35, 12 }, { -37, -15, 36, 16 }, { -37, -11, 34, 19 }, { -37, -10, 34, 20 },
+	{ -31, -24, 33, 16 }, { -31, -23, 33, 17 }, { -32, -19, 33, 22 }, { -34, -17, 29, 24 }, { -35, -15, 28, 25 },
+	{ -25, -28, 29, 21 }, { -25, -28, 29, 20 }, { -30, -25, 28, 27 }, { -30, -20, 24, 28 }, { -31, -19, 23, 29 },
+	{ -18, -31, 26, 25 }, { -18, -31, 25, 25 }, { -23, -28, 22, 30 }, { -25, -24, 18, 32 }, { -26, -24, 18, 32 },
+	{ 35, -19, -35, 12 }, { 36, -19, -35, 11 }, { 39, -16, -38, 16 }, { 37, -12, -35, 19 }, { 37, -11, -34, 20 },
+	{ 30, -23, -33, 17 }, { 31, -23, -33, 17 }, { 33, -20, -32, 21 }, { 35, -17, -29, 23 }, { 34, -17, -30, 24 },
+	{ 25, -28, -30, 21 }, { 26, -27, -28, 23 }, { 27, -25, -28, 25 }, { 29, -20, -24, 27 }, { 30, -22, -24, 28 },
+	{ 18, -32, -26, 25 }, { 19, -31, -25, 26 }, { 22, -28, -22, 29 }, { 25, -24, -19, 31 }, { 25, -24, -17, 31 }
+};
+
+static const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
+	{ 37, -14, -37, 6 }, { -38, -12, -36, 7 }, { -41, -10, 41, 10 }, { 39, -6, -36, 13 }, { 38, -5, -36, 15 },
+	{ 41, -8, -39, 1 }, { 40, -7, -38, 1 }, { 41, -4, -40, 5 }, { 39, -1, -40, 9 }, { 39, 1, -40, 10 },
+	{ -38, -5, 42, -3 }, { -39, -4, 42, -1 }, { -43, 0, 40, 2 }, { -41, 2, 39, 5 }, { -42, 4, 37, 6 },
+	{ -36, -10, 42, 4 }, { -36, -10, 42, 4 }, { -42, -6, 41, 9 }, { -41, -3, 37, 11 }, { -42, -2, 36, 12 },
+	{ -33, -15, 42, 10 }, { -33, -15, 42, 10 }, { -39, -12, 39, 14 }, { -39, -8, 36, 17 }, { -41, -8, 33, 17 },
+	{ -41, -8, 39, 1 }, { -40, -8, 40, 1 }, { -43, -4, 42, 4 }, { -39, 0, 41, 7 }, { -39, 1, 41, 9 },
+	{ 37, -4, -43, -1 }, { 39, -4, -42, -1 }, { 42, -1, -43, 3 }, { 42, 1, -39, 5 }, { 42, 2, -38, 6 },
+	{ 36, -10, -43, 5 }, { 38, -8, -41, 5 }, { 38, -7, -42, 8 }, { 41, -3, -37, 11 }, { 41, -4, -37, 11 },
+	{ 32, -15, -42, 10 }, { 34, -14, -40, 11 }, { 36, -12, -38, 13 }, { 39, -10, -35, 17 }, { 41, -9, -33, 17 }
+};
+
+static const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
+	{ -18, 0, 18, 0 }, { -18, -1, 18, 0 }, { -18, 0, 18, 0 }, { -18, 0, 17, 0 }, { -18, -1, 18, -1 },
+	{ -14, -3, 19, 3 }, { -15, -5, 20, 0 }, { -17, -4, 18, 1 }, { -15, -4, 18, 1 }, { -19, -4, 19, 2 },
+	{ -13, -9, 20, 2 }, { -16, -8, 19, 3 }, { -15, -9, 21, 3 }, { -14, -4, 18, 5 }, { -14, -2, 17, 6 },
+	{ -9, -11, 19, 1 }, { -8, -10, 21, 2 }, { -11, -11, 18, 3 }, { -13, -11, 20, 4 }, { -14, -9, 16, 4 },
+	{ -7, -13, 20, 4 }, { -10, -14, 19, 5 }, { -11, -14, 19, 5 }, { -10, -13, 18, 6 }, { -11, -11, 16, 6 },
+	{ 14, -5, -18, 0 }, { 16, -6, -19, -1 }, { 17, -5, -20, 1 }, { 17, -5, -19, 2 }, { 17, -2, -16, 2 },
+	{ 12, -8, -19, 3 }, { 13, -8, -19, 3 }, { 11, -7, -19, 3 }, { 14, -8, -17, 2 }, { 14, -7, -17, 3 },
+	{ -16, 3, 10, -12 }, { -20, 3, 11, -11 }, { -20, 4, 10, -10 }, { -17, 4, 12, -11 }, { -19, 3, 13, -11 },
+	{ -18, 3, 5, -13 }, { -17, 3, 7, -14 }, { -18, 3, 8, -15 }, { -19, 3, 9, -11 }, { -16, 5, 11, -11 }
+};
+
 // Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
 static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 	int x, int y, const RA1Sprite &spr, uint32 flags) {
@@ -459,6 +497,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
 			 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
 	if (hasTargetingPipeline) {
+		const bool flightVariantTargetingMode =
+			hasFrameGameOpcode(0x09) ||
+			(!haveFrameGameOpcodes && _activeGameOpcode == 0x09);
 		const bool turretTargetingMode =
 			hasFrameGameOpcode(0x0A) ||
 			(!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
@@ -483,6 +524,8 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			const int16 shipOffsetY = (int16)(_posAccumY >> 8);
 			_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
 			_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
+		} else if (flightVariantTargetingMode) {
+			updateFlightVariantCursor();
 		}
 	} else {
 		// Keep lock/target accumulators quiescent when current handler doesn't
@@ -518,7 +561,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
 	}
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
-	const bool projectTargetMarkers = (_activeGameOpcode == 0x0B);
+	const bool projectTargetMarkers = (getEffectiveGameOpcode() == 0x0B);
 
 	for (int i = _targetCount - 1; i >= 0; --i) {
 		if (i >= kMaxTargetBoxes)
@@ -567,8 +610,8 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 			marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
 		}
 
-		int cursorX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
-		int cursorY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
+		int cursorX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
+		int cursorY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
 		drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
 
 		if (altMarkerSet) {
@@ -624,7 +667,7 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	}
 
 	const int overlayX = ra1OverlayViewOffsetX(this);
-	const bool projectGostMarkers = (_activeGameOpcode == 0x0B);
+	const bool projectGostMarkers = (getEffectiveGameOpcode() == 0x0B);
 	for (int i = 0; i < kMaxGostSlots; i++) {
 		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
 			int sprIdx = _gostSlots[i].frame;
@@ -684,17 +727,20 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	const int overlayY = ra1OverlayViewOffsetY(this);
 	const int leftStartX = 0;
 	const int rightStartX = 0x13F; // 319
-	const bool onFootMode = (_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A);
-	const bool turretMode = (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A);
+	const uint16 effectiveOpcode = getEffectiveGameOpcode();
+	const bool onFootMode = (effectiveOpcode == 0x19 || effectiveOpcode == 0x1A);
+	const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
+	const bool flightVariantMode = (effectiveOpcode == 0x09);
 	const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
-	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17)) : (overlayY + _shipPosY);
+	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17))
+		: (flightVariantMode ? _shipPosY : (overlayY + _shipPosY));
 
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
 			const int timer = _shotSlots[i].timer;
 			const int frame = spritesPerSet - timer;
-			const int targetX = CLIP<int>(overlayX + _shipPosX, 0, width - 1);
-			const int targetY = CLIP<int>(overlayY + _shipPosY, 0, height - 1);
+			const int targetX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
+			const int targetY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
 
 			if (onFootMode) {
 				// HandleGameOp1A_OnFootVariant: single beam from character to crosshair.
@@ -774,10 +820,51 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 				continue;
 			}
 
+			if (flightVariantMode) {
+				if ((_gameplayFlags75fe & 8) != 0)
+					continue;
+
+				if (_shipDirIndex < 0 || _shipDirIndex >= ARRAYSIZE(kRA1FlightShotEmitters28BC))
+					continue;
+
+				const RA1ShotEmitterPair &emit =
+					(_flyControlMode == 1)
+						? ((_shotSlots[i].variant != 0) ? kRA1FlightShotEmitters25EC[_shipDirIndex]
+														: kRA1FlightShotEmitters25F0[_shipDirIndex])
+						: kRA1FlightShotEmitters28BC[_shipDirIndex];
+				const int start1X = shipBaseX + emit.x1;
+				const int start1Y = shipBaseY + emit.y1;
+				const int start2X = shipBaseX + emit.x2;
+				const int start2Y = shipBaseY + emit.y2;
+				const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
+				const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
+				const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
+				const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
+				const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
+				const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
+				const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
+				const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
+				const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
+				const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
+
+				if (_currentLevel == 4) {
+					debug(1, "RA1 op09 shotRender: frame=%d timer=%d shipBase=(%d,%d) target=(%d,%d) emit1=(%d,%d) emit2=(%d,%d) dir=%d variant=%d mode=%d",
+						_gameCounter, timer, shipBaseX, shipBaseY, targetX, targetY,
+						start1X, start1Y, start2X, start2Y, _shipDirIndex,
+						_shotSlots[i].variant, _flyControlMode);
+				}
+
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
+				renderSpriteWithFlags(dst, pitch, width, height,
+					interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+				continue;
+			}
+
 			if (torpedoMode) {
-				const int trailY = overlayY + _shipPosY;
-				const int leftTrailX = overlayX + _shipPosX - 0x14 - (timer << 3);
-				const int rightTrailX = overlayX + _shipPosX + 0x14 + (timer << 3);
+				const int trailY = targetY;
+				const int leftTrailX = targetX - 0x14 - (timer << 3);
+				const int rightTrailX = targetX + 0x14 + (timer << 3);
 				const int leftWidth = getFontBankStringWidth(kRA1TorpedoTrailLeft);
 				const int rightWidth = getFontBankStringWidth(kRA1TorpedoTrailRight);
 				drawFontBankString(dst, pitch, width, height,
@@ -1010,11 +1097,13 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 
 	const RA1Sprite &spr = shipBank->sprites[_shipDirIndex];
 
-	// In 0x08/0x0A turret handlers, _shipPos holds pointer center (_74BE/_74C0),
-	// while ship sprite center is still (_74B6+_74BA, _74B8+_74BC).
+	// In 0x08/0x0A turret handlers, _shipPos holds targeting/cursor state, while
+	// the ship sprite is still anchored from camera perspective + ship drift
+	// offsets. Flight handlers already store the ship sprite center in _shipPos.
 	int shipScreenX = _shipPosX;
 	int shipScreenY = _shipPosY;
-	if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+	const uint16 effectiveOpcode = getEffectiveGameOpcode();
+	if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
 		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
 		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
 	}
@@ -1033,11 +1122,13 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const int overlayY = ra1OverlayViewOffsetY(this);
-	// In 0x08/0x0A turret handlers, explosion anchors use ship center
-	// (_74B6+_74BA, _74B8+_74BC), not pointer center (_74BE/_74C0).
+	// In 0x08/0x0A turret handlers, explosion anchors use the ship center, not
+	// the targeting cursor stored in _shipPos. Flight handlers already keep the
+	// ship center in _shipPos.
 	int shipScreenX = overlayX + _shipPosX;
 	int shipScreenY = overlayY + _shipPosY;
-	if (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A) {
+	const uint16 effectiveOpcode = getEffectiveGameOpcode();
+	if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
 		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
 		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
 	}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 44920dec6b3..e5a88ef4f7d 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -438,7 +438,8 @@ bool InsaneRebel1::runLevel5() {
 
 	_currentLevel = 4;
 	loadLevelSprites(5);
-	loadTuningForLevel(4);
+	// DOS RunLevel5Flow passes segment 6 for L5PLAY and segment 7 for L5PLAY2.
+	loadTuningForLevel(6);
 
 	beginLevelTitleOverlay(4);
 	playCinematic("LVL5/L5INTRO.ANM");
@@ -447,6 +448,7 @@ bool InsaneRebel1::runLevel5() {
 
 	while (!_vm->shouldQuit()) {
 		loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
+		loadTuningForLevel(6);
 		_flyControlMode = 1;
 		_health = kMaxHealth;
 		_damageFlags = 0;
@@ -463,7 +465,20 @@ bool InsaneRebel1::runLevel5() {
 		_gameLatch5F = 0;
 		_gameplayFlags75ff = 0;
 		_killCount = 0;
-		_levelGameplayPhase = 0;
+		_targetCount = 0;
+		_prevTargetCount = 0;
+		_lastHitTarget = 0;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+		_shipDirIndex = 17;
+		_rollAccum = 0;
+		_liftSmooth = 0;
+		_posAccumX = 0;
+		_posAccumY = 0;
+		_perspectiveX = 0;
+		_perspectiveY = 0;
+		_levelGameplayPhase = 1;
+		_level5SuccessFramesRemaining = 0x14;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -506,10 +521,13 @@ bool InsaneRebel1::runLevel5() {
 			return false;
 
 		loadRA1Nut("LVL5/L5LASER2.NUT", _laserBank);
+		loadTuningForLevel(7);
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
 		_killCount = 0;
+		_levelGameplayPhase = 2;
+		_level5SuccessFramesRemaining = 0;
 		playInteractiveVideo("LVL5/L5PLAY2.ANM");
 		if (_vm->shouldQuit())
 			return false;


Commit: 35220322a7da2dc60464ee8dc300ba22c9e2e6ba
    https://github.com/scummvm/scummvm/commit/35220322a7da2dc60464ee8dc300ba22c9e2e6ba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:44+02:00

Commit Message:
SCUMM: RA1: Improve collisions

Changed paths:
    engines/scumm/insane/insane_rebel1_iact.cpp


diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index 8a809478938..fbdcd9445f8 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -875,45 +875,7 @@ void InsaneRebel1::updateShipPhysics() {
 	_shipPosX = CLIP<int16>(_shipPosX, kRA1MinX, kRA1MaxX);
 	_shipPosY = CLIP<int16>(_shipPosY, kRA1MinY, kRA1MaxY);
 
-	// --- Step 7: Corridor collision (FUN_1C54D) ---
-	// Wall contact forces position accumulators to corridor edge and sets
-	// damage flags. Flag bit 0x10 (zone hit) suppresses damage bits only.
-	{
-		bool hasZoneHit = (_damageFlags & 0x10) != 0;
-
-			if (_shipPosX > _corridorRightX) {
-				_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
-				_shipPosX = _corridorRightX;
-				if (!hasZoneHit) {
-					if (_rollAccum > -0x100)
-					_rollAccum = -0x100;  // Push left
-				_damageFlags |= 0x02;  // Right wall
-			}
-			}
-			if (_shipPosX < _corridorLeftX) {
-				_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
-				_shipPosX = _corridorLeftX;
-				if (!hasZoneHit) {
-					if (_rollAccum < 0x100)
-					_rollAccum = 0x100;   // Push right
-				_damageFlags |= 0x04;  // Left wall
-			}
-			}
-			if (_shipPosY < _corridorTopY) {
-				_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
-				_shipPosY = _corridorTopY;
-				if (!hasZoneHit)
-					_damageFlags |= 0x01;
-			}
-			if (_shipPosY > _corridorBottomY) {
-				_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
-				_shipPosY = _corridorBottomY;
-				if (!hasZoneHit)
-					_damageFlags |= 0x08;
-		}
-	}
-
-	// --- Step 8: Perspective offsets (SetCameraOffset) ---
+	// --- Step 7: Perspective offsets (SetCameraOffset) ---
 	// FUN_1DEB5 computes these linearly from ship offsets:
 	//   viewX = clamp((_74BA + 0x20), 0, 0x40)
 	//   viewY = clamp((_74BC + 0x17), 0, 0x2E)
@@ -930,7 +892,7 @@ void InsaneRebel1::updateShipPhysics() {
 	// accumulator so side-looking still bends the gameplay projection.
 	rebuildProjectionTable(CLIP<int16>((int16)(-(_rollAccum >> 7)), -0x1A, 0x1A), 0x1A);
 
-	// --- Step 9: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
+	// --- Step 8: Direction sprite index (FUN_1DEB5 LAB_1e23e) ---
 	// Horizontal component from _74CA (rollAccum):
 	//   |rollAccum| <= 0x80: center (0)
 	//   rollAccum > 0x80:  ((rollAccum - 0x80) >> 8) * 5 + 5   (right: 5,10,15,20)
@@ -951,7 +913,7 @@ void InsaneRebel1::updateShipPhysics() {
 	if (_shipBank.numSprites > 0)
 		_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
 
-	// --- Step 10: Damage/event bit synthesis + damage processing ---
+	// --- Step 9: Damage/event bit synthesis + damage processing ---
 	// RA1 FUN_1B297-style latches from GAME opcodes:
 	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
@@ -1713,6 +1675,47 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			_corridorTopY = centerY - corridorHeight / 2;
 			_corridorRightX = _corridorLeftX + corridorWidth;
 			_corridorBottomY = _corridorTopY + corridorHeight;
+
+			// Apply 0x0D immediately so it sees the same pre-physics ship state as
+			// the other per-frame GAME latches. Deferring this until after 0x07/0x09
+			// movement made right-wall hits fire too early when steering into a wall.
+			const bool suppressDirectionalDamage = (_damageFlags & 0x10) != 0;
+			const byte oldDirectionalFlags = _damageFlags & 0x0F;
+			if (_health >= 0) {
+				if (_shipPosX < _corridorLeftX) {
+					_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+					if (!suppressDirectionalDamage) {
+						if (_rollAccum < 0x100)
+							_rollAccum = 0x100;
+						_damageFlags |= 0x04;
+					}
+				}
+				if (_shipPosX > _corridorRightX) {
+					_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+					if (!suppressDirectionalDamage) {
+						if (_rollAccum > -0x100)
+							_rollAccum = -0x100;
+						_damageFlags |= 0x02;
+					}
+				}
+				if (_shipPosY < _corridorTopY) {
+					_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+					if (!suppressDirectionalDamage)
+						_damageFlags |= 0x01;
+				}
+				if (_shipPosY > _corridorBottomY) {
+					_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+					if (!suppressDirectionalDamage)
+						_damageFlags |= 0x08;
+				}
+			}
+			if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
+				debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
+					_shipPosX, _shipPosY,
+					_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+					_damageFlags, suppressDirectionalDamage ? 1 : 0);
+			}
+
 			debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
 				corridorLeft, corridorTop, corridorWidth, corridorHeight,
 				_perspectiveX, _perspectiveY,


Commit: aaa9ef1714176428710c3af1361343095119d793
    https://github.com/scummvm/scummvm/commit/aaa9ef1714176428710c3af1361343095119d793
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:44+02:00

Commit Message:
SCUMM: RA1: Fix level 4 progression

Changed paths:
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index e634893131a..b6262e48ba4 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -367,10 +367,20 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	_frameDispatchFlags = 0;
 
 	if (_interactiveVideoActive && _player) {
-		// FUN_224FD stores absolute 320x200 window origin in a 384x242 frame:
-		// X in [0..0x40], Y in [0..0x2E], centered at (0x20,0x17).
-		_player->_ra1ViewportOffsetX = _perspectiveX;
-		_player->_ra1ViewportOffsetY = _perspectiveY;
+		const bool usePerspectiveViewport =
+			_activeGameOpcode == 0x07 || _activeGameOpcode == 0x08 ||
+			_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+			_activeGameOpcode == 0x0B;
+		// Only gameplay handlers that actually execute FUN_224FD own the scrolling
+		// 320x200 window inside the 384x242 buffer. Interactive movies with no
+		// GAME stream (for example LVL4/L4PLAY2.ANM) keep a static camera.
+		if (usePerspectiveViewport) {
+			_player->_ra1ViewportOffsetX = _perspectiveX;
+			_player->_ra1ViewportOffsetY = _perspectiveY;
+		} else {
+			_player->_ra1ViewportOffsetX = 0;
+			_player->_ra1ViewportOffsetY = 0;
+		}
 	} else if (_player) {
 		_player->_ra1ViewportOffsetX = 0;
 		_player->_ra1ViewportOffsetY = 0;
@@ -415,6 +425,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
 		(!haveFrameGameOpcodes &&
 			(_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
+	const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
+		(!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
+	const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
+		(!haveFrameGameOpcodes &&
+			(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
 	if (asteroidMode) {
 		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
 		updateAsteroidPhysics();
@@ -437,12 +452,6 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			renderSprite(renderBitmap, pitch, width, height, drawX, drawY, spr);
 		}
 	} else {
-		const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
-			(!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
-		const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
-			(!haveFrameGameOpcodes &&
-				(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
-
 		// Dispatch movement path by GAME handler family:
 		//   0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
 		//   0x07/0x09 -> flight-family handlers
@@ -471,7 +480,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
 	// On-foot mode uses SetCameraOffset(0,0) — no viewport crop.
 	if (_player) {
-		if (onFootMode) {
+		if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
 			_player->_ra1ViewportOffsetX = 0;
 			_player->_ra1ViewportOffsetY = 0;
 		} else {
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index f20a05d3357..030cd2c79de 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1355,7 +1355,7 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	assert(chunk_buffer);
 	b.read(chunk_buffer, chunk_size);
 
-	if (isRA2()) {
+	if (isRA1() || isRA2()) {
 		ra2RememberLastFobj(codec, chunk_buffer, chunk_size, left, top, width, height);
 	}
 
@@ -1398,7 +1398,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
 	uint8 *audioChunk = nullptr;
 	_skipNext = false;
-	if (isRA2())
+	if (isRA2() || isRA1())
 		_hasFrameFobjForGost = false;
 
 	bool interactiveRA1 = false;
@@ -1409,9 +1409,13 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		const uint16 activeOpcode = rebel1->getActiveGameOpcode();
 		// Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
 		// (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
-		// Keep a conservative fallback for early L2 frames before first 0x0B arrives.
+		// Keep conservative fallbacks for the early L2 frames before first 0x0B
+		// arrives and for L4PLAY2, whose static 320x180 FTCH background leaves the
+		// uncovered top band reliant on a clean frame base.
 		forceInteractiveClearRA1 = interactiveRA1 &&
-			(activeOpcode == 0x0B || (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
+			(activeOpcode == 0x0B ||
+			 (activeOpcode == 0 && rebel1->getCurrentLevel() == 1) ||
+			 _seekFile.equalsIgnoreCase("LVL4/L4PLAY2.ANM"));
 	}
 
 	// Keep the previous decoded frame (without post-render overlays) as delta source.
@@ -1531,7 +1535,9 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleLoad(subSize, b);
 			break;
 		case MKTAG('G','O','S','T'):
-			if (isRA2() || isRA1())
+			if (isRA1())
+				ra1HandleGost(subSize, b);
+			else if (isRA2())
 				ra2HandleGost(subSize, b);
 			break;
 		// RA1-specific chunk types: skip gracefully
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index de3093cfe59..4f6b4f2ab3f 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -191,8 +191,8 @@ private:
 	int _ra1ViewportOffsetX;
 	int _ra1ViewportOffsetY;
 
-	// RA2: Most recently decoded FOBJ in the current frame, used by GOST chunks
-	// to re-render the same sprite payload at a different position.
+	// RA1/RA2: Most recently decoded FOBJ in the current frame, used by GOST
+	// chunks to re-render the same sprite payload at a different position.
 	byte *_lastFobjData;
 	int32 _lastFobjDataSize;
 	int _lastFobjCodec;
@@ -361,6 +361,7 @@ private:
 						  int left, int top, int width, int height);
 	void ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
 							 int left, int top, int width, int height);
+	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
 	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
 	void ra2ResetDeltaPalette();
 	SmushFont *ra1GetFont(int font);
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 698c64e831e..47b043829f4 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -129,10 +129,12 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 			InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 			if (rebel1->isInteractiveVideoActive()) {
 				const uint16 gameOp = rebel1->getActiveGameOpcode();
+				const bool fullWidthStoredPatch = (_storedFobjWidth == _vm->_screenWidth);
 				// 0x0B (asteroid/surface) and 0x19/0x1A (on-foot) use SetCameraOffset
-				// directly — no projection-based FTCH placement.
-				if ((gameOp == 0x0B && _storedFobjWidth == _vm->_screenWidth) ||
-					gameOp == 0x19 || gameOp == 0x1A) {
+				// directly — no projection-based FTCH placement. Level 4 phase 2
+				// also stores a full-width screen-space patch (320x180) that DOS
+				// restores without the centered 1/4 projection warp.
+				if (fullWidthStoredPatch || gameOp == 0x19 || gameOp == 0x1A) {
 					left += _ra1ViewportOffsetX;
 					top += _ra1ViewportOffsetY;
 				} else {
@@ -159,6 +161,50 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 	return true;
 }
 
+void SmushPlayer::ra1HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 12) {
+		warning("SmushPlayer::ra1HandleGost: chunk too small (%d bytes)", subSize);
+		return;
+	}
+
+	const uint32 ghostType = b.readUint32BE();
+	const int32 ghostX = b.readSint32BE();
+	const int32 ghostY = b.readSint32BE();
+
+	if (!_hasFrameFobjForGost || _lastFobjData == nullptr || _lastFobjDataSize <= 0) {
+		debug("RA1 GOST: frame=%d ignored type=0x%08x pos=(%d,%d) (no current-frame FOBJ cached)",
+			_frame, ghostType, ghostX, ghostY);
+		return;
+	}
+
+	uint16 priorityFlags = 0;
+	switch (ghostType) {
+	case 0x1C:
+		priorityFlags = 0x2000;
+		break;
+	case 0x1D:
+		priorityFlags = 0x4000;
+		break;
+	case 0x1E:
+		priorityFlags = 0x6000;
+		break;
+	default:
+		debug("RA1 GOST: frame=%d ignored unknown type=0x%08x pos=(%d,%d)",
+			_frame, ghostType, ghostX, ghostY);
+		return;
+	}
+
+	debug("RA1 GOST: frame=%d type=0x%08x flags=0x%04x pos=(%d,%d) size=%dx%d codec=%d",
+		_frame, ghostType, priorityFlags, ghostX, ghostY,
+		_lastFobjWidth, _lastFobjHeight, _lastFobjCodec);
+
+	// DOS reuses the most recent FOBJ payload for RA1 GOST and places it at the
+	// absolute BE32 coordinates stored in the chunk. Priority bits are identified
+	// here but not yet modeled in the generic ScummVM decode path.
+	decodeFrameObject(_lastFobjCodec, _lastFobjData, ghostX, ghostY,
+		_lastFobjWidth, _lastFobjHeight, _lastFobjDataSize);
+}
+
 bool SmushPlayerRebel1::handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
 	if (subType != MKTAG('T','E','X','T'))
 		return false;
@@ -185,6 +231,8 @@ bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
 		_specialBuffer = (byte *)calloc(bufSize, 1);
 		_specialBufferSize = bufSize;
 	}
+	if (_specialBuffer != nullptr)
+		memset(_specialBuffer, 0, bufSize);
 	_dst = _specialBuffer;
 	return true;
 }


Commit: b3a580ac85f3cc88fcaf48474de899347610345c
    https://github.com/scummvm/scummvm/commit/b3a580ac85f3cc88fcaf48474de899347610345c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:44+02:00

Commit Message:
SCUMM: RA1: Remove level 4 workaround

Changed paths:
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 030cd2c79de..c75c4dae429 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1394,6 +1394,42 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	free(chunk_buffer);
 }
 
+static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
+	const int64 frameStart = b.pos();
+	int32 remaining = frameSize;
+
+	while (remaining > 1) {
+		if ((b.pos() & 1) && remaining > 0) {
+			const byte pad = b.readByte();
+			if (pad == 0) {
+				remaining--;
+			} else {
+				b.seek(-1, SEEK_CUR);
+			}
+		}
+
+		if (remaining < 8)
+			break;
+
+		const uint32 subType = b.readUint32BE();
+		const int32 subSize = b.readUint32BE();
+		const int64 subDataPos = b.pos();
+
+		if (subType == MKTAG('F', 'R', 'M', 'E'))
+			break;
+		if (subType == MKTAG('G', 'A', 'M', 'E')) {
+			b.seek(frameStart, SEEK_SET);
+			return true;
+		}
+
+		remaining -= subSize + 8;
+		b.seek(subDataPos + subSize, SEEK_SET);
+	}
+
+	b.seek(frameStart, SEEK_SET);
+	return false;
+}
+
 void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
 	uint8 *audioChunk = nullptr;
@@ -1403,25 +1439,33 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 
 	bool interactiveRA1 = false;
 	bool forceInteractiveClearRA1 = false;
+	bool preserveFrameHistoryRA1 = false;
 	if (isRA1() && _insane) {
 		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 		interactiveRA1 = rebel1->isInteractiveVideoActive();
 		const uint16 activeOpcode = rebel1->getActiveGameOpcode();
 		// Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
 		// (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
-		// Keep conservative fallbacks for the early L2 frames before first 0x0B
-		// arrives and for L4PLAY2, whose static 320x180 FTCH background leaves the
-		// uncovered top band reliant on a clean frame base.
+		// Keep a conservative fallback for the early L2 frames before first 0x0B
+		// arrives.
 		forceInteractiveClearRA1 = interactiveRA1 &&
 			(activeOpcode == 0x0B ||
-			 (activeOpcode == 0 && rebel1->getCurrentLevel() == 1) ||
-			 _seekFile.equalsIgnoreCase("LVL4/L4PLAY2.ANM"));
+			 (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
+
+		// The clean-frame cache only exists to strip gameplay overlays back out of
+		// the previous decoded frame before the next gameplay frame is composed.
+		// Transitional interactive clips like LVL4/L4PLAY2 have no GAME chunks in
+		// their FRME stream, so restoring the prior clean frame there just smears
+		// stale gameplay pixels into the cutscene.
+		preserveFrameHistoryRA1 = interactiveRA1 &&
+			!forceInteractiveClearRA1 &&
+			ra1FrameHasGameChunk(b, frameSize);
 	}
 
 	// Keep the previous decoded frame (without post-render overlays) as delta source.
 	// FUN_1FDBC (0x1FDBC) decodes frame data first; gameplay overlays from
 	// FUN_1BBCB/FUN_1CB22/FUN_1CDA7 are presentation-stage effects.
-	if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+	if (isRA1() && preserveFrameHistoryRA1 &&
 		_ra1HasCleanFrame && _ra1CleanFrame &&
 		_dst && _width > 0 && _height > 0) {
 		const int frameBytes = _width * _height;
@@ -1437,7 +1481,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	// but passive cinematics in current implementation need a per-frame clear to
 	// avoid trails in intro/text sequences.
 	if (isRA1() && _dst && _width > 0 && _height > 0) {
-		if (!interactiveRA1 || forceInteractiveClearRA1)
+		if (!preserveFrameHistoryRA1)
 			memset(_dst, 0, _width * _height);
 	}
 
@@ -1672,7 +1716,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
 	}
 
-	if (isRA1() && interactiveRA1 && !forceInteractiveClearRA1 &&
+	if (isRA1() && preserveFrameHistoryRA1 &&
 		_dst && _width > 0 && _height > 0) {
 		const int frameBytes = _width * _height;
 		byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
@@ -1684,7 +1728,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		} else {
 			_ra1HasCleanFrame = false;
 		}
-	} else if (isRA1() && forceInteractiveClearRA1) {
+	} else if (isRA1()) {
 		_ra1HasCleanFrame = false;
 	}
 


Commit: 22518d3549e861b8778c6894efa12ca196596e52
    https://github.com/scummvm/scummvm/commit/22518d3549e861b8778c6894efa12ca196596e52
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:45+02:00

Commit Message:
SCUMM: RA1: Improve difficulty and energy checks

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 9c77eb506a6..1898a8393a8 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -186,9 +186,9 @@ void InsaneRebel1::loadTuningForLevel(int level) {
 	_tuning.levelPts = kTuningTable[l][d][10];
 	_tuning.bonus    = kTuningTable[l][d][11];
 	_tuning.flags    = kTuningTable[l][d][12];
-	// initLevelFromTuning (0x13E7B) copies tuning flags into g_hudDisableFlags (0x75FE)
-	// and clears protected targets (DAT_00007732/7734).
-	_gameplayFlags75fe = (uint16)_tuning.flags;
+	// initLevelFromTuning (0x13E7B) writes the 16-bit tuning flags word across
+	// 0x75FE/0x75FF, so we must preserve both bytes.
+	resetGameplayFlagsFromTuning();
 	_protectedTargetA = 0;
 	_protectedTargetB = 0;
 
@@ -199,6 +199,12 @@ void InsaneRebel1::loadTuningForLevel(int level) {
 		_tuning.time, _tuning.levelPts, _tuning.bonus, _tuning.flags);
 }
 
+void InsaneRebel1::resetGameplayFlagsFromTuning() {
+	const uint16 tuningFlags = (uint16)_tuning.flags;
+	_gameplayFlags75fe = tuningFlags & 0x00FF;
+	_gameplayFlags75ff = tuningFlags >> 8;
+}
+
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenWidth = 384;
 	_screenHeight = 242;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index c7e970975d9..eed1e12a5e4 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -323,6 +323,7 @@ private:
 	TuningParams _tuning;
 
 	void loadTuningForLevel(int level);
+	void resetGameplayFlagsFromTuning();
 
 	// Damage system (from Ghidra decompilation of FUN_1DEB5)
 	int16 _health;               // 0x7560: current health (init=98, negative=dead, max=98)
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index b6262e48ba4..f8e6d60f9d2 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -433,6 +433,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (asteroidMode) {
 		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
 		updateAsteroidPhysics();
+
+		// DOS 0x0B loops test health after each frontend frame and leave the
+		// interactive movie as soon as it drops below zero. Mirror that here so
+		// asteroid/surface chapters transition to their retry/death clips like
+		// the other gameplay families do.
+		if (_health < 0) {
+			_fireCooldown = _playerFired ? 1 : 0;
+			_vm->_smushVideoShouldFinish = true;
+			return;
+		}
 	} else if (onFootMode) {
 		// On-foot handler — opcodes 0x19/0x1A (Level 9 Stormtroopers)
 		updateOnFootPhysics();
@@ -603,30 +613,36 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 		// Baseline RA1 targeting uses '^' and animation e..h.
 		const bool altMarkerSet = (_gameplayFlags75ff & 0x2) != 0;
 
-		// Lock indicator at fixed center positions:
-		// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
-		if (_targetProximity > 0) {
-			drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x78, ']');
-			if (_targetProximity > 1)
-				drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x7E, 'a');
+		// DAT_75FF bit 2 suppresses the fixed lock/readiness overlay.
+		if ((_gameplayFlags75ff & 0x4) == 0) {
+			// Lock indicator at fixed center positions:
+			// FUN_1CB22 draws marker strings at (0xA0,0x78) and (0xA0,0x7E).
+			if (_targetProximity > 0) {
+				drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x78, ']');
+				if (_targetProximity > 1)
+					drawCenteredBankGlyph(markerBank, dst, pitch, width, height, overlayX + 0xA0, overlayY + 0x7E, 'a');
+			}
 		}
 
-		// Pointer glyph at current aim position. Original uses two variants:
-		// default marker ('^' or 'x') and animated lock marker (e..h or y..|).
-		char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
-		if (_targetProximity > 1) {
-			_targetAnimCounter++;
-			marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
-		}
+		// DAT_75FE bit 2 suppresses the cursor glyph entirely.
+		if ((_gameplayFlags75fe & 0x4) == 0) {
+			// Pointer glyph at current aim position. Original uses two variants:
+			// default marker ('^' or 'x') and animated lock marker (e..h or y..|).
+			char marker[2] = { (char)(altMarkerSet ? 'x' : '^'), '\0' };
+			if (_targetProximity > 1) {
+				_targetAnimCounter++;
+				marker[0] = (char)((altMarkerSet ? 'y' : 'e') + (_targetAnimCounter & 3));
+			}
 
-		int cursorX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
-		int cursorY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
-		drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
+			int cursorX = CLIP<int>(overlayX + getGameplayCursorX(), 0, width - 1);
+			int cursorY = CLIP<int>(overlayY + getGameplayCursorY(), 0, height - 1);
+			drawCenteredBankGlyph(markerBank, dst, pitch, width, height, cursorX, cursorY, marker[0]);
 
-		if (altMarkerSet) {
-			const int indicatorWidth = getFontBankStringWidth(kRA1TorpedoIndicator);
-			drawFontBankString(dst, pitch, width, height,
-				overlayX + 0xA0 - indicatorWidth / 2, overlayY + 0x6E, kRA1TorpedoIndicator);
+			if (altMarkerSet) {
+				const int indicatorWidth = getFontBankStringWidth(kRA1TorpedoIndicator);
+				drawFontBankString(dst, pitch, width, height,
+					overlayX + 0xA0 - indicatorWidth / 2, overlayY + 0x6E, kRA1TorpedoIndicator);
+			}
 		}
 	}
 
@@ -694,8 +710,8 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
 
 			// Per-kill score popup glyph — RenderGostOverlaySlots (0x1CA35)
-			// Suppressed in on-foot mode (combatModeFlags & 8)
-			if ((_gameplayFlags75fe & 8) == 0) {
+			// Suppressed when DAT_75FF bit 3 is set.
+			if ((_gameplayFlags75ff & 8) == 0) {
 				renderGostScorePopup(dst, pitch, width, height,
 									overlayX + centerX, centerY, _gostSlots[i].frame);
 			}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index e5a88ef4f7d..1908c89bb48 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -316,7 +316,7 @@ bool InsaneRebel1::runLevel3() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
@@ -379,7 +379,7 @@ bool InsaneRebel1::runLevel4() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_levelGameplayPhase = 0;
 
@@ -403,7 +403,7 @@ bool InsaneRebel1::runLevel4() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
-			_gameplayFlags75ff = 0;
+			resetGameplayFlagsFromTuning();
 			_killCount = 0;
 			_levelGameplayPhase = 2;
 			playInteractiveVideo("LVL4/L4PLAY2.ANM");
@@ -463,7 +463,7 @@ bool InsaneRebel1::runLevel5() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -558,7 +558,9 @@ bool InsaneRebel1::runLevel6() {
 
 	_currentLevel = 5;
 	loadLevelSprites(6);
-	loadTuningForLevel(5);
+	// DOS RunLevel6Flow starts L6PLAY with PlayAnmFile(..., 8), so chapter 6
+	// uses tuning slot 8 ("6"), not the chapter-4B slot 5.
+	loadTuningForLevel(8);
 
 	beginLevelTitleOverlay(5);
 	playCinematic("LVL6/L6INTRO.ANM");
@@ -580,7 +582,7 @@ bool InsaneRebel1::runLevel6() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_levelGameplayPhase = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
@@ -651,7 +653,7 @@ bool InsaneRebel1::runLevel7() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -747,7 +749,7 @@ bool InsaneRebel1::runLevel8() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -851,7 +853,7 @@ bool InsaneRebel1::runLevel9() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1039,7 +1041,7 @@ bool InsaneRebel1::runLevel10() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1116,7 +1118,7 @@ bool InsaneRebel1::runLevel11() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1209,7 +1211,7 @@ bool InsaneRebel1::runLevel12() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1286,7 +1288,7 @@ bool InsaneRebel1::runLevel13() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1363,7 +1365,7 @@ bool InsaneRebel1::runLevel14() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1395,7 +1397,7 @@ bool InsaneRebel1::runLevel14() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
-			_gameplayFlags75ff = 0;
+			resetGameplayFlagsFromTuning();
 			_killCount = 0;
 
 			playInteractiveVideo("LVL14/L14PLAY2.ANM");
@@ -1455,7 +1457,7 @@ bool InsaneRebel1::runLevel15() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
-		_gameplayFlags75ff = 0;
+		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -1495,7 +1497,7 @@ bool InsaneRebel1::runLevel15() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
-			_gameplayFlags75ff = 0;
+			resetGameplayFlagsFromTuning();
 			_killCount = 0;
 			_torpedoFired = false;
 			_levelGameplayPhase = 2;


Commit: 03a3e02a5a995ab79627fc08b9ad826498251df0
    https://github.com/scummvm/scummvm/commit/03a3e02a5a995ab79627fc08b9ad826498251df0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:45+02:00

Commit Message:
SCUMM: RA1: Add skip-RLE line update decoder

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index c75c4dae429..5e4420e5833 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -937,6 +937,53 @@ static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top
 	}
 }
 
+/**
+ * RA1 codec 23: Additive line-update overlay (FUN_10B40).
+ *
+ * Each line starts with a 16-bit row payload size followed by repeated
+ * [skip:u8][run:u8] pairs. Unlike codec 21, the run length is stored directly,
+ * not minus one. Instead of copying bytes from the stream, the decoder adds
+ * `(paletteBase - 0x30)` to the destination pixels already present in the
+ * framebuffer.
+ */
+static void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
+	const uint8 colorDelta = (uint8)(paletteBase - 0x30);
+
+	for (int row = 0; row < height; row++) {
+		const uint16 lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + lineSize;
+		const int dstY = top + row;
+		int srcX = 0;
+
+		while (srcX < width && lineData < lineEnd) {
+			const int skip = *lineData++;
+			srcX += skip;
+			if (srcX >= width || lineData >= lineEnd)
+				break;
+
+			const int runLength = (int)(*lineData++);
+			const int dstStartX = left + srcX;
+			const int dstEndX = dstStartX + runLength;
+
+			if (dstY >= 0 && dstY < bufHeight) {
+				const int clippedStartX = MAX(dstStartX, 0);
+				const int clippedEndX = MIN(dstEndX, bufWidth);
+				if (clippedStartX < clippedEndX) {
+					byte *dstPixel = dst + dstY * pitch + clippedStartX;
+					for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
+						*dstPixel = (byte)(*dstPixel + colorDelta);
+				}
+			}
+
+			srcX += runLength;
+		}
+
+		src += lineSize + 2;
+	}
+}
+
 /**
  * RA1 codec 2: Scatter/point draw (FUN_110D7).
  *
@@ -1177,7 +1224,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	int origHeight = height;
 
 	int srcSkipY = 0;
-	if (isRA1() || isRA2()) {
+	const bool ra1AdditiveCodec = isRA1() && (codec == SMUSH_CODEC_SKIP_RLE);
+	if ((isRA1() || isRA2()) && !ra1AdditiveCodec) {
 		ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
 		if (width <= 0 || height <= 0)
 			return;
@@ -1243,6 +1291,15 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 			break;
 		}
 		// Fall through for RA2
+	case SMUSH_CODEC_SKIP_RLE:
+		if (isRA1()) {
+			const int bufWidth = pitch;
+			const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+			smushDecodeRA1AdditiveLineUpdate(_dst, src, origLeft, origTop, origWidth, origHeight,
+				pitch, bufWidth, bufHeight, ra1Param);
+			break;
+		}
+		// Fall through for RA2
 	default:
 		if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
 			break;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 4f6b4f2ab3f..cd87ff57af0 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -83,7 +83,7 @@ namespace Scumm {
 #define SMUSH_CODEC_RA1_BLOCK    5    // RA1: Block-based frame codec (no skip)
 #define SMUSH_CODEC_UNCOMPRESSED 20
 #define SMUSH_CODEC_LINE_UPDATE  21   // RA2: Skip/copy with literal pixels
-#define SMUSH_CODEC_SKIP_RLE     23   // RA2: Skip/copy with embedded RLE
+#define SMUSH_CODEC_SKIP_RLE     23   // RA1: additive line-update overlay; RA2: skip/copy with embedded RLE
 #define SMUSH_CODEC_DELTA_BLOCKS 37
 #define SMUSH_CODEC_LINE_UPDATE2 44   // RA2: Variant of codec 21
 #define SMUSH_CODEC_RA2_BOMP     45   // RA2: BOMP RLE with variable header


Commit: c33d5d1c06c9bd0912d8ab0a15d0771749a34432
    https://github.com/scummvm/scummvm/commit/c33d5d1c06c9bd0912d8ab0a15d0771749a34432
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:45+02:00

Commit Message:
SCUMM: RA1: Fix level 8 armor targets

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index eed1e12a5e4..f28bbd45fae 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -461,7 +461,7 @@ private:
 	int16 _gostSlotIdx;      // 0x23EB: next slot to write (circular 0-9)
 
 	int16 _killCount;        // 0x75D0: targets destroyed this stage
-	int16 _lastHitTarget;    // 0x75D6: prevents double-hit on same target
+	int16 _lastHitTarget;    // 0x75D6: recent-kill latch, allows at most one hit per frame
 
 	// Protected target IDs — 0x7732/0x7734 in original
 	// Targets listed here can be hit repeatedly (no event mask toggle).
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index fbdcd9445f8..b949fb62eae 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -1613,6 +1613,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_frameGameOpcodeMask = 0;
 		_frameDispatchFlags = 0;
 		resetProjectionTable();
+
+		// RunLevel8Flow seeds the walker armor mask after the interactive segment
+		// has entered its runtime reset path, not before playback begins.
+		if (_currentLevel == 7)
+			memset(_frameObjectState + 150, 0xFF, 150);
+
 		debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
 		break;
 
@@ -1937,8 +1943,10 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 			curY > top - snap && curY < bottom + snap) {
 			_targetProximity = 2;  // On-target
 
-			// Check if any active shot slot hits this target
-			if (_lastHitTarget != targetIdx + 1) {
+			// DOS uses g_recentKillObjectIdPlus1 as a frame-wide latch. Once one
+			// target is hit this frame, overlapping FOBJ layers must not consume the
+			// same shot again.
+			if (_lastHitTarget == 0) {
 				for (int i = 0; i < kMaxShotSlots; i++) {
 					if (_shotSlots[i].timer == 1) {  // Shot in final frame = impact
 						// Hit! Record in GOST slot for explosion animation
@@ -1953,6 +1961,32 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						_score += _tuning.kill;
 						_killCount++;
 						applyFrameObjectHitState(targetIdx);
+
+						if (_currentLevel == 7) {
+							Common::String damagedState;
+							Common::String hiddenState;
+							for (int objectId = 20; objectId <= 43; objectId++) {
+								const int bitIndex = objectId - 1;
+								const int byteIdx = bitIndex >> 3;
+								if (byteIdx < 0 || byteIdx >= 0x96 || byteIdx >= kFrameObjectStateBytes)
+									continue;
+
+								const byte stateBit = (byte)(0x80 >> (bitIndex & 7));
+								const int altIdx = byteIdx + 0x96;
+								const bool primarySet = (_frameObjectState[byteIdx] & stateBit) != 0;
+								const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
+									((_frameObjectState[altIdx] & stateBit) != 0);
+
+								if (primarySet)
+									hiddenState += Common::String::format("%d,", objectId);
+								else if (!secondarySet)
+									damagedState += Common::String::format("%d,", objectId);
+							}
+
+							debug(1, "RA1 L8 armor: hitObject=%d damaged=[%s] hidden=[%s]",
+								targetIdx + 1, damagedState.c_str(), hiddenState.c_str());
+						}
+
 						int16 hitCenterX = (left + right) / 2;
 						int16 hitCenterY = (top + bottom) / 2;
 						projectGameplayPoint(hitCenterX, hitCenterY);
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 1908c89bb48..77807baaf57 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -776,11 +776,6 @@ bool InsaneRebel1::runLevel8() {
 		_walkerTimer = 0;
 		_walkerBranchChoice = 0;
 
-		// g_level8HitboxBuffer (0x7698) = _frameObjectState[150..299] filled with 0xFF.
-		// This enables all frame object event masks in the secondary half of the array,
-		// which the IACT 0x5A handler uses to gate walker-related frame objects.
-		memset(_frameObjectState + 150, 0xFF, 150);
-
 		int route = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;


Commit: d0f4a422ec6555ed392ac1091340c5d0f718358e
    https://github.com/scummvm/scummvm/commit/d0f4a422ec6555ed392ac1091340c5d0f718358e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:46+02:00

Commit Message:
SCUMM: RA1: Fix level 7 video transition

Changed paths:
    engines/scumm/insane/insane_rebel1.cpp
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_iact.cpp
    engines/scumm/insane/insane_rebel1_runlevels.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/insane_rebel1.cpp
index 1898a8393a8..0610f978d45 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/insane_rebel1.cpp
@@ -291,6 +291,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		_rightPathSelected = false;
 		_levelRouteIndex = -1;
 		_pendingRouteIndex = -1;
+		_pendingRouteStartFrame = 0;
+		_pendingRouteCutoverFrame = -1;
 		_levelRouteChoice = 0;
 		_levelGameplayPhase = 0;
 		_level5SuccessFramesRemaining = 0;
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index f28bbd45fae..1ef1e542835 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -381,6 +381,8 @@ private:
 		bool _rightPathSelected;     // True if player chose the right/easy path
 		int _levelRouteIndex;        // Current mid-level route/segment for branching levels
 		int _pendingRouteIndex;      // Next route requested by original frame-branch logic
+		int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
+		int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
 		int _levelRouteChoice;       // Level-local pending branch choice (0=none, 1=left, 2=right)
 		int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
 
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/insane_rebel1_iact.cpp
index b949fb62eae..4a900b5dd5b 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/insane_rebel1_iact.cpp
@@ -498,7 +498,22 @@ void InsaneRebel1::resetProjectionTable() {
 }
 
 void InsaneRebel1::checkDynamicLevelBranch() {
-	if (!_interactiveVideoActive || _levelRouteIndex < 0 || _pendingRouteIndex >= 0 || _vm->_smushVideoShouldFinish)
+	if (!_interactiveVideoActive || _levelRouteIndex < 0)
+		return;
+
+	if (_currentLevel == 6 && _pendingRouteIndex >= 0) {
+		if (!_vm->_smushVideoShouldFinish &&
+			_pendingRouteCutoverFrame >= 0 &&
+			_frameCounter >= (uint32)_pendingRouteCutoverFrame) {
+			_vm->_smushVideoShouldFinish = true;
+			debug(1, "RA1 L7 cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
+				_levelRouteIndex, _pendingRouteIndex, (unsigned)_frameCounter,
+				(int)_pendingRouteStartFrame);
+		}
+		return;
+	}
+
+	if (_pendingRouteIndex >= 0 || _vm->_smushVideoShouldFinish)
 		return;
 
 	if (_currentLevel == 6) {
@@ -515,9 +530,11 @@ void InsaneRebel1::checkDynamicLevelBranch() {
 				continue;
 
 			_pendingRouteIndex = nextRoute;
-			_vm->_smushVideoShouldFinish = true;
-			debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d",
-				route, nextRoute, (unsigned)_frameCounter, _shipPosX);
+			_pendingRouteCutoverFrame = (int32)_frameCounter + 7;
+			_pendingRouteStartFrame = _pendingRouteCutoverFrame;
+			debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d resumeTimelineFrame=%d cutoverFrame=%d",
+				route, nextRoute, (unsigned)_frameCounter, _shipPosX,
+				(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
 			return;
 		}
 	}
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/insane_rebel1_runlevels.cpp
index 77807baaf57..04392541bff 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/insane_rebel1_runlevels.cpp
@@ -24,12 +24,125 @@
 #include "graphics/cursorman.h"
 #include "graphics/wincursor.h"
 
+#include "scumm/file.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/insane/insane_rebel1.h"
 
 namespace Scumm {
 
+struct RA1Level7ResumeSegment {
+	int16 timelineStart;
+	int16 timelineEnd;
+	int16 localStart;
+};
+
+static const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
+	{
+		{    0,  638,   0 },
+		{ 1416, 1468, 639 },
+		{   -1,   -1,  -1 },
+		{   -1,   -1,  -1 }
+	},
+	{
+		{   80,  639, 189 },
+		{  691,  879,   0 },
+		{ 1416, 1468, 749 },
+		{   -1,   -1,  -1 }
+	},
+	{
+		{   80,  639, 189 },
+		{  777,  879,  86 },
+		{  880,  965,   0 },
+		{ 1416, 1468, 749 }
+	},
+	{
+		{  398,  639, 284 },
+		{ 1132, 1415,   0 },
+		{ 1416, 1468, 526 },
+		{   -1,   -1,  -1 }
+	},
+	{
+		{  398,  639, 143 },
+		{  966, 1076,   0 },
+		{ 1384, 1415, 111 },
+		{ 1416, 1468, 385 }
+	},
+	{
+		{   80,  639, 114 },
+		{  821,  879,  55 },
+		{ 1077, 1131,   0 },
+		{ 1416, 1468, 674 }
+	}
+};
+
+static int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
+	if (timelineFrame <= 0)
+		return 0;
+
+	const int clampedRoute = CLIP<int>(route, 0, ARRAYSIZE(kLevel7ResumeSegments) - 1);
+	const RA1Level7ResumeSegment *segments = kLevel7ResumeSegments[clampedRoute];
+
+	for (int i = 0; i < ARRAYSIZE(kLevel7ResumeSegments[0]); ++i) {
+		const RA1Level7ResumeSegment &segment = segments[i];
+		if (segment.timelineStart < 0)
+			break;
+		if (timelineFrame < segment.timelineStart)
+			return segment.localStart;
+		if (timelineFrame <= segment.timelineEnd)
+			return segment.localStart + (timelineFrame - segment.timelineStart);
+	}
+
+	for (int i = ARRAYSIZE(kLevel7ResumeSegments[0]) - 1; i >= 0; --i) {
+		const RA1Level7ResumeSegment &segment = segments[i];
+		if (segment.timelineStart >= 0)
+			return segment.localStart;
+	}
+
+	return 0;
+}
+
+static int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
+	if (targetFrame <= 0)
+		return 0;
+
+	ScummFile *file = vm->instantiateScummFile();
+	if (!vm->openFile(*file, Common::Path(filename))) {
+		delete file;
+		return -1;
+	}
+
+	int32 result = -1;
+	if (file->size() >= 8) {
+		file->readUint32BE();
+		file->readUint32BE();
+
+		int32 frameIndex = 0;
+		while (file->pos() + 8 <= file->size()) {
+			const int32 chunkOffset = (int32)file->pos();
+			const uint32 chunkTag = file->readUint32BE();
+			const int32 chunkSize = (int32)file->readUint32BE();
+			const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
+			if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
+				break;
+
+			if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
+				if (frameIndex == targetFrame) {
+					result = chunkOffset;
+					break;
+				}
+				frameIndex++;
+			}
+
+			file->seek(nextChunkOffset, SEEK_SET);
+		}
+	}
+
+	file->close();
+	delete file;
+	return result;
+}
+
 // ---------------------------------------------------------------------------
 // Game flow (matching original at 0x15597)
 // ---------------------------------------------------------------------------
@@ -44,8 +157,8 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
 	_interactiveVideoActive = false;
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve
-	if (startFrame > 0)
-		splayer->setFastForwardToFrame(startFrame);
+	splayer->setFastForwardFromFrame(0);
+	splayer->setFastForwardToFrame(startFrame > 0 ? startFrame : 0);
 	splayer->play(filename, 12);
 
 	// Level-title text is only meant for the intro cinematic that armed it.
@@ -379,6 +492,7 @@ bool InsaneRebel1::runLevel4() {
 		_activeGameOpcode = 0;
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
+		_pendingRouteStartFrame = 0;
 		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_levelGameplayPhase = 0;
@@ -676,10 +790,13 @@ bool InsaneRebel1::runLevel7() {
 		_avgInputY = 0;
 
 		int route = 0;
+		int32 routeStartFrame = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;
 			_pendingRouteIndex = -1;
-			playInteractiveVideo(kLevel7Segments[route]);
+			_pendingRouteStartFrame = routeStartFrame;
+			_pendingRouteCutoverFrame = -1;
+			playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
 			if (_vm->shouldQuit())
 				return false;
 
@@ -689,11 +806,17 @@ bool InsaneRebel1::runLevel7() {
 			if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
 				break;
 
+			// RunLevel7Flow arms the next route inline, lets the current route run for
+			// seven more gameplay frames, then opens the destination ANM while keeping
+			// the existing video state alive.
+			routeStartFrame = _pendingRouteStartFrame;
 			route = _pendingRouteIndex;
 		}
 
 		_levelRouteIndex = -1;
 		_pendingRouteIndex = -1;
+		_pendingRouteStartFrame = 0;
+		_pendingRouteCutoverFrame = -1;
 
 		if (_health >= 0) {
 			playCinematic("LVL7/L7END.ANM");
@@ -1611,6 +1734,9 @@ void InsaneRebel1::runGame() {
 // Play interactive gameplay video (with ship physics + HUD).
 void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
 	debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+	const bool resumingRoute = (startFrame > 0);
+	int32 videoStartFrame = 0;
+	int32 videoOffset = 0;
 
 	// Stop any leftover audio from previous video
 	terminateAudio();
@@ -1618,22 +1744,42 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
-	clearBit(0);
+	if (!resumingRoute)
+		clearBit(0);
 	_interactiveVideoActive = true;
 	_levelRouteChoice = 0;
-	_onFootInitialized = false;  // Reset so each segment triggers counter==0 init
-	resetFrameObjectState();
+	if (!resumingRoute) {
+		_onFootInitialized = false;  // Reset so each segment triggers counter==0 init
+		resetFrameObjectState();
+	}
 	_vm->_smushVideoShouldFinish = false;
+	// Route resumes stay in the same gameplay flow in the original executable.
+	// Preserve the previous video/runtime state, but keep the destination clip
+	// fully interactive from its first visible frame.
+	splayer->setPreserveVideoStateOnNextPlay(resumingRoute);
 	splayer->setCurVideoFlags(0x28);
-	if (startFrame > 0)
-		splayer->setFastForwardToFrame(startFrame);
+	splayer->setFastForwardFromFrame(0);
+	splayer->setFastForwardToFrame(0);
+	if (_currentLevel == 6 && resumingRoute) {
+		videoStartFrame = mapLevel7TimelineFrameToLocal(_levelRouteIndex, startFrame);
+		videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
+		if (videoOffset < 0) {
+			debug(1, "RA1 L7 resume: route=%d timelineFrame=%d localFrame=%d offset lookup failed",
+				_levelRouteIndex, (int)startFrame, (int)videoStartFrame);
+			videoStartFrame = 0;
+			videoOffset = 0;
+		} else {
+			debug(1, "RA1 L7 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
+				_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
+		}
+	}
 
 	// Center mouse, hide system cursor (we draw our own), lock mouse to window
 	smush_warpMouse(160, 100, -1);
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
 
-	splayer->play(filename, 12);
+	splayer->play(filename, 12, videoOffset, videoStartFrame);
 	_interactiveVideoActive = false;
 
 	g_system->lockMouse(false);
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 5e4420e5833..abece384a6a 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -284,6 +284,9 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_height = 0;
 	_scrollX = 0;
 	_scrollY = 0;
+	_fastForwardFromFrame = 0;
+	_fastForwardToFrame = 0;
+	_preserveVideoStateOnNextPlay = false;
 
 	ra2InitFields();
 	_IACTpos = 0;
@@ -348,11 +351,16 @@ void SmushPlayer::init(int32 speed) {
 	_fobjOffsetY = 0;
 	_scrollX = 0;
 	_scrollY = 0;
+	_fastForwardFromFrame = 0;
 	_fastForwardToFrame = 0;
-	_ra1HasCleanFrame = false;
-	// RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
-	// new ANM so data from a previous segment isn't re-applied.
-	resetGameVideoState();
+	const bool preserveVideoState = _preserveVideoStateOnNextPlay;
+	_preserveVideoStateOnNextPlay = false;
+	if (!preserveVideoState) {
+		_ra1HasCleanFrame = false;
+		// RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
+		// new ANM unless the original route-switch path requested state preservation.
+		resetGameVideoState();
+	}
 
 	_vm->_smushVideoShouldFinish = false;
 	_vm->_smushActive = true;
@@ -379,6 +387,12 @@ void SmushPlayer::init(int32 speed) {
 	_IACTpos = 0;
 }
 
+bool SmushPlayer::isFastForwardingCurrentFrame() const {
+	return (_fastForwardToFrame > _fastForwardFromFrame &&
+		_frame >= _fastForwardFromFrame &&
+		_frame < _fastForwardToFrame);
+}
+
 void SmushPlayer::release() {
 	_vm->_smushVideoShouldFinish = true;
 
@@ -1601,7 +1615,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleZlibFrameObject(subSize, b);
 			break;
 		case MKTAG('P','S','A','D'):
-			if (!_compressedFileMode && _fastForwardToFrame == 0) {
+			if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
 				audioChunk = (uint8 *)malloc(subSize + 8);
 				b.seek(-8, SEEK_CUR);
 				b.read(audioChunk, subSize + 8);
@@ -1652,7 +1666,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			// RA1 voice-over audio: same 12-byte header format as PSAD
 			// (3 × BE32: trackId, seqNum, param) followed by SAUD data.
 			// Feed to audio system identically to PSAD.
-			if (!_compressedFileMode && _fastForwardToFrame == 0) {
+			if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
 				audioChunk = (uint8 *)malloc(subSize + 8);
 				b.seek(-8, SEEK_CUR);
 				b.read(audioChunk, subSize + 8);
@@ -1725,7 +1739,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 							InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
 							rebel1->handleGameChunk(embSize, embStream);
 						} else if (embTag == MKTAG('P','S','A','D')) {
-							if (!_compressedFileMode && _fastForwardToFrame == 0) {
+							if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
 								uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
 								memcpy(audioBuf, objBuf + objPos, embSize + 8);
 								feedAudio(audioBuf, 0, 127, 0, 0);
@@ -2201,9 +2215,15 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 	_seekPos = offset;
 	_seekFrame = startFrame;
 	_base = 0;
+	const uint32 fastForwardFromFrame = _fastForwardFromFrame;
+	const uint32 fastForwardToFrame = _fastForwardToFrame;
 
 	setupAnim(filename);
 	init(speed);
+	// Callers configure RA1 pre-roll before play(); preserve that target across
+	// init() so the playback loop can rebuild state up to the requested frame.
+	_fastForwardFromFrame = fastForwardFromFrame;
+	_fastForwardToFrame = fastForwardToFrame;
 
 	_startTime = _vm->_system->getMillis();
 	_startFrame = startFrame;
@@ -2228,9 +2248,10 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 	for (;;) {
 		bool skipFrame = false;
 
-		// RA1 fast-forward: process frames rapidly without display/audio
-		// until reaching the target frame. Used to skip recap sections.
-		bool fastForwarding = (_fastForwardToFrame > 0 && _frame < _fastForwardToFrame);
+		// RA1 pre-roll: process the configured hidden frame window rapidly
+		// without display/audio. Route splices can leave the first frames visible
+		// and only suppress the middle span before the resume target.
+		bool fastForwarding = isFastForwardingCurrentFrame();
 
 		if (fastForwarding) {
 			// Process frame immediately without timing
@@ -2276,9 +2297,10 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 		// When fast-forwarding completes, reset timing so playback
 		// starts from the correct point without trying to catch up.
-		if (_fastForwardToFrame > 0 && _frame >= _fastForwardToFrame) {
+		if (_fastForwardToFrame > _fastForwardFromFrame && _frame >= _fastForwardToFrame) {
 			_startFrame = _frame;
 			_startTime = _vm->_system->getMillis();
+			_fastForwardFromFrame = 0;
 			_fastForwardToFrame = 0;
 		}
 
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index cd87ff57af0..0421e032452 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -211,7 +211,9 @@ private:
 	bool _skipNext;
 	bool _ra2FastForwarding;  // Fast-forwarding RA2 BEG video to establish background
 	uint32 _frame;
+	uint32 _fastForwardFromFrame;  // First frame hidden by fast-forward (0 = hide from frame 0)
 	uint32 _fastForwardToFrame;  // RA1: skip display/audio until this frame (0 = disabled)
+	bool _preserveVideoStateOnNextPlay;
 
 	// RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
 	// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
@@ -279,6 +281,9 @@ public:
 	void setCurVideoFlags(int16 flags);
 	SmushMultiFont *getMultiFont() const { return _multiFont; }
 	void ensureMultiFont();
+	bool isFastForwardingCurrentFrame() const;
+	void setPreserveVideoStateOnNextPlay(bool preserve) { _preserveVideoStateOnNextPlay = preserve; }
+	void setFastForwardFromFrame(uint32 frame) { _fastForwardFromFrame = frame; }
 	void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
 
 	// Masked regions - areas where video should not update (e.g., destroyed enemies)


Commit: cea798d8c7e2d52cee16a4fe645c3f1e14b605d5
    https://github.com/scummvm/scummvm/commit/cea798d8c7e2d52cee16a4fe645c3f1e14b605d5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:46+02:00

Commit Message:
SCUMM: RA2: Move main loop out of scumm.cpp

Changed paths:
    engines/scumm/insane/insane_rebel2.h
    engines/scumm/insane/insane_rebel2_levels.cpp
    engines/scumm/scumm.cpp


diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/insane_rebel2.h
index f123efb5bb7..bdc246cb85a 100644
--- a/engines/scumm/insane/insane_rebel2.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -335,6 +335,10 @@ public:
 		kLevelReturnToMenu = 4    // Return to main menu
 	};
 
+	// Main game entry point — full game loop (intro, menu, pilot, chapter, levels)
+	// Emulates the retail game flow from FUN_004142BD
+	void runGame();
+
 	// Play the intro sequence (CREDITS/O_OPEN_C, O_OPEN_D, OPEN/O_OPEN_A, O_OPEN_B)
 	// Emulates case 0 in FUN_004142BD
 	void playIntroSequence();
diff --git a/engines/scumm/insane/insane_rebel2_levels.cpp b/engines/scumm/insane/insane_rebel2_levels.cpp
index 83d4f7f03fa..7b6907386e0 100644
--- a/engines/scumm/insane/insane_rebel2_levels.cpp
+++ b/engines/scumm/insane/insane_rebel2_levels.cpp
@@ -45,6 +45,82 @@ Common::String InsaneRebel2::getLevelPrefix(int levelId) {
 	return Common::String::format("%02d", levelId);
 }
 
+//
+// runGame -- Main game entry point (FUN_004142BD)
+//
+// Full game loop: intro, main menu, pilot select, chapter select, level
+// progression. Called from ScummEngine::go().
+//
+void InsaneRebel2::runGame() {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+
+	// Demo: just play the demo video and return
+	if (_vm->_game.features & GF_DEMO) {
+		splayer->play("OPEN/O_DEMO.SAN", 12);
+		return;
+	}
+
+	// Case 0: Play intro sequence (Fox logo, LucasArts logo, O_OPEN_A, O_OPEN_B)
+	playIntroSequence();
+
+	// Cases 1-4: Main menu -> pilot select -> chapter select -> gameplay loop
+	while (!_vm->shouldQuit()) {
+		int menuResult = runMainMenu();
+
+		if (menuResult == 0 || _vm->shouldQuit())
+			break;
+
+		if (menuResult == kMenuNewGame || menuResult == kMenuContinue) {
+			int pilotResult = runLevelSelect();
+
+			if (pilotResult == kLevelSelectQuit || _vm->shouldQuit())
+				break;
+
+			if (pilotResult == kLevelSelectBack)
+				continue;
+
+			int chapterResult = runChapterSelect();
+
+			if (chapterResult == kChapterSelectQuit || _vm->shouldQuit())
+				break;
+
+			if (chapterResult == kChapterSelectPlay) {
+				// _selectedChapter is 0-based, runLevel expects 1-based
+				int selectedLevel = _selectedChapter + 1;
+				debug("InsaneRebel2: Starting chapter %d (level %d)", _selectedChapter + 1, selectedLevel);
+
+				// Ending selected directly from chapter select (FUN_0041bbe8, case 0xf)
+				if (selectedLevel == 16) {
+					playEndingSequence();
+				}
+
+				// Level progression loop: on success, advance to next level
+				while (!_vm->shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
+					int result = runLevel(selectedLevel);
+
+					if (result == kLevelNextLevel) {
+						updatePilotProgress(selectedLevel - 1,
+							_playerScore, _playerLives, _playerDamage);
+						selectedLevel++;
+						if (selectedLevel > 15) {
+							playEndingSequence();
+							break;
+						}
+					} else {
+						if (_vm->shouldQuit() || result == kLevelQuit)
+							break;
+						break;
+					}
+				}
+
+				if (_vm->shouldQuit())
+					break;
+			}
+			// If kChapterSelectBack, loop back to main menu
+		}
+	}
+}
+
 //
 // playIntroSequence -- Intro sequence (FUN_004142BD case 0)
 //
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 424b222793e..36758e54d25 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -2720,90 +2720,7 @@ Common::Error ScummEngine::go() {
 	if (_game.id == GID_REBEL2) {
 		ScummEngine_v7 *vm7 = (ScummEngine_v7 *)this;
 		InsaneRebel2 *rebel = (InsaneRebel2 *)vm7->getInsane();
-
-		// Use 12 FPS as default, same as The Dig. FT uses 10.
-		// Since we don't have the standard scripts initializing this, we pass it here.
-		if (_game.features & GF_DEMO) {
-			vm7->_splayer->play("OPEN/O_DEMO.SAN", 12);
-			return Common::kNoError;
-		}
-
-		// Full game: Emulates the retail game flow from FUN_004142BD
-		// Case 0: Play intro sequence (Fox logo, LucasArts logo, O_OPEN_A, O_OPEN_B)
-		rebel->playIntroSequence();
-
-		// Cases 1-4: Main menu -> pilot select -> chapter select -> gameplay loop
-		while (!shouldQuit()) {
-			// Run main menu and get result
-			int menuResult = rebel->runMainMenu();
-
-			if (menuResult == 0 || shouldQuit()) {
-				// Quit selected
-				break;
-			}
-
-			if (menuResult == InsaneRebel2::kMenuNewGame || menuResult == InsaneRebel2::kMenuContinue) {
-				// Start Game or Continue: Show pilot selection screen (FUN_00414A41)
-				int pilotResult = rebel->runLevelSelect();
-
-				if (pilotResult == InsaneRebel2::kLevelSelectQuit || shouldQuit()) {
-					break;
-				}
-
-				if (pilotResult == InsaneRebel2::kLevelSelectBack) {
-					// Back to main menu
-					continue;
-				}
-
-				// Pilot selected: Show chapter selection screen (FUN_00415CF8)
-				int chapterResult = rebel->runChapterSelect();
-
-				if (chapterResult == InsaneRebel2::kChapterSelectQuit || shouldQuit()) {
-					break;
-				}
-
-				if (chapterResult == InsaneRebel2::kChapterSelectPlay) {
-					// Play the selected chapter using the level loading system
-					// Note: _selectedChapter is 0-based, runLevel expects 1-based
-					int selectedLevel = rebel->_selectedChapter + 1;
-					debug("ScummEngine: Starting chapter %d (level %d)", rebel->_selectedChapter + 1, selectedLevel);
-
-					// Ending selected directly from chapter select (FUN_0041bbe8, case 0xf)
-					if (selectedLevel == 16) {
-						rebel->playEndingSequence();
-					}
-
-					// Level progression loop: on success, advance to next level
-					// Original game chains levels directly (e.g. FUN_0040598c(FUN_00418063,0))
-					while (!shouldQuit() && selectedLevel >= 1 && selectedLevel <= 15) {
-						int result = rebel->runLevel(selectedLevel);
-
-						if (result == InsaneRebel2::kLevelNextLevel) {
-							rebel->updatePilotProgress(selectedLevel - 1,
-								rebel->_playerScore, rebel->_playerLives, rebel->_playerDamage);
-							selectedLevel++;
-							if (selectedLevel > 15) {
-								// Beat the game — play ending sequence (FUN_0041bbe8)
-								rebel->playEndingSequence();
-								break;
-							}
-						} else {
-							// kLevelGameOver, kLevelQuit, kLevelReturnToMenu — back to menu
-							if (shouldQuit() || result == InsaneRebel2::kLevelQuit) {
-								// Propagate quit to outer loop
-								break;
-							}
-							break;
-						}
-					}
-
-					if (shouldQuit()) {
-						break;
-					}
-				}
-				// If kChapterSelectBack, loop back to main menu
-			}
-		}
+		rebel->runGame();
 		return Common::kNoError;
 	}
 #endif


Commit: 00f9be188bf7b9a8028635845fc6b05c0beaebd6
    https://github.com/scummvm/scummvm/commit/00f9be188bf7b9a8028635845fc6b05c0beaebd6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:46+02:00

Commit Message:
SCUMM: RA: Move Rebel Assault SMUSH logic into RA players

Changed paths:
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra1.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 36758e54d25..8363ec50108 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -1846,7 +1846,7 @@ void ScummEngine_v7::setupScumm(const Common::Path &macResourceFile) {
 		// Rebel Assault 2 doesn't use iMUSE for audio - audio is handled directly by INSANE
 		_musicEngine = _imuseDigital = nullptr;
 		_insane = new InsaneRebel2(this);
-		_splayer = new SmushPlayer(this, nullptr, _insane);
+		_splayer = new SmushPlayerRebel2(this, nullptr, _insane);
 
 		// Initialize cursor
 		_macGui = nullptr; // Ensure this is null as we don't want MacGui behavior
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index abece384a6a..474324591c1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -288,7 +288,6 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_fastForwardToFrame = 0;
 	_preserveVideoStateOnNextPlay = false;
 
-	ra2InitFields();
 	_IACTpos = 0;
 	_speed = -1;
 	_insanity = false;
@@ -327,7 +326,6 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 SmushPlayer::~SmushPlayer() {
 	delete _IACTchannel;
 	delete _compressedFileSoundHandle;
-	ra2DestroyFields();
 	terminateAudio();
 
 	free(_frameBuffer);
@@ -368,8 +366,9 @@ void SmushPlayer::init(int32 speed) {
 	_vm->setDirtyColors(0, 255);
 	_dst = vs->getPixels(0, 0);
 
-	if (isRA2())
-		ra2InitVideo();
+	// Game-specific per-video init that needs _dst to be set
+	// (e.g. RA2 palette re-push + buffer clear/preserve).
+	initGameVideoState();
 
 	// HACK HACK HACK: This is an *evil* trick, beware!
 	// We do this to fix bug #1792. A proper solution would change all the
@@ -414,13 +413,11 @@ void SmushPlayer::release() {
 	_ra1CleanFrameSize = 0;
 	_ra1HasCleanFrame = false;
 
-	if (isRA2()) {
-		ra2ReleaseVideo();
-	} else {
+	releaseGameVideoState();
+	if (!shouldPreserveFrameBuffer()) {
 		free(_frameBuffer);
 		_frameBuffer = nullptr;
 	}
-	releaseGameVideoState();
 
 	_IACTstream = nullptr;
 
@@ -439,27 +436,15 @@ void SmushPlayer::release() {
 }
 
 void SmushPlayer::handleStore(int32 subSize, Common::SeekableReadStream &b) {
-	debugC(DEBUG_SMUSH, "SmushPlayer::handleStore()");
+	debugC(DEBUG_SMUSH, "SmushPlayer::handleStore() frame=%d", _frame);
 	assert(subSize >= 4);
 	_storeFrame = true;
-	if (isRA2()) {
-		debug("SmushPlayer STOR: frame=%d - will store next FOBJ", _frame);
-	}
-	if (isRA1()) {
-		debug("RA1 STOR: frame=%d", _frame);
-	}
 }
 
 void SmushPlayer::handleFetch(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFetch()");
-	assert(subSize >= (isRA1() ? 4 : 6));
 
-	if (isRA2()) {
-		ra2HandleFetch(b);
-		return;
-	}
-
-	// RA1 FTCH re-decodes the raw FOBJ captured by STOR (not a framebuffer memcpy).
+	// Game-specific FTCH handling (RA1 re-decodes stored FOBJ, RA2 does offset-aware re-decode)
 	if (handleGameFetch(subSize, b))
 		return;
 
@@ -484,7 +469,7 @@ void SmushPlayer::handleIACT(int32 subSize, Common::SeekableReadStream &b) {
 	// (RA2 audio uses PSAD chunks, not IACT)
 	bool isAudioIACT = (code == 8) && (flags == 46);
 
-	if (!isAudioIACT || isRA2()) {
+	if (!isAudioIACT || shouldRouteAllIACTs()) {
 		_vm->_insane->procIACT(_dst, 0, 0, 0, b, subSize - 8, 0, code, flags, unknown, userId);
 		return;
 	}
@@ -712,7 +697,7 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 	// RA2: The original game always shows subtitle text during cinematics
 	// (there is no subtitle toggle in the retail options menu). Skip
 	// this check so TRES text is always rendered.
-	if (!isRA2() && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
+	if (!shouldAlwaysShowSubtitles() && (!ConfMan.getBool("subtitles")) && ((flags & 8) == 8))
 		return;
 
 	bool isCJKComi = (_vm->_game.id == GID_CMI && _vm->_useCJKMode);
@@ -780,8 +765,8 @@ void SmushPlayer::handleTextResource(uint32 subType, int32 subSize, Common::Seek
 	// bit 7 - skip ^ codes (COMI)     0x80        (should be irrelevant for Smush, we strip these commands anyway)
 	// bit 8 - no vertical fix (COMI)  0x100       (COMI handles this in the printing method, but I haven't seen a case where it is used)
 
-	if (isRA2()) {
-		ra2HandleTextResource(str, fontId, color, pos_x, pos_y, left, top, width, height, flg);
+	if (handleGameTextRendering(str, fontId, color, pos_x, pos_y, left, top, width, height, flg)) {
+		// Handled by game-specific renderer
 	} else {
 		SmushFont *sf = getFont(fontId);
 		assert(sf != nullptr);
@@ -905,7 +890,7 @@ void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int widt
  * followed by (copyLen+1) literal bytes. Destination advances by pitch
  * per line, source advances by lineSize+2 per line.
  */
-static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
 	dst += top * pitch + left;
 
 	for (int row = 0; row < height; row++) {
@@ -960,7 +945,7 @@ static void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top
  * `(paletteBase - 0x30)` to the destination pixels already present in the
  * framebuffer.
  */
-static void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
 		int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
 	const uint8 colorDelta = (uint8)(paletteBase - 0x30);
 
@@ -1006,7 +991,7 @@ static void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int lef
  * Position starts at (left, top) and accumulates (dx, dy) per entry.
  * Pixel is drawn if position is within buffer bounds.
  */
-static void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
 	int curX = left;
 	int curY = top;
 
@@ -1187,19 +1172,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 			_specialBufferSize = bufSize;
 		}
 		_dst = _specialBuffer;
-	} else if (isRA1()) {
-		// RA1 sub-fullscreen frames (e.g. O1OPTION.ANM uses ~319x196 codec 2
-		// frames for the starfield animation). Render into _specialBuffer at
-		// their (left, top) offset position.
-		int bufSize = 384 * 242;
-		if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
-			free(_specialBuffer);
-			_specialBuffer = (byte *)calloc(bufSize, 1);
-			_specialBufferSize = bufSize;
-		}
-		_dst = _specialBuffer;
-	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		ra2SelectFrameBuffer(width, height);
+	} else if (handleGameFrameBufferSelect(codec, width, height)) {
+		// Game-specific buffer selection handled
 	} else if ((height > _vm->_screenHeight) || (width > _vm->_screenWidth))
 		return;
 	// FT Insane uses smaller frames to draw overlays with moving objects
@@ -1211,15 +1185,8 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	if ((height == 242) && (width == 384)) {
 		_width = width;
 		_height = height;
-	} else if (isRA1() && _dst == _specialBuffer) {
-		// RA1: sub-fullscreen FOBJs should not override the 384x242 dimensions.
-		// Set dimensions on first use if not yet established.
-		if (_width == 0 || _height == 0) {
-			_width = 384;
-			_height = 242;
-		}
-	} else if (isRA2() && ((height != _vm->_screenHeight) || (width != _vm->_screenWidth))) {
-		// RA2: preserve _width/_height set during buffer allocation
+	} else if (handleGameDimensionOverride(codec, width, height)) {
+		// Game-specific dimension handling
 	} else {
 		_width = _vm->_screenWidth;
 		_height = _vm->_screenHeight;
@@ -1232,15 +1199,11 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	// Save original FOBJ position and dimensions before clipping. Codec 37/47
 	// (delta block/glyph) decode the full frame starting at (0,0). Codec 2
 	// (scatter draw) needs the original start position for accumulation.
-	int origLeft = left;
-	int origTop = top;
 	int origWidth = width;
 	int origHeight = height;
 
 	int srcSkipY = 0;
-	const bool ra1AdditiveCodec = isRA1() && (codec == SMUSH_CODEC_SKIP_RLE);
-	if ((isRA1() || isRA2()) && !ra1AdditiveCodec) {
-		ra2AdjustFrameCoords(left, top, width, height, pitch, &srcSkipY);
+	if (handleGameAdjustCoords(codec, left, top, width, height, pitch, &srcSkipY)) {
 		if (width <= 0 || height <= 0)
 			return;
 	}
@@ -1248,103 +1211,46 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	// For RLE codecs, skip source rows that were clipped from the top.
 	// Each RLE row has a 2-byte size prefix, so we can advance past them.
 	const uint8 *adjustedSrc = src;
-	if (isRA1() && srcSkipY > 0 && (codec == SMUSH_CODEC_RLE || codec == SMUSH_CODEC_RLE_ALT || codec == SMUSH_CODEC_LINE_UPDATE)) {
-		for (int i = 0; i < srcSkipY; i++) {
-			adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
-		}
+	for (int i = 0; i < srcSkipY; i++) {
+		adjustedSrc += READ_LE_UINT16(adjustedSrc) + 2;
 	}
 
-	switch (codec) {
-	case SMUSH_CODEC_RLE:
-		if (isRA1()) {
-			// RA1 codec 1: pixel 0 is transparent (background shows through)
-			smushDecodeRA1Transparent(_dst, adjustedSrc, left, top, width, height, pitch);
-		} else {
+	// Try game-specific codec handler first (pass adjustedSrc for row-skip-aware codecs)
+	if (!handleGameCodecDecode(codec, adjustedSrc, left, top, width, height, pitch, dataSize)) {
+		// Standard SCUMM codec dispatch
+		switch (codec) {
+		case SMUSH_CODEC_RLE:
 			smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
-		}
-		break;
-	case SMUSH_CODEC_RLE_ALT:
-		// Codec 3: all pixels opaque (pixel 0 is written).
-		// Original FUN_00010916 writes every byte including value 0.
-		if (isRA1()) {
-			smushDecodeRLEOpaque(_dst, adjustedSrc, left, top, width, height, pitch);
-		} else {
+			break;
+		case SMUSH_CODEC_RLE_ALT:
 			smushDecodeRLE(_dst, adjustedSrc, left, top, width, height, pitch);
-		}
-		break;
-	case SMUSH_CODEC_RA1_SCATTER:
-		// Codec 2: Scatter draw uses absolute buffer coords, not clipped FOBJ coords.
-		// Pass full buffer dimensions for clipping (not the FOBJ width/height).
-		smushDecodeRA1Scatter(_dst, src, origLeft, origTop, _width, _height, pitch, dataSize);
-		break;
-	case SMUSH_CODEC_DELTA_BLOCKS:
-		// Codec 37 writes the full frame to dst via memcpy — always uses original
-		// FOBJ dimensions, not position-clipped dimensions.
-		if (!_deltaBlocksCodec)
-			_deltaBlocksCodec = new SmushDeltaBlocksDecoder(origWidth, origHeight);
-		if (_deltaBlocksCodec)
-			_deltaBlocksCodec->decode(_dst, src);
-		break;
-	case SMUSH_CODEC_DELTA_GLYPHS:
-		// Codec 47 also writes the full frame — use original dimensions.
-		if (!_deltaGlyphsCodec)
-			_deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(origWidth, origHeight);
-		if (_deltaGlyphsCodec)
-			_deltaGlyphsCodec->decode(_dst, src);
-		break;
-	case SMUSH_CODEC_RA1_DELTA:
-	case SMUSH_CODEC_RA1_BLOCK:
-		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, ra1Param, ra1Parm2, codec);
-		break;
-	case SMUSH_CODEC_UNCOMPRESSED:
-		smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
-		break;
-	case SMUSH_CODEC_LINE_UPDATE:
-		if (isRA1()) {
-			smushDecodeRA1SkipCopy(_dst, adjustedSrc, left, top, width, height, pitch);
 			break;
-		}
-		// Fall through for RA2
-	case SMUSH_CODEC_SKIP_RLE:
-		if (isRA1()) {
-			const int bufWidth = pitch;
-			const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
-			smushDecodeRA1AdditiveLineUpdate(_dst, src, origLeft, origTop, origWidth, origHeight,
-				pitch, bufWidth, bufHeight, ra1Param);
+		case SMUSH_CODEC_DELTA_BLOCKS:
+			if (!_deltaBlocksCodec)
+				_deltaBlocksCodec = new SmushDeltaBlocksDecoder(origWidth, origHeight);
+			if (_deltaBlocksCodec)
+				_deltaBlocksCodec->decode(_dst, src);
 			break;
-		}
-		// Fall through for RA2
-	default:
-		if (isRA2() && ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize))
+		case SMUSH_CODEC_DELTA_GLYPHS:
+			if (!_deltaGlyphsCodec)
+				_deltaGlyphsCodec = new SmushDeltaGlyphsDecoder(origWidth, origHeight);
+			if (_deltaGlyphsCodec)
+				_deltaGlyphsCodec->decode(_dst, src);
 			break;
-		if (isRA1() || isRA2()) {
-			debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
-				codec, left, top, width, height);
+		case SMUSH_CODEC_UNCOMPRESSED:
+			smushDecodeUncompressed(_dst, src, left, top, width, height, pitch);
 			break;
+		default:
+			if (isInsaneGame()) {
+				debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
+					codec, left, top, width, height);
+			} else {
+				error("Invalid codec for frame object : %d", codec);
+			}
 		}
-		error("Invalid codec for frame object : %d", codec);
-	}
-
-	// RA2 debug: check buffer fill after decode
-	if (isRA2() && _dst == _specialBuffer && _frame < 3) {
-		int nonZero = 0;
-		int total = _width * _height;
-		for (int i = 0; i < total; i++) {
-			if (_dst[i] != 0) nonZero++;
-		}
-		// Sample bottom half
-		int bottomNonZero = 0;
-		int bottomStart = (_height / 2) * _width;
-		int bottomTotal = total - bottomStart;
-		for (int i = bottomStart; i < total; i++) {
-			if (_dst[i] != 0) bottomNonZero++;
-		}
-		debug("SmushPlayer FOBJ decode done: frame=%d codec=%d buf=%dx%d total=%d nonzero=%d (%d%%) bottomHalf=%d/%d (%d%%)",
-			_frame, codec, _width, _height, total, nonZero, (nonZero * 100) / total,
-			bottomNonZero, bottomTotal, bottomTotal > 0 ? (bottomNonZero * 100) / bottomTotal : 0);
 	}
 
-	if (_storeFrame && !isRA1() && !isRA2()) {
+	if (_storeFrame && !handleGameStoreFrame()) {
 		if (_frameBuffer == nullptr) {
 			_frameBuffer = (byte *)malloc(_width * _height);
 		}
@@ -1412,27 +1318,16 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
 		_frame, codec, left, top, width, height, subSize - 14);
 
-	if (isRA2()) {
-		debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
-			_frame, codec, left, top, width, height, subSize - 14, _storeFrame, _width, _height);
-	}
-	if (isRA1()) {
-		debug("RA1 FOBJ: frame=%d codec=%d object=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
-			_frame, codec, ra1ObjectId, left, top, width, height, subSize - 14, _storeFrame);
-	}
+	handleGameFrameObjectPre(codec, left, top, width, height, subSize - 14);
 
 	int32 chunk_size = subSize - 14;
 	byte *chunk_buffer = (byte *)malloc(chunk_size);
 	assert(chunk_buffer);
 	b.read(chunk_buffer, chunk_size);
 
-	if (isRA1() || isRA2()) {
-		ra2RememberLastFobj(codec, chunk_buffer, chunk_size, left, top, width, height);
-	}
+	handleGameFrameObjectPost(codec, chunk_buffer, chunk_size, left, top, width, height);
 
-	if (_storeFrame && isRA2()) {
-		ra2StoreFobjData(codec, chunk_buffer, chunk_size, left, top, width, height);
-	}
+	// RA1 STOR needs ra1Param/rawLeft/rawTop which aren't available through the virtual hook
 	if (_storeFrame && isRA1()) {
 		free(_storedFobjData);
 		_storedFobjData = (byte *)malloc(chunk_size);
@@ -1505,8 +1400,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
 	uint8 *audioChunk = nullptr;
 	_skipNext = false;
-	if (isRA2() || isRA1())
-		_hasFrameFobjForGost = false;
+	handleGameFrameStart();
 
 	bool interactiveRA1 = false;
 	bool forceInteractiveClearRA1 = false;
@@ -1591,10 +1485,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			break;
 		}
 
-		// RA2: When _skipNext is set, skip the NEXT chunk of ANY type
-		if (isRA2() && _skipNext) {
-			_skipNext = false;
-			debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+		if (handleGameSkipChunk(subType, subSize, b)) {
 			frameSize -= subSize + 8;
 			b.seek(subOffset + subSize, SEEK_SET);
 			if (subSize & 1) {
@@ -1650,10 +1541,7 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleLoad(subSize, b);
 			break;
 		case MKTAG('G','O','S','T'):
-			if (isRA1())
-				ra1HandleGost(subSize, b);
-			else if (isRA2())
-				ra2HandleGost(subSize, b);
+			handleGameGost(subSize, b);
 			break;
 		// RA1-specific chunk types: skip gracefully
 		case MKTAG('G','A','M','E'):
@@ -1846,10 +1734,6 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			byte *palettePtr = &headerContent[6];
 			memcpy(_pal, palettePtr, sizeof(_pal));
 			adjustGamePalette();
-
-			if (isRA2())
-				ra2ResetDeltaPalette();
-
 			setDirtyColors(0, 255);
 		}
 
@@ -1858,17 +1742,15 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 			_height = READ_LE_UINT16(&headerContent[6]);
 		}
 
-		if (isRA2())
-			ra2FixupAnimHeader();
-
 		free(headerContent);
 	}
 }
 
 void SmushPlayer::setupAnim(const char *file) {
 	if (_insanity) {
-		if (isRA2()) {
-			_strings = getStrings(_vm, "SYSTM/GAME.TRS", true);
+		const char *gameStringResource = getGameStringResource();
+		if (gameStringResource) {
+			_strings = getStrings(_vm, gameStringResource, true);
 		} else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
 			readString("mineroad.trs");
 		}
@@ -1902,8 +1784,6 @@ SmushFont *SmushPlayer::getFont(int font) {
 
 			_sf[font] = new SmushFont(_vm, ft_fonts[font], true);
 		}
-	} else if (isRA2()) {
-		return ra2GetFont(font);
 	} else {
 		int numFonts = (_vm->_game.id == GID_CMI && !(_vm->_game.features & GF_DEMO)) ? 5 : 4;
 		assert(font >= 0 && font < numFonts);
@@ -2086,11 +1966,8 @@ void SmushPlayer::parseNextFrame() {
 	if (_vm->_imuseDigital)
 		_vm->_imuseDigital->flushTracks();
 
-	if (!_imuseDigital && isRA2())
-		ra2ParseNextFrame();
-
-	if (!_imuseDigital && isRA1())
-		processDispatches(_smushAudioSampleRate / _speed);
+	if (!_imuseDigital)
+		handleGameParseNextFrame();
 }
 
 void SmushPlayer::setPalette(const byte *palette) {
@@ -2207,7 +2084,7 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 
 	// Hide mouse
 	bool oldMouseState = CursorMan.showMouse(false);
-	if (isRA1() || isRA2())
+	if (isInsaneGame())
 		insanity(true);
 
 	// Load the video
@@ -2716,13 +2593,7 @@ void SmushPlayer::processDispatches(int16 feedSize) {
 	bool speechIsPlaying = false;
 
 	if (!_imuseDigital) {
-		if (isRA2() && _insane) {
-			InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
-			rebel2->processAudioFrame(feedSize);
-		} else if (isRA1() && _insane) {
-			InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-			rebel1->processAudioFrame(feedSize);
-		}
+		handleGameProcessAudio(feedSize);
 		return;
 	}
 
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 0421e032452..8ae998f8a7c 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -99,12 +99,14 @@ class SmushDeltaGlyphsDecoder;
 class IMuseDigital;
 class Insane;
 class SmushPlayerRebel1;
+class SmushPlayerRebel2;
 
 class SmushPlayer {
 	friend class Insane;
 	friend class InsaneRebel1;
 	friend class InsaneRebel2;
 	friend class SmushPlayerRebel1;
+	friend class SmushPlayerRebel2;
 	friend class SmushMultiFont;
 private:
 	struct SmushAudioDispatch {
@@ -316,12 +318,31 @@ protected:
 	virtual void initGamePlayerFields() {}
 	virtual void destroyGamePlayerFields() {}
 	virtual void resetGameVideoState() {}
+	virtual void initGameVideoState() {}
 	virtual void releaseGameVideoState() {}
+	virtual bool shouldPreserveFrameBuffer() const { return false; }
 	virtual bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) { return false; }
 	virtual bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
+	virtual bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) { return false; }
+	virtual bool shouldAlwaysShowSubtitles() const { return false; }
 	virtual SmushFont *getGameFont(int font) { return nullptr; }
 	virtual void adjustGamePalette() {}
 	virtual bool handleGameAnimHeader(byte *headerContent) { return false; }
+	virtual const char *getGameStringResource() const { return nullptr; }
+	virtual void handleGameParseNextFrame() {}
+	virtual bool shouldRouteAllIACTs() const { return false; }
+	virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
+	virtual bool handleGameDimensionOverride(int codec, int width, int height) { return false; }
+	virtual bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) { return false; }
+	virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) { return false; }
+	virtual bool handleGameStoreFrame() { return false; }
+	virtual void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {}
+	virtual void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {}
+	virtual void handleGameFrameStart() {}
+	virtual bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) { return false; }
+	virtual void handleGameGost(int32 subSize, Common::SeekableReadStream &b) {}
+	virtual void handleGameProcessAudio(int16 feedSize) {}
+	virtual bool isInsaneGame() const { return false; }
 
 private:
 	SmushFont *getFont(int font);
@@ -347,14 +368,10 @@ private:
 	void handleLoad(int32 subSize, Common::SeekableReadStream &);  // RA2 only (impl in smush_player_ra2.cpp)
 	void readPalette(byte *, Common::SeekableReadStream &);
 
-	// RA1/RA2 identification
+	// RA1/RA2 identification (isRA1 still used in handleFrameObject/handleFrame RA1 paths)
 	bool isRA1() const;
 	bool isRA2() const;
-	void ra2InitFields();
-	void ra2DestroyFields();
-	void ra2InitVideo();
-	void ra2ReleaseVideo();
-	void ra2HandleFetch(Common::SeekableReadStream &b);
+	// RA2 helper methods called from SmushPlayerRebel2 overrides
 	void ra2HandleTextResource(const char *str, int fontId, int color,
 							   int pos_x, int pos_y, int left, int top,
 							   int width, int height, TextStyleFlags flg);
@@ -368,12 +385,8 @@ private:
 							 int left, int top, int width, int height);
 	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
 	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
-	void ra2ResetDeltaPalette();
 	SmushFont *ra1GetFont(int font);
 	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
-	SmushFont *ra2GetFont(int font);
-	void ra2ParseNextFrame();
-	void ra2FixupAnimHeader();
 
 	// LOAD chunk streaming buffer (RA2 - embedded resource data)
 	byte *_loadBuffer;        // Accumulated LOAD data
@@ -412,6 +425,52 @@ protected:
 	SmushFont *getGameFont(int font) override;
 	void adjustGamePalette() override;
 	bool handleGameAnimHeader(byte *headerContent) override;
+	void handleGameParseNextFrame() override;
+	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+	bool handleGameDimensionOverride(int codec, int width, int height) override;
+	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+	bool handleGameStoreFrame() override;
+	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+	void handleGameFrameStart() override;
+	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+	void handleGameProcessAudio(int16 feedSize) override;
+	bool isInsaneGame() const override { return true; }
+};
+
+class SmushPlayerRebel2 : public SmushPlayer {
+public:
+	SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+	~SmushPlayerRebel2() override;
+
+protected:
+	void initGamePlayerFields() override;
+	void destroyGamePlayerFields() override;
+	void initGameVideoState() override;
+	void releaseGameVideoState() override;
+	bool shouldPreserveFrameBuffer() const override { return true; }
+	bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+	bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) override;
+	bool shouldAlwaysShowSubtitles() const override { return true; }
+	SmushFont *getGameFont(int font) override;
+	void adjustGamePalette() override;
+	bool handleGameAnimHeader(byte *headerContent) override;
+	const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
+	void handleGameParseNextFrame() override;
+	bool shouldRouteAllIACTs() const override { return true; }
+	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+	bool handleGameDimensionOverride(int codec, int width, int height) override;
+	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+	bool handleGameStoreFrame() override;
+	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+	void handleGameFrameStart() override;
+	bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+	void handleGameProcessAudio(int16 feedSize) override;
+	bool isInsaneGame() const override { return true; }
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 47b043829f4..cbd47b224a1 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -237,4 +237,115 @@ bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
 	return true;
 }
 
+void SmushPlayerRebel1::handleGameParseNextFrame() {
+	processDispatches(_smushAudioSampleRate / _speed);
+}
+
+// Forward declarations for RA1 codec functions (defined in smush_player.cpp and codec1.cpp)
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+	int pitch, int bufWidth, int bufHeight, uint8 param);
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
+
+bool SmushPlayerRebel1::handleGameFrameBufferSelect(int codec, int width, int height) {
+	// RA1 sub-fullscreen frames render into _specialBuffer at their (left, top) offset position.
+	int bufSize = 384 * 242;
+	if (_specialBuffer == nullptr || bufSize > _specialBufferSize) {
+		free(_specialBuffer);
+		_specialBuffer = (byte *)calloc(bufSize, 1);
+		_specialBufferSize = bufSize;
+	}
+	_dst = _specialBuffer;
+	return true;
+}
+
+bool SmushPlayerRebel1::handleGameDimensionOverride(int codec, int width, int height) {
+	if (_dst == _specialBuffer) {
+		// RA1: sub-fullscreen FOBJs should not override the 384x242 dimensions.
+		if (_width == 0 || _height == 0) {
+			_width = 384;
+			_height = 242;
+		}
+		return true;
+	}
+	return false;
+}
+
+bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+	// RA1 additive codec (SKIP_RLE) uses original coords, not adjusted
+	if (codec == SMUSH_CODEC_SKIP_RLE)
+		return false;
+	ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+	return true;
+}
+
+bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	// The base class passes clipped coords. For additive codec and scatter, we need original coords
+	// which are stored in the origLeft/origTop locals of decodeFrameObject. Since we can't access those
+	// from the override, the additive codec and scatter draw are special-cased.
+	// For now, handle the codecs that have RA1-specific behavior.
+	switch (codec) {
+	case SMUSH_CODEC_RLE:
+		smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
+		return true;
+	case SMUSH_CODEC_RLE_ALT:
+		smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch);
+		return true;
+	case SMUSH_CODEC_RA1_SCATTER:
+		smushDecodeRA1Scatter(_dst, src, left, top, _width, _height, pitch, dataSize);
+		return true;
+	case SMUSH_CODEC_RA1_DELTA:
+	case SMUSH_CODEC_RA1_BLOCK:
+		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, 0, 0, codec);
+		return true;
+	case SMUSH_CODEC_LINE_UPDATE:
+		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
+		return true;
+	case SMUSH_CODEC_SKIP_RLE: {
+		const int bufWidth = pitch;
+		const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+		smushDecodeRA1AdditiveLineUpdate(_dst, src, left, top, width, height,
+			pitch, bufWidth, bufHeight, 0);
+		return true;
+	}
+	default:
+		debugC(DEBUG_SMUSH, "SmushPlayer::decodeFrameObject: Skipping unknown codec %d (left=%d, top=%d, %dx%d)",
+			codec, left, top, width, height);
+		return true;
+	}
+}
+
+bool SmushPlayerRebel1::handleGameStoreFrame() {
+	// RA1 handles STOR via handleGameFrameObjectPost
+	return true;
+}
+
+void SmushPlayerRebel1::handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {
+	debug("RA1 FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d",
+		_frame, codec, left, top, width, height, dataSize, _storeFrame);
+}
+
+void SmushPlayerRebel1::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
+	ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+	// RA1 STOR handling remains in handleFrameObject (needs ra1Param/rawLeft/rawTop)
+}
+
+void SmushPlayerRebel1::handleGameFrameStart() {
+	_hasFrameFobjForGost = false;
+}
+
+void SmushPlayerRebel1::handleGameGost(int32 subSize, Common::SeekableReadStream &b) {
+	ra1HandleGost(subSize, b);
+}
+
+void SmushPlayerRebel1::handleGameProcessAudio(int16 feedSize) {
+	if (_insane) {
+		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+		rebel1->processAudioFrame(feedSize);
+	}
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index e19f4b51a76..8feab912d52 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -19,11 +19,10 @@
  *
  */
 
-// Rebel Assault 2 specific SmushPlayer methods
+// SmushPlayerRebel2 — RA2-specific SmushPlayer subclass
 //
-// These are methods of the SmushPlayer class that contain RA2-specific logic.
-// Keeping them in a separate file minimizes the diff on smush_player.cpp and
-// reduces the risk of regressions in Full Throttle / The Dig / CMI.
+// Overrides the virtual hooks defined in SmushPlayer to provide
+// Rebel Assault 2 specific video, font, text and codec handling.
 
 #include "common/endian.h"
 #include "common/rect.h"
@@ -53,10 +52,24 @@ void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int wi
 void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 
-/**
- * Initialize RA2-specific fields in the SmushPlayer constructor.
- */
-void SmushPlayer::ra2InitFields() {
+// ---------------------------------------------------------------------------
+// SmushPlayerRebel2 — construction / destruction
+// ---------------------------------------------------------------------------
+
+SmushPlayerRebel2::SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
+	: SmushPlayer(scumm, imuseDigital, insane) {
+	initGamePlayerFields();
+}
+
+SmushPlayerRebel2::~SmushPlayerRebel2() {
+	destroyGamePlayerFields();
+}
+
+// ---------------------------------------------------------------------------
+// Virtual hook overrides
+// ---------------------------------------------------------------------------
+
+void SmushPlayerRebel2::initGamePlayerFields() {
 	_multiFont = nullptr;
 	_storedFobjData = nullptr;
 	_storedFobjDataSize = 0;
@@ -98,10 +111,7 @@ void SmushPlayer::ra2InitFields() {
 	_scrollY = 0;
 }
 
-/**
- * Free RA2-specific resources in the SmushPlayer destructor.
- */
-void SmushPlayer::ra2DestroyFields() {
+void SmushPlayerRebel2::destroyGamePlayerFields() {
 	delete _multiFont;
 	_multiFont = nullptr;
 	free(_storedFobjData);
@@ -119,7 +129,7 @@ void SmushPlayer::ra2DestroyFields() {
  * Re-pushes the SMUSH palette (videos without NPAL inherit from previous),
  * and handles background preservation between cinematic and gameplay videos.
  */
-void SmushPlayer::ra2InitVideo() {
+void SmushPlayerRebel2::initGameVideoState() {
 	// Re-push the SMUSH palette to the system. Videos like O_LEVEL.SAN
 	// have no NPAL chunk and inherit the palette from the previous video.
 	// Since play() resets _palDirtyMin/Max, the palette would never be pushed otherwise.
@@ -149,7 +159,7 @@ void SmushPlayer::ra2InitVideo() {
  * RA2-specific cleanup in SmushPlayer::release().
  * Frees stored FOBJ data but preserves _frameBuffer across videos.
  */
-void SmushPlayer::ra2ReleaseVideo() {
+void SmushPlayerRebel2::releaseGameVideoState() {
 	free(_storedFobjData);
 	_storedFobjData = nullptr;
 	_storedFobjDataSize = 0;
@@ -170,7 +180,7 @@ void SmushPlayer::ra2ReleaseVideo() {
  * For Handler 25, skips FTCH to preserve overlays.
  * For other handlers, re-decodes stored FOBJ with current offsets.
  */
-void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
+bool SmushPlayerRebel2::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
 	int16 ftchUnknown = b.readSint16LE();
 	int16 ftchX = b.readSint16LE();
 	int16 ftchY = b.readSint16LE();
@@ -186,7 +196,7 @@ void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
 		int handler = rebel2->getHandler();
 		if (handler == 25) {
 			debug("SmushPlayer::handleFetch: Skipping FTCH for Handler 25 - preserving overlays");
-			return;
+			return true;
 		}
 	}
 
@@ -202,8 +212,77 @@ void SmushPlayer::ra2HandleFetch(Common::SeekableReadStream &b) {
 	} else {
 		debug("SmushPlayer FTCH: No stored FOBJ data! (frame=%d)", _frame);
 	}
+
+	return true;
+}
+
+/**
+ * RA2-specific text rendering using SmushMultiFont for inline font switching.
+ */
+bool SmushPlayerRebel2::handleGameTextRendering(const char *str, int fontId, int color,
+												int pos_x, int pos_y, int left, int top,
+												int width, int height, TextStyleFlags flg) {
+	ra2HandleTextResource(str, fontId, color, pos_x, pos_y, left, top, width, height, flg);
+	return true;
+}
+
+SmushFont *SmushPlayerRebel2::getGameFont(int font) {
+	// Font table for low-res mode (320x200). Original exe uses pointer
+	// arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
+	// Font 0: TALKFONT (TKHIFONT hi-res)
+	// Font 1: SMALFONT (SMHIFONT hi-res)
+	// Font 2: TITLFONT (TIHIFONT hi-res)
+	// Font 3: POVFONT  (POHIFONT hi-res)
+	const char *ra2_fonts[] = {
+		"SYSTM/TALKFONT.NUT",
+		"SYSTM/SMALFONT.NUT",
+		"SYSTM/TITLFONT.NUT",
+		"SYSTM/POVFONT.NUT"
+	};
+	int numFonts = ARRAYSIZE(ra2_fonts);
+	if (font >= 0 && font < numFonts) {
+		_sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
+	} else {
+		debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
+		_sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+	}
+	return _sf[font];
 }
 
+/**
+ * Reset XPAL delta palette from the current base palette.
+ * Prevents stale delta values from a previous video corrupting the palette.
+ */
+void SmushPlayerRebel2::adjustGamePalette() {
+	for (int j = 0; j < 768; ++j) {
+		_shiftedDeltaPal[j] = _pal[j] << 7;
+	}
+	memset(_deltaPal, 0, sizeof(_deltaPal));
+}
+
+/**
+ * RA2-specific handleAnimHeader fixup: when AHDR reports 0x0 dimensions,
+ * use screen dimensions instead.
+ */
+bool SmushPlayerRebel2::handleGameAnimHeader(byte *headerContent) {
+	int width = READ_LE_UINT16(&headerContent[4]);
+	int height = READ_LE_UINT16(&headerContent[6]);
+
+	if (width == 0 && height == 0) {
+		_width = _vm->_screenWidth;
+		_height = _vm->_screenHeight;
+		debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+	} else {
+		_width = width;
+		_height = height;
+	}
+	return true;
+}
+
+// ---------------------------------------------------------------------------
+// RA2 helper methods (still on SmushPlayer for now, used by base class)
+// ---------------------------------------------------------------------------
+
 /**
  * Handle LOAD chunk for Rebel Assault 2.
  *
@@ -492,60 +571,84 @@ void SmushPlayer::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
 }
 
 /**
- * Reset XPAL delta palette from the current base palette.
- * Prevents stale delta values from a previous video corrupting the palette.
+ * RA2 per-frame audio processing.
  */
-void SmushPlayer::ra2ResetDeltaPalette() {
-	for (int j = 0; j < 768; ++j) {
-		_shiftedDeltaPal[j] = _pal[j] << 7;
+void SmushPlayerRebel2::handleGameParseNextFrame() {
+	// Call processDispatches directly since RA2 has no iMUSE
+	// 11025 Hz / 12 fps = ~918 samples per frame
+	processDispatches(_smushAudioSampleRate / 12);
+}
+
+// ---------------------------------------------------------------------------
+// Frame decode pipeline overrides
+// ---------------------------------------------------------------------------
+
+bool SmushPlayerRebel2::handleGameFrameBufferSelect(int codec, int width, int height) {
+	if ((height != _vm->_screenHeight) || (width != _vm->_screenWidth)) {
+		ra2SelectFrameBuffer(width, height);
+		return true;
 	}
-	memset(_deltaPal, 0, sizeof(_deltaPal));
+	return false;
 }
 
-/**
- * RA2 font path table.
- */
-SmushFont *SmushPlayer::ra2GetFont(int font) {
-	// Font table for low-res mode (320x200). Original exe uses pointer
-	// arithmetic to select hi/lo font pairs (e.g. TKHIFONT+0x14=TALKFONT).
-	// Font 0: TALKFONT (TKHIFONT hi-res)
-	// Font 1: SMALFONT (SMHIFONT hi-res)
-	// Font 2: TITLFONT (TIHIFONT hi-res)
-	// Font 3: POVFONT  (POHIFONT hi-res)
-	const char *ra2_fonts[] = {
-		"SYSTM/TALKFONT.NUT",
-		"SYSTM/SMALFONT.NUT",
-		"SYSTM/TITLFONT.NUT",
-		"SYSTM/POVFONT.NUT"
-	};
-	int numFonts = ARRAYSIZE(ra2_fonts);
-	if (font >= 0 && font < numFonts) {
-		_sf[font] = new SmushFont(_vm, ra2_fonts[font], true);
-	} else {
-		debugC(DEBUG_SMUSH, "SmushPlayer::getFont: RA2 unknown font %d, using TALKFONT", font);
-		_sf[font] = new SmushFont(_vm, ra2_fonts[0], true);
+bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int height) {
+	if ((height != _vm->_screenHeight) || (width != _vm->_screenWidth)) {
+		// RA2: preserve _width/_height set during buffer allocation
+		return true;
 	}
-	return _sf[font];
+	return false;
 }
 
-/**
- * RA2 per-frame audio processing (called from parseNextFrame).
- */
-void SmushPlayer::ra2ParseNextFrame() {
-	// Call processDispatches directly since RA2 has no iMUSE
-	// 11025 Hz / 12 fps = ~918 samples per frame
-	processDispatches(_smushAudioSampleRate / 12);
+bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+	ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+	return true;
 }
 
-/**
- * RA2-specific handleAnimHeader fixup: when AHDR reports 0x0 dimensions,
- * use screen dimensions instead.
- */
-void SmushPlayer::ra2FixupAnimHeader() {
-	if (_width == 0 && _height == 0) {
-		_width = _vm->_screenWidth;   // 320
-		_height = _vm->_screenHeight; // 200
-		debug("SmushPlayer::handleAnimHeader: RA2 AHDR has 0x0 dims - using screen size %dx%d", _width, _height);
+bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	// Handle RA2-specific codecs (21, 23, 44, 45); return false for standard
+	// codecs (RLE, uncompressed, codec 37/47) so the base class decodes them.
+	return ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize);
+}
+
+bool SmushPlayerRebel2::handleGameStoreFrame() {
+	// RA2 handles STOR via ra2StoreFobjData in handleGameFrameObjectPost
+	return true;
+}
+
+void SmushPlayerRebel2::handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {
+	debug("SmushPlayer FOBJ: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d storeFrame=%d _width=%d _height=%d",
+		_frame, codec, left, top, width, height, dataSize, _storeFrame, _width, _height);
+}
+
+void SmushPlayerRebel2::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
+	ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+
+	if (_storeFrame) {
+		ra2StoreFobjData(codec, data, dataSize, left, top, width, height);
+	}
+}
+
+void SmushPlayerRebel2::handleGameFrameStart() {
+	_hasFrameFobjForGost = false;
+}
+
+bool SmushPlayerRebel2::handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) {
+	if (_skipNext) {
+		_skipNext = false;
+		debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: SKIP consumed chunk %s frame=%d", tag2str(subType), _frame);
+		return true;
+	}
+	return false;
+}
+
+void SmushPlayerRebel2::handleGameGost(int32 subSize, Common::SeekableReadStream &b) {
+	ra2HandleGost(subSize, b);
+}
+
+void SmushPlayerRebel2::handleGameProcessAudio(int16 feedSize) {
+	if (_insane) {
+		InsaneRebel2 *rebel2 = static_cast<InsaneRebel2 *>(_insane);
+		rebel2->processAudioFrame(feedSize);
 	}
 }
 


Commit: 90cab45daee2a235ab9be9f5e3bc52707b017755
    https://github.com/scummvm/scummvm/commit/90cab45daee2a235ab9be9f5e3bc52707b017755
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00

Commit Message:
SCUMM: RA: Move RA1 SMUSH frame handling into RA player

Changed paths:
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra1.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index 1ef1e542835..db6f84a3890 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -67,11 +67,15 @@ static const int16 kRA1FocalY = 25;
  * Star Wars: Rebel Assault (RA1) game logic.
  * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
  */
+class SmushPlayerRebel1;
+
 class InsaneRebel1 : public Insane, public Common::EventObserver {
 public:
 	InsaneRebel1(ScummEngine_v7 *scumm);
 	~InsaneRebel1() override;
 
+	SmushPlayerRebel1 *ra1Player() const { return static_cast<SmushPlayerRebel1 *>(_player); }
+
 	bool notifyEvent(const Common::Event &event) override;
 
 	void procPreRendering(byte *renderBitmap) override;
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index f8e6d60f9d2..93fdc31dc07 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -375,15 +375,15 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 		// 320x200 window inside the 384x242 buffer. Interactive movies with no
 		// GAME stream (for example LVL4/L4PLAY2.ANM) keep a static camera.
 		if (usePerspectiveViewport) {
-			_player->_ra1ViewportOffsetX = _perspectiveX;
-			_player->_ra1ViewportOffsetY = _perspectiveY;
+			ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
+			ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
 		} else {
-			_player->_ra1ViewportOffsetX = 0;
-			_player->_ra1ViewportOffsetY = 0;
+			ra1Player()->_ra1ViewportOffsetX = 0;
+			ra1Player()->_ra1ViewportOffsetY = 0;
 		}
 	} else if (_player) {
-		_player->_ra1ViewportOffsetX = 0;
-		_player->_ra1ViewportOffsetY = 0;
+		ra1Player()->_ra1ViewportOffsetX = 0;
+		ra1Player()->_ra1ViewportOffsetY = 0;
 	}
 }
 
@@ -491,18 +491,18 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// On-foot mode uses SetCameraOffset(0,0) — no viewport crop.
 	if (_player) {
 		if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
-			_player->_ra1ViewportOffsetX = 0;
-			_player->_ra1ViewportOffsetY = 0;
+			ra1Player()->_ra1ViewportOffsetX = 0;
+			ra1Player()->_ra1ViewportOffsetY = 0;
 		} else {
-			_player->_ra1ViewportOffsetX = _perspectiveX;
-			_player->_ra1ViewportOffsetY = _perspectiveY;
+			ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
+			ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
 		}
 
 		// Screen shake — SetCameraOffset (0x224FD): random [-2,+2] jitter when
 		// _screenFlash > 0. Original uses RandScaleByte(5) - 2 for each axis.
 		if (_screenFlash > 0) {
-			_player->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
-			_player->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+			ra1Player()->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+			ra1Player()->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
 		}
 	}
 
@@ -1228,8 +1228,8 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 	int hudOriginX = 0;
 	int hudOriginY = 0;
 	if (_interactiveVideoActive && _player) {
-		hudOriginX = _player->_ra1ViewportOffsetX;
-		hudOriginY = _player->_ra1ViewportOffsetY;
+		hudOriginX = ra1Player()->_ra1ViewportOffsetX;
+		hudOriginY = ra1Player()->_ra1ViewportOffsetY;
 	}
 
 	int hudX = hudOriginX + bar.xoffs;
@@ -1489,8 +1489,8 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 		return;
 
 	// Viewport offset: screen-space → buffer-space (same approach as renderHUD)
-	int viewX = _player ? _player->_ra1ViewportOffsetX : _perspectiveX;
-	int viewY = _player ? _player->_ra1ViewportOffsetY : _perspectiveY;
+	int viewX = _player ? ra1Player()->_ra1ViewportOffsetX : _perspectiveX;
+	int viewY = _player ? ra1Player()->_ra1ViewportOffsetY : _perspectiveY;
 
 	// Walker health display — "<<WALKER %d%%" at projected (0x61, 0x8D)
 	// Blinks when health < 16: only drawn when (frameCounter & 2) != 0
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 474324591c1..0a010f80aa3 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,6 +40,7 @@
 #include "scumm/smush/codec37.h"
 #include "scumm/smush/codec47.h"
 #include "scumm/smush/smush_font.h"
+#include "scumm/smush/smush_multi_font.h"
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
@@ -272,9 +273,26 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_frameBuffer = nullptr;
 	_specialBuffer = nullptr;
 	_specialBufferSize = 0;
-	_ra1CleanFrame = nullptr;
-	_ra1CleanFrameSize = 0;
-	_ra1HasCleanFrame = false;
+	_storedFobjData = nullptr;
+	_storedFobjDataSize = 0;
+	_storedFobjCodec = 0;
+	_storedFobjParm2 = 0;
+	_storedFobjLeft = 0;
+	_storedFobjTop = 0;
+	_storedFobjWidth = 0;
+	_storedFobjHeight = 0;
+	_lastFobjData = nullptr;
+	_lastFobjDataSize = 0;
+	_lastFobjCodec = 0;
+	_lastFobjLeft = 0;
+	_lastFobjTop = 0;
+	_lastFobjWidth = 0;
+	_lastFobjHeight = 0;
+	_hasFrameFobjForGost = false;
+	_fobjOffsetX = 0;
+	_fobjOffsetY = 0;
+	_skipNext = false;
+	_storeFrame = false;
 
 	_seekPos = -1;
 
@@ -332,10 +350,12 @@ SmushPlayer::~SmushPlayer() {
 	_frameBuffer = nullptr;
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
-	free(_ra1CleanFrame);
-	_ra1CleanFrame = nullptr;
-	_ra1CleanFrameSize = 0;
-	_ra1HasCleanFrame = false;
+	free(_storedFobjData);
+	_storedFobjData = nullptr;
+	free(_lastFobjData);
+	_lastFobjData = nullptr;
+	delete _multiFont;
+	_multiFont = nullptr;
 }
 
 void SmushPlayer::init(int32 speed) {
@@ -354,9 +374,6 @@ void SmushPlayer::init(int32 speed) {
 	const bool preserveVideoState = _preserveVideoStateOnNextPlay;
 	_preserveVideoStateOnNextPlay = false;
 	if (!preserveVideoState) {
-		_ra1HasCleanFrame = false;
-		// RA1 OBJ overlay chunks are video-local. Reset cached overlay state for each
-		// new ANM unless the original route-switch path requested state preservation.
 		resetGameVideoState();
 	}
 
@@ -408,10 +425,6 @@ void SmushPlayer::release() {
 
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
-	free(_ra1CleanFrame);
-	_ra1CleanFrame = nullptr;
-	_ra1CleanFrameSize = 0;
-	_ra1HasCleanFrame = false;
 
 	releaseGameVideoState();
 	if (!shouldPreserveFrameBuffer()) {
@@ -1298,22 +1311,13 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 	}
 
 	int codec = b.readUint16LE();
-	uint8 ra1Param = 0;   // RA1: palette base byte (FOBJ byte[1])
-	uint16 ra1ObjectId = 0; // RA1: object id / event target id (FOBJ bytes[10-11])
-	uint16 ra1Parm2 = 0;  // RA1: tile count (FOBJ bytes[12-13])
-	if (isRA1()) {
-		ra1Param = (codec >> 8) & 0xFF; // byte[1] = palette base (e.g. 0xF0)
-		codec &= 0xFF;                  // byte[0] = actual codec number
-	}
 	int left = (int)b.readSint16LE();
 	int top = (int)b.readSint16LE();
-	const int rawLeft = left;
-	const int rawTop = top;
 	int width = b.readUint16LE();
 	int height = b.readUint16LE();
 
-	ra1ObjectId = b.readUint16LE();
-	ra1Parm2 = b.readUint16LE();
+	b.readUint16LE(); // objectId (RA1 uses this)
+	b.readUint16LE(); // parm2 (RA1 uses this)
 
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrameObject: frame=%d codec=%d pos=(%d,%d) size=%dx%d dataSize=%d",
 		_frame, codec, left, top, width, height, subSize - 14);
@@ -1327,149 +1331,22 @@ void SmushPlayer::handleFrameObject(int32 subSize, Common::SeekableReadStream &b
 
 	handleGameFrameObjectPost(codec, chunk_buffer, chunk_size, left, top, width, height);
 
-	// RA1 STOR needs ra1Param/rawLeft/rawTop which aren't available through the virtual hook
-	if (_storeFrame && isRA1()) {
-		free(_storedFobjData);
-		_storedFobjData = (byte *)malloc(chunk_size);
-		if (_storedFobjData != nullptr) {
-			memcpy(_storedFobjData, chunk_buffer, chunk_size);
-			_storedFobjDataSize = chunk_size;
-			_storedFobjCodec = codec | ((int)ra1Param << 8);
-			_storedFobjParm2 = ra1Parm2;
-			_storedFobjLeft = rawLeft;
-			_storedFobjTop = rawTop;
-			_storedFobjWidth = width;
-			_storedFobjHeight = height;
-		} else {
-			_storedFobjDataSize = 0;
-		}
-		_storeFrame = false;
-	}
-
-	if (isRA1() && _insane) {
-		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-		if (!rebel1->handleFrameObjectTarget((int16)ra1ObjectId, (int16)rawLeft, (int16)rawTop,
-				(int16)width, (int16)height, codec, ra1Param)) {
-			free(chunk_buffer);
-			return;
-		}
-	}
-
-	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
+	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size);
 
 	free(chunk_buffer);
 }
 
-static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
-	const int64 frameStart = b.pos();
-	int32 remaining = frameSize;
-
-	while (remaining > 1) {
-		if ((b.pos() & 1) && remaining > 0) {
-			const byte pad = b.readByte();
-			if (pad == 0) {
-				remaining--;
-			} else {
-				b.seek(-1, SEEK_CUR);
-			}
-		}
-
-		if (remaining < 8)
-			break;
-
-		const uint32 subType = b.readUint32BE();
-		const int32 subSize = b.readUint32BE();
-		const int64 subDataPos = b.pos();
-
-		if (subType == MKTAG('F', 'R', 'M', 'E'))
-			break;
-		if (subType == MKTAG('G', 'A', 'M', 'E')) {
-			b.seek(frameStart, SEEK_SET);
-			return true;
-		}
-
-		remaining -= subSize + 8;
-		b.seek(subDataPos + subSize, SEEK_SET);
-	}
-
-	b.seek(frameStart, SEEK_SET);
-	return false;
-}
-
 void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame(%d)", _frame);
 	uint8 *audioChunk = nullptr;
 	_skipNext = false;
 	handleGameFrameStart();
 
-	bool interactiveRA1 = false;
-	bool forceInteractiveClearRA1 = false;
-	bool preserveFrameHistoryRA1 = false;
-	if (isRA1() && _insane) {
-		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-		interactiveRA1 = rebel1->isInteractiveVideoActive();
-		const uint16 activeOpcode = rebel1->getActiveGameOpcode();
-		// Opcode 0x0B path (FUN_1CDA7) uses heavy partial-layer composition
-		// (codec1/2 + FTCH). Force clear there to avoid stale-trail ghosting.
-		// Keep a conservative fallback for the early L2 frames before first 0x0B
-		// arrives.
-		forceInteractiveClearRA1 = interactiveRA1 &&
-			(activeOpcode == 0x0B ||
-			 (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
-
-		// The clean-frame cache only exists to strip gameplay overlays back out of
-		// the previous decoded frame before the next gameplay frame is composed.
-		// Transitional interactive clips like LVL4/L4PLAY2 have no GAME chunks in
-		// their FRME stream, so restoring the prior clean frame there just smears
-		// stale gameplay pixels into the cutscene.
-		preserveFrameHistoryRA1 = interactiveRA1 &&
-			!forceInteractiveClearRA1 &&
-			ra1FrameHasGameChunk(b, frameSize);
-	}
-
-	// Keep the previous decoded frame (without post-render overlays) as delta source.
-	// FUN_1FDBC (0x1FDBC) decodes frame data first; gameplay overlays from
-	// FUN_1BBCB/FUN_1CB22/FUN_1CDA7 are presentation-stage effects.
-	if (isRA1() && preserveFrameHistoryRA1 &&
-		_ra1HasCleanFrame && _ra1CleanFrame &&
-		_dst && _width > 0 && _height > 0) {
-		const int frameBytes = _width * _height;
-		if (_ra1CleanFrameSize >= frameBytes)
-			memcpy(_dst, _ra1CleanFrame, frameBytes);
-	}
-
 	if (_insanity) {
 		_vm->_insane->procPreRendering(_dst);
 	}
 
-	// RA1: gameplay/interactivity relies on previous-frame history (delta codecs),
-	// but passive cinematics in current implementation need a per-frame clear to
-	// avoid trails in intro/text sequences.
-	if (isRA1() && _dst && _width > 0 && _height > 0) {
-		if (!preserveFrameHistoryRA1)
-			memset(_dst, 0, _width * _height);
-	}
-
 	while (frameSize > 0) {
-		// RA1 parser exits when <=1 byte remains in a frame (FUN_1FDBC).
-		// Treat any tiny tail as frame trailer/padding and stop cleanly.
-		if (isRA1() && frameSize <= 1) {
-			if (frameSize == 1)
-				b.skip(1);
-			break;
-		}
-
-		// RA1: Top-of-loop alignment check matching original assembly FUN_1FDBC:
-		// if ((ptr & 1) && (*ptr == 0)) { ptr++; remaining--; }
-		if (isRA1() && (b.pos() & 1) && frameSize > 0) {
-			byte peek = b.readByte();
-			if (peek == 0) {
-				frameSize--;
-			} else {
-				b.seek(-1, SEEK_CUR);
-			}
-		}
-
 		if (frameSize < 8) {
 			b.skip(frameSize);
 			break;
@@ -1479,12 +1356,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 		int32 subSize = b.readUint32BE();
 		int32 subOffset = b.pos();
 
-		// Guard against consuming the next frame marker as an in-frame chunk.
-		if (isRA1() && subType == MKTAG('F','R','M','E')) {
-			b.seek(-8, SEEK_CUR);
-			break;
-		}
-
 		if (handleGameSkipChunk(subType, subSize, b)) {
 			frameSize -= subSize + 8;
 			b.seek(subOffset + subSize, SEEK_SET);
@@ -1514,7 +1385,6 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 				free(audioChunk);
 				audioChunk = nullptr;
 			}
-
 			break;
 		case MKTAG('T','R','E','S'):
 			handleTextResource(subType, subSize, b);
@@ -1538,159 +1408,28 @@ void SmushPlayer::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 			handleTextResource(subType, subSize, b);
 			break;
 		case MKTAG('L','O','A','D'):
-			handleLoad(subSize, b);
+			handleGameLoad(subSize, b);
 			break;
 		case MKTAG('G','O','S','T'):
 			handleGameGost(subSize, b);
 			break;
-		// RA1-specific chunk types: skip gracefully
-		case MKTAG('G','A','M','E'):
-			if (isRA1()) {
-				InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
-				rebel1->handleGameChunk(subSize, b);
-			}
-			break;
-		case MKTAG('P','V','O','C'):
-			// RA1 voice-over audio: same 12-byte header format as PSAD
-			// (3 × BE32: trackId, seqNum, param) followed by SAUD data.
-			// Feed to audio system identically to PSAD.
-			if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
-				audioChunk = (uint8 *)malloc(subSize + 8);
-				b.seek(-8, SEEK_CUR);
-				b.read(audioChunk, subSize + 8);
-				feedAudio(audioChunk, 0, 127, 0, 0);
-				free(audioChunk);
-				audioChunk = nullptr;
-			}
-			break;
-		case MKTAG('G','A','M','2'):
-		case MKTAG('F','A','D','E'):
-		case MKTAG('S','E','G','A'):
-		case MKTAG('A','D','L',' '):
-		case MKTAG('A','D','L','2'):
-		case MKTAG('S','B','L',' '):
-		case MKTAG('S','B','L','2'):
-		case MKTAG('P','S','D','2'):
-			debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping RA1 chunk %s (%d bytes)", tag2str(subType), subSize);
-			break;
-		case MKTAG('O','B','J','\0'):
-			// RA1 object overlay chunk: variable-size header + embedded FOBJ
-			// sprites (including the cockpit overlay), GAME, and PSAD chunks.
-			// The reported size field is unreliable — remaining FRME data after
-			// OBJ\0 contains unstructured data between the reported end and
-			// subsequent sub-chunks. Read ALL remaining FRME data and scan for
-			// embedded sub-chunks, then stop frame parsing.
-			if (isRA1()) {
-				int32 objDataSize = frameSize - 8;
-				if (objDataSize > 0) {
-					byte *objBuf = (byte *)malloc(objDataSize);
-					b.read(objBuf, objDataSize);
-
-					int32 objPos = 0;
-					while (objPos + 8 < objDataSize) {
-						uint32 embTag = READ_BE_UINT32(objBuf + objPos);
-						uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
-						int32 embRemaining = objDataSize - objPos - 8;
-
-						bool recognized = (embTag == MKTAG('F','O','B','J') ||
-						                   embTag == MKTAG('G','A','M','E') ||
-						                   embTag == MKTAG('P','S','A','D'));
-
-						if (!recognized || embSize > (uint32)embRemaining) {
-							// Not a recognized tag or size exceeds remaining data.
-							// Advance byte-by-byte through the OBJ header.
-							objPos++;
-							continue;
-						}
-
-						if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
-							Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
-							handleFrameObject(embSize, embStream);
-
-							// Save the largest OBJ embedded FOBJ as the cockpit overlay
-							// to re-render every subsequent frame.
-							if (_ra1ObjOverlayData == nullptr ||
-							    (int32)embSize > _ra1ObjOverlayDataSize) {
-								free(_ra1ObjOverlayData);
-								_ra1ObjOverlayDataSize = embSize;
-								_ra1ObjOverlayData = (byte *)malloc(embSize);
-								memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
-								// Parse FOBJ header for codec/position/size
-								_ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
-								_ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
-								_ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
-								_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
-								_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
-							}
-						} else if (embTag == MKTAG('G','A','M','E')) {
-							Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
-							InsaneRebel1 *rebel1 = (InsaneRebel1 *)_vm->_insane;
-							rebel1->handleGameChunk(embSize, embStream);
-						} else if (embTag == MKTAG('P','S','A','D')) {
-							if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
-								uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
-								memcpy(audioBuf, objBuf + objPos, embSize + 8);
-								feedAudio(audioBuf, 0, 127, 0, 0);
-								free(audioBuf);
-							}
-						}
-
-						objPos += 8 + embSize;
-						if (embSize & 1)
-							objPos++;
-					}
-					free(objBuf);
-				}
-				frameSize = 0;
-				continue;
-			}
-			break;
 		default:
-			if (isRA1()) {
-				// Original FUN_1FDBC lines 163-168: unknown tag with all uppercase
-				// letters (A-Z) → silently return. Otherwise error.
-				byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
-				byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
-				if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
-				    tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
-					debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
-					frameSize = 0;
-					continue;
-				}
+			if (isInsaneGame()) {
+				debugC(DEBUG_SMUSH, "SmushPlayer::handleFrame: skipping unknown chunk %s (%d bytes) frame=%d",
+					tag2str(subType), subSize, _frame);
+			} else {
+				error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 			}
-			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
 		}
 
 		frameSize -= subSize + 8;
 		b.seek(subOffset + subSize, SEEK_SET);
-		// RA1 uses top-of-loop alignment (matching FUN_1FDBC), not bottom-of-loop padding.
-		if (!isRA1() && (subSize & 1)) {
+		if (subSize & 1) {
 			b.skip(1);
 			frameSize--;
 		}
 	}
 
-	if (isRA1() && _ra1ObjOverlayData != nullptr && _frame > 0) {
-		Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
-		handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
-	}
-
-	if (isRA1() && preserveFrameHistoryRA1 &&
-		_dst && _width > 0 && _height > 0) {
-		const int frameBytes = _width * _height;
-		byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
-		if (newClean != nullptr) {
-			_ra1CleanFrame = newClean;
-			_ra1CleanFrameSize = frameBytes;
-			memcpy(_ra1CleanFrame, _dst, frameBytes);
-			_ra1HasCleanFrame = true;
-		} else {
-			_ra1HasCleanFrame = false;
-		}
-	} else if (isRA1()) {
-		_ra1HasCleanFrame = false;
-	}
-
 	if (_insanity) {
 		_vm->_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
 	}
@@ -2003,6 +1742,14 @@ void SmushPlayer::updateScreen() {
 	debugC(DEBUG_SMUSH, "Smush stats: updateScreen( %03d )", end_time - start_time);
 }
 
+void SmushPlayer::handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) {
+	if (_vm->_macScreen) {
+		_vm->mac_drawBufferToScreen(src, srcPitch, 0, 0, width, height);
+	} else {
+		_vm->_system->copyRectToScreen(src, srcPitch, 0, 0, width, height);
+	}
+}
+
 void SmushPlayer::insanity(bool flag) {
 	_insanity = flag;
 }
@@ -2223,38 +1970,11 @@ void SmushPlayer::play(const char *filename, int32 speed, int32 offset, int32 st
 					int frameHeight;
 					const byte *dst;
 
-					if (isRA1()) {
-						if (_dst == nullptr || _width <= 0 || _height <= 0) {
-							_updateNeeded = false;
-							continue;
-						}
-
-						int ra1ViewX = _ra1ViewportOffsetX;
-						int ra1ViewY = _ra1ViewportOffsetY;
-
-						const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
-						const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
-
-						frameWidth = MIN(_width - srcX, _vm->_screenWidth);
-						frameHeight = MIN(_height - srcY, _vm->_screenHeight);
-						if (frameWidth <= 0 || frameHeight <= 0) {
-							_updateNeeded = false;
-							continue;
-						}
+					frameWidth = MIN(_width, _vm->_screenWidth);
+					frameHeight = MIN(_height, _vm->_screenHeight);
+					dst = _dst + _scrollY * _width + _scrollX;
 
-						dst = _dst + srcY * _width + srcX;
-					} else {
-						frameWidth = MIN(_width, _vm->_screenWidth);
-						frameHeight = MIN(_height, _vm->_screenHeight);
-						dst = _dst + _scrollY * _width + _scrollX;
-					}
-
-					if (_vm->_macScreen) {
-						int srcPitch = isRA1() ? _width : frameWidth;
-						_vm->mac_drawBufferToScreen(dst, srcPitch, 0, 0, frameWidth, frameHeight);
-					} else {
-						_vm->_system->copyRectToScreen(dst, _width, 0, 0, frameWidth, frameHeight);
-					}
+					handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
 
 					_vm->_system->updateScreen();
 					_updateNeeded = false;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 8ae998f8a7c..ce56b23bbc5 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -160,39 +160,17 @@ private:
 	byte *_frameBuffer;
 	byte *_specialBuffer;
 	int _specialBufferSize;
-	byte *_ra1CleanFrame;
-	int32 _ra1CleanFrameSize;
-	bool _ra1HasCleanFrame;
 
-	// RA2: Raw FOBJ data stored by STOR chunk (matching original DAT_00482c04).
-	// The original stores raw FOBJ chunk data and re-decodes it on FTCH with
-	// current FOBJ offsets. This is essential for O_LEVEL.SAN where the stored
-	// FOBJ is the 80x800 preview strip at X=320, and FTCH must re-render it
-	// at the current scroll offset each frame.
+	// RA1/RA2: Raw FOBJ data stored by STOR chunk for later re-decoding by FTCH.
 	byte *_storedFobjData;
 	int32 _storedFobjDataSize;
 	int _storedFobjCodec;
-	uint16 _storedFobjParm2; // RA1: FOBJ bytes[12..13] needed by codec 4/5
+	uint16 _storedFobjParm2;
 	int _storedFobjLeft;
 	int _storedFobjTop;
 	int _storedFobjWidth;
 	int _storedFobjHeight;
 
-	// RA1: OBJ\0 embedded cockpit overlay FOBJ — drawn once in frame 0,
-	// saved here and re-rendered every subsequent frame after scene FOBJs.
-	byte *_ra1ObjOverlayData;
-	int32 _ra1ObjOverlayDataSize;
-	int _ra1ObjOverlayCodec;
-	int _ra1ObjOverlayLeft;
-	int _ra1ObjOverlayTop;
-	int _ra1ObjOverlayWidth;
-	int _ra1ObjOverlayHeight;
-
-	// RA1: Viewport scroll offset for interactive gameplay (FUN_224FD at 0x224FD).
-	// Set by InsaneRebel1::procPreRendering(), applied to FOBJ decode positions.
-	int _ra1ViewportOffsetX;
-	int _ra1ViewportOffsetY;
-
 	// RA1/RA2: Most recently decoded FOBJ in the current frame, used by GOST
 	// chunks to re-render the same sprite payload at a different position.
 	byte *_lastFobjData;
@@ -204,6 +182,11 @@ private:
 	int _lastFobjHeight;
 	bool _hasFrameFobjForGost;
 
+	// RA2: Global FOBJ position offsets.
+	// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
+	int16 _fobjOffsetX;
+	int16 _fobjOffsetY;
+
 	Common::String _seekFile;
 	uint32 _startFrame;
 	uint32 _startTime;
@@ -211,18 +194,11 @@ private:
 	uint32 _seekFrame;
 
 	bool _skipNext;
-	bool _ra2FastForwarding;  // Fast-forwarding RA2 BEG video to establish background
 	uint32 _frame;
-	uint32 _fastForwardFromFrame;  // First frame hidden by fast-forward (0 = hide from frame 0)
-	uint32 _fastForwardToFrame;  // RA1: skip display/audio until this frame (0 = disabled)
+	uint32 _fastForwardFromFrame;
+	uint32 _fastForwardToFrame;
 	bool _preserveVideoStateOnNextPlay;
 
-	// RA2: Global FOBJ position offsets (DAT_00482c1c / DAT_00482c20 in original)
-	// Set by InsaneRebel2 during IACT opcode 6 processing, reset in procPostRendering.
-	// Applied to all FOBJ left/top positions during decoding.
-	int16 _fobjOffsetX;
-	int16 _fobjOffsetY;
-
 	Audio::SoundHandle *_IACTchannel;
 	Audio::QueuingAudioStream *_IACTstream;
 
@@ -343,6 +319,11 @@ protected:
 	virtual void handleGameGost(int32 subSize, Common::SeekableReadStream &b) {}
 	virtual void handleGameProcessAudio(int16 feedSize) {}
 	virtual bool isInsaneGame() const { return false; }
+	virtual void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) {}
+	virtual void handleFrameObject(int32 subSize, Common::SeekableReadStream &b);
+	virtual void handleFrame(int32 frameSize, Common::SeekableReadStream &b);
+	virtual void handleGameUpdateScreen(const byte *
+		src, int srcPitch, int width, int height);
 
 private:
 	SmushFont *getFont(int font);
@@ -355,22 +336,16 @@ private:
 	bool readString(const char *file);
 	void decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize = 0, uint8 ra1Param = 0, uint16 ra1Parm2 = 0);
 	void handleAnimHeader(int32 subSize, Common::SeekableReadStream &);
-	void handleFrame(int32 frameSize, Common::SeekableReadStream &);
 	void handleNewPalette(int32 subSize, Common::SeekableReadStream &);
 	void handleZlibFrameObject(int32 subSize, Common::SeekableReadStream &b);
-	void handleFrameObject(int32 subSize, Common::SeekableReadStream &);
 	void handleSAUDChunk(uint8 *srcBuf, uint32 size, int groupId, int vol, int pan, int16 flags, int trkId, int index, int maxFrames);
 	void handleStore(int32 subSize, Common::SeekableReadStream &);
 	void handleFetch(int32 subSize, Common::SeekableReadStream &);
 	void handleIACT(int32 subSize, Common::SeekableReadStream &);
 	void handleTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &);
 	void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
-	void handleLoad(int32 subSize, Common::SeekableReadStream &);  // RA2 only (impl in smush_player_ra2.cpp)
 	void readPalette(byte *, Common::SeekableReadStream &);
 
-	// RA1/RA2 identification (isRA1 still used in handleFrameObject/handleFrame RA1 paths)
-	bool isRA1() const;
-	bool isRA2() const;
 	// RA2 helper methods called from SmushPlayerRebel2 overrides
 	void ra2HandleTextResource(const char *str, int fontId, int color,
 							   int pos_x, int pos_y, int left, int top,
@@ -388,14 +363,6 @@ private:
 	SmushFont *ra1GetFont(int font);
 	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
 
-	// LOAD chunk streaming buffer (RA2 - embedded resource data)
-	byte *_loadBuffer;        // Accumulated LOAD data
-	int32 _loadBufferSize;    // Allocated buffer size
-	int32 _loadBufferOffset;  // Current write position (how much data accumulated)
-	int32 _loadReadOffset;    // Current read position (for streaming consumption)
-	int16 _lastLoadChunkIdx;  // Last processed chunk index (-1 = none)
-	int16 _totalLoadChunks;   // Total chunks expected in current sequence
-
 	void initAudio(int samplerate, int32 maxChunkSize);
 	void terminateAudio();
 	int isChanActive(int flagId);
@@ -411,6 +378,7 @@ private:
 };
 
 class SmushPlayerRebel1 : public SmushPlayer {
+	friend class InsaneRebel1;
 public:
 	SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
 	~SmushPlayerRebel1() override;
@@ -437,6 +405,27 @@ protected:
 	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
 	void handleGameProcessAudio(int16 feedSize) override;
 	bool isInsaneGame() const override { return true; }
+	void handleFrameObject(int32 subSize, Common::SeekableReadStream &b) override;
+	void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
+	void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
+
+	// RA1 clean frame buffer for delta source restoration
+	byte *_ra1CleanFrame;
+	int32 _ra1CleanFrameSize;
+	bool _ra1HasCleanFrame;
+
+	// RA1 OBJ overlay FOBJ — cockpit drawn once frame 0, re-rendered every frame
+	byte *_ra1ObjOverlayData;
+	int32 _ra1ObjOverlayDataSize;
+	int _ra1ObjOverlayCodec;
+	int _ra1ObjOverlayLeft;
+	int _ra1ObjOverlayTop;
+	int _ra1ObjOverlayWidth;
+	int _ra1ObjOverlayHeight;
+
+	// RA1 viewport scroll offset for interactive gameplay
+	int _ra1ViewportOffsetX;
+	int _ra1ViewportOffsetY;
 };
 
 class SmushPlayerRebel2 : public SmushPlayer {
@@ -471,6 +460,18 @@ protected:
 	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
 	void handleGameProcessAudio(int16 feedSize) override;
 	bool isInsaneGame() const override { return true; }
+	void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) override;
+
+private:
+	void handleLoad(int32 subSize, Common::SeekableReadStream &b);
+
+	// LOAD chunk streaming buffer (embedded resource data)
+	byte *_loadBuffer;
+	int32 _loadBufferSize;
+	int32 _loadBufferOffset;
+	int32 _loadReadOffset;
+	int16 _lastLoadChunkIdx;
+	int16 _totalLoadChunks;
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index cbd47b224a1..07f2e312f94 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -25,6 +25,7 @@
 // to upstream while RA1 behavior is isolated in one place.
 
 #include "common/endian.h"
+#include "common/memstream.h"
 
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
@@ -61,6 +62,9 @@ SmushPlayerRebel1::~SmushPlayerRebel1() {
 }
 
 void SmushPlayerRebel1::initGamePlayerFields() {
+	_ra1CleanFrame = nullptr;
+	_ra1CleanFrameSize = 0;
+	_ra1HasCleanFrame = false;
 	_ra1ObjOverlayData = nullptr;
 	_ra1ObjOverlayDataSize = 0;
 	_ra1ObjOverlayCodec = 0;
@@ -76,9 +80,13 @@ void SmushPlayerRebel1::destroyGamePlayerFields() {
 	free(_ra1ObjOverlayData);
 	_ra1ObjOverlayData = nullptr;
 	_ra1ObjOverlayDataSize = 0;
+	free(_ra1CleanFrame);
+	_ra1CleanFrame = nullptr;
+	_ra1CleanFrameSize = 0;
 }
 
 void SmushPlayerRebel1::resetGameVideoState() {
+	_ra1HasCleanFrame = false;
 	free(_ra1ObjOverlayData);
 	_ra1ObjOverlayData = nullptr;
 	_ra1ObjOverlayDataSize = 0;
@@ -105,6 +113,11 @@ void SmushPlayerRebel1::releaseGameVideoState() {
 	free(_ra1ObjOverlayData);
 	_ra1ObjOverlayData = nullptr;
 	_ra1ObjOverlayDataSize = 0;
+
+	free(_ra1CleanFrame);
+	_ra1CleanFrame = nullptr;
+	_ra1CleanFrameSize = 0;
+	_ra1HasCleanFrame = false;
 }
 
 bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -348,4 +361,372 @@ void SmushPlayerRebel1::handleGameProcessAudio(int16 feedSize) {
 	}
 }
 
+// ---------------------------------------------------------------------------
+// handleFrameObject override — RA1 FOBJ has extra fields in the codec word
+// ---------------------------------------------------------------------------
+
+void SmushPlayerRebel1::handleFrameObject(int32 subSize, Common::SeekableReadStream &b) {
+	assert(subSize >= 14);
+	if (_skipNext) {
+		_skipNext = false;
+		return;
+	}
+
+	int codec = b.readUint16LE();
+	uint8 ra1Param = (codec >> 8) & 0xFF;
+	codec &= 0xFF;
+
+	int left = (int)b.readSint16LE();
+	int top = (int)b.readSint16LE();
+	const int rawLeft = left;
+	const int rawTop = top;
+	int width = b.readUint16LE();
+	int height = b.readUint16LE();
+
+	uint16 ra1ObjectId = b.readUint16LE();
+	uint16 ra1Parm2 = b.readUint16LE();
+
+	handleGameFrameObjectPre(codec, left, top, width, height, subSize - 14);
+
+	int32 chunk_size = subSize - 14;
+	byte *chunk_buffer = (byte *)malloc(chunk_size);
+	assert(chunk_buffer);
+	b.read(chunk_buffer, chunk_size);
+
+	handleGameFrameObjectPost(codec, chunk_buffer, chunk_size, left, top, width, height);
+
+	// RA1 STOR: save raw FOBJ with original (pre-clipped) coords and full codec byte
+	if (_storeFrame) {
+		free(_storedFobjData);
+		_storedFobjData = (byte *)malloc(chunk_size);
+		if (_storedFobjData != nullptr) {
+			memcpy(_storedFobjData, chunk_buffer, chunk_size);
+			_storedFobjDataSize = chunk_size;
+			_storedFobjCodec = codec | ((int)ra1Param << 8);
+			_storedFobjParm2 = ra1Parm2;
+			_storedFobjLeft = rawLeft;
+			_storedFobjTop = rawTop;
+			_storedFobjWidth = width;
+			_storedFobjHeight = height;
+		} else {
+			_storedFobjDataSize = 0;
+		}
+		_storeFrame = false;
+	}
+
+	// RA1 target check — Insane can reject certain FOBJs
+	if (_insane) {
+		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+		if (!rebel1->handleFrameObjectTarget((int16)ra1ObjectId, (int16)rawLeft, (int16)rawTop,
+				(int16)width, (int16)height, codec, ra1Param)) {
+			free(chunk_buffer);
+			return;
+		}
+	}
+
+	decodeFrameObject(codec, chunk_buffer, left, top, width, height, chunk_size, ra1Param, ra1Parm2);
+	free(chunk_buffer);
+}
+
+// ---------------------------------------------------------------------------
+// handleFrame override — RA1 frame parsing with alignment, OBJ chunks, clean frame
+// ---------------------------------------------------------------------------
+
+static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
+	const int64 frameStart = b.pos();
+	int32 remaining = frameSize;
+
+	while (remaining > 1) {
+		if ((b.pos() & 1) && remaining > 0) {
+			const byte pad = b.readByte();
+			if (pad == 0) {
+				remaining--;
+			} else {
+				b.seek(-1, SEEK_CUR);
+			}
+		}
+		if (remaining < 8)
+			break;
+
+		const uint32 subType = b.readUint32BE();
+		const int32 subSize = b.readUint32BE();
+		const int64 subDataPos = b.pos();
+
+		if (subType == MKTAG('F', 'R', 'M', 'E'))
+			break;
+		if (subType == MKTAG('G', 'A', 'M', 'E')) {
+			b.seek(frameStart, SEEK_SET);
+			return true;
+		}
+
+		remaining -= subSize + 8;
+		b.seek(subDataPos + subSize, SEEK_SET);
+	}
+
+	b.seek(frameStart, SEEK_SET);
+	return false;
+}
+
+void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
+	debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame(%d)", _frame);
+	uint8 *audioChunk = nullptr;
+	_skipNext = false;
+	handleGameFrameStart();
+
+	bool preserveFrameHistory = false;
+	if (_insane) {
+		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+		bool interactive = rebel1->isInteractiveVideoActive();
+		const uint16 activeOpcode = rebel1->getActiveGameOpcode();
+		bool forceClear = interactive &&
+			(activeOpcode == 0x0B ||
+			 (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
+
+		preserveFrameHistory = interactive && !forceClear &&
+			ra1FrameHasGameChunk(b, frameSize);
+	}
+
+	// Restore clean frame for delta source
+	if (preserveFrameHistory &&
+		_ra1HasCleanFrame && _ra1CleanFrame &&
+		_dst && _width > 0 && _height > 0) {
+		const int frameBytes = _width * _height;
+		if (_ra1CleanFrameSize >= frameBytes)
+			memcpy(_dst, _ra1CleanFrame, frameBytes);
+	}
+
+	if (_insanity)
+		_insane->procPreRendering(_dst);
+
+	// Clear buffer for non-interactive frames to avoid trails
+	if (_dst && _width > 0 && _height > 0) {
+		if (!preserveFrameHistory)
+			memset(_dst, 0, _width * _height);
+	}
+
+	while (frameSize > 0) {
+		// RA1 exits when <=1 byte remains
+		if (frameSize <= 1) {
+			if (frameSize == 1)
+				b.skip(1);
+			break;
+		}
+
+		// RA1 top-of-loop alignment (FUN_1FDBC)
+		if ((b.pos() & 1) && frameSize > 0) {
+			byte peek = b.readByte();
+			if (peek == 0) {
+				frameSize--;
+			} else {
+				b.seek(-1, SEEK_CUR);
+			}
+		}
+
+		if (frameSize < 8) {
+			b.skip(frameSize);
+			break;
+		}
+
+		uint32 subType = b.readUint32BE();
+		int32 subSize = b.readUint32BE();
+		int32 subOffset = b.pos();
+
+		// Guard against consuming next frame marker
+		if (subType == MKTAG('F','R','M','E')) {
+			b.seek(-8, SEEK_CUR);
+			break;
+		}
+
+		switch (subType) {
+		case MKTAG('N','P','A','L'):
+			handleNewPalette(subSize, b);
+			break;
+		case MKTAG('F','O','B','J'):
+			handleFrameObject(subSize, b);
+			break;
+		case MKTAG('Z','F','O','B'):
+			handleZlibFrameObject(subSize, b);
+			break;
+		case MKTAG('P','S','A','D'):
+		case MKTAG('P','V','O','C'):
+			if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
+				audioChunk = (uint8 *)malloc(subSize + 8);
+				b.seek(-8, SEEK_CUR);
+				b.read(audioChunk, subSize + 8);
+				feedAudio(audioChunk, 0, 127, 0, 0);
+				free(audioChunk);
+				audioChunk = nullptr;
+			}
+			break;
+		case MKTAG('T','R','E','S'):
+		case MKTAG('T','E','X','T'):
+			handleTextResource(subType, subSize, b);
+			break;
+		case MKTAG('X','P','A','L'):
+			handleDeltaPalette(subSize, b);
+			break;
+		case MKTAG('I','A','C','T'):
+			handleIACT(subSize, b);
+			break;
+		case MKTAG('S','T','O','R'):
+			handleStore(subSize, b);
+			break;
+		case MKTAG('F','T','C','H'):
+			handleFetch(subSize, b);
+			break;
+		case MKTAG('S','K','I','P'):
+			_insane->procSKIP(subSize, b);
+			break;
+		case MKTAG('G','O','S','T'):
+			handleGameGost(subSize, b);
+			break;
+		case MKTAG('G','A','M','E'): {
+			InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+			rebel1->handleGameChunk(subSize, b);
+			break;
+		}
+		case MKTAG('O','B','J','\0'): {
+			// RA1 object overlay chunk: variable-size header + embedded FOBJ/GAME/PSAD.
+			int32 objDataSize = frameSize - 8;
+			if (objDataSize > 0) {
+				byte *objBuf = (byte *)malloc(objDataSize);
+				b.read(objBuf, objDataSize);
+
+				int32 objPos = 0;
+				while (objPos + 8 < objDataSize) {
+					uint32 embTag = READ_BE_UINT32(objBuf + objPos);
+					uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
+					int32 embRemaining = objDataSize - objPos - 8;
+
+					bool recognized = (embTag == MKTAG('F','O','B','J') ||
+					                   embTag == MKTAG('G','A','M','E') ||
+					                   embTag == MKTAG('P','S','A','D'));
+
+					if (!recognized || embSize > (uint32)embRemaining) {
+						objPos++;
+						continue;
+					}
+
+					if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
+						Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+						handleFrameObject(embSize, embStream);
+
+						if (_ra1ObjOverlayData == nullptr ||
+						    (int32)embSize > _ra1ObjOverlayDataSize) {
+							free(_ra1ObjOverlayData);
+							_ra1ObjOverlayDataSize = embSize;
+							_ra1ObjOverlayData = (byte *)malloc(embSize);
+							memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
+							_ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
+							_ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
+							_ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
+							_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
+							_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
+						}
+					} else if (embTag == MKTAG('G','A','M','E')) {
+						Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+						InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+						rebel1->handleGameChunk(embSize, embStream);
+					} else if (embTag == MKTAG('P','S','A','D')) {
+						if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
+							uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
+							memcpy(audioBuf, objBuf + objPos, embSize + 8);
+							feedAudio(audioBuf, 0, 127, 0, 0);
+							free(audioBuf);
+						}
+					}
+
+					objPos += 8 + embSize;
+					if (embSize & 1)
+						objPos++;
+				}
+				free(objBuf);
+			}
+			frameSize = 0;
+			continue;
+		}
+		case MKTAG('G','A','M','2'):
+		case MKTAG('F','A','D','E'):
+		case MKTAG('S','E','G','A'):
+		case MKTAG('A','D','L',' '):
+		case MKTAG('A','D','L','2'):
+		case MKTAG('S','B','L',' '):
+		case MKTAG('S','B','L','2'):
+		case MKTAG('P','S','D','2'):
+			debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame: skipping chunk %s (%d bytes)", tag2str(subType), subSize);
+			break;
+		default: {
+			// Original FUN_1FDBC: unknown uppercase tag → silently stop
+			byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
+			byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
+			if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
+			    tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
+				debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
+				frameSize = 0;
+				continue;
+			}
+			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
+		}
+		}
+
+		frameSize -= subSize + 8;
+		b.seek(subOffset + subSize, SEEK_SET);
+		// RA1 uses top-of-loop alignment, not bottom-of-loop padding
+	}
+
+	// Re-render cockpit overlay
+	if (_ra1ObjOverlayData != nullptr && _frame > 0) {
+		Common::MemoryReadStream overlayStream(_ra1ObjOverlayData, _ra1ObjOverlayDataSize);
+		handleFrameObject(_ra1ObjOverlayDataSize, overlayStream);
+	}
+
+	// Save clean frame for next delta
+	if (preserveFrameHistory && _dst && _width > 0 && _height > 0) {
+		const int frameBytes = _width * _height;
+		byte *newClean = (byte *)realloc(_ra1CleanFrame, frameBytes);
+		if (newClean != nullptr) {
+			_ra1CleanFrame = newClean;
+			_ra1CleanFrameSize = frameBytes;
+			memcpy(_ra1CleanFrame, _dst, frameBytes);
+			_ra1HasCleanFrame = true;
+		} else {
+			_ra1HasCleanFrame = false;
+		}
+	} else {
+		_ra1HasCleanFrame = false;
+	}
+
+	if (_insanity)
+		_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
+
+	if (_width != 0 && _height != 0)
+		updateScreen();
+
+	_frame++;
+}
+
+// ---------------------------------------------------------------------------
+// handleGameUpdateScreen — RA1 viewport-aware screen blit
+// ---------------------------------------------------------------------------
+
+void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) {
+	if (_dst == nullptr || _width <= 0 || _height <= 0)
+		return;
+
+	int ra1ViewX = _ra1ViewportOffsetX;
+	int ra1ViewY = _ra1ViewportOffsetY;
+
+	const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
+	const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
+
+	int frameWidth = MIN(_width - srcX, _vm->_screenWidth);
+	int frameHeight = MIN(_height - srcY, _vm->_screenHeight);
+	if (frameWidth <= 0 || frameHeight <= 0)
+		return;
+
+	const byte *dst = _dst + srcY * _width + srcX;
+
+	SmushPlayer::handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 8feab912d52..4c7f8eb3bc3 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -39,14 +39,6 @@
 
 namespace Scumm {
 
-bool SmushPlayer::isRA1() const {
-	return _vm->_game.id == GID_REBEL1;
-}
-
-bool SmushPlayer::isRA2() const {
-	return _vm->_game.id == GID_REBEL2;
-}
-
 // Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
 void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
@@ -87,17 +79,7 @@ void SmushPlayerRebel2::initGamePlayerFields() {
 	_lastFobjWidth = 0;
 	_lastFobjHeight = 0;
 	_hasFrameFobjForGost = false;
-	_ra1ObjOverlayData = nullptr;
-	_ra1ObjOverlayDataSize = 0;
-	_ra1ObjOverlayCodec = 0;
-	_ra1ObjOverlayLeft = 0;
-	_ra1ObjOverlayTop = 0;
-	_ra1ObjOverlayWidth = 0;
-	_ra1ObjOverlayHeight = 0;
-	_ra1ViewportOffsetX = 0;
-	_ra1ViewportOffsetY = 0;
 	_skipNext = false;
-	_ra2FastForwarding = false;
 	_fobjOffsetX = 0;
 	_fobjOffsetY = 0;
 	_storeFrame = false;
@@ -120,8 +102,6 @@ void SmushPlayerRebel2::destroyGamePlayerFields() {
 	_lastFobjData = nullptr;
 	free(_loadBuffer);
 	_loadBuffer = nullptr;
-	free(_ra1ObjOverlayData);
-	_ra1ObjOverlayData = nullptr;
 }
 
 /**
@@ -167,9 +147,6 @@ void SmushPlayerRebel2::releaseGameVideoState() {
 	free(_lastFobjData);
 	_lastFobjData = nullptr;
 	_lastFobjDataSize = 0;
-	free(_ra1ObjOverlayData);
-	_ra1ObjOverlayData = nullptr;
-	_ra1ObjOverlayDataSize = 0;
 	_hasFrameFobjForGost = false;
 	// Preserve _frameBuffer across videos so that gameplay videos (which have no
 	// background FOBJ) can use the stored background from the previous BEG video.
@@ -283,13 +260,17 @@ bool SmushPlayerRebel2::handleGameAnimHeader(byte *headerContent) {
 // RA2 helper methods (still on SmushPlayer for now, used by base class)
 // ---------------------------------------------------------------------------
 
+void SmushPlayerRebel2::handleGameLoad(int32 subSize, Common::SeekableReadStream &b) {
+	handleLoad(subSize, b);
+}
+
 /**
  * Handle LOAD chunk for Rebel Assault 2.
  *
  * LOAD chunks stream embedded resource data across multiple frames.
  * The data is accumulated in a buffer and consumed by the audio system.
  */
-void SmushPlayer::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
+void SmushPlayerRebel2::handleLoad(int32 subSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayer::handleLoad()");
 
 	if (subSize < 10) {


Commit: f3e88ea73219edd5ad3a271a77e5246867dc7e81
    https://github.com/scummvm/scummvm/commit/f3e88ea73219edd5ad3a271a77e5246867dc7e81
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00

Commit Message:
SCUMM: RA: Move text and GOST handlers into RA SMUSH players

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra1.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0a010f80aa3..2f154e93fe1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -1535,99 +1535,6 @@ SmushFont *SmushPlayer::getFont(int font) {
 	return _sf[font];
 }
 
-SmushFont *SmushPlayer::ra1GetFont(int font) {
-	const char *ra1Fonts[] = {
-		"SYS/TALKFONT.NUT",
-		"SYS/TECHFONT.NUT",
-		"SYS/TITLFONT.NUT",
-		"SYS/DISPLAY.NUT"
-	};
-	const char *ra2FallbackFonts[] = {
-		"SYSTM/TALKFONT.NUT",
-		"SYSTM/SMALFONT.NUT",
-		"SYSTM/TITLFONT.NUT",
-		"SYSTM/SMALFONT.NUT"
-	};
-
-	int numFonts = ARRAYSIZE(ra1Fonts);
-	if (font < 0 || font >= numFonts) {
-		debugC(DEBUG_SMUSH, "SmushPlayer::ra1GetFont: unknown font %d, using TALKFONT", font);
-		font = 0;
-	}
-
-	if (_sf[font])
-		return _sf[font];
-
-	const char *fontPath = ra1Fonts[font];
-	ScummFile *testFile = _vm->instantiateScummFile();
-	bool ok = _vm->openFile(*testFile, Common::Path(fontPath));
-	if (ok)
-		testFile->close();
-	delete testFile;
-
-	if (!ok)
-		fontPath = ra2FallbackFonts[font];
-
-	_sf[font] = new SmushFont(_vm, fontPath, true);
-	return _sf[font];
-}
-
-void SmushPlayer::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
-	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
-		return;
-
-	InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
-	if (!rebel1)
-		return;
-
-	const int textAnchorX = b.readSint32BE();
-	int cursorY = b.readSint32BE();
-
-	int textLen = subSize - 8;
-	if (textLen <= 0)
-		return;
-
-	byte *textBuf = (byte *)malloc(textLen);
-	if (!textBuf)
-		return;
-	b.read(textBuf, textLen);
-
-	int start = 0;
-	if (textLen > 0 && textBuf[0] == '.')
-		start = 1;
-
-	int remaining = textLen - start;
-	while (remaining > 0) {
-		int lineLen = 0;
-		while (lineLen < remaining && textBuf[start + lineLen] != 0)
-			lineLen++;
-
-		if (lineLen > 0) {
-			char *line = (char *)malloc(lineLen + 1);
-			if (!line) {
-				cursorY += 12;
-			} else {
-				memcpy(line, textBuf + start, lineLen);
-				line[lineLen] = '\0';
-				const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
-				rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
-				cursorY += rebel1->getFontBankLineAdvance(line);
-				free(line);
-			}
-		} else {
-			cursorY += rebel1->getFontBankLineAdvance(nullptr);
-		}
-
-		int consumed = lineLen;
-		if (consumed < remaining && textBuf[start + consumed] == 0)
-			consumed++;
-		start += consumed;
-		remaining -= consumed;
-	}
-
-	free(textBuf);
-}
-
 void SmushPlayer::parseNextFrame() {
 
 	if (_seekPos >= 0) {
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index ce56b23bbc5..22f2665069e 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -346,22 +346,10 @@ private:
 	void handleDeltaPalette(int32 subSize, Common::SeekableReadStream &);
 	void readPalette(byte *, Common::SeekableReadStream &);
 
-	// RA2 helper methods called from SmushPlayerRebel2 overrides
-	void ra2HandleTextResource(const char *str, int fontId, int color,
-							   int pos_x, int pos_y, int left, int top,
-							   int width, int height, TextStyleFlags flg);
-	void ra2SelectFrameBuffer(int width, int height);
-	void ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY = nullptr);
-	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
-						int width, int height, int pitch, int dataSize);
-	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+	// Shared RA1/RA2 helpers (access _storedFobj*/_lastFobj* on base)
+	void adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY = nullptr);
+	void rememberLastFobj(int codec, const byte *data, int32 dataSize,
 						  int left, int top, int width, int height);
-	void ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
-							 int left, int top, int width, int height);
-	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
-	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
-	SmushFont *ra1GetFont(int font);
-	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
 
 	void initAudio(int samplerate, int32 maxChunkSize);
 	void terminateAudio();
@@ -409,6 +397,11 @@ protected:
 	void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
 	void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
 
+private:
+	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
+	SmushFont *ra1GetFont(int font);
+	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+
 	// RA1 clean frame buffer for delta source restoration
 	byte *_ra1CleanFrame;
 	int32 _ra1CleanFrameSize;
@@ -464,6 +457,15 @@ protected:
 
 private:
 	void handleLoad(int32 subSize, Common::SeekableReadStream &b);
+	void ra2HandleTextResource(const char *str, int fontId, int color,
+							   int pos_x, int pos_y, int left, int top,
+							   int width, int height, TextStyleFlags flg);
+	void ra2SelectFrameBuffer(int width, int height);
+	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+						int width, int height, int pitch, int dataSize);
+	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+						  int left, int top, int width, int height);
+	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
 
 	// LOAD chunk streaming buffer (embedded resource data)
 	byte *_loadBuffer;
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 07f2e312f94..4846ad016f0 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -174,9 +174,9 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 	return true;
 }
 
-void SmushPlayer::ra1HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+void SmushPlayerRebel1::ra1HandleGost(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 12) {
-		warning("SmushPlayer::ra1HandleGost: chunk too small (%d bytes)", subSize);
+		warning("SmushPlayerRebel1::ra1HandleGost: chunk too small (%d bytes)", subSize);
 		return;
 	}
 
@@ -291,7 +291,7 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
 	// RA1 additive codec (SKIP_RLE) uses original coords, not adjusted
 	if (codec == SMUSH_CODEC_SKIP_RLE)
 		return false;
-	ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+	adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
 	return true;
 }
 
@@ -342,7 +342,7 @@ void SmushPlayerRebel1::handleGameFrameObjectPre(int codec, int left, int top, i
 }
 
 void SmushPlayerRebel1::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
-	ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+	rememberLastFobj(codec, data, dataSize, left, top, width, height);
 	// RA1 STOR handling remains in handleFrameObject (needs ra1Param/rawLeft/rawTop)
 }
 
@@ -729,4 +729,97 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
 	SmushPlayer::handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
 }
 
+SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
+	const char *ra1Fonts[] = {
+		"SYS/TALKFONT.NUT",
+		"SYS/TECHFONT.NUT",
+		"SYS/TITLFONT.NUT",
+		"SYS/DISPLAY.NUT"
+	};
+	const char *ra2FallbackFonts[] = {
+		"SYSTM/TALKFONT.NUT",
+		"SYSTM/SMALFONT.NUT",
+		"SYSTM/TITLFONT.NUT",
+		"SYSTM/SMALFONT.NUT"
+	};
+
+	int numFonts = ARRAYSIZE(ra1Fonts);
+	if (font < 0 || font >= numFonts) {
+		debugC(DEBUG_SMUSH, "SmushPlayerRebel1::ra1GetFont: unknown font %d, using TALKFONT", font);
+		font = 0;
+	}
+
+	if (_sf[font])
+		return _sf[font];
+
+	const char *fontPath = ra1Fonts[font];
+	ScummFile *testFile = _vm->instantiateScummFile();
+	bool ok = _vm->openFile(*testFile, Common::Path(fontPath));
+	if (ok)
+		testFile->close();
+	delete testFile;
+
+	if (!ok)
+		fontPath = ra2FallbackFonts[font];
+
+	_sf[font] = new SmushFont(_vm, fontPath, true);
+	return _sf[font];
+}
+
+void SmushPlayerRebel1::ra1HandleText(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 8 || !_dst || _width <= 0 || _height <= 0)
+		return;
+
+	InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
+	if (!rebel1)
+		return;
+
+	const int textAnchorX = b.readSint32BE();
+	int cursorY = b.readSint32BE();
+
+	int textLen = subSize - 8;
+	if (textLen <= 0)
+		return;
+
+	byte *textBuf = (byte *)malloc(textLen);
+	if (!textBuf)
+		return;
+	b.read(textBuf, textLen);
+
+	int start = 0;
+	if (textLen > 0 && textBuf[0] == '.')
+		start = 1;
+
+	int remaining = textLen - start;
+	while (remaining > 0) {
+		int lineLen = 0;
+		while (lineLen < remaining && textBuf[start + lineLen] != 0)
+			lineLen++;
+
+		if (lineLen > 0) {
+			char *line = (char *)malloc(lineLen + 1);
+			if (!line) {
+				cursorY += 12;
+			} else {
+				memcpy(line, textBuf + start, lineLen);
+				line[lineLen] = '\0';
+				const int drawX = textAnchorX - (rebel1->getFontBankStringWidth(line) / 2);
+				rebel1->drawFontBankString(_dst, _width, _width, _height, drawX, cursorY, line);
+				cursorY += rebel1->getFontBankLineAdvance(line);
+				free(line);
+			}
+		} else {
+			cursorY += rebel1->getFontBankLineAdvance(nullptr);
+		}
+
+		int consumed = lineLen;
+		if (consumed < remaining && textBuf[start + consumed] == 0)
+			consumed++;
+		start += consumed;
+		remaining -= consumed;
+	}
+
+	free(textBuf);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 4c7f8eb3bc3..e59e6831254 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -346,7 +346,7 @@ void SmushPlayer::ensureMultiFont() {
 /**
  * RA2-specific text rendering using SmushMultiFont for inline font switching.
  */
-void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
+void SmushPlayerRebel2::ra2HandleTextResource(const char *str, int fontId, int color,
 										int pos_x, int pos_y, int left, int top,
 										int width, int height, TextStyleFlags flg) {
 	ensureMultiFont();
@@ -368,7 +368,7 @@ void SmushPlayer::ra2HandleTextResource(const char *str, int fontId, int color,
  * RA2-specific buffer selection for non-standard FOBJ dimensions.
  * Returns the destination buffer to use and updates _dst, _width, _height.
  */
-void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
+void SmushPlayerRebel2::ra2SelectFrameBuffer(int width, int height) {
 	// Rebel2 uses a special buffer for all non-matching frames.
 	// Level 1: First frame is 424x260 (background), small sprites reuse same buffer
 	// Level 2: Uses virtual screen directly (handled below when _specialBuffer stays null)
@@ -412,7 +412,7 @@ void SmushPlayer::ra2SelectFrameBuffer(int width, int height) {
  * When srcSkipY is non-null, outputs the number of source rows to skip
  * when top is clipped from negative (for codecs with row-size prefixes).
  */
-void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+void SmushPlayer::adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
 	left += _fobjOffsetX;
 	top += _fobjOffsetY;
 
@@ -439,7 +439,7 @@ void SmushPlayer::ra2AdjustFrameCoords(int &left, int &top, int &width, int &hei
  * Dispatch to RA2-specific codec functions.
  * Returns true if the codec was handled, false for standard codecs.
  */
-bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+bool SmushPlayerRebel2::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
 								 int width, int height, int pitch, int dataSize) {
 	switch (codec) {
 	case SMUSH_CODEC_LINE_UPDATE:
@@ -460,7 +460,7 @@ bool SmushPlayer::ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
 /**
  * Save raw FOBJ data when STOR is pending (for later re-decoding by FTCH).
  */
-void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+void SmushPlayerRebel2::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
 								   int left, int top, int width, int height) {
 	free(_storedFobjData);
 	_storedFobjData = (byte *)malloc(dataSize);
@@ -478,7 +478,7 @@ void SmushPlayer::ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
 /**
  * Cache the most recent frame FOBJ for GOST re-rendering.
  */
-void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSize,
+void SmushPlayer::rememberLastFobj(int codec, const byte *data, int32 dataSize,
 									  int left, int top, int width, int height) {
 	if (dataSize <= 0) {
 		_hasFrameFobjForGost = false;
@@ -487,7 +487,7 @@ void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSiz
 
 	byte *newData = (byte *)realloc(_lastFobjData, dataSize);
 	if (newData == nullptr) {
-		warning("SmushPlayer::ra2RememberLastFobj: Failed to allocate %d bytes", dataSize);
+		warning("SmushPlayer::rememberLastFobj: Failed to allocate %d bytes", dataSize);
 		free(_lastFobjData);
 		_lastFobjData = nullptr;
 		_lastFobjDataSize = 0;
@@ -510,9 +510,9 @@ void SmushPlayer::ra2RememberLastFobj(int codec, const byte *data, int32 dataSiz
  * RA2 GOST chunk handler.
  * Re-renders the most recent frame FOBJ at the supplied ghost position.
  */
-void SmushPlayer::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
+void SmushPlayerRebel2::ra2HandleGost(int32 subSize, Common::SeekableReadStream &b) {
 	if (subSize < 6) {
-		warning("SmushPlayer::ra2HandleGost: chunk too small (%d bytes)", subSize);
+		warning("SmushPlayerRebel2::ra2HandleGost: chunk too small (%d bytes)", subSize);
 		return;
 	}
 
@@ -581,7 +581,7 @@ bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int he
 }
 
 bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
-	ra2AdjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+	adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
 	return true;
 }
 
@@ -602,7 +602,7 @@ void SmushPlayerRebel2::handleGameFrameObjectPre(int codec, int left, int top, i
 }
 
 void SmushPlayerRebel2::handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {
-	ra2RememberLastFobj(codec, data, dataSize, left, top, width, height);
+	rememberLastFobj(codec, data, dataSize, left, top, width, height);
 
 	if (_storeFrame) {
 		ra2StoreFobjData(codec, data, dataSize, left, top, width, height);


Commit: cbbdde480bac5a130c9358263c94b26cf6c685ca
    https://github.com/scummvm/scummvm/commit/cbbdde480bac5a130c9358263c94b26cf6c685ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00

Commit Message:
SCUMM: RA: Move RA1 codec 1 decoder into RA player

Changed paths:
    engines/scumm/smush/codec1.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/codec1.cpp b/engines/scumm/smush/codec1.cpp
index 7c6efb9bd61..087e4d53aaf 100644
--- a/engines/scumm/smush/codec1.cpp
+++ b/engines/scumm/smush/codec1.cpp
@@ -36,48 +36,4 @@ void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, in
 	} while (--height);
 }
 
-
-/**
- * RA1 codec 1: RLE with transparency on pixel 0.
- * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
- * allowing the background (restored via FTCH) to show through.
- * Matches FFmpeg's old_codec1() with opaque=0.
- */
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch;
-	do {
-		byte *rowDst = dst + left;
-		const byte *lineData = src + 2;
-		int remaining = width;
-
-		while (remaining > 0) {
-			byte code = *lineData++;
-			byte num = (code >> 1) + 1;
-			if (num > remaining)
-				num = remaining;
-			if (code & 1) {
-				// Fill: repeat single byte
-				byte color = *lineData++;
-				if (color != 0) {
-					memset(rowDst, color, num);
-				}
-				// If color == 0: skip (transparent)
-			} else {
-				// Copy: write each byte, skipping 0
-				for (int j = 0; j < num; j++) {
-					byte c = lineData[j];
-					if (c != 0)
-						rowDst[j] = c;
-				}
-				lineData += num;
-			}
-			rowDst += num;
-			remaining -= num;
-		}
-
-		src += READ_LE_UINT16(src) + 2;
-		dst += pitch;
-	} while (--height);
-}
-
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 2f154e93fe1..0ed39b3ae9d 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -891,10 +891,7 @@ byte *SmushPlayer::getVideoPalette() {
 }
 
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
 
 /**
  * RA1 codec 21: Skip/copy line codec (FUN_10D41).
@@ -1039,143 +1036,6 @@ void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bu
  */
 
 // Persistent tile table state for RA1 codec 4/5
-static uint8 s_ra1C4Tbl[2][256][16];
-static uint16 s_ra1C4Param = 0xFFFF;
-
-static void ra1Codec4GenTiles(uint16 param1) {
-	uint8 *dst = &s_ra1C4Tbl[0][0][0];
-
-	for (int i = 1; i < 16; i += 2) {
-		for (int k = 0; k < 16; k++) {
-			int j = i + param1;
-			int l = k + param1;
-			int m = (j + l) / 2;
-			int n = (j + m) / 2;
-			int o = (l + m) / 2;
-			if (j == m || l == m) {
-				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
-				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
-				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
-				*dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
-			} else {
-				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
-				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
-				*dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
-				*dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
-			}
-		}
-	}
-
-	for (int i = 0; i < 16; i += 2) {
-		for (int k = 0; k < 16; k++) {
-			int j = i + param1;
-			int l = k + param1;
-			int m = (j + l) / 2;
-			int n = (j + m) / 2;
-			int o = (l + m) / 2;
-			if (m == j || m == l) {
-				*dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
-				*dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
-				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
-				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
-			} else {
-				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
-				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
-				*dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
-				*dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
-			}
-		}
-	}
-}
-
-static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
-	uint8 *dst = &s_ra1C4Tbl[1][0][0];
-	int loop = param2 * 8;
-
-	if (param2 > 256 || remaining < loop)
-		return false;
-
-	for (int i = 0; i < loop; i++) {
-		byte c = *src++;
-		remaining--;
-		*dst++ = (c >> 4) + clr;
-		*dst++ = (c & 0xF) + clr;
-	}
-	return true;
-}
-
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
-						 int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
-	const int mx = pitch;  // framebuffer width
-	const int my = height; // framebuffer height
-
-	// Generate dither tile table if palette base changed
-	if (s_ra1C4Param != param) {
-		ra1Codec4GenTiles(param);
-		s_ra1C4Param = param;
-	}
-
-	// Load frame-specific tiles from data stream (4bpp nibble-split)
-	int remaining = dataSize;
-	const byte *data = src;
-	if (parm2 > 0) {
-		if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
-			warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
-			return;
-		}
-	}
-
-	// Decode blocks: iterate columns by 4, then rows by 4 (column-major order)
-	for (int j = 0; j < width; j += 4) {
-		byte mask = 0, bits = 0;
-		int x = left + j;
-		for (int i = 0; i < height; i += 4) {
-			int y = top + i;
-			int bit = 0;
-
-			if (parm2 > 0) {
-				if (bits == 0) {
-					if (remaining < 1)
-						return;
-					mask = *data++;
-					remaining--;
-					bits = 8;
-				}
-				bit = !!(mask & 0x80);
-				mask <<= 1;
-				bits--;
-			}
-
-			if (remaining < 1)
-				return;
-			byte idx = *data++;
-			remaining--;
-
-			// Codec 4: index 0x80 = skip (delta). Codec 5: no skip.
-			if (bit == 0 && idx == 0x80 && codec != 5)
-				continue;
-			if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
-				continue;
-
-			const byte *gs = &s_ra1C4Tbl[bit][idx][0];
-			if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
-				// Fast path: fully within bounds
-				for (int k = 0; k < 4; k++, gs += 4)
-					memcpy(dst + x + (y + k) * pitch, gs, 4);
-			} else {
-				// Slow path: clipping
-				for (int k = 0; k < 4; k++) {
-					for (int l = 0; l < 4; l++, gs++) {
-						int yo = y + k, xo = x + l;
-						if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
-							*(dst + yo * pitch + xo) = *gs;
-					}
-				}
-			}
-		}
-	}
-}
-
 void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize, uint8 ra1Param, uint16 ra1Parm2) {
 	if ((height == 242) && (width == 384)) {
 		int bufSize = 242 * 384;
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 4846ad016f0..e25d29c267b 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -255,13 +255,155 @@ void SmushPlayerRebel1::handleGameParseNextFrame() {
 }
 
 // Forward declarations for RA1 codec functions (defined in smush_player.cpp and codec1.cpp)
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+/**
+ * RA1 codec 1: RLE with transparency on pixel 0.
+ * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
+ * allowing the background (restored via FTCH) to show through.
+ */
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch;
+	do {
+		byte *rowDst = dst + left;
+		const byte *lineData = src + 2;
+		int remaining = width;
+
+		while (remaining > 0) {
+			byte code = *lineData++;
+			byte num = (code >> 1) + 1;
+			if (num > remaining)
+				num = remaining;
+			if (code & 1) {
+				byte color = *lineData++;
+				if (color != 0)
+					memset(rowDst, color, num);
+			} else {
+				for (int j = 0; j < num; j++) {
+					byte c = lineData[j];
+					if (c != 0)
+						rowDst[j] = c;
+				}
+				lineData += num;
+			}
+			rowDst += num;
+			remaining -= num;
+		}
+
+		src += READ_LE_UINT16(src) + 2;
+		dst += pitch;
+	} while (--height);
+}
 void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
 	int pitch, int bufWidth, int bufHeight, uint8 param);
 void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2, int codec);
+// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
+static uint8 s_ra1C4Tbl[2][256][16];
+static uint16 s_ra1C4Param = 0xFFFF;
+
+static void ra1Codec4GenTiles(uint16 param1) {
+	uint8 *dst = &s_ra1C4Tbl[0][0][0];
+	for (int i = 1; i < 16; i += 2) {
+		for (int k = 0; k < 16; k++) {
+			int j = i + param1, l = k + param1;
+			int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+			if (j == m || l == m) {
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
+			} else {
+				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+				*dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
+				*dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
+			}
+		}
+	}
+	for (int i = 0; i < 16; i += 2) {
+		for (int k = 0; k < 16; k++) {
+			int j = i + param1, l = k + param1;
+			int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+			if (m == j || m == l) {
+				*dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
+				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
+			} else {
+				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+				*dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
+				*dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
+			}
+		}
+	}
+}
+
+static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
+	uint8 *dst = &s_ra1C4Tbl[1][0][0];
+	int loop = param2 * 8;
+	if (param2 > 256 || remaining < loop)
+		return false;
+	for (int i = 0; i < loop; i++) {
+		byte c = *src++;
+		remaining--;
+		*dst++ = (c >> 4) + clr;
+		*dst++ = (c & 0xF) + clr;
+	}
+	return true;
+}
+
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+						 int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
+	const int mx = pitch;
+	const int my = height;
+	if (s_ra1C4Param != param) {
+		ra1Codec4GenTiles(param);
+		s_ra1C4Param = param;
+	}
+	int remaining = dataSize;
+	const byte *data = src;
+	if (parm2 > 0) {
+		if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
+			warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
+			return;
+		}
+	}
+	for (int j = 0; j < width; j += 4) {
+		byte mask = 0, bits = 0;
+		int x = left + j;
+		for (int i = 0; i < height; i += 4) {
+			int y = top + i;
+			int bit = 0;
+			if (parm2 > 0) {
+				if (bits == 0) {
+					if (remaining < 1) return;
+					mask = *data++; remaining--; bits = 8;
+				}
+				bit = !!(mask & 0x80);
+				mask <<= 1; bits--;
+			}
+			if (remaining < 1) return;
+			byte idx = *data++; remaining--;
+			if (bit == 0 && idx == 0x80 && codec != 5)
+				continue;
+			if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
+				continue;
+			const byte *gs = &s_ra1C4Tbl[bit][idx][0];
+			if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
+				for (int k = 0; k < 4; k++, gs += 4)
+					memcpy(dst + x + (y + k) * pitch, gs, 4);
+			} else {
+				for (int k = 0; k < 4; k++)
+					for (int l = 0; l < 4; l++, gs++) {
+						int yo = y + k, xo = x + l;
+						if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
+							*(dst + yo * pitch + xo) = *gs;
+					}
+			}
+		}
+	}
+}
 
 bool SmushPlayerRebel1::handleGameFrameBufferSelect(int codec, int width, int height) {
 	// RA1 sub-fullscreen frames render into _specialBuffer at their (left, top) offset position.


Commit: f854095da73d2b7f6579b8486f1b615251ee12a1
    https://github.com/scummvm/scummvm/commit/f854095da73d2b7f6579b8486f1b615251ee12a1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:47+02:00

Commit Message:
SCUMM: RA: Move RA1 auxiliary decoders into RA player

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0ed39b3ae9d..07d97275f32 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -44,8 +44,6 @@
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel2.h"
-#include "scumm/insane/insane_rebel1.h"
 
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
@@ -893,149 +891,6 @@ byte *SmushPlayer::getVideoPalette() {
 void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
-/**
- * RA1 codec 21: Skip/copy line codec (FUN_10D41).
- *
- * Each line: [u16 lineSize] then alternating [u16 skip] [u16 copyLen]
- * followed by (copyLen+1) literal bytes. Destination advances by pitch
- * per line, source advances by lineSize+2 per line.
- */
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch + left;
-
-	for (int row = 0; row < height; row++) {
-		uint16 lineSize = READ_LE_UINT16(src);
-		const byte *lineData = src + 2;
-		const byte *lineEnd = lineData + lineSize;
-		byte *dstRow = dst;
-		int remaining = width;
-
-		while (remaining > 0 && lineData < lineEnd) {
-			// Read skip distance
-			if (lineData + 2 > lineEnd)
-				break;
-			uint16 skip = READ_LE_UINT16(lineData);
-			lineData += 2;
-			dstRow += skip;
-			remaining -= skip;
-			if (remaining <= 0)
-				break;
-
-			// Read copy count (+1)
-			if (lineData + 2 > lineEnd)
-				break;
-			uint16 copyLen = READ_LE_UINT16(lineData) + 1;
-			lineData += 2;
-
-			int toCopy = MIN<int>(copyLen, remaining);
-			if (lineData + toCopy > lineEnd)
-				toCopy = (int)(lineEnd - lineData);
-			if (toCopy > 0) {
-				memcpy(dstRow, lineData, toCopy);
-				lineData += toCopy;
-				dstRow += toCopy;
-				remaining -= toCopy;
-			}
-			// If copyLen was clamped by remaining, skip rest of source
-			if (copyLen > toCopy)
-				lineData += (copyLen - toCopy);
-		}
-
-		src += lineSize + 2;
-		dst += pitch;
-	}
-}
-
-/**
- * RA1 codec 23: Additive line-update overlay (FUN_10B40).
- *
- * Each line starts with a 16-bit row payload size followed by repeated
- * [skip:u8][run:u8] pairs. Unlike codec 21, the run length is stored directly,
- * not minus one. Instead of copying bytes from the stream, the decoder adds
- * `(paletteBase - 0x30)` to the destination pixels already present in the
- * framebuffer.
- */
-void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
-		int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
-	const uint8 colorDelta = (uint8)(paletteBase - 0x30);
-
-	for (int row = 0; row < height; row++) {
-		const uint16 lineSize = READ_LE_UINT16(src);
-		const byte *lineData = src + 2;
-		const byte *lineEnd = lineData + lineSize;
-		const int dstY = top + row;
-		int srcX = 0;
-
-		while (srcX < width && lineData < lineEnd) {
-			const int skip = *lineData++;
-			srcX += skip;
-			if (srcX >= width || lineData >= lineEnd)
-				break;
-
-			const int runLength = (int)(*lineData++);
-			const int dstStartX = left + srcX;
-			const int dstEndX = dstStartX + runLength;
-
-			if (dstY >= 0 && dstY < bufHeight) {
-				const int clippedStartX = MAX(dstStartX, 0);
-				const int clippedEndX = MIN(dstEndX, bufWidth);
-				if (clippedStartX < clippedEndX) {
-					byte *dstPixel = dst + dstY * pitch + clippedStartX;
-					for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
-						*dstPixel = (byte)(*dstPixel + colorDelta);
-				}
-			}
-
-			srcX += runLength;
-		}
-
-		src += lineSize + 2;
-	}
-}
-
-/**
- * RA1 codec 2: Scatter/point draw (FUN_110D7).
- *
- * Draws individual pixels at accumulated offsets — used for starfield
- * backgrounds. Each 4-byte entry: [dx:int16_le, dy:uint8, pixel:uint8].
- * Position starts at (left, top) and accumulates (dx, dy) per entry.
- * Pixel is drawn if position is within buffer bounds.
- */
-void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
-	int curX = left;
-	int curY = top;
-
-	while (dataSize >= 4) {
-		int16 dx = (int16)READ_LE_UINT16(src);
-		uint8 dy = src[2];
-		uint8 pixel = src[3];
-		src += 4;
-		dataSize -= 4;
-
-		curX += dx;
-		curY += dy;
-
-		if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight) {
-			dst[curY * pitch + curX] = pixel;
-		}
-	}
-}
-
-/**
- * RA1 codec 4/5: Block-based frame decoder.
- * Adapted from FFmpeg's libavcodec/sanm.c old_codec4() (LGPL 2.1+).
- *
- * Two tile tables (c4tbl[2][256][16]):
- *   Table 0: Generated dither patterns from 16-color sub-palette
- *   Table 1: Loaded from frame data (4bpp nibble-split tiles)
- *
- * Each 4x4 block is decoded by reading a bit flag (from mask bytes)
- * and an index byte. c4tbl[bit][index] gives the 16-pixel pattern.
- * Codec 4: index 0x80 means "skip block" (delta frame).
- * Codec 5: all indices are drawn (keyframe).
- */
-
-// Persistent tile table state for RA1 codec 4/5
 void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int top, int width, int height, int dataSize, uint8 ra1Param, uint16 ra1Parm2) {
 	if ((height == 242) && (width == 384)) {
 		int bufSize = 242 * 384;
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index e25d29c267b..7c43346d315 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -293,10 +293,85 @@ void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, in
 	} while (--height);
 }
 void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+
+/**
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41).
+ */
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+	dst += top * pitch + left;
+	for (int row = 0; row < height; row++) {
+		uint16 lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + lineSize;
+		byte *dstRow = dst;
+		int remaining = width;
+		while (remaining > 0 && lineData < lineEnd) {
+			if (lineData + 2 > lineEnd) break;
+			uint16 skip = READ_LE_UINT16(lineData); lineData += 2;
+			dstRow += skip; remaining -= skip;
+			if (remaining <= 0) break;
+			if (lineData + 2 > lineEnd) break;
+			uint16 copyLen = READ_LE_UINT16(lineData) + 1; lineData += 2;
+			int toCopy = MIN<int>(copyLen, remaining);
+			if (lineData + toCopy > lineEnd) toCopy = (int)(lineEnd - lineData);
+			if (toCopy > 0) { memcpy(dstRow, lineData, toCopy); lineData += toCopy; dstRow += toCopy; remaining -= toCopy; }
+			if (copyLen > toCopy) lineData += (copyLen - toCopy);
+		}
+		src += lineSize + 2;
+		dst += pitch;
+	}
+}
+
+/**
+ * RA1 codec 23: Additive line-update overlay (FUN_10B40).
+ */
 void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
-	int pitch, int bufWidth, int bufHeight, uint8 param);
-void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
+		int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
+	const uint8 colorDelta = (uint8)(paletteBase - 0x30);
+	for (int row = 0; row < height; row++) {
+		const uint16 lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + lineSize;
+		const int dstY = top + row;
+		int srcX = 0;
+		while (srcX < width && lineData < lineEnd) {
+			const int skip = *lineData++; srcX += skip;
+			if (srcX >= width || lineData >= lineEnd) break;
+			const int runLength = (int)(*lineData++);
+			const int dstStartX = left + srcX;
+			const int dstEndX = dstStartX + runLength;
+			if (dstY >= 0 && dstY < bufHeight) {
+				const int clippedStartX = MAX(dstStartX, 0);
+				const int clippedEndX = MIN(dstEndX, bufWidth);
+				if (clippedStartX < clippedEndX) {
+					byte *dstPixel = dst + dstY * pitch + clippedStartX;
+					for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
+						*dstPixel = (byte)(*dstPixel + colorDelta);
+				}
+			}
+			srcX += runLength;
+		}
+		src += lineSize + 2;
+	}
+}
+
+/**
+ * RA1 codec 2: Scatter/point draw (FUN_110D7).
+ */
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+	int curX = left;
+	int curY = top;
+	while (dataSize >= 4) {
+		int16 dx = (int16)READ_LE_UINT16(src);
+		uint8 dy = src[2];
+		uint8 pixel = src[3];
+		src += 4; dataSize -= 4;
+		curX += dx; curY += dy;
+		if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight)
+			dst[curY * pitch + curX] = pixel;
+	}
+}
+
 // RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
 static uint8 s_ra1C4Tbl[2][256][16];
 static uint16 s_ra1C4Param = 0xFFFF;


Commit: 31d2c9a5730d30851d078f3460f10ee7c73bf05d
    https://github.com/scummvm/scummvm/commit/31d2c9a5730d30851d078f3460f10ee7c73bf05d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:48+02:00

Commit Message:
SCUMM: RA: Move specific class definitions out of smush_player.h

Changed paths:
  A engines/scumm/smush/smush_player_ra1.h
  A engines/scumm/smush/smush_player_ra2.h
    engines/scumm/insane/insane_rebel1.h
    engines/scumm/insane/insane_rebel1_render.cpp
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra1.cpp
    engines/scumm/smush/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/insane_rebel1.h
index db6f84a3890..24ebbced021 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/insane_rebel1.h
@@ -26,11 +26,11 @@
 #include "audio/mixer.h"
 #include "common/events.h"
 #include "scumm/insane/insane.h"
+#include "scumm/smush/smush_player_ra1.h"
 
 namespace Scumm {
 
 class ScummEngine_v7;
-class SmushPlayer;
 class SmushFont;
 
 // Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
@@ -67,8 +67,6 @@ static const int16 kRA1FocalY = 25;
  * Star Wars: Rebel Assault (RA1) game logic.
  * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
  */
-class SmushPlayerRebel1;
-
 class InsaneRebel1 : public Insane, public Common::EventObserver {
 public:
 	InsaneRebel1(ScummEngine_v7 *scumm);
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/insane_rebel1_render.cpp
index 93fdc31dc07..dd34b22c87f 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/insane_rebel1_render.cpp
@@ -22,7 +22,7 @@
 #include "common/system.h"
 
 #include "scumm/scumm_v7.h"
-#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra1.h"
 #include "scumm/insane/insane_rebel1.h"
 
 namespace Scumm {
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 8363ec50108..3a89da5bf87 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -50,6 +50,8 @@
 #include "scumm/imuse/imuse.h"
 #include "scumm/imuse_digi/dimuse_engine.h"
 #include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra1.h"
+#include "scumm/smush/smush_player_ra2.h"
 #include "scumm/players/player_towns.h"
 #include "scumm/insane/insane.h"
 #include "scumm/insane/insane_rebel2.h"
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 22f2665069e..980cfa26fa7 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -365,117 +365,6 @@ private:
 	void timerCallback();
 };
 
-class SmushPlayerRebel1 : public SmushPlayer {
-	friend class InsaneRebel1;
-public:
-	SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
-	~SmushPlayerRebel1() override;
-
-protected:
-	void initGamePlayerFields() override;
-	void destroyGamePlayerFields() override;
-	void resetGameVideoState() override;
-	void releaseGameVideoState() override;
-	bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
-	bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
-	SmushFont *getGameFont(int font) override;
-	void adjustGamePalette() override;
-	bool handleGameAnimHeader(byte *headerContent) override;
-	void handleGameParseNextFrame() override;
-	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
-	bool handleGameDimensionOverride(int codec, int width, int height) override;
-	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
-	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
-	bool handleGameStoreFrame() override;
-	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
-	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
-	void handleGameFrameStart() override;
-	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
-	void handleGameProcessAudio(int16 feedSize) override;
-	bool isInsaneGame() const override { return true; }
-	void handleFrameObject(int32 subSize, Common::SeekableReadStream &b) override;
-	void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
-	void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
-
-private:
-	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
-	SmushFont *ra1GetFont(int font);
-	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
-
-	// RA1 clean frame buffer for delta source restoration
-	byte *_ra1CleanFrame;
-	int32 _ra1CleanFrameSize;
-	bool _ra1HasCleanFrame;
-
-	// RA1 OBJ overlay FOBJ — cockpit drawn once frame 0, re-rendered every frame
-	byte *_ra1ObjOverlayData;
-	int32 _ra1ObjOverlayDataSize;
-	int _ra1ObjOverlayCodec;
-	int _ra1ObjOverlayLeft;
-	int _ra1ObjOverlayTop;
-	int _ra1ObjOverlayWidth;
-	int _ra1ObjOverlayHeight;
-
-	// RA1 viewport scroll offset for interactive gameplay
-	int _ra1ViewportOffsetX;
-	int _ra1ViewportOffsetY;
-};
-
-class SmushPlayerRebel2 : public SmushPlayer {
-public:
-	SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
-	~SmushPlayerRebel2() override;
-
-protected:
-	void initGamePlayerFields() override;
-	void destroyGamePlayerFields() override;
-	void initGameVideoState() override;
-	void releaseGameVideoState() override;
-	bool shouldPreserveFrameBuffer() const override { return true; }
-	bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
-	bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) override;
-	bool shouldAlwaysShowSubtitles() const override { return true; }
-	SmushFont *getGameFont(int font) override;
-	void adjustGamePalette() override;
-	bool handleGameAnimHeader(byte *headerContent) override;
-	const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
-	void handleGameParseNextFrame() override;
-	bool shouldRouteAllIACTs() const override { return true; }
-	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
-	bool handleGameDimensionOverride(int codec, int width, int height) override;
-	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
-	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
-	bool handleGameStoreFrame() override;
-	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
-	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
-	void handleGameFrameStart() override;
-	bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
-	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
-	void handleGameProcessAudio(int16 feedSize) override;
-	bool isInsaneGame() const override { return true; }
-	void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) override;
-
-private:
-	void handleLoad(int32 subSize, Common::SeekableReadStream &b);
-	void ra2HandleTextResource(const char *str, int fontId, int color,
-							   int pos_x, int pos_y, int left, int top,
-							   int width, int height, TextStyleFlags flg);
-	void ra2SelectFrameBuffer(int width, int height);
-	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
-						int width, int height, int pitch, int dataSize);
-	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
-						  int left, int top, int width, int height);
-	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
-
-	// LOAD chunk streaming buffer (embedded resource data)
-	byte *_loadBuffer;
-	int32 _loadBufferSize;
-	int32 _loadBufferOffset;
-	int32 _loadReadOffset;
-	int16 _lastLoadChunkIdx;
-	int16 _totalLoadChunks;
-};
-
 } // End of namespace Scumm
 
 #endif
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/smush_player_ra1.cpp
index 7c43346d315..00bd2535d11 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/smush_player_ra1.cpp
@@ -30,7 +30,7 @@
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra1.h"
 
 #include "scumm/insane/insane_rebel1.h"
 
diff --git a/engines/scumm/smush/smush_player_ra1.h b/engines/scumm/smush/smush_player_ra1.h
new file mode 100644
index 00000000000..cd072afcabf
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra1.h
@@ -0,0 +1,87 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_PLAYER_RA1_H
+#define SCUMM_SMUSH_PLAYER_RA1_H
+
+#include "scumm/smush/smush_player.h"
+
+namespace Scumm {
+
+class SmushPlayerRebel1 : public SmushPlayer {
+	friend class InsaneRebel1;
+public:
+	SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+	~SmushPlayerRebel1() override;
+
+protected:
+	void initGamePlayerFields() override;
+	void destroyGamePlayerFields() override;
+	void resetGameVideoState() override;
+	void releaseGameVideoState() override;
+	bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+	bool handleGameTextResource(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+	SmushFont *getGameFont(int font) override;
+	void adjustGamePalette() override;
+	bool handleGameAnimHeader(byte *headerContent) override;
+	void handleGameParseNextFrame() override;
+	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+	bool handleGameDimensionOverride(int codec, int width, int height) override;
+	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+	bool handleGameStoreFrame() override;
+	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+	void handleGameFrameStart() override;
+	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+	void handleGameProcessAudio(int16 feedSize) override;
+	bool isInsaneGame() const override { return true; }
+	void handleFrameObject(int32 subSize, Common::SeekableReadStream &b) override;
+	void handleFrame(int32 frameSize, Common::SeekableReadStream &b) override;
+	void handleGameUpdateScreen(const byte *src, int srcPitch, int width, int height) override;
+
+private:
+	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
+	SmushFont *ra1GetFont(int font);
+	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+
+	// RA1 clean frame buffer for delta source restoration
+	byte *_ra1CleanFrame;
+	int32 _ra1CleanFrameSize;
+	bool _ra1HasCleanFrame;
+
+	// RA1 OBJ overlay FOBJ — cockpit drawn once frame 0, re-rendered every frame
+	byte *_ra1ObjOverlayData;
+	int32 _ra1ObjOverlayDataSize;
+	int _ra1ObjOverlayCodec;
+	int _ra1ObjOverlayLeft;
+	int _ra1ObjOverlayTop;
+	int _ra1ObjOverlayWidth;
+	int _ra1ObjOverlayHeight;
+
+	// RA1 viewport scroll offset for interactive gameplay
+	int _ra1ViewportOffsetX;
+	int _ra1ViewportOffsetY;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index e59e6831254..2f5a47b25d1 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -32,7 +32,7 @@
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
 #include "scumm/smush/smush_multi_font.h"
-#include "scumm/smush/smush_player.h"
+#include "scumm/smush/smush_player_ra2.h"
 
 #include "scumm/insane/insane.h"
 #include "scumm/insane/insane_rebel2.h"
diff --git a/engines/scumm/smush/smush_player_ra2.h b/engines/scumm/smush/smush_player_ra2.h
new file mode 100644
index 00000000000..9e180a852a0
--- /dev/null
+++ b/engines/scumm/smush/smush_player_ra2.h
@@ -0,0 +1,86 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_PLAYER_RA2_H
+#define SCUMM_SMUSH_PLAYER_RA2_H
+
+#include "scumm/smush/smush_player.h"
+
+namespace Scumm {
+
+class SmushPlayerRebel2 : public SmushPlayer {
+public:
+	SmushPlayerRebel2(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane);
+	~SmushPlayerRebel2() override;
+
+protected:
+	void initGamePlayerFields() override;
+	void destroyGamePlayerFields() override;
+	void initGameVideoState() override;
+	void releaseGameVideoState() override;
+	bool shouldPreserveFrameBuffer() const override { return true; }
+	bool handleGameFetch(int32 subSize, Common::SeekableReadStream &b) override;
+	bool handleGameTextRendering(const char *str, int fontId, int color, int pos_x, int pos_y, int left, int top, int width, int height, TextStyleFlags flg) override;
+	bool shouldAlwaysShowSubtitles() const override { return true; }
+	SmushFont *getGameFont(int font) override;
+	void adjustGamePalette() override;
+	bool handleGameAnimHeader(byte *headerContent) override;
+	const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
+	void handleGameParseNextFrame() override;
+	bool shouldRouteAllIACTs() const override { return true; }
+	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
+	bool handleGameDimensionOverride(int codec, int width, int height) override;
+	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
+	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+	bool handleGameStoreFrame() override;
+	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
+	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
+	void handleGameFrameStart() override;
+	bool handleGameSkipChunk(uint32 subType, int32 subSize, Common::SeekableReadStream &b) override;
+	void handleGameGost(int32 subSize, Common::SeekableReadStream &b) override;
+	void handleGameProcessAudio(int16 feedSize) override;
+	bool isInsaneGame() const override { return true; }
+	void handleGameLoad(int32 subSize, Common::SeekableReadStream &b) override;
+
+private:
+	void handleLoad(int32 subSize, Common::SeekableReadStream &b);
+	void ra2HandleTextResource(const char *str, int fontId, int color,
+							   int pos_x, int pos_y, int left, int top,
+							   int width, int height, TextStyleFlags flg);
+	void ra2SelectFrameBuffer(int width, int height);
+	bool ra2DecodeCodec(int codec, const uint8 *src, int left, int top,
+						int width, int height, int pitch, int dataSize);
+	void ra2StoreFobjData(int codec, const byte *data, int32 dataSize,
+						  int left, int top, int width, int height);
+	void ra2HandleGost(int32 subSize, Common::SeekableReadStream &b);
+
+	// LOAD chunk streaming buffer (embedded resource data)
+	byte *_loadBuffer;
+	int32 _loadBufferSize;
+	int32 _loadBufferOffset;
+	int32 _loadReadOffset;
+	int16 _lastLoadChunkIdx;
+	int16 _totalLoadChunks;
+};
+
+} // End of namespace Scumm
+
+#endif


Commit: 3dbe2d7eb7988fca2d5b00cf10dc7e8246b822c5
    https://github.com/scummvm/scummvm/commit/3dbe2d7eb7988fca2d5b00cf10dc7e8246b822c5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:48+02:00

Commit Message:
SCUMM: RA2: Remove redundant code

Changed paths:
    engines/scumm/insane/insane_rebel2.cpp
    engines/scumm/insane/insane_rebel2.h
    engines/scumm/insane/insane_rebel2_menu.cpp
    engines/scumm/insane/insane_rebel2_render.cpp


diff --git a/engines/scumm/insane/insane_rebel2.cpp b/engines/scumm/insane/insane_rebel2.cpp
index ad54fabdcc7..458c0b75437 100644
--- a/engines/scumm/insane/insane_rebel2.cpp
+++ b/engines/scumm/insane/insane_rebel2.cpp
@@ -98,10 +98,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_rebelDetailMode = 1;
 	_smush_cockpitNut = new NutRenderer(_vm, "SYSTM/DISPFONT.NUT");
 
-	// Load SMALFONT.NUT for HUD score/lives rendering (DAT_00482200 equivalent)
-	// This is used by FUN_0041c012 to render the score in the status bar
-	_smush_dispfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
-
 	// Load DIHIFONT.NUT for in-video messages/subtitles (Opcode 9)
 	_rebelMsgFont = new SmushFont(_vm, "SYSTM/DIHIFONT.NUT", true);
 
@@ -115,16 +111,9 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_smush_smalfontNut = new NutRenderer(_vm, "SYSTM/SMALFONT.NUT");
 	_smush_titlefontNut = new NutRenderer(_vm, "SYSTM/TITLFONT.NUT");
 
-	// SmushFont for menu text rendering - uses SMALFONT with proper drawString support
-	_menuFont = new SmushFont(_vm, "SYSTM/SMALFONT.NUT", true);
 	_pauseOverlayActive = false;
 	memset(_savedPausePalette, 0, sizeof(_savedPausePalette));
 
-	// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
-	// This is loaded into DAT_0047aba8 and used as a background overlay via FUN_004236e0
-	// The original game uses the standard Windows arrow cursor (IDC_ARROW via LoadCursorA)
-	_smush_mouseoverNut = new NutRenderer(_vm, "SYSTM/MSTOVER.NUT");
-
 	_enemies.clear();
 	_rebelHandler = 0;  // Not set yet - will be set by IACT opcode 6
 	_rebelLevelType = 0;  // Level type from Opcode 6 par3, determines HUD sprite variant
@@ -503,12 +492,9 @@ InsaneRebel2::~InsaneRebel2() {
 	terminateAudio();
 	freeSfx();
 	delete _rebelMsgFont;
-	delete _menuFont;
-	delete _smush_dispfontNut;
 	delete _smush_talkfontNut;
 	delete _smush_smalfontNut;
 	delete _smush_titlefontNut;
-	delete _smush_mouseoverNut;
 
 	// Clean up Handler 8 ship sprites
 	delete _shipSprite;
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/insane_rebel2.h
index bdc246cb85a..90b8c162d29 100644
--- a/engines/scumm/insane/insane_rebel2.h
+++ b/engines/scumm/insane/insane_rebel2.h
@@ -102,6 +102,10 @@ public:
 	int processMenuInput();
 
 	// Format-code-aware string rendering (^fNN=font, ^cNNN=color)
+	// parseFormatCode: advances str past ^fNN/^cNNN/^^/^l codes.
+	// Returns: fontIdx (>=0) on font change, -2 on color/newline, -1 on no match.
+	static int parseFormatCode(const char *&str, int &outColor);
+
 	int getMenuStringWidth(const char *str) const;
 	void drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor = 1);
 	void drawMenuStringCentered(byte *renderBitmap, const char *str, int cx, int y, int defaultColor = 1);
@@ -455,10 +459,6 @@ public:
 
 	NutRenderer *_smush_cockpitNut;
 
-	// Font used for HUD score/lives/damage display (SMALFONT.NUT)
-	// DAT_00482200 equivalent - used by FUN_0041c012 for status bar rendering
-	NutRenderer *_smush_dispfontNut;
-
 	// Font used for opcode 9 text/subtitle rendering (DIHIFONT / TALKFONT)
 	SmushFont *_rebelMsgFont;
 
@@ -472,17 +472,10 @@ public:
 	NutRenderer *_smush_smalfontNut;   // Font 1 - small font for ^f01 switching
 	NutRenderer *_smush_titlefontNut;  // Font 2 - title font
 
-	// SmushFont for menu text rendering (uses SMALFONT.NUT with proper string drawing)
-	SmushFont *_menuFont;
-
 	// Saved palette for pause overlay restoration (FUN_405A21)
 	byte _savedPausePalette[768];
 	bool _pauseOverlayActive;
 
-	// MSTOVER.NUT - Mouse Over background overlay (NOT a cursor!)
-	// Loaded into DAT_0047aba8 and rendered via FUN_004236e0 as background
-	NutRenderer *_smush_mouseoverNut;
-
 	bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
 	
 
diff --git a/engines/scumm/insane/insane_rebel2_menu.cpp b/engines/scumm/insane/insane_rebel2_menu.cpp
index 15151d30504..696589e9133 100644
--- a/engines/scumm/insane/insane_rebel2_menu.cpp
+++ b/engines/scumm/insane/insane_rebel2_menu.cpp
@@ -235,122 +235,20 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	const int itemBaseY = numItems * -5 + 0x68;
 	const int itemSpacing = 10;
 
-	// -------------------------------------------------------------------
-	// Font system - Emulates linked list from FUN_00403bd0
-	// -------------------------------------------------------------------
-	//   Font 0 (^f00): TALKFONT.NUT
-	//   Font 1 (^f01): SMALFONT.NUT (menu items)
-	//   Font 2 (^f02): TITLFONT.NUT (title)
-	NutRenderer *fonts[3] = {
-		_smush_talkfontNut,
-		_smush_smalfontNut,
-		_smush_titlefontNut
-	};
-
+	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
-	if (!defaultFont) {
-		debug(1, "drawMenuItems: no fonts available!");
+	if (!defaultFont)
 		return;
-	}
 
 	Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
 	int actualPitch = _vm->_screenWidth;
 
-	// -------------------------------------------------------------------
-	// Format code parser - Emulates FUN_00434d10 / FUN_00433da0
-	// -------------------------------------------------------------------
-	//   ^^ = literal ^, ^fNN = font switch, ^cNNN = color code, ^l = newline
-	// Fixed-width format codes: ^fNN (2-digit font), ^cNNN (3-digit color)
-	auto parseFormatCode = [&](const char *&str, int &outColor) -> int {
-		if (*str != '^')
-			return -1;
-
-		const char *p = str + 1;
-		if (*p == '^') {
-			str = p;
-			return -1;
-		}
-		if (*p == 'f') {
-			p++;
-			int fontIdx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
-			fontIdx = fontIdx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
-			str = p;
-			return (fontIdx >= 0 && fontIdx < 3) ? fontIdx : 0;
-		}
-		if (*p == 'c') {
-			p++;
-			int color = 0;
-			for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
-				color = color * 10 + (*p++ - '0');
-			str = p;
-			outColor = color;
-			return -2;
-		}
-		if (*p == 'l') {
-			str = p + 1;
-			return -2;
-		}
-		return -1;
-	};
-
-	// String width calculation - Emulates FUN_00433da0
 	auto getStringWidth = [&](const char *str) -> int {
-		int w = 0;
-		NutRenderer *curFont = defaultFont;
-		int curColor = -1;
-
-		while (*str) {
-			int fontChange = parseFormatCode(str, curColor);
-			if (fontChange >= 0) {
-				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
-				continue;
-			}
-			if (fontChange == -2)
-				continue;
-
-			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z')
-				c = c - 'a' + 'A';
-			if (curFont && c < curFont->getNumChars()) {
-				w += curFont->getCharWidth(c);
-			}
-		}
-		return w;
+		return getMenuStringWidth(str);
 	};
 
-	// String rendering - Emulates FUN_00434d10
-	// Codec 44 color substitution: font pixels with value 1 → ^cNNN color
 	auto drawString = [&](const char *str, int x, int y) {
-		NutRenderer *curFont = defaultFont;
-		int curColor = 1;
-
-		while (*str) {
-			int fontChange = parseFormatCode(str, curColor);
-			if (fontChange >= 0) {
-				curFont = fonts[fontChange] ? fonts[fontChange] : defaultFont;
-				continue;
-			}
-			if (fontChange == -2)
-				continue;
-
-			byte c = (byte)*str++;
-			if (c >= 'a' && c <= 'z')
-				c = c - 'a' + 'A';
-
-			if (!curFont)
-				continue;
-			int numChars = curFont->getNumChars();
-			if (c >= numChars)
-				continue;
-
-			int charW = curFont->getCharWidth(c);
-
-			if (x >= 0 && y >= 0 && charW > 0) {
-				curFont->drawCharV7(renderBitmap, clipRect, x, y, actualPitch, curColor,
-				                    kStyleAlignLeft, c, false, false);
-			}
-			x += charW;
-		}
+		drawMenuString(renderBitmap, str, x, y, 1);
 	};
 
 	// -------------------------------------------------------------------
@@ -424,7 +322,33 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 	}
 }
 
-// getMenuStringWidth -- Format-code-aware string width (^fNN, ^cNNN, ^^).
+// parseFormatCode -- Shared ^fNN/^cNNN/^^/^l parser.
+// Returns: fontIdx (>=0) on font change, -2 on color/newline, -1 on no match.
+int InsaneRebel2::parseFormatCode(const char *&str, int &outColor) {
+	if (*str != '^')
+		return -1;
+	const char *p = str + 1;
+	if (*p == '^') { str = p; return -1; }
+	if (*p == 'f') {
+		p++;
+		int idx = 0;
+		while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
+		str = p;
+		return (idx >= 0 && idx < 3) ? idx : 0;
+	}
+	if (*p == 'c') {
+		p++;
+		int color = 0;
+		while (*p >= '0' && *p <= '9') { color = color * 10 + (*p - '0'); p++; }
+		str = p;
+		outColor = color;
+		return -2;
+	}
+	if (*p == 'l') { str = p + 1; return -2; }
+	return -1;
+}
+
+// getMenuStringWidth -- Format-code-aware string width.
 int InsaneRebel2::getMenuStringWidth(const char *str) const {
 	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -433,36 +357,20 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
 
 	int w = 0;
 	NutRenderer *curFont = defaultFont;
+	int dummyColor = 0;
 	while (*str) {
-		if (*str == '^') {
-			const char *p = str + 1;
-			if (*p == '^') { str = p + 1; continue; }
-			if (*p == 'f') {
-				p++;
-				int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
-				idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
-				curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
-				str = p;
-				continue;
-			}
-			if (*p == 'c') {
-				p++;
-				for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++) p++;
-				str = p;
-				continue;
-			}
-			if (*p == 'l') { str = p + 1; continue; }
-		}
+		int fc = parseFormatCode(str, dummyColor);
+		if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); continue; }
+		if (fc == -2) continue;
 		byte c = (byte)*str++;
-		if (c >= 'a' && c <= 'z')
-			c = c - 'a' + 'A';
+		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
 		if (curFont && c < curFont->getNumChars())
 			w += curFont->getCharWidth(c);
 	}
 	return w;
 }
 
-// Format-code-aware string rendering at (x, y)
+// Format-code-aware string rendering at (x, y).
 void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, int y, int defaultColor) {
 	NutRenderer *fonts[3] = { _smush_talkfontNut, _smush_smalfontNut, _smush_titlefontNut };
 	NutRenderer *defaultFont = fonts[0] ? fonts[0] : _smush_smalfontNut;
@@ -475,35 +383,12 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
 	NutRenderer *curFont = defaultFont;
 	int curColor = defaultColor;
 	while (*str) {
-		if (*str == '^') {
-			const char *p = str + 1;
-			if (*p == '^') { str = p + 1; continue; }
-			if (*p == 'f') {
-				p++;
-				int idx = (*p >= '0' && *p <= '9') ? (*p++ - '0') : 0;
-				idx = idx * 10 + ((*p >= '0' && *p <= '9') ? (*p++ - '0') : 0);
-				curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
-				str = p;
-				continue;
-			}
-			if (*p == 'c') {
-				p++;
-				int color = 0;
-				for (int d = 0; d < 3 && *p >= '0' && *p <= '9'; d++)
-					color = color * 10 + (*p++ - '0');
-				curColor = color;
-				str = p;
-				continue;
-			}
-			if (*p == 'l') { str = p + 1; continue; }
-		}
+		int fc = parseFormatCode(str, curColor);
+		if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); continue; }
+		if (fc == -2) continue;
 		byte c = (byte)*str++;
-		if (c >= 'a' && c <= 'z')
-			c = c - 'a' + 'A';
-		if (!curFont)
-			continue;
-		if (c >= curFont->getNumChars())
-			continue;
+		if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
+		if (!curFont || c >= curFont->getNumChars()) continue;
 		int charW = curFont->getCharWidth(c);
 		if (x >= 0 && y >= 0 && charW > 0)
 			curFont->drawCharV7(renderBitmap, clipRect, x, y, pitch, curColor,
diff --git a/engines/scumm/insane/insane_rebel2_render.cpp b/engines/scumm/insane/insane_rebel2_render.cpp
index 72ad585eed4..3f3dbd37963 100644
--- a/engines/scumm/insane/insane_rebel2_render.cpp
+++ b/engines/scumm/insane/insane_rebel2_render.cpp
@@ -2700,29 +2700,11 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
 
 	Common::Rect clipRect(0, 0, width, height);
 
-	// Format code parser (same as drawMenuItems / FUN_00434d10)
+	// Wrapper around shared parseFormatCode that also updates curFont
 	auto parseFormat = [&](const char *&s, NutRenderer *&curFont, int &curColor) {
-		if (*s != '^')
-			return false;
-		const char *p = s + 1;
-		if (*p == '^') { s = p; return false; }
-		if (*p == 'f') {
-			p++;
-			int idx = 0;
-			while (*p >= '0' && *p <= '9') { idx = idx * 10 + (*p - '0'); p++; }
-			s = p;
-			curFont = (idx >= 0 && idx < 3 && fonts[idx]) ? fonts[idx] : defaultFont;
-			return true;
-		}
-		if (*p == 'c') {
-			p++;
-			int col = 0;
-			while (*p >= '0' && *p <= '9') { col = col * 10 + (*p - '0'); p++; }
-			s = p;
-			curColor = col;
-			return true;
-		}
-		return false;
+		int fc = parseFormatCode(s, curColor);
+		if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); return true; }
+		return fc == -2;
 	};
 
 	// The TRS parser joins multi-line strings with spaces (stripping \n//),


Commit: 3350b7b032ee4cb74def671c2581c2c26971f3b1
    https://github.com/scummvm/scummvm/commit/3350b7b032ee4cb74def671c2581c2c26971f3b1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:48+02:00

Commit Message:
SCUMM: RA2: Reduce StringResource change impact

Changed paths:
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h
    engines/scumm/smush/smush_player_ra2.cpp
    engines/scumm/smush/smush_player_ra2.h
    engines/scumm/string_v7.cpp


diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 07d97275f32..cd71a780ff2 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -56,10 +56,10 @@
 
 namespace Scumm {
 
-static const int MAX_STRINGS = 800;  // RA2 has ~658 strings
+static const int MAX_STRINGS = 200;
 static const int ETRS_HEADER_LENGTH = 16;
 
-class StringResource {
+class StringResourceImpl : public StringResource {
 private:
 
 	struct {
@@ -70,28 +70,24 @@ private:
 	int _nbStrings;
 	int _lastId;
 	const char *_lastString;
-	bool _preserveNewlines;
 
 public:
 
-	StringResource() :
+	StringResourceImpl() :
 		_nbStrings(0),
 		_lastId(-1),
-		_lastString(nullptr),
-		_preserveNewlines(false) {
+		_lastString(nullptr) {
 		for (int i = 0; i < MAX_STRINGS; i++) {
 			_strings[i].id = 0;
 			_strings[i].string = nullptr;
 		}
 	}
-	~StringResource() {
+	~StringResourceImpl() override {
 		for (int32 i = 0; i < _nbStrings; i++) {
 			delete[] _strings[i].string;
 		}
 	}
 
-	void setPreserveNewlines(bool preserve) { _preserveNewlines = preserve; }
-
 	bool init(char *buffer, int32 length) {
 		char *def_start = strchr(buffer, '#');
 		while (def_start != nullptr) {
@@ -142,24 +138,7 @@ public:
 			}
 
 			data_end -= 2;
-			// Handle empty entries (e.g., RA2 TRS files have entries with no content)
-			if (data_end <= data_start) {
-				// Skip this entry - no content
-				def_start = strchr(def_end + 1, '#');
-				continue;
-			}
-
-			// Strip leading // from first line (RA2 format uses // prefix for content)
-			if (data_start[0] == '/' && data_start[1] == '/') {
-				data_start += 2;
-			}
-
-			// Recalculate length after stripping
-			if (data_end <= data_start) {
-				def_start = strchr(def_end + 1, '#');
-				continue;
-			}
-
+			assert(data_end > data_start);
 			char *value = new char[data_end - data_start + 1];
 			assert(value);
 			memcpy(value, data_start, data_end - data_start);
@@ -171,20 +150,11 @@ public:
 				line_start = line_end+1;
 				if (line_start[0] == '/' && line_start[1] == '/') {
 					line_start += 2;
-					// RA2: preserve newlines for multi-line TRES text
-					// (credits, cast lists). Other games join with spaces.
-					if (_preserveNewlines) {
-						if (line_end[-1] == '\r')
-							line_end[-1] = '\n';
-						// else line_end already points to '\n'
-						memmove(line_end + 1, line_start, strlen(line_start)+1);
-					} else {
-						if	(line_end[-1] == '\r')
-							line_end[-1] = ' ';
-						else
-							*line_end++ = ' ';
-						memmove(line_end, line_start, strlen(line_start)+1);
-					}
+					if	(line_end[-1] == '\r')
+						line_end[-1] = ' ';
+					else
+						*line_end++ = ' ';
+					memmove(line_end, line_start, strlen(line_start)+1);
 				}
 			}
 			_strings[_nbStrings].id = id;
@@ -195,7 +165,7 @@ public:
 		return true;
 	}
 
-	const char *get(int id) {
+	const char *get(int id) override {
 		if (id == _lastId) {
 			return _lastString;
 		}
@@ -215,16 +185,14 @@ public:
 };
 
 static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_encoded) {
-	debugC(DEBUG_SMUSH, "getStrings: trying to read text resources from %s", file);
+	debugC(DEBUG_SMUSH, "trying to read text resources from %s", file);
 	ScummFile *theFile = vm->instantiateScummFile();
 
 	vm->openFile(*theFile, file);
 	if (!theFile->isOpen()) {
-		debugC(DEBUG_SMUSH, "getStrings: Failed to open %s", file);
 		delete theFile;
 		return 0;
 	}
-	debugC(DEBUG_SMUSH, "getStrings: Successfully opened %s", file);
 	int32 length = theFile->size();
 	char *filebuffer = new char [length + 1];
 	assert(filebuffer);
@@ -241,9 +209,8 @@ static StringResource *getStrings(ScummEngine *vm, const char *file, bool is_enc
 		}
 		filebuffer[length] = '\0';
 	}
-	StringResource *sr = new StringResource;
+	StringResourceImpl *sr = new StringResourceImpl;
 	assert(sr);
-	sr->setPreserveNewlines(vm->_game.id == GID_REBEL2);
 	sr->init(filebuffer, length);
 	delete[] filebuffer;
 	return sr;
@@ -1202,11 +1169,9 @@ void SmushPlayer::handleAnimHeader(int32 subSize, Common::SeekableReadStream &b)
 
 void SmushPlayer::setupAnim(const char *file) {
 	if (_insanity) {
-		const char *gameStringResource = getGameStringResource();
-		if (gameStringResource) {
-			_strings = getStrings(_vm, gameStringResource, true);
-		} else if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS))) {
-			readString("mineroad.trs");
+		if (!handleGameSetupStrings()) {
+			if (!((_vm->_game.features & GF_DEMO) && (_vm->_game.platform == Common::kPlatformDOS)))
+				readString("mineroad.trs");
 		}
 	} else
 		readString(file);
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 980cfa26fa7..b84d1880afb 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -93,7 +93,11 @@ class ScummEngine_v7;
 class SmushFont;
 class SmushMultiFont;
 class SmushMixer;
-class StringResource;
+class StringResource {
+public:
+	virtual ~StringResource() {}
+	virtual const char *get(int id) = 0;
+};
 class SmushDeltaBlocksDecoder;
 class SmushDeltaGlyphsDecoder;
 class IMuseDigital;
@@ -304,7 +308,7 @@ protected:
 	virtual SmushFont *getGameFont(int font) { return nullptr; }
 	virtual void adjustGamePalette() {}
 	virtual bool handleGameAnimHeader(byte *headerContent) { return false; }
-	virtual const char *getGameStringResource() const { return nullptr; }
+	virtual bool handleGameSetupStrings() { return false; }
 	virtual void handleGameParseNextFrame() {}
 	virtual bool shouldRouteAllIACTs() const { return false; }
 	virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/smush_player_ra2.cpp
index 2f5a47b25d1..c256493fec5 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/smush_player_ra2.cpp
@@ -226,6 +226,160 @@ SmushFont *SmushPlayerRebel2::getGameFont(int font) {
 	return _sf[font];
 }
 
+// ---------------------------------------------------------------------------
+// RA2 string resource loading — separate from shared StringResource
+// ---------------------------------------------------------------------------
+
+// RA2 TRS format differences from standard SCUMM:
+//   - Up to ~658 entries (standard only supports 200)
+//   - Entries can be empty (no content between header and next #)
+//   - Content lines prefixed with //
+//   - Multi-line entries preserve newlines (credits, cast lists)
+
+static const int RA2_MAX_STRINGS = 800;
+static const int RA2_ETRS_HEADER_LENGTH = 16;
+
+struct RA2StringEntry {
+	int id;
+	char *string;
+};
+
+class StringResourceRA2 : public StringResource {
+	RA2StringEntry _ra2Strings[RA2_MAX_STRINGS];
+	int _ra2NbStrings;
+	int _ra2LastId;
+	const char *_ra2LastString;
+
+public:
+	StringResourceRA2() : _ra2NbStrings(0), _ra2LastId(-1), _ra2LastString(nullptr) {
+		for (int i = 0; i < RA2_MAX_STRINGS; i++) {
+			_ra2Strings[i].id = 0;
+			_ra2Strings[i].string = nullptr;
+		}
+	}
+
+	~StringResourceRA2() override {
+		for (int i = 0; i < _ra2NbStrings; i++)
+			delete[] _ra2Strings[i].string;
+	}
+
+	bool init(char *buffer, int32 length) {
+		char *def_start = strchr(buffer, '#');
+		while (def_start != nullptr) {
+			char *def_end = strchr(def_start, '\n');
+			if (!def_end) break;
+
+			char *id_end = def_end;
+			while (id_end >= def_start && !Common::isDigit(*(id_end-1)))
+				id_end--;
+			if (id_end <= def_start) { def_start = strchr(def_end + 1, '#'); continue; }
+
+			char *id_start = id_end;
+			while (Common::isDigit(*(id_start - 1)))
+				id_start--;
+
+			char idstring[32];
+			memcpy(idstring, id_start, id_end - id_start);
+			idstring[id_end - id_start] = 0;
+			int32 id = atoi(idstring);
+
+			char *data_start = def_end;
+			while (*data_start == '\n' || *data_start == '\r')
+				data_start++;
+
+			char *data_end = data_start;
+			while (1) {
+				if (data_end[-2] == '\r' && data_end[-1] == '\n' && data_end[0] == '\r' && data_end[1] == '\n') break;
+				if (data_end[-2] == '\n' && data_end[-1] == '\n') break;
+				if (data_end[-2] == '\r' && data_end[-1] == '\n' && data_end[0] == '#') break;
+				data_end++;
+				if (data_end >= buffer + length) { data_end = buffer + length; break; }
+			}
+			data_end -= 2;
+
+			// Skip empty entries
+			if (data_end <= data_start) { def_start = strchr(def_end + 1, '#'); continue; }
+
+			// Strip leading // prefix
+			if (data_start[0] == '/' && data_start[1] == '/')
+				data_start += 2;
+			if (data_end <= data_start) { def_start = strchr(def_end + 1, '#'); continue; }
+
+			char *value = new char[data_end - data_start + 1];
+			memcpy(value, data_start, data_end - data_start);
+			value[data_end - data_start] = 0;
+
+			// Preserve newlines for multi-line TRES text (credits, cast lists)
+			char *line_start = value;
+			char *line_end;
+			while ((line_end = strchr(line_start, '\n'))) {
+				line_start = line_end + 1;
+				if (line_start[0] == '/' && line_start[1] == '/') {
+					line_start += 2;
+					if (line_end[-1] == '\r')
+						line_end[-1] = '\n';
+					memmove(line_end + 1, line_start, strlen(line_start) + 1);
+				}
+			}
+
+			if (_ra2NbStrings < RA2_MAX_STRINGS) {
+				_ra2Strings[_ra2NbStrings].id = id;
+				_ra2Strings[_ra2NbStrings].string = value;
+				_ra2NbStrings++;
+			} else {
+				delete[] value;
+			}
+			def_start = strchr(data_end + 2, '#');
+		}
+		return true;
+	}
+
+	const char *get(int id) override {
+		if (id == _ra2LastId)
+			return _ra2LastString;
+		for (int i = 0; i < _ra2NbStrings; i++) {
+			if (_ra2Strings[i].id == id) {
+				_ra2LastId = id;
+				_ra2LastString = _ra2Strings[i].string;
+				return _ra2LastString;
+			}
+		}
+		warning("StringResourceRA2: invalid string id : %d", id);
+		_ra2LastId = -1;
+		_ra2LastString = "unknown string";
+		return _ra2LastString;
+	}
+};
+
+bool SmushPlayerRebel2::handleGameSetupStrings() {
+	ScummFile *theFile = _vm->instantiateScummFile();
+	_vm->openFile(*theFile, "SYSTM/GAME.TRS");
+	if (!theFile->isOpen()) {
+		delete theFile;
+		return true; // handled (no strings available)
+	}
+	int32 length = theFile->size();
+	char *filebuffer = new char[length + 1];
+	theFile->read(filebuffer, length);
+	filebuffer[length] = 0;
+	theFile->close();
+	delete theFile;
+
+	if (READ_BE_UINT32(filebuffer) == MKTAG('E','T','R','S')) {
+		assert(length > RA2_ETRS_HEADER_LENGTH);
+		length -= RA2_ETRS_HEADER_LENGTH;
+		for (int i = 0; i < length; ++i)
+			filebuffer[i] = filebuffer[i + RA2_ETRS_HEADER_LENGTH] ^ 0xCC;
+		filebuffer[length] = '\0';
+	}
+
+	StringResourceRA2 *sr = new StringResourceRA2;
+	sr->init(filebuffer, length);
+	delete[] filebuffer;
+	_strings = sr;
+	return true;
+}
+
 /**
  * Reset XPAL delta palette from the current base palette.
  * Prevents stale delta values from a previous video corrupting the palette.
diff --git a/engines/scumm/smush/smush_player_ra2.h b/engines/scumm/smush/smush_player_ra2.h
index 9e180a852a0..e4680866e58 100644
--- a/engines/scumm/smush/smush_player_ra2.h
+++ b/engines/scumm/smush/smush_player_ra2.h
@@ -43,7 +43,7 @@ protected:
 	SmushFont *getGameFont(int font) override;
 	void adjustGamePalette() override;
 	bool handleGameAnimHeader(byte *headerContent) override;
-	const char *getGameStringResource() const override { return "SYSTM/GAME.TRS"; }
+	bool handleGameSetupStrings() override;
 	void handleGameParseNextFrame() override;
 	bool shouldRouteAllIACTs() const override { return true; }
 	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
diff --git a/engines/scumm/string_v7.cpp b/engines/scumm/string_v7.cpp
index a9fe0c80ab9..9088be0728d 100644
--- a/engines/scumm/string_v7.cpp
+++ b/engines/scumm/string_v7.cpp
@@ -129,10 +129,7 @@ int TextRenderer_v7::getStringHeight(const char *str, uint numBytesMax) {
 		}
 
 		if (*str == '\n') {
-			int lh = lineHeight ? lineHeight : _gr->getFontHeight();
-			// RA2: add extra inter-line spacing for larger fonts (credits text)
-			int gap = (_gameId == GID_REBEL2 && lh > 8) ? lh / 2 : 1;
-			totalHeight += lh + gap;
+			totalHeight += (lineHeight ? lineHeight : _gr->getFontHeight()) + 1;
 			lineHeight = 0;
 		} else if (*str != '\r' && *str != _lineBreakMarker) {
 			lineHeight = MAX<int>(lineHeight, _gr->getCharHeight(*str));


Commit: 58b9cc95c4cf69ab16d70cbfeb4ed919c95cb4f2
    https://github.com/scummvm/scummvm/commit/58b9cc95c4cf69ab16d70cbfeb4ed919c95cb4f2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:49+02:00

Commit Message:
SCUMM: RA: Reduce insane.h change impact

Changed paths:
    engines/scumm/insane/insane.h


diff --git a/engines/scumm/insane/insane.h b/engines/scumm/insane/insane.h
index d8dddfeb305..b610ccdc7b2 100644
--- a/engines/scumm/insane/insane.h
+++ b/engines/scumm/insane/insane.h
@@ -26,9 +26,6 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "common/list.h"
-#include "common/rect.h"
-
 namespace Scumm {
 
 #define INV_CHAIN    0
@@ -53,8 +50,8 @@ namespace Scumm {
 
 class Insane {
 public:
-	Insane() {};
-    Insane(ScummEngine_v7 *scumm);
+	Insane() {}
+	Insane(ScummEngine_v7 *scumm);
 	virtual ~Insane();
 
 	void setSmushParams(int speed);
@@ -62,9 +59,9 @@ public:
 	void runScene(int arraynum);
 
 	virtual void procPreRendering(byte *renderBitmap);
-	void virtual procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	virtual void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 						   int32 setupsan13, int32 curFrame, int32 maxFrame);
-	void virtual procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+	virtual void procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags, int16 par1,
 				  int16 par2, int16 par3, int16 par4);
 	virtual void procSKIP(int32 subSize, Common::SeekableReadStream &b);
@@ -425,7 +422,7 @@ public:
 	void actor10Reaction(int32 buttons);
 	int32 actionEnemy();
 	int32 processKeyboard();
-	int32 virtual processMouse();
+	virtual int32 processMouse();
 	void setEnemyAnimation(int32 actornum, int anim);
 	void chooseEnemyWeaponAnim(int32 buttons);
 	void switchEnemyWeapon();
@@ -451,8 +448,8 @@ public:
 	void iactScene21(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 				  int32 setupsan13, Common::SeekableReadStream &b, int32 size, int32 flags,
 				  int16 par1, int16 par2, int16 par3, int16 par4);
-	bool virtual isBitSet(int n);
-	void virtual setBit(int n);
+	virtual bool isBitSet(int n);
+	virtual void setBit(int n);
 	void clearBit(int n);
 	void chooseEnemy();
 	void removeEmptyEnemies();
@@ -460,7 +457,7 @@ public:
 
  public:
 
-	void virtual loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
+	virtual void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 		// Nothing by default
 	};
 };


Commit: 091dcd30183a519518edec3849dffa087c8d08b6
    https://github.com/scummvm/scummvm/commit/091dcd30183a519518edec3849dffa087c8d08b6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:49+02:00

Commit Message:
SCUMM: RA: Reduce NutRenderer change impact

Changed paths:
    engines/scumm/insane/insane_rebel2_iact.cpp
    engines/scumm/nut_renderer.cpp
    engines/scumm/nut_renderer.h


diff --git a/engines/scumm/insane/insane_rebel2_iact.cpp b/engines/scumm/insane/insane_rebel2_iact.cpp
index 161c89192f3..3a237a9ad16 100644
--- a/engines/scumm/insane/insane_rebel2_iact.cpp
+++ b/engines/scumm/insane/insane_rebel2_iact.cpp
@@ -1895,8 +1895,9 @@ bool InsaneRebel2::loadHandler7FlySprites(Common::SeekableReadStream &b, int64 r
 	}
 
 	// Load as NUT
-	NutRenderer *newNut = new NutRenderer(_vm, nutData, bytesRead);
-	if (!newNut || newNut->getNumChars() <= 0) {
+	NutRenderer *newNut = new NutRenderer(_vm);
+	newNut->loadFontFromData(nutData, bytesRead);
+	if (newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadHandler7FlySprites: NUT load failed for par4=%d", par4);
 		delete newNut;
 		free(nutData);
@@ -1959,7 +1960,8 @@ bool InsaneRebel2::loadTurretHudOverlay(byte *animData, int32 size, int16 par3)
 		return false;  // Not a turret HUD slot
 	}
 
-	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+	NutRenderer *newNut = new NutRenderer(_vm);
+	newNut->loadFontFromData(animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadTurretHudOverlay: NUT load failed for par3=%d", par3);
 		delete newNut;
@@ -2000,7 +2002,8 @@ bool InsaneRebel2::loadHandler8ShipSprites(byte *animData, int32 size, int16 par
 		return false;
 	}
 
-	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+	NutRenderer *newNut = new NutRenderer(_vm);
+	newNut->loadFontFromData(animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadHandler8ShipSprites: NUT load failed for par4=%d", par4);
 		delete newNut;
@@ -2051,7 +2054,8 @@ bool InsaneRebel2::loadHandler25GrdSprites(byte *animData, int32 size, int16 par
 		return false;
 	}
 
-	NutRenderer *newNut = new NutRenderer(_vm, animData, size);
+	NutRenderer *newNut = new NutRenderer(_vm);
+	newNut->loadFontFromData(animData, size);
 	if (!newNut || newNut->getNumChars() <= 0) {
 		debug("Rebel2 loadHandler25GrdSprites: NUT load failed for par4=%d", par4);
 		delete newNut;
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index 8b16fbd99c2..b84990ba1f5 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -47,30 +47,8 @@ NutRenderer::NutRenderer(ScummEngine *vm, const char *filename) :
 		memset(_2byteColorTable, 0, _2byteSteps);
 		_2byteMainColor = &_2byteColorTable[_2byteSteps - 1];
 		memset(_chars, 0, sizeof(_chars));
-		loadFont(filename);
-}
-
-NutRenderer::NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize) :
-	_vm(vm),
-	_numChars(0),
-	_fontHeight(0),
-	_decodedData(0),
-	_2byteColorTable(0),
-	_2byteShadowXOffsetTable(0),
-	_2byteShadowYOffsetTable(0),
-	_2byteMainColor(0),
-	_spacing(vm->_useCJKMode && vm->_language != Common::JA_JPN ? 1 : 0),
-	_2byteSteps(vm->_game.version == 8 ? 4 : 2),
-	_direction(vm->_language == Common::HE_ISR ? -1 : 1) {
-		static const int8 cjkShadowOffsetsX[4] = { -1, 0, 1, 0 };
-		static const int8 cjkShadowOffsetsY[4] = { 0, 1, 0, 0 };
-		_2byteShadowXOffsetTable = &cjkShadowOffsetsX[ARRAYSIZE(cjkShadowOffsetsX) - _2byteSteps];
-		_2byteShadowYOffsetTable = &cjkShadowOffsetsY[ARRAYSIZE(cjkShadowOffsetsY) - _2byteSteps];
-		_2byteColorTable = new uint8[_2byteSteps];
-		memset(_2byteColorTable, 0, _2byteSteps);
-		_2byteMainColor = &_2byteColorTable[_2byteSteps - 1];
-		memset(_chars, 0, sizeof(_chars));
-		loadFontFromData(data, dataSize);
+		if (filename)
+			loadFont(filename);
 }
 
 NutRenderer::~NutRenderer() {
diff --git a/engines/scumm/nut_renderer.h b/engines/scumm/nut_renderer.h
index c83ac830400..7dc15ebec76 100644
--- a/engines/scumm/nut_renderer.h
+++ b/engines/scumm/nut_renderer.h
@@ -64,12 +64,11 @@ protected:
 	void codec21(byte *dst, const byte *src, int width, int height, int pitch);
 
 	void loadFont(const char *filename);
-	void loadFontFromData(const byte *data, int32 dataSize);
 
 public:
-	NutRenderer(ScummEngine *vm, const char *filename);
-	NutRenderer(ScummEngine *vm, const byte *data, int32 dataSize);
+	NutRenderer(ScummEngine *vm, const char *filename = nullptr);
 	virtual ~NutRenderer();
+	void loadFontFromData(const byte *data, int32 dataSize);
 	int getNumChars() const { return _numChars; }
 
 	void drawFrame(byte *dst, int c, int x, int y, int pitch = -1);


Commit: 90cfb6851df30e3442fd849fa5164ecc743fbfc8
    https://github.com/scummvm/scummvm/commit/90cfb6851df30e3442fd849fa5164ecc743fbfc8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:49+02:00

Commit Message:
SCUMM: RA: Move Rebel Assault code into rebel directories

Changed paths:
  A engines/scumm/insane/rebel1/audio.cpp
  A engines/scumm/insane/rebel1/iact.cpp
  A engines/scumm/insane/rebel1/levels.cpp
  A engines/scumm/insane/rebel1/menu.cpp
  A engines/scumm/insane/rebel1/rebel.cpp
  A engines/scumm/insane/rebel1/rebel.h
  A engines/scumm/insane/rebel1/render.cpp
  A engines/scumm/insane/rebel1/runlevels.cpp
  A engines/scumm/insane/rebel2/audio.cpp
  A engines/scumm/insane/rebel2/iact.cpp
  A engines/scumm/insane/rebel2/levels.cpp
  A engines/scumm/insane/rebel2/menu.cpp
  A engines/scumm/insane/rebel2/rebel.cpp
  A engines/scumm/insane/rebel2/rebel.h
  A engines/scumm/insane/rebel2/render.cpp
  A engines/scumm/insane/rebel2/runlevels.cpp
  A engines/scumm/smush/rebel/codec_ra2.cpp
  A engines/scumm/smush/rebel/smush_multi_font.cpp
  A engines/scumm/smush/rebel/smush_multi_font.h
  A engines/scumm/smush/rebel/smush_player_ra1.cpp
  A engines/scumm/smush/rebel/smush_player_ra1.h
  A engines/scumm/smush/rebel/smush_player_ra2.cpp
  A engines/scumm/smush/rebel/smush_player_ra2.h
  R engines/scumm/insane/insane_rebel1.cpp
  R engines/scumm/insane/insane_rebel1.h
  R engines/scumm/insane/insane_rebel1_audio.cpp
  R engines/scumm/insane/insane_rebel1_iact.cpp
  R engines/scumm/insane/insane_rebel1_levels.cpp
  R engines/scumm/insane/insane_rebel1_menu.cpp
  R engines/scumm/insane/insane_rebel1_render.cpp
  R engines/scumm/insane/insane_rebel1_runlevels.cpp
  R engines/scumm/insane/insane_rebel2.cpp
  R engines/scumm/insane/insane_rebel2.h
  R engines/scumm/insane/insane_rebel2_audio.cpp
  R engines/scumm/insane/insane_rebel2_iact.cpp
  R engines/scumm/insane/insane_rebel2_levels.cpp
  R engines/scumm/insane/insane_rebel2_menu.cpp
  R engines/scumm/insane/insane_rebel2_render.cpp
  R engines/scumm/insane/insane_rebel2_runlevels.cpp
  R engines/scumm/smush/codec_ra2.cpp
  R engines/scumm/smush/smush_multi_font.cpp
  R engines/scumm/smush/smush_multi_font.h
  R engines/scumm/smush/smush_player_ra1.cpp
  R engines/scumm/smush/smush_player_ra1.h
  R engines/scumm/smush/smush_player_ra2.cpp
  R engines/scumm/smush/smush_player_ra2.h
    engines/scumm/module.mk
    engines/scumm/scumm.cpp
    engines/scumm/smush/smush_player.cpp


diff --git a/engines/scumm/insane/insane_rebel1_audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_audio.cpp
rename to engines/scumm/insane/rebel1/audio.cpp
index 5801d0dce6f..35529463d17 100644
--- a/engines/scumm/insane/insane_rebel1_audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -28,7 +28,7 @@
 #include "audio/mixer.h"
 
 #include "scumm/scumm_v7.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1_iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_iact.cpp
rename to engines/scumm/insane/rebel1/iact.cpp
index 4a900b5dd5b..f7da03c2091 100644
--- a/engines/scumm/insane/insane_rebel1_iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -24,7 +24,7 @@
 #include "common/endian.h"
 
 #include "scumm/scumm_v7.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1_levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_levels.cpp
rename to engines/scumm/insane/rebel1/levels.cpp
index c51577b751c..d4681b2d47d 100644
--- a/engines/scumm/insane/insane_rebel1_levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -24,7 +24,7 @@
 
 #include "scumm/scumm_v7.h"
 #include "scumm/file.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1_menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_menu.cpp
rename to engines/scumm/insane/rebel1/menu.cpp
index 7d3857c673c..0bd204db2f9 100644
--- a/engines/scumm/insane/insane_rebel1_menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -28,7 +28,7 @@
 
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1.cpp b/engines/scumm/insane/rebel1/rebel.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1.cpp
rename to engines/scumm/insane/rebel1/rebel.cpp
index 0610f978d45..544cb026cab 100644
--- a/engines/scumm/insane/insane_rebel1.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -32,7 +32,7 @@
 #include "scumm/scumm_v7.h"
 #include "scumm/scumm.h"
 #include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1.h b/engines/scumm/insane/rebel1/rebel.h
similarity index 99%
rename from engines/scumm/insane/insane_rebel1.h
rename to engines/scumm/insane/rebel1/rebel.h
index 24ebbced021..c230a44aae3 100644
--- a/engines/scumm/insane/insane_rebel1.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -26,7 +26,7 @@
 #include "audio/mixer.h"
 #include "common/events.h"
 #include "scumm/insane/insane.h"
-#include "scumm/smush/smush_player_ra1.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1_render.cpp b/engines/scumm/insane/rebel1/render.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_render.cpp
rename to engines/scumm/insane/rebel1/render.cpp
index dd34b22c87f..2b6719f5a40 100644
--- a/engines/scumm/insane/insane_rebel1_render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -22,8 +22,8 @@
 #include "common/system.h"
 
 #include "scumm/scumm_v7.h"
-#include "scumm/smush/smush_player_ra1.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel1_runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel1_runlevels.cpp
rename to engines/scumm/insane/rebel1/runlevels.cpp
index 04392541bff..1052fef9ad2 100644
--- a/engines/scumm/insane/insane_rebel1_runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -27,7 +27,7 @@
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_player.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel2_audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_audio.cpp
rename to engines/scumm/insane/rebel2/audio.cpp
index 3341bc18f72..f679b70f47d 100644
--- a/engines/scumm/insane/insane_rebel2_audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -26,7 +26,7 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 #include "audio/audiostream.h"
 #include "audio/decoders/raw.h"
diff --git a/engines/scumm/insane/insane_rebel2_iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_iact.cpp
rename to engines/scumm/insane/rebel2/iact.cpp
index 3a237a9ad16..2d653a2cf16 100644
--- a/engines/scumm/insane/insane_rebel2_iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -28,7 +28,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel2_levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_levels.cpp
rename to engines/scumm/insane/rebel2/levels.cpp
index 7b6907386e0..01c49a03452 100644
--- a/engines/scumm/insane/insane_rebel2_levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -27,7 +27,7 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel2_menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_menu.cpp
rename to engines/scumm/insane/rebel2/menu.cpp
index 696589e9133..f631909a4f6 100644
--- a/engines/scumm/insane/insane_rebel2_menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -33,9 +33,9 @@
 
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel2.cpp b/engines/scumm/insane/rebel2/rebel.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2.cpp
rename to engines/scumm/insane/rebel2/rebel.cpp
index 458c0b75437..7231de9997a 100644
--- a/engines/scumm/insane/insane_rebel2.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -43,7 +43,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 #include "common/config-manager.h"
 #include "audio/audiostream.h"
diff --git a/engines/scumm/insane/insane_rebel2.h b/engines/scumm/insane/rebel2/rebel.h
similarity index 100%
rename from engines/scumm/insane/insane_rebel2.h
rename to engines/scumm/insane/rebel2/rebel.h
diff --git a/engines/scumm/insane/insane_rebel2_render.cpp b/engines/scumm/insane/rebel2/render.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_render.cpp
rename to engines/scumm/insane/rebel2/render.cpp
index 3f3dbd37963..ef0c79af23c 100644
--- a/engines/scumm/insane/insane_rebel2_render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -31,7 +31,7 @@
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/insane/insane_rebel2_runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
similarity index 99%
rename from engines/scumm/insane/insane_rebel2_runlevels.cpp
rename to engines/scumm/insane/rebel2/runlevels.cpp
index a48dbc5b8c7..75da1488559 100644
--- a/engines/scumm/insane/insane_rebel2_runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -25,7 +25,7 @@
 
 #include "scumm/smush/smush_player.h"
 
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index b7fe661ee74..ea6cc308dc7 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,29 +133,29 @@ MODULE_OBJS += \
 	insane/insane_enemy.o \
 	insane/insane_scenes.o \
 	insane/insane_iact.o \
-	insane/insane_rebel1.o \
-	insane/insane_rebel1_audio.o \
-	insane/insane_rebel1_iact.o \
-	insane/insane_rebel1_levels.o \
-	insane/insane_rebel1_menu.o \
-	insane/insane_rebel1_render.o \
-	insane/insane_rebel1_runlevels.o \
-	insane/insane_rebel2.o \
-	insane/insane_rebel2_audio.o \
-	insane/insane_rebel2_iact.o \
-	insane/insane_rebel2_levels.o \
-	insane/insane_rebel2_menu.o \
-	insane/insane_rebel2_render.o \
-	insane/insane_rebel2_runlevels.o \
+	insane/rebel1/rebel.o \
+	insane/rebel1/audio.o \
+	insane/rebel1/iact.o \
+	insane/rebel1/levels.o \
+	insane/rebel1/menu.o \
+	insane/rebel1/render.o \
+	insane/rebel1/runlevels.o \
+	insane/rebel2/rebel.o \
+	insane/rebel2/audio.o \
+	insane/rebel2/iact.o \
+	insane/rebel2/levels.o \
+	insane/rebel2/menu.o \
+	insane/rebel2/render.o \
+	insane/rebel2/runlevels.o \
 	smush/codec1.o \
 	smush/codec20.o \
 	smush/codec37.o \
 	smush/codec47.o \
-	smush/codec_ra2.o \
-	smush/smush_multi_font.o \
 	smush/smush_player.o \
-	smush/smush_player_ra1.o \
-	smush/smush_player_ra2.o
+	smush/rebel/codec_ra2.o \
+	smush/rebel/smush_multi_font.o \
+	smush/rebel/smush_player_ra1.o \
+	smush/rebel/smush_player_ra2.o
 
 ifdef USE_ARM_SMUSH_ASM
 MODULE_OBJS += \
diff --git a/engines/scumm/scumm.cpp b/engines/scumm/scumm.cpp
index 3a89da5bf87..7d19565b732 100644
--- a/engines/scumm/scumm.cpp
+++ b/engines/scumm/scumm.cpp
@@ -50,12 +50,12 @@
 #include "scumm/imuse/imuse.h"
 #include "scumm/imuse_digi/dimuse_engine.h"
 #include "scumm/smush/smush_player.h"
-#include "scumm/smush/smush_player_ra1.h"
-#include "scumm/smush/smush_player_ra2.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
+#include "scumm/smush/rebel/smush_player_ra2.h"
 #include "scumm/players/player_towns.h"
 #include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel2.h"
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel2/rebel.h"
+#include "scumm/insane/rebel1/rebel.h"
 #include "scumm/he/animation_he.h"
 #include "scumm/he/font_he.h"
 #include "scumm/he/intern_he.h"
diff --git a/engines/scumm/smush/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
similarity index 100%
rename from engines/scumm/smush/codec_ra2.cpp
rename to engines/scumm/smush/rebel/codec_ra2.cpp
diff --git a/engines/scumm/smush/smush_multi_font.cpp b/engines/scumm/smush/rebel/smush_multi_font.cpp
similarity index 98%
rename from engines/scumm/smush/smush_multi_font.cpp
rename to engines/scumm/smush/rebel/smush_multi_font.cpp
index e68871d6f33..9c4205b7a65 100644
--- a/engines/scumm/smush/smush_multi_font.cpp
+++ b/engines/scumm/smush/rebel/smush_multi_font.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
 #include "scumm/smush/smush_font.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/scumm.h"
diff --git a/engines/scumm/smush/smush_multi_font.h b/engines/scumm/smush/rebel/smush_multi_font.h
similarity index 100%
rename from engines/scumm/smush/smush_multi_font.h
rename to engines/scumm/smush/rebel/smush_multi_font.h
diff --git a/engines/scumm/smush/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
similarity index 99%
rename from engines/scumm/smush/smush_player_ra1.cpp
rename to engines/scumm/smush/rebel/smush_player_ra1.cpp
index 00bd2535d11..6fc23c1ae22 100644
--- a/engines/scumm/smush/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -30,9 +30,9 @@
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_player_ra1.h"
+#include "scumm/smush/rebel/smush_player_ra1.h"
 
-#include "scumm/insane/insane_rebel1.h"
+#include "scumm/insane/rebel1/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/smush/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
similarity index 100%
rename from engines/scumm/smush/smush_player_ra1.h
rename to engines/scumm/smush/rebel/smush_player_ra1.h
diff --git a/engines/scumm/smush/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
similarity index 99%
rename from engines/scumm/smush/smush_player_ra2.cpp
rename to engines/scumm/smush/rebel/smush_player_ra2.cpp
index c256493fec5..c215c4cd465 100644
--- a/engines/scumm/smush/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -31,11 +31,11 @@
 #include "scumm/scumm.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
-#include "scumm/smush/smush_player_ra2.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_player_ra2.h"
 
 #include "scumm/insane/insane.h"
-#include "scumm/insane/insane_rebel2.h"
+#include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
diff --git a/engines/scumm/smush/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
similarity index 100%
rename from engines/scumm/smush/smush_player_ra2.h
rename to engines/scumm/smush/rebel/smush_player_ra2.h
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index cd71a780ff2..35431cd6da1 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -40,7 +40,7 @@
 #include "scumm/smush/codec37.h"
 #include "scumm/smush/codec47.h"
 #include "scumm/smush/smush_font.h"
-#include "scumm/smush/smush_multi_font.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"


Commit: dafba02a3b9020e8c94d82e068d229245dd5de35
    https://github.com/scummvm/scummvm/commit/dafba02a3b9020e8c94d82e068d229245dd5de35
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:50+02:00

Commit Message:
SCUMM: RA1: Fix regression in RA1 block decoder

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra1.h
    engines/scumm/smush/rebel/smush_player_ra2.cpp
    engines/scumm/smush/rebel/smush_player_ra2.h
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 6fc23c1ae22..5e8b3da8cf9 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -512,11 +512,7 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
 	return true;
 }
 
-bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
-	// The base class passes clipped coords. For additive codec and scatter, we need original coords
-	// which are stored in the origLeft/origTop locals of decodeFrameObject. Since we can't access those
-	// from the override, the additive codec and scatter draw are special-cased.
-	// For now, handle the codecs that have RA1-specific behavior.
+bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2) {
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
 		smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
@@ -529,7 +525,7 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		return true;
 	case SMUSH_CODEC_RA1_DELTA:
 	case SMUSH_CODEC_RA1_BLOCK:
-		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, 0, 0, codec);
+		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, param, parm2, codec);
 		return true;
 	case SMUSH_CODEC_LINE_UPDATE:
 		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index cd072afcabf..407fbb309e2 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -46,7 +46,7 @@ protected:
 	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
 	bool handleGameDimensionOverride(int codec, int width, int height) override;
 	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
-	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) override;
 	bool handleGameStoreFrame() override;
 	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
 	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index c215c4cd465..bdf5502ed92 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -739,7 +739,7 @@ bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, i
 	return true;
 }
 
-bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) {
+bool SmushPlayerRebel2::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2) {
 	// Handle RA2-specific codecs (21, 23, 44, 45); return false for standard
 	// codecs (RLE, uncompressed, codec 37/47) so the base class decodes them.
 	return ra2DecodeCodec(codec, src, left, top, width, height, pitch, dataSize);
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index e4680866e58..375d2a6a015 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -49,7 +49,7 @@ protected:
 	bool handleGameFrameBufferSelect(int codec, int width, int height) override;
 	bool handleGameDimensionOverride(int codec, int width, int height) override;
 	bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) override;
-	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) override;
+	bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) override;
 	bool handleGameStoreFrame() override;
 	void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) override;
 	void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) override;
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 35431cd6da1..0da86094c01 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -911,7 +911,7 @@ void SmushPlayer::decodeFrameObject(int codec, const uint8 *src, int left, int t
 	}
 
 	// Try game-specific codec handler first (pass adjustedSrc for row-skip-aware codecs)
-	if (!handleGameCodecDecode(codec, adjustedSrc, left, top, width, height, pitch, dataSize)) {
+	if (!handleGameCodecDecode(codec, adjustedSrc, left, top, width, height, pitch, dataSize, ra1Param, ra1Parm2)) {
 		// Standard SCUMM codec dispatch
 		switch (codec) {
 		case SMUSH_CODEC_RLE:
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index b84d1880afb..0ec4bd9311e 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -314,7 +314,7 @@ protected:
 	virtual bool handleGameFrameBufferSelect(int codec, int width, int height) { return false; }
 	virtual bool handleGameDimensionOverride(int codec, int width, int height) { return false; }
 	virtual bool handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) { return false; }
-	virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize) { return false; }
+	virtual bool handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param = 0, uint16 parm2 = 0) { return false; }
 	virtual bool handleGameStoreFrame() { return false; }
 	virtual void handleGameFrameObjectPre(int codec, int left, int top, int width, int height, int dataSize) {}
 	virtual void handleGameFrameObjectPost(int codec, const byte *data, int32 dataSize, int left, int top, int width, int height) {}


Commit: d208eb1f6bd90909198bae7a79900e4907e98d37
    https://github.com/scummvm/scummvm/commit/d208eb1f6bd90909198bae7a79900e4907e98d37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:50+02:00

Commit Message:
SCUMM: RA1: Fix RA1 block decoder edge cases

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 5e8b3da8cf9..ad3ea174974 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -429,9 +429,9 @@ static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2,
 }
 
 void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
-						 int pitch, int dataSize, uint8 param, uint16 parm2, int codec) {
+						 int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec) {
 	const int mx = pitch;
-	const int my = height;
+	const int my = bufHeight;
 	if (s_ra1C4Param != param) {
 		ra1Codec4GenTiles(param);
 		s_ra1C4Param = param;
@@ -505,8 +505,9 @@ bool SmushPlayerRebel1::handleGameDimensionOverride(int codec, int width, int he
 }
 
 bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
-	// RA1 additive codec (SKIP_RLE) uses original coords, not adjusted
-	if (codec == SMUSH_CODEC_SKIP_RLE)
+	// RA1 additive codec (SKIP_RLE) and scatter (RA1_SCATTER) use absolute
+	// positions — they must NOT be clipped/adjusted.
+	if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
 		return false;
 	adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
 	return true;
@@ -525,7 +526,9 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		return true;
 	case SMUSH_CODEC_RA1_DELTA:
 	case SMUSH_CODEC_RA1_BLOCK:
-		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch, dataSize, param, parm2, codec);
+		smushDecodeRA1Block(_dst, src, left, top, width, height, pitch,
+			(_dst == _specialBuffer) ? _height : _vm->_screenHeight,
+			dataSize, param, parm2, codec);
 		return true;
 	case SMUSH_CODEC_LINE_UPDATE:
 		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);


Commit: f86c294c7821d3b3e810175150b00d64eef59f58
    https://github.com/scummvm/scummvm/commit/f86c294c7821d3b3e810175150b00d64eef59f58
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:50+02:00

Commit Message:
SCUMM: RA: Remove useless static qualifiers

Changed paths:
    engines/scumm/insane/rebel1/audio.cpp
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/levels.cpp
    engines/scumm/insane/rebel1/menu.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/insane/rebel2/audio.cpp
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/insane/rebel2/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
index 35529463d17..6c1540aa368 100644
--- a/engines/scumm/insane/rebel1/audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -32,7 +32,7 @@
 
 namespace Scumm {
 
-static const char *const kRA1SfxFiles[8] = {
+const char *const kRA1SfxFiles[8] = {
 	"SYS/LASRSHOT.SAD",
 	"SYS/EXPLODE.SAD",
 	"SYS/BOOM.SAD",
diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index f7da03c2091..b66c6d91bf1 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -28,12 +28,12 @@
 
 namespace Scumm {
 
-static inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
+inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
 	const int deadZone = MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000;
 	return (ABS(axisValue) <= deadZone) ? 0 : axisValue;
 }
 
-static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
+inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
 	const int delta = (int)inputValue - (int)filteredValue;
 	int step = delta / 10;
 
@@ -44,13 +44,13 @@ static inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filtere
 	return filteredValue;
 }
 
-static const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
-static const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
+const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
+const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
 
 // LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
 // (6..18), while the recovered outer loop compares the post-latch state against the
 // later translated values seen in the executable. Accept both representations.
-static inline bool isL1Stage2DamageLatch(uint16 code) {
+inline bool isL1Stage2DamageLatch(uint16 code) {
 	switch (code) {
 	case 0x0006:
 	case 0x0007:
@@ -81,7 +81,7 @@ static inline bool isL1Stage2DamageLatch(uint16 code) {
 	}
 }
 
-static inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
+inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
 	switch (frameCounter) {
 	case 0x0034:
 	case 0x00ED:
@@ -96,7 +96,7 @@ static inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX
 	}
 }
 
-static const int16 kLevel7BranchFrames[6][6] = {
+const int16 kLevel7BranchFrames[6][6] = {
 	{ -1,  78, 267, 398, 556, 630 },
 	{ -1, 187, 376, 507, 665, 739 },
 	{ -1, 187, 376, 507, 665, 739 },
@@ -105,21 +105,21 @@ static const int16 kLevel7BranchFrames[6][6] = {
 	{ -1, 112, 301, 432, 590, 664 }
 };
 
-static const int16 kLevel7BranchDir[6] = {
+const int16 kLevel7BranchDir[6] = {
 	0, 1, 1, -1, 1, 1
 };
 
-static const int16 kLevel7BranchThreshold[6] = {
+const int16 kLevel7BranchThreshold[6] = {
 	0, 170, 170, 160, 160, 160
 };
 
-static const int16 kLevel8BranchFrames[3][3] = {
+const int16 kLevel8BranchFrames[3][3] = {
 	{ 2588, 1709,  262 },
 	{ 2323, 1444,   -2 },
 	{  877,   -2,   -2 }
 };
 
-static inline bool isLevel4DamageLatch(uint16 code) {
+inline bool isLevel4DamageLatch(uint16 code) {
 	switch (code) {
 	case 0x0008:
 	case 0x000A:
@@ -138,7 +138,7 @@ static inline bool isLevel4DamageLatch(uint16 code) {
 	}
 }
 
-static inline bool isLevel6DamageLatch(uint16 code) {
+inline bool isLevel6DamageLatch(uint16 code) {
 	switch (code) {
 	case 0x0003:
 	case 0x0008:
@@ -152,7 +152,7 @@ static inline bool isLevel6DamageLatch(uint16 code) {
 	}
 }
 
-static inline bool isLevel10DamageLatch(uint16 code) {
+inline bool isLevel10DamageLatch(uint16 code) {
 	if (code < 0x7F) {
 		if (code > 0x3D) {
 			if (code > 0x3E) {
@@ -283,7 +283,7 @@ static inline bool isLevel10DamageLatch(uint16 code) {
 	return true;
 }
 
-static inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
 	switch (frame) {
 	case 0x006A:
 	case 0x00FD:
@@ -321,7 +321,7 @@ static inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX,
 	}
 }
 
-static inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
 	switch (frame) {
 	case 0x00CD:
 		return perspectiveX < 0x29;
@@ -342,7 +342,7 @@ static inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspect
 	}
 }
 
-static inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
 	switch (frame) {
 	case 0x0189:
 		return perspectiveY < 0x0F;
@@ -360,7 +360,7 @@ static inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspect
 	}
 }
 
-static inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
 	switch (frame) {
 	case 0x00BB:
 		return perspectiveX < 0x29 && perspectiveY < 0x20;
@@ -1431,8 +1431,8 @@ void InsaneRebel1::updateAsteroidPhysics() {
 //   g_perspectiveX/Y  = crosshair center (on-foot targeting)
 // Our _perspectiveX/_perspectiveY maps to the camera offset (DAT_000041a0/41a2).
 // The crosshair center (0xA3, 0x82) is a separate constant for on-foot mode.
-static const int16 kOnFootCenterX = 0xA3;  // g_perspectiveX in HandleGameOp19
-static const int16 kOnFootCenterY = 0x82;  // g_perspectiveY in HandleGameOp19
+const int16 kOnFootCenterX = 0xA3;  // g_perspectiveX in HandleGameOp19
+const int16 kOnFootCenterY = 0x82;  // g_perspectiveY in HandleGameOp19
 
 void InsaneRebel1::updateOnFootPhysics() {
 	// --- First-frame initialization (0x19 counter==0) ---
diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index d4681b2d47d..cbe0b1afda2 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -31,7 +31,7 @@ namespace Scumm {
 // From smush/codec1.cpp
 void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
-static void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
+void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
 	while (height--) {
 		byte *dstNext = dst + pitch;
 		const byte *srcNext = src + 2 + READ_LE_UINT16(src);
@@ -57,7 +57,7 @@ static void decodeBomp(byte *dst, const byte *src, int width, int height, int pi
 	}
 }
 
-static void resetSpriteBank(RA1SpriteBank &bank) {
+void resetSpriteBank(RA1SpriteBank &bank) {
 	delete[] bank.sprites;
 	bank.sprites = nullptr;
 	free(bank.decodedData);
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 0bd204db2f9..4f7ef736b5d 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -32,9 +32,9 @@
 
 namespace Scumm {
 
-static const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
-static const int kRA1LevelSelectRowsPerCol = 8;
-static const int kRA1NumLevels = 15;
+const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
+const int kRA1LevelSelectRowsPerCol = 8;
+const int kRA1NumLevels = 15;
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	if (event.type == Common::EVENT_MOUSEMOVE && !_mouseRecentering) {
@@ -400,7 +400,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 
 	if (_optionsActive) {
 		// --- Options submenu (matching original RunGameOptionsMenu) ---
-		static const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+		const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
 
 		const int titleW = getTalkTextWidth("GAME OPTIONS");
 		drawTalkText((width - titleW) / 2, 30, "GAME OPTIONS");
@@ -456,7 +456,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		const int titleW = getTalkTextWidth("LEVEL SELECT");
 		drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
 
-		static const char *kLevelItems[kRA1LevelSelectItemCount] = {
+		const char *kLevelItems[kRA1LevelSelectItemCount] = {
 			" 1 TRAINING",
 			" 2 ASTEROIDS",
 			" 3 KOLAADOR",
@@ -510,7 +510,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	}
 
 	// --- Main menu ---
-	static const char *kMenuItems[5] = {
+	const char *kMenuItems[5] = {
 		"START NEW GAME",
 		"GAME OPTIONS",
 		"LEVEL SELECT",
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 544cb026cab..e7ac7a13a0a 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -39,7 +39,7 @@ namespace Scumm {
 // Per-difficulty tuning tables from assault_data_3.bin (also loadable from C:\rebltune.txt)
 // 21 sub-levels x 3 difficulties x 13 fields
 // Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
-static const int16 kTuningTable[21][3][13] = {
+const int16 kTuningTable[21][3][13] = {
 	// Sub-level 0: "1A" (Flight Training - canyon flight)
 	{
 		{ 100, 100,  60, 110,   0,   0,  15,   0,   0,   5,  500,  100, 2048 },  // Easy
@@ -167,7 +167,7 @@ static const int16 kTuningTable[21][3][13] = {
 		{   0,   0,   0,   0,   2,  22,  35,   4,  75,  10, 1500,  500, 2050 },  // Hard
 	},
 };
-static const int kNumTunedLevels = 21;
+const int kNumTunedLevels = 21;
 
 
 void InsaneRebel1::loadTuningForLevel(int level) {
@@ -319,7 +319,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
 
 	// Default high scores — from DS:0x1D0/0x298/0x2C0
-	static const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
+	const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
 		{"Vince",   10000, 2}, {"Tamlynn",  9000, 2}, {"Chip",    8000, 2},
 		{"Brett",    7000, 1}, {"Casey",    6000, 1}, {"Justin",  5000, 1},
 		{"Bill",     4000, 0}, {"Aaron",    3000, 0}, {"Mary",    2000, 0},
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 2b6719f5a40..c54c7580478 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -27,7 +27,7 @@
 
 namespace Scumm {
 
-static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
+inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
 	if (!rebel1 || !rebel1->isInteractiveVideoActive())
 		return 0;
 
@@ -37,14 +37,14 @@ static inline int ra1OverlayViewOffsetX(const InsaneRebel1 *rebel1) {
 	return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveX() : 0;
 }
 
-static inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
+inline int ra1OverlayViewOffsetY(const InsaneRebel1 *rebel1) {
 	if (!rebel1 || !rebel1->isInteractiveVideoActive())
 		return 0;
 
 	return (rebel1->getEffectiveGameOpcode() == 0x0B) ? rebel1->getPerspectiveY() : 0;
 }
 
-static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
 	int x, int y, const char *text) {
 	if (!dst || !text || bank.numSprites <= 0)
 		return;
@@ -107,7 +107,7 @@ static void drawBankString(const RA1SpriteBank &bank, byte *dst, int pitch, int
 	}
 }
 
-static const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
+const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
 	if (bank.numSprites <= 0)
 		return nullptr;
 	if ((byte)ch < 0x21)
@@ -127,7 +127,7 @@ static const RA1Sprite *lookupBankGlyph(const RA1SpriteBank &bank, char ch) {
 // Glyph markers in FUN_1C940/FUN_1CB22 go through DrawStringEx(..., flags=3),
 // which centers the glyph and ignores the NUT x/y offsets. Use the same anchor
 // rules here instead of the generic left-anchored text path.
-static void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
+void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitch, int width, int height,
 	int centerX, int centerY, char ch) {
 	char glyphStr[2] = { ch, '\0' };
 	const RA1Sprite *glyph = lookupBankGlyph(bank, ch);
@@ -141,7 +141,7 @@ static void drawCenteredBankGlyph(const RA1SpriteBank &bank, byte *dst, int pitc
 	drawBankString(bank, dst, pitch, width, height, drawX, drawY, glyphStr);
 }
 
-static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
+int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
 	if (!text || bank.numSprites <= 0)
 		return 0;
 
@@ -169,14 +169,14 @@ static int getBankStringWidth(const RA1SpriteBank &bank, const char *text) {
 
 // Approximate FUN_221B7/FUN_20BD3 space-advance behavior from available NUT glyphs.
 // The original reads per-font space width from metadata tables and caps it to 8.
-static int getBankSpaceAdvance(const RA1SpriteBank &bank) {
+int getBankSpaceAdvance(const RA1SpriteBank &bank) {
 	const int exclWidth = getBankStringWidth(bank, "!");
 	if (exclWidth <= 0)
 		return 6;
 	return MIN(exclWidth, 8);
 }
 
-static const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
+const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
 		const RA1SpriteBank &hudBank, const RA1SpriteBank &techBank, int layer) {
 	const bool techLayer = (layer >= 2);
 	const bool talkLayer = (layer == 1);
@@ -187,7 +187,7 @@ static const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
 	return (titleBank.numSprites > 0) ? titleBank : hudBank;
 }
 
-static int getBankSpaceHeight(const RA1SpriteBank &bank) {
+int getBankSpaceHeight(const RA1SpriteBank &bank) {
 	// In FUN_221B7 line advance is derived from the layer's space-glyph height (+4).
 	// With current NUT decoding we approximate that using the '!' glyph (index 0).
 	if (bank.numSprites > 0) {
@@ -199,7 +199,7 @@ static int getBankSpaceHeight(const RA1SpriteBank &bank) {
 }
 
 // FUN_1C794: direction bucket in range -4..4 from two points.
-static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
+int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
 	int dx = x2 - x1;
 	int dy = y1 - y2;
 	if (dy < 0) {
@@ -232,7 +232,7 @@ static int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2) {
 }
 
 // FUN_1CDA7 maps abs(FUN_1C794) to sprite base index: <=1 -> 0, ==2 -> 5, else -> 10.
-static int ra1ShotDirectionBucket(int dir) {
+int ra1ShotDirectionBucket(int dir) {
 	const int absDir = ABS(dir);
 	if (absDir <= 1)
 		return 0;
@@ -249,7 +249,7 @@ struct RA1ShotEmitterPair {
 };
 
 // DAT_244A and DAT_251A in ASSAULT.EXE data section, used by FUN_1D79C.
-static const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
+const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
 	{ 11, -11, -11, 0 }, { 16, -9, -16, -1 }, { 20, -6, -19, -3 }, { 20, -5, -21, -4 }, { -20, -6, 20, -5 },
 	{ -18, -9, 16, -1 }, { -13, -11, 13, 0 }, { -7, -13, 8, 2 }, { 1, -10, 3, 2 }, { 11, -16, -11, 4 },
 	{ 16, -14, -15, 1 }, { 19, -10, -19, -2 }, { 20, -5, -20, -4 }, { -20, -8, 19, -2 }, { -17, -11, 17, 1 },
@@ -258,7 +258,7 @@ static const RA1ShotEmitterPair kRA1ShotEmitters244A[27] = {
 	{ -5, -18, 9, 6 }, { -1, -11, -3, -6 }
 };
 
-static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
+const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
 	{ -1, -11, -3, -6 }, { 7, -12, -8, 1 }, { 14, -11, -12, 0 }, { 18, -9, -17, -1 }, { 21, -7, -19, -4 },
 	{ -20, -6, 21, -5 }, { -18, -8, 19, -2 }, { -16, -10, 16, -1 }, { -11, -12, 11, 0 }, { 1, -18, -2, -1 },
 	{ 8, -17, -5, 1 }, { 13, -15, -12, 2 }, { 17, -13, -15, 0 }, { 21, -8, -19, -2 }, { -19, -6, 21, -4 },
@@ -269,7 +269,7 @@ static const RA1ShotEmitterPair kRA1ShotEmitters251A[27] = {
 
 // DAT_25EC/DAT_25F0 and DAT_28BC in ASSAULT.EXE. GAME opcode 0x09 uses these
 // emitter offsets instead of the generic edge-beam fallback.
-static const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
+const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
 	{ -38, -14, 37, 6 }, { 37, -14, 37, 7 }, { 42, -11, -40, 11 }, { -37, -6, 38, 14 }, { -37, -5, 38, 15 },
 	{ -35, -19, 36, 11 }, { -35, -18, 35, 12 }, { -37, -15, 36, 16 }, { -37, -11, 34, 19 }, { -37, -10, 34, 20 },
 	{ -31, -24, 33, 16 }, { -31, -23, 33, 17 }, { -32, -19, 33, 22 }, { -34, -17, 29, 24 }, { -35, -15, 28, 25 },
@@ -281,7 +281,7 @@ static const RA1ShotEmitterPair kRA1FlightShotEmitters25EC[45] = {
 	{ 18, -32, -26, 25 }, { 19, -31, -25, 26 }, { 22, -28, -22, 29 }, { 25, -24, -19, 31 }, { 25, -24, -17, 31 }
 };
 
-static const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
+const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
 	{ 37, -14, -37, 6 }, { -38, -12, -36, 7 }, { -41, -10, 41, 10 }, { 39, -6, -36, 13 }, { 38, -5, -36, 15 },
 	{ 41, -8, -39, 1 }, { 40, -7, -38, 1 }, { 41, -4, -40, 5 }, { 39, -1, -40, 9 }, { 39, 1, -40, 10 },
 	{ -38, -5, 42, -3 }, { -39, -4, 42, -1 }, { -43, 0, 40, 2 }, { -41, 2, 39, 5 }, { -42, 4, 37, 6 },
@@ -293,7 +293,7 @@ static const RA1ShotEmitterPair kRA1FlightShotEmitters25F0[45] = {
 	{ 32, -15, -42, 10 }, { 34, -14, -40, 11 }, { 36, -12, -38, 13 }, { 39, -10, -35, 17 }, { 41, -9, -33, 17 }
 };
 
-static const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
+const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
 	{ -18, 0, 18, 0 }, { -18, -1, 18, 0 }, { -18, 0, 18, 0 }, { -18, 0, 17, 0 }, { -18, -1, 18, -1 },
 	{ -14, -3, 19, 3 }, { -15, -5, 20, 0 }, { -17, -4, 18, 1 }, { -15, -4, 18, 1 }, { -19, -4, 19, 2 },
 	{ -13, -9, 20, 2 }, { -16, -8, 19, 3 }, { -15, -9, 21, 3 }, { -14, -4, 18, 5 }, { -14, -2, 17, 6 },
@@ -306,7 +306,7 @@ static const RA1ShotEmitterPair kRA1FlightShotEmitters28BC[45] = {
 };
 
 // Small subset of FUN_20D43 draw flags used by RA1 shot sprites.
-static void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
+void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 	int x, int y, const RA1Sprite &spr, uint32 flags) {
 	if (!spr.data || spr.width <= 0 || spr.height <= 0)
 		return;
@@ -604,7 +604,7 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
 // The original does not draw a hardcoded pixel cross; it renders glyph markers
 // whose state depends on _targetProximity.
 void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
-	static const char kRA1TorpedoIndicator[] = "<d";
+	const char kRA1TorpedoIndicator[] = "<d";
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const int overlayY = ra1OverlayViewOffsetY(this);
@@ -725,8 +725,8 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 
 // renderLaserShots — FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
-	static const char kRA1TorpedoTrailLeft[] = "<<&";
-	static const char kRA1TorpedoTrailRight[] = "<<'";
+	const char kRA1TorpedoTrailLeft[] = "<<&";
+	const char kRA1TorpedoTrailRight[] = "<<'";
 	const bool torpedoMode = (_gameplayFlags75ff & 0x2) != 0;
 
 	if (_laserBank.numSprites <= 0 && !torpedoMode)
@@ -734,16 +734,16 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 
 	// DAT_2407 lookup used by FUN_1CDA7/FUN_1D79C for timer 1..5 interpolation.
 	// Entry 0 unused.
-	static const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
+	const int kShotLerpByTimer[6] = { 0, 8, 7, 6, 4, 0 };
 	// DAT_2413: on-foot lerp table (timer 5 = 1, not 0 like flight mode).
-	static const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
+	const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
 	// DAT_240e: gun barrel X offset indexed by shipDirIndex (for timer==5 first frame).
-	static const int16 kOnFootGunBarrelX[20] = {
+	const int16 kOnFootGunBarrelX[20] = {
 		4, 0, 8, 8, 7, 6, 4, 1, 0, 0,
 		0, -56, -47, -23, -13, 0, 13, 30, 54, 59
 	};
 	// DAT_2420: gun barrel Y offset indexed by shipDirIndex (for timer==5 first frame).
-	static const int16 kOnFootGunBarrelY[20] = {
+	const int16 kOnFootGunBarrelY[20] = {
 		0, 0, -56, -47, -23, -13, 0, 13, 30, 54,
 		59, -3, -19, -24, -30, -28, -30, -29, -20, -5
 	};
@@ -947,7 +947,7 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 // Level intro title data (from RunTwoLineTextSplash calls in original binary).
 // titleText is drawn at y=10, subtitleText at y=25 (matching original DrawUiString positions).
 // revealStartFrame/revealEndFrame control the frame range during which text is visible.
-static const struct {
+const struct {
 	const char *titleText;      // Top line (chapter number)
 	const char *subtitleText;   // Bottom line (level name)
 	int16 revealStartFrame;     // Frame at which text begins appearing
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 1052fef9ad2..68d9c2f404a 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -37,7 +37,7 @@ struct RA1Level7ResumeSegment {
 	int16 localStart;
 };
 
-static const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
+const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
 	{
 		{    0,  638,   0 },
 		{ 1416, 1468, 639 },
@@ -76,7 +76,7 @@ static const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
 	}
 };
 
-static int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
+int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
 	if (timelineFrame <= 0)
 		return 0;
 
@@ -102,7 +102,7 @@ static int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
 	return 0;
 }
 
-static int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
+int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
 	if (targetFrame <= 0)
 		return 0;
 
@@ -734,7 +734,7 @@ bool InsaneRebel1::runLevel6() {
 bool InsaneRebel1::runLevel7() {
 	debug(1, "InsaneRebel1: Running level 7");
 
-	static const char *const kLevel7Segments[] = {
+	const char *const kLevel7Segments[] = {
 		"LVL7/L7PLAY1.ANM",
 		"LVL7/L7PLAY2.ANM",
 		"LVL7/L7PLAY3.ANM",
@@ -842,7 +842,7 @@ bool InsaneRebel1::runLevel7() {
 bool InsaneRebel1::runLevel8() {
 	debug(1, "InsaneRebel1: Running level 8");
 
-	static const char *const kLevel8Routes[] = {
+	const char *const kLevel8Routes[] = {
 		"LVL8/L8PLAY.ANM",
 		"LVL8/L8PLAY2.ANM",
 		"LVL8/L8PLAY3.ANM"
@@ -1655,7 +1655,7 @@ bool InsaneRebel1::runLevel15() {
 // Matches original flow at 0x15597: intro → menu → level.
 void InsaneRebel1::runGame() {
 	typedef bool (InsaneRebel1::*RunLevelMethod)();
-	static const RunLevelMethod kLevelRunners[] = {
+	const RunLevelMethod kLevelRunners[] = {
 		&InsaneRebel1::runLevel1,
 		&InsaneRebel1::runLevel2,
 		&InsaneRebel1::runLevel3,
diff --git a/engines/scumm/insane/rebel2/audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
index f679b70f47d..14ace0f131a 100644
--- a/engines/scumm/insane/rebel2/audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -275,7 +275,7 @@ void InsaneRebel2::processAudioFrame(int16 feedSize) {
 // Standalone SAUD files from SYSTM/ loaded at init for one-shot SFX.
 // Original: FUN_0042a3b0 loads into DAT_00456888[0..7].
 
-static const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
+const char *const kRA2SfxFiles[InsaneRebel2::kRA2NumSfx] = {
 	"SYSTM/BLAST.SAD",    // 0 - Player laser fire
 	"SYSTM/CRASH.SAD",    // 1 - Corridor/wall collision
 	"SYSTM/EXPLODE.SAD",  // 2 - Enemy explosion
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 01c49a03452..97eb5bf40d8 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -218,7 +218,7 @@ void InsaneRebel2::playLevelBegin(int levelId) {
 	// Table of per-level text overlay parameters
 	// All levels use FUN_004171c5 (verified against decompiled level handlers)
 	// Text IDs are sequential: 0xAA (level 1) through 0xB8 (level 15)
-	static const TextOverlayParams levelTextParams[16] = {
+	const TextOverlayParams levelTextParams[16] = {
 		{ -1,   0,  0,   0,    0},    // Level 0 (unused)
 		{0xAA, 0xA0, 10,   5, 0x4B},  // Level 1:  FUN_00417E53
 		{0xAB, 0xA0, 10,   2, 0x46},  // Level 2:  FUN_00418063
@@ -605,7 +605,7 @@ int InsaneRebel2::runLevel(int levelId) {
 	// Levels 1-6 use types 0-5, but Level 6 also uses type 6 mid-level.
 	// Levels 7-15 use types 7-15 (gap at type 6 which is Level 6 phase 2).
 	// Level 15 also switches to type 16 mid-level at frame 0x21e.
-	static const int kLevelTypeMap[16] = {
+	const int kLevelTypeMap[16] = {
 		-1, 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15
 	};
 	_rebelLevelType = kLevelTypeMap[levelId];
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index f631909a4f6..d95d3ba7cf5 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -284,9 +284,7 @@ void InsaneRebel2::drawMenuItems(byte *renderBitmap, int pitch, int width, int h
 
 			// Flash color: (-((DAT_0047a7e4 & 1) == 0) & 8U) - 0x10
 			// bit0==0: 8-16=248(0xF8), bit0==1: 0-16=240(0xF0)
-			static int frameCounter = 0;
-			frameCounter++;
-			byte highlightColor = ((frameCounter / 8) & 1) ? 248 : 240;
+			byte highlightColor = ((_vm->_system->getMillis() / 133) & 1) ? 248 : 240;
 
 			// Box position: Y = itemY - 1 (0x67 vs 0x68)
 			int leftX = leftAligned ? 20 : (centerX - bracketWidth / 2);
@@ -447,7 +445,7 @@ void InsaneRebel2::drawMenuOverlay(byte *renderBitmap, int pitch, int width, int
 // ---------------------------------------------------------------------------
 
 // pauseFillRect -- Helper to fill a rectangle in the frame buffer with bounds checking.
-static void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
+void pauseFillRect(byte *buf, int bufW, int bufH, int x, int y, int w, int h, byte color) {
 	if (x < 0) { w += x; x = 0; }
 	if (y < 0) { h += y; y = 0; }
 	if (x + w > bufW) w = bufW - x;
@@ -1128,7 +1126,7 @@ Common::String InsaneRebel2::getRankString(int rating) {
 // Password table lookup - emulates FUN_0041BCE0
 // 90 entries: 15 levels × 6 difficulty slots, extracted from RA2WIN95.EXE at 0x481AF0
 // Index formula: difficulty + (level * 3 - 3) * 2, level is 1-based (1-15), difficulty 0-5
-static const char *const kPasswordTable[90] = {
+const char *const kPasswordTable[90] = {
 	// Level 1:  diff 0-5
 	"JABBA",    "EWOKS",    "BANTHA",   "ANAKIN",   "WOOKIEE",  "WOOKIEE",
 	// Level 2:  diff 0-5
@@ -1205,9 +1203,7 @@ void InsaneRebel2::drawChapterInfoLine(byte *renderBitmap, int pitch, int width,
 		if (!lockStr || !lockStr[0])
 			lockStr = "^f01^c248UNREGISTERED - PASSCODE REQUIRED";
 
-		static int cursorCounter = 0;
-		cursorCounter++;
-		char cursor = ((cursorCounter / 8) & 1) ? '_' : ' ';
+		char cursor = ((_vm->_system->getMillis() / 133) & 1) ? '_' : ' ';
 
 		Common::String displayStr = Common::String::format("%s ^c005%s%c",
 			lockStr, _passwordInput.c_str(), cursor);
@@ -1985,7 +1981,7 @@ void InsaneRebel2::drawOptionsOverlay(byte *renderBitmap, int pitch, int width,
 		items[6] = _optRapidFire ? "^f01^c005Rapid Fire On" : "^f01^c005Rapid Fire Off";
 
 	// [7] Volume Level (slider) — TRS 103 = "^f01^c005Volume Level: %hd%%"
-	static char volumeBuf[64];
+	char volumeBuf[64];
 	const char *volFmt = splayer->getString(103);
 	if (volFmt && volFmt[0])
 		Common::sprintf_s(volumeBuf, volFmt, (short)(_optVolumeLevel * 100 / 127));
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 7231de9997a..17e712ec3a1 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -951,8 +951,8 @@ void InsaneRebel2::renderScoreHUD(byte *renderBitmap, int pitch, int width, int
 // Save/load pilot profiles using ScummVM's save file system.
 // Original: FUN_00411980 (load) / FUN_00411A5D (save).
 
-static const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
-static const uint16 kPilotSaveVersion = 2;
+const uint32 kPilotSaveMagic = MKTAG('R', 'A', '2', 'P');
+const uint16 kPilotSaveVersion = 2;
 
 // loadPilots -- Load all pilot profiles from save files (FUN_00411980).
 bool InsaneRebel2::loadPilots() {
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index ef0c79af23c..43fa7090935 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -728,7 +728,7 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 }
 
 // drawTexturedSegment -- Textured segment between two points (FUN_00429360 port).
-static void drawTexturedSegment(byte *dst, int pitch, int width, int height,
+void drawTexturedSegment(byte *dst, int pitch, int width, int height,
                          int param_3, int param_4, int param_5, int param_6, int param_7, const byte *param_8,
                          int clipLeft, int clipTop, int clipRight, int clipBottom) {
 	// Near-direct port of FUN_00429360.
@@ -2093,7 +2093,7 @@ void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height,
 }
 
 // renderNutSpriteClipped -- Draw a NUT sprite with explicit clip rectangle.
-static void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
+void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
 		int clipLeft, int clipTop, int clipRight, int clipBottom,
 		int x, int y, NutRenderer *nut, int spriteIdx) {
 	if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars())
@@ -4234,10 +4234,7 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		reticleIndex = 47;  // 0x2F
 		break;
 	case 0x26: { // Turret/Cockpit - animated crosshair
-		static int turretAnimCounter = 0;
-		turretAnimCounter++;
-
-		int animOffset = (_targetLockTimer == 0) ? 0 : 3 - (turretAnimCounter & 3);
+		int animOffset = (_targetLockTimer == 0) ? 0 : 3 - ((_vm->_system->getMillis() / 33) & 3);
 
 		if (_rebelLevelType == 5) {
 			reticleIndex = 0x30 + animOffset;
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 75da1488559..44bab3adfd0 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -215,7 +215,7 @@ int InsaneRebel2::runLevel2() {
 	// Each phase gets a budget = tableBase + random(3). processWaveEnd() uses
 	// this budget to randomly redistribute kill credits, creating non-deterministic
 	// wave progression. Using calibrated defaults until exact table values extracted.
-	static const int16 kLevel2BudgetBase[3] = { 3, 3, 3 };  // Phase 1, 2, 3
+	const int16 kLevel2BudgetBase[3] = { 3, 3, 3 };  // Phase 1, 2, 3
 
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	int bonusCount = 0;     // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
@@ -1218,7 +1218,7 @@ int InsaneRebel2::runLevel11() {
 	int prevPhaseState = 0;
 
 	// Kill credit budget bases per phase (from level data table DAT_0047e0e8)
-	static const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
+	const int16 kLevel11BudgetBase[4] = { 3, 3, 3, 3 };
 
 	// Play cutscene (11CUT.SAN)
 	playCinematic("LEV11/11CUT.SAN");
@@ -1607,7 +1607,7 @@ int InsaneRebel2::runLevel12() {
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 
 	// Kill credit budget bases per phase
-	static const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
+	const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
 
 	// Play cutscene (12CUT.SAN)
 	playCinematic("LEV12/12CUT.SAN");


Commit: 8b81bcfb80e42cd41668bdd327a3ecb07de56c78
    https://github.com/scummvm/scummvm/commit/8b81bcfb80e42cd41668bdd327a3ecb07de56c78
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:51+02:00

Commit Message:
SCUMM: RA2: Fix mixed indentation

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


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 2d653a2cf16..77de59759ad 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -392,7 +392,7 @@ void InsaneRebel2::iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2,
 			}
 
 		} else if (value > 0x3ff) { // Bitmask case: value > 0x3FF
- 			for (int slot = 1; slot <= 9; ++slot) {
+			for (int slot = 1; slot <= 9; ++slot) {
 				if ((value & (1 << (slot - 1))) != 0) {
 					if (!isBitSet(targetId)) {
 						_rebelMaskCounters[slot]++;


Commit: 41a8508ee0c5398814b3616f4e38f9d2202ae353
    https://github.com/scummvm/scummvm/commit/41a8508ee0c5398814b3616f4e38f9d2202ae353
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:51+02:00

Commit Message:
SCUMM: RA: Clean up spacing and indentation

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


diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index e7ac7a13a0a..f108f223686 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -287,16 +287,16 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_maxChapterUnlocked = 0;
 	_interactiveVideoActive = false;
 	_gameCounter = 0;
-		_pathBranchEnabled = false;
-		_rightPathSelected = false;
-		_levelRouteIndex = -1;
-		_pendingRouteIndex = -1;
-		_pendingRouteStartFrame = 0;
-		_pendingRouteCutoverFrame = -1;
-		_levelRouteChoice = 0;
-		_levelGameplayPhase = 0;
-		_level5SuccessFramesRemaining = 0;
-		_menuActive = false;
+	_pathBranchEnabled = false;
+	_rightPathSelected = false;
+	_levelRouteIndex = -1;
+	_pendingRouteIndex = -1;
+	_pendingRouteStartFrame = 0;
+	_pendingRouteCutoverFrame = -1;
+	_levelRouteChoice = 0;
+	_levelGameplayPhase = 0;
+	_level5SuccessFramesRemaining = 0;
+	_menuActive = false;
 	_introTextActive = false;
 	_introTextStartFrame = 0;
 	_introTextEndFrame = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index c230a44aae3..5a85489b373 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -251,24 +251,24 @@ private:
 	int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
 	int16 _mouseOffsetY; // 0x9760-style accumulated recenter offset in DOS 200-space
 	int16 _mouseBiasX;   // 0x9774: current preprocessed horizontal bias
-		int16 _mouseBiasY;   // 0x9772: current preprocessed vertical bias
-		int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
-		int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
-		bool _mouseBiasLatch;  // 0x4486: one-frame large-jump latch
-		bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
-		int16 _joystickAxisX;   // Rebel-specific left-stick X captured from keymapper axis events
-		int16 _joystickAxisY;   // Rebel-specific left-stick Y captured from keymapper axis events
-		int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
-		int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
-		enum InputSource {
-			kInputSourceMouse,
-			kInputSourceJoystickAnalog,
-			kInputSourceJoystickDigital
-		};
-		InputSource _activeInputSource;
-
-		// 0x0B handler physics update (asteroid/surface levels)
-		void updateAsteroidPhysics();
+	int16 _mouseBiasY;   // 0x9772: current preprocessed vertical bias
+	int16 _mousePrevBiasX; // 0x9770: previous-frame biasX
+	int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
+	bool _mouseBiasLatch;  // 0x4486: one-frame large-jump latch
+	bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
+	int16 _joystickAxisX;   // Rebel-specific left-stick X captured from keymapper axis events
+	int16 _joystickAxisY;   // Rebel-specific left-stick Y captured from keymapper axis events
+	int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
+	int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
+	enum InputSource {
+		kInputSourceMouse,
+		kInputSourceJoystickAnalog,
+		kInputSourceJoystickDigital
+	};
+	InputSource _activeInputSource;
+
+	// 0x0B handler physics update (asteroid/surface levels)
+	void updateAsteroidPhysics();
 
 	// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
 	void updateOnFootPhysics();
@@ -377,16 +377,16 @@ private:
 	// Path branching for levels with left/right alternative videos.
 	// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
 	// We check ship position at that counter value to decide left vs right path.
-		static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
-		int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
-		bool _pathBranchEnabled;     // True when branching is active for this video
-		bool _rightPathSelected;     // True if player chose the right/easy path
-		int _levelRouteIndex;        // Current mid-level route/segment for branching levels
-		int _pendingRouteIndex;      // Next route requested by original frame-branch logic
-		int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
-		int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
-		int _levelRouteChoice;       // Level-local pending branch choice (0=none, 1=left, 2=right)
-		int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
+	static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
+	int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
+	bool _pathBranchEnabled;     // True when branching is active for this video
+	bool _rightPathSelected;     // True if player chose the right/easy path
+	int _levelRouteIndex;        // Current mid-level route/segment for branching levels
+	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
+	int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
+	int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
+	int _levelRouteChoice;       // Level-local pending branch choice (0=none, 1=left, 2=right)
+	int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
 
 	// Main menu / options state
 	void runOptionsMenu();
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 77de59759ad..f05bb063e1e 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -177,7 +177,7 @@ void InsaneRebel2::iactRebel2Scene1(byte *renderBitmap, int32 codecparam, int32
 	// par4 is word at offset +6
 	//
 	// Based on disassembly of FUN_4028C5 and FUN_4033CF:
-	// 
+	//
 	// For IACT opcode 4 (enemy position update), the structure is:
 	//   Offset +0x06: Type/SubType (par3)
 	//   Offset +0x08: Enemy ID
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 17e712ec3a1..c9fb01a370c 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -239,7 +239,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_iactSceneId2 = 0;
 
 	int i, j;
-	
+
 	for (i = 0; i < 12; i++)
 		_metEnemiesList[i] = 0;
 
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 90b8c162d29..ea3f31dbe69 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -477,7 +477,7 @@ public:
 	bool _pauseOverlayActive;
 
 	bool _introCursorPushed; // true when we've pushed an invisible cursor for intro
-	
+
 
 	int32 processMouse() override;
 	Common::Point getGameplayAimPoint();
@@ -672,15 +672,15 @@ public:
 	void enemyUpdate(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	Common::List<enemy> _enemies;
-	
+
 	// Current handler type for Rebel Assault 2 (determines crosshair sprite)
 	// Handler 0: Background only
 	// Handler 7: Third-Person Ship - uses crosshair sprite 0x2F (47)
-	// Handler 8: Third-Person On Foot - uses crosshair sprite 0x2E (46)  
+	// Handler 8: Third-Person On Foot - uses crosshair sprite 0x2E (46)
 	// Handler 0x19: FPS/Mixed view - uses crosshair sprite 0x2F (47)
 	// Handler 0x26: Turret/Cockpit - crosshair varies by level type
 	int _rebelHandler;
-	
+
 	// Level type from IACT opcode 6 par3 (corresponds to DAT_004436de)
 	// Determines crosshair variant for turret mode:
 	// - levelType == 5: Use sprites 0x30+ (48+) for crosshair
@@ -702,7 +702,7 @@ public:
 		int renderY;       // Y position to render
 		bool valid;        // True if this slot has valid data
 	};
-	
+
 	EmbeddedSanFrame _rebelEmbeddedHud[16];  // HUD overlay slots (userId 0-15)
 
 	// Load and decode an embedded SAN animation from IACT chunk data
@@ -744,7 +744,7 @@ public:
 		int scale;       // Determines sprite set (small/med/large)
 		bool active;
 	};
-	
+
 	Explosion _explosions[5];
 	void spawnExplosion(int x, int y, int objectHalfWidth);
 
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 43fa7090935..6c256f77c64 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -310,9 +310,9 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 		debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
 		return;
 	}
-	
+
 	Common::MemoryReadStream stream(animData, size);
-	
+
 	// Read ANIM header
 	uint32 animTag = stream.readUint32BE();
 	if (animTag != MKTAG('A','N','I','M')) {
@@ -321,7 +321,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 	}
 	uint32 animSize = stream.readUint32BE();
 	debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
-	
+
 	// Iterate through chunks to find FRME -> FOBJ
 	while (!stream.eos() && stream.pos() < size) {
 		uint32 tag = stream.readUint32BE();
@@ -389,7 +389,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 						// Update render position from FOBJ header
 						frame.renderX = left;
 						frame.renderY = top;
-						
+
 						// Read the raw FOBJ data
 						int32 dataSize = subSize - 14;
 						if (dataSize > 0) {
@@ -443,7 +443,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 							free(fobjData);
 						}
 					}
-					
+
 					// Done with FOBJ - assume only one relevant frame per embedded SAN
 					stream.seek(nextChunkPos);
 					goto end_parsing;
@@ -461,7 +461,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 				stream.skip(1);
 		}
 	}
-	
+
 	debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
 
 end_parsing:;
@@ -683,7 +683,7 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	const byte *srcData = nut->getCharData(spriteIdx);
 	int texW = nut->getCharWidth(spriteIdx);
 	int texH = nut->getCharHeight(spriteIdx);
-	
+
 	if (!srcData || texW <= 0 || texH <= 0)
 		return;
 	if (v < 0)
@@ -694,12 +694,12 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 	int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
 	int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
 	int err = dx + dy, e2;
-	
+
 	// Total length approximation for UV mapping
 	int totalDist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy);
 	if (totalDist == 0)
 		totalDist = 1;
-	
+
 	int currentDist = 0;
 
 	for (;;) {
@@ -708,21 +708,21 @@ void InsaneRebel2::drawTexturedLine(byte *dst, int pitch, int width, int height,
 			int u = (currentDist * texW) / totalDist;
 			if (u >= texW)
 				u = texW - 1;
-			
+
 			byte color = srcData[v * texW + u];
-			
+
 			// Check for transparency (0 and optionally 231)
-			if (color != 0 && (!mask231 || color != 231)) { 
+			if (color != 0 && (!mask231 || color != 231)) {
 				dst[y0 * pitch + x0] = color;
 			}
 		}
-		
+
 		if (x0 == x1 && y0 == y1)
 			break;
 		e2 = 2 * err;
 		if (e2 >= dy) { err += dy; x0 += sx; }
 		if (e2 <= dx) { err += dx; y0 += sy; }
-		
+
 		currentDist++;
 	}
 }
@@ -2227,17 +2227,17 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// Map mouse Y (0-200) to Scroll Y (0-60)
 	int maxScrollX = width - _vm->_screenWidth;
 	int maxScrollY = height - _vm->_screenHeight;
-	
+
 	if (maxScrollX < 0)
 		maxScrollX = 0;
 	if (maxScrollY < 0)
 		maxScrollY = 0;
-	
+
 	// Simple linear mapping: Center of screen corresponds to center of buffer
 	Common::Point aimPos = getGameplayAimPoint();
 	_viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
 	_viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
-	
+
 	_player->setScrollOffset(_viewX, _viewY);
 
 	// Death check: original game (FUN_417E53 line 25) exits video playback
@@ -2263,10 +2263,10 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
 	//
 	// We draw directly to screen at Y=180
-	
+
 	// Use video content coordinates, NOT buffer coordinates
 	const int videoWidth = 320;    // Native video width
-	const int videoHeight = 200;   // Native video height  
+	const int videoHeight = 200;   // Native video height
 	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
 
 	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 5cb8d9c1b88..bb952987007 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1120,35 +1120,35 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
 		act->addDefaultInputMapping("JOY_LEFT");
 		rebel1Keymap->addAction(act);
 
-			act = new Action("RA1RIGHT", _("Aim right / menu right"));
-			act->setCustomEngineActionEvent(kScummActionInsaneRight);
-			act->addDefaultInputMapping("JOY_RIGHT");
-			rebel1Keymap->addAction(act);
-
-			act = new Action("RA1STICKUP", _("Stick up"));
-			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
-			act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
-			rebel1Keymap->addAction(act);
-
-			act = new Action("RA1STICKDOWN", _("Stick down"));
-			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
-			act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
-			rebel1Keymap->addAction(act);
-
-			act = new Action("RA1STICKLEFT", _("Stick left"));
-			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
-			act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
-			rebel1Keymap->addAction(act);
-
-			act = new Action("RA1STICKRIGHT", _("Stick right"));
-			act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
-			act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
-			rebel1Keymap->addAction(act);
-
-			act = new Action("RA1FIRE", _("Fire / select"));
-			act->setCustomEngineActionEvent(kScummActionInsaneAttack);
-			act->addDefaultInputMapping("JOY_A");
-			rebel1Keymap->addAction(act);
+		act = new Action("RA1RIGHT", _("Aim right / menu right"));
+		act->setCustomEngineActionEvent(kScummActionInsaneRight);
+		act->addDefaultInputMapping("JOY_RIGHT");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1STICKUP", _("Stick up"));
+		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
+		act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1STICKDOWN", _("Stick down"));
+		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
+		act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1STICKLEFT", _("Stick left"));
+		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
+		act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1STICKRIGHT", _("Stick right"));
+		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
+		act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+		rebel1Keymap->addAction(act);
+
+		act = new Action("RA1FIRE", _("Fire / select"));
+		act->setCustomEngineActionEvent(kScummActionInsaneAttack);
+		act->addDefaultInputMapping("JOY_A");
+		rebel1Keymap->addAction(act);
 
 		act = new Action("RA1BACK", _("Back / skip"));
 		act->setKeyEvent(KeyState(KEYCODE_ESCAPE, ASCII_ESCAPE));


Commit: 10d5865288773b160b821ed28790dad04138babf
    https://github.com/scummvm/scummvm/commit/10d5865288773b160b821ed28790dad04138babf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:51+02:00

Commit Message:
SCUMM: RA: Improve descriptions and error messages

Changed paths:
    engines/scumm/metaengine.cpp
    engines/scumm/nut_renderer.cpp


diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index bb952987007..4cc3fa5cb8b 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -880,7 +880,7 @@ static const ExtraGuiOption enableTTS = {
 
 static const ExtraGuiOption enableRebel2HiRes = {
 	_s("High resolution mode"),
-	_s("Run the game in 640x400 high resolution mode instead of 320x200."),
+	_s("Run the game in 640x400 high resolution mode instead of 320x200"),
 	"rebel2_hires",
 	true,
 	0,
@@ -889,7 +889,7 @@ static const ExtraGuiOption enableRebel2HiRes = {
 
 static const ExtraGuiOption enableRebel2UnlockAll = {
 	_s("Unlock all levels"),
-	_s("All levels will be available without requiring passwords."),
+	_s("All levels will be available without requiring passwords"),
 	"rebel2_unlock_all",
 	false,
 	0,
diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index b84990ba1f5..ed45e6ad886 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -368,7 +368,7 @@ int NutRenderer::getCharHeight(byte c) const {
 
 const byte *NutRenderer::getCharData(byte c) {
 	if (c >= _numChars)
-		error("invalid character in NutRenderer::getCharData : %d (%d)", c, _numChars);
+		error("invalid character in NutRenderer::getCharData: %d (%d)", c, _numChars);
 
 	return _chars[c].src;
 }


Commit: 96ad3cc3b9cee58c929ee7622e07d50e25460f05
    https://github.com/scummvm/scummvm/commit/96ad3cc3b9cee58c929ee7622e07d50e25460f05
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:52+02:00

Commit Message:
SCUMM: RA: Update hashes in scumm-md5.txt

Changed paths:
    devtools/scumm-md5.txt
    engines/scumm/detection_tables.h
    engines/scumm/scumm-md5.h


diff --git a/devtools/scumm-md5.txt b/devtools/scumm-md5.txt
index 3a9ed9599a2..11029af684a 100644
--- a/devtools/scumm-md5.txt
+++ b/devtools/scumm-md5.txt
@@ -447,6 +447,14 @@ ft	Full Throttle
 	9b7452b5cd6d3ffb2b2f5118010af84f	116463537	en	Mac	Demo	Demo	Mac bundle	Fingolfin, Joachim Eberhard
 	8ead2c01c48c2e3d15d6a89b86cd84ab	19697	he	DOS	-	-	-	Hebrew fan translation
 
+rebel1	Star Wars: Rebel Assault
+	b9ac90e4411da1b6f7dd075cc4b03c2a	214435	en	DOS	-	-	-	-
+
+rebel2	Star Wars: Rebel Assault II: The Hidden Empire
+	6b73a08c535c0544785d73ff812908a0	9430	en	DOS	-	-	-	-
+
+	e977ed046b88e75b645491caeff37b0c	313689	en	DOS	Demo	Demo	-	-
+
 dig	The Dig
 	d8323015ecb8b10bf53474f6e6b0ae33	16304	All	All	-	-	-	Fingolfin
 	aad201302286c1cfee92321cd406e427	811008	All	Windows	Steam	Steam	Steam Version	Ben Castricum, Filippos Karapetis
diff --git a/engines/scumm/detection_tables.h b/engines/scumm/detection_tables.h
index d06d0de36c4..b1bcb6e8b46 100644
--- a/engines/scumm/detection_tables.h
+++ b/engines/scumm/detection_tables.h
@@ -49,7 +49,6 @@ static const char *const directoryGlobs[] = {
 	"Contents", // Mac Steam versions
 	"MacOS",    // Mac Steam versions
 	"Resources", // Mac SE/Remastered versions
-	"OPEN",     // RA2 demo detection (O_DEMO.SAN)
 	0
 };
 
@@ -504,8 +503,8 @@ static const GameFilenamePattern gameFilenamesTable[] = {
 
 	{ "rebel1", "ASSAULT.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
 
-	{ "rebel2", "RA2START.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
-	{ "rebel2", "O_DEMO.SAN", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
+	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "" },
+	{ "rebel2", "REBEL2.EXE", kGenUnchanged, UNK_LANG, Common::kPlatformDOS, "Demo" },
 
 	{ "comi", "comi.la%d", kGenDiskNum, UNK_LANG, UNK, 0 },
 
diff --git a/engines/scumm/scumm-md5.h b/engines/scumm/scumm-md5.h
index 72d64bcb65c..38d31631068 100644
--- a/engines/scumm/scumm-md5.h
+++ b/engines/scumm/scumm-md5.h
@@ -1,5 +1,5 @@
 /*
-  This file was generated by the md5table tool on Wed Mar  4 16:30:02 2026
+  This file was generated by the md5table tool on Thu Apr 30 19:26:15 2026
   DO NOT EDIT MANUALLY!
  */
 
@@ -373,7 +373,7 @@ static const MD5Table md5table[] = {
 	{ "6b27dbcd8d5697d5c918eeca0f68ef6a", "puttrace", "HE CUP", "Preview", 3901484, Common::UNK_LANG, Common::kPlatformUnknown },
 	{ "6b3ec67da214f558dc5ceaa2acd47453", "indy3", "EGA", "EGA", 5361, Common::EN_ANY, Common::kPlatformDOS },
 	{ "6b5a3fef241e90d4b2e77f1e222773ee", "maniac", "NES", "", 2082, Common::SV_SWE, Common::kPlatformNES },
-	{ "TODO", "rebel2", "VGA", "VGA", 9430, Common::EN_ANY, Common::kPlatformDOS },
+	{ "6b73a08c535c0544785d73ff812908a0", "rebel2", "", "", 9430, Common::EN_ANY, Common::kPlatformDOS },
 	{ "6bca7a1a96d16e52b8f3c42b50dbdca3", "fbear", "HE 62", "", -1, Common::JA_JPN, Common::kPlatform3DO },
 	{ "6bf70eee5de3d24d2403e0dd3d267e8a", "spyfox", "", "", 49221, Common::EN_USA, Common::kPlatformWindows },
 	{ "6c2bff0e327f2962e809c2e1a82d7309", "monkey", "VGA", "", 8347, Common::EN_ANY, Common::kPlatformAmiga },


Commit: 5a64207352e9dc70bc7483be6a39790e773ef6f5
    https://github.com/scummvm/scummvm/commit/5a64207352e9dc70bc7483be6a39790e773ef6f5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:52+02:00

Commit Message:
SCUMM: RA2: Make codec 45 line-size and probe fields signed

Changed paths:
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/smush/rebel/codec_ra2.cpp


diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 6c256f77c64..32a4463f66e 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -148,7 +148,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
 	if (!foundValidOffset) {
 		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-			int testLineSize = READ_LE_UINT16(src + testOffset);
+			int testLineSize = READ_LE_INT16(src + testOffset);
 			// A valid first line size should be: > 0, <= width*2
 			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
 				// Validate line-size sequence
@@ -157,7 +157,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 				bool validSum = true;
 
 				while (linesTest < height && testPtr + 2 <= src + dataSize) {
-					int ls = READ_LE_UINT16(testPtr);
+					int ls = READ_LE_INT16(testPtr);
 					if (ls <= 0 || ls > width * 2) {
 						validSum = false;
 						break;
@@ -185,13 +185,13 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	const byte *dataEnd = src + dataSize;
 
 	// Check if this is per-line RLE or continuous RLE
-	int firstVal = READ_LE_UINT16(srcPtr);
+	int firstVal = READ_LE_INT16(srcPtr);
 	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
 
 	if (perLineMode) {
 		debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
 		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-			int lineSize = READ_LE_UINT16(srcPtr);
+			int lineSize = READ_LE_INT16(srcPtr);
 			srcPtr += 2;
 			if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr))
 				break;
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index 7b9309f8d51..d9914ab9e4d 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -170,7 +170,7 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
 		// Probe offsets to find valid RLE start
 		// Valid start should have reasonable line size values
 		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-			int testLineSize = READ_LE_UINT16(src + testOffset);
+			int testLineSize = READ_LE_INT16(src + testOffset);
 			// A valid line size should be positive and reasonable for the width
 			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
 				// Further validation: try to count valid line sizes
@@ -179,7 +179,7 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
 				bool validSum = true;
 
 				while (linesTest < height && testPtr + 2 <= src + dataSize) {
-					int ls = READ_LE_UINT16(testPtr);
+					int ls = READ_LE_INT16(testPtr);
 					if (ls <= 0 || ls > width * 2) {
 						validSum = false;
 						break;
@@ -200,13 +200,13 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
 	const byte *dataEnd = src + (dataSize - headerSkip);
 
 	// Check first value to determine per-line vs continuous mode
-	int firstVal = (src + 2 <= dataEnd) ? READ_LE_UINT16(src) : 0;
+	int firstVal = (src + 2 <= dataEnd) ? READ_LE_INT16(src) : 0;
 	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
 
 	if (perLineMode) {
 		// Per-line RLE with 2-byte size headers
 		for (int row = 0; row < height && src < dataEnd; row++) {
-			int lineSize = READ_LE_UINT16(src);
+			int lineSize = READ_LE_INT16(src);
 			src += 2;
 			if (lineSize <= 0 || lineSize > (int)(dataEnd - src))
 				break;


Commit: 804223f4226fcc02e967570391d483d408b44e29
    https://github.com/scummvm/scummvm/commit/804223f4226fcc02e967570391d483d408b44e29
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:52+02:00

Commit Message:
SCUMM: RA2: Clean up codec 45 debug statements

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 32a4463f66e..181a32786ea 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -129,11 +129,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
 	// May have a 6-byte sub-header starting with "01 FE"
 
-	debug("Rebel2: Codec 45 first 20 bytes: %02X %02X %02X %02X %02X %02X | %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-		src[0], src[1], src[2], src[3], src[4], src[5], src[6], src[7],
-		src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
-		src[16], src[17], src[18], src[19]);
-
 	// Probe for header offset
 	int headerSkip = 0;
 	bool foundValidOffset = false;
@@ -141,7 +136,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	// Check for known 6-byte header pattern: 01 FE XX XX XX XX
 	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
 		headerSkip = 6;
-		debug("Rebel2: Codec 45 found 01 FE header, skipping 6 bytes");
 		foundValidOffset = true;
 	}
 
@@ -170,7 +164,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 				if (validSum && linesTest >= height - 1) {
 					headerSkip = testOffset;
 					foundValidOffset = true;
-					debug("Rebel2: Codec 45 found valid RLE at offset %d (tested %d lines)", testOffset, linesTest);
 					break;
 				}
 			}
@@ -178,7 +171,7 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	}
 
 	if (!foundValidOffset) {
-		debug("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
+		warning("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
 	}
 
 	const byte *srcPtr = src + headerSkip;
@@ -189,7 +182,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
 
 	if (perLineMode) {
-		debug("Rebel2: Codec 45 using per-line RLE (firstLineSize=%d)", firstVal);
 		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
 			int lineSize = READ_LE_INT16(srcPtr);
 			srcPtr += 2;
@@ -216,7 +208,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 		}
 	} else {
 		// Continuous BOMP RLE (no per-line headers)
-		debug("Rebel2: Codec 45 using continuous BOMP RLE");
 		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
 			byte *rowDst = dst + row * width;
 			int x = 0;
@@ -240,15 +231,6 @@ void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int heig
 			}
 		}
 	}
-
-	// Count non-zero pixels for debug
-	int nonZero = 0;
-	for (int i = 0; i < width * height; i++) {
-		if (dst[i] != 0)
-			nonZero++;
-	}
-	debug("Rebel2: Decoded codec 45: %dx%d, %d non-zero (%d%%)",
-		width, height, nonZero, (nonZero * 100) / (width * height));
 }
 
 // renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.


Commit: 865d55c54b5d22720b22b651e503fad4df640da5
    https://github.com/scummvm/scummvm/commit/865d55c54b5d22720b22b651e503fad4df640da5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:53+02:00

Commit Message:
SCUMM: RA2: Remove goto and redundant seek

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 181a32786ea..e96a7b91234 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -427,8 +427,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 					}
 
 					// Done with FOBJ - assume only one relevant frame per embedded SAN
-					stream.seek(nextChunkPos);
-					goto end_parsing;
+					return;
 				} else {
 					// Skip other sub-chunks (AHDR inside FRME?) or padding
 					stream.seek(nextSubPos);
@@ -445,8 +444,6 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 	}
 
 	debug("Rebel2: No FOBJ found in embedded SAN userId=%d", userId);
-
-end_parsing:;
 }
 
 // Spawn explosion into the shared 5-slot system.


Commit: 2e39caffe5665da7efec9cef50d965d14ead1ece
    https://github.com/scummvm/scummvm/commit/2e39caffe5665da7efec9cef50d965d14ead1ece
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:53+02:00

Commit Message:
SCUMM: Remove extra blank lines from NutRenderer

Changed paths:
    engines/scumm/nut_renderer.cpp


diff --git a/engines/scumm/nut_renderer.cpp b/engines/scumm/nut_renderer.cpp
index ed45e6ad886..5fa5819a613 100644
--- a/engines/scumm/nut_renderer.cpp
+++ b/engines/scumm/nut_renderer.cpp
@@ -414,8 +414,6 @@ int NutRenderer::drawCharV7(byte *buffer, Common::Rect &clipRect, int x, int y,
 	if (chr >= _numChars)
 		return 0;
 
-
-
 	if (_direction < 0)
 		x -= _chars[chr].width;
 


Commit: e4ee27d61fc2b9401c774f74d2e88b932e2f51d9
    https://github.com/scummvm/scummvm/commit/e4ee27d61fc2b9401c774f74d2e88b932e2f51d9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:53+02:00

Commit Message:
SCUMM: RA2: Remove codec duplication

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/smush/rebel/codec_ra2.cpp
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index f05bb063e1e..1f676bf0733 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -27,14 +27,12 @@
 
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
 
 #include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
-// External codec functions from codec1.cpp
-extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
 //
 // procPreRendering -- Pre-frame setup: background restore and corridor overlays.
 //
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index ea3f31dbe69..650613168e1 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -709,30 +709,10 @@ public:
 	// userId: HUD slot (1-4), animData: raw ANIM data, size: data size, renderBitmap: current frame buffer
 	void loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) override;
 
-	// ---------------------------------------------------------------------------
-	// Embedded Frame Codec Decoders
-	// ---------------------------------------------------------------------------
-	// Decode different codec formats used in embedded ANIM/FOBJ data.
-	// Based on retail FUN_0042C590 (codec 1), FUN_0042BD60 (codec 21), etc.
-
-	// Decode codec 21/44 (Line Update) - skip/copy pairs per line
-	// Used for fonts and some HUD frames (FUN_0042BD60)
-	void decodeCodec21(byte *dst, const byte *src, int width, int height);
-
-	// Decode codec 23 (Skip/Copy with embedded RLE) - hybrid format
-	// Used for embedded HUD frames with transparency (FUN_0042BBF0)
-	void decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize);
-
-	// Decode codec 45 (RA2-specific BOMP RLE) - variable header format
-	// Used for small animation elements and HUD pieces (FUN_0042B5F0)
-	void decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize);
-
 	// Render a decoded embedded frame to the video buffer
 	// Handles transparency (color 0 and 231) and boundary checks
 	void renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId);
 
-
-
 	int16 _rebelLinks[512][3]; // Dependency links: Slot 0 (Disable on death), Slot 1/2 (Enable on death)
 	void clearBit(int n);
 	bool isShootingAllowed();  // FUN_0040d836/FUN_00401CCF: Check control mode before spawning shots
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index e96a7b91234..d01ecd6dbd2 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -30,209 +30,16 @@
 
 #include "scumm/smush/smush_player.h"
 #include "scumm/smush/smush_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
 
 #include "scumm/insane/rebel2/rebel.h"
 
 namespace Scumm {
 
-// External codec functions from codec1.cpp / codec_ra2.cpp
+// External codec functions from codec1.cpp
 extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-extern void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
-
-// ---------------------------------------------------------------------------
-// Embedded Frame Codec Decoders
-// ---------------------------------------------------------------------------
-// Retail codec functions FUN_0042BD60, FUN_0042BBF0, FUN_0042B5F0.
-
-// decodeCodec21 -- Codec 21/44: line update codec (FUN_0042BD60).
-void InsaneRebel2::decodeCodec21(byte *dst, const byte *src, int width, int height) {
-	// Codec 21/44: Line Update codec (FUN_0042BD60)
-	// Format: each line has 2-byte size header, then pairs of (skip, count+1, literal_bytes)
-	for (int row = 0; row < height; row++) {
-		int lineDataSize = READ_LE_UINT16(src);
-		src += 2;
-		const byte *lineEnd = src + lineDataSize;
-		byte *lineDst = dst + row * width;
-		int x = 0;
-
-		while (src < lineEnd && x < width) {
-			int skip = READ_LE_UINT16(src);
-			src += 2;
-			x += skip;
-			if (src >= lineEnd)
-				break;
-
-			int count = READ_LE_UINT16(src) + 1;
-			src += 2;
-			while (count-- > 0 && x < width && src < lineEnd) {
-				lineDst[x++] = *src++;
-			}
-		}
-		src = lineEnd;
-	}
-}
-
-// decodeCodec23 -- Codec 23: skip/copy with embedded RLE (FUN_0042BBF0).
-void InsaneRebel2::decodeCodec23(byte *dst, const byte *src, int width, int height, int dataSize) {
-	// Codec 23: Skip/Copy with embedded RLE (FUN_0042BBF0)
-	// Format: each line has 2-byte size, then pairs of (skip, runSize, RLE_data)
-	const byte *dataEnd = src + dataSize;
-
-	for (int row = 0; row < height && src < dataEnd; row++) {
-		int lineDataSize = READ_LE_UINT16(src);
-		src += 2;
-		const byte *lineEnd = src + lineDataSize;
-		byte *lineDst = dst + row * width;
-		int x = 0;
-
-		while (src < lineEnd && x < width) {
-			int skip = READ_LE_UINT16(src);
-			src += 2;
-			x += skip;
-			if (src >= lineEnd || x >= width)
-				break;
-
-			int runSize = READ_LE_UINT16(src);
-			src += 2;
-
-			// Decode RLE within this run
-			const byte *runEnd = src + runSize;
-			while (src < runEnd && x < width) {
-				byte code = *src++;
-				int num = (code >> 1) + 1;
-				if (num > width - x)
-					num = width - x;
-
-				if (code & 1) {
-					// RLE run
-					byte color = (src < runEnd) ? *src++ : 0;
-					for (int i = 0; i < num && x < width; i++) {
-						lineDst[x++] = color;
-					}
-				} else {
-					// Literal run
-					for (int i = 0; i < num && x < width && src < runEnd; i++) {
-						lineDst[x++] = *src++;
-					}
-				}
-			}
-			src = runEnd;
-		}
-		src = lineEnd;
-	}
-}
-
-// decodeCodec45 -- Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0).
-void InsaneRebel2::decodeCodec45(byte *dst, const byte *src, int width, int height, int dataSize) {
-	// Codec 45: RA2-specific BOMP RLE with variable header (FUN_0042B5F0)
-	// May have a 6-byte sub-header starting with "01 FE"
-
-	// Probe for header offset
-	int headerSkip = 0;
-	bool foundValidOffset = false;
-
-	// Check for known 6-byte header pattern: 01 FE XX XX XX XX
-	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
-		headerSkip = 6;
-		foundValidOffset = true;
-	}
-
-	// If no known header found, probe offsets 0, 2, 4, 6 to find valid RLE start
-	if (!foundValidOffset) {
-		for (int testOffset = 0; testOffset <= 6 && testOffset + 2 <= dataSize; testOffset += 2) {
-			int testLineSize = READ_LE_INT16(src + testOffset);
-			// A valid first line size should be: > 0, <= width*2
-			if (testLineSize > 0 && testLineSize <= width * 2 && testLineSize < dataSize - testOffset) {
-				// Validate line-size sequence
-				int linesTest = 0;
-				const byte *testPtr = src + testOffset;
-				bool validSum = true;
-
-				while (linesTest < height && testPtr + 2 <= src + dataSize) {
-					int ls = READ_LE_INT16(testPtr);
-					if (ls <= 0 || ls > width * 2) {
-						validSum = false;
-						break;
-					}
-					testPtr += ls + 2;
-					linesTest++;
-				}
-
-				// Accept if we got close to expected number of lines
-				if (validSum && linesTest >= height - 1) {
-					headerSkip = testOffset;
-					foundValidOffset = true;
-					break;
-				}
-			}
-		}
-	}
-
-	if (!foundValidOffset) {
-		warning("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
-	}
-
-	const byte *srcPtr = src + headerSkip;
-	const byte *dataEnd = src + dataSize;
-
-	// Check if this is per-line RLE or continuous RLE
-	int firstVal = READ_LE_INT16(srcPtr);
-	bool perLineMode = (firstVal > 0 && firstVal <= width * 2);
-
-	if (perLineMode) {
-		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-			int lineSize = READ_LE_INT16(srcPtr);
-			srcPtr += 2;
-			if (lineSize <= 0 || lineSize > (int)(dataEnd - srcPtr))
-				break;
-
-			const byte *lineEnd = srcPtr + lineSize;
-			byte *rowDst = dst + row * width;
-			int x = 0;
-
-			while (srcPtr < lineEnd && x < width) {
-				byte ctrl = *srcPtr++;
-				int count = (ctrl >> 1) + 1;
-				if (ctrl & 1) {
-					byte color = (srcPtr < lineEnd) ? *srcPtr++ : 0;
-					for (int i = 0; i < count && x < width; i++)
-						rowDst[x++] = color;
-				} else {
-					for (int i = 0; i < count && x < width && srcPtr < lineEnd; i++)
-						rowDst[x++] = *srcPtr++;
-				}
-			}
-			srcPtr = lineEnd;
-		}
-	} else {
-		// Continuous BOMP RLE (no per-line headers)
-		for (int row = 0; row < height && srcPtr < dataEnd; row++) {
-			byte *rowDst = dst + row * width;
-			int x = 0;
-
-			while (x < width && srcPtr < dataEnd) {
-				byte ctrl = *srcPtr++;
-				int count = (ctrl >> 1) + 1;
-
-				if (ctrl & 1) {
-					// RLE fill
-					byte color = (srcPtr < dataEnd) ? *srcPtr++ : 0;
-					for (int i = 0; i < count && x < width; i++) {
-						rowDst[x++] = color;
-					}
-				} else {
-					// Literal copy
-					for (int i = 0; i < count && x < width && srcPtr < dataEnd; i++) {
-						rowDst[x++] = *srcPtr++;
-					}
-				}
-			}
-		}
-	}
-}
-
 // renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
 void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
 	// Render the decoded embedded frame to the video buffer
@@ -391,16 +198,16 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
 							} else if (codec == 21 || codec == 44) {
 								// Codec 21/44: Line update (FUN_0042BD60)
-								decodeCodec21(frame.pixels, fobjData, width, height);
+								smushDecodeLineUpdate(frame.pixels, fobjData, 0, 0, width, height, width);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
 							} else if (codec == 45) {
 								// Codec 45: RA2-specific BOMP RLE (FUN_0042B5F0)
-								decodeCodec45(frame.pixels, fobjData, width, height, dataSize);
+								smushDecodeRA2Bomp(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
 								frame.valid = true;
 							} else if (codec == 23) {
 								// Codec 23: Skip/copy with embedded RLE (FUN_0042BBF0)
-								decodeCodec23(frame.pixels, fobjData, width, height, dataSize);
+								smushDecodeSkipRLE(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
 							} else {
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index d9914ab9e4d..4251a4342e1 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -21,7 +21,10 @@
 
 // Rebel Assault 2 SMUSH video codecs
 
+#include "scumm/smush/rebel/codec_ra2.h"
+
 #include "common/endian.h"
+#include "common/textconsole.h"
 
 #include "scumm/bomp.h"
 
@@ -162,10 +165,12 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
 
 	// Detect header pattern and find RLE data start
 	int headerSkip = 0;
+	bool foundValidOffset = false;
 
 	// Check for common 6-byte header pattern: 01 FE XX XX XX XX
 	if (dataSize > 6 && src[0] == 0x01 && src[1] == 0xFE) {
 		headerSkip = 6;
+		foundValidOffset = true;
 	} else {
 		// Probe offsets to find valid RLE start
 		// Valid start should have reasonable line size values
@@ -190,12 +195,17 @@ void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width
 
 				if (validSum && linesTest >= height - 1) {
 					headerSkip = testOffset;
+					foundValidOffset = true;
 					break;
 				}
 			}
 		}
 	}
 
+	if (!foundValidOffset) {
+		warning("Rebel2: Codec 45 couldn't find valid RLE offset, using offset 0");
+	}
+
 	src += headerSkip;
 	const byte *dataEnd = src + (dataSize - headerSkip);
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index ad3ea174974..9c7dcfd685c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -30,6 +30,7 @@
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
 #include "scumm/smush/rebel/smush_player_ra1.h"
 
 #include "scumm/insane/rebel1/rebel.h"
@@ -292,8 +293,6 @@ void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, in
 		dst += pitch;
 	} while (--height);
 }
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
 /**
  * RA1 codec 21: Skip/copy line codec (FUN_10D41).
  */
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index bdf5502ed92..7dd4a90e0c1 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -32,6 +32,7 @@
 #include "scumm/scumm_v7.h"
 #include "scumm/smush/smush_font.h"
 #include "scumm/smush/rebel/smush_multi_font.h"
+#include "scumm/smush/rebel/codec_ra2.h"
 #include "scumm/smush/rebel/smush_player_ra2.h"
 
 #include "scumm/insane/insane.h"
@@ -39,11 +40,6 @@
 
 namespace Scumm {
 
-// Forward declarations for RA2 codec functions (defined in codec_ra2.cpp)
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
-
 // ---------------------------------------------------------------------------
 // SmushPlayerRebel2 — construction / destruction
 // ---------------------------------------------------------------------------


Commit: 8b11f84c8d15a829ac3b686197cb1ed79f2af030
    https://github.com/scummvm/scummvm/commit/8b11f84c8d15a829ac3b686197cb1ed79f2af030
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:54+02:00

Commit Message:
SCUMM: RA2: Add missing header

Changed paths:
  A engines/scumm/smush/rebel/codec_ra2.h


diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
new file mode 100644
index 00000000000..f9b0191623b
--- /dev/null
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -0,0 +1,36 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_REBEL_CODEC_RA2_H
+#define SCUMM_SMUSH_REBEL_CODEC_RA2_H
+
+#include "common/scummsys.h"
+
+namespace Scumm {
+
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+
+} // End of namespace Scumm
+
+#endif


Commit: 695edd8a7938ccd4f256fe8a5aae979765465429
    https://github.com/scummvm/scummvm/commit/695edd8a7938ccd4f256fe8a5aae979765465429
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:54+02:00

Commit Message:
SCUMM: RA2: Refactor embedded frame region blitting

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 d01ecd6dbd2..0023a910441 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -40,6 +40,55 @@ namespace Scumm {
 extern void smushDecodeRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 extern void smushDecodeUncompressed(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
 
+static bool isValidEmbeddedFrame(const InsaneRebel2::EmbeddedSanFrame &frame) {
+	return frame.valid && frame.pixels && frame.width > 0 && frame.height > 0;
+}
+
+static int countEmbeddedFramePixels(const InsaneRebel2::EmbeddedSanFrame &frame) {
+	if (!isValidEmbeddedFrame(frame))
+		return 0;
+
+	int count = 0;
+	for (int i = 0; i < frame.width * frame.height; i++) {
+		if (frame.pixels[i] != 0)
+			count++;
+	}
+
+	return count;
+}
+
+static void blitEmbeddedFrameRegion(byte *renderBitmap, int pitch, int clipWidth, int clipHeight,
+		const InsaneRebel2::EmbeddedSanFrame &frame, int destX, int destY,
+		int srcX, int srcY, int drawWidth, int drawHeight) {
+	if (!renderBitmap || !isValidEmbeddedFrame(frame) || drawWidth <= 0 || drawHeight <= 0)
+		return;
+	if (srcX < 0 || srcY < 0)
+		return;
+
+	drawWidth = MIN(drawWidth, frame.width - srcX);
+	drawHeight = MIN(drawHeight, frame.height - srcY);
+	if (drawWidth <= 0 || drawHeight <= 0)
+		return;
+
+	for (int y = 0; y < drawHeight; y++) {
+		int dy = destY + y;
+		if (dy < 0 || dy >= clipHeight)
+			continue;
+
+		const byte *srcRow = frame.pixels + (srcY + y) * frame.width + srcX;
+		byte *dstRow = renderBitmap + dy * pitch;
+		for (int x = 0; x < drawWidth; x++) {
+			int dx = destX + x;
+			if (dx < 0 || dx >= clipWidth)
+				continue;
+
+			byte pixel = srcRow[x];
+			if (pixel != 0 && pixel != 231)
+				dstRow[dx] = pixel;
+		}
+	}
+}
+
 // renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
 void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
 	// Render the decoded embedded frame to the video buffer
@@ -72,18 +121,8 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
 	int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
 	int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
 
-	for (int y = 0; y < frame.height && (frame.renderY + y) < bufHeight; y++) {
-		for (int x = 0; x < frame.width && (frame.renderX + x) < pitch; x++) {
-			byte pixel = frame.pixels[y * frame.width + x];
-			if (pixel != 0 && pixel != 231) {  // 0 and 231 = transparent
-				int destX = frame.renderX + x;
-				int destY = frame.renderY + y;
-				if (destX >= 0 && destY >= 0) {
-					renderBitmap[destY * pitch + destX] = pixel;
-				}
-			}
-		}
-	}
+	blitEmbeddedFrameRegion(renderBitmap, pitch, pitch, bufHeight, frame,
+		frame.renderX, frame.renderY, 0, 0, frame.width, frame.height);
 	debug("Rebel2: Rendered embedded HUD %d at (%d,%d)", userId, frame.renderX, frame.renderY);
 }
 
@@ -217,11 +256,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 
 							// Count non-zero pixels to verify frame has content
 							if (frame.valid) {
-								int nonZeroPixels = 0;
-								for (int i = 0; i < width * height; i++) {
-									if (frame.pixels[i] != 0)
-										nonZeroPixels++;
-								}
+								int nonZeroPixels = countEmbeddedFramePixels(frame);
 								debug("Rebel2: Frame userId=%d has %d non-zero pixels (%d%%)",
 									userId, nonZeroPixels, (nonZeroPixels * 100) / (width * height));
 							}
@@ -2672,7 +2707,7 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 
 	for (int hudSlot = 1; hudSlot < 16; hudSlot++) {
 		EmbeddedSanFrame &frame = _rebelEmbeddedHud[hudSlot];
-		if (!frame.valid || !frame.pixels || frame.width <= 0 || frame.height <= 0)
+		if (!isValidEmbeddedFrame(frame))
 			continue;
 
 		// Handler 25: Skip slot 4 (corridor overlay) in post-rendering.
@@ -2707,23 +2742,11 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 				int selectedOffset = _shipDirectionIndex % groupCount;
 				int selectedId = groupMembers[selectedOffset];
 
-				// Verify selected frame has pixels
 				EmbeddedSanFrame &selectedFrame = _rebelEmbeddedHud[selectedId];
-				int nonZero = 0;
-				for (int i = 0; i < selectedFrame.width * selectedFrame.height; i++) {
-					if (selectedFrame.pixels[i] != 0)
-						nonZero++;
-				}
-
-				if (nonZero == 0) {
+				if (countEmbeddedFramePixels(selectedFrame) == 0) {
 					for (int i = 0; i < groupCount; i++) {
 						EmbeddedSanFrame &altFrame = _rebelEmbeddedHud[groupMembers[i]];
-						int altNonZero = 0;
-						for (int j = 0; j < altFrame.width * altFrame.height; j++) {
-							if (altFrame.pixels[j] != 0)
-								altNonZero++;
-						}
-						if (altNonZero > 0) {
+						if (countEmbeddedFramePixels(altFrame) > 0) {
 							selectedId = groupMembers[i];
 							break;
 						}
@@ -2762,19 +2785,8 @@ void InsaneRebel2::renderEmbeddedHudOverlays(byte *renderBitmap, int pitch, int
 		debug(3, "Rebel2: Rendering embedded HUD slot=%d size=%dx%d at (%d,%d)",
 			hudSlot, frame.width, frame.height, destX, destY);
 
-		// Draw frame with transparency (pixel 0 and 231 = transparent)
-		for (int y = 0; y < frame.height && (destY + y) < height; y++) {
-			for (int x = 0; x < frame.width && (destX + x) < pitch; x++) {
-				byte pixel = frame.pixels[y * frame.width + x];
-				if (pixel != 0 && pixel != 231) {
-					int fx = destX + x;
-					int fy = destY + y;
-					if (fx >= 0 && fy >= 0) {
-						renderBitmap[fy * pitch + fx] = pixel;
-					}
-				}
-			}
-		}
+		blitEmbeddedFrameRegion(renderBitmap, pitch, pitch, height, frame,
+			destX, destY, 0, 0, frame.width, frame.height);
 	}
 }
 
@@ -3336,7 +3348,7 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
 		return;
 
 	EmbeddedSanFrame &shipFrame = _rebelEmbeddedHud[11];
-	if (!shipFrame.valid || !shipFrame.pixels || shipFrame.width <= 0 || shipFrame.height <= 0)
+	if (!isValidEmbeddedFrame(shipFrame))
 		return;
 
 	// Calculate display offset
@@ -3368,21 +3380,8 @@ void InsaneRebel2::renderFallbackShip(byte *renderBitmap, int pitch, int width,
 	int drawX = shipScreenX - spriteW / 2 + _viewX;
 	int drawY = shipScreenY - spriteH / 2 + _viewY;
 
-	// Blit from embedded HUD
-	for (int y = 0; y < spriteH && (drawY + y) < height; y++) {
-		if (drawY + y < 0)
-			continue;
-		for (int x = 0; x < spriteW && (drawX + x) < width; x++) {
-			if (drawX + x < 0)
-				continue;
-			int srcIdx = (srcY + y) * shipFrame.width + (srcX + x);
-			byte pixel = shipFrame.pixels[srcIdx];
-			if (pixel != 0 && pixel != 231) {
-				int dstIdx = (drawY + y) * pitch + (drawX + x);
-				renderBitmap[dstIdx] = pixel;
-			}
-		}
-	}
+	blitEmbeddedFrameRegion(renderBitmap, pitch, width, height, shipFrame,
+		drawX, drawY, srcX, srcY, spriteW, spriteH);
 
 	debug("Rebel2: Ship (fallback) at (%d,%d) strip=(%d,%d) of (%dx%d) dir=(%d,%d)",
 		drawX, drawY, srcX, srcY, numHorizontal, numVertical, _shipDirectionH, _shipDirectionV);


Commit: 5568808962e2bf964c997d4dbc04214037986e2d
    https://github.com/scummvm/scummvm/commit/5568808962e2bf964c997d4dbc04214037986e2d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:54+02:00

Commit Message:
SCUMM: RA2: Harden embedded SAN loading

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 0023a910441..82eb8f85b63 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -89,6 +89,30 @@ static void blitEmbeddedFrameRegion(byte *renderBitmap, int pitch, int clipWidth
 	}
 }
 
+static bool readEmbeddedSanChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
+		uint32 &tag, uint32 &chunkSize, int64 &dataEnd, int64 &nextChunkPos) {
+	const int64 headerPos = stream.pos();
+	if (headerPos < 0 || headerPos + 8 > containerEnd)
+		return false;
+
+	tag = stream.readUint32BE();
+	chunkSize = stream.readUint32BE();
+
+	const int64 dataStart = stream.pos();
+	if ((int64)chunkSize > containerEnd - dataStart) {
+		debug("Rebel2: Truncated embedded SAN %s chunk 0x%08X at %lld: size=%u, remaining=%lld",
+			context, tag, headerPos, chunkSize, containerEnd - dataStart);
+		return false;
+	}
+
+	dataEnd = dataStart + chunkSize;
+	nextChunkPos = dataEnd + (chunkSize & 1);
+	if (nextChunkPos > containerEnd)
+		nextChunkPos = dataEnd;
+
+	return true;
+}
+
 // renderEmbeddedFrame -- Blit a decoded embedded frame to the video buffer.
 void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFrame &frame, int userId) {
 	// Render the decoded embedded frame to the video buffer
@@ -134,12 +158,13 @@ void InsaneRebel2::renderEmbeddedFrame(byte *renderBitmap, const EmbeddedSanFram
 //
 void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte *renderBitmap) {
 	// Validate userId - Level 3 uses slots 0-11, allow up to 15 for safety
-	if (userId < 0 || userId > 15 || !animData || size < 32) {
+	if (userId < 0 || userId > 15 || !animData || size < 8) {
 		debug("Rebel2: Invalid embedded SAN: userId=%d, size=%d", userId, size);
 		return;
 	}
 
 	Common::MemoryReadStream stream(animData, size);
+	const int64 streamEnd = stream.size();
 
 	// Read ANIM header
 	uint32 animTag = stream.readUint32BE();
@@ -148,42 +173,44 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 		return;
 	}
 	uint32 animSize = stream.readUint32BE();
-	debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%d", userId, animSize, size - 8);
+	int64 animEnd = streamEnd;
+	if ((int64)animSize <= streamEnd - 8) {
+		animEnd = 8 + (int64)animSize;
+	} else {
+		debug("Rebel2: Embedded ANIM truncated: reported size=%u, actual=%lld", animSize, streamEnd - 8);
+	}
+	debug("Rebel2: Parsing embedded ANIM: userId=%d, reported size=%u, actual=%lld", userId, animSize, streamEnd - 8);
 
 	// Iterate through chunks to find FRME -> FOBJ
-	while (!stream.eos() && stream.pos() < size) {
-		uint32 tag = stream.readUint32BE();
-		uint32 chunkSize = stream.readUint32BE();
-		int32 nextChunkPos = stream.pos() + chunkSize;
+	while (!stream.eos() && stream.pos() + 8 <= animEnd) {
+		uint32 tag;
+		uint32 chunkSize;
+		int64 chunkDataEnd;
+		int64 nextChunkPos;
+		if (!readEmbeddedSanChunkHeader(stream, animEnd, "top-level", tag, chunkSize, chunkDataEnd, nextChunkPos))
+			break;
 
 		if (tag == MKTAG('F','R','M','E')) {
 			// Iterate sub-chunks in FRME
-			while (stream.pos() < nextChunkPos && !stream.eos()) {
-				uint32 subTag = stream.readUint32BE();
-				uint32 subSize = stream.readUint32BE();
-				int32 nextSubPos = stream.pos() + subSize;
+			while (stream.pos() + 8 <= chunkDataEnd && !stream.eos()) {
+				uint32 subTag;
+				uint32 subSize;
+				int64 subDataEnd;
+				int64 nextSubPos;
+				if (!readEmbeddedSanChunkHeader(stream, chunkDataEnd, "FRME", subTag, subSize, subDataEnd, nextSubPos))
+					break;
 
 				if (subTag == MKTAG('F','O','B','J')) {
-					// Found FOBJ - Embedded HUD Frame
-					// Dump raw FOBJ bytes for analysis
-					int32 fobjStart = stream.pos();
-					byte rawHeader[20];
-					int headerBytesToRead = MIN((int)subSize, 20);
-					stream.read(rawHeader, headerBytesToRead);
-					stream.seek(fobjStart);  // Reset to read normally
-
-					debug("Rebel2: Raw FOBJ header (%d bytes): %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
-						headerBytesToRead,
-						rawHeader[0], rawHeader[1], rawHeader[2], rawHeader[3],
-						rawHeader[4], rawHeader[5], rawHeader[6], rawHeader[7],
-						rawHeader[8], rawHeader[9], rawHeader[10], rawHeader[11],
-						rawHeader[12], rawHeader[13], rawHeader[14], rawHeader[15],
-						rawHeader[16], rawHeader[17], rawHeader[18], rawHeader[19]);
+					if (subSize < 14) {
+						debug("Rebel2: Embedded FOBJ too small: userId=%d, size=%u", userId, subSize);
+						stream.seek(nextSubPos);
+						continue;
+					}
 
 					// Read FOBJ header
 					int codec = stream.readUint16LE();
-					int left = stream.readUint16LE();
-					int top = stream.readUint16LE();
+					int left = stream.readSint16LE();
+					int top = stream.readSint16LE();
 					int width = stream.readUint16LE();
 					int height = stream.readUint16LE();
 					stream.readUint16LE();  // unknown
@@ -203,11 +230,16 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 
 					// Allocate storage for the decoded frame
 					EmbeddedSanFrame &frame = _rebelEmbeddedHud[userId];
+					frame.valid = false;
 
 					if (width > 0 && height > 0 && width <= 800 && height <= 480) {
 						if (frame.width != width || frame.height != height || !frame.pixels) {
 							free(frame.pixels);
 							frame.pixels = (byte *)malloc(width * height);
+							if (!frame.pixels) {
+								warning("Rebel2: Failed to allocate embedded HUD frame: %dx%d", width, height);
+								return;
+							}
 							frame.width = width;
 							frame.height = height;
 						}
@@ -219,10 +251,19 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 						frame.renderY = top;
 
 						// Read the raw FOBJ data
-						int32 dataSize = subSize - 14;
+						int32 dataSize = (int32)(subDataEnd - stream.pos());
 						if (dataSize > 0) {
 							byte *fobjData = (byte *)malloc(dataSize);
-							stream.read(fobjData, dataSize);
+							if (!fobjData) {
+								warning("Rebel2: Failed to allocate embedded FOBJ data: %d bytes", dataSize);
+								return;
+							}
+							uint32 bytesRead = stream.read(fobjData, dataSize);
+							if (bytesRead != (uint32)dataSize) {
+								debug("Rebel2: Short embedded FOBJ read: got %u of %d bytes", bytesRead, dataSize);
+								free(fobjData);
+								return;
+							}
 
 							// Decode based on codec - use extracted helper functions (FUN_0042BD60, etc.)
 							if (codec == 1 || codec == 3) {
@@ -273,15 +314,12 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 				} else {
 					// Skip other sub-chunks (AHDR inside FRME?) or padding
 					stream.seek(nextSubPos);
-					if (subSize & 1)
-						stream.skip(1);
 				}
 			}
+			stream.seek(nextChunkPos);
 		} else {
 			// Skip non-FRME chunks (AHDR, etc at top level)
 			stream.seek(nextChunkPos);
-			if (chunkSize & 1)
-				stream.skip(1);
 		}
 	}
 


Commit: b1fe2f8fd0acec06be33544a0b04b42066fb0f1c
    https://github.com/scummvm/scummvm/commit/b1fe2f8fd0acec06be33544a0b04b42066fb0f1c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:55+02:00

Commit Message:
SCUMM: RA2: Refactor level 2 background loading

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


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 1f676bf0733..b9c81478028 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -33,6 +33,30 @@
 
 namespace Scumm {
 
+static bool readLevel2BackgroundChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
+		uint32 &tag, uint32 &chunkSize, int64 &dataEnd, int64 &nextChunkPos) {
+	const int64 headerPos = stream.pos();
+	if (headerPos < 0 || headerPos + 8 > containerEnd)
+		return false;
+
+	tag = stream.readUint32BE();
+	chunkSize = stream.readUint32BE();
+
+	const int64 dataStart = stream.pos();
+	if ((int64)chunkSize > containerEnd - dataStart) {
+		debug("Rebel2 loadLevel2Background: Truncated %s chunk 0x%08X at %lld: size=%u, remaining=%lld",
+			context, tag, headerPos, chunkSize, containerEnd - dataStart);
+		return false;
+	}
+
+	dataEnd = dataStart + chunkSize;
+	nextChunkPos = dataEnd + (chunkSize & 1);
+	if (nextChunkPos > containerEnd)
+		nextChunkPos = dataEnd;
+
+	return true;
+}
+
 //
 // procPreRendering -- Pre-frame setup: background restore and corridor overlays.
 //
@@ -2103,77 +2127,104 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
 		memset(_level2Background, 0, 320 * 200);
 	}
 
-	// Parse embedded ANIM to find FOBJ
-	// Structure: ANIM tag at offset 0, AHDR, then FRME with FOBJ
-	int animOffset = 0;
-	if (READ_BE_UINT32(animData) == MKTAG('A','N','I','M')) {
-		uint32 animSize = READ_BE_UINT32(animData + 4);
-		debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
+	Common::MemoryReadStream stream(animData, size);
+	const int64 streamEnd = stream.size();
 
-		// Skip ANIM header (8 bytes) + AHDR chunk
-		if (size >= 16 && READ_BE_UINT32(animData + 8) == MKTAG('A','H','D','R')) {
-			uint32 ahdrSize = READ_BE_UINT32(animData + 12);
-			animOffset = 8 + 8 + ahdrSize;  // After ANIM tag + AHDR
-			debug("Rebel2 loadLevel2Background: AHDR size=%u, FRME expected at offset %d", ahdrSize, animOffset);
-		}
+	uint32 animTag = stream.readUint32BE();
+	if (animTag != MKTAG('A','N','I','M')) {
+		debug("Rebel2 loadLevel2Background: Missing ANIM tag, got 0x%08X", animTag);
+		return false;
+	}
+
+	uint32 animSize = stream.readUint32BE();
+	int64 animEnd = streamEnd;
+	if ((int64)animSize <= streamEnd - 8) {
+		animEnd = 8 + (int64)animSize;
+	} else {
+		debug("Rebel2 loadLevel2Background: ANIM truncated: reported size=%u, actual=%lld",
+			animSize, streamEnd - 8);
 	}
+	debug("Rebel2 loadLevel2Background: Found ANIM tag, size=%u", animSize);
 
-	// Look for FRME containing FOBJ
 	bool foundBackground = false;
-	for (int scanPos = animOffset; scanPos + 16 < size && !foundBackground; scanPos++) {
-		if (READ_BE_UINT32(animData + scanPos) == MKTAG('F','R','M','E')) {
-			int frmeSize = READ_BE_UINT32(animData + scanPos + 4);
-			debug("Rebel2 loadLevel2Background: Found FRME at %d, size=%d", scanPos, frmeSize);
-
-			for (int fobjPos = scanPos + 8; fobjPos + 18 < scanPos + 8 + frmeSize && fobjPos + 18 < size; fobjPos++) {
-				if (READ_BE_UINT32(animData + fobjPos) == MKTAG('F','O','B','J')) {
-					byte *fobjData = animData + fobjPos + 8;
-
-					// FOBJ header: codec(2), x(2), y(2), w(2), h(2)
-					int16 codec = READ_LE_INT16(fobjData);
-					int16 fobjX = READ_LE_INT16(fobjData + 2);
-					int16 fobjY = READ_LE_INT16(fobjData + 4);
-					int16 fobjW = READ_LE_INT16(fobjData + 6);
-					int16 fobjH = READ_LE_INT16(fobjData + 8);
-
-					debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
-						codec, fobjX, fobjY, fobjW, fobjH);
-
-					// Decode codec 3 (RLE) into background buffer
-					// Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
-					// The standard smushDecodeRLE treats color 0 as transparent, which causes
-					// the background to appear as a "sketch" with black pixels missing.
-					if (codec == 3 && fobjW > 0 && fobjH > 0 && fobjW <= 320 && fobjH <= 200) {
-						byte *rleData = fobjData + 14;  // Skip full 14-byte FOBJ header
-						smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
-
-						debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
-							fobjW, fobjH, fobjX, fobjY);
-						_level2BackgroundLoaded = true;
-						foundBackground = true;
-
-						// Copy to render bitmap immediately if provided.
-						// Only copy when render buffer pitch is 320 (standard screen size).
-						// For oversized buffers (e.g., Level 12's 640x260 corridor),
-						// the FOBJ/FETCH system handles background rendering and copying
-						// 320-wide data into a wider buffer would corrupt the corridor.
-						if (renderBitmap) {
-							int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
-							if (bufferPitch == 320) {
-								for (int by = 0; by < 200; by++) {
-									memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
-								}
-								debug("Rebel2 loadLevel2Background: Copied to renderBitmap (pitch=%d)", bufferPitch);
-							} else {
-								debug("Rebel2 loadLevel2Background: Skipping renderBitmap copy (pitch=%d != 320)", bufferPitch);
-							}
+	while (!stream.eos() && stream.pos() + 8 <= animEnd && !foundBackground) {
+		uint32 tag;
+		uint32 chunkSize;
+		int64 chunkDataEnd;
+		int64 nextChunkPos;
+		if (!readLevel2BackgroundChunkHeader(stream, animEnd, "ANIM", tag, chunkSize, chunkDataEnd, nextChunkPos))
+			break;
+
+		if (tag != MKTAG('F','R','M','E')) {
+			stream.seek(nextChunkPos);
+			continue;
+		}
+
+		debug("Rebel2 loadLevel2Background: Found FRME at %lld, size=%u", stream.pos() - 8, chunkSize);
+
+		while (stream.pos() + 8 <= chunkDataEnd && !stream.eos() && !foundBackground) {
+			uint32 subTag;
+			uint32 subSize;
+			int64 subDataEnd;
+			int64 nextSubPos;
+			if (!readLevel2BackgroundChunkHeader(stream, chunkDataEnd, "FRME", subTag, subSize, subDataEnd, nextSubPos))
+				break;
+
+			if (subTag != MKTAG('F','O','B','J')) {
+				stream.seek(nextSubPos);
+				continue;
+			}
+
+			if (subSize < 14) {
+				debug("Rebel2 loadLevel2Background: FOBJ too small: size=%u", subSize);
+				stream.seek(nextSubPos);
+				continue;
+			}
+
+			// FOBJ header: codec(2), x(2), y(2), w(2), h(2)
+			int codec = stream.readUint16LE();
+			int fobjX = stream.readSint16LE();
+			int fobjY = stream.readSint16LE();
+			int fobjW = stream.readSint16LE();
+			int fobjH = stream.readSint16LE();
+			stream.readUint16LE();  // unknown
+			stream.readUint16LE();  // unknown
+
+			debug("Rebel2 loadLevel2Background: Found FOBJ: codec=%d pos=(%d,%d) size=%dx%d",
+				codec, fobjX, fobjY, fobjW, fobjH);
+
+			// Decode codec 3 (RLE) into background buffer.
+			// Use smushDecodeRLEOpaque to write ALL colors including color 0 (black).
+			if (codec == 3 && fobjX >= 0 && fobjY >= 0 && fobjW > 0 && fobjH > 0 &&
+					fobjX + fobjW <= 320 && fobjY + fobjH <= 200 && stream.pos() < subDataEnd) {
+				const byte *rleData = animData + stream.pos();
+				smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+
+				debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
+					fobjW, fobjH, fobjX, fobjY);
+				_level2BackgroundLoaded = true;
+				foundBackground = true;
+
+				// Copy to render bitmap immediately if provided.
+				// Only copy when render buffer pitch is 320 (standard screen size).
+				// For oversized buffers, the FOBJ/FETCH system handles background rendering.
+				if (renderBitmap) {
+					int bufferPitch = (_player && _player->_width > 0) ? _player->_width : 320;
+					if (bufferPitch == 320) {
+						for (int by = 0; by < 200; by++) {
+							memcpy(renderBitmap + by * 320, _level2Background + by * 320, 320);
 						}
+						debug("Rebel2 loadLevel2Background: Copied to renderBitmap (pitch=%d)", bufferPitch);
+					} else {
+						debug("Rebel2 loadLevel2Background: Skipping renderBitmap copy (pitch=%d != 320)", bufferPitch);
 					}
-					break;
 				}
 			}
-			break;
+
+			stream.seek(nextSubPos);
 		}
+
+		stream.seek(nextChunkPos);
 	}
 
 	if (!foundBackground) {


Commit: 3881b4371b00bf85cda374ceedeb7296abe195b3
    https://github.com/scummvm/scummvm/commit/3881b4371b00bf85cda374ceedeb7296abe195b3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:55+02:00

Commit Message:
SCUMM: RA: Pass data size to line update decoder

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/smush/rebel/codec_ra2.cpp
    engines/scumm/smush/rebel/codec_ra2.h
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra1.h
    engines/scumm/smush/rebel/smush_player_ra2.cpp
    engines/scumm/smush/rebel/smush_player_ra2.h


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index b9c81478028..e0903fc0d27 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -2198,7 +2198,8 @@ bool InsaneRebel2::loadLevel2Background(byte *animData, int32 size, byte *render
 			if (codec == 3 && fobjX >= 0 && fobjY >= 0 && fobjW > 0 && fobjH > 0 &&
 					fobjX + fobjW <= 320 && fobjY + fobjH <= 200 && stream.pos() < subDataEnd) {
 				const byte *rleData = animData + stream.pos();
-				smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320);
+				smushDecodeRLEOpaque(_level2Background, rleData, fobjX, fobjY, fobjW, fobjH, 320,
+					(int)(subDataEnd - stream.pos()));
 
 				debug("Rebel2 loadLevel2Background: Decoded Level 2 background (%dx%d at %d,%d)",
 					fobjW, fobjH, fobjX, fobjY);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 82eb8f85b63..5a2b14241a9 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -278,7 +278,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								debug("Rebel2: Decoded embedded HUD (codec 20/raw): %dx%d", width, height);
 							} else if (codec == 21 || codec == 44) {
 								// Codec 21/44: Line update (FUN_0042BD60)
-								smushDecodeLineUpdate(frame.pixels, fobjData, 0, 0, width, height, width);
+								smushDecodeLineUpdate(frame.pixels, fobjData, 0, 0, width, height, width, dataSize);
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec %d/line update): %dx%d", codec, width, height);
 							} else if (codec == 45) {
diff --git a/engines/scumm/smush/rebel/codec_ra2.cpp b/engines/scumm/smush/rebel/codec_ra2.cpp
index 4251a4342e1..7ca7dd2e0d1 100644
--- a/engines/scumm/smush/rebel/codec_ra2.cpp
+++ b/engines/scumm/smush/rebel/codec_ra2.cpp
@@ -26,10 +26,57 @@
 #include "common/endian.h"
 #include "common/textconsole.h"
 
-#include "scumm/bomp.h"
-
 namespace Scumm {
 
+static void bompDecodeLineOpaqueBounded(byte *dst, const byte *src, const byte *srcEnd, int len) {
+	while (len > 0 && src < srcEnd) {
+		byte code = *src++;
+		int num = (code >> 1) + 1;
+		if (num > len)
+			num = len;
+
+		if (code & 1) {
+			if (src >= srcEnd)
+				break;
+			memset(dst, *src++, num);
+			dst += num;
+			len -= num;
+		} else {
+			int toCopy = num;
+			if (toCopy > (int)(srcEnd - src))
+				toCopy = (int)(srcEnd - src);
+			memcpy(dst, src, toCopy);
+			src += toCopy;
+			dst += toCopy;
+			len -= toCopy;
+			if (toCopy < num)
+				break;
+		}
+	}
+}
+
+const byte *smushSkipRLELines(const byte *src, int &dataSize, int lines) {
+	for (int i = 0; i < lines; i++) {
+		if (dataSize < 2) {
+			src += dataSize;
+			dataSize = 0;
+			break;
+		}
+
+		int rowSize = READ_LE_UINT16(src) + 2;
+		if (rowSize > dataSize) {
+			src += dataSize;
+			dataSize = 0;
+			break;
+		}
+
+		src += rowSize;
+		dataSize -= rowSize;
+	}
+
+	return src;
+}
+
 /**
  * Codec 3 RLE decoder that writes ALL colors including color 0 (black).
  * Use this for background images where color 0 should NOT be treated as transparent.
@@ -38,14 +85,24 @@ namespace Scumm {
  *
  * Used by: Rebel Assault 2 Level 2 background loading (IACT opcode 8, par4=5)
  */
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	if (dataSize <= 0)
+		return;
+
+	const byte *srcEnd = src + dataSize;
 	dst += top * pitch;
-	do {
+	while (height-- && srcEnd - src >= 2) {
+		int lineSize = READ_LE_UINT16(src);
+		src += 2;
+		if (lineSize > srcEnd - src)
+			lineSize = (int)(srcEnd - src);
+		const byte *lineEnd = src + lineSize;
+
 		dst += left;
-		bompDecodeLine(dst, src + 2, width, true);  // setZero = TRUE to write all colors
-		src += READ_LE_UINT16(src) + 2;
+		bompDecodeLineOpaqueBounded(dst, src, lineEnd, width);
+		src = lineEnd;
 		dst += pitch - left;
-	} while (--height);
+	}
 }
 
 /**
@@ -55,39 +112,53 @@ void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int wid
  * The count value needs +1 to get the actual number of pixels to copy.
  * Note: Skip regions preserve previous frame content (delta compression).
  */
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	if (dataSize <= 0)
+		return;
+
+	const byte *srcEnd = src + dataSize;
 	dst += top * pitch + left;
 
-	while (height--) {
+	while (height-- && srcEnd - src >= 2) {
 		byte *dstPtrNext = dst + pitch;
-		const byte *srcPtrNext = src + 2 + READ_LE_UINT16(src);
-		src += 2;  // Skip line size header
+		int lineDataSize = READ_LE_UINT16(src);
+		src += 2;
+		if (lineDataSize > srcEnd - src)
+			lineDataSize = (int)(srcEnd - src);
+		const byte *lineEnd = src + lineDataSize;
 		int len = width;
 		byte *lineDst = dst;
 
-		while (len > 0) {
+		while (len > 0 && lineEnd - src >= 2) {
 			// Read 2-byte LE skip value
 			int skip = READ_LE_UINT16(src);
 			src += 2;
+			if (skip >= len)
+				break;
 			lineDst += skip;
 			len -= skip;
-			if (len <= 0)
-				break;
 
 			// Read 2-byte LE copy count (+1 for actual count)
+			if (lineEnd - src < 2)
+				break;
 			int count = READ_LE_UINT16(src) + 1;
 			src += 2;
 			if (count > len)
 				count = len;
-			len -= count;
 
 			// Copy literal pixels
-			memcpy(lineDst, src, count);
-			lineDst += count;
-			src += count;
+			int toCopy = count;
+			if (toCopy > (int)(lineEnd - src))
+				toCopy = (int)(lineEnd - src);
+			memcpy(lineDst, src, toCopy);
+			lineDst += toCopy;
+			src += toCopy;
+			len -= toCopy;
+			if (toCopy < count)
+				break;
 		}
 		dst = dstPtrNext;
-		src = srcPtrNext;
+		src = lineEnd;
 	}
 }
 
diff --git a/engines/scumm/smush/rebel/codec_ra2.h b/engines/scumm/smush/rebel/codec_ra2.h
index f9b0191623b..1aa8d2e86d0 100644
--- a/engines/scumm/smush/rebel/codec_ra2.h
+++ b/engines/scumm/smush/rebel/codec_ra2.h
@@ -26,10 +26,11 @@
 
 namespace Scumm {
 
-void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
+void smushDecodeRLEOpaque(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeSkipRLE(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
 void smushDecodeRA2Bomp(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+const byte *smushSkipRLELines(const byte *src, int &dataSize, int lines);
 
 } // End of namespace Scumm
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 9c7dcfd685c..3a2df42c5e4 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -75,6 +75,7 @@ void SmushPlayerRebel1::initGamePlayerFields() {
 	_ra1ObjOverlayHeight = 0;
 	_ra1ViewportOffsetX = 0;
 	_ra1ViewportOffsetY = 0;
+	_ra1FrameSourceSkipY = 0;
 }
 
 void SmushPlayerRebel1::destroyGamePlayerFields() {
@@ -508,7 +509,16 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
 	// positions — they must NOT be clipped/adjusted.
 	if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
 		return false;
-	adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+	int sourceSkipY = 0;
+	_ra1FrameSourceSkipY = 0;
+	adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
+	if (codec == SMUSH_CODEC_RLE_ALT) {
+		_ra1FrameSourceSkipY = sourceSkipY;
+		if (srcSkipY)
+			*srcSkipY = 0;
+	} else if (srcSkipY) {
+		*srcSkipY = sourceSkipY;
+	}
 	return true;
 }
 
@@ -518,7 +528,8 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
 		return true;
 	case SMUSH_CODEC_RLE_ALT:
-		smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch);
+		src = smushSkipRLELines(src, dataSize, _ra1FrameSourceSkipY);
+		smushDecodeRLEOpaque(_dst, src, left, top, width, height, pitch, dataSize);
 		return true;
 	case SMUSH_CODEC_RA1_SCATTER:
 		smushDecodeRA1Scatter(_dst, src, left, top, _width, _height, pitch, dataSize);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index 407fbb309e2..e53fbea39a1 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -80,6 +80,7 @@ private:
 	// RA1 viewport scroll offset for interactive gameplay
 	int _ra1ViewportOffsetX;
 	int _ra1ViewportOffsetY;
+	int _ra1FrameSourceSkipY;
 };
 
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 7dd4a90e0c1..311b34df0fe 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -85,6 +85,7 @@ void SmushPlayerRebel2::initGamePlayerFields() {
 	_loadReadOffset = 8;  // Original starts reading at offset 8 (skips header)
 	_lastLoadChunkIdx = -1;
 	_totalLoadChunks = 0;
+	_ra2FrameSourceSkipY = 0;
 	_scrollX = 0;
 	_scrollY = 0;
 }
@@ -593,15 +594,21 @@ bool SmushPlayerRebel2::ra2DecodeCodec(int codec, const uint8 *src, int left, in
 								 int width, int height, int pitch, int dataSize) {
 	switch (codec) {
 	case SMUSH_CODEC_LINE_UPDATE:
-	case SMUSH_CODEC_LINE_UPDATE2:
-		smushDecodeLineUpdate(_dst, src, left, top, width, height, pitch);
+	case SMUSH_CODEC_LINE_UPDATE2: {
+		const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+		smushDecodeLineUpdate(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
 		return true;
-	case SMUSH_CODEC_SKIP_RLE:
-		smushDecodeSkipRLE(_dst, src, left, top, width, height, pitch, dataSize);
+	}
+	case SMUSH_CODEC_SKIP_RLE: {
+		const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+		smushDecodeSkipRLE(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
 		return true;
-	case SMUSH_CODEC_RA2_BOMP:
-		smushDecodeRA2Bomp(_dst, src, left, top, width, height, pitch, dataSize);
+	}
+	case SMUSH_CODEC_RA2_BOMP: {
+		const uint8 *adjustedSrc = smushSkipRLELines(src, dataSize, _ra2FrameSourceSkipY);
+		smushDecodeRA2Bomp(_dst, adjustedSrc, left, top, width, height, pitch, dataSize);
 		return true;
+	}
 	default:
 		return false;
 	}
@@ -731,7 +738,17 @@ bool SmushPlayerRebel2::handleGameDimensionOverride(int codec, int width, int he
 }
 
 bool SmushPlayerRebel2::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
-	adjustFrameCoords(left, top, width, height, pitch, srcSkipY);
+	int sourceSkipY = 0;
+	_ra2FrameSourceSkipY = 0;
+	adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
+	if (codec == SMUSH_CODEC_LINE_UPDATE || codec == SMUSH_CODEC_LINE_UPDATE2 ||
+			codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA2_BOMP) {
+		_ra2FrameSourceSkipY = sourceSkipY;
+		if (srcSkipY)
+			*srcSkipY = 0;
+	} else if (srcSkipY) {
+		*srcSkipY = sourceSkipY;
+	}
 	return true;
 }
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.h b/engines/scumm/smush/rebel/smush_player_ra2.h
index 375d2a6a015..7a1004df06c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.h
+++ b/engines/scumm/smush/rebel/smush_player_ra2.h
@@ -79,6 +79,7 @@ private:
 	int32 _loadReadOffset;
 	int16 _lastLoadChunkIdx;
 	int16 _totalLoadChunks;
+	int _ra2FrameSourceSkipY;
 };
 
 } // End of namespace Scumm


Commit: 0bf0542235af92ee644556fcae0f99e507760347
    https://github.com/scummvm/scummvm/commit/0bf0542235af92ee644556fcae0f99e507760347
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:55+02:00

Commit Message:
SCUMM: RA2: Remove unused code and old TODOs

Changed paths:
    engines/scumm/insane/rebel2/iact.cpp
    engines/scumm/insane/rebel2/levels.cpp
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/insane/rebel2/render.cpp
    engines/scumm/insane/rebel2/runlevels.cpp
    engines/scumm/smush/rebel/smush_player_ra2.cpp


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index e0903fc0d27..5d1423ca472 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1176,7 +1176,6 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		if (_grdSpriteMode == 3) {
 			if (_rebelDamageLevel == 5) {
 				// At max damage, check for direction change input
-				// For now, use mouse X position to determine direction
 				int16 mouseX = getGameplayAimPoint().x;
 				if (_player && _player->_width > 320) {
 					mouseX = (mouseX * 320) / _player->_width;
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 97eb5bf40d8..8d6e118e5bf 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -252,195 +252,6 @@ void InsaneRebel2::playLevelBegin(int levelId) {
 	}
 }
 
-//
-// playLevelGameplay -- Main gameplay video(s) for a level
-//
-// Returns true if level completed (shield > 0), false if died.
-// Structures vary by level: single SAN, multi-part subdirs, or two phases.
-//
-bool InsaneRebel2::playLevelGameplay(int levelId) {
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-	Common::String filename;
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
-	// Set gameplay flags (interactive with HUD)
-	splayer->setCurVideoFlags(0x28);
-
-	// Reset damage/shield for this level
-	_playerShield = 255;
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // Will be set by IACT opcode 6 if par4==1
-
-	debug("Rebel2: Starting gameplay for level %d", levelId);
-
-	switch (levelId) {
-	case 1:
-		// Level 1: Single gameplay file (01P01.SAN)
-		// Level 1 uses Handler 0x26 (turret mode) - set before gameplay
-		_rebelHandler = 0x26;
-		filename = Common::String::format("%s/%sP01.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 2:
-		// Level 2: Has cutscene first, then multiple parts
-		// Level 2 uses Handler 8 (third-person on foot mode) - set before gameplay
-		_rebelHandler = 8;
-		// First play the cutscene
-		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing cutscene %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Part 1 (multiple variations - play A for now)
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/P1/%sP01_A.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Post segment 1
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPST1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Part 2
-		_rebelHandler = 8;
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/P2/%sP02_A.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Post segment 2
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPST2.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Part 3
-		_rebelHandler = 8;
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/P3/%sP03_A.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 3:
-		// Level 3: Two gameplay phases (third-person ship)
-		// Level 3 uses Handler 7 (third-person ship mode) - FUN_0040d836/FUN_0040c3cc
-		_rebelHandler = 7;
-		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Post segment
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Phase 2 — handler will be re-set by IACT opcode 6
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 4:
-		_rebelHandler = 0x26;
-		// Level 4: Has cutscene, then single gameplay
-		filename = Common::String::format("%s/%sCUT.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing cutscene %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit())
-			return false;
-
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 5:
-		// Level 5: Single gameplay file
-		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	case 6:
-		// Level 6: Two gameplay phases
-		filename = Common::String::format("%s/%sPLAY1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Post segment
-		_rebelHandler = 0;
-		_rebelStatusBarSprite = 0;
-		filename = Common::String::format("%s/%sPOST1.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->setCurVideoFlags(0x28);
-		splayer->play(filename.c_str(), 12);
-
-		if (_vm->shouldQuit() || _playerShield == 0)
-			return false;
-
-		// Phase 2 — handler will be re-set by IACT opcode 6
-		splayer->setCurVideoFlags(0x28);
-		filename = Common::String::format("%s/%sPLAY2.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Playing %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-
-	default:
-		// For levels 7-15 (not in demo), try common patterns
-		// First try XXPLAY.SAN
-		filename = Common::String::format("%s/%sPLAY.SAN", dir.c_str(), prefix.c_str());
-		debug("Rebel2: Trying %s", filename.c_str());
-		splayer->play(filename.c_str(), 12);
-		break;
-	}
-
-	// Return true if player survived (shield > 0), false if died
-	return (_playerShield > 0);
-}
-
 // playLevelEnd -- Level completion video (FUN_00417327).
 void InsaneRebel2::playLevelEnd(int levelId) {
 
@@ -459,31 +270,6 @@ void InsaneRebel2::playLevelEnd(int levelId) {
 	splayer->play(filename.c_str(), 12);
 }
 
-// playLevelDeath -- Death video (LEVXX/XXDIE_X.SAN, FUN_00417168).
-void InsaneRebel2::playLevelDeath(int levelId) {
-
-	_rebelHandler = 0;
-	_rebelStatusBarSprite = 0;  // No status bar during death cinematic
-
-	Common::String dir = getLevelDir(levelId);
-	Common::String prefix = getLevelPrefix(levelId);
-
-	// Most levels have DIE_A, some have just DIE
-	Common::String filename;
-	if (levelId == 2 || levelId == 4 || levelId == 10 || levelId == 12 || levelId == 14) {
-		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
-	} else {
-		filename = Common::String::format("%s/%sDIE_A.SAN", dir.c_str(), prefix.c_str());
-	}
-
-	debug("Rebel2: Playing level %d death: %s", levelId, filename.c_str());
-
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play(filename.c_str(), 12);
-}
-
 // playLevelRetry -- Retry prompt video (LEVXX/XXRETRY.SAN, FUN_00417168).
 void InsaneRebel2::playLevelRetry(int levelId) {
 
@@ -819,8 +605,8 @@ Common::String InsaneRebel2::selectDeathVideoVariant(int levelId, int phase, int
 
 	case 9:
 		// Level 9 (FUN_00419B86): Based on DAT_0047ab94 (death cause)
-		// 0→A, 1→C, else→B. Use phase as proxy.
-		return "A";  // Default; exact tracking of DAT_0047ab94 deferred
+		// 0→A, 1→C, else→B. DAT_0047ab94 is not tracked yet.
+		return "A";
 
 	case 10:
 		// Level 10 (FUN_00419E0A): Single death video (no variant suffix)
@@ -888,7 +674,7 @@ void InsaneRebel2::playLevelDeathVariant(int levelId, int phase, int frame) {
 	Common::String filename;
 
 	if (variant.empty()) {
-		// No variant suffix (Level 2, 4)
+		// No variant suffix.
 		filename = Common::String::format("%s/%sDIE.SAN", dir.c_str(), prefix.c_str());
 	} else {
 		filename = Common::String::format("%s/%sDIE_%s.SAN", dir.c_str(), prefix.c_str(), variant.c_str());
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index d95d3ba7cf5..b03b822305a 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -82,7 +82,7 @@ Common::String InsaneRebel2::getRandomMenuVideo() {
 //
 // processMenuInput -- Menu input handling (FUN_0041f5ae)
 //
-// Returns -1 (no action) or 0-4 (menu item selected).
+// Returns -1 (no action) or a 0-based selected menu item.
 // Events captured by notifyEvent() before ScummEngine consumes them.
 // Keyboard: Up=0x148, Down=0x150, Enter=0x0d, ESC=0x1b.
 // Mouse mode (DAT_0047a806 == 1): Y position maps to selection.
@@ -93,10 +93,9 @@ int InsaneRebel2::processMenuInput() {
 
 	// Menu item Y positions (low-res 320x200 mode):
 	// From FUN_0041f5ae: baseY = numItems * -5 + 0x68
-	// With 8 total items (title + 7 options): 8 * -5 + 104 = 64
-	// Items at Y = 64, 74, 84, 94, 104, 114, 124 with spacing of 10
-	const int numItemsTotal = 8;  // Title + 7 selectable items (matching assembly)
-	const int baseY = numItemsTotal * -5 + 0x68;  // = 64
+	// With 7 selectable items: 7 * -5 + 104 = 69
+	// Items at Y = 69, 79, 89, 99, 109, 119, 129 with spacing of 10
+	const int baseY = _menuItemCount * -5 + 0x68;
 	const int itemSpacing = 10;
 
 	// Process events from the queue (populated by notifyEvent)
@@ -138,7 +137,7 @@ int InsaneRebel2::processMenuInput() {
 				break;
 
 			case Common::KEYCODE_ESCAPE:
-				// ESC - Quit (index 4 = last item) - emulates key code 0x1b
+				// ESC - Quit (last item) - emulates key code 0x1b
 				result = _menuItemCount - 1;  // Select quit option
 				debug("Menu: ESC pressed - selecting quit (item %d)", result);
 				break;
@@ -150,32 +149,30 @@ int InsaneRebel2::processMenuInput() {
 
 		case Common::EVENT_LBUTTONDOWN:
 			_menuInactivityTimer = 0;
-			// TODO: Re-enable click-to-confirm (currently disabled for easier testing)
-			// Original behavior: clicking on a menu item both highlights and confirms it.
+			_vm->_mouse.x = event.mouse.x;
+			_vm->_mouse.y = event.mouse.y;
+			for (int i = 0; i < _menuItemCount; i++) {
+				int itemY = baseY + i * itemSpacing;
+				if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+					_menuSelection = i;
+					result = _menuSelection;
+					debug("Menu: Item %d selected (mouse)", _menuSelection);
+					break;
+				}
+			}
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
-			// Update hover selection based on Y position
-			// This emulates FUN_0041f5ae mouse mode behavior (DAT_0047a806 == 1)
 			{
 				int mouseY = event.mouse.y;
-				// Calculate selection from mouse Y position
-				// From assembly: DAT_00459988 = ((mouseY + 100) - (param_3 * -5 + 0x67)) / 10
-				int newSelection = (mouseY + 100 - (numItemsTotal * -5 + 0x67)) / 10;
-
-				// Clamp to valid range
-				if (newSelection < 0)
-					newSelection = 0;
-				if (newSelection >= _menuItemCount)
-					newSelection = _menuItemCount - 1;
-
-				// Only update if within menu area (not too far above/below)
-				int topY = baseY - 5;
-				int bottomY = baseY + (_menuItemCount - 1) * itemSpacing + 10;
-				if (mouseY >= topY && mouseY <= bottomY) {
-					if (newSelection != _menuSelection) {
-						_menuSelection = newSelection;
-						debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
+				for (int i = 0; i < _menuItemCount; i++) {
+					int itemY = baseY + i * itemSpacing;
+					if (mouseY >= itemY - 4 && mouseY < itemY + 6) {
+						if (i != _menuSelection) {
+							_menuSelection = i;
+							debug(5, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
+						}
+						break;
 					}
 				}
 			}
@@ -890,8 +887,21 @@ int InsaneRebel2::processChapterSelectInput() {
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
-			// TODO: Re-enable click-to-confirm (currently disabled for easier testing)
-			// Original behavior: any click confirms current selection (DAT_0047a7e4 & 1)
+			_vm->_mouse.x = event.mouse.x;
+			_vm->_mouse.y = event.mouse.y;
+			{
+				int baseY = _chapterItemCount * -5 + 0x68;
+				for (int i = 0; i < _chapterItemCount; i++) {
+					int itemY = baseY + i * 10;
+					if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+						_chapterSelection = i;
+						_previewOffsetY = _chapterSelection * -50 + 75;
+						result = _chapterSelection;
+						debug("ChapterSelect: Item %d selected (mouse)", _chapterSelection);
+						break;
+					}
+				}
+			}
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
@@ -1474,6 +1484,8 @@ int InsaneRebel2::processLevelSelectInput() {
 	bool isDifficultyMode = (_gameState == kStateDifficultySelect);
 	int &selection = isDifficultyMode ? _difficultySelection : _levelSelection;
 	int itemCount = isDifficultyMode ? 6 : _levelItemCount;
+	const int itemBaseY = itemCount * -5 + 0x68;
+	const int itemSpacing = 10;
 
 	while (!_menuEventQueue.empty()) {
 		Common::Event event = _menuEventQueue.pop();
@@ -1515,12 +1527,28 @@ int InsaneRebel2::processLevelSelectInput() {
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
-			// TODO: Re-enable click-to-confirm (currently disabled for easier testing)
+			_vm->_mouse.x = event.mouse.x;
+			_vm->_mouse.y = event.mouse.y;
+			for (int i = 0; i < itemCount; i++) {
+				int itemY = itemBaseY + i * itemSpacing;
+				if (event.mouse.y >= itemY - 1 && event.mouse.y < itemY + 9) {
+					selection = i;
+					result = selection;
+					break;
+				}
+			}
 			break;
 
 		case Common::EVENT_MOUSEMOVE:
 			_vm->_mouse.x = event.mouse.x;
 			_vm->_mouse.y = event.mouse.y;
+			for (int i = 0; i < itemCount; i++) {
+				int itemY = itemBaseY + i * itemSpacing;
+				if (event.mouse.y >= itemY - 4 && event.mouse.y < itemY + 6) {
+					selection = i;
+					break;
+				}
+			}
 			break;
 
 		case Common::EVENT_QUIT:
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index c9fb01a370c..3c576b408f6 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -73,7 +73,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 
 	// Rebel Assault 2: Load cockpit sprites NUT which contains crosshairs, explosions, status bar
 	// CPITIMAG.NUT = low-res (320x200), CPITIMHI.NUT = high-res (640x480)
-	// For now, use CPITIMAG since the game runs at 320x200
+	// The current renderer runs at 320x200, so use the low-res assets.
 	_smush_iconsNut = new NutRenderer(_vm, "SYSTM/CPITIMAG.NUT");
 	_smush_icons2Nut = nullptr;  // Not used for Rebel2
 
@@ -169,7 +169,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	}
 	_rebelLastCounter = 0;
 
-	_difficulty = 1; // Default to Medium (1). TODO: Read from game config
+	_difficulty = 1; // Default to Medium.
 	_targetLockTimer = 0;  // DAT_00443676 equivalent
 
 	_speed = 12;
@@ -401,8 +401,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Main menu has 7 selectable items (0-6) matching GAME.TRS indices 11-17:
 	//   0: Start Game, 1: Options, 2: Calibrate Joystick, 3: Continue Intro,
 	//   4: Show Top Pilots, 5: Show Credits, 6: Return to Launcher
-	// Note: The coordinate formula uses numItemsTotal = 8 (includes title) for Y position calculation
-	// Formula from FUN_0041f5ae: (DAT_0047a806 == 0) + 6 = 7 items for keyboard mode
+	// FUN_0041f5ae uses the selectable item count for Y position calculation.
 	_menuItemCount = 7;
 	_menuInactivityTimer = 0;
 	_lastMenuVariant = -1;        // No previous menu video
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 650613168e1..a16874082ce 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -359,16 +359,9 @@ public:
 	// Play level beginning cinematic (LEVXX/XXBEG.SAN)
 	void playLevelBegin(int levelId);
 
-	// Play main gameplay video(s) for a level
-	// Returns true if level completed, false if player died
-	bool playLevelGameplay(int levelId);
-
 	// Play level completion video (LEVXX/XXEND.SAN)
 	void playLevelEnd(int levelId);
 
-	// Play death video (LEVXX/XXDIE_X.SAN) - variant based on frame/location
-	void playLevelDeath(int levelId);
-
 	// Play retry prompt video (LEVXX/XXRETRY.SAN)
 	void playLevelRetry(int levelId);
 
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 5a2b14241a9..950256d543f 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -291,7 +291,7 @@ void InsaneRebel2::loadEmbeddedSan(int userId, byte *animData, int32 size, byte
 								frame.valid = true;
 								debug("Rebel2: Decoded embedded HUD (codec 23/skip-RLE): %dx%d", width, height);
 							} else {
-								debug("Rebel2: TODO: Decode codec %d for embedded HUD", codec);
+								debug("Rebel2: Unsupported embedded HUD codec %d", codec);
 								frame.valid = false;
 							}
 
@@ -519,7 +519,7 @@ void InsaneRebel2::spawnSpaceShot(int x, int y) {
 			// Calculate gun positions from direction-based lookup tables
 			// In the original, these come from tables indexed by _shipDirectionIndex
 			// DAT_004437c2/DAT_00443808 for left gun, DAT_0044384e/DAT_00443894 for right gun
-			// For now, use simplified positions relative to ship
+			// Use simplified positions relative to the ship.
 			int shipScreenX = 160 + ((_shipPosX - 160) >> 3);
 			int shipScreenY = 105 + ((_shipPosY - 40) >> 2);
 
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 44bab3adfd0..bb32a844149 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -125,8 +125,8 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 	// The SmushPlayer::play() call already blocks until video ends, so this step
 	// is handled implicitly. The early-exit logic (threshold > 0: if frame > 50
 	// AND all required enemy type bits are set, count up and break when > threshold)
-	// would need per-frame callbacks to work precisely. For now, the primary effect
-	// is covered by the video playing to completion and accumulating state.
+	// requires per-frame callbacks to work precisely. The current implementation
+	// covers the primary effect by playing the full video and accumulating state.
 	// TODO: Implement per-frame early exit callback for threshold-based wave termination.
 
 	// Step 2: Copy wave state to phase state (line 33)
@@ -230,7 +230,6 @@ int InsaneRebel2::runLevel2() {
 
 	// Play level beginning cinematic (02BEG.SAN)
 	// Original: FUN_004171c5("LEV02/02BEG.SAN", 0x20, 0xab, 0xa0, 10, 2, 0x46)
-	// Includes text overlay from GAME.TRS — deferred until text rendering is ready.
 	playLevelBegin(2);
 	if (_vm->shouldQuit())
 		return kLevelQuit;
@@ -498,7 +497,7 @@ int InsaneRebel2::runLevel2() {
 
 		// Level completed! Calculate accuracy score.
 		// Original: FUN_00417327 with score thresholds and medal ranks
-		// Score presentation deferred until GAME.TRS text rendering is implemented.
+		// Score presentation remains to be implemented.
 		{
 			totalMisses += _rebelHitCounter;
 			int accuracy = 0;
@@ -522,9 +521,8 @@ int InsaneRebel2::runLevel2() {
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
-		// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award)
-		// DAT_0047ab5c is set when player earns a bonus life (e.g., score threshold).
-		// Currently not tracked — will be wired when bonus life system is implemented.
+		// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award).
+		// DAT_0047ab5c is set when the player earns a bonus life.
 		_playerLives--;
 		if (_playerLives <= 0) {
 			// Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
@@ -617,6 +615,7 @@ int InsaneRebel2::runLevel3() {
 	while (!_vm->shouldQuit()) {
 		_playerShield = 255;
 		_playerDamage = 0;
+		_playerScore = phase1Score;
 
 		// Reset bit table before gameplay starts
 		clearBit(0);
@@ -800,7 +799,7 @@ int InsaneRebel2::runLevel6() {
 	// phase 2 retries + death handling. Phase 1 death breaks inner → RETRY at outer bottom.
 	// Phase 2 death → RETRYB → re-enters phase 2 within inner loop.
 	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-	int totalScore = 0;
+	int phase1Score = 0;
 
 	// Play level beginning cinematic (06BEG.SAN)
 	// Original: FUN_004171c5(s_LEV06_06BEG_SAN, 0x20, 0xaf, 0xa0, 10, 5, 0x4b)
@@ -852,8 +851,8 @@ int InsaneRebel2::runLevel6() {
 			continue;
 		}
 
-		// Phase 1 survived — save score, play POST1
-		totalScore = _playerScore;  // variantIdx = DAT_0047ab84
+		// Phase 1 survived — preserve this score across phase 2 retries.
+		phase1Score = _playerScore;  // variantIdx = DAT_0047ab84
 
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
@@ -866,6 +865,7 @@ int InsaneRebel2::runLevel6() {
 		while (!_vm->shouldQuit()) {
 			_rebelLevelType = 6;  // DAT_0047a7f8 = 6
 			_currentPhase = 2;
+			_playerScore = phase1Score;
 			clearBit(0);  // FUN_00407d10
 
 			debug("Rebel2: Level 6 Phase 2");
@@ -876,9 +876,6 @@ int InsaneRebel2::runLevel6() {
 			if (_vm->shouldQuit())
 				return kLevelQuit;
 
-			// Accumulate score: variantIdx = DAT_0047ab84 + variantIdx
-			totalScore += _playerScore;
-
 			if (_playerShield > 0) {
 				// Level completed!
 				debug("Rebel2: Level 6 completed!");
@@ -947,8 +944,8 @@ int InsaneRebel2::runLevel7() {
 		// Original: FUN_0041f4d0("07PLAY.SAN", 0x28, -1, -1, 0)
 		// At frame 0x638 (1592), if DAT_0047ab8c != 0: play 07PLAYB.SAN (0x468)
 		// TODO: Mid-level fork at frame 1592 requires per-frame callback.
-		// For now, play the main video. The fork video (07PLAYB) would be triggered
-		// by IACT callbacks setting a state flag during gameplay.
+		// The fork video (07PLAYB) should be triggered by IACT callbacks setting
+		// a state flag during gameplay.
 		splayer->setCurVideoFlags(0x28);
 		splayer->play("LEV07/07PLAY.SAN", 12);
 		_deathFrame = splayer->_frame;
@@ -1461,7 +1458,7 @@ int InsaneRebel2::runLevel11() {
 			if (!allBasicKilled) {
 				// Normal bridge drop cinematic
 				playCinematic("LEV11/11POST3.SAN");
-				// Bonus checks (FUN_0042aa70) — deferred, play standard path
+				// Bonus checks (FUN_0042aa70) are not implemented; play the standard path.
 				// Original checks 0x77 and 0x62 for special POST3C cinematic
 			} else {
 				// All enemy types killed — bridge dropped successfully
@@ -1969,8 +1966,8 @@ int InsaneRebel2::runLevel13() {
 		// If alive after Phase A, play Phase B (reactor destruction loop)
 		// Original: at frame == maxFrame-10, play 13PLAY_B.SAN (0x468)
 		// Then loop while (DAT_0047ab90 != 0 || DAT_0047ab7c != 0)
-		// For now, play B as a sequential video. The IACT callbacks will manage
-		// the reactor target state through opcode interactions.
+		// Play B as a sequential video. The IACT callbacks manage the reactor
+		// target state through opcode interactions.
 		if (_playerShield > 0) {
 			splayer->setCurVideoFlags(0x468);
 			splayer->play("LEV13/13PLAY_B.SAN", 12);
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 311b34df0fe..2610822cdee 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -408,7 +408,7 @@ bool SmushPlayerRebel2::handleGameAnimHeader(byte *headerContent) {
 }
 
 // ---------------------------------------------------------------------------
-// RA2 helper methods (still on SmushPlayer for now, used by base class)
+// RA2 helper methods used by the base SmushPlayer pipeline.
 // ---------------------------------------------------------------------------
 
 void SmushPlayerRebel2::handleGameLoad(int32 subSize, Common::SeekableReadStream &b) {


Commit: 4ab5b29537b1c3e0903c031c99b6485a71d0c34f
    https://github.com/scummvm/scummvm/commit/4ab5b29537b1c3e0903c031c99b6485a71d0c34f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:56+02:00

Commit Message:
SCUMM: RA2: Remove remaining dead code

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


diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 8d6e118e5bf..d0d2368c7d7 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -413,8 +413,6 @@ int InsaneRebel2::runLevel(int levelId) {
 	_damageHighFlashCounter = 0;
 	_damageShakeCounter = 0;
 	_currentPhase = 1;
-	_phaseScore = 0;
-	_phaseMisses = 0;
 	_skipSectionRequested = false;
 
 	// Dispatch to per-level handler
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index b03b822305a..ffe9ccdb0cf 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -1003,125 +1003,6 @@ void InsaneRebel2::drawPreviewBox(byte *renderBitmap, int pitch, int width, int
 	}
 }
 
-// Draw preview thumbnail content - emulates FUN_00428a10 + FUN_00429b40
-// Based on FUN_00415CF8 assembly analysis:
-//
-// The original uses O_LEVEL.SAN (640x400) with chapter previews stacked vertically.
-// Video offset (FUN_00425170) shifts which preview is visible:
-//   X offset = -90 (0xffa6)
-//   Y offset = chapter * -50 + 75
-//
-// For 320x200 mode, O_MENU_X.SAN doesn't contain chapter-specific preview images.
-// Those are only in O_LEVEL.SAN (640x400). We display a styled placeholder instead
-// with the chapter number and visual styling to match the original UI appearance.
-void InsaneRebel2::drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter) {
-	// Preview destination area coordinates (inside the inner border)
-	// From assembly: Inner box at X=230, Y=75, W=80, H=50
-	const int destX = 230;
-	const int destY = 75;
-	const int thumbW = 80;  // 0x50
-	const int thumbH = 50;  // 0x32
-
-	// Fill preview area with a dark blue gradient background
-	// This creates a styled placeholder since O_MENU_X.SAN doesn't have previews
-	for (int py = 0; py < thumbH; py++) {
-		int dy = destY + py;
-		if (dy < 0 || dy >= height)
-			continue;
-
-		// Create vertical gradient: darker at top (0x10), lighter at bottom (0x18)
-		byte bgColor = 0x10 + (py * 8 / thumbH);
-
-		for (int px = 0; px < thumbW; px++) {
-			int dx = destX + px;
-			if (dx < 0 || dx >= width)
-				continue;
-			renderBitmap[dy * pitch + dx] = bgColor;
-		}
-	}
-
-	// Draw chapter number overlay in the center of the preview
-	NutRenderer *font = _smush_smalfontNut;
-	if (!font)
-		return;
-
-	char chapterStr[16];
-	if (chapter < 15) {
-		snprintf(chapterStr, sizeof(chapterStr), "CH.%d", chapter + 1);
-	} else {
-		snprintf(chapterStr, sizeof(chapterStr), "FINALE");
-	}
-
-	// Calculate text width for centering
-	int textWidth = 0;
-	int numChars = font->getNumChars();
-	for (const char *c = chapterStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numChars) {
-			textWidth += font->getCharWidth(charIdx);
-		}
-	}
-
-	// Center the text in the preview area
-	int textX = destX + (thumbW - textWidth) / 2;
-	int textY = destY + thumbH / 2 - 4;
-
-	Common::Rect clipRect(0, 0, width, height);
-
-	// Draw text shadow (offset by 1,1)
-	int curX = textX + 1;
-	for (const char *c = chapterStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && textY + 1 >= 0 && textY + 1 < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, textY + 1, pitch, 0,
-				                 kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-
-	// Draw main text (bright)
-	curX = textX;
-	for (const char *c = chapterStr; *c; c++) {
-		int charIdx = (unsigned char)*c;
-		if (charIdx < numChars) {
-			int charWidth = font->getCharWidth(charIdx);
-			if (curX >= 0 && curX + charWidth <= width && textY >= 0 && textY < height) {
-				font->drawCharV7(renderBitmap, clipRect, curX, textY, pitch, -1,
-				                 kStyleAlignLeft, charIdx, true, true);
-			}
-			curX += charWidth;
-		}
-	}
-
-	// Draw lock icon for locked chapters
-	if (!_chapterUnlocked[chapter]) {
-		byte lockColor = 0xF8;
-		int lockX = destX + thumbW - 15;
-		int lockY = destY + 5;
-
-		// Draw padlock shape
-		for (int i = 2; i < 6; i++) {
-			if (lockX + i < width && lockY < height && lockY >= 0)
-				renderBitmap[lockY * pitch + lockX + i] = lockColor;
-		}
-		for (int i = 1; i < 4; i++) {
-			if (lockX + 2 < width && lockY + i < height && lockY + i >= 0)
-				renderBitmap[(lockY + i) * pitch + lockX + 2] = lockColor;
-			if (lockX + 5 < width && lockY + i < height && lockY + i >= 0)
-				renderBitmap[(lockY + i) * pitch + lockX + 5] = lockColor;
-		}
-		for (int y = 0; y < 4; y++) {
-			for (int x = 1; x < 7; x++) {
-				if (lockX + x < width && lockY + 4 + y < height && lockY + 4 + y >= 0)
-					renderBitmap[(lockY + 4 + y) * pitch + lockX + x] = lockColor;
-			}
-		}
-	}
-}
-
 // Rating to medal string (FUN_0042001f): TALKFONT glyphs 3=big, 2=medium, 1=small
 Common::String InsaneRebel2::getRankString(int rating) {
 	if (rating > 50)
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 3c576b408f6..42fe279ad97 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -276,12 +276,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_corridorBottomY = 0x104;  // 260 — full game buffer height
 	_hitCooldown = 0;
 
-	// Initialize legacy shot system (backwards compatibility)
-	for (i = 0; i < 2; i++) {
-		_shots[i].active = false;
-		_shots[i].counter = 0;
-	}
-
 	// Initialize Handler 0x26 Turret shot system (FUN_40AD63)
 	for (i = 0; i < 2; i++) {
 		_turretShots[i].counter = 0;
@@ -475,8 +469,6 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// Initialize level state tracking for multi-phase levels
 	_currentPhase = 1;
 	_deathFrame = 0;
-	_phaseScore = 0;
-	_phaseMisses = 0;
 	_skipSectionRequested = false;
 
 	// Register as EventObserver to capture input events before ScummEngine consumes them
@@ -1106,21 +1098,6 @@ void InsaneRebel2::updatePilotProgress(int levelIndex, int32 score, int32 lives,
 	savePilots();
 }
 
-int InsaneRebel2::getPilotHighestLevel() const {
-	if (_activePilot < 0 || _activePilot >= _numPilots)
-		return 0;
-
-	const PilotData &pilot = _pilots[_activePilot];
-	int highest = 0;
-	for (int i = kNumLevels - 1; i >= 0; i--) {
-		if (pilot.damage[i] < 0xFF) {
-			highest = i;
-			break;
-		}
-	}
-	return highest;
-}
-
 // processMouse -- Mouse input with edge detection for buttons.
 int32 InsaneRebel2::processMouse() {
 	int32 buttons = 0;
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index a16874082ce..9325a802fe5 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -160,9 +160,6 @@ public:
 	// Draw the preview thumbnail box - emulates FUN_004292D0 calls in FUN_00415CF8
 	void drawPreviewBox(byte *renderBitmap, int pitch, int width, int height);
 
-	// Draw the preview thumbnail content - shows chapter number/status in preview area
-	void drawPreviewThumbnail(byte *renderBitmap, int pitch, int width, int height, int chapter);
-
 	// View offset for chapter preview scrolling (DAT_0047abe2/DAT_0047abe4)
 	int16 _previewOffsetX;   // X offset = -90 for chapter select
 	int16 _previewOffsetY;   // Y offset = chapter * -50 + 75
@@ -283,9 +280,6 @@ public:
 	// Update pilot progress after level completion
 	void updatePilotProgress(int levelIndex, int32 score, int32 lives, int32 damage);
 
-	// Get highest unlocked level for active pilot (checks damage[] < 0xFF)
-	int getPilotHighestLevel() const;
-
 	// ---------------------------------------------------------------------------
 	// Pilot Selection Menu (FUN_00414A41)
 	// ---------------------------------------------------------------------------
@@ -301,9 +295,7 @@ public:
 	enum PilotMenuMode {
 		kPilotModeSelect = 0,     // Normal pilot list selection
 		kPilotModeNameInput = 1,  // Typing a new pilot name
-		kPilotModeDifficulty = 2, // Difficulty submenu
-		kPilotModeDeleteConfirm = 3, // Delete confirmation
-		kPilotModeCopySelect = 4  // Copy source selection
+		kPilotModeDifficulty = 2  // Difficulty submenu
 	};
 	PilotMenuMode _pilotMenuMode;
 	Common::String _pilotNameInput;      // Current name being typed
@@ -442,8 +434,6 @@ public:
 	// Level state tracking for multi-phase levels
 	int _currentPhase;        // Current gameplay phase (1, 2, 3 for Level 2; 1, 2 for Level 3/6)
 	int _deathFrame;          // Frame number where player died (for death video selection)
-	int _phaseScore;          // Accumulated score from previous phases (preserved on phase retry)
-	int _phaseMisses;         // Accumulated misses from previous phases
 	bool _skipSectionRequested; // Debug shortcut (Shift+S): force current gameplay section to end
 
 	// ---------------------------------------------------------------------------
@@ -541,20 +531,6 @@ public:
 	// Reset enemy active flags and collision zones at frame end
 	void frameEndCleanup();
 
-	// ---------------------------------------------------------------------------
-	// Opcode 6 Helper Functions
-	// ---------------------------------------------------------------------------
-	// Handler-specific setup extracted from iactRebel2Opcode6.
-
-	// Handler 8 (third-person on foot) setup - FUN_00401234 case 4
-	void opcode6Handler8Setup(int16 par3, int16 par4);
-
-	// Handler 7 (third-person ship) setup - FUN_0040c3cc case 4
-	void opcode6Handler7Setup(int16 par3, int16 par4);
-
-	// Calculate view offsets based on level type (lines 182-213)
-	void opcode6CalcViewOffsets();
-
 	// ---------------------------------------------------------------------------
 	// Opcode 8 Helper Functions
 	// ---------------------------------------------------------------------------
@@ -618,7 +594,6 @@ public:
 	// _rebelDetailMode (DAT_0047a7fc): Controls whether edge highlights are drawn.
 	// Set from IACT opcode 6. When >= 0, edge highlights are enabled.
 	byte _edgeTable[256 * 256];       // DAT_0046a7d0 - primary edge blend table
-	byte _edgeTableAlt[256 * 256];    // DAT_00443fb0 - secondary blend table (hi-res mode)
 	int16 _rebelDetailMode;           // DAT_0047a7fc - edge highlight enable flag
 
 	// Initialize edge blend table (FUN_410510)
@@ -914,14 +889,6 @@ public:
 	SpaceShot _spaceShots[2];
 	int16 _spaceShotDirection;  // DAT_0044374e - ship direction for gun lookup
 
-	// Legacy struct for backwards compatibility
-	struct Shot {
-		bool active;
-		int counter;
-		int x, y;       // Target position
-	};
-	Shot _shots[2];
-
 	// Handler-specific shot spawning
 	void spawnTurretShot(int x, int y);    // Handler 0x26
 	void spawnVehicleShot(int x, int y);   // Handler 8
@@ -1018,9 +985,6 @@ public:
 	int16 _shipDirectionH;           // Horizontal direction (0-4, center=2)
 	int16 _shipDirectionV;           // Vertical direction (0-6, center=3)
 
-	// Helper to load a NUT file from IACT chunk data
-	NutRenderer *loadNutFromIact(Common::SeekableReadStream &b, int dataSize);
-
 	// ---------------------------------------------------------------------------
 	// Handler 7 FLY Ship System
 	// ---------------------------------------------------------------------------
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 950256d543f..08a0b731211 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -372,16 +372,6 @@ void InsaneRebel2::spawnShot(int x, int y) {
 		spawnHandler25Shot(x, y);
 		break;
 	default:
-		// Legacy fallback
-		for (int i = 0; i < 2; i++) {
-			if (!_shots[i].active) {
-				_shots[i].active = true;
-				_shots[i].counter = getShotMaxDuration();
-				_shots[i].x = x + _viewX;
-				_shots[i].y = y + _viewY;
-				break;
-			}
-		}
 		break;
 	}
 }
@@ -394,14 +384,14 @@ void InsaneRebel2::spawnTurretShot(int x, int y) {
 			// levelType 5: BLAST.SAD (slot 0), otherwise: TBLAST.SAD (slot 7)
 			playSfx((_rebelLevelType == 5) ? 0 : 7, 127, 0);
 
-				_turretShots[i].counter = getShotMaxDuration();
-				_turretShots[i].seqNum = _turretShotSeqCounter;
-				_turretShotSeqCounter++;
-				_turretShots[i].targetX = x + _viewX;  // DAT_0044366e in original
-				_turretShots[i].targetY = y + _viewY;  // DAT_00443670 in original
-				break;
-			}
+			_turretShots[i].counter = getShotMaxDuration();
+			_turretShots[i].seqNum = _turretShotSeqCounter;
+			_turretShotSeqCounter++;
+			_turretShots[i].targetX = x + _viewX;  // DAT_0044366e in original
+			_turretShots[i].targetY = y + _viewY;  // DAT_00443670 in original
+			break;
 		}
+	}
 }
 
 // spawnVehicleShot -- Handler 8 vehicle shot spawn (FUN_401CCF).
@@ -1025,11 +1015,10 @@ void InsaneRebel2::freeLaserTexture() {
 }
 
 //
-// initEdgeTable -- Initialize edge blend tables (FUN_410510).
+// initEdgeTable -- Initialize edge blend table (FUN_410510).
 //
-// When data is nullptr, fills with default tables:
+// When data is nullptr, fills with the default table:
 //   _edgeTable[a*256+b] = min(a,b) (symmetric identity blend)
-//   _edgeTableAlt[a*256+b] = special blend for hi-res mode
 // When data is non-null, loads the primary table from data+8 (upper triangle, symmetric).
 //
 void InsaneRebel2::initEdgeTable(const byte *data) {
@@ -1040,27 +1029,12 @@ void InsaneRebel2::initEdgeTable(const byte *data) {
 				// Primary table: table[a][b] = a (i.e. min(a,b) since b >= a)
 				_edgeTable[a + b * 256] = (byte)a;
 				_edgeTable[b + a * 256] = (byte)a;
-
-				// Secondary table: special blend rules (FUN_410510 lines 17-31)
-				if (a < 0x10 || b > 0x4f) {
-					// Outside blend range: use b if b==0, or (0xf < b && b < 0x50), or b==4
-					if (b == 0 || (b > 0xf && b < 0x50) || b == 4) {
-						_edgeTableAlt[a + b * 256] = (byte)b;
-					} else {
-						_edgeTableAlt[a + b * 256] = (byte)a;
-					}
-				} else {
-					// Blend range [0x10..0x4f]: average of a and b
-					_edgeTableAlt[a + b * 256] = (byte)((a + b) / 2);
-				}
-				_edgeTableAlt[b + a * 256] = _edgeTableAlt[a + b * 256];
 			}
 		}
 		// Special entries (FUN_410510 lines 33-36)
 		_edgeTable[0x42 * 256 + 0xf1] = 0x42;   // DAT_00447ff1
 		_edgeTable[0x42 + 0xf0 * 256] = 0x42;   // DAT_004480f0 (symmetric)
 		_edgeTable[0x41 * 256 + 0xb0] = 0x41;   // DAT_00447fb0
-		_edgeTableAlt[0x41 * 256 + 0xf0] = 0x41; // DAT_00443ff0
 	} else {
 		// Load table from IACT data (FUN_410510 non-NULL path, lines 39-47)
 		// Data format: 8-byte header + upper triangle of 256x256 symmetric table


Commit: 14f6c845da9687af84e8fa71ffd59feac957db7c
    https://github.com/scummvm/scummvm/commit/14f6c845da9687af84e8fa71ffd59feac957db7c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:56+02:00

Commit Message:
SCUMM: RA2: Use playLevelSegment in runlevels

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 9325a802fe5..4beb48494ac 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -399,6 +399,13 @@ public:
 	// flags: bit 1 = add random unkilled types, bit 0 = limit credits to 2 (else 8)
 	uint16 processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
 
+	// Play a raw SAN segment from a scripted level handler.
+	// Retail reaches these call sites through different wrappers/direct paths; this
+	// only collapses ScummVM's shared dispatch step. Callers still choose the original
+	// flags and when to call processWaveEnd(). recordFrame preserves the original
+	// split between gameplay/wave calls and transition/init-only segments.
+	bool playLevelSegment(const char *filename, uint16 flags, bool recordFrame = true);
+
 	// Random number helper (emulates FUN_004233a0)
 	int getRandomVariant(int max);
 
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index bb32a844149..ac2c2ccefa2 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -35,8 +35,6 @@ namespace Scumm {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel1() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// Play level beginning cinematic (01BEG.SAN)
 	playLevelBegin(1);
 	if (_vm->shouldQuit())
@@ -54,13 +52,7 @@ int InsaneRebel2::runLevel1() {
 		clearBit(0);
 
 		// Play gameplay (01P01.SAN with 0x28 flags)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV01/01P01.SAN", 12);
-
-		// Store death frame for video selection
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV01/01P01.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -197,6 +189,15 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 	return result;
 }
 
+bool InsaneRebel2::playLevelSegment(const char *filename, uint16 flags, bool recordFrame) {
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	splayer->setCurVideoFlags(flags);
+	splayer->play(filename, 12);
+	if (recordFrame)
+		_deathFrame = splayer->_frame;
+	return !_vm->shouldQuit();
+}
+
 // ---------------------------------------------------------------------------
 // Level 2 Handler - FUN_00418063
 // Multiple parts with P1/P2/P3 subdirectories
@@ -217,7 +218,6 @@ int InsaneRebel2::runLevel2() {
 	// wave progression. Using calibrated defaults until exact table values extracted.
 	const int16 kLevel2BudgetBase[3] = { 3, 3, 3 };  // Phase 1, 2, 3
 
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	int bonusCount = 0;     // local_1c: tracks bonus events (DAT_0047ab9c & 0x10)
 	int totalKills = 0;     // local_c: accumulated kill count across phases
 	int totalMisses = 0;    // Accumulated misses (sVar1 + sVar2 from hit counters)
@@ -273,11 +273,7 @@ int InsaneRebel2::runLevel2() {
 
 		// Play A.SAN (background loader) — flags 0x28 (preserve buffer, gameplay mode)
 		debug("Rebel2: Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/P1/02P01_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV02/P1/02P01_A.SAN", 0x28))
 			return kLevelQuit;
 
 		// processWaveEnd after A.SAN (threshold=0, no early exit for background loader)
@@ -298,9 +294,8 @@ int InsaneRebel2::runLevel2() {
 			};
 			debug("Rebel2: Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
 			// Wave videos use flags 0x428 (original: FUN_0041f4d0 param_2=0x428)
-			splayer->setCurVideoFlags(0x428);
-			splayer->play(variants[variant], 12);
-			_deathFrame = splayer->_frame;
+			if (!playLevelSegment(variants[variant], 0x428))
+				return kLevelQuit;
 
 			// processWaveEnd with threshold=0x14 (20) — enables early exit when enemies killed
 			processWaveEnd(0x36, &budget, 0x14, 0);
@@ -322,9 +317,7 @@ int InsaneRebel2::runLevel2() {
 		// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/02PST1.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV02/02PST1.SAN", 0x28, false))
 			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
@@ -346,11 +339,7 @@ int InsaneRebel2::runLevel2() {
 
 		// Play A.SAN (background loader)
 		debug("Rebel2: Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/P2/02P02_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV02/P2/02P02_A.SAN", 0x28))
 			return kLevelQuit;
 
 		// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
@@ -386,9 +375,8 @@ int InsaneRebel2::runLevel2() {
 			}
 
 			debug("Rebel2: Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
-			splayer->setCurVideoFlags(0x428);
-			splayer->play(filename, 12);
-			_deathFrame = splayer->_frame;
+			if (!playLevelSegment(filename, 0x428))
+				return kLevelQuit;
 		}
 
 		if ((_rebelPhaseState & 0x10) != 0)
@@ -404,9 +392,7 @@ int InsaneRebel2::runLevel2() {
 		// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/02PST2.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV02/02PST2.SAN", 0x28, false))
 			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
@@ -429,11 +415,7 @@ int InsaneRebel2::runLevel2() {
 
 		// Play A.SAN (background loader)
 		debug("Rebel2: Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV02/P3/02P03_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV02/P3/02P03_A.SAN", 0x28))
 			return kLevelQuit;
 
 		// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
@@ -476,9 +458,8 @@ int InsaneRebel2::runLevel2() {
 				}
 
 				debug("Rebel2: Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				// processWaveEnd at BOTTOM with threshold=0x14
 				waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
@@ -546,7 +527,6 @@ int InsaneRebel2::runLevel2() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel3() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	int phase1Score = 0;  // Score preserved across phase 2 retries
 
 	// Play level beginning cinematic (03BEG.SAN)
@@ -565,11 +545,7 @@ int InsaneRebel2::runLevel3() {
 
 		// Play phase 1 gameplay (03PLAY1.SAN)
 		debug("Rebel2: Level 3 Phase 1");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV03/03PLAY1.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV03/03PLAY1.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -604,9 +580,7 @@ int InsaneRebel2::runLevel3() {
 	// Reset handler to 0 so procPostRendering skips HUD/sprite drawing during cinematic
 	_rebelHandler = 0;
 	_rebelStatusBarSprite = 0;
-	splayer->setCurVideoFlags(0x28);
-	splayer->play("LEV03/03POST1.SAN", 12);
-	if (_vm->shouldQuit())
+	if (!playLevelSegment("LEV03/03POST1.SAN", 0x28, false))
 		return kLevelQuit;
 
 	// ----- PHASE 2 retry loop (preserves phase 1 score) -----
@@ -622,11 +596,7 @@ int InsaneRebel2::runLevel3() {
 
 		// Play phase 2 gameplay (03PLAY2.SAN)
 		debug("Rebel2: Level 3 Phase 2");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV03/03PLAY2.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV03/03PLAY2.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -665,13 +635,9 @@ int InsaneRebel2::runLevel3() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel4() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// Play cutscene (04CUT.SAN)
 	// Original: FUN_00417168 adds | 8, so flags = 0x20 | 0x08 = 0x28
-	splayer->setCurVideoFlags(0x28);
-	splayer->play("LEV04/04CUT.SAN", 12);
-	if (_vm->shouldQuit())
+	if (!playLevelSegment("LEV04/04CUT.SAN", 0x28, false))
 		return kLevelQuit;
 
 	// Play level beginning cinematic (04BEG.SAN)
@@ -690,11 +656,7 @@ int InsaneRebel2::runLevel4() {
 
 		// Play gameplay (04PLAY.SAN)
 		debug("Rebel2: Level 4 gameplay");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV04/04PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV04/04PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -732,8 +694,6 @@ int InsaneRebel2::runLevel4() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel5() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// Play level beginning cinematic (05BEG.SAN)
 	playLevelBegin(5);
 	if (_vm->shouldQuit())
@@ -750,11 +710,7 @@ int InsaneRebel2::runLevel5() {
 
 		// Play gameplay (05PLAY.SAN)
 		debug("Rebel2: Level 5 gameplay");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV05/05PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV05/05PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -798,7 +754,6 @@ int InsaneRebel2::runLevel6() {
 	// Original structure: outer do-while for phase 1 retries, inner while(true) for
 	// phase 2 retries + death handling. Phase 1 death breaks inner → RETRY at outer bottom.
 	// Phase 2 death → RETRYB → re-enters phase 2 within inner loop.
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	int phase1Score = 0;
 
 	// Play level beginning cinematic (06BEG.SAN)
@@ -822,14 +777,10 @@ int InsaneRebel2::runLevel6() {
 		_currentPhase = 1;
 
 		debug("Rebel2: Level 6 Phase 1");
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV06/06PLAY1.SAN", 12);
+		if (!playLevelSegment("LEV06/06PLAY1.SAN", 0x28))
+			return kLevelQuit;
 		// TODO: Mid-level switch at frame 0x2a8 to 06PLAY1B.SAN (flags 0x468)
 		// + score checkpoint (FUN_00407f55) — needs per-frame callback
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
-			return kLevelQuit;
 
 		if (_playerShield <= 0) {
 			// Died in phase 1
@@ -856,9 +807,7 @@ int InsaneRebel2::runLevel6() {
 
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV06/06POST1.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV06/06POST1.SAN", 0x28, false))
 			return kLevelQuit;
 
 		// ----- PHASE 2 retry loop (inner while(true) in original) -----
@@ -869,11 +818,7 @@ int InsaneRebel2::runLevel6() {
 			clearBit(0);  // FUN_00407d10
 
 			debug("Rebel2: Level 6 Phase 2");
-			splayer->setCurVideoFlags(0x28);
-			splayer->play("LEV06/06PLAY2.SAN", 12);
-			_deathFrame = splayer->_frame;
-
-			if (_vm->shouldQuit())
+			if (!playLevelSegment("LEV06/06PLAY2.SAN", 0x28))
 				return kLevelQuit;
 
 			if (_playerShield > 0) {
@@ -916,7 +861,6 @@ int InsaneRebel2::runLevel6() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel7() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	bool reachedFork = false;  // DAT_0047ab8c equivalent — tracks if 07PLAYB was played
 
 	// Play cutscene (07CUT.SAN)
@@ -946,11 +890,7 @@ int InsaneRebel2::runLevel7() {
 		// TODO: Mid-level fork at frame 1592 requires per-frame callback.
 		// The fork video (07PLAYB) should be triggered by IACT callbacks setting
 		// a state flag during gameplay.
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV07/07PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV07/07PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -993,8 +933,6 @@ int InsaneRebel2::runLevel7() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel8() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("08BEG.SAN", 0x20, 0xb1, 0xa0, 10, 5, 0x4b)
 	playLevelBegin(8);
@@ -1013,11 +951,7 @@ int InsaneRebel2::runLevel8() {
 
 		// Play gameplay (08PLAY.SAN)
 		// Original: FUN_0041f4d0("08PLAY.SAN", 8, -1, -1, 0) — note flags=0x08
-		splayer->setCurVideoFlags(0x08);
-		splayer->play("LEV08/08PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV08/08PLAY.SAN", 0x08))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -1061,8 +995,6 @@ int InsaneRebel2::runLevel8() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel9() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("09BEG.SAN", 0x20, 0xb2, 0xa0, 10, 200, 0x10e)
 	playLevelBegin(9);
@@ -1086,11 +1018,7 @@ int InsaneRebel2::runLevel9() {
 		// Original: FUN_0041f4d0("09PLAY.SAN", 0x28, -1, -1, 0)
 		// Mid-events at frames 415 and 850: FUN_00407f55 (score save)
 		// These are handled implicitly — the IACT callbacks manage scoring.
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV09/09PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV09/09PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -1133,8 +1061,6 @@ int InsaneRebel2::runLevel9() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel10() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// Play cutscene (10CUT.SAN)
 	playCinematic("LEV10/10CUT.SAN");
 	if (_vm->shouldQuit())
@@ -1158,11 +1084,7 @@ int InsaneRebel2::runLevel10() {
 
 		// Play gameplay (10PLAY.SAN)
 		// Original: FUN_0041f4d0("10PLAY.SAN", 0x28, -1, -1, 0)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV10/10PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV10/10PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -1209,7 +1131,6 @@ int InsaneRebel2::runLevel10() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel11() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
 	int totalKills = 0;
 	int totalMisses = 0;
 	int prevPhaseState = 0;
@@ -1262,11 +1183,7 @@ int InsaneRebel2::runLevel11() {
 
 		// Play A.SAN (background loader)
 		debug("Rebel2: Level 11 Phase 1 - playing 11P01_A.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P1/11P01_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV11/P1/11P01_A.SAN", 0x28))
 			return kLevelQuit;
 
 		{
@@ -1294,9 +1211,8 @@ int InsaneRebel2::runLevel11() {
 				}
 
 				debug("Rebel2: Level 11 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
 			}
@@ -1310,9 +1226,7 @@ int InsaneRebel2::runLevel11() {
 		// Post segment 1 (11POST1.SAN)
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/11POST1.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV11/11POST1.SAN", 0x28, false))
 			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
@@ -1331,11 +1245,7 @@ int InsaneRebel2::runLevel11() {
 
 		// Play A.SAN (background loader)
 		debug("Rebel2: Level 11 Phase 2 - playing 11P02_A.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P2/11P02_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV11/P2/11P02_A.SAN", 0x28))
 			return kLevelQuit;
 
 		{
@@ -1357,9 +1267,8 @@ int InsaneRebel2::runLevel11() {
 				}
 
 				debug("Rebel2: Level 11 Phase 2 wave - %s (state=0x%x)", filename, _rebelPhaseState);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
 			}
@@ -1373,9 +1282,7 @@ int InsaneRebel2::runLevel11() {
 		// Post segment 2 (11POST2.SAN)
 		_rebelHandler = 0;
 		_rebelStatusBarSprite = 0;
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/11POST2.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV11/11POST2.SAN", 0x28, false))
 			return kLevelQuit;
 
 		totalKills += _rebelKillCounter;
@@ -1395,11 +1302,7 @@ int InsaneRebel2::runLevel11() {
 		_rebelHandler = 8;
 
 		debug("Rebel2: Level 11 Phase 3 first half - playing 11P03_A.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P3/11P03_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV11/P3/11P03_A.SAN", 0x28))
 			return kLevelQuit;
 
 		{
@@ -1437,9 +1340,8 @@ int InsaneRebel2::runLevel11() {
 				}
 
 				debug("Rebel2: Level 11 Phase 3a wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				// Threshold only for higher variants (original: (2 < variantIdx) - 1 & 0x14)
 				int16 threshold = (variantIdx > 2) ? 0x14 : 0;
@@ -1486,11 +1388,7 @@ int InsaneRebel2::runLevel11() {
 
 		// Play G.SAN (background loader for second half)
 		debug("Rebel2: Level 11 Phase 3 second half - playing 11P03_G.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV11/P3/11P03_G.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV11/P3/11P03_G.SAN", 0x28))
 			return kLevelQuit;
 
 		// Only enter wave loop if not all basic types killed already
@@ -1523,9 +1421,8 @@ int InsaneRebel2::runLevel11() {
 				}
 
 				debug("Rebel2: Level 11 Phase 3b wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				int16 threshold = (variantIdx > 2) ? 0x14 : 0;
 				waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
@@ -1601,8 +1498,6 @@ int InsaneRebel2::runLevel11() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel12() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// Kill credit budget bases per phase
 	const int16 kLevel12BudgetBase[4] = { 3, 4, 4, 4 };
 
@@ -1648,17 +1543,12 @@ int InsaneRebel2::runLevel12() {
 
 		// Initialization video (12P05.SAN)
 		debug("Rebel2: Level 12 Phase 1 - init 12P05.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV12/12P05.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/12P05.SAN", 0x28, false))
 			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P1/12P01_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P1/12P01_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/P1/12P01_A.SAN", 0x428))
 			return kLevelQuit;
 
 		{
@@ -1679,9 +1569,8 @@ int InsaneRebel2::runLevel12() {
 				}
 
 				debug("Rebel2: Level 12 Phase 1 wave - %s (state=0x%x sel=%d)", filename, _rebelPhaseState, sel);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
 			}
@@ -1701,17 +1590,12 @@ int InsaneRebel2::runLevel12() {
 
 		// Initialization video (12P06.SAN)
 		debug("Rebel2: Level 12 Phase 2 - init 12P06.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P06.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/12P06.SAN", 0x428, false))
 			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P2/12P02_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P2/12P02_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/P2/12P02_A.SAN", 0x428))
 			return kLevelQuit;
 
 		{
@@ -1740,9 +1624,8 @@ int InsaneRebel2::runLevel12() {
 				}
 
 				debug("Rebel2: Level 12 Phase 2 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				// Variants E(2) and F(5) reset threshold to 0
 				int16 threshold = (variantIdx == 2 || variantIdx == 5) ? 0 : 0x14;
@@ -1764,17 +1647,12 @@ int InsaneRebel2::runLevel12() {
 
 		// Initialization video (12P07.SAN)
 		debug("Rebel2: Level 12 Phase 3 - init 12P07.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P07.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/12P07.SAN", 0x428, false))
 			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P3/12P03_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P3/12P03_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/P3/12P03_A.SAN", 0x428))
 			return kLevelQuit;
 
 		{
@@ -1803,9 +1681,8 @@ int InsaneRebel2::runLevel12() {
 				}
 
 				debug("Rebel2: Level 12 Phase 3 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
 			}
@@ -1825,17 +1702,12 @@ int InsaneRebel2::runLevel12() {
 
 		// Initialization video (12P08.SAN)
 		debug("Rebel2: Level 12 Phase 4 - init 12P08.SAN budget=%d", budget);
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P08.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/12P08.SAN", 0x428, false))
 			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
 		// First wave (P4/12P04_A.SAN)
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/P4/12P04_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/P4/12P04_A.SAN", 0x428))
 			return kLevelQuit;
 
 		{
@@ -1863,9 +1735,8 @@ int InsaneRebel2::runLevel12() {
 				}
 
 				debug("Rebel2: Level 12 Phase 4 wave - %s (state=0x%x variantIdx=%d)", filename, _rebelPhaseState, variantIdx);
-				splayer->setCurVideoFlags(0x428);
-				splayer->play(filename, 12);
-				_deathFrame = splayer->_frame;
+				if (!playLevelSegment(filename, 0x428))
+					return kLevelQuit;
 
 				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
 			}
@@ -1877,9 +1748,7 @@ int InsaneRebel2::runLevel12() {
 			return kLevelQuit;
 
 		// ----- CLOSING: 12P09.SAN -----
-		splayer->setCurVideoFlags(0x428);
-		splayer->play("LEV12/12P09.SAN", 12);
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV12/12P09.SAN", 0x428, false))
 			return kLevelQuit;
 		processWaveEnd(1, &budget, 0, 0);
 
@@ -1933,8 +1802,6 @@ int InsaneRebel2::runLevel12() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel13() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("13BEG.SAN", 0x20, 0xb6, 0xa0, 10, 2, 0x46)
 	playLevelBegin(13);
@@ -1956,11 +1823,7 @@ int InsaneRebel2::runLevel13() {
 		// First inner loop runs until frame reaches maxFrame-10
 		// Then Phase B (13PLAY_B.SAN, flags 0x468) plays at that exact frame
 		// The 0x468 flags indicate seamless mid-video transition.
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV13/13PLAY_A.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV13/13PLAY_A.SAN", 0x28))
 			return kLevelQuit;
 
 		// If alive after Phase A, play Phase B (reactor destruction loop)
@@ -1969,14 +1832,10 @@ int InsaneRebel2::runLevel13() {
 		// Play B as a sequential video. The IACT callbacks manage the reactor
 		// target state through opcode interactions.
 		if (_playerShield > 0) {
-			splayer->setCurVideoFlags(0x468);
-			splayer->play("LEV13/13PLAY_B.SAN", 12);
-			_deathFrame = splayer->_frame;
+			if (!playLevelSegment("LEV13/13PLAY_B.SAN", 0x468))
+				return kLevelQuit;
 		}
 
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-
 		if (_playerShield > 0) {
 			int accuracy = 0;
 			if (_rebelKillCounter > 0) {
@@ -2015,8 +1874,6 @@ int InsaneRebel2::runLevel13() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel14() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// No cutscene — starts directly with BEG
 	// Original: FUN_004171c5("14BEG.SAN", 0x20, 0xb7, 0xa0, 10, 2, 0x46)
 	playLevelBegin(14);
@@ -2035,11 +1892,7 @@ int InsaneRebel2::runLevel14() {
 
 		// Play gameplay (14PLAY.SAN)
 		// Original: FUN_0041f4d0("14PLAY.SAN", 0x28, -1, -1, 0)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV14/14PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV14/14PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
@@ -2083,8 +1936,6 @@ int InsaneRebel2::runLevel14() {
 // ---------------------------------------------------------------------------
 
 int InsaneRebel2::runLevel15() {
-	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
-
 	// Play cutscene (15CUT.SAN)
 	playCinematic("LEV15/15CUT.SAN");
 	if (_vm->shouldQuit())
@@ -2116,11 +1967,7 @@ int InsaneRebel2::runLevel15() {
 
 		// Play gameplay (15PLAY.SAN)
 		// Original: FUN_0041f4d0("15PLAY.SAN", 0x28, -1, -1, 0)
-		splayer->setCurVideoFlags(0x28);
-		splayer->play("LEV15/15PLAY.SAN", 12);
-		_deathFrame = splayer->_frame;
-
-		if (_vm->shouldQuit())
+		if (!playLevelSegment("LEV15/15PLAY.SAN", 0x28))
 			return kLevelQuit;
 
 		if (_playerShield > 0) {


Commit: 6fae78c49d0ca16743b9536a6dd7062c75a62dcf
    https://github.com/scummvm/scummvm/commit/6fae78c49d0ca16743b9536a6dd7062c75a62dcf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:56+02:00

Commit Message:
SCUMM: RA2: Use calculateAccuracy in runlevels

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 4beb48494ac..62518613619 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -406,6 +406,8 @@ public:
 	// split between gameplay/wave calls and transition/init-only segments.
 	bool playLevelSegment(const char *filename, uint16 flags, bool recordFrame = true);
 
+	int calculateAccuracy(int kills, int misses) const;
+
 	// Random number helper (emulates FUN_004233a0)
 	int getRandomVariant(int max);
 
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index ac2c2ccefa2..dfd1ce18e85 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -198,6 +198,14 @@ bool InsaneRebel2::playLevelSegment(const char *filename, uint16 flags, bool rec
 	return !_vm->shouldQuit();
 }
 
+int InsaneRebel2::calculateAccuracy(int kills, int misses) const {
+	const int totalShots = kills + misses;
+	if (kills <= 0 || totalShots <= 0)
+		return 0;
+
+	return (kills * 100) / totalShots;
+}
+
 // ---------------------------------------------------------------------------
 // Level 2 Handler - FUN_00418063
 // Multiple parts with P1/P2/P3 subdirectories
@@ -481,11 +489,7 @@ int InsaneRebel2::runLevel2() {
 		// Score presentation remains to be implemented.
 		{
 			totalMisses += _rebelHitCounter;
-			int accuracy = 0;
-			int totalShots = totalKills + totalMisses;
-			if (totalKills > 0 && totalShots > 0) {
-				accuracy = (totalKills * 100) / totalShots;
-			}
+			int accuracy = calculateAccuracy(totalKills, totalMisses);
 			debug("Rebel2: Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
 				totalKills, totalMisses, accuracy, bonusCount);
 		}
@@ -955,10 +959,7 @@ int InsaneRebel2::runLevel8() {
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 8 completed! accuracy=%d%%", accuracy);
 			playLevelEnd(8);
 			_levelUnlocked[8] = true;
@@ -1022,10 +1023,7 @@ int InsaneRebel2::runLevel9() {
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 9 completed! accuracy=%d%%", accuracy);
 			playLevelEnd(9);
 			_levelUnlocked[9] = true;
@@ -1088,10 +1086,7 @@ int InsaneRebel2::runLevel10() {
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 10 completed! accuracy=%d%%", accuracy);
 			playLevelEnd(10);
 			_levelUnlocked[10] = true;
@@ -1439,11 +1434,7 @@ int InsaneRebel2::runLevel11() {
 		// ----- LEVEL COMPLETED -----
 		{
 			totalMisses += _rebelHitCounter;
-			int accuracy = 0;
-			int totalShots = totalKills + totalMisses;
-			if (totalKills > 0 && totalShots > 0) {
-				accuracy = (totalKills * 100) / totalShots;
-			}
+			int accuracy = calculateAccuracy(totalKills, totalMisses);
 			debug("Rebel2: Level 11 completed! kills=%d misses=%d accuracy=%d%%",
 				totalKills, totalMisses, accuracy);
 		}
@@ -1754,10 +1745,7 @@ int InsaneRebel2::runLevel12() {
 
 		// ----- LEVEL COMPLETED -----
 		{
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 12 completed! kills=%d misses=%d accuracy=%d%%",
 				_rebelKillCounter, _rebelHitCounter, accuracy);
 		}
@@ -1837,10 +1825,7 @@ int InsaneRebel2::runLevel13() {
 		}
 
 		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 13 completed! accuracy=%d%%", accuracy);
 			playLevelEnd(13);
 			_levelUnlocked[13] = true;
@@ -1896,10 +1881,7 @@ int InsaneRebel2::runLevel14() {
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 14 completed! accuracy=%d%%", accuracy);
 			playLevelEnd(14);
 			_levelUnlocked[14] = true;
@@ -1971,10 +1953,7 @@ int InsaneRebel2::runLevel15() {
 			return kLevelQuit;
 
 		if (_playerShield > 0) {
-			int accuracy = 0;
-			if (_rebelKillCounter > 0) {
-				accuracy = (_rebelKillCounter * 100) / (_rebelHitCounter + _rebelKillCounter);
-			}
+			int accuracy = calculateAccuracy(_rebelKillCounter, _rebelHitCounter);
 			debug("Rebel2: Level 15 completed! accuracy=%d%%", accuracy);
 			playLevelEnd(15);
 			_levelUnlocked[15] = true;


Commit: de00bf9dd4afe466ae186a9deab1041e276dcfad
    https://github.com/scummvm/scummvm/commit/de00bf9dd4afe466ae186a9deab1041e276dcfad
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:57+02:00

Commit Message:
SCUMM: RA2: Refactor explosion rendering

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 62518613619..0ed42c2b99d 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -702,6 +702,11 @@ public:
 		bool active;
 	};
 
+	enum ExplosionFrameAdvance {
+		kExplosionAdvanceAfterDraw,
+		kExplosionAdvanceBeforeDraw
+	};
+
 	Explosion _explosions[5];
 	void spawnExplosion(int x, int y, int objectHalfWidth);
 
@@ -906,6 +911,8 @@ public:
 	void spawnShot(int x, int y);          // Dispatcher based on current handler
 
 	// Handler-specific explosion rendering
+	void renderExplosionFrame(byte *renderBitmap, int pitch, int width, int height,
+	                          Explosion &explosion, int screenX, int screenY, ExplosionFrameAdvance advance);
 	void renderTurretExplosions(byte *renderBitmap, int pitch, int width, int height);     // FUN_409FBC (Handler 0x26)
 	void renderVehicleExplosions(byte *renderBitmap, int pitch, int width, int height);    // FUN_402696 (Handler 8)
 	void renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height);      // FUN_40F1C5 (Handler 7)
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 08a0b731211..d0db992e1ff 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -3506,6 +3506,46 @@ void InsaneRebel2::renderExplosions(byte *renderBitmap, int pitch, int width, in
 	}
 }
 
+// renderExplosionFrame -- Shared low-res explosion sprite path.
+// The original handlers reach this through separate retail functions. In the
+// 320x200 path used here, they share the same scale buckets and centered NUT
+// draw; callers keep their coordinate transforms and frame timing explicit.
+void InsaneRebel2::renderExplosionFrame(byte *renderBitmap, int pitch, int width, int height,
+		InsaneRebel2::Explosion &explosion, int screenX, int screenY, ExplosionFrameAdvance advance) {
+	if (!explosion.active)
+		return;
+
+	if (explosion.counter <= 0) {
+		explosion.active = false;
+		return;
+	}
+
+	if (advance == kExplosionAdvanceBeforeDraw)
+		explosion.counter--;
+
+	// Fixed low-res thresholds (0x0b=11, 0x15=21).
+	int baseIndex;
+	if (explosion.scale < 11) {
+		baseIndex = 9;
+	} else if (explosion.scale < 21) {
+		baseIndex = 19;
+	} else {
+		baseIndex = 29;
+	}
+
+	int spriteIndex = baseIndex + (12 - explosion.counter);
+
+	if (_smush_iconsNut->getNumChars() > spriteIndex) {
+		int ew = _smush_iconsNut->getCharWidth(spriteIndex);
+		int eh = _smush_iconsNut->getCharHeight(spriteIndex);
+		renderNutSprite(renderBitmap, pitch, width, height,
+			screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
+	}
+
+	if (advance == kExplosionAdvanceAfterDraw)
+		explosion.counter--;
+}
+
 // renderTurretExplosions -- Handler 0x26 turret explosion rendering (FUN_409FBC).
 // Position: FUN_0041c720 3D->2D projection (identity at low-res).
 // Scale thresholds: <11, <21. Secondary NUT: DAT_0047fe80 (if DAT_0047a7fc >= 0).
@@ -3517,36 +3557,12 @@ void InsaneRebel2::renderTurretExplosions(byte *renderBitmap, int pitch, int wid
 		if (!_explosions[i].active)
 			continue;
 
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_409FBC: Fixed thresholds (0x0b=11, 0x15=21)
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;   // Small (sprites 11-20)
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;  // Medium (sprites 21-30)
-		} else {
-			baseIndex = 29;  // Large (sprites 31-40)
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
 		// Position: world coords passed through FUN_0041c720 (3D→2D projection).
 		// At 320x200 low-res turret view, projection is effectively identity.
 		int screenX = _explosions[i].x;
 		int screenY = _explosions[i].y;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
+		renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+			screenX, screenY, kExplosionAdvanceAfterDraw);
 	}
 }
 
@@ -3561,41 +3577,17 @@ void InsaneRebel2::renderVehicleExplosions(byte *renderBitmap, int pitch, int wi
 		if (!_explosions[i].active)
 			continue;
 
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_402696: Fixed thresholds (0x0b=11, 0x15=21)
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;
-		} else {
-			baseIndex = 29;
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
 		// FUN_402696 line 22-23: screenX = worldX - DAT_0043e006, screenY = worldY - DAT_0043e008
 		int screenX = _explosions[i].x - _viewX;
 		int screenY = _explosions[i].y - _viewY;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
+		renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+			screenX, screenY, kExplosionAdvanceAfterDraw);
 	}
 }
 
 // renderSpaceExplosions -- Handler 7 space explosion rendering (FUN_40F1C5).
 // Position: FUN_0041c720 3D->2D projection.
-// Scale thresholds: resolution-dependent (low-res: <11/<21, hi-res: <21/<41).
+// Original scale thresholds are resolution-dependent; current low-res path uses <11/<21.
 // Secondary NUT: DAT_0047ff00 (FLY004, if DAT_0047a7fc >= 0).
 void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
@@ -3606,39 +3598,12 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 		if (!_explosions[i].active)
 			continue;
 
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// FUN_40F1C5 lines 41-51: Resolution-dependent thresholds.
-		// Low-res (DAT_0047a808 < 2): thresholds 20, 10
-		// High-res: thresholds 40, 20
-		// We run at low-res (320x200), so use 10/20 (same as fixed handlers).
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;
-		} else {
-			baseIndex = 29;
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
 		// Position: world coords through FUN_0041c720 (3D→2D projection).
 		// At low-res, this is close to identity for the ship view.
 		int screenX = _explosions[i].x;
 		int screenY = _explosions[i].y;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
-
-		_explosions[i].counter--;
+		renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+			screenX, screenY, kExplosionAdvanceAfterDraw);
 	}
 
 	// --- Part 2: Corridor/zone hit explosion (FUN_40F1C5 lines 61-85) ---
@@ -3697,7 +3662,7 @@ void InsaneRebel2::renderSpaceExplosions(byte *renderBitmap, int pitch, int widt
 
 // renderHandler25Explosions -- Handler 25 FPS explosion rendering (FUN_41F29A).
 // Position: world coords + view offset (DAT_0045790c/0e = _rebelViewOffsetX/Y).
-// Scale thresholds: resolution-dependent (same as Handler 7). No sound panning.
+// Original scale thresholds follow Handler 7; current low-res path uses <11/<21. No sound panning.
 void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int width, int height) {
 	if (!_smush_iconsNut)
 		return;
@@ -3706,36 +3671,11 @@ void InsaneRebel2::renderHandler25Explosions(byte *renderBitmap, int pitch, int
 		if (!_explosions[i].active)
 			continue;
 
-		if (_explosions[i].counter <= 0) {
-			_explosions[i].active = false;
-			continue;
-		}
-
-		// Match FUN_41F29A exactly: decrement first, then select frame.
-		_explosions[i].counter--;
-
-		// FUN_41F29A lines 27-37: Resolution-dependent thresholds (same as Handler 7).
-		int baseIndex;
-		if (_explosions[i].scale < 11) {
-			baseIndex = 9;
-		} else if (_explosions[i].scale < 21) {
-			baseIndex = 19;
-		} else {
-			baseIndex = 29;
-		}
-
-		int spriteIndex = baseIndex + (12 - _explosions[i].counter);
-
 		// FUN_41F29A line 22-23: screenX = worldX + DAT_0045790c, screenY = worldY + DAT_0045790e
 		int screenX = _explosions[i].x + _rebelViewOffsetX;
 		int screenY = _explosions[i].y + _rebelViewOffsetY;
-
-		if (_smush_iconsNut->getNumChars() > spriteIndex) {
-			int ew = _smush_iconsNut->getCharWidth(spriteIndex);
-			int eh = _smush_iconsNut->getCharHeight(spriteIndex);
-			renderNutSprite(renderBitmap, pitch, width, height,
-				screenX - ew / 2, screenY - eh / 2, _smush_iconsNut, spriteIndex);
-		}
+		renderExplosionFrame(renderBitmap, pitch, width, height, _explosions[i],
+			screenX, screenY, kExplosionAdvanceBeforeDraw);
 	}
 }
 


Commit: c6e257251eb84bf925afc0c080f9c4678cfe15e7
    https://github.com/scummvm/scummvm/commit/c6e257251eb84bf925afc0c080f9c4678cfe15e7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:57+02:00

Commit Message:
SCUMM: RA2: Remove remaining gotos

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 0ed42c2b99d..812038eff10 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -408,6 +408,9 @@ public:
 
 	int calculateAccuracy(int kills, int misses) const;
 
+	// Play DIE/OVER/RETRY tail. Returns true when the caller should restart its retry loop.
+	bool handleLevelDeath(int levelId, int phase, const char *deathVideo, const char *retryVideo, int &levelResult);
+
 	// Random number helper (emulates FUN_004233a0)
 	int getRandomVariant(int max);
 
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index dfd1ce18e85..f7d7a8c0ff8 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -206,6 +206,32 @@ int InsaneRebel2::calculateAccuracy(int kills, int misses) const {
 	return (kills * 100) / totalShots;
 }
 
+bool InsaneRebel2::handleLevelDeath(int levelId, int phase,
+		const char *deathVideo, const char *retryVideo, int &levelResult) {
+	debug("Rebel2: Level %d Phase %d death", levelId, phase);
+	playCinematic(deathVideo);
+	if (_vm->shouldQuit()) {
+		levelResult = kLevelQuit;
+		return false;
+	}
+
+	_playerLives--;
+	if (_playerLives <= 0) {
+		playLevelGameOver(levelId);
+		levelResult = kLevelGameOver;
+		return false;
+	}
+
+	playCinematic(retryVideo);
+	_playerDamage = 0;
+	if (_vm->shouldQuit()) {
+		levelResult = kLevelQuit;
+		return false;
+	}
+
+	return true;
+}
+
 // ---------------------------------------------------------------------------
 // Level 2 Handler - FUN_00418063
 // Multiple parts with P1/P2/P3 subdirectories
@@ -314,8 +340,12 @@ int InsaneRebel2::runLevel2() {
 		if ((_rebelPhaseState & 0x10) != 0)
 			bonusCount++;
 
-		if (_playerDamage >= 255)
-			goto level2_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -390,8 +420,12 @@ int InsaneRebel2::runLevel2() {
 		if ((_rebelPhaseState & 0x10) != 0)
 			bonusCount++;
 
-		if (_playerDamage >= 255)
-			goto level2_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -479,8 +513,12 @@ int InsaneRebel2::runLevel2() {
 			bonusCount++;
 		totalKills += _rebelKillCounter;
 
-		if (_playerDamage >= 255)
-			goto level2_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -497,28 +535,6 @@ int InsaneRebel2::runLevel2() {
 		playLevelEnd(2);
 		_levelUnlocked[2] = true;  // Unlock level 3
 		return kLevelNextLevel;
-
-	level2_death:
-		// Player died — play death sequence and retry or game over
-		// Original: FUN_00417168("LEV02/02DIE.SAN", 0x20)
-		debug("Rebel2: Level 2 Phase %d death", _currentPhase);
-		playCinematic("LEV02/02DIE.SAN");
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-
-		// Original: if (DAT_0047ab5c != 0) DAT_0047a7ee++ (bonus life award).
-		// DAT_0047ab5c is set when the player earns a bonus life.
-		_playerLives--;
-		if (_playerLives <= 0) {
-			// Original: FUN_00417ab2("LEV02/02OVER.SAN", 0x20, 2)
-			playLevelGameOver(2);
-			return kLevelGameOver;
-		}
-		playCinematic("LEV02/02RETRY.SAN");
-		_playerDamage = 0;
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-		continue;  // Restart from beginning
 	}
 
 	return kLevelQuit;
@@ -1213,8 +1229,12 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level11_death_phase1;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(11, 1, "LEV11/11DIE_A.SAN", "LEV11/11RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1269,8 +1289,12 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level11_death_phase2;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(11, 2, "LEV11/11DIE_B.SAN", "LEV11/11RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1344,8 +1368,12 @@ int InsaneRebel2::runLevel11() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level11_death_phase3;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(11, 3, "LEV11/11DIE_C.SAN", "LEV11/11RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1426,8 +1454,12 @@ int InsaneRebel2::runLevel11() {
 
 		totalKills += _rebelKillCounter;
 
-		if (_playerDamage >= 255)
-			goto level11_death_phase3;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(11, 3, "LEV11/11DIE_C.SAN", "LEV11/11RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1442,35 +1474,6 @@ int InsaneRebel2::runLevel11() {
 		playLevelEnd(11);
 		_levelUnlocked[11] = true;  // Unlock level 12
 		return kLevelNextLevel;
-
-	level11_death_phase1:
-		debug("Rebel2: Level 11 Phase 1 death");
-		playCinematic("LEV11/11DIE_A.SAN");
-		goto level11_retry;
-
-	level11_death_phase2:
-		debug("Rebel2: Level 11 Phase 2 death");
-		playCinematic("LEV11/11DIE_B.SAN");
-		goto level11_retry;
-
-	level11_death_phase3:
-		debug("Rebel2: Level 11 Phase 3 death");
-		playCinematic("LEV11/11DIE_C.SAN");
-		goto level11_retry;
-
-	level11_retry:
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(11);
-			return kLevelGameOver;
-		}
-		playCinematic("LEV11/11RETRY.SAN");
-		_playerDamage = 0;
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-		continue;  // Restart from Phase 1
 	}
 
 	return kLevelQuit;
@@ -1567,8 +1570,12 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level12_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1624,8 +1631,12 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level12_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1679,8 +1690,12 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level12_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1733,8 +1748,12 @@ int InsaneRebel2::runLevel12() {
 			}
 		}
 
-		if (_playerDamage >= 255)
-			goto level12_death;
+		if (_playerDamage >= 255) {
+			int levelResult;
+			if (handleLevelDeath(12, _currentPhase, "LEV12/12DIE.SAN", "LEV12/12RETRY.SAN", levelResult))
+				continue;
+			return levelResult;
+		}
 		if (_vm->shouldQuit())
 			return kLevelQuit;
 
@@ -1757,24 +1776,6 @@ int InsaneRebel2::runLevel12() {
 		playLevelEnd(12);
 		_levelUnlocked[12] = true;  // Unlock level 13
 		return kLevelNextLevel;
-
-	level12_death:
-		// Single death video for all phases
-		debug("Rebel2: Level 12 Phase %d death", _currentPhase);
-		playCinematic("LEV12/12DIE.SAN");
-
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-		_playerLives--;
-		if (_playerLives <= 0) {
-			playLevelGameOver(12);
-			return kLevelGameOver;
-		}
-		playCinematic("LEV12/12RETRY.SAN");
-		_playerDamage = 0;
-		if (_vm->shouldQuit())
-			return kLevelQuit;
-		continue;  // Restart from Phase 1
 	}
 
 	return kLevelQuit;


Commit: ae5c073de3ac1089059be0cee52cc91c305de119
    https://github.com/scummvm/scummvm/commit/ae5c073de3ac1089059be0cee52cc91c305de119
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:57+02:00

Commit Message:
SCUMM: RA2: Deduplicate runlevel helpers

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 812038eff10..2d2210494cc 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -410,6 +410,9 @@ public:
 
 	// Play DIE/OVER/RETRY tail. Returns true when the caller should restart its retry loop.
 	bool handleLevelDeath(int levelId, int phase, const char *deathVideo, const char *retryVideo, int &levelResult);
+	void resetLevelAttemptState(int initialPhase);
+	void resetLevelPhaseState(bool clearEnemies);
+	void resetLevelWaveState();
 
 	// Random number helper (emulates FUN_004233a0)
 	int getRandomVariant(int max);
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index f7d7a8c0ff8..89ac7d43442 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -232,6 +232,37 @@ bool InsaneRebel2::handleLevelDeath(int levelId, int phase,
 	return true;
 }
 
+void InsaneRebel2::resetLevelAttemptState(int initialPhase) {
+	_playerShield = 255;
+	_playerDamage = 0;
+	_currentPhase = initialPhase;
+
+	_rebelAutopilot = 0;
+	_rebelDamageLevel = 0;
+	_rebelControlMode = 0;
+
+	_enemies.clear();
+	for (int i = 0; i < 512; i++) {
+		_rebelLinks[i][0] = 0;
+		_rebelLinks[i][1] = 0;
+		_rebelLinks[i][2] = 0;
+	}
+}
+
+void InsaneRebel2::resetLevelPhaseState(bool clearEnemies) {
+	_rebelKillCounter = 0;
+	_rebelHitCounter = 0;
+	resetLevelWaveState();
+
+	if (clearEnemies)
+		_enemies.clear();
+}
+
+void InsaneRebel2::resetLevelWaveState() {
+	_rebelPhaseState = 0;
+	_rebelWaveState = 0;
+}
+
 // ---------------------------------------------------------------------------
 // Level 2 Handler - FUN_00418063
 // Multiple parts with P1/P2/P3 subdirectories
@@ -273,33 +304,14 @@ int InsaneRebel2::runLevel2() {
 
 	// Main gameplay retry loop (restarts from beginning on death)
 	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
+		resetLevelAttemptState(1);
 		bonusCount = 0;
 		totalKills = 0;
 		totalMisses = 0;
 
-		// Reset Handler 25 cover state — player starts uncovered at level start
-		// DAT_00457904 and DAT_0045790a are zero-initialized globals in the original
-		_rebelAutopilot = 0;
-		_rebelDamageLevel = 0;
-		_rebelControlMode = 0;
-
-		// FUN_0041c7d0: Reset per-attempt state
-		_enemies.clear();
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
-
 		// ----- PHASE 1: P1/02P01_X.SAN -----
 		// FUN_0041c7d0: Reset per-phase counters
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
+		resetLevelPhaseState(false);
 
 		// Initialize kill budget from level data table + random(3)
 		// Original: sVar4 = levelData[phase1Offset]; local_14[0] = sVar4 + random(3)
@@ -363,11 +375,7 @@ int InsaneRebel2::runLevel2() {
 
 		// ----- PHASE 2: P2/02P02_X.SAN -----
 		_currentPhase = 2;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
+		resetLevelPhaseState(true);
 
 		// Initialize Phase 2 budget
 		budget = kLevel2BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
@@ -442,11 +450,7 @@ int InsaneRebel2::runLevel2() {
 
 		// ----- PHASE 3: P3/02P03_X.SAN -----
 		_currentPhase = 3;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
+		resetLevelPhaseState(true);
 		prevWaveState = 0;
 
 		// Initialize Phase 3 budget
@@ -1164,31 +1168,13 @@ int InsaneRebel2::runLevel11() {
 
 	// Main gameplay retry loop (restarts from Phase 1 on death)
 	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
+		resetLevelAttemptState(1);
 		totalKills = 0;
 		totalMisses = 0;
 		prevPhaseState = 0;
 
-		// Reset Handler 8 cover state
-		_rebelAutopilot = 0;
-		_rebelDamageLevel = 0;
-		_rebelControlMode = 0;
-
-		// FUN_0041c7d0: Reset per-attempt state
-		_enemies.clear();
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
-
 		// ----- PHASE 1: P1/11P01_X.SAN -----
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
+		resetLevelPhaseState(false);
 
 		int16 budget = kLevel11BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
 
@@ -1249,11 +1235,7 @@ int InsaneRebel2::runLevel11() {
 
 		// ----- PHASE 2: P2/11P02_X.SAN -----
 		_currentPhase = 2;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
+		resetLevelPhaseState(true);
 
 		budget = kLevel11BudgetBase[1] + _vm->_rnd.getRandomNumber(2);
 		_rebelHandler = 8;
@@ -1310,11 +1292,7 @@ int InsaneRebel2::runLevel11() {
 		// ----- PHASE 3 FIRST HALF: P3/11P03_X (A-F) -----
 		// Bridge puzzle — exit when (phaseState & 0x70) == 0x70
 		_currentPhase = 3;
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
-		_enemies.clear();
+		resetLevelPhaseState(true);
 		prevPhaseState = 0;
 
 		budget = kLevel11BudgetBase[2] + _vm->_rnd.getRandomNumber(2);
@@ -1510,28 +1488,11 @@ int InsaneRebel2::runLevel12() {
 
 	// Main gameplay retry loop (restarts from Phase 1 on death)
 	while (!_vm->shouldQuit()) {
-		_playerShield = 255;
-		_playerDamage = 0;
-		_currentPhase = 1;
-
-		// Reset state
-		_rebelAutopilot = 0;
-		_rebelDamageLevel = 0;
-		_rebelControlMode = 0;
-
-		_enemies.clear();
-		for (int i = 0; i < 512; i++) {
-			_rebelLinks[i][0] = 0;
-			_rebelLinks[i][1] = 0;
-			_rebelLinks[i][2] = 0;
-		}
+		resetLevelAttemptState(1);
 
 		// ----- PHASE 1: 12P05 → P1/12P01_X -----
 		// FUN_00401000: Reset at top of each retry
-		_rebelKillCounter = 0;
-		_rebelHitCounter = 0;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
+		resetLevelPhaseState(false);
 
 		int16 budget = kLevel12BudgetBase[0] + _vm->_rnd.getRandomNumber(2);
 
@@ -1581,8 +1542,7 @@ int InsaneRebel2::runLevel12() {
 
 		// ----- PHASE 2: 12P06 → P2/12P02_X -----
 		_currentPhase = 2;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
+		resetLevelWaveState();
 
 		budget = kLevel12BudgetBase[1] + _vm->_rnd.getRandomNumber(3);
 
@@ -1642,8 +1602,7 @@ int InsaneRebel2::runLevel12() {
 
 		// ----- PHASE 3: 12P07 → P3/12P03_X -----
 		_currentPhase = 3;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
+		resetLevelWaveState();
 
 		budget = kLevel12BudgetBase[2] + _vm->_rnd.getRandomNumber(3);
 
@@ -1701,8 +1660,7 @@ int InsaneRebel2::runLevel12() {
 
 		// ----- PHASE 4: 12P08 → P4/12P04_X -----
 		_currentPhase = 4;
-		_rebelPhaseState = 0;
-		_rebelWaveState = 0;
+		resetLevelWaveState();
 
 		budget = kLevel12BudgetBase[3] + _vm->_rnd.getRandomNumber(3);
 


Commit: 4879d743c66bdee6ebefdbf9e6f94847a100d462
    https://github.com/scummvm/scummvm/commit/4879d743c66bdee6ebefdbf9e6f94847a100d462
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00

Commit Message:
SCUMM: RA2: Refactor wave handling

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 2d2210494cc..60a06d265e4 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -392,12 +392,24 @@ public:
 
 	// Wave state management (FUN_00417b61)
 	// Waits for current video to finish, accumulates kill state, redistributes
-	// kill credits from the budget. Returns credited kill bits, or 0xFFFF on death/quit.
+	// kill credits from the budget.
 	// mask: required enemy bits (0x36 for Phase 1, 0x3e for Phases 2/3)
 	// budget: kill credit budget counter (decremented per credit transfer)
 	// threshold: early-exit frame threshold (0=disabled, 0x14=20 for wave loops)
 	// flags: bit 1 = add random unkilled types, bit 0 = limit credits to 2 (else 8)
-	uint16 processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
+	struct WaveEndResult {
+		WaveEndResult() : creditedBits(0), died(false), quit(false), completed(false), skipped(false) {}
+
+		bool shouldStop() const { return died || quit || completed || skipped; }
+
+		uint16 creditedBits;
+		bool died;
+		bool quit;
+		bool completed;
+		bool skipped;
+	};
+
+	WaveEndResult processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags);
 
 	// Play a raw SAN segment from a scripted level handler.
 	// Retail reaches these call sites through different wrappers/direct paths; this
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 89ac7d43442..93a763db401 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -88,28 +88,27 @@ int InsaneRebel2::runLevel1() {
 // ---------------------------------------------------------------------------
 // Wave State Management - FUN_00417b61
 // Waits for video completion, accumulates kill state, redistributes kill credits.
-// Used by all multi-wave levels (Level 2, 3, 6, etc.) as the core wave loop primitive.
+// Used by randomized multi-wave level handlers as the core wave loop primitive.
 // ---------------------------------------------------------------------------
 
-uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
+InsaneRebel2::WaveEndResult InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold, uint16 flags) {
 	// FUN_00417b61: Core wave management function
 	// Called after each wave video plays. Handles:
 	// 1. Waiting for video to finish (with early exit on enemy completion)
 	// 2. Copying wave state to accumulated phase state
 	// 3. Redistributing kill credits from the budget
-	//
-	// Returns: kill bits credited this wave, or 0xFFFF on death/quit/completion
 
-	uint16 result = 0;
+	WaveEndResult result;
 
 	// Debug shortcut path: force-end current section when requested via Shift+S.
-	// This returns the same sentinel (0xFFFF) used for section completion/death/quit.
 	if (_skipSectionRequested) {
 		_skipSectionRequested = false;
 		_rebelPhaseState = mask;
 		_rebelWaveState = mask;
 		debug("Rebel2 processWaveEnd: Shift+S skip consumed (mask=0x%x)", (uint16)mask);
-		return 0xFFFF;
+		result.completed = true;
+		result.skipped = true;
+		return result;
 	}
 
 	// Step 1: Wait for video to finish (lines 21-32)
@@ -167,7 +166,7 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 	while (creditCount < maxCredits && numKilled > 0 && budget && *budget > 0) {
 		int idx = _vm->_rnd.getRandomNumber(numKilled - 1);
 		_rebelPhaseState -= killed[idx];   // Remove from accumulated state
-		result |= killed[idx];              // Credit to return value
+		result.creditedBits |= killed[idx]; // Credit to return value
 		(*budget)--;
 
 		// Remove from array (shift remaining elements)
@@ -179,13 +178,13 @@ uint16 InsaneRebel2::processWaveEnd(int16 mask, int16 *budget, int16 threshold,
 	}
 
 	debug("Rebel2 processWaveEnd: result=0x%x phaseState=0x%x (after redistribution) budget=%d",
-		result, _rebelPhaseState, budget ? *budget : -1);
+		result.creditedBits, _rebelPhaseState, budget ? *budget : -1);
+
+	// Step 5: Stop conditions (lines 74-78)
+	result.died = (_playerDamage >= 255);
+	result.completed = ((int16)_rebelPhaseState >= mask);
+	result.quit = _vm->shouldQuit();
 
-	// Step 5: Return value (lines 74-78)
-	// Return 0xFFFF if: dead, phase complete, or quit
-	if (_playerDamage >= 255 || (int16)_rebelPhaseState >= mask || _vm->shouldQuit()) {
-		return 0xFFFF;
-	}
 	return result;
 }
 
@@ -391,8 +390,9 @@ int InsaneRebel2::runLevel2() {
 		// Phase 2 wave loop: processWaveEnd at TOP of loop (matches assembly structure)
 		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); then switch(local_10)
 		while (true) {
-			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
-			if (waveSelect == 0xFFFF || (_rebelPhaseState & 0x0e) == 0x0e)
+			WaveEndResult waveEnd = processWaveEnd(0x3e, &budget, 0, 0);
+			uint16 waveSelect = waveEnd.creditedBits;
+			if (waveEnd.shouldStop() || (_rebelPhaseState & 0x0e) == 0x0e)
 				break;
 			if (_vm->shouldQuit())
 				return kLevelQuit;
@@ -467,12 +467,14 @@ int InsaneRebel2::runLevel2() {
 		// Phase 3: processWaveEnd at BOTTOM (like Phase 1), waveSelect carried across iterations
 		// Original: local_10 = FUN_00417b61(0x3e, local_14, 0, 0); while (loop) { ...; local_10 = FUN_00417b61(0x3e, local_14, 0x14, 0); }
 		{
-			uint16 waveSelect = processWaveEnd(0x3e, &budget, 0, 0);
+			WaveEndResult waveEnd = processWaveEnd(0x3e, &budget, 0, 0);
 
-			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x0e) != 0x0e) {
+			while (!waveEnd.shouldStop() && (_rebelPhaseState & 0x0e) != 0x0e) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
+				uint16 waveSelect = waveEnd.creditedBits;
+
 				// Phase 3 randomization (original lines 113-115):
 				// If previous wave state bit 0 was clear AND random(8)==0, set bit 0
 				if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
@@ -508,7 +510,7 @@ int InsaneRebel2::runLevel2() {
 					return kLevelQuit;
 
 				// processWaveEnd at BOTTOM with threshold=0x14
-				waveSelect = processWaveEnd(0x3e, &budget, 0x14, 0);
+				waveEnd = processWaveEnd(0x3e, &budget, 0x14, 0);
 				debug("Rebel2: Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
 			}
 		}
@@ -1184,14 +1186,16 @@ int InsaneRebel2::runLevel11() {
 			return kLevelQuit;
 
 		{
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+			WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0, 0);
 
 			// Phase 1 wave loop: random(2) | (waveSelect & 8) → variants
 			// 0→D, 1→C, 8→B, 9→A
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
+				uint16 waveSelect = waveEnd.creditedBits;
+
 				// Bonus sound check
 				if ((_rebelPhaseState & 0x10) != 0 && (prevPhaseState & 0x10) == 0) {
 					// FUN_00411931 bonus sound — not yet implemented
@@ -1211,7 +1215,7 @@ int InsaneRebel2::runLevel11() {
 				if (!playLevelSegment(filename, 0x428))
 					return kLevelQuit;
 
-				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+				waveEnd = processWaveEnd(0x0e, &budget, 0x14, 0);
 			}
 		}
 
@@ -1247,10 +1251,10 @@ int InsaneRebel2::runLevel11() {
 
 		{
 			// Phase 2: flags=3 (maxCredits=2, redistribution ON)
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 3);
+			WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0, 3);
 
 			// Random(4) for variant selection: A, B, C, D
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
@@ -1267,7 +1271,7 @@ int InsaneRebel2::runLevel11() {
 				if (!playLevelSegment(filename, 0x428))
 					return kLevelQuit;
 
-				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 3);
+				waveEnd = processWaveEnd(0x0e, &budget, 0x14, 3);
 			}
 		}
 
@@ -1303,11 +1307,11 @@ int InsaneRebel2::runLevel11() {
 			return kLevelQuit;
 
 		{
-			uint16 waveSelect = processWaveEnd(0x7e, &budget, 0, 0);
+			WaveEndResult waveEnd = processWaveEnd(0x7e, &budget, 0, 0);
 			int variantIdx = 0;  // Tracks variant for randomization threshold
 
 			// Loop until (phaseState & 0x70) == 0x70 (bridge targets destroyed)
-			while (waveSelect != 0xFFFF && (_rebelPhaseState & 0x70) != 0x70) {
+			while (!waveEnd.shouldStop() && (_rebelPhaseState & 0x70) != 0x70) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
@@ -1342,7 +1346,7 @@ int InsaneRebel2::runLevel11() {
 
 				// Threshold only for higher variants (original: (2 < variantIdx) - 1 & 0x14)
 				int16 threshold = (variantIdx > 2) ? 0x14 : 0;
-				waveSelect = processWaveEnd(0x7e, &budget, threshold, 0);
+				waveEnd = processWaveEnd(0x7e, &budget, threshold, 0);
 			}
 		}
 
@@ -1395,9 +1399,9 @@ int InsaneRebel2::runLevel11() {
 		// Only enter wave loop if not all basic types killed already
 		if ((_rebelPhaseState & 0x0e) < 0x0e) {
 			int variantIdx = 0;
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0, 0);
+			WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0, 0);
 
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
@@ -1426,7 +1430,7 @@ int InsaneRebel2::runLevel11() {
 					return kLevelQuit;
 
 				int16 threshold = (variantIdx > 2) ? 0x14 : 0;
-				waveSelect = processWaveEnd(0x0e, &budget, threshold, 0);
+				waveEnd = processWaveEnd(0x0e, &budget, threshold, 0);
 			}
 		}
 
@@ -1507,13 +1511,14 @@ int InsaneRebel2::runLevel12() {
 			return kLevelQuit;
 
 		{
-			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+			WaveEndResult waveEnd = processWaveEnd(6, &budget, 0x14, 0);
 
 			// Wave loop: random(2) | (waveSelect & 2) → 0:C, 1:D, 2:A, 3:B
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
+				uint16 waveSelect = waveEnd.creditedBits;
 				int sel = _vm->_rnd.getRandomNumber(1) | (waveSelect & 2);
 				const char *filename;
 				switch (sel) {
@@ -1527,7 +1532,7 @@ int InsaneRebel2::runLevel12() {
 				if (!playLevelSegment(filename, 0x428))
 					return kLevelQuit;
 
-				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+				waveEnd = processWaveEnd(6, &budget, 0x14, 0);
 			}
 		}
 
@@ -1557,12 +1562,14 @@ int InsaneRebel2::runLevel12() {
 			return kLevelQuit;
 
 		{
-			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+			WaveEndResult waveEnd = processWaveEnd(6, &budget, 0x14, 0);
 
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
+				uint16 waveSelect = waveEnd.creditedBits;
+
 				// Variant selection: (waveSelect & 2) controls which set
 				int variantIdx;
 				if ((waveSelect & 2) == 0) {
@@ -1587,7 +1594,7 @@ int InsaneRebel2::runLevel12() {
 
 				// Variants E(2) and F(5) reset threshold to 0
 				int16 threshold = (variantIdx == 2 || variantIdx == 5) ? 0 : 0x14;
-				waveSelect = processWaveEnd(6, &budget, threshold, 0);
+				waveEnd = processWaveEnd(6, &budget, threshold, 0);
 			}
 		}
 
@@ -1618,9 +1625,9 @@ int InsaneRebel2::runLevel12() {
 
 		{
 			int variantIdx = 0;
-			uint16 waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+			WaveEndResult waveEnd = processWaveEnd(6, &budget, 0x14, 0);
 
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
@@ -1645,7 +1652,7 @@ int InsaneRebel2::runLevel12() {
 				if (!playLevelSegment(filename, 0x428))
 					return kLevelQuit;
 
-				waveSelect = processWaveEnd(6, &budget, 0x14, 0);
+				waveEnd = processWaveEnd(6, &budget, 0x14, 0);
 			}
 		}
 
@@ -1676,9 +1683,9 @@ int InsaneRebel2::runLevel12() {
 
 		{
 			int variantIdx = 0;
-			uint16 waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+			WaveEndResult waveEnd = processWaveEnd(0x0e, &budget, 0x14, 0);
 
-			while (waveSelect != 0xFFFF) {
+			while (!waveEnd.shouldStop()) {
 				if (_vm->shouldQuit())
 					return kLevelQuit;
 
@@ -1702,7 +1709,7 @@ int InsaneRebel2::runLevel12() {
 				if (!playLevelSegment(filename, 0x428))
 					return kLevelQuit;
 
-				waveSelect = processWaveEnd(0x0e, &budget, 0x14, 0);
+				waveEnd = processWaveEnd(0x0e, &budget, 0x14, 0);
 			}
 		}
 


Commit: 6eab30feedda5009d334ed8b6d305092caf964a5
    https://github.com/scummvm/scummvm/commit/6eab30feedda5009d334ed8b6d305092caf964a5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00

Commit Message:
SCUMM: RA2: Split opcode 6 IACT handling

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


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 5d1423ca472..7c088bf7026 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -583,29 +583,9 @@ void InsaneRebel2::iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2,
 	}
 }
 
-//
-// iactRebel2Opcode6 -- Level setup / mode switch (FUN_41CADB case 4)
-//
-// Per-wave initialization: clears bit table, resets link tables, configures
-// handler mode (ship/turret/corridor), and loads collision zones. Called once
-// per wave video on frame 0.
-//
-void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
-	// Opcode 6: Level setup / mode switch
-	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
-	//
-	// For Handler 8 (third-person on foot) - FUN_00401234 case 4:
-	// - par3 sets ship level mode (DAT_0043e000)
-	// - par4 == 1 triggers status bar display and state reset
-	// - Updates ship position based on mouse input
-	//
-	// For Handler 0x26/0x19 (turret/FPS):
-	// - Same par4 == 1 behavior
-	// - Different view offset calculations
-
-	debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
-
-	// Update handler type if par2 is a known handler value (from FUN_4033CF case 6)
+// ScummVM refactor helper for opcode 6, not a separate retail function.
+void InsaneRebel2::updateOpcode6Handler(int16 par2) {
+	// Update handler type if par2 is a known handler value (from FUN_4033CF case 6).
 	if (par2 == 7 || par2 == 8 || par2 == 0x19 || par2 == 0x26) {
 		// Reset Level 2 background flag when transitioning away from Handler 8
 		if (_rebelHandler == 8 && par2 != 8) {
@@ -614,725 +594,728 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		_rebelHandler = par2;
 		debug("Rebel2 Opcode 6: Setting handler=%d", par2);
 	}
+}
 
-	// Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4
-	if (_rebelHandler == 8) {
-		// Set ship level mode (DAT_0043e000 = par3)
-		_shipLevelMode = par3;
-
-		// If par4 == 1, enable status bar and re-render laser texture (FUN_0040bb87)
-		if (par4 == 1) {
-			_rebelStatusBarSprite = 5;
-			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
-				initLaserTexture(_smush_iconsNut, 5);
-			}
-		}
+// ScummVM refactor helper for opcode 6 Handler 8, not a separate retail function.
+void InsaneRebel2::handleOpcode6Handler8(int16 par3, int16 par4) {
+	// Handler 8 specific logic (third-person on foot) - FUN_00401234 case 4.
+	// Set ship level mode (DAT_0043e000 = par3)
+	_shipLevelMode = par3;
 
-		// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
-		// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
-		if (_shipLevelMode != 0 && par4 == 1 && !_rebelOp6Initialized) {
-			clearBit(0);
-			for (int i = 0; i < 512; i++) {
-				_rebelLinks[i][0] = 0;
-				_rebelLinks[i][1] = 0;
-				_rebelLinks[i][2] = 0;
-			}
-			_rebelWaveState = _rebelPhaseState;
-			_rebelOp6Initialized = true;
-			debug("Rebel2 Opcode 6 (Handler 8): Wave init, wave=0x%x", _rebelWaveState);
-		}
-
-		// Skip position calculation for special modes 4 and 5
-		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
-			// ----- Movement Range Transition (Covered vs Shooting) -----
-			// Based on FUN_00401234 lines 85-120:
-			// Mode 2 = "Covered" state - contract movement range to 41 (0x29)
-			// Other modes = "Shooting" state - expand movement range to 127 (0x7f)
-			// Transition happens gradually at ±10 per frame for smooth animation
-			if (_shipLevelMode == 2) {
-				// Covered state - contract movement range
-				if (_movementRangeLimit > 41) {
-					_movementRangeLimit -= 10;
-				}
-				if (_movementRangeLimit < 41) {
-					_movementRangeLimit = 41;
-				}
-			} else {
-				// Shooting state - expand movement range
-				if (_movementRangeLimit < 127) {
-					_movementRangeLimit += 10;
-				}
-				if (_movementRangeLimit > 127) {
-					_movementRangeLimit = 127;
-				}
-			}
+	// If par4 == 1, enable status bar and re-render laser texture (FUN_0040bb87)
+	if (par4 == 1) {
+		_rebelStatusBarSprite = 5;
+		if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+			initLaserTexture(_smush_iconsNut, 5);
+		}
+	}
 
-			// Calculate target position from mouse input
-			// Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
-			// Based on FUN_00401234 lines 151-166:
-			// local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
-			// local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
-
-			// Map the effective aim position (-127 to 127 range) to the ship target.
-			Common::Point aimPos = getGameplayAimPoint();
-			int16 mouseOffsetX = (int16)((aimPos.x - 160) * 127 / 160);
-			int16 mouseOffsetY = (int16)((aimPos.y - 100) * 127 / 100);
-
-			// Clamp X offset to movement range limit (covered/shooting state)
-			// Based on FUN_00401234 lines 119-136
-			if (mouseOffsetX > _movementRangeLimit)
-				mouseOffsetX = _movementRangeLimit;
-			if (mouseOffsetX < -_movementRangeLimit)
-				mouseOffsetX = -_movementRangeLimit;
-			// Y offset always uses full range (±127)
-			if (mouseOffsetY > 127)
-				mouseOffsetY = 127;
-			if (mouseOffsetY < -127)
-				mouseOffsetY = -127;
-
-			// Calculate target positions using the original formula
-			// Original FUN_00401234 lines 151-166:
-			//   local_18 = ((mouseX * 5 + 0x27b) * 0x40) / 0xfe    -> X target
-			//   local_1c = ((mouseY * 5 + 0x27b) * 0x10) / 0xfe    -> Y target
-			//   _DAT_0043e004 = -local_1c   (stored negated for cursor display)
-			// The interpolation (lines 181-193) uses local_1c (positive), NOT _DAT_0043e004.
-			// So the interpolation target must be the positive formula result.
-			_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
-			_shipTargetY = (int16)(((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
-
-			// Smooth interpolation toward target (max 50 pixels per frame)
-			const int16 maxStep = 50;  // 0x32 in hex
-			if (_shipPosX < _shipTargetX) {
-				int16 newX = _shipPosX + maxStep;
-				_shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
-			} else if (_shipPosX > _shipTargetX) {
-				int16 newX = _shipPosX - maxStep;
-				_shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
+	// Reset state when shipLevelMode != 0 && par4 == 1 (FUN_401234 lines 97-103)
+	// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+	if (_shipLevelMode != 0 && par4 == 1 && !_rebelOp6Initialized) {
+		clearBit(0);
+		for (int i = 0; i < 512; i++) {
+			_rebelLinks[i][0] = 0;
+			_rebelLinks[i][1] = 0;
+			_rebelLinks[i][2] = 0;
+		}
+		_rebelWaveState = _rebelPhaseState;
+		_rebelOp6Initialized = true;
+		debug("Rebel2 Opcode 6 (Handler 8): Wave init, wave=0x%x", _rebelWaveState);
+	}
+
+	// Skip position calculation for special modes 4 and 5
+	if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+		// ----- Movement Range Transition (Covered vs Shooting) -----
+		// Based on FUN_00401234 lines 85-120:
+		// Mode 2 = "Covered" state - contract movement range to 41 (0x29)
+		// Other modes = "Shooting" state - expand movement range to 127 (0x7f)
+		// Transition happens gradually at +/-10 per frame for smooth animation
+		if (_shipLevelMode == 2) {
+			// Covered state - contract movement range
+			if (_movementRangeLimit > 41) {
+				_movementRangeLimit -= 10;
 			}
-
-			if (_shipPosY < _shipTargetY) {
-				int16 newY = _shipPosY + maxStep;
-				_shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
-			} else if (_shipPosY > _shipTargetY) {
-				int16 newY = _shipPosY - maxStep;
-				_shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
+			if (_movementRangeLimit < 41) {
+				_movementRangeLimit = 41;
 			}
-
-			// Calculate ship direction indices for sprite selection
-			// Map mouse position to 5x7 direction grid (like Handler 7)
-			int16 mouseX = aimPos.x;
-			int16 mouseY = aimPos.y;
-
-			// Scale mouse if video is larger than 320x200
-			if (_player && _player->_width > 320) {
-				mouseX = (mouseX * 320) / _player->_width;
+		} else {
+			// Shooting state - expand movement range
+			if (_movementRangeLimit < 127) {
+				_movementRangeLimit += 10;
 			}
-			if (_player && _player->_height > 200) {
-				mouseY = (mouseY * 200) / _player->_height;
+			if (_movementRangeLimit > 127) {
+				_movementRangeLimit = 127;
 			}
-
-			// Horizontal: 5 zones (0=far left, 2=center, 4=far right)
-			if (mouseX < 64)
-				_shipDirectionH = 0;
-			else if (mouseX < 128)
-				_shipDirectionH = 1;
-			else if (mouseX < 192)
-				_shipDirectionH = 2;
-			else if (mouseX < 256)
-				_shipDirectionH = 3;
-			else
-				_shipDirectionH = 4;
-
-			// Vertical: 7 zones (0=far up, 3=center, 6=far down)
-			if (mouseY < 28)
-				_shipDirectionV = 0;
-			else if (mouseY < 57)
-				_shipDirectionV = 1;
-			else if (mouseY < 86)
-				_shipDirectionV = 2;
-			else if (mouseY < 114)
-				_shipDirectionV = 3;
-			else if (mouseY < 143)
-				_shipDirectionV = 4;
-			else if (mouseY < 171)
-				_shipDirectionV = 5;
-			else
-				_shipDirectionV = 6;
-
-			_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
-		}
-
-		// Update firing state from mouse button or joystick fire action
-		// Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
-		if (_shipLevelMode == 4) {
-			_shipFiring = false;
-		} else {
-			_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
-				_vm->getActionState(kScummActionInsaneAttack);
 		}
 
-		debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
-			_shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
-			_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+		// Calculate target position from mouse input
+		// Mouse X maps to ship horizontal tilt, Mouse Y to vertical tilt
+		// Based on FUN_00401234 lines 151-166:
+		// local_18 = ((DAT_0047a7e0 * 5 + 0x27b) * 0x40) / 0xfe
+		// local_1c = ((DAT_0047a7e2 * 5 + 0x27b) * 0x10) / 0xfe
 
-		// Handler 8 doesn't use the same view offset logic as other handlers
-		// Skip the rest of the function for Handler 8
-		return;
+		// Map the effective aim position (-127 to 127 range) to the ship target.
+		Common::Point aimPos = getGameplayAimPoint();
+		int16 mouseOffsetX = (int16)((aimPos.x - 160) * 127 / 160);
+		int16 mouseOffsetY = (int16)((aimPos.y - 100) * 127 / 100);
+
+		// Clamp X offset to movement range limit (covered/shooting state)
+		// Based on FUN_00401234 lines 119-136
+		if (mouseOffsetX > _movementRangeLimit)
+			mouseOffsetX = _movementRangeLimit;
+		if (mouseOffsetX < -_movementRangeLimit)
+			mouseOffsetX = -_movementRangeLimit;
+		// Y offset always uses full range (+/-127)
+		if (mouseOffsetY > 127)
+			mouseOffsetY = 127;
+		if (mouseOffsetY < -127)
+			mouseOffsetY = -127;
+
+		// Calculate target positions using the original formula
+		// Original FUN_00401234 lines 151-166:
+		//   local_18 = ((mouseX * 5 + 0x27b) * 0x40) / 0xfe    -> X target
+		//   local_1c = ((mouseY * 5 + 0x27b) * 0x10) / 0xfe    -> Y target
+		//   _DAT_0043e004 = -local_1c   (stored negated for cursor display)
+		// The interpolation (lines 181-193) uses local_1c (positive), NOT _DAT_0043e004.
+		// So the interpolation target must be the positive formula result.
+		_shipTargetX = (int16)(((mouseOffsetX * 5 + 0x27b) * 0x40) / 0xfe);
+		_shipTargetY = (int16)(((mouseOffsetY * 5 + 0x27b) * 0x10) / 0xfe);
+
+		// Smooth interpolation toward target (max 50 pixels per frame)
+		const int16 maxStep = 50;  // 0x32 in hex
+		if (_shipPosX < _shipTargetX) {
+			int16 newX = _shipPosX + maxStep;
+			_shipPosX = (newX > _shipTargetX) ? _shipTargetX : newX;
+		} else if (_shipPosX > _shipTargetX) {
+			int16 newX = _shipPosX - maxStep;
+			_shipPosX = (newX < _shipTargetX) ? _shipTargetX : newX;
+		}
+
+		if (_shipPosY < _shipTargetY) {
+			int16 newY = _shipPosY + maxStep;
+			_shipPosY = (newY > _shipTargetY) ? _shipTargetY : newY;
+		} else if (_shipPosY > _shipTargetY) {
+			int16 newY = _shipPosY - maxStep;
+			_shipPosY = (newY < _shipTargetY) ? _shipTargetY : newY;
+		}
+
+		// Calculate ship direction indices for sprite selection
+		// Map mouse position to 5x7 direction grid (like Handler 7)
+		int16 mouseX = aimPos.x;
+		int16 mouseY = aimPos.y;
+
+		// Scale mouse if video is larger than 320x200
+		if (_player && _player->_width > 320) {
+			mouseX = (mouseX * 320) / _player->_width;
+		}
+		if (_player && _player->_height > 200) {
+			mouseY = (mouseY * 200) / _player->_height;
+		}
+
+		// Horizontal: 5 zones (0=far left, 2=center, 4=far right)
+		if (mouseX < 64)
+			_shipDirectionH = 0;
+		else if (mouseX < 128)
+			_shipDirectionH = 1;
+		else if (mouseX < 192)
+			_shipDirectionH = 2;
+		else if (mouseX < 256)
+			_shipDirectionH = 3;
+		else
+			_shipDirectionH = 4;
+
+		// Vertical: 7 zones (0=far up, 3=center, 6=far down)
+		if (mouseY < 28)
+			_shipDirectionV = 0;
+		else if (mouseY < 57)
+			_shipDirectionV = 1;
+		else if (mouseY < 86)
+			_shipDirectionV = 2;
+		else if (mouseY < 114)
+			_shipDirectionV = 3;
+		else if (mouseY < 143)
+			_shipDirectionV = 4;
+		else if (mouseY < 171)
+			_shipDirectionV = 5;
+		else
+			_shipDirectionV = 6;
+
+		_shipDirectionIndex = _shipDirectionH * 7 + _shipDirectionV;
+	}
+
+	// Update firing state from mouse button or joystick fire action
+	// Mode 4 (autopilot) disables shooting - FUN_00401CCF line 82-84
+	if (_shipLevelMode == 4) {
+		_shipFiring = false;
+	} else {
+		_shipFiring = (_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+			_vm->getActionState(kScummActionInsaneAttack);
 	}
 
+	debug("Rebel2 Opcode 6 (Handler 8): mode=%d range=%d shipPos=(%d,%d) target=(%d,%d) firing=%d dir=(%d,%d,%d)",
+		_shipLevelMode, _movementRangeLimit, _shipPosX, _shipPosY, _shipTargetX, _shipTargetY, _shipFiring,
+		_shipDirectionH, _shipDirectionV, _shipDirectionIndex);
+}
+
+// ScummVM refactor helper for opcode 6 Handler 7, not a separate retail function.
+void InsaneRebel2::handleOpcode6Handler7(Common::SeekableReadStream &b, int16 par4) {
 	// Handler 7 specific logic (third-person ship) - FUN_0040d836 / FUN_0040c3cc
-	// Used for Level 3 and similar space combat levels
-	if (_rebelHandler == 7) {
-		// Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
-		// This determines collision mode and shooting capability:
-		//   Mode 0: Obstacle avoidance — SECONDARY zones, corridor boundaries
-		//   Mode 1: Tunnel flight — PRIMARY zones, per-edge push-back (hMargin=0x28)
-		//   Mode 2: Combat mode — shooting ENABLED, SECONDARY zones
-		//   Mode 3: Tunnel flight — PRIMARY zones, per-edge push-back (hMargin=0x0f)
-		_flyControlMode = par4;
-		debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
-			par4, (par4 == 2) ? "ENABLED" : "DISABLED");
-
-		// Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
-		// In our parsing, par3 maps to param_5[2] and the body follows par4.
-		// FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
-		// par3 is param_5[2], which the original doesn't use here.
-		// The body word for status bar is read separately below.
-		int16 bodyStatusFlag = 0;
-		if (b.size() - b.pos() >= 2) {
-			bodyStatusFlag = b.readSint16LE();
-		}
-		if (bodyStatusFlag == 1) {
-			_rebelStatusBarSprite = 5;
-			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
-				initLaserTexture(_smush_iconsNut, 5);
-			}
-			debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
+	// Used for Level 3 and similar space combat levels.
+
+	// Set control mode: DAT_004437c0 = param_5[3] = par4 in FUN_40C3CC case 4.
+	// This determines collision mode and shooting capability:
+	//   Mode 0: Obstacle avoidance - SECONDARY zones, corridor boundaries
+	//   Mode 1: Tunnel flight - PRIMARY zones, per-edge push-back (hMargin=0x28)
+	//   Mode 2: Combat mode - shooting ENABLED, SECONDARY zones
+	//   Mode 3: Tunnel flight - PRIMARY zones, per-edge push-back (hMargin=0x0f)
+	_flyControlMode = par4;
+	debug("Rebel2 Opcode 6 (Handler 7): Control mode set to %d (shooting %s)",
+		par4, (par4 == 2) ? "ENABLED" : "DISABLED");
+
+	// Status bar: param_5[4] == 1 in original (first body word, 5th IACT word)
+	// In our parsing, par3 maps to param_5[2] and the body follows par4.
+	// FUN_40C3CC: if (param_5[4] == 1) FUN_0040bb87(DAT_0047a828,5);
+	// par3 is param_5[2], which the original doesn't use here.
+	// The body word for status bar is read separately below.
+	int16 bodyStatusFlag = 0;
+	if (b.size() - b.pos() >= 2) {
+		bodyStatusFlag = b.readSint16LE();
+	}
+	if (bodyStatusFlag == 1) {
+		_rebelStatusBarSprite = 5;
+		if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+			initLaserTexture(_smush_iconsNut, 5);
 		}
+		debug("Rebel2 Opcode 6 (Handler 7): Status bar enabled (body flag=%d)", bodyStatusFlag);
+	}
 
-		// ------------------------------------------------------------
-		// Ship position update — FUN_40C3CC case 4, lines 49-327
-		// ------------------------------------------------------------
-		// Velocity-based physics with momentum/inertia:
-		//   Mouse offset from center → scaled input [-127,127]
-		//   → velocity history averaging → physics delta (clamped ±12/frame)
-		//   → position clamping → corridor collision → perspective offsets
-		//
-		// Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
-		//   offset 0: smoothing param (>>4 +1 = window size)
-		//   offset 2: Y speed          offset 4: X speed (levelSpeed)
-		//   offset 6: wind multiplier  offset 14: corridor damage
-		// We don't have the actual level data, so we use calibrated defaults.
-
-		// --- Step 1: Mouse input as offset from screen center ---
-		// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100
-		// _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
-		// consistent with handler 8 which uses _vm->_mouse.x directly.
-		Common::Point aimPos = getGameplayAimPoint();
-		int16 inputX = (int16)(aimPos.x - 160);  // DAT_0047a7e0
-		int16 inputY = (int16)(aimPos.y - 100);  // DAT_0047a7e2
-
-		// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70)
-		if (inputX > 160)
-			inputX = 160;
-		if (inputX < -160)
-			inputX = -160;
-		if (inputY > 127)
-			inputY = 127;
-		if (inputY < -127)
-			inputY = -127;
-
-		// --- Step 2: Scale to [-127, 127] (lines 82-84) ---
-		// Mouse mode: scaledInputX = (DAT_0047a7e0 * 0x7f) / 0xa0
-		int16 scaledInputX = (int16)((inputX * 127) / 160);
-		int16 scaledInputY = inputY;  // Y already in [-127, 127]
-
-		// --- Step 3: Velocity history + smoothed average (lines 141-157) ---
-		for (int i = 24; i > 0; i--) {
-			_velocityHistory[i] = _velocityHistory[i - 1];
-		}
-		_velocityHistory[0] = scaledInputX;
-
-		// Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
-		const int smoothWindow = 5;
-		int velSum = 0;
-		for (int i = 0; i < smoothWindow; i++) {
-			velSum += _velocityHistory[i];
-		}
-		_smoothedVelocity = (int16)(velSum / smoothWindow);  // DAT_0044370c
-
-		// --- Step 4: Wind history (lines 158-173) ---
-		// Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
-		const int16 windMult = 0;
-		int windSumX = 0, windSumY = 0;
-		for (int i = 14; i > 0; i--) {
-			_windHistoryX[i] = _windHistoryX[i - 1];
-			windSumX += _windHistoryX[i];
-		}
-		_windHistoryX[0] = _windParamX;
-		int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
-
-		for (int i = 14; i > 0; i--) {
-			_windHistoryY[i] = _windHistoryY[i - 1];
-			windSumY += _windHistoryY[i];
-		}
-		_windHistoryY[0] = _windParamY;
-		int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
-
-		// --- Step 5: Position delta (lines 174-242) ---
-		// levelSpeed (offset 4): calibrated so max velocity (127) → delta 12.
-		//   8 = (speed * 127) >> 9 → speed ≈ 32
-		// levelYSpeed (offset 2): calibrated so max input (127) → delta ~6.
-		//   6 = (speed * 127) >> 10 → speed ≈ 48
-		const int16 levelSpeed = 32;
-		const int16 levelYSpeed = 48;
-		int16 absSmoothVel = ABS(_smoothedVelocity);
-		int16 positionDeltaX;
-
-		if (_flyControlMode == 1) {
-			// Mode 1: Full cross-axis coupling (lines 174-186)
-			// Banking: vertical input deflects horizontal movement
-			if (scaledInputX < 1) {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
-			} else {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
-			}
+	// Ship position update - FUN_40C3CC case 4, lines 49-327.
+	// Velocity-based physics with momentum/inertia:
+	//   Mouse offset from center -> scaled input [-127,127]
+	//   -> velocity history averaging -> physics delta (clamped +/-12/frame)
+	//   -> position clamping -> corridor collision -> perspective offsets
+	//
+	// Level data table (DAT_0047e0e8 + level*0x242 + difficulty*0x22):
+	//   offset 0: smoothing param (>>4 +1 = window size)
+	//   offset 2: Y speed          offset 4: X speed (levelSpeed)
+	//   offset 6: wind multiplier  offset 14: corridor damage
+	// We don't have the actual level data, so we use calibrated defaults.
+
+	// Step 1: Mouse input as offset from screen center.
+	// DAT_0047a7e0 = mouseX - 160, DAT_0047a7e2 = mouseY - 100.
+	// _vm->_mouse.x/y are in virtual screen coords (0-319, 0-199)
+	// consistent with handler 8 which uses _vm->_mouse.x directly.
+	Common::Point aimPos = getGameplayAimPoint();
+	int16 inputX = (int16)(aimPos.x - 160);  // DAT_0047a7e0
+	int16 inputY = (int16)(aimPos.y - 100);  // DAT_0047a7e2
+
+	// Clamp: mouse mode uses [-160, 160] for X, [-127, 127] for Y (lines 55-70).
+	if (inputX > 160)
+		inputX = 160;
+	if (inputX < -160)
+		inputX = -160;
+	if (inputY > 127)
+		inputY = 127;
+	if (inputY < -127)
+		inputY = -127;
+
+	// Step 2: Scale to [-127, 127] (lines 82-84).
+	// Mouse mode: scaledInputX = (DAT_0047a7e0 * 0x7f) / 0xa0.
+	int16 scaledInputX = (int16)((inputX * 127) / 160);
+	int16 scaledInputY = inputY;  // Y already in [-127, 127]
+
+	// Step 3: Velocity history + smoothed average (lines 141-157).
+	for (int i = 24; i > 0; i--) {
+		_velocityHistory[i] = _velocityHistory[i - 1];
+	}
+	_velocityHistory[0] = scaledInputX;
+
+	// Window size = (levelData[0] >> 4) + 1. Calibrated default: 5.
+	const int smoothWindow = 5;
+	int velSum = 0;
+	for (int i = 0; i < smoothWindow; i++) {
+		velSum += _velocityHistory[i];
+	}
+	_smoothedVelocity = (int16)(velSum / smoothWindow);  // DAT_0044370c
+
+	// Step 4: Wind history (lines 158-173).
+	// Wind multiplier comes from level data[6]. Without data, use 0 (no wind).
+	const int16 windMult = 0;
+	int windSumX = 0, windSumY = 0;
+	for (int i = 14; i > 0; i--) {
+		_windHistoryX[i] = _windHistoryX[i - 1];
+		windSumX += _windHistoryX[i];
+	}
+	_windHistoryX[0] = _windParamX;
+	int16 windEffectX = (int16)((windMult * (windSumX + _windParamX)) / 15);
+
+	for (int i = 14; i > 0; i--) {
+		_windHistoryY[i] = _windHistoryY[i - 1];
+		windSumY += _windHistoryY[i];
+	}
+	_windHistoryY[0] = _windParamY;
+	int16 windEffectY = (int16)((windMult * (windSumY + _windParamY)) / 15);
+
+	// Step 5: Position delta (lines 174-242).
+	// levelSpeed (offset 4): calibrated so max velocity (127) -> delta 12.
+	//   8 = (speed * 127) >> 9 -> speed around 32
+	// levelYSpeed (offset 2): calibrated so max input (127) -> delta ~6.
+	//   6 = (speed * 127) >> 10 -> speed around 48
+	const int16 levelSpeed = 32;
+	const int16 levelYSpeed = 48;
+	int16 absSmoothVel = ABS(_smoothedVelocity);
+	int16 positionDeltaX;
+
+	if (_flyControlMode == 1) {
+		// Mode 1: Full cross-axis coupling (lines 174-186).
+		// Banking: vertical input deflects horizontal movement.
+		if (scaledInputX < 1) {
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - absSmoothVel * scaledInputY - windEffectX) >> 9);
 		} else {
-			// Mode 0/2/3: Reduced cross-axis coupling (lines 218-230)
-			if (scaledInputX < 1) {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
-			} else {
-				positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
-			}
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + absSmoothVel * scaledInputY - windEffectX) >> 9);
 		}
-
-		// Clamp X delta to ±12 per frame (lines 187-192 / 231-236)
-		if (positionDeltaX < -11)
-			positionDeltaX = -12;
-		if (positionDeltaX > 11)
-			positionDeltaX = 12;
-
-		// Apply X delta (line 193 / 237)
-		_flyShipScreenX += positionDeltaX;
-
-		// Y delta
-		if (_flyControlMode == 1) {
-			// Mode 1: clamped to ±12 with wind (lines 194-216)
-			int yCalc = levelYSpeed * scaledInputY - (windEffectY >> 1);
-			int yDelta = yCalc >> 10;
-			if (yDelta < -12)
-				yDelta = -12;
-			if (yDelta > 12)
-				yDelta = 12;
-			_flyShipScreenY -= (int16)yDelta;
+	} else {
+		// Mode 0/2/3: Reduced cross-axis coupling (lines 218-230).
+		if (scaledInputX < 1) {
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity - (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
 		} else {
-			// Mode 0/2/3: unclamped (lines 238-241)
-			_flyShipScreenY -= (int16)((levelYSpeed * scaledInputY) >> 10);
-		}
-
-		// Store vertical input for direction sprite (line 243)
-		_verticalInput = scaledInputY;  // DAT_0044370e
-
-		// Ship facing direction (line 244)
-		_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
-
-		// --- Step 6: Position clamping (lines 245-256) ---
-		if (_flyShipScreenX > 0x194)
-			_flyShipScreenX = 0x194;  // 404
-		if (_flyShipScreenY > 0xF0)
-			_flyShipScreenY = 0xF0;    // 240
-		if (_flyShipScreenX < 0x14)
-			_flyShipScreenX = 0x14;    // 20
-		if (_flyShipScreenY < 0x14)
-			_flyShipScreenY = 0x14;    // 20
-
-		// --- Step 7: Corridor collision — mode 0/2 only (lines 257-292) ---
-		if (_flyControlMode == 0 || _flyControlMode == 2) {
-			LevelDifficultyParams wallParams = getDifficultyParams();
-			int corridorWallDmg = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
-
-			// Right boundary (lines 258-270)
-			// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5
-			if (_corridorRightX < _flyShipScreenX) {
-				_flyShipScreenX = _corridorRightX;
-				if (_hitCooldown < 5) {
-					for (int i = 0; i < 25; i++)
-						_velocityHistory[i] = -127;
-					_hitCooldown = 10;
-					_spaceShotDirection = 1;
-					initDamageFlash();
-					if (!_rebelInvulnerable) {
-						_playerDamage += corridorWallDmg;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-					}
-					_rebelHitCounter++;
-					playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right
-				}
-			}
-			// Left boundary (lines 271-283)
-			if (_flyShipScreenX < _corridorLeftX) {
-				_flyShipScreenX = _corridorLeftX;
-				if (_hitCooldown < 5) {
-					for (int i = 0; i < 25; i++)
-						_velocityHistory[i] = 127;
-					_hitCooldown = 10;
-					_spaceShotDirection = 0;
-					initDamageFlash();
-					if (!_rebelInvulnerable) {
-						_playerDamage += corridorWallDmg;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-					}
-					_rebelHitCounter++;
-					playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left
-				}
-			}
-			// Y boundary clamping — no damage (lines 285-292)
-			if (_corridorBottomY < _flyShipScreenY) {
-				_flyShipScreenY = _corridorBottomY;
-			}
-			if (_flyShipScreenY < _corridorTopY) {
-				_flyShipScreenY = _corridorTopY;
-			}
+			positionDeltaX = (int16)((levelSpeed * _smoothedVelocity + (absSmoothVel * scaledInputY >> 2) - windEffectX) >> 9);
 		}
+	}
 
-		// --- Step 8: Perspective offsets (lines 293-316) ---
-		// f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
-		// Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d
-		// Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19
-		{
-			int absOffX = ABS(_flyShipScreenX - 0xd4);
-			int16 focalX = 0x2b;  // Far view default for Level 3
-			if (absOffX > 0) {
-				_perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
-					((0xd4 - focalX) * absOffX + focalX * 0xd4));
-			} else {
-				_perspectiveX = 0;
-			}
-			if (_flyShipScreenX < 0xd5)
-				_perspectiveX = -_perspectiveX;
-
-			int absOffY = ABS(_flyShipScreenY - 0x82);
-			int16 focalY = 0x19;  // Far view default for Level 3
-			if (absOffY > 0) {
-				_perspectiveY = (int16)((focalY * 0x82 * absOffY) /
-					((0x82 - focalY) * absOffY + focalY * 0x82));
-			} else {
-				_perspectiveY = 0;
-			}
-			if (_flyShipScreenY < 0x83)
-				_perspectiveY = -_perspectiveY;
-		}
-
-		// View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74)
-		_viewShift = _smoothedVelocity;
-		if (_viewShift > 127)
-			_viewShift = 127;
-		if (_viewShift < -127)
-			_viewShift = -127;
-
-		// --- Step 9: Direction sprite (FUN_0040d836 lines 88-106) ---
-		// 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34)
-		// vDir from vertical input: (0xa0 - verticalInput) >> 6
-		int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
-		if (vDir < 0)
-			vDir = 0;
-		if (vDir > 4)
-			vDir = 4;
-
-		// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b
-		int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
-		if (hDir < 0)
-			hDir = 0;
-		if (hDir > 6)
-			hDir = 6;
-
-		// Hysteresis at center (lines 90-97, 98-105)
-		if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
-			hDir = (_smoothedVelocity < 1) ? 4 : 2;
-		}
-		if (vDir == 2 && ABS(_verticalInput) > 15) {
-			vDir = (_verticalInput < 1) ? 3 : 1;
-		}
-
-		_shipDirectionIndex = vDir * 7 + hDir;
-		if (_shipDirectionIndex < 0)
-			_shipDirectionIndex = 0;
-		if (_shipDirectionIndex > 34)
-			_shipDirectionIndex = 34;
-
-		_shipFiring = (_flyControlMode == 2) &&
-			((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
-			 _vm->getActionState(kScummActionInsaneAttack));
-
-		debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
-			_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
-			_verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
+	// Clamp X delta to +/-12 per frame (lines 187-192 / 231-236).
+	if (positionDeltaX < -11)
+		positionDeltaX = -12;
+	if (positionDeltaX > 11)
+		positionDeltaX = 12;
 
-		return;
-	}
+	// Apply X delta (line 193 / 237).
+	_flyShipScreenX += positionDeltaX;
 
-	// Handler 25 (0x19) specific logic (mixed mode - speeder bike)
-	// Based on FUN_0041cadb case 4 (opcode 6) lines 113-229
-	if (_rebelHandler == 25) {
-		// Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code)
-		// The stream position should be at offset 8 after par4 was read
-		// From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
-		int16 par5 = 0;
-		if (b.pos() + 2 <= b.size()) {
-			int64 savedPos = b.pos();
-			par5 = b.readSint16LE();
-			b.seek(savedPos);  // Don't consume the stream
-		}
-
-		// If par5 == 1, enable status bar and reset state (lines 114-121)
-		// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4)
-		if (par5 == 1) {
-			_rebelStatusBarSprite = 5;
-			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
-				initLaserTexture(_smush_iconsNut, 5);
-			}
-			// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
-			if (!_rebelOp6Initialized) {
-				clearBit(0);
-				for (int i = 0; i < 512; i++) {
-					_rebelLinks[i][0] = 0;
-					_rebelLinks[i][1] = 0;
-					_rebelLinks[i][2] = 0;
+	// Y delta.
+	if (_flyControlMode == 1) {
+		// Mode 1: clamped to +/-12 with wind (lines 194-216).
+		int yCalc = levelYSpeed * scaledInputY - (windEffectY >> 1);
+		int yDelta = yCalc >> 10;
+		if (yDelta < -12)
+			yDelta = -12;
+		if (yDelta > 12)
+			yDelta = 12;
+		_flyShipScreenY -= (int16)yDelta;
+	} else {
+		// Mode 0/2/3: unclamped (lines 238-241).
+		_flyShipScreenY -= (int16)((levelYSpeed * scaledInputY) >> 10);
+	}
+
+	// Store vertical input for direction sprite (line 243).
+	_verticalInput = scaledInputY;  // DAT_0044370e
+
+	// Ship facing direction (line 244).
+	_facingRight = (0xd4 < _smoothedVelocity + _flyShipScreenX);
+
+	// Step 6: Position clamping (lines 245-256).
+	if (_flyShipScreenX > 0x194)
+		_flyShipScreenX = 0x194;  // 404
+	if (_flyShipScreenY > 0xF0)
+		_flyShipScreenY = 0xF0;    // 240
+	if (_flyShipScreenX < 0x14)
+		_flyShipScreenX = 0x14;    // 20
+	if (_flyShipScreenY < 0x14)
+		_flyShipScreenY = 0x14;    // 20
+
+	// Step 7: Corridor collision - mode 0/2 only (lines 257-292).
+	if (_flyControlMode == 0 || _flyControlMode == 2) {
+		LevelDifficultyParams wallParams = getDifficultyParams();
+		int corridorWallDmg = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
+
+		// Right boundary (lines 258-270).
+		// Original: position is ALWAYS clamped; damage/bounce only when cooldown < 5.
+		if (_corridorRightX < _flyShipScreenX) {
+			_flyShipScreenX = _corridorRightX;
+			if (_hitCooldown < 5) {
+				for (int i = 0; i < 25; i++)
+					_velocityHistory[i] = -127;
+				_hitCooldown = 10;
+				_spaceShotDirection = 1;
+				initDamageFlash();
+				if (!_rebelInvulnerable) {
+					_playerDamage += corridorWallDmg;
+					if (_playerDamage > 255)
+						_playerDamage = 255;
 				}
-				_rebelWaveState = _rebelPhaseState;
-				_rebelOp6Initialized = true;
-				debug("Rebel2 Opcode 6 (Handler 25): Wave init, wave=0x%x autopilot=%d damageLevel=%d",
-					_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
+				_rebelHitCounter++;
+				playSfx(1, 127, 100);  // CRASH.SAD, right wall, pan right
 			}
 		}
 
-		// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render
-		// From FUN_0041cadb line 122: DAT_00457900 = local_14[3];
-		// In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4
-		// Mode 1: Uncovered, shooting position - sprite on left
-		// Mode 2: Covered, vertical shift
-		// Mode 3: Transition between covered/uncovered - sprite position depends on direction
-		// Mode 4: Alternative uncovered position - sprite on right
-		_grdSpriteMode = par4;  // local_14[3] maps to par4 (offset 6-7)
-
-		debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
-			par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
-
-		// Autopilot logic (lines 123-146)
-		// From original FUN_0041cadb - NO damageLevel check, toggle happens immediately
-		// The damage level counter provides the smooth visual transition
-		if (!_rebelInvulnerable) {
-			if (_rebelAutopilot == 0) {
-				// Uncovered: RIGHT button enters cover
-				if ((_rebelControlMode & 2) != 0) {
-					_rebelAutopilot = 1;
-					debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
-				}
-			} else {
-				// Covered: ANY button exits cover
-				if (_rebelControlMode != 0) {
-					_rebelAutopilot = 0;
-					debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
+		// Left boundary (lines 271-283).
+		if (_flyShipScreenX < _corridorLeftX) {
+			_flyShipScreenX = _corridorLeftX;
+			if (_hitCooldown < 5) {
+				for (int i = 0; i < 25; i++)
+					_velocityHistory[i] = 127;
+				_hitCooldown = 10;
+				_spaceShotDirection = 0;
+				initDamageFlash();
+				if (!_rebelInvulnerable) {
+					_playerDamage += corridorWallDmg;
+					if (_playerDamage > 255)
+						_playerDamage = 255;
 				}
+				_rebelHitCounter++;
+				playSfx(1, 127, -100);  // CRASH.SAD, left wall, pan left
 			}
-			// Clear control mode after processing (sticky flags consumed)
-			_rebelControlMode = 0;
+		}
+
+		// Y boundary clamping - no damage (lines 285-292).
+		if (_corridorBottomY < _flyShipScreenY) {
+			_flyShipScreenY = _corridorBottomY;
+		}
+		if (_flyShipScreenY < _corridorTopY) {
+			_flyShipScreenY = _corridorTopY;
+		}
+	}
+
+	// Step 8: Perspective offsets (lines 293-316).
+	// f(x) = (focal * center * |offset|) / ((center - focal) * |offset| + focal * center)
+	// Close view (DAT_0047a7fc < 1): focalX=0x34, focalY=0x2d.
+	// Far view (DAT_0047a7fc >= 1): focalX=0x2b, focalY=0x19.
+	{
+		int absOffX = ABS(_flyShipScreenX - 0xd4);
+		int16 focalX = 0x2b;  // Far view default for Level 3
+		if (absOffX > 0) {
+			_perspectiveX = (int16)((focalX * 0xd4 * absOffX) /
+				((0xd4 - focalX) * absOffX + focalX * 0xd4));
 		} else {
-			// Invulnerable mode: random autopilot changes
-			if (_rebelAutopilot == 0) {
-				if (_vm->_rnd.getRandomNumber(100) == 0) {
-					_rebelAutopilot = 1;
-				}
-			} else {
-				if (_vm->_rnd.getRandomNumber(15) == 0) {
-					_rebelAutopilot = 0;
-					_rebelFlightDir = _vm->_rnd.getRandomNumber(2);
-				}
+			_perspectiveX = 0;
+		}
+		if (_flyShipScreenX < 0xd5)
+			_perspectiveX = -_perspectiveX;
+
+		int absOffY = ABS(_flyShipScreenY - 0x82);
+		int16 focalY = 0x19;  // Far view default for Level 3
+		if (absOffY > 0) {
+			_perspectiveY = (int16)((focalY * 0x82 * absOffY) /
+				((0x82 - focalY) * absOffY + focalY * 0x82));
+		} else {
+			_perspectiveY = 0;
+		}
+		if (_flyShipScreenY < 0x83)
+			_perspectiveY = -_perspectiveY;
+	}
+
+	// View shift = clamped smoothed velocity (FUN_0040d836 lines 68-74).
+	_viewShift = _smoothedVelocity;
+	if (_viewShift > 127)
+		_viewShift = 127;
+	if (_viewShift < -127)
+		_viewShift = -127;
+
+	// Step 9: Direction sprite (FUN_0040d836 lines 88-106).
+	// 5x7 grid: vDir(0-4) * 7 + hDir(0-6) = sprite index (0-34).
+	// vDir from vertical input: (0xa0 - verticalInput) >> 6.
+	int16 vDir = (int16)(((int)(0xa0 - _verticalInput) + ((0xa0 - _verticalInput) < 0 ? 63 : 0)) >> 6);
+	if (vDir < 0)
+		vDir = 0;
+	if (vDir > 4)
+		vDir = 4;
+
+	// hDir from smoothed velocity: (0x95 - smoothedVelocity) / 0x2b.
+	int16 hDir = (int16)((0x95 - _smoothedVelocity) / 0x2b);
+	if (hDir < 0)
+		hDir = 0;
+	if (hDir > 6)
+		hDir = 6;
+
+	// Hysteresis at center (lines 90-97, 98-105).
+	if (hDir == 3 && ABS(_smoothedVelocity) > 10) {
+		hDir = (_smoothedVelocity < 1) ? 4 : 2;
+	}
+	if (vDir == 2 && ABS(_verticalInput) > 15) {
+		vDir = (_verticalInput < 1) ? 3 : 1;
+	}
+
+	_shipDirectionIndex = vDir * 7 + hDir;
+	if (_shipDirectionIndex < 0)
+		_shipDirectionIndex = 0;
+	if (_shipDirectionIndex > 34)
+		_shipDirectionIndex = 34;
+
+	_shipFiring = (_flyControlMode == 2) &&
+		((_vm->VAR(_vm->VAR_LEFTBTN_HOLD) != 0) ||
+		 _vm->getActionState(kScummActionInsaneAttack));
+
+	debug("Rebel2 H7: pos=(%d,%d) vel=%d vIn=%d dx=%d dir=%d mode=%d",
+		_flyShipScreenX, _flyShipScreenY, _smoothedVelocity,
+		_verticalInput, positionDeltaX, _shipDirectionIndex, _flyControlMode);
+}
+
+// ScummVM refactor helper for opcode 6 Handler 25, not a separate retail function.
+void InsaneRebel2::handleOpcode6Handler25(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4) {
+	// Handler 25 (0x19) specific logic (mixed mode - speeder bike).
+	// Based on FUN_0041cadb case 4 (opcode 6) lines 113-229.
+
+	// Read the reset flag from IACT data at offset 8-9 (local_14[4] in decompiled code).
+	// The stream position should be at offset 8 after par4 was read.
+	// From FUN_0041cadb line 114: if (local_14[4] == 1) { ... reset ... }
+	int16 par5 = 0;
+	if (b.pos() + 2 <= b.size()) {
+		int64 savedPos = b.pos();
+		par5 = b.readSint16LE();
+		b.seek(savedPos);  // Don't consume the stream
+	}
+
+	// If par5 == 1, enable status bar and reset state (lines 114-121).
+	// Note: This is local_14[4] in the decompiled code, NOT local_14[3] (par4).
+	if (par5 == 1) {
+		_rebelStatusBarSprite = 5;
+		if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
+			initLaserTexture(_smush_iconsNut, 5);
+		}
+
+		// Guard with _rebelOp6Initialized: runs once per wave video, not per frame.
+		if (!_rebelOp6Initialized) {
+			clearBit(0);
+			for (int i = 0; i < 512; i++) {
+				_rebelLinks[i][0] = 0;
+				_rebelLinks[i][1] = 0;
+				_rebelLinks[i][2] = 0;
 			}
+			_rebelWaveState = _rebelPhaseState;
+			_rebelOp6Initialized = true;
+			debug("Rebel2 Opcode 6 (Handler 25): Wave init, wave=0x%x autopilot=%d damageLevel=%d",
+				_rebelWaveState, _rebelAutopilot, _rebelDamageLevel);
 		}
+	}
 
-		// Update damage level counter (lines 147-154)
-		// This provides the smooth transition animation between covered/uncovered states
-		int prevDamageLevel = _rebelDamageLevel;
+	// Set sprite mode (DAT_00457900 = local_14[3]) - controls which GRD sprite to render.
+	// From FUN_0041cadb line 122: DAT_00457900 = local_14[3].
+	// In ScummVM's IACT parsing: local_14[3] = offset 6-7 = par4.
+	// Mode 1: Uncovered, shooting position - sprite on left
+	// Mode 2: Covered, vertical shift
+	// Mode 3: Transition between covered/uncovered - sprite position depends on direction
+	// Mode 4: Alternative uncovered position - sprite on right
+	_grdSpriteMode = par4;  // local_14[3] maps to par4 (offset 6-7)
+
+	debug("Rebel2 Handler25 Opcode6: par2=%d par3=%d par4=%d(mode) par5=%d(reset) autopilot=%d damageLevel=%d controlMode=%d",
+		par2, par3, par4, par5, _rebelAutopilot, _rebelDamageLevel, _rebelControlMode);
+
+	// Autopilot logic (lines 123-146).
+	// From original FUN_0041cadb - NO damageLevel check, toggle happens immediately.
+	// The damage level counter provides the smooth visual transition.
+	if (!_rebelInvulnerable) {
 		if (_rebelAutopilot == 0) {
-			// Uncovered: decrement damage level towards 0
-			if (_rebelDamageLevel > 0) {
-				_rebelDamageLevel--;
+			// Uncovered: RIGHT button enters cover.
+			if ((_rebelControlMode & 2) != 0) {
+				_rebelAutopilot = 1;
+				debug("Rebel2 Handler25: Entering cover (right click), controlMode=%d", _rebelControlMode);
 			}
 		} else {
-			// Covered: increment damage level towards 5
-			if (_rebelDamageLevel < 5) {
-				_rebelDamageLevel++;
+			// Covered: ANY button exits cover.
+			if (_rebelControlMode != 0) {
+				_rebelAutopilot = 0;
+				debug("Rebel2 Handler25: Exiting cover (button click), controlMode=%d", _rebelControlMode);
 			}
 		}
-		if (_rebelDamageLevel != prevDamageLevel) {
-			debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
-				prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
-		}
 
-		// Flight direction logic for mode 3 (lines 155-177)
-		if (_grdSpriteMode == 3) {
-			if (_rebelDamageLevel == 5) {
-				// At max damage, check for direction change input
-				int16 mouseX = getGameplayAimPoint().x;
-				if (_player && _player->_width > 320) {
-					mouseX = (mouseX * 320) / _player->_width;
-				}
-				if (mouseX > 235) {  // 0x4b + 160 = 235
-					_rebelFlightDir = 1;
-				}
-				if (mouseX < 85) {   // 160 - 0x4b = 85
-					_rebelFlightDir = 0;
-				}
+		// Clear control mode after processing (sticky flags consumed).
+		_rebelControlMode = 0;
+	} else {
+		// Invulnerable mode: random autopilot changes.
+		if (_rebelAutopilot == 0) {
+			if (_vm->_rnd.getRandomNumber(100) == 0) {
+				_rebelAutopilot = 1;
 			}
 		} else {
-			_rebelFlightDir = 0;
-		}
-
-		// Calculate sprite and view offset positions based on mode (lines 182-213)
-		// DAT_0045790c = view offset X (for corridor overlay)
-		// DAT_0045790e = view offset Y (for corridor overlay)
-		// DAT_00457910 = sprite position X (relative to center)
-		// DAT_00457912 = sprite position Y (relative to center)
-		if (_grdSpriteMode == 1) {
-			// Mode 1: Uncovered, shooting - sprite shifts left as damage increases
-			_rebelViewMode1 = 0x0e;
-			_rebelViewMode2 = 0;
-			_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;   // DAT_0045790c
-			_rebelViewOffset2X = _rebelDamageLevel * -22;       // DAT_00457910
-			_rebelViewOffsetY = 0;                              // DAT_0045790e
-			_rebelViewOffset2Y = 0;                             // DAT_00457912
-		} else if (_grdSpriteMode == 4) {
-			// Mode 4: Alternative uncovered - sprite shifts right
-			_rebelViewMode1 = 0x22;
-			_rebelViewMode2 = 0;
-			_rebelViewOffsetX = _rebelDamageLevel * 10 + -16;   // DAT_0045790c
-			_rebelViewOffset2X = _rebelDamageLevel * 17 + -85;  // DAT_00457910 (0x11 = 17, -0x55 = -85)
-			_rebelViewOffsetY = 0;
-			_rebelViewOffset2Y = 0;
-		} else if (_grdSpriteMode == 2) {
-			// Mode 2: Covered - vertical shift
-			_rebelViewMode1 = 0;
-			_rebelViewMode2 = 0x0e;
-			_rebelViewOffsetY = _rebelDamageLevel * -5 + -14;   // DAT_0045790e
-			_rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60;  // DAT_00457912 (0xf = 15, -0x3c = -60)
-			_rebelViewOffsetX = 0;
-			_rebelViewOffset2X = 0;
-		} else if (_grdSpriteMode == 3) {
-			// Mode 3: Transition - direction-dependent horizontal shift
-			_rebelViewMode1 = 0x0f;
-			_rebelViewMode2 = 0;
-			// (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
-			int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
-			// (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
-			int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
-			_rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15;  // DAT_0045790c
-			_rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel);      // DAT_00457910
-			_rebelViewOffsetY = 0;
-			_rebelViewOffset2Y = 0;
-		} else {
-			// Mode 0 or unknown: use Mode 1 defaults as fallback
-			_rebelViewMode1 = 0x0e;
-			_rebelViewMode2 = 0;
-			_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
-			_rebelViewOffset2X = _rebelDamageLevel * -22;
-			_rebelViewOffsetY = 0;
-			_rebelViewOffset2Y = 0;
-			debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
+			if (_vm->_rnd.getRandomNumber(15) == 0) {
+				_rebelAutopilot = 0;
+				_rebelFlightDir = _vm->_rnd.getRandomNumber(2);
+			}
 		}
+	}
 
-		debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
-			_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
-			_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+	// Update damage level counter (lines 147-154).
+	// This provides the smooth transition animation between covered/uncovered states.
+	int prevDamageLevel = _rebelDamageLevel;
+	if (_rebelAutopilot == 0) {
+		// Uncovered: decrement damage level towards 0.
+		if (_rebelDamageLevel > 0) {
+			_rebelDamageLevel--;
+		}
+	} else {
+		// Covered: increment damage level towards 5.
+		if (_rebelDamageLevel < 5) {
+			_rebelDamageLevel++;
+		}
+	}
+	if (_rebelDamageLevel != prevDamageLevel) {
+		debug("Rebel2 Handler25: damageLevel transition %d -> %d (autopilot=%d)",
+			prevDamageLevel, _rebelDamageLevel, _rebelAutopilot);
+	}
 
-		// Set FOBJ position offsets (FUN_00424510 in original, line 214)
-		// All subsequent FOBJs in this frame will be shifted by these offsets
-		if (_player) {
-			_player->_fobjOffsetX = _rebelViewOffsetX;
-			_player->_fobjOffsetY = _rebelViewOffsetY;
+	// Flight direction logic for mode 3 (lines 155-177).
+	if (_grdSpriteMode == 3) {
+		if (_rebelDamageLevel == 5) {
+			// At max damage, check for direction change input.
+			int16 mouseX = getGameplayAimPoint().x;
+			if (_player && _player->_width > 320) {
+				mouseX = (mouseX * 320) / _player->_width;
+			}
+			if (mouseX > 235) {  // 0x4b + 160 = 235
+				_rebelFlightDir = 1;
+			}
+			if (mouseX < 85) {   // 160 - 0x4b = 85
+				_rebelFlightDir = 0;
+			}
 		}
+	} else {
+		_rebelFlightDir = 0;
+	}
 
-		// Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216)
-		// This wipes previous frame content so codec 23 delta skip regions show clean corridor
-		if (renderBitmap) {
-			EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
-			if (corridorOverlay.valid && corridorOverlay.pixels) {
-				int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
-				int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
-
-				int srcOffsetX = 0;
-				int srcOffsetY = 0;
-				int destX = _rebelViewOffsetX;
-				int destY = _rebelViewOffsetY;
-				int drawWidth = corridorOverlay.width;
-				int drawHeight = corridorOverlay.height;
-
-				if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
-				if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
-				if (destX + drawWidth > pitch)
-					drawWidth = pitch - destX;
-				if (destY + drawHeight > bufHeight)
-					drawHeight = bufHeight - destY;
-				if (drawWidth > corridorOverlay.width - srcOffsetX)
-					drawWidth = corridorOverlay.width - srcOffsetX;
-				if (drawHeight > corridorOverlay.height - srcOffsetY)
-					drawHeight = corridorOverlay.height - srcOffsetY;
-
-				if (drawWidth > 0 && drawHeight > 0) {
-					for (int y = 0; y < drawHeight; y++) {
-						memcpy(renderBitmap + (destY + y) * pitch + destX,
-							   corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
-							   drawWidth);
-					}
+	// Calculate sprite and view offset positions based on mode (lines 182-213).
+	// DAT_0045790c = view offset X (for corridor overlay)
+	// DAT_0045790e = view offset Y (for corridor overlay)
+	// DAT_00457910 = sprite position X (relative to center)
+	// DAT_00457912 = sprite position Y (relative to center)
+	if (_grdSpriteMode == 1) {
+		// Mode 1: Uncovered, shooting - sprite shifts left as damage increases.
+		_rebelViewMode1 = 0x0e;
+		_rebelViewMode2 = 0;
+		_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;   // DAT_0045790c
+		_rebelViewOffset2X = _rebelDamageLevel * -22;       // DAT_00457910
+		_rebelViewOffsetY = 0;                              // DAT_0045790e
+		_rebelViewOffset2Y = 0;                             // DAT_00457912
+	} else if (_grdSpriteMode == 4) {
+		// Mode 4: Alternative uncovered - sprite shifts right.
+		_rebelViewMode1 = 0x22;
+		_rebelViewMode2 = 0;
+		_rebelViewOffsetX = _rebelDamageLevel * 10 + -16;   // DAT_0045790c
+		_rebelViewOffset2X = _rebelDamageLevel * 17 + -85;  // DAT_00457910 (0x11 = 17, -0x55 = -85)
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2Y = 0;
+	} else if (_grdSpriteMode == 2) {
+		// Mode 2: Covered - vertical shift.
+		_rebelViewMode1 = 0;
+		_rebelViewMode2 = 0x0e;
+		_rebelViewOffsetY = _rebelDamageLevel * -5 + -14;   // DAT_0045790e
+		_rebelViewOffset2Y = (5 - _rebelDamageLevel) * 15 + -60;  // DAT_00457912 (0xf = 15, -0x3c = -60)
+		_rebelViewOffsetX = 0;
+		_rebelViewOffset2X = 0;
+	} else if (_grdSpriteMode == 3) {
+		// Mode 3: Transition - direction-dependent horizontal shift.
+		_rebelViewMode1 = 0x0f;
+		_rebelViewMode2 = 0;
+		// (-(DAT_00457902 == 0) & 6) - 3 = if dir==0: 6-3=3, else 0-3=-3
+		int16 dirMultX = (_rebelFlightDir == 0) ? 3 : -3;
+		// (-(DAT_00457902 == 0) & 0x28) - 0x14 = if dir==0: 40-20=20, else 0-20=-20
+		int16 dirMultX2 = (_rebelFlightDir == 0) ? 20 : -20;
+		_rebelViewOffsetX = dirMultX * (5 - _rebelDamageLevel) + -15;  // DAT_0045790c
+		_rebelViewOffset2X = dirMultX2 * (5 - _rebelDamageLevel);      // DAT_00457910
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2Y = 0;
+	} else {
+		// Mode 0 or unknown: use Mode 1 defaults as fallback.
+		_rebelViewMode1 = 0x0e;
+		_rebelViewMode2 = 0;
+		_rebelViewOffsetX = _rebelDamageLevel * -5 + -14;
+		_rebelViewOffset2X = _rebelDamageLevel * -22;
+		_rebelViewOffsetY = 0;
+		_rebelViewOffset2Y = 0;
+		debug("Rebel2 Opcode 6 (Handler 25): Unknown mode %d, using Mode 1 fallback", _grdSpriteMode);
+	}
+
+	debug("Rebel2 Opcode 6 (Handler 25): mode=%d damage=%d dir=%d autopilot=%d viewOff=(%d,%d) spritePos=(%d,%d)",
+		_grdSpriteMode, _rebelDamageLevel, _rebelFlightDir, _rebelAutopilot,
+		_rebelViewOffsetX, _rebelViewOffsetY, _rebelViewOffset2X, _rebelViewOffset2Y);
+
+	// Set FOBJ position offsets (FUN_00424510 in original, line 214).
+	// All subsequent FOBJs in this frame will be shifted by these offsets.
+	if (_player) {
+		_player->_fobjOffsetX = _rebelViewOffsetX;
+		_player->_fobjOffsetY = _rebelViewOffsetY;
+	}
+
+	// Draw corridor overlay OPAQUELY (FUN_00428A10 in original, line 216).
+	// This wipes previous frame content so codec 23 delta skip regions show clean corridor.
+	if (renderBitmap) {
+		EmbeddedSanFrame &corridorOverlay = _rebelEmbeddedHud[4];
+		if (corridorOverlay.valid && corridorOverlay.pixels) {
+			int pitch = (_player && _player->_width > 0) ? _player->_width : 320;
+			int bufHeight = (_player && _player->_height > 0) ? _player->_height : 200;
+
+			int srcOffsetX = 0;
+			int srcOffsetY = 0;
+			int destX = _rebelViewOffsetX;
+			int destY = _rebelViewOffsetY;
+			int drawWidth = corridorOverlay.width;
+			int drawHeight = corridorOverlay.height;
+
+			if (destX < 0) { srcOffsetX = -destX; drawWidth -= srcOffsetX; destX = 0; }
+			if (destY < 0) { srcOffsetY = -destY; drawHeight -= srcOffsetY; destY = 0; }
+			if (destX + drawWidth > pitch)
+				drawWidth = pitch - destX;
+			if (destY + drawHeight > bufHeight)
+				drawHeight = bufHeight - destY;
+			if (drawWidth > corridorOverlay.width - srcOffsetX)
+				drawWidth = corridorOverlay.width - srcOffsetX;
+			if (drawHeight > corridorOverlay.height - srcOffsetY)
+				drawHeight = corridorOverlay.height - srcOffsetY;
+
+			if (drawWidth > 0 && drawHeight > 0) {
+				for (int y = 0; y < drawHeight; y++) {
+					memcpy(renderBitmap + (destY + y) * pitch + destX,
+						   corridorOverlay.pixels + (srcOffsetY + y) * corridorOverlay.width + srcOffsetX,
+						   drawWidth);
 				}
-				debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
-					_rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
 			}
+			debug("Rebel2 Opcode 6: Corridor overlay drawn at (%d,%d) size(%d,%d)",
+				_rebelViewOffsetX, _rebelViewOffsetY, corridorOverlay.width, corridorOverlay.height);
 		}
+	}
+}
 
-		return;
+// ScummVM refactor helper for opcode 6 Handler 0x26, not a separate retail function.
+void InsaneRebel2::handleOpcode6Turret(Common::SeekableReadStream &b, int16 par4) {
+	// Handler 0x26: FUN_407FCB line 77-79 - set level type from par4, read par5 for init trigger.
+	// param_5[3] = par4 = levelType, param_5[4] = par5 = init flag.
+	_rebelLevelType = par4;
+
+	// Read par5 from IACT body (param_5[4]).
+	int16 par5 = 0;
+	if (b.pos() + 2 <= b.size()) {
+		int64 savedPos = b.pos();
+		par5 = b.readSint16LE();
+		b.seek(savedPos);
 	}
 
-	// Handler 0x26: FUN_407FCB line 77-79 — set level type from par4, read par5 for init trigger
-	// param_5[3] = par4 = levelType, param_5[4] = par5 = init flag
-	if (_rebelHandler == 0x26) {
-		_rebelLevelType = par4;
-
-		// Read par5 from IACT body (param_5[4])
-		int16 par5 = 0;
-		if (b.pos() + 2 <= b.size()) {
-			int64 savedPos = b.pos();
-			par5 = b.readSint16LE();
-			b.seek(savedPos);
-		}
-
-		if (par5 == 1) {
-			// Re-render laser texture for this level (FUN_0040bb87)
-			// levelType 5 uses sprite 53, all others use sprite 5
-			_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
-			if (_smush_iconsNut && _smush_iconsNut->getNumChars() > _rebelStatusBarSprite) {
-				initLaserTexture(_smush_iconsNut, _rebelStatusBarSprite);
-			}
+	if (par5 == 1) {
+		// Re-render laser texture for this level (FUN_0040bb87).
+		// levelType 5 uses sprite 53, all others use sprite 5.
+		_rebelStatusBarSprite = (_rebelLevelType == 5) ? 53 : 5;
+		if (_smush_iconsNut && _smush_iconsNut->getNumChars() > _rebelStatusBarSprite) {
+			initLaserTexture(_smush_iconsNut, _rebelStatusBarSprite);
+		}
 
-			if (!_rebelOp6Initialized) {
-				clearBit(0);
-				for (int i = 0; i < 512; i++) {
-					_rebelLinks[i][0] = 0;
-					_rebelLinks[i][1] = 0;
-					_rebelLinks[i][2] = 0;
-				}
-				_rebelWaveState = _rebelPhaseState;
-				_rebelHitCounter = 0;
-				_rebelOp6Initialized = true;
-				debug("Rebel2 Opcode 6 (Handler 0x26): Wave init, levelType=%d waveState=0x%x",
-					_rebelLevelType, _rebelWaveState);
+		if (!_rebelOp6Initialized) {
+			clearBit(0);
+			for (int i = 0; i < 512; i++) {
+				_rebelLinks[i][0] = 0;
+				_rebelLinks[i][1] = 0;
+				_rebelLinks[i][2] = 0;
 			}
+			_rebelWaveState = _rebelPhaseState;
+			_rebelHitCounter = 0;
+			_rebelOp6Initialized = true;
+			debug("Rebel2 Opcode 6 (Handler 0x26): Wave init, levelType=%d waveState=0x%x",
+				_rebelLevelType, _rebelWaveState);
 		}
 	}
+}
 
-	// Other handlers: par4 == 1 triggers init (NOT level type)
+// ScummVM refactor helper for opcode 6 generic init, not a separate retail function.
+void InsaneRebel2::handleOpcode6GenericInit(int16 par4) {
+	// Other handlers: par4 == 1 triggers init (NOT level type).
 	if (_rebelHandler != 0x26 && par4 == 1) {
 		_rebelStatusBarSprite = 5;
 		if (_smush_iconsNut && _smush_iconsNut->getNumChars() > 5) {
@@ -1352,11 +1335,14 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			debug("Rebel2 Opcode 6: Wave init - cleared bits/links, waveState=0x%x", _rebelWaveState);
 		}
 	}
+}
 
+// ScummVM refactor helper for opcode 6 generic flight state, not a separate retail function.
+void InsaneRebel2::updateOpcode6GenericFlightState() {
 	// Step 3: Autopilot/control mode logic (lines 123-146)
-	// This determines whether the ship flies on autopilot or manual control
+	// This determines whether the ship flies on autopilot or manual control.
 	if (!_rebelInvulnerable) {
-		// Normal mode: check control mode flags
+		// Normal mode: check control mode flags.
 		if (_rebelAutopilot == 0) {
 			if ((_rebelControlMode & 2) != 0) {
 				_rebelAutopilot = 1;
@@ -1367,7 +1353,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 			}
 		}
 	} else {
-		// Invulnerable mode: random autopilot changes
+		// Invulnerable mode: random autopilot changes.
 		if (_rebelAutopilot == 0) {
 			if (_vm->_rnd.getRandomNumber(100) == 0) {
 				_rebelAutopilot = 1;
@@ -1380,7 +1366,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// Step 4: Update damage level counter (lines 147-154)
+	// Step 4: Update damage level counter (lines 147-154).
 	if (_rebelAutopilot == 0) {
 		if (_rebelDamageLevel > 0) {
 			_rebelDamageLevel--;
@@ -1391,11 +1377,11 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		}
 	}
 
-	// Handle level type 3 special direction logic (lines 155-181)
+	// Handle level type 3 special direction logic (lines 155-181).
 	if (_rebelLevelType == 3) {
 		if (_rebelDamageLevel == 5) {
-			// Check for joystick/key input to change direction
-			// Simplified: use mouse position
+			// Check for joystick/key input to change direction.
+			// Simplified: use mouse position.
 			int16 mouseX = getGameplayAimPoint().x;
 			if (mouseX > 235) {
 				_rebelFlightDir = 1;
@@ -1408,10 +1394,10 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		_rebelFlightDir = 0;
 	}
 
-	// Step 5: Calculate view offsets based on level type (lines 182-213)
+	// Step 5: Calculate view offsets based on level type (lines 182-213).
 	switch (_rebelLevelType) {
 	case 1:
-		// Type 1: Vertical movement
+		// Type 1: Vertical movement.
 		_rebelViewMode1 = 0x0e;
 		_rebelViewMode2 = 0;
 		_rebelViewOffsetX = _rebelDamageLevel * -5 - 0x0e;
@@ -1421,7 +1407,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		break;
 
 	case 4:
-		// Type 4: Different vertical movement
+		// Type 4: Different vertical movement.
 		_rebelViewMode1 = 0x22;
 		_rebelViewMode2 = 0;
 		_rebelViewOffsetX = _rebelDamageLevel * 10 - 0x10;
@@ -1431,7 +1417,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		break;
 
 	case 2:
-		// Type 2: Horizontal movement
+		// Type 2: Horizontal movement.
 		_rebelViewMode1 = 0;
 		_rebelViewMode2 = 0x0e;
 		_rebelViewOffsetY = _rebelDamageLevel * -5 - 0x0e;
@@ -1441,7 +1427,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		break;
 
 	case 3:
-		// Type 3: Direction-based movement
+		// Type 3: Direction-based movement.
 		_rebelViewMode1 = 0x0f;
 		_rebelViewMode2 = 0;
 		{
@@ -1455,7 +1441,7 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 		break;
 
 	default:
-		// Default: No special offsets
+		// Default: No special offsets.
 		_rebelViewMode1 = 0;
 		_rebelViewMode2 = 0;
 		_rebelViewOffsetX = 0;
@@ -1467,45 +1453,96 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 
 	debug("Rebel2 Opcode 6: levelType=%d autopilot=%d damageLevel=%d viewOffset=(%d,%d)",
 		_rebelLevelType, _rebelAutopilot, _rebelDamageLevel, _rebelViewOffsetX, _rebelViewOffsetY);
+}
 
-	// Detect and load embedded ANIM (SAN) within the remaining IACT payload
-	// Note: chunkSize is the remaining IACT payload size after par1-par4 header
-	{
-		int64 startPos = b.pos();
-		// Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream)
-		int64 remaining = chunkSize;
-		if (remaining > 0) {
-			int scanSize = (int)MIN<int64>(remaining, 65536);
-			byte *scanBuf = (byte *)malloc(scanSize);
-			if (scanBuf) {
-				int bytesRead = b.read(scanBuf, scanSize);
-				for (int i = 0; i + 8 <= bytesRead; ++i) {
-					if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
-						int64 animStreamPos = startPos + i;
-						uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
-						// Limit to remaining IACT payload (chunkSize - offset into payload)
-						int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
-						if (toCopy > 0) {
-							byte *animData = (byte *)malloc(toCopy);
-							if (animData) {
-								b.seek(animStreamPos);
-								b.read(animData, toCopy);
-								loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
-								free(animData);
-							}
+// ScummVM refactor helper for opcode 6 embedded ANIM scan, not a separate retail function.
+void InsaneRebel2::scanOpcode6EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par4) {
+	// Detect and load embedded ANIM (SAN) within the remaining IACT payload.
+	// Note: chunkSize is the remaining IACT payload size after par1-par4 header.
+	int64 startPos = b.pos();
+
+	// Use chunkSize (remaining IACT payload) rather than b.size() (entire FRME stream).
+	int64 remaining = chunkSize;
+	if (remaining > 0) {
+		int scanSize = (int)MIN<int64>(remaining, 65536);
+		byte *scanBuf = (byte *)malloc(scanSize);
+		if (scanBuf) {
+			int bytesRead = b.read(scanBuf, scanSize);
+			for (int i = 0; i + 8 <= bytesRead; ++i) {
+				if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
+					int64 animStreamPos = startPos + i;
+					uint32 animReportedSize = READ_BE_UINT32(scanBuf + i + 4);
+
+					// Limit to remaining IACT payload (chunkSize - offset into payload).
+					int32 toCopy = (int)MIN<int64>((int64)animReportedSize + 8, chunkSize - i);
+					if (toCopy > 0) {
+						byte *animData = (byte *)malloc(toCopy);
+						if (animData) {
+							b.seek(animStreamPos);
+							b.read(animData, toCopy);
+							loadEmbeddedSan(par4, animData, toCopy, renderBitmap);
+							free(animData);
 						}
-						b.seek(startPos);
-						free(scanBuf);
-						return;
 					}
+					b.seek(startPos);
+					free(scanBuf);
+					return;
 				}
-				b.seek(startPos);
-				free(scanBuf);
 			}
+			b.seek(startPos);
+			free(scanBuf);
 		}
 	}
 }
 
+//
+// iactRebel2Opcode6 -- Level setup / mode switch (FUN_41CADB case 4)
+//
+// Per-wave initialization: clears bit table, resets link tables, configures
+// handler mode (ship/turret/corridor), and loads collision zones. Called once
+// per wave video on frame 0.
+//
+void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
+	// Opcode 6: Level setup / mode switch
+	// Based on FUN_41CADB case 4 (switch on *local_14 - 2 == 4, meaning opcode 6)
+	//
+	// For Handler 8 (third-person on foot) - FUN_00401234 case 4:
+	// - par3 sets ship level mode (DAT_0043e000)
+	// - par4 == 1 triggers status bar display and state reset
+	// - Updates ship position based on mouse input
+	//
+	// For Handler 0x26/0x19 (turret/FPS):
+	// - Same par4 == 1 behavior
+	// - Different view offset calculations
+
+	debug("Rebel2 IACT Opcode 6: par2=%d par3=%d par4=%d", par2, par3, par4);
+
+	updateOpcode6Handler(par2);
+
+	if (_rebelHandler == 8) {
+		handleOpcode6Handler8(par3, par4);
+		return;
+	}
+
+	if (_rebelHandler == 7) {
+		handleOpcode6Handler7(b, par4);
+		return;
+	}
+
+	if (_rebelHandler == 25) {
+		handleOpcode6Handler25(renderBitmap, b, par2, par3, par4);
+		return;
+	}
+
+	if (_rebelHandler == 0x26) {
+		handleOpcode6Turret(b, par4);
+	}
+
+	handleOpcode6GenericInit(par4);
+	updateOpcode6GenericFlightState();
+	scanOpcode6EmbeddedAnim(renderBitmap, b, chunkSize, par4);
+}
+
 //
 // iactRebel2Opcode8 -- HUD/Ship resource loading (FUN_0040c3cc / FUN_00401234 / FUN_00407fcb)
 //
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 60a06d265e4..fe4813e0a62 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -505,6 +505,14 @@ public:
 	void iactRebel2Opcode2(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode3(Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
+	void updateOpcode6Handler(int16 par2);
+	void handleOpcode6Handler8(int16 par3, int16 par4);
+	void handleOpcode6Handler7(Common::SeekableReadStream &b, int16 par4);
+	void handleOpcode6Handler25(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
+	void handleOpcode6Turret(Common::SeekableReadStream &b, int16 par4);
+	void handleOpcode6GenericInit(int16 par4);
+	void updateOpcode6GenericFlightState();
+	void scanOpcode6EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par4);
 	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
 	void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 


Commit: 9092da7f1609a61fa4ae31a62af7bcdb8ce055cd
    https://github.com/scummvm/scummvm/commit/9092da7f1609a61fa4ae31a62af7bcdb8ce055cd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00

Commit Message:
SCUMM: RA2: Split opcode 8 handling

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


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 7c088bf7026..2a050339962 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1555,33 +1555,30 @@ void InsaneRebel2::iactRebel2Opcode6(byte *renderBitmap, Common::SeekableReadStr
 //   Handler 0x26 (turret): Turret HUD NUT via par3 (1-4)
 //   Handler 0x19: Mixed turret mode, similar to 0x26
 //
-void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
-	// Sound loading: par3 in range 21-47
-
-	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
-		_rebelHandler, par2, par3, par4, _gameState);
-
-	int64 startPos = b.pos();
-	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
-
-	// ----- Handler 7: FLY NUT Loading (Third-Person Ship) -----
-	// FUN_0040c3cc case 6: par4 determines FLY sprite slot
+// ScummVM refactor helper for opcode 8 Handler 7 FLY loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8Handler7FlySprites(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+	// Handler 7: FLY NUT Loading (Third-Person Ship)
+	// FUN_0040c3cc case 6: par4 determines FLY sprite slot.
 	bool isHandler7FLY = (_rebelHandler == 7 && (par4 == 1 || par4 == 2 || par4 == 3 || par4 == 11));
 	if (isHandler7FLY && remaining >= 14) {
 		if (loadHandler7FlySprites(b, remaining, par4)) {
 			b.seek(startPos);
-			return;
+			return true;
 		}
 		b.seek(startPos);
 	}
 
-	// ----- Edge Blend Table Loading (par4 == 1000) -----
+	return false;
+}
+
+// ScummVM refactor helper for opcode 8 edge table loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8EdgeTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+	// Edge Blend Table Loading (par4 == 1000)
 	// FUN_405663: After all handler-specific opcode 8 processing, checks if par4==1000.
 	// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
 	// This table controls the edge glow color of laser beams (e.g. red vs green).
 	// Data starts at byte offset 18 in the IACT chunk (in_stack_00000014 + 9 shorts).
 	if (par4 == 1000 && remaining >= 18 + 8 + 32896) {
-		// Read the raw edge table data from the stream
 		// Layout: 18 bytes IACT header params already consumed by caller,
 		// but 'b' is positioned at startPos which is after par1..par4.
 		// The original code passes (param + 9 shorts) = data at byte offset 18 from chunk start.
@@ -1595,63 +1592,75 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 			debug("Rebel2 Opcode 8: Loaded per-level edge blend table (par4=1000)");
 		}
 		b.seek(startPos);
-		return;
+		return true;
 	}
 
-	// ----- Auxiliary Sound Buffer Loading (par4 20-47) -----
-	// FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) → aux buffer 0
-	// FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) → aux buffer 0,
-	//   0x1f-0x25 (31-37) → aux buffer 1, 0x28 (40) → aux buffer 3,
-	//   0x29-0x2f (41-47) → aux buffer 2
+	return false;
+}
+
+// ScummVM refactor helper for opcode 8 aux SFX loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8AuxSfx(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+	// Auxiliary Sound Buffer Loading (par4 20-47)
+	// FUN_401234 case 6 (handler 8): par4 0x14-0x1b (20-27) -> aux buffer 0
+	// FUN_41CADB case 6 (handler 25): par4 0x15-0x1b (21-27) -> aux buffer 0,
+	//   0x1f-0x25 (31-37) -> aux buffer 1, 0x28 (40) -> aux buffer 3,
+	//   0x29-0x2f (41-47) -> aux buffer 2
 	// Data layout: offset 14 = uint32 data size, offset 18 = PCM data start.
 	// Stream is at offset 8 (after par1-par4), so data size at +6, PCM at +10.
-	if (par4 >= 20 && par4 <= 47) {
-		int auxBuffer = -1;
-		if (par4 >= 20 && par4 <= 27) {
-			auxBuffer = 0;
-		} else if (par4 >= 31 && par4 <= 37) {
-			auxBuffer = 1;
-		} else if (par4 == 40) {
-			auxBuffer = 3;
-		} else if (par4 >= 41 && par4 <= 47) {
-			auxBuffer = 2;
-		}
-
-		if (auxBuffer >= 0 && remaining >= 10) {
-			b.seek(startPos + 6); // Skip to data size field (byte offset 14 from IACT start)
-			uint32 dataSize = b.readUint32LE();
-			if (dataSize > 0 && remaining >= (int64)(10 + dataSize)) {
-				byte *soundData = (byte *)malloc(dataSize);
-				if (soundData) {
-					b.read(soundData, dataSize);
-					loadAuxSfx(auxBuffer, soundData, dataSize);
-					free(soundData);
-					debug("Rebel2 Opcode 8: Loaded %d bytes into aux sound buffer %d (par4=%d)",
-						dataSize, auxBuffer, par4);
-				}
-			} else {
-				debug("Rebel2 Opcode 8: Aux sound par4=%d dataSize=%d exceeds remaining=%lld",
-					par4, dataSize, (long long)remaining);
+	if (par4 < 20 || par4 > 47)
+		return false;
+
+	int auxBuffer = -1;
+	if (par4 >= 20 && par4 <= 27) {
+		auxBuffer = 0;
+	} else if (par4 >= 31 && par4 <= 37) {
+		auxBuffer = 1;
+	} else if (par4 == 40) {
+		auxBuffer = 3;
+	} else if (par4 >= 41 && par4 <= 47) {
+		auxBuffer = 2;
+	}
+
+	if (auxBuffer >= 0 && remaining >= 10) {
+		b.seek(startPos + 6); // Skip to data size field (byte offset 14 from IACT start)
+		uint32 dataSize = b.readUint32LE();
+		if (dataSize > 0 && remaining >= (int64)(10 + dataSize)) {
+			byte *soundData = (byte *)malloc(dataSize);
+			if (soundData) {
+				b.read(soundData, dataSize);
+				loadAuxSfx(auxBuffer, soundData, dataSize);
+				free(soundData);
+				debug("Rebel2 Opcode 8: Loaded %u bytes into aux sound buffer %d (par4=%d)",
+					dataSize, auxBuffer, par4);
 			}
+		} else {
+			debug("Rebel2 Opcode 8: Aux sound par4=%d dataSize=%u exceeds remaining=%lld",
+				par4, dataSize, (long long)remaining);
 		}
-		b.seek(startPos);
-		return;
 	}
+	b.seek(startPos);
+	return true;
+}
 
-	// ----- Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8) -----
+// ScummVM refactor helper for opcode 8 Handler 25 shot-origin loading, not a separate retail function.
+bool InsaneRebel2::loadOpcode8ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4) {
+	// Handler 25 (0x19): Shot-Origin Lookup Table (par4 == 8)
 	// FUN_0041CADB case 6 pushes 30 short pointers into sscanf with format at 0x482360:
 	//   "%hd %hd  %hd %hd ... %hd %hd" (15 X/Y pairs).
 	// Parsed values are written into DAT_004578a6 / DAT_004578c6 at indices 5..19.
 	if (_rebelHandler == 25 && par4 == 8) {
-		bool loaded = loadHandler25ShotOriginTable(b, startPos, remaining);
-		if (loaded) {
+		if (loadHandler25ShotOriginTable(b, startPos, remaining)) {
 			b.seek(startPos);
-			return;
+			return true;
 		}
 	}
 
-	// ----- Scan for embedded ANIM data -----
-	// Remaining handlers require finding ANIM tag in the stream
+	return false;
+}
+
+// ScummVM refactor helper for opcode 8 embedded ANIM scanning, not a separate retail function.
+void InsaneRebel2::loadOpcode8EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par3, int16 par4) {
+	// Remaining handlers require finding ANIM tag in the stream.
 	debug("Rebel2 Opcode 8: Scanning for ANIM tag (startPos=%lld remaining=%lld)",
 		(long long)startPos, (long long)remaining);
 
@@ -1668,7 +1677,6 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	int bytesRead = b.read(scanBuf, scanSize);
 	debug("Rebel2 Opcode 8: Read %d bytes for ANIM scan", bytesRead);
 
-	// Find ANIM tag
 	int animOffset = -1;
 	for (int i = 0; i + 8 <= bytesRead; ++i) {
 		if (READ_BE_UINT32(scanBuf + i) == MKTAG('A','N','I','M')) {
@@ -1685,7 +1693,6 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		return;
 	}
 
-	// Extract ANIM data
 	uint32 animReportedSize = READ_BE_UINT32(scanBuf + animOffset + 4);
 	int32 animDataSize = (int)MIN<int64>((int64)animReportedSize + 8, remaining - animOffset);
 	if (animDataSize <= 0) {
@@ -1703,33 +1710,38 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 
 	b.seek(startPos + animOffset);
 	b.read(animData, animDataSize);
+	handleOpcode8EmbeddedAnim(renderBitmap, animData, animDataSize, par3, par4);
 
+	free(animData);
+	free(scanBuf);
+	b.seek(startPos);
+}
+
+// ScummVM refactor helper for opcode 8 embedded ANIM routing, not a separate retail function.
+bool InsaneRebel2::handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData, int32 animDataSize, int16 par3, int16 par4) {
 	bool handled = false;
 
-	// ----- Handler 0x26/0x19: Turret HUD Overlays -----
-	// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading
+	// Handler 0x26/0x19: Turret HUD Overlays.
+	// FUN_00407fcb case 8: par3 1-4 for HUD NUT loading.
 	if (!handled && (_rebelHandler == 0x26 || _rebelHandler == 0x19)) {
 		if (par3 >= 1 && par3 <= 4) {
 			handled = loadTurretHudOverlay(animData, animDataSize, par3);
 		}
 	}
 
-	// ----- Handler 8: POV Ship Sprites or Background -----
-	// FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5)
-	// NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type
+	// Handler 8: POV Ship Sprites or Background.
+	// FUN_00401234 case 6: par4 selects POV NUT type (1,3,6,7) or background (5).
+	// NOTE: par3 is always 0 for Handler 8; par4 contains the actual sprite type.
 	if (!handled && _rebelHandler == 8) {
-		// Check for background loading first (par4=5)
 		if (par4 == 5) {
 			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
-		}
-		// Check for POV NUT sprites (par4=1,3,6,7)
-		else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
+		} else if (par4 == 1 || par4 == 3 || par4 == 6 || par4 == 7) {
 			handled = loadHandler8ShipSprites(animData, animDataSize, par4);
 		}
 	}
 
-	// ----- Handler 25 (0x19): Level 2 GRD Ship Sprites and Background -----
-	// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection
+	// Handler 25 (0x19): Level 2 GRD Ship Sprites and Background.
+	// FUN_0041cadb case 6 (opcode 8): Uses PAR4 for switch selection.
 	//   par4=1: GRD001 - Primary ship sprite -> DAT_00482240 / _grd001Sprite
 	//   par4=2: GRD002 - Secondary ship sprite -> DAT_00482238 / _grd002Sprite
 	//   par4=4: 350x230 corridor overlay -> DAT_00482268, draws immediately
@@ -1738,38 +1750,33 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 	//   par4=7: Overlay -> DAT_00482248, draws immediately
 	if (!handled && _rebelHandler == 25) {
 		if (par4 == 1 || par4 == 2) {
-			// GRD ship sprites - load into NutRenderer for per-frame rendering
 			handled = loadHandler25GrdSprites(animData, animDataSize, par4);
 		} else if (par4 == 5) {
-			// Background (320x200) - stored for per-frame restoration
 			handled = loadLevel2Background(animData, animDataSize, renderBitmap);
 		} else if (par4 == 4 || par4 == 6 || par4 == 7) {
-			// Overlays - draw immediately to renderBitmap
-			// These complete the visual scene along with the background
 			debug("Rebel2 Opcode 8: Handler 25 overlay par4=%d - drawing to screen", par4);
 			loadEmbeddedSan(par4, animData, animDataSize, renderBitmap);
 			handled = true;
 		}
 	}
 
-	// ----- Fallback: Embedded SAN HUD overlays -----
-	// For other cases, load as embedded SAN frame to HUD overlay slots
+	// Fallback: Embedded SAN HUD overlays.
+	// For other cases, load as embedded SAN frame to HUD overlay slots.
 	if (!handled) {
-		// Skip high-res data (par3 == 2, 4)
+		// Skip high-res data (par3 == 2, 4).
 		if (par3 == 2 || par3 == 4) {
 			debug("Rebel2 Opcode 8: Skipping high-res HUD par3=%d", par3);
 			handled = true;
 		} else {
-			// Determine userId: Handler 0x19 uses par3, others use par4
-			// Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3
-			int userId;
+			// Determine userId: Handler 0x19 uses par3, others use par4.
+			// Heuristic: if par3 is valid GRD range (1-13) and par4 is invalid, prefer par3.
 			bool usePar3 = (_rebelHandler == 0x19);
 			if (!usePar3 && par3 >= 1 && par3 <= 13 && (par4 <= 0 || par4 >= 1000)) {
 				usePar3 = true;
 			}
-			userId = usePar3 ? par3 : par4;
+			int userId = usePar3 ? par3 : par4;
 
-			// Skip audio tracks (userId >= 1000)
+			// Skip audio tracks (userId >= 1000).
 			if (userId > 0 && userId < 1000) {
 				debug("Rebel2 Opcode 8: Loading embedded SAN HUD userId=%d (handler=%d par3=%d par4=%d)",
 					userId, _rebelHandler, par3, par4);
@@ -1783,9 +1790,29 @@ void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStr
 		debug("Rebel2 Opcode 8: Unhandled case - handler=%d par3=%d par4=%d", _rebelHandler, par3, par4);
 	}
 
-	free(animData);
-	free(scanBuf);
-	b.seek(startPos);
+	return handled;
+}
+
+void InsaneRebel2::iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4) {
+	debug("Rebel2 IACT Opcode 8: handler=%d par2=%d par3=%d par4=%d (gameState=%d)",
+		_rebelHandler, par2, par3, par4, _gameState);
+
+	int64 startPos = b.pos();
+	int64 remaining = (chunkSize > 0) ? chunkSize : (b.size() - startPos);
+
+	if (loadOpcode8Handler7FlySprites(b, startPos, remaining, par4))
+		return;
+
+	if (loadOpcode8EdgeTable(b, startPos, remaining, par4))
+		return;
+
+	if (loadOpcode8AuxSfx(b, startPos, remaining, par4))
+		return;
+
+	if (loadOpcode8ShotOriginTable(b, startPos, remaining, par4))
+		return;
+
+	loadOpcode8EmbeddedAnim(renderBitmap, b, startPos, remaining, par3, par4);
 }
 
 // loadHandler25ShotOriginTable -- Parse shot origin coordinate pairs from IACT payload.
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index fe4813e0a62..add64ec8c4f 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -514,6 +514,12 @@ public:
 	void updateOpcode6GenericFlightState();
 	void scanOpcode6EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par4);
 	void iactRebel2Opcode8(byte *renderBitmap, Common::SeekableReadStream &b, int32 chunkSize, int16 par2, int16 par3, int16 par4);
+	bool loadOpcode8Handler7FlySprites(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+	bool loadOpcode8EdgeTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+	bool loadOpcode8AuxSfx(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+	bool loadOpcode8ShotOriginTable(Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par4);
+	void loadOpcode8EmbeddedAnim(byte *renderBitmap, Common::SeekableReadStream &b, int64 startPos, int64 remaining, int16 par3, int16 par4);
+	bool handleOpcode8EmbeddedAnim(byte *renderBitmap, byte *animData, int32 animDataSize, int16 par3, int16 par4);
 	void iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStream &b, int16 par2, int16 par3, int16 par4);
 
 	void procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,


Commit: 79342da6bcd496c422f1ae65e523188841cf44df
    https://github.com/scummvm/scummvm/commit/79342da6bcd496c422f1ae65e523188841cf44df
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:58+02:00

Commit Message:
SCUMM: RA2: Split post-render handling

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index add64ec8c4f..6002a4e63c2 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -538,6 +538,16 @@ public:
 	void renderStatusBarBackground(byte *renderBitmap, int pitch, int width, int height,
 								   int videoWidth, int videoHeight, int statusBarY);
 
+	void updatePostRenderScroll(int width, int height);
+	void updatePostRenderDeath();
+	void showPostRenderMenuCursor();
+	bool handlePostRenderMenuModes(byte *renderBitmap, int pitch, int width, int height, bool introPlaying);
+	bool handlePostRenderIntro(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+	void renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
+								 int videoWidth, int videoHeight, int statusBarY, int32 curFrame);
+	void updateGameplayDamageEffects(byte *renderBitmap, int pitch, int width, int height);
+	void checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+
 	// Draw NUT-based HUD overlays for Handler 0x26/0x19 turret modes
 	void renderTurretHudOverlays(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
 
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index d0db992e1ff..1d5bb9147fe 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2036,28 +2036,10 @@ void InsaneRebel2::renderNutSpriteMirrored(byte *dst, int pitch, int width, int
 	}
 }
 
-//
-// procPostRendering -- Post-frame rendering: HUD, ships, enemies, effects, status bar.
-//
-// Called after FOBJ decoding. Dispatches to per-handler rendering functions
-// for ship sprites, laser shots, explosions, crosshair, and damage effects.
-//
-void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
-							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
-
-	// Determine correct pitch for the video buffer (usually 320 for Rebel2)
-	int width = _player->_width;
-	int height = _player->_height;
-	if (width == 0)
-		width = _vm->_screenWidth;
-	if (height == 0)
-		height = _vm->_screenHeight;
-	int pitch = width;
-
-	// Calculate View/Scroll Offsets
-	// Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200)
-	// Map mouse X (0-320) to Scroll X (0-104)
-	// Map mouse Y (0-200) to Scroll Y (0-60)
+// updatePostRenderScroll -- Set SmushPlayer scroll offsets for the current frame.
+void InsaneRebel2::updatePostRenderScroll(int width, int height) {
+	// Rebel Assault 2 uses a buffer larger (424x260) than screen (320x200).
+	// Map mouse X (0-320) to Scroll X (0-104), and Y (0-200) to Scroll Y (0-60).
 	int maxScrollX = width - _vm->_screenWidth;
 	int maxScrollY = height - _vm->_screenHeight;
 
@@ -2066,15 +2048,18 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (maxScrollY < 0)
 		maxScrollY = 0;
 
-	// Simple linear mapping: Center of screen corresponds to center of buffer
+	// Simple linear mapping: Center of screen corresponds to center of buffer.
 	Common::Point aimPos = getGameplayAimPoint();
 	_viewX = (aimPos.x * maxScrollX) / _vm->_screenWidth;
 	_viewY = (aimPos.y * maxScrollY) / _vm->_screenHeight;
 
 	_player->setScrollOffset(_viewX, _viewY);
+}
 
-	// Death check: original game (FUN_417E53 line 25) exits video playback
-	// when DAT_0047a7ec >= 0xff (damage accumulator reaches 255).
+// updatePostRenderDeath -- End gameplay playback when player damage reaches 255.
+void InsaneRebel2::updatePostRenderDeath() {
+	// Original game (FUN_417E53 line 25) exits video playback when
+	// DAT_0047a7ec >= 0xff (damage accumulator reaches 255).
 	// Sync _playerShield from _playerDamage and break out of video on death.
 	if (_rebelHandler != 0) {
 		_playerShield = 255 - _playerDamage;
@@ -2083,150 +2068,130 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			_vm->_smushVideoShouldFinish = true;
 		}
 	}
+}
 
-	// --- HUD Drawing Order (from FUN_004089ab assembly analysis) ---
-	// Based on FUN_004089ab:
-	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
-	// 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
-	// 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
-	//
-	// In FUN_0041c012:
-	// - Sprites are drawn to buffer DAT_00482204 at position (0,0)
-	// - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
-	// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
-	//
-	// We draw directly to screen at Y=180
-
-	// Use video content coordinates, NOT buffer coordinates
-	const int videoWidth = 320;    // Native video width
-	const int videoHeight = 200;   // Native video height
-	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
-
-	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
-	// The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
-	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+// showPostRenderMenuCursor -- Restore the default cursor for menu videos.
+void InsaneRebel2::showPostRenderMenuCursor() {
+	Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
+	CursorMan.replaceCursor(cursor);
+	delete cursor;
+	CursorMan.showMouse(true);
+}
 
-	// Check if we're in menu mode (menu state + intro flag)
+// handlePostRenderMenuModes -- Process menu-like videos drawn during post-rendering.
+bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int width, int height, bool introPlaying) {
+	// Check if we're in menu mode (menu state + intro flag).
 	bool menuMode = (introPlaying && _gameState == kStateMainMenu);
 	bool pilotSelectMode = (introPlaying && (_gameState == kStatePilotSelect || _gameState == kStateDifficultySelect));
 	bool chapterSelectMode = (introPlaying && _gameState == kStateChapterSelect);
 
-	// Handle pilot selection input and rendering (FUN_00414A41)
-	// This is the pilot/save slot selection screen with centered menu
+	// Handle pilot selection input and rendering (FUN_00414A41).
+	// This is the pilot/save slot selection screen with centered menu.
 	if (pilotSelectMode) {
-		// Show the standard Windows arrow cursor
-		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-		CursorMan.replaceCursor(cursor);
-		delete cursor;
-		CursorMan.showMouse(true);
+		showPostRenderMenuCursor();
 
-		// Process pilot selection input - emulates FUN_00414A41 input handling
+		// Process pilot selection input - emulates FUN_00414A41 input handling.
 		int selection = processLevelSelectInput();
 
-		// Draw pilot selection overlay - centered menu like main menu
+		// Draw pilot selection overlay - centered menu like main menu.
 		drawLevelSelectOverlay(renderBitmap, pitch, width, height);
 
-		// If a selection was confirmed, signal video to stop
+		// If a selection was confirmed, signal video to stop.
 		if (selection >= 0) {
 			debug("Rebel2: Pilot selection confirmed: %d", selection);
 			_menuSelectionConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
 
-		// Skip normal HUD rendering in pilot select mode
-		return;
+		// Skip normal HUD rendering in pilot select mode.
+		return true;
 	}
 
-	// Handle chapter selection input and rendering (FUN_00415CF8)
-	// This is the actual level/chapter selection screen with preview and password
+	// Handle chapter selection input and rendering (FUN_00415CF8).
+	// This is the actual level/chapter selection screen with preview and password.
 	if (chapterSelectMode) {
-		// Show the standard Windows arrow cursor (same as menu)
-		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-		CursorMan.replaceCursor(cursor);
-		delete cursor;
-		CursorMan.showMouse(true);
+		showPostRenderMenuCursor();
 
 		// O_LEVEL.SAN provides the background with chapter preview thumbnails.
 		// The FOBJ offset system (set in procPreRendering) scrolls the correct preview
 		// into the preview box area. No black fill needed — video frame shows through.
 
-		// Process chapter selection input - emulates FUN_00415CF8 input handling
+		// Process chapter selection input - emulates FUN_00415CF8 input handling.
 		int selection = processChapterSelectInput();
 
-		// Draw chapter selection overlay - emulates FUN_00415CF8 rendering
+		// Draw chapter selection overlay - emulates FUN_00415CF8 rendering.
 		drawChapterSelectOverlay(renderBitmap, pitch, width, height);
 
-		// If a selection was confirmed, signal video to stop
+		// If a selection was confirmed, signal video to stop.
 		if (selection >= 0) {
 			debug("Rebel2: Chapter selection confirmed: %d", selection);
 			_menuSelectionConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
 
-		// Skip normal HUD rendering in chapter select mode
-		return;
+		// Skip normal HUD rendering in chapter select mode.
+		return true;
 	}
 
-	// Handle Top Pilots screen (FUN_00420116)
-	bool topPilotsMode = (introPlaying && _gameState == kStateTopPilots);
-	if (topPilotsMode) {
+	// Handle Top Pilots screen (FUN_00420116).
+	if (introPlaying && _gameState == kStateTopPilots) {
 		drawTopPilotsOverlay(renderBitmap, pitch, width, height);
-		return;
+		return true;
 	}
 
-	// Handle Options menu (FUN_004167A6)
-	bool optionsMode = (introPlaying && _gameState == kStateOptions);
-	if (optionsMode) {
+	// Handle Options menu (FUN_004167A6).
+	if (introPlaying && _gameState == kStateOptions) {
 		processOptionsInput();
 		drawOptionsOverlay(renderBitmap, pitch, width, height);
-		return;
+		return true;
 	}
 
-	// Handle menu input and rendering if in menu mode
 	if (menuMode) {
 		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
-		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt
-		// MSTOVER.NUT is a background overlay, NOT a cursor
-		Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-		CursorMan.replaceCursor(cursor);
-		delete cursor;
-		CursorMan.showMouse(true);
-
-		// Process menu input during each frame
+		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt.
+		// MSTOVER.NUT is a background overlay, NOT a cursor.
+		showPostRenderMenuCursor();
+
+		// Process menu input during each frame.
 		int selection = processMenuInput();
 
-		// Update inactivity timer (only increments when no input is received)
-		// Input resets timer in processMenuInput()
+		// Update inactivity timer (only increments when no input is received).
+		// Input resets timer in processMenuInput().
 		_menuInactivityTimer++;
 
-		// Check for inactivity timeout
-		// From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode)
-		// At 12fps video rate, 300 frames = ~25 seconds of inactivity
-		// The original checks: if (local_8 > 299) return 0;
+		// Check for inactivity timeout.
+		// From FUN_004147b2: 300 frames of inactivity returns 0 (exit to intro/attract mode).
+		// At 12fps video rate, 300 frames = ~25 seconds of inactivity.
+		// The original checks: if (local_8 > 299) return 0.
 		if (_menuInactivityTimer > 300) {
 			debug("Rebel2: Menu inactivity timeout - ending video to loop");
-			// Signal video to end so menu loop plays new video
+			// Signal video to end so menu loop plays new video.
 			// This emulates the attract mode behavior where a new random
-			// menu video is selected after inactivity
+			// menu video is selected after inactivity.
 			_menuInactivityTimer = 0;
-			// Don't set _smushVideoShouldFinish here - let video end naturally
-			// This will cause runMainMenu to loop and play a new random video
+			// Don't set _smushVideoShouldFinish here - let video end naturally.
+			// This will cause runMainMenu to loop and play a new random video.
 		}
 
-		// Draw menu selection overlay
+		// Draw menu selection overlay.
 		drawMenuOverlay(renderBitmap, pitch, width, height);
 
-		// If a selection was confirmed, signal video to stop
+		// If a selection was confirmed, signal video to stop.
 		if (selection >= 0) {
 			debug("Rebel2: Menu selection confirmed: %d", selection);
 			_menuSelectionConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
 
-		// Skip normal HUD rendering in menu mode
-		return;
+		// Skip normal HUD rendering in menu mode.
+		return true;
 	}
 
+	return false;
+}
+
+// handlePostRenderIntro -- Hide gameplay HUD for intro/cinematic videos.
+bool InsaneRebel2::handlePostRenderIntro(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
 	// During intro/cinematic sequences:
 	// - Hide the mouse cursor (original: ShowCursor(0) at startup in FUN_00420c70)
 	// - Skip all HUD/status bar/crosshair rendering
@@ -2238,34 +2203,77 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// - Cinematics/intros don't have opcode 6, so handler stays 0
 	// - We use _rebelHandler == 0 as the primary indicator for intro/cinematic mode
 	if (_rebelHandler == 0) {
-		// Hide mouse cursor during intro - no crosshair, no clicking
+		// Hide mouse cursor during intro - no crosshair, no clicking.
 		CursorMan.showMouse(false);
 
-		// Track state transition for debugging
+		// Track state transition for debugging.
 		if (!_introCursorPushed) {
 			_introCursorPushed = true;
 			debug("Rebel2: Intro/cinematic mode (handler=0, flags=0x%x, state=%d) - HUD disabled, mouse hidden",
 				  _player->_curVideoFlags, _gameState);
 		}
 
-		// Chapter title text overlay (FUN_004171c5)
+		// Chapter title text overlay (FUN_004171c5).
 		if (_textOverlayActive)
 			renderTextOverlay(renderBitmap, pitch, width, height, curFrame);
 
-		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9
-		return;
-	} else {
-		// Gameplay mode - handler was set by IACT opcode 6
-		if (_introCursorPushed) {
-			_introCursorPushed = false;
-			debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
-				  _rebelHandler, _player->_curVideoFlags, _gameState);
+		// Skip all HUD rendering during intro - subtitles are rendered via opcode 9.
+		return true;
+	}
+
+	// Gameplay mode - handler was set by IACT opcode 6.
+	if (_introCursorPushed) {
+		_introCursorPushed = false;
+		debug("Rebel2: Gameplay mode (handler=%d, flags=0x%x, state=%d) - HUD enabled",
+			  _rebelHandler, _player->_curVideoFlags, _gameState);
+	}
+
+	return false;
+}
+
+// updateGameplayDamageEffects -- Apply handler-specific damage visuals.
+void InsaneRebel2::updateGameplayDamageEffects(byte *renderBitmap, int pitch, int width, int height) {
+	// Damage visual effects - handler-specific per original architecture:
+	// Handler 8:    FUN_401CCF line 119 -> FUN_00420754 (palette flash + screen shake)
+	// Handler 0x19: FUN_41DB5E line 192 -> FUN_00420562 (palette flash only, every frame)
+	// Handler 0x26: FUN_4092D9 lines 135/225/237 -> FUN_00420515 trigger + palette flash
+	// Handler 7:    FUN_40E35E -> FUN_00420515 trigger + palette flash
+	if (_rebelHandler == 8) {
+		// Full damage effect: palette flash + screen shake.
+		// Suppressed during autopilot (mode 4) and cutscene (mode 5).
+		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
+			updateDamageEffect(renderBitmap, pitch, width, height);
 		}
+	} else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
+		// Palette flash only - no screen shake for turret/FPS/ship handlers.
+		updateDamageFlashPalette();
+	}
+}
+
+// checkGameplayPostRenderCollisions -- Run handler-specific collision checks.
+void InsaneRebel2::checkGameplayPostRenderCollisions(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
+	// Per-frame collision checking against registered zones.
+	//
+	// Handler 0x26 (turret): FUN_4092D9 - aim position vs primary zones (centered coords)
+	//   Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
+	//
+	// Handler 7 (ship): FUN_40E35E - ship position vs zones per control mode:
+	//   Mode 0/2: SECONDARY zones (0x0E) - obstacle collision (inside quad = hit)
+	//   Mode 1/3: PRIMARY zones (0x0D) - wall/boundary per-edge with push-back
+	//   Uses ship position in raw buffer coords, hit cooldown, directional damage.
+	if (_rebelHandler == 0x26) {
+		checkCollisionZones();
+	} else if (_rebelHandler == 7) {
+		checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
 	}
+}
 
-	// From here on, we're in gameplay mode (_rebelHandler != 0)
-	// Process mouse input for shooting
-	// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0
+// renderGameplayPostFrame -- Draw the gameplay post-render pipeline in original order.
+void InsaneRebel2::renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
+										   int videoWidth, int videoHeight, int statusBarY, int32 curFrame) {
+	// From here on, we're in gameplay mode (_rebelHandler != 0).
+	// Process mouse input for shooting.
+	// Original: FUN_00403240 only runs handlers when DAT_0047a814 == 0.
 	processMouse();
 
 	// NOTE: Level 2 background is drawn ONCE during IACT opcode 8 par4=5 processing
@@ -2285,10 +2293,22 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// The cockpit frame covers laser beam edges, giving the appearance
 	// that beams emerge from behind the cockpit.
 
-	// STEP 0: Fill status bar background (FUN_004288c0)
+	// Based on FUN_004089ab:
+	// 1. Line 156: FUN_004288c0 fills status bar background at Y=0xb4 (180)
+	// 2. Lines 171-226: Draw turret overlays, targeting reticle, crosshair
+	// 3. Line 243: FUN_0041c012 draws status bar sprites LAST (on top)
+	//
+	// In FUN_0041c012:
+	// - Sprites are drawn to buffer DAT_00482204 at position (0,0)
+	// - Buffer is composited at Y=0xb4 (180) via FUN_0042f780
+	// - DISPFONT.NUT (DAT_00482200) sprites 1-7 contain the status bar elements
+	//
+	// We draw directly to screen at Y=180.
+
+	// STEP 0: Fill status bar background (FUN_004288c0).
 	renderStatusBarBackground(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY);
 
-	// Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7)
+	// Ship rendering (FUN_00401ccf for Handler 8, FUN_0040d836 for Handler 7).
 	debug("Rebel2 Ship Check: handler=%d shipSprite=%p flyShipSprite=%p shipLevelMode=%d numSprites=%d/%d",
 		_rebelHandler, (void*)_shipSprite, (void*)_flyShipSprite, _shipLevelMode,
 		_shipSprite ? _shipSprite->getNumChars() : 0,
@@ -2298,13 +2318,13 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	renderHandler8Ship(renderBitmap, pitch, width, height);
 	renderFallbackShip(renderBitmap, pitch, width, height);
 
-	// Enemy target indicators (handler-specific; sprite-based in turret mode)
+	// Enemy target indicators (handler-specific; sprite-based in turret mode).
 	renderEnemyOverlays(renderBitmap, pitch, width, height, videoWidth);
 
-	// Explosion animations (FUN_409FBC) — drawn before lasers in original
+	// Explosion animations (FUN_409FBC) - drawn before lasers in original.
 	renderExplosions(renderBitmap, pitch, width, height);
 
-	// Laser shot beams — drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges
+	// Laser shot beams - drawn BEFORE cockpit/HUD overlays so cockpit covers beam edges.
 	renderLaserShots(renderBitmap, pitch, width, height);
 
 	// Handler 25 GRD sprites drawn AFTER enemies/explosions/lasers per original FUN_0041DB5E:
@@ -2316,63 +2336,73 @@ void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	renderHandler25ShipPre(renderBitmap, pitch, width, height);
 	renderHandler25Ship(renderBitmap, pitch, width, height);
 
-	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226)
-	// These are cockpit frame, crosshair, and reticle — drawn ON TOP of laser beams
+	// STEP 1A: Draw NUT-based HUD overlays for Handler 0x26/0x19 (FUN_004089ab lines 195-226).
+	// These are cockpit frame, crosshair, and reticle - drawn ON TOP of laser beams.
 	renderTurretHudOverlays(renderBitmap, pitch, width, height, curFrame);
 
-	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks)
+	// STEP 1B: Draw embedded SAN HUD overlays (from IACT chunks).
 	renderEmbeddedHudOverlays(renderBitmap, pitch, width, height);
 
-	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012)
+	// STEP 2: Draw DISPFONT.NUT status bar sprites (FUN_0041c012).
 	renderStatusBarSprites(renderBitmap, pitch, width, height, statusBarY, curFrame);
 
-	// Damage visual effects — handler-specific per original architecture:
-	//   Handler 8:    FUN_401CCF line 119 → FUN_00420754 (palette flash + screen shake)
-	//   Handler 0x19: FUN_41DB5E line 192 → FUN_00420562 (palette flash only, every frame)
-	//   Handler 0x26: FUN_4092D9 lines 135/225/237 → FUN_00420515 trigger + palette flash
-	//   Handler 7:    FUN_40E35E → FUN_00420515 trigger + palette flash
-	if (_rebelHandler == 8) {
-		// Full damage effect: palette flash + screen shake
-		// Suppressed during autopilot (mode 4) and cutscene (mode 5)
-		if (_shipLevelMode != 4 && _shipLevelMode != 5) {
-			updateDamageEffect(renderBitmap, pitch, width, height);
-		}
-	} else if (_rebelHandler == 0x19 || _rebelHandler == 0x26 || _rebelHandler == 7) {
-		// Palette flash only — no screen shake for turret/FPS/ship handlers
-		updateDamageFlashPalette();
-	}
-
-	// Per-frame collision checking against registered zones.
-	//
-	// Handler 0x26 (turret): FUN_4092D9 — aim position vs primary zones (centered coords)
-	//   Zones with filterValue < 1000 tested via point-in-quad against mouse/aim position.
-	//
-	// Handler 7 (ship): FUN_40E35E — ship position vs zones per control mode:
-	//   Mode 0/2: SECONDARY zones (0x0E) — obstacle collision (inside quad = hit)
-	//   Mode 1/3: PRIMARY zones (0x0D) — wall/boundary per-edge with push-back
-	//   Uses ship position in raw buffer coords, hit cooldown, directional damage.
-	if (_rebelHandler == 0x26) {
-		checkCollisionZones();
-	} else if (_rebelHandler == 7) {
-		checkHandler7CollisionZones(renderBitmap, pitch, width, height, curFrame);
-	}
+	updateGameplayDamageEffects(renderBitmap, pitch, width, height);
+	checkGameplayPostRenderCollisions(renderBitmap, pitch, width, height, curFrame);
 
-	// Crosshair/reticle (FUN_004089ab, FUN_0040d836)
+	// Crosshair/reticle (FUN_004089ab, FUN_0040d836).
 	renderCrosshair(renderBitmap, pitch, width, height);
 
-	// HUD score/lives rendering (FUN_0041c012)
+	// HUD score/lives rendering (FUN_0041c012).
 	renderScoreHUD(renderBitmap, pitch, width, height, statusBarY);
 
-	// Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271)
+	// Reset FOBJ position offsets (FUN_00424510(0,0) in original FUN_0041DB5E line 271).
 	if (_player) {
 		_player->_fobjOffsetX = 0;
 		_player->_fobjOffsetY = 0;
 	}
 
-	// Frame end cleanup: reset enemy active flags and collision zones (FUN_403240)
+	// Frame end cleanup: reset enemy active flags and collision zones (FUN_403240).
 	frameEndCleanup();
 }
 
+//
+// procPostRendering -- Post-frame rendering: HUD, ships, enemies, effects, status bar.
+//
+// Called after FOBJ decoding. Dispatches to per-handler rendering functions
+// for ship sprites, laser shots, explosions, crosshair, and damage effects.
+//
+void InsaneRebel2::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
+							   int32 setupsan13, int32 curFrame, int32 maxFrame) {
+
+	// Determine correct pitch for the video buffer (usually 320 for Rebel2)
+	int width = _player->_width;
+	int height = _player->_height;
+	if (width == 0)
+		width = _vm->_screenWidth;
+	if (height == 0)
+		height = _vm->_screenHeight;
+	int pitch = width;
+
+	updatePostRenderScroll(width, height);
+	updatePostRenderDeath();
+
+	// Use video content coordinates, NOT buffer coordinates
+	const int videoWidth = 320;    // Native video width
+	const int videoHeight = 200;   // Native video height
+	const int statusBarY = 180;    // 0xb4 - status bar starts at Y=180 in video coords
+
+	// Hide HUD/status bar during intro videos (marked by SmushPlayer video flag 0x20)
+	// The 0x20 flag indicates a non-interactive cutscene/intro sequence OR menu
+	bool introPlaying = ((_player->_curVideoFlags & 0x20) != 0);
+
+	if (handlePostRenderMenuModes(renderBitmap, pitch, width, height, introPlaying))
+		return;
+
+	if (handlePostRenderIntro(renderBitmap, pitch, width, height, curFrame))
+		return;
+	renderGameplayPostFrame(renderBitmap, pitch, width, height, videoWidth, videoHeight, statusBarY, curFrame);
+}
+
 // ---------------------------------------------------------------------------
 // Damage Visual Effect Functions
 // ---------------------------------------------------------------------------


Commit: d90e9a159ab0e624b6f4c94a96145c7a1bacb914
    https://github.com/scummvm/scummvm/commit/d90e9a159ab0e624b6f4c94a96145c7a1bacb914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:59+02:00

Commit Message:
SCUMM: RA2: Split handler 7 code

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 6002a4e63c2..3fb850bc78a 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -813,6 +813,18 @@ public:
 	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords
 	// and draws proximity shadow cues for nearby danger zones.
 	void checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
+	bool isHandler7ShipInsideObstacleZone(const CollisionZone &zone, int margin);
+	void applyHandler7ObstacleHit(const CollisionZone &zone, int zoneIndex);
+	void awardHandler7DodgeScore();
+	void checkHandler7ObstacleZones(uint16 &warningMask);
+	bool applyHandler7WallDamage(int wallDamage);
+	void resetHandler7HorizontalVelocity(int16 velocity);
+	void checkHandler7TopBoundary(const CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask);
+	void checkHandler7BottomBoundary(const CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask);
+	void checkHandler7LeftBoundary(const CollisionZone &zone, int16 hMargin, int wallDamage);
+	void checkHandler7RightBoundary(const CollisionZone &zone, int16 hMargin, int wallDamage);
+	void checkHandler7BoundaryZones(uint16 &warningMask);
+	void renderHandler7WarningCues(byte *renderBitmap, int pitch, int width, int height, int32 curFrame, uint16 warningMask);
 
 	int16 _playerDamage;  // Legacy damage counter (kept for compatibility/telemetry)
 	int16 _playerShield;  // Shields: 0..255 where 255 = full
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 1d5bb9147fe..04ce01e7f48 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -1665,236 +1665,252 @@ void InsaneRebel2::checkCollisionZones() {
 // Two modes: obstacle collision (secondary zones) and wall/boundary
 // collision (primary zones with per-edge push-back).
 //
-void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
-	// FUN_40E35E — Handler 7 per-frame collision system.
-	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
-	// Two modes depending on _flyControlMode:
-	//   Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
-	//   Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
+// The helpers in this block are ScummVM refactor helpers split out of
+// checkHandler7CollisionZones; they are not separate retail functions.
+//
+bool InsaneRebel2::isHandler7ShipInsideObstacleZone(const InsaneRebel2::CollisionZone &zone, int margin) {
+	int x1 = zone.x1, y1 = zone.y1;
+	int x2 = zone.x2, y2 = zone.y2;
+	int x3 = zone.x3, y3 = zone.y3;
+	int x4 = zone.x4, y4 = zone.y4;
 
-	// Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
-	// to match the original where the decrement happens during rendering.
-	//
-	// local_c in FUN_40E35E: proximity mask for nearby danger-zone shadow cues.
-	// bit 0=left, bit 1=right, bit 2=top, bit 3=bottom
-	uint16 warningMask = 0;
+	// Point-in-quad test (lines 75-89).
+	// Start assuming inside, clear if outside any edge (with margin).
+	bool inside = true;
 
-	if (_flyControlMode == 0 || _flyControlMode == 2) {
-		// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
-		// Original lines 52-132: Point-in-quad test with 15px inward margin.
-		// Inside the quad = collision with obstacle.
-		const int margin = 15;  // local_14 = 0x0f, local_20 = 0x0f
-
-		for (int i = 0; i < _secondaryZoneCount; i++) {
-			CollisionZone &zone = _secondaryZones[i];
-			if (!zone.active)
-				continue;
+	// Top edge: interpolate Y along v1->v2 at shipX, +15 margin.
+	if (x2 != x1) {
+		int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
+		if (_flyShipScreenY < interpY)
+			inside = false;
+	}
+	// Bottom edge: interpolate Y along v4->v3 at shipX, -15 margin.
+	if (inside && x3 != x4) {
+		int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
+		if (interpY < _flyShipScreenY)
+			inside = false;
+	}
+	// Left edge: interpolate X along v1->v4 at shipY, +15 margin.
+	if (inside && y4 != y1) {
+		int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
+		if (_flyShipScreenX < interpX)
+			inside = false;
+	}
+	// Right edge: interpolate X along v2->v3 at shipY, -15 margin.
+	if (inside && y3 != y2) {
+		int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
+		if (interpX < _flyShipScreenX)
+			inside = false;
+	}
 
-			int x1 = zone.x1, y1 = zone.y1;
-			int x2 = zone.x2, y2 = zone.y2;
-			int x3 = zone.x3, y3 = zone.y3;
-			int x4 = zone.x4, y4 = zone.y4;
+	return inside;
+}
 
-			// Point-in-quad test (lines 75-89)
-			// Start assuming inside, clear if outside any edge (with margin)
-			bool inside = true;
+void InsaneRebel2::applyHandler7ObstacleHit(const InsaneRebel2::CollisionZone &zone, int zoneIndex) {
+	// Collision with obstacle - apply damage and break.
+	_hitCooldown = 10;
+	_spaceShotDirection = zone.filterValue + 2;
 
-			// Top edge: interpolate Y along v1→v2 at shipX, +15 margin
-			if (x2 != x1) {
-				int interpY = (_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + margin + y1;
-				if (_flyShipScreenY < interpY)
-					inside = false;
-			}
-			// Bottom edge: interpolate Y along v4→v3 at shipX, -15 margin
-			if (inside && x3 != x4) {
-				int interpY = (_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - margin;
-				if (interpY < _flyShipScreenY)
-					inside = false;
-			}
-			// Left edge: interpolate X along v1→v4 at shipY, +15 margin
-			if (inside && y4 != y1) {
-				int interpX = (_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + margin + x1;
-				if (_flyShipScreenX < interpX)
-					inside = false;
-			}
-			// Right edge: interpolate X along v2→v3 at shipY, -15 margin
-			if (inside && y3 != y2) {
-				int interpX = (_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - margin;
-				if (interpX < _flyShipScreenX)
-					inside = false;
-			}
+	LevelDifficultyParams params = getDifficultyParams();
+	int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
+	if (!_rebelInvulnerable) {
+		_playerDamage += collisionDamage;
+		if (_playerDamage > 255)
+			_playerDamage = 255;
+	}
+	_rebelHitCounter++;
+	initDamageFlash();
+	// Pan based on ship X position relative to screen center.
+	playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
+	debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
+		zoneIndex, _flyShipScreenX, _flyShipScreenY, collisionDamage);
+}
 
-			// Frame match: field2 - 1 == field1 (line 90)
-			if (zone.field2 - 1 == zone.field1) {
-				if (inside) {
-					// Collision with obstacle — apply damage and break
-					_hitCooldown = 10;
-					_spaceShotDirection = zone.filterValue + 2;
-
-					LevelDifficultyParams params = getDifficultyParams();
-					int collisionDamage = (params.dodgeDamage >= 0) ? params.dodgeDamage : 0;
-					if (!_rebelInvulnerable) {
-						_playerDamage += collisionDamage;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-					}
-					_rebelHitCounter++;
-					initDamageFlash();
-					// Pan based on ship X position relative to screen center
-					playSfx(1, 127, CLIP((_flyShipScreenX - 212) * 127 / 160, -127, 127));
-					debug("Rebel2: Handler7 Mode0/2 OBSTACLE HIT zone=%d ship=(%d,%d) damage=%d",
-						i, _flyShipScreenX, _flyShipScreenY, collisionDamage);
-					break;  // Only one collision per frame (original breaks)
-				} else {
-					// Safely avoided obstacle — award score
-					// Original: FUN_0041bf8d(DAT_0047e100[levelIdx])
-					LevelDifficultyParams scoreParams = getDifficultyParams();
-					if (scoreParams.dodgePoints > 0) {
-						addScore(scoreParams.dodgePoints);
-					}
-				}
-			}
+void InsaneRebel2::awardHandler7DodgeScore() {
+	// Safely avoided obstacle - award score.
+	// Original: FUN_0041bf8d(DAT_0047e100[levelIdx]).
+	LevelDifficultyParams scoreParams = getDifficultyParams();
+	if (scoreParams.dodgePoints > 0) {
+		addScore(scoreParams.dodgePoints);
+	}
+}
+
+void InsaneRebel2::checkHandler7ObstacleZones(uint16 &warningMask) {
+	// ---- Mode 0/2: Obstacle collision using SECONDARY zones (FUN_403b5b) ----
+	// Original lines 52-132: Point-in-quad test with 15px inward margin.
+	// Inside the quad = collision with obstacle.
+	const int margin = 15;  // local_14 = 0x0f, local_20 = 0x0f
 
-			// FUN_40E35E line 104: mark near-danger proximity for shadow cue rendering.
-			// Uses the low byte of zone.filterValue (retail local_1c) to pick direction bits.
-			if (zone.field2 - 13 < zone.field1) {
-				uint32 bit = 4u << ((byte)zone.filterValue & 0x1f);
-				warningMask = (uint16)(warningMask | (uint16)bit);
+	for (int i = 0; i < _secondaryZoneCount; i++) {
+		CollisionZone &zone = _secondaryZones[i];
+		if (!zone.active)
+			continue;
+
+		bool inside = isHandler7ShipInsideObstacleZone(zone, margin);
+
+		// Frame match: field2 - 1 == field1 (line 90).
+		if (zone.field2 - 1 == zone.field1) {
+			if (inside) {
+				applyHandler7ObstacleHit(zone, i);
+				break;  // Only one collision per frame (original breaks).
+			} else {
+				awardHandler7DodgeScore();
 			}
 		}
 
-		// Corridor side proximity (FUN_40E35E lines 127-131)
-		if (_flyShipScreenX < _corridorLeftX + 0x28)
-			warningMask |= 1;
-		if (_corridorRightX - 0x28 < _flyShipScreenX)
-			warningMask |= 2;
+		// FUN_40E35E line 104: mark near-danger proximity for shadow cue rendering.
+		// Uses the low byte of zone.filterValue (retail local_1c) to pick direction bits.
+		if (zone.field2 - 13 < zone.field1) {
+			uint32 bit = 4u << ((byte)zone.filterValue & 0x1f);
+			warningMask = (uint16)(warningMask | (uint16)bit);
+		}
+	}
 
-	} else {
-		// ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
-		// Original lines 133-235: Per-edge interpolation with push-back.
-		// Ship position is clamped to wall boundaries when hitting.
-		int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f;  // local_14
-		const int16 vMargin = 0x0f;  // local_20
-		LevelDifficultyParams wallParams = getDifficultyParams();
-		int wallDamage = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
-
-		for (int i = 0; i < _primaryZoneCount; i++) {
-			CollisionZone &zone = _primaryZones[i];
-			if (!zone.active)
-				continue;
+	// Corridor side proximity (FUN_40E35E lines 127-131).
+	if (_flyShipScreenX < _corridorLeftX + 0x28)
+		warningMask |= 1;
+	if (_corridorRightX - 0x28 < _flyShipScreenX)
+		warningMask |= 2;
+}
 
-			int x1 = zone.x1, y1 = zone.y1;
-			int x2 = zone.x2, y2 = zone.y2;
-			int x3 = zone.x3, y3 = zone.y3;
-			int x4 = zone.x4, y4 = zone.y4;
-
-			// Top edge: interpolate Y along v1→v2 at shipX (lines 152-166)
-			if (x2 != x1) {
-				int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
-				if (_flyShipScreenY < edgeY) {
-					// Ship above top wall — push down
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = wallDamage;
-						_playerDamage += damage;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
-					}
-					_spaceShotDirection = 2;  // Direction: pushed down
-					_flyShipScreenY = edgeY;  // Push-back
-					playSfx(1, 127, 0);  // CRASH.SAD, top wall → center pan (always)
-					initDamageFlash();
-				} else if (_flyShipScreenY < edgeY + 0x28) {
-					warningMask |= 4;
-				}
+bool InsaneRebel2::applyHandler7WallDamage(int wallDamage) {
+	if (_hitCooldown < 5 && !_rebelInvulnerable) {
+		_playerDamage += wallDamage;
+		if (_playerDamage > 255)
+			_playerDamage = 255;
+		_rebelHitCounter++;
+		_hitCooldown = 10;
+		return true;
+	}
+
+	return false;
+}
+
+void InsaneRebel2::resetHandler7HorizontalVelocity(int16 velocity) {
+	for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
+		_velocityHistory[j] = velocity;
+	}
+}
+
+void InsaneRebel2::checkHandler7TopBoundary(const InsaneRebel2::CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask) {
+	int x1 = zone.x1, y1 = zone.y1;
+	int x2 = zone.x2, y2 = zone.y2;
+
+	// Top edge: interpolate Y along v1->v2 at shipX (lines 152-166).
+	if (x2 != x1) {
+		int16 edgeY = (int16)((_flyShipScreenX - x1) * (y2 - y1) / (x2 - x1) + y1 + vMargin);
+		if (_flyShipScreenY < edgeY) {
+			// Ship above top wall - push down.
+			if (applyHandler7WallDamage(wallDamage)) {
+				debug("Rebel2: Handler7 Mode1/3 TOP WALL ship=(%d,%d) edgeY=%d damage=%d",
+					_flyShipScreenX, _flyShipScreenY, edgeY, wallDamage);
 			}
+			_spaceShotDirection = 2;  // Direction: pushed down
+			_flyShipScreenY = edgeY;  // Push-back
+			playSfx(1, 127, 0);  // CRASH.SAD, top wall -> center pan (always)
+			initDamageFlash();
+		} else if (_flyShipScreenY < edgeY + 0x28) {
+			warningMask |= 4;
+		}
+	}
+}
 
-			// Bottom edge: interpolate Y along v4→v3 at shipX (lines 167-183)
-			if (x3 != x4) {
-				int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
-				_corridorBottomY = vMargin + edgeY;  // DAT_00443b10 update
-				if (edgeY < _flyShipScreenY) {
-					// Ship below bottom wall — push up
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = wallDamage;
-						_playerDamage += damage;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeY, damage);
-					}
-					_spaceShotDirection = 3;  // Direction: pushed up
-					_flyShipScreenY = edgeY;  // Push-back
-					playSfx(1, 127, 0);  // CRASH.SAD, bottom wall → center pan (always)
-					initDamageFlash();
-				} else if (edgeY - 0x28 < _flyShipScreenY) {
-					warningMask |= 8;
-				}
+void InsaneRebel2::checkHandler7BottomBoundary(const InsaneRebel2::CollisionZone &zone, int16 vMargin, int wallDamage, uint16 &warningMask) {
+	int x3 = zone.x3, y3 = zone.y3;
+	int x4 = zone.x4, y4 = zone.y4;
+
+	// Bottom edge: interpolate Y along v4->v3 at shipX (lines 167-183).
+	if (x3 != x4) {
+		int16 edgeY = (int16)((_flyShipScreenX - x4) * (y3 - y4) / (x3 - x4) + y4 - vMargin);
+		_corridorBottomY = vMargin + edgeY;  // DAT_00443b10 update
+		if (edgeY < _flyShipScreenY) {
+			// Ship below bottom wall - push up.
+			if (applyHandler7WallDamage(wallDamage)) {
+				debug("Rebel2: Handler7 Mode1/3 BOTTOM WALL ship=(%d,%d) edgeY=%d damage=%d",
+					_flyShipScreenX, _flyShipScreenY, edgeY, wallDamage);
 			}
+			_spaceShotDirection = 3;  // Direction: pushed up
+			_flyShipScreenY = edgeY;  // Push-back
+			playSfx(1, 127, 0);  // CRASH.SAD, bottom wall -> center pan (always)
+			initDamageFlash();
+		} else if (edgeY - 0x28 < _flyShipScreenY) {
+			warningMask |= 8;
+		}
+	}
+}
 
-			// Left edge: interpolate X along v1→v4 at shipY (lines 184-199)
-			if (y4 != y1) {
-				int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
-				if (_flyShipScreenX < edgeX) {
-					// Ship left of left wall — push right
-					_flyShipScreenX = edgeX;  // Push-back
+void InsaneRebel2::checkHandler7LeftBoundary(const InsaneRebel2::CollisionZone &zone, int16 hMargin, int wallDamage) {
+	int x1 = zone.x1, y1 = zone.y1;
+	int x4 = zone.x4, y4 = zone.y4;
 
-					// FUN_40E35E resets horizontal history to force immediate rightward correction.
-					for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
-						_velocityHistory[j] = 127;
-					}
+	// Left edge: interpolate X along v1->v4 at shipY (lines 184-199).
+	if (y4 != y1) {
+		int16 edgeX = (int16)((_flyShipScreenY - y1) * (x4 - x1) / (y4 - y1) + x1 + hMargin);
+		if (_flyShipScreenX < edgeX) {
+			// Ship left of left wall - push right.
+			_flyShipScreenX = edgeX;  // Push-back
 
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = wallDamage;
-						_playerDamage += damage;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
-					}
-					_spaceShotDirection = 0;  // Direction: pushed right
-					playSfx(1, 127, -100);  // CRASH.SAD, left wall → pan left (always)
-					initDamageFlash();
-				}
+			// FUN_40E35E resets horizontal history to force immediate rightward correction.
+			resetHandler7HorizontalVelocity(127);
+
+			if (applyHandler7WallDamage(wallDamage)) {
+				debug("Rebel2: Handler7 Mode1/3 LEFT WALL ship=(%d,%d) edgeX=%d damage=%d",
+					_flyShipScreenX, _flyShipScreenY, edgeX, wallDamage);
 			}
+			_spaceShotDirection = 0;  // Direction: pushed right
+			playSfx(1, 127, -100);  // CRASH.SAD, left wall -> pan left (always)
+			initDamageFlash();
+		}
+	}
+}
 
-			// Right edge: interpolate X along v2→v3 at shipY (lines 200-215)
-			if (y3 != y2) {
-				int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
-				if (edgeX < _flyShipScreenX) {
-					// Ship right of right wall — push left
-					_flyShipScreenX = edgeX;  // Push-back
+void InsaneRebel2::checkHandler7RightBoundary(const InsaneRebel2::CollisionZone &zone, int16 hMargin, int wallDamage) {
+	int x2 = zone.x2, y2 = zone.y2;
+	int x3 = zone.x3, y3 = zone.y3;
 
-					// FUN_40E35E resets horizontal history to force immediate leftward correction.
-					for (int j = 0; j < ARRAYSIZE(_velocityHistory); j++) {
-						_velocityHistory[j] = -127;
-					}
+	// Right edge: interpolate X along v2->v3 at shipY (lines 200-215).
+	if (y3 != y2) {
+		int16 edgeX = (int16)((_flyShipScreenY - y2) * (x3 - x2) / (y3 - y2) + x2 - hMargin);
+		if (edgeX < _flyShipScreenX) {
+			// Ship right of right wall - push left.
+			_flyShipScreenX = edgeX;  // Push-back
 
-					if (_hitCooldown < 5 && !_rebelInvulnerable) {
-						int damage = wallDamage;
-						_playerDamage += damage;
-						if (_playerDamage > 255)
-							_playerDamage = 255;
-						_rebelHitCounter++;
-						_hitCooldown = 10;
-						debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
-							_flyShipScreenX, _flyShipScreenY, edgeX, damage);
-					}
-					_spaceShotDirection = 1;  // Direction: pushed left
-					playSfx(1, 127, 100);  // CRASH.SAD, right wall → pan right (always)
-					initDamageFlash();
-				}
+			// FUN_40E35E resets horizontal history to force immediate leftward correction.
+			resetHandler7HorizontalVelocity(-127);
+
+			if (applyHandler7WallDamage(wallDamage)) {
+				debug("Rebel2: Handler7 Mode1/3 RIGHT WALL ship=(%d,%d) edgeX=%d damage=%d",
+					_flyShipScreenX, _flyShipScreenY, edgeX, wallDamage);
 			}
+			_spaceShotDirection = 1;  // Direction: pushed left
+			playSfx(1, 127, 100);  // CRASH.SAD, right wall -> pan right (always)
+			initDamageFlash();
 		}
 	}
+}
+
+void InsaneRebel2::checkHandler7BoundaryZones(uint16 &warningMask) {
+	// ---- Mode 1/3: Wall/boundary collision using PRIMARY zones (FUN_403b34) ----
+	// Original lines 133-235: Per-edge interpolation with push-back.
+	// Ship position is clamped to wall boundaries when hitting.
+	int16 hMargin = (_flyControlMode == 1) ? 0x28 : 0x0f;  // local_14
+	const int16 vMargin = 0x0f;  // local_20
+	LevelDifficultyParams wallParams = getDifficultyParams();
+	int wallDamage = (wallParams.dodgeDamage >= 0) ? wallParams.dodgeDamage : 0;
 
+	for (int i = 0; i < _primaryZoneCount; i++) {
+		CollisionZone &zone = _primaryZones[i];
+		if (!zone.active)
+			continue;
+
+		checkHandler7TopBoundary(zone, vMargin, wallDamage, warningMask);
+		checkHandler7BottomBoundary(zone, vMargin, wallDamage, warningMask);
+		checkHandler7LeftBoundary(zone, hMargin, wallDamage);
+		checkHandler7RightBoundary(zone, hMargin, wallDamage);
+	}
+}
+
+void InsaneRebel2::renderHandler7WarningCues(byte *renderBitmap, int pitch, int width, int height, int32 curFrame, uint16 warningMask) {
 	// FUN_40E35E tail: draw proximity danger shadow cues when enabled by frame/flags.
 	// Note: These are cue sprites (often perceived as "shadows"), not the aiming reticle.
 	LevelDifficultyParams dparams = getDifficultyParams();
@@ -1920,6 +1936,29 @@ void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, in
 	}
 }
 
+void InsaneRebel2::checkHandler7CollisionZones(byte *renderBitmap, int pitch, int width, int height, int32 curFrame) {
+	// FUN_40E35E - Handler 7 per-frame collision system.
+	// Uses ship position (_flyShipScreenX/_flyShipScreenY) in raw buffer coords.
+	// Two modes depending on _flyControlMode:
+	//   Mode 0/2: Obstacle collision using SECONDARY zones (inside quad = hit)
+	//   Mode 1/3: Wall/boundary collision using PRIMARY zones (per-edge push-back)
+
+	// Note: _hitCooldown is decremented in renderSpaceExplosions (FUN_40F1C5)
+	// to match the original where the decrement happens during rendering.
+	//
+	// local_c in FUN_40E35E: proximity mask for nearby danger-zone shadow cues.
+	// bit 0=left, bit 1=right, bit 2=top, bit 3=bottom
+	uint16 warningMask = 0;
+
+	if (_flyControlMode == 0 || _flyControlMode == 2) {
+		checkHandler7ObstacleZones(warningMask);
+	} else {
+		checkHandler7BoundaryZones(warningMask);
+	}
+
+	renderHandler7WarningCues(renderBitmap, pitch, width, height, curFrame, warningMask);
+}
+
 // renderNutSprite -- Draw a NUT sprite with transparency.
 void InsaneRebel2::renderNutSprite(byte *dst, int pitch, int width, int height, int x, int y, NutRenderer *nut, int spriteIdx) {
 	renderNutSpriteMirrored(dst, pitch, width, height, x, y, nut, spriteIdx, false);


Commit: 1da3f8d5805fc65ce0c145468135cc7cf7d35be9
    https://github.com/scummvm/scummvm/commit/1da3f8d5805fc65ce0c145468135cc7cf7d35be9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:59+02:00

Commit Message:
SCUMM: RA2: Use original cursor instead of generic one

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


diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 3fb850bc78a..8b4d436768d 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -540,7 +540,7 @@ public:
 
 	void updatePostRenderScroll(int width, int height);
 	void updatePostRenderDeath();
-	void showPostRenderMenuCursor();
+	void renderPostRenderMenuCursor(byte *renderBitmap, int pitch, int width, int height);
 	bool handlePostRenderMenuModes(byte *renderBitmap, int pitch, int width, int height, bool introPlaying);
 	bool handlePostRenderIntro(byte *renderBitmap, int pitch, int width, int height, int32 curFrame);
 	void renderGameplayPostFrame(byte *renderBitmap, int pitch, int width, int height,
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 04ce01e7f48..060ca42f8fb 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -24,7 +24,6 @@
 #include "common/util.h"
 
 #include "graphics/cursorman.h"
-#include "graphics/wincursor.h"
 
 #include "scumm/scumm_v7.h"
 
@@ -2109,12 +2108,47 @@ void InsaneRebel2::updatePostRenderDeath() {
 	}
 }
 
-// showPostRenderMenuCursor -- Restore the default cursor for menu videos.
-void InsaneRebel2::showPostRenderMenuCursor() {
-	Graphics::Cursor *cursor = Graphics::makeDefaultWinCursor();
-	CursorMan.replaceCursor(cursor);
-	delete cursor;
-	CursorMan.showMouse(true);
+// renderPostRenderMenuCursor -- Draw RA2's software cursor for menu videos.
+void InsaneRebel2::renderPostRenderMenuCursor(byte *renderBitmap, int pitch, int width, int height) {
+	// Original menu stages call FUN_0042a6d0() to show the game's software
+	// cursor object. Slot 0 is initialized by FUN_0042a660() from the cursor
+	// table embedded in RA2WIN95.EXE at VA 0x482f30 (7x10, hotspot 0,0).
+	// FUN_0042a660() passes 1 as the transparent color to FUN_00430380().
+	static const byte kRa2MenuCursor[] = {
+		 0,  0,  1,  1,  1,  1,  1,
+		 0, 15,  0,  1,  1,  1,  1,
+		 0, 15, 15,  0,  1,  1,  1,
+		 0, 15, 15, 15,  0,  1,  1,
+		 0, 15, 15, 15, 15,  0,  1,
+		 0, 15, 15, 15, 15, 15,  0,
+		 0, 15, 15, 15,  0,  0,  0,
+		 0, 15,  0, 15, 15,  1,  1,
+		 0,  0,  0,  0, 15,  0,  1,
+		 1,  1,  1,  0,  0,  0,  1
+	};
+	const int cursorWidth = 7;
+	const int cursorHeight = 10;
+
+	CursorMan.showMouse(false);
+	_vm->_system->showMouse(false);
+
+	const int cursorX = _vm->_mouse.x;
+	const int cursorY = _vm->_mouse.y;
+	for (int y = 0; y < cursorHeight; y++) {
+		int dstY = cursorY + y;
+		if (dstY < 0 || dstY >= height)
+			continue;
+
+		for (int x = 0; x < cursorWidth; x++) {
+			int dstX = cursorX + x;
+			if (dstX < 0 || dstX >= width)
+				continue;
+
+			byte color = kRa2MenuCursor[y * cursorWidth + x];
+			if (color != 1)
+				renderBitmap[dstY * pitch + dstX] = color;
+		}
+	}
 }
 
 // handlePostRenderMenuModes -- Process menu-like videos drawn during post-rendering.
@@ -2127,13 +2161,12 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
 	// Handle pilot selection input and rendering (FUN_00414A41).
 	// This is the pilot/save slot selection screen with centered menu.
 	if (pilotSelectMode) {
-		showPostRenderMenuCursor();
-
 		// Process pilot selection input - emulates FUN_00414A41 input handling.
 		int selection = processLevelSelectInput();
 
 		// Draw pilot selection overlay - centered menu like main menu.
 		drawLevelSelectOverlay(renderBitmap, pitch, width, height);
+		renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
 
 		// If a selection was confirmed, signal video to stop.
 		if (selection >= 0) {
@@ -2149,8 +2182,6 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
 	// Handle chapter selection input and rendering (FUN_00415CF8).
 	// This is the actual level/chapter selection screen with preview and password.
 	if (chapterSelectMode) {
-		showPostRenderMenuCursor();
-
 		// O_LEVEL.SAN provides the background with chapter preview thumbnails.
 		// The FOBJ offset system (set in procPreRendering) scrolls the correct preview
 		// into the preview box area. No black fill needed — video frame shows through.
@@ -2160,6 +2191,7 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
 
 		// Draw chapter selection overlay - emulates FUN_00415CF8 rendering.
 		drawChapterSelectOverlay(renderBitmap, pitch, width, height);
+		renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
 
 		// If a selection was confirmed, signal video to stop.
 		if (selection >= 0) {
@@ -2182,15 +2214,11 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
 	if (introPlaying && _gameState == kStateOptions) {
 		processOptionsInput();
 		drawOptionsOverlay(renderBitmap, pitch, width, height);
+		renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
 		return true;
 	}
 
 	if (menuMode) {
-		// The original game uses the standard Windows arrow cursor (IDC_ARROW)
-		// loaded via LoadCursorA(NULL, 0x7f00) in FUN_420C70.decompiled.txt.
-		// MSTOVER.NUT is a background overlay, NOT a cursor.
-		showPostRenderMenuCursor();
-
 		// Process menu input during each frame.
 		int selection = processMenuInput();
 
@@ -2214,6 +2242,7 @@ bool InsaneRebel2::handlePostRenderMenuModes(byte *renderBitmap, int pitch, int
 
 		// Draw menu selection overlay.
 		drawMenuOverlay(renderBitmap, pitch, width, height);
+		renderPostRenderMenuCursor(renderBitmap, pitch, width, height);
 
 		// If a selection was confirmed, signal video to stop.
 		if (selection >= 0) {


Commit: d027a4158db545ab2fe5c1a72e353dad9eea8b25
    https://github.com/scummvm/scummvm/commit/d027a4158db545ab2fe5c1a72e353dad9eea8b25
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:09:59+02:00

Commit Message:
SCUMM: RA2: Render full shooting rays

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


diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 2a050339962..fd69da34581 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -1578,14 +1578,11 @@ bool InsaneRebel2::loadOpcode8EdgeTable(Common::SeekableReadStream &b, int64 sta
 	// If so, loads a per-level 256x256 color blend table from the IACT chunk data.
 	// This table controls the edge glow color of laser beams (e.g. red vs green).
 	// Data starts at byte offset 18 in the IACT chunk (in_stack_00000014 + 9 shorts).
-	if (par4 == 1000 && remaining >= 18 + 8 + 32896) {
-		// Layout: 18 bytes IACT header params already consumed by caller,
-		// but 'b' is positioned at startPos which is after par1..par4.
-		// The original code passes (param + 9 shorts) = data at byte offset 18 from chunk start.
-		// Since our stream starts after the 6 par shorts (12 bytes), the data is at offset 6 from startPos.
+	// The stream is positioned after par1..par4 (8 bytes), so retail's +18 is startPos + 10.
+	if (par4 == 1000 && remaining >= 10 + 8 + 32896) {
 		byte *edgeData = (byte *)malloc(8 + 32896);
 		if (edgeData) {
-			b.seek(startPos + 6);  // Skip 3 remaining shorts (par2, par3, par4 already read; 3 more padding shorts)
+			b.seek(startPos + 10);
 			b.read(edgeData, 8 + 32896);
 			initEdgeTable(edgeData);
 			free(edgeData);
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 060ca42f8fb..5746004151c 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -1326,13 +1326,9 @@ void InsaneRebel2::drawLaserBeam(byte *dst, int pitch, int width, int height,
 	byte *texPixels = _laserTexture.pixels;
 
 	// FUN_0040BBF6 line 23: sVar7 = (thickness * animFrame * 16) / maxFrames
-	// Tuned beam segment spacing: 60% of original.
-	constexpr int kBeamAnimScaleNumerator = 48;   // 16 * 0.6 * 5
-	constexpr int kBeamAnimScaleDenominator = 5;
 	if (maxFrames == 0)
 		maxFrames = 1;
-	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * kBeamAnimScaleNumerator) /
-	                      ((int)maxFrames * kBeamAnimScaleDenominator));
+	int16 sVar7 = (int16)(((int)thickness * (int)animFrame * 16) / (int)maxFrames);
 
 	// FUN_0040BBF6 lines 24-25: Calculate delta with scaling
 	int16 dx = targetX - gunX;


Commit: 2771d282b951bc1492feaf3b7ef9c1e6af8f5767
    https://github.com/scummvm/scummvm/commit/2771d282b951bc1492feaf3b7ef9c1e6af8f5767
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:00+02:00

Commit Message:
SCUMM: RA1: Improve level 8 UI rendering

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra1.h


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index b66c6d91bf1..6d8233ab8ea 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -113,12 +113,6 @@ const int16 kLevel7BranchThreshold[6] = {
 	0, 170, 170, 160, 160, 160
 };
 
-const int16 kLevel8BranchFrames[3][3] = {
-	{ 2588, 1709,  262 },
-	{ 2323, 1444,   -2 },
-	{  877,   -2,   -2 }
-};
-
 inline bool isLevel4DamageLatch(uint16 code) {
 	switch (code) {
 	case 0x0008:
@@ -501,14 +495,15 @@ void InsaneRebel1::checkDynamicLevelBranch() {
 	if (!_interactiveVideoActive || _levelRouteIndex < 0)
 		return;
 
-	if (_currentLevel == 6 && _pendingRouteIndex >= 0) {
+	if ((_currentLevel == 6 || _currentLevel == 7) && _pendingRouteIndex >= 0) {
+		const uint32 routeFrame = (_currentLevel == 7) ? (uint32)_gameCounter : _frameCounter;
 		if (!_vm->_smushVideoShouldFinish &&
 			_pendingRouteCutoverFrame >= 0 &&
-			_frameCounter >= (uint32)_pendingRouteCutoverFrame) {
+			routeFrame >= (uint32)_pendingRouteCutoverFrame) {
 			_vm->_smushVideoShouldFinish = true;
-			debug(1, "RA1 L7 cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
-				_levelRouteIndex, _pendingRouteIndex, (unsigned)_frameCounter,
-				(int)_pendingRouteStartFrame);
+			debug(1, "RA1 L%d cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
+				_currentLevel + 1, _levelRouteIndex, _pendingRouteIndex,
+				(unsigned)routeFrame, (int)_pendingRouteStartFrame);
 		}
 		return;
 	}
@@ -539,45 +534,9 @@ void InsaneRebel1::checkDynamicLevelBranch() {
 		}
 	}
 
-	if (_currentLevel == 7) {
-		const int route = CLIP<int>(_levelRouteIndex, 0, 2);
-		const int frame = (int)_frameCounter;
-		const int leftBlockedFrame = kLevel8BranchFrames[route][2];
-		const int rightBlockedFrame = kLevel8BranchFrames[route][1];
-		const bool shotEdge = _playerFired && _fireCooldown == 0;
-		int nextRoute = -1;
-
-		for (int i = 0; i < 3; ++i) {
-			const int triggerFrame = kLevel8BranchFrames[route][i];
-			if (triggerFrame < 0)
-				continue;
-
-			if (shotEdge && frame > triggerFrame - 0x32 && frame <= triggerFrame)
-				_levelRouteChoice = (_shipPosX < kRA1CenterX) ? 1 : 2;
-
-			if (frame != triggerFrame)
-				continue;
-
-			const bool chooseLeft = (_levelRouteChoice == 1) ||
-				((_shipPosX < kRA1CenterX) && (_levelRouteChoice != 2));
-			if (chooseLeft) {
-				if (frame != leftBlockedFrame)
-					nextRoute = 1;
-			} else {
-				if (frame != rightBlockedFrame)
-					nextRoute = 2;
-			}
-			_levelRouteChoice = 0;
-			break;
-		}
-
-		if (nextRoute >= 0 && nextRoute != route) {
-			_pendingRouteIndex = nextRoute;
-			_vm->_smushVideoShouldFinish = true;
-			debug(1, "RA1 L8 branch: route=%d -> %d at frame=%u shipX=%d",
-				route, nextRoute, (unsigned)_frameCounter, _shipPosX);
-		}
-	}
+	// Level 8 owns its branch choice in updateLevel8WalkerState(), where the
+	// original RunLevel8Flow also draws the timer/arrows and updates the local
+	// choice variable. This function only performs the delayed route cutover.
 }
 
 void InsaneRebel1::projectGameplayPoint(int16 &x, int16 &y) const {
@@ -1208,16 +1167,17 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		_damageFlags |= 0x20;
 
 	if (_currentLevel == 7) {
+		const uint16 walkerFrame = (uint16)_gameCounter;
 		bool walkerHazard = false;
 		switch (CLIP<int>(_levelRouteIndex, 0, 2)) {
 		case 0:
-			walkerHazard = hasLevel8PerspectiveHazardRoute0((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+			walkerHazard = hasLevel8PerspectiveHazardRoute0(walkerFrame, _perspectiveX, _perspectiveY);
 			break;
 		case 1:
-			walkerHazard = hasLevel8PerspectiveHazardRoute1((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+			walkerHazard = hasLevel8PerspectiveHazardRoute1(walkerFrame, _perspectiveX, _perspectiveY);
 			break;
 		case 2:
-			walkerHazard = hasLevel8PerspectiveHazardRoute2((uint16)_frameCounter, _perspectiveX, _perspectiveY);
+			walkerHazard = hasLevel8PerspectiveHazardRoute2(walkerFrame, _perspectiveX, _perspectiveY);
 			break;
 		}
 
@@ -1301,6 +1261,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		_level2JoystickFilteredX = 0;
 		_level2JoystickFilteredY = 0;
 	}
+	_inputAxisDeltaX = inputX;
 
 	debug("RA1 asteroid input: source=%s axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
 		inputSourceName,
@@ -1588,6 +1549,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
 		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_inputAxisDeltaX = 0;
 		_avgInputX = 0;
 		_avgInputY = 0;
 		_mouseOffsetX = 0;
@@ -1622,6 +1584,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 		// Field1 == 0 corresponds to baseline recenter behavior in the original.
 		if ((int32)param1 == 0) {
+			_perspectiveX = 0x20;
+			_perspectiveY = 0x17;
 			_shipPosX = kRA1CenterX;
 			_shipPosY = kRA1CenterY;
 		}
@@ -1630,9 +1594,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_frameGameOpcodeMask = 0;
 		_frameDispatchFlags = 0;
 		resetProjectionTable();
+		if ((int32)param1 == 0)
+			syncViewportOffset(true);
 
-		// RunLevel8Flow seeds the walker armor mask after the interactive segment
-		// has entered its runtime reset path, not before playback begins.
+		// Original RunLevel8Flow initializes its separate g_level8HitboxBuffer
+		// after the first L8PLAY runtime reset. We fold that mask into the
+		// secondary half of _frameObjectState; route resumes preserve it.
 		if (_currentLevel == 7)
 			memset(_frameObjectState + 150, 0xFF, 150);
 
@@ -1792,6 +1759,11 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			}
 		}
 		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+		if (!_asteroidPhysicsUpdatedThisFrame) {
+			updateAsteroidPhysics();
+			_asteroidPhysicsUpdatedThisFrame = true;
+			syncViewportOffset(true);
+		}
 		break;
 
 	case 0x5A:
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index f108f223686..9dc75ef8307 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -242,6 +242,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 	memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
 	memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+	_inputAxisDeltaX = 0;
 	_avgInputX = 0;
 	_avgInputY = 0;
 	_mouseOffsetX = 0;
@@ -267,6 +268,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_activeGameOpcode = 0;
 	_frameGameOpcodeMask = 0;
 	_frameDispatchFlags = 0;
+	_asteroidPhysicsUpdatedThisFrame = false;
 
 	_health = kMaxHealth;
 	_lives = 3;
@@ -293,7 +295,6 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_pendingRouteIndex = -1;
 	_pendingRouteStartFrame = 0;
 	_pendingRouteCutoverFrame = -1;
-	_levelRouteChoice = 0;
 	_levelGameplayPhase = 0;
 	_level5SuccessFramesRemaining = 0;
 	_menuActive = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 5a85489b373..53e7bb55573 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -161,8 +161,10 @@ private:
 	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
 							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
-	void renderLevel8Overlay(byte *dst, int pitch, int width, int height);
+	void renderLevel8Overlay(byte *dst, int pitch, int width, int height,
+		int viewportX, int viewportY);
 	void updateLevel8WalkerState();
+	void syncViewportOffset(bool usePerspectiveViewport);
 	void renderSprite(byte *dst, int pitch, int width, int height,
 					  int x, int y, const RA1Sprite &sprite);
 	void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
@@ -246,6 +248,7 @@ private:
 	int16 _inputHistoryY[kInputHistorySize];  // 0x7594: vertical input history
 	int16 _viewHistoryX[kInputHistorySize];   // 0x75A8: viewport horizontal history
 	int16 _viewHistoryY[kInputHistorySize];   // 0x75BC: viewport vertical history
+	int16 _inputAxisDeltaX; // Current 0x0B horizontal input sample, before history averaging
 	int16 _avgInputX;    // smoothed horizontal input (clamped to [-0xA0, 0xA0])
 	int16 _avgInputY;    // smoothed vertical input (clamped to [-0x46, 0x41])
 	int16 _mouseOffsetX; // 0x9762-style accumulated recenter offset in DOS 640-space
@@ -300,6 +303,7 @@ private:
 	uint16 _activeGameOpcode;
 	uint32 _frameGameOpcodeMask;
 	uint16 _frameDispatchFlags;
+	bool _asteroidPhysicsUpdatedThisFrame;
 
 	// Difficulty (0=easy, 1=normal, 2=hard) — matches original DAT_22BC
 	int _difficulty;
@@ -384,8 +388,7 @@ private:
 	int _levelRouteIndex;        // Current mid-level route/segment for branching levels
 	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
 	int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
-	int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (Level 7 uses branchFrame + 7)
-	int _levelRouteChoice;       // Level-local pending branch choice (0=none, 1=left, 2=right)
+	int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
 	int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
 
 	// Main menu / options state
@@ -494,19 +497,6 @@ private:
 	static const int16 kWalkerAttackWindow2[3];
 	static const int16 kWalkerAttackWindow3[3];
 
-	// Per-route damage frame tables — FUN_12fe1/FUN_130c9/FUN_13195
-	// Each entry: {frameNumber, hitboxType} where hitboxType determines the check
-	struct WalkerDamageFrame {
-		int16 frame;
-		int16 type;    // 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
-	};
-	static const WalkerDamageFrame kWalkerDamageRoute0[];
-	static const WalkerDamageFrame kWalkerDamageRoute1[];
-	static const WalkerDamageFrame kWalkerDamageRoute2[];
-	static const int kWalkerDamageRoute0Count;
-	static const int kWalkerDamageRoute1Count;
-	static const int kWalkerDamageRoute2Count;
-
 	static const int kFrameObjectStateBytes = 300;
 	byte _frameObjectState[kFrameObjectStateBytes];
 };
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index c54c7580478..183ec966e64 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -365,6 +365,7 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	_frameGameOpcodeMask = 0;
 	_frameDispatchFlags = 0;
+	_asteroidPhysicsUpdatedThisFrame = false;
 
 	if (_interactiveVideoActive && _player) {
 		const bool usePerspectiveViewport =
@@ -387,6 +388,28 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	}
 }
 
+void InsaneRebel1::syncViewportOffset(bool usePerspectiveViewport) {
+	if (!_player)
+		return;
+
+	if (!usePerspectiveViewport) {
+		ra1Player()->_ra1ViewportOffsetX = 0;
+		ra1Player()->_ra1ViewportOffsetY = 0;
+		return;
+	}
+
+	ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
+	ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
+
+	// SetCameraOffset() applies random shake to the camera value used by the
+	// visible source-window origin. Store it once so FTCH restore, overlays, and
+	// the final crop share one viewport origin for this frontend frame.
+	if (_screenFlash > 0) {
+		ra1Player()->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+		ra1Player()->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+	}
+}
+
 void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32 setupsan12,
 	int32 setupsan13, int32 curFrame, int32 maxFrame) {
 	if (_menuActive && renderBitmap) {
@@ -432,7 +455,11 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 			(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
 	if (asteroidMode) {
 		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
-		updateAsteroidPhysics();
+		if (!_asteroidPhysicsUpdatedThisFrame) {
+			updateAsteroidPhysics();
+			_asteroidPhysicsUpdatedThisFrame = true;
+			syncViewportOffset(true);
+		}
 
 		// DOS 0x0B loops test health after each frontend frame and leave the
 		// interactive movie as soon as it drops below zero. Mirror that here so
@@ -487,22 +514,13 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	// GAME handlers in the original update FUN_224FD during the same frame that
 	// the new control state is computed. Sync the current frame's viewport window
-	// before HUD/screen copy so 0x0B doesn't lag one frame behind the mouse.
+	// before HUD/screen copy so overlays and final crop observe the same camera.
 	// On-foot mode uses SetCameraOffset(0,0) — no viewport crop.
 	if (_player) {
 		if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
-			ra1Player()->_ra1ViewportOffsetX = 0;
-			ra1Player()->_ra1ViewportOffsetY = 0;
-		} else {
-			ra1Player()->_ra1ViewportOffsetX = _perspectiveX;
-			ra1Player()->_ra1ViewportOffsetY = _perspectiveY;
-		}
-
-		// Screen shake — SetCameraOffset (0x224FD): random [-2,+2] jitter when
-		// _screenFlash > 0. Original uses RandScaleByte(5) - 2 for each axis.
-		if (_screenFlash > 0) {
-			ra1Player()->_ra1ViewportOffsetX += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
-			ra1Player()->_ra1ViewportOffsetY += (int16)(_vm->_rnd.getRandomNumber(4) - 2);
+			syncViewportOffset(false);
+		} else if (!asteroidMode) {
+			syncViewportOffset(true);
 		}
 	}
 
@@ -563,7 +581,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// game loop. We call it from procPostRendering when _currentLevel == 7.
 	if (_currentLevel == 7) {
 		updateLevel8WalkerState();
-		renderLevel8Overlay(renderBitmap, pitch, width, height);
+		const int viewportX = _player ? ra1Player()->_ra1ViewportOffsetX : 0;
+		const int viewportY = _player ? ra1Player()->_ra1ViewportOffsetY : 0;
+		renderLevel8Overlay(renderBitmap, pitch, width, height, viewportX, viewportY);
 	}
 
 	renderHUD(renderBitmap, pitch, width, height);
@@ -1331,32 +1351,13 @@ const int16 InsaneRebel1::kWalkerAttackWindow1[3] = { 2588, 2323, 877 };
 const int16 InsaneRebel1::kWalkerAttackWindow2[3] = { 1709, 1444, -2 };
 const int16 InsaneRebel1::kWalkerAttackWindow3[3] = { 262, -2, -2 };
 
-// Per-route walker damage frame tables — FUN_12fe1/FUN_130c9/FUN_13195.
-// type: 0=proximity(41,32), 1=offsetY(15), 2=offsetX(24), 3=proximity(40,31), 4=offsetY(31)
-const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute0[] = {
-	{0x00CD, 2}, {0x00EF, 2}, {0x0294, 1}, {0x03A2, 2},
-	{0x04BE, 0}, {0x05C9, 2}, {0x076C, 0}, {0x085A, 4}, {0x096F, 2}
-};
-const int InsaneRebel1::kWalkerDamageRoute0Count = ARRAYSIZE(kWalkerDamageRoute0);
-
-const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute1[] = {
-	{0x0189, 1}, {0x0297, 2}, {0x03B3, 0}, {0x04BE, 4},
-	{0x0661, 0}, {0x074F, 4}, {0x0864, 4}
-};
-const int InsaneRebel1::kWalkerDamageRoute1Count = ARRAYSIZE(kWalkerDamageRoute1);
-
-const InsaneRebel1::WalkerDamageFrame InsaneRebel1::kWalkerDamageRoute2[] = {
-	{0x00BB, 0}, {0x01A9, 4}, {0x02BE, 4}
-};
-const int InsaneRebel1::kWalkerDamageRoute2Count = ARRAYSIZE(kWalkerDamageRoute2);
-
-// updateLevel8WalkerState — Per-frame walker health + attack window + damage logic.
+// updateLevel8WalkerState — Per-frame walker health + attack window logic.
 // Called from procPostRendering when _currentLevel == 7.
 void InsaneRebel1::updateLevel8WalkerState() {
 	// Walker health computation — RunLevel8Flow (0x18634-0x18655)
 	if (_walkerHealth >= 11) {
 		_walkerHealth = (int16)(100 - (_killCount + (_killCount >> 2)));
-	} else if (_walkerHealth > 0 && (_frameCounter & 3) == 0) {
+	} else if (_walkerHealth > 0 && (_gameCounter & 3) == 0) {
 		_walkerHealth--;
 	}
 
@@ -1366,40 +1367,10 @@ void InsaneRebel1::updateLevel8WalkerState() {
 		return;
 	}
 
-	// Per-route damage check — FUN_12fe1/FUN_130c9/FUN_13195
+	// FUN_12fe1/FUN_130c9/FUN_13195 walker collision damage is applied from
+	// updateAsteroidPhysics(), where the 0x0B damage flags are consumed.
 	int route = CLIP(_levelRouteIndex, 0, 2);
-	const WalkerDamageFrame *table;
-	int tableCount;
-	switch (route) {
-	case 0:  table = kWalkerDamageRoute0; tableCount = kWalkerDamageRoute0Count; break;
-	case 1:  table = kWalkerDamageRoute1; tableCount = kWalkerDamageRoute1Count; break;
-	default: table = kWalkerDamageRoute2; tableCount = kWalkerDamageRoute2Count; break;
-	}
-
-	uint16 fc = (uint16)_frameCounter;
-	for (int i = 0; i < tableCount; i++) {
-		if (fc != (uint16)table[i].frame)
-			continue;
-
-		int16 dx = _shipPosX - kRA1CenterX;  // distance from center
-		int16 dy = _shipPosY - kRA1CenterY;
-		if (dx < 0) dx = -dx;
-		if (dy < 0) dy = -dy;
-
-		bool hit = false;
-		switch (table[i].type) {
-		case 0: hit = (dx < 0x29 && dy < 0x20); break;  // proximity(41,32)
-		case 1: hit = (dy < 0x0F); break;                // offsetY(15)
-		case 2: hit = (dx < 0x18); break;                // offsetX(24)
-		case 3: hit = (dx < 0x28 && dy < 0x1F); break;   // proximity(40,31)
-		case 4: hit = (dy < 0x1F); break;                // offsetY(31)
-		default: break;
-		}
-
-		if (hit)
-			_damageFlags |= 0x20;
-		break;
-	}
+	uint16 fc = (uint16)_gameCounter;
 
 	// Attack window logic — RunLevel8Flow (0x18778-0x18B4A)
 	const int16 *windows[3] = {
@@ -1440,12 +1411,12 @@ void InsaneRebel1::updateLevel8WalkerState() {
 
 		if (inDirectionalPhase) {
 			// Player can choose direction during last 50 frames
-			if (_playerFired && _avgInputX == 0) {
+			if (_playerFired && _inputAxisDeltaX == 0) {
 				_walkerBranchChoice = (_shipPosX < 0xA0) ? 1 : 2;
 			}
 		} else {
 			// Torpedo sound every 8 frames during targeting phase
-			if ((_frameCounter & 7) == 0)
+			if ((_gameCounter & 7) == 0)
 				playSfx(kSfxLockOn, 127, 0);
 		}
 	}
@@ -1471,9 +1442,13 @@ void InsaneRebel1::updateLevel8WalkerState() {
 				newRoute = 2;
 		}
 
-		if (newRoute != 0) {
+		if (newRoute != 0 && newRoute != route) {
 			_pendingRouteIndex = newRoute;
-			_vm->_smushVideoShouldFinish = true;
+			_pendingRouteCutoverFrame = _gameCounter + 7;
+			_pendingRouteStartFrame = _pendingRouteCutoverFrame;
+			debug(1, "RA1 L8 branch: route=%d -> %d at frame=%u shipX=%d resumeTimelineFrame=%d cutoverFrame=%d",
+				route, newRoute, (unsigned)_gameCounter, _shipPosX,
+				(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
 		}
 		_walkerBranchChoice = 0;
 		break;
@@ -1482,28 +1457,27 @@ void InsaneRebel1::updateLevel8WalkerState() {
 
 // renderLevel8Overlay — Walker-specific UI from RunLevel8Flow (0x18660-0x18A7E).
 // Draws walker health %, attack timer, directional arrows, and target reticle.
-// Original draws via DrawStringEx/FormatAndDrawText in screen-space (within viewport).
-// We draw into 384x242 buffer, so add viewport offset to convert screen→buffer coords.
-void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int height) {
+// Original RunLevel8Flow projects these fixed cockpit-panel points and then uses
+// 1/4 parallax compensation. We draw into the 384x242 SMUSH buffer, so add the
+// viewport offset to convert the original screen-space result back to buffer
+// coordinates before the final RA1 crop.
+void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int height,
+		int viewportX, int viewportY) {
 	if (_currentLevel != 7)
 		return;
 
-	// Viewport offset: screen-space → buffer-space (same approach as renderHUD)
-	int viewX = _player ? ra1Player()->_ra1ViewportOffsetX : _perspectiveX;
-	int viewY = _player ? ra1Player()->_ra1ViewportOffsetY : _perspectiveY;
-
-	// Walker health display — "<<WALKER %d%%" at projected (0x61, 0x8D)
-	// Blinks when health < 16: only drawn when (frameCounter & 2) != 0
-	if (_walkerHealth > 0 && (_walkerHealth >= 16 || (_frameCounter & 2) != 0)) {
+	// Walker health display — "<<WALKER %d%%" at projected cockpit panel point (0x61, 0x8D).
+	// Blinks when health < 16: only drawn when (GAME counter & 2) != 0.
+	if (_walkerHealth > 0 && (_walkerHealth >= 16 || (_gameCounter & 2) != 0)) {
 		int16 projX = 0x61, projY = 0x8D;
 		projectGameplayPoint(projX, projY);
-		// Apply 1/4 parallax compensation (original: 0x61 - (projX - 0x61) >> 2)
 		projX = (int16)(0x61 - ((projX - 0x61) >> 2));
 		projY = (int16)(0x8D - ((projY - 0x8D) >> 2));
 
 		char walkerStr[24];
-		Common::sprintf_s(walkerStr, "<<WALKER %d%%%%", (int)_walkerHealth);
-		drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, walkerStr);
+		Common::sprintf_s(walkerStr, "<<WALKER %d%%", (int)_walkerHealth);
+		drawFontBankString(dst, pitch, width, height,
+			viewportX + projX, viewportY + projY, walkerStr);
 	}
 
 	// Attack window overlay (timer + arrows/reticle)
@@ -1513,7 +1487,7 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 		&kWalkerAttackWindow2[route],
 		&kWalkerAttackWindow3[route]
 	};
-	int16 frameNum = (int16)(uint16)_frameCounter;
+	int16 frameNum = (int16)(uint16)_gameCounter;
 
 	bool inWindow = false;
 	bool inDirectionalPhase = false;
@@ -1531,7 +1505,7 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 	if (!inWindow || _walkerBranchChoice != 0)
 		return;
 
-	// Timer countdown — "<<TIME %d" at projected (0x62, 0x9C)
+	// Timer countdown — "<<TIME %d" at projected cockpit panel point (0x62, 0x9C).
 	{
 		int16 projX = 0x62, projY = 0x9C;
 		projectGameplayPoint(projX, projY);
@@ -1540,7 +1514,8 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 
 		char timerStr[16];
 		Common::sprintf_s(timerStr, "<<TIME %d", (int)_walkerTimer);
-		drawFontBankString(dst, pitch, width, height, viewX + projX, viewY + projY, timerStr);
+		drawFontBankString(dst, pitch, width, height,
+			viewportX + projX, viewportY + projY, timerStr);
 	}
 
 	if (inDirectionalPhase) {
@@ -1553,21 +1528,21 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 		if (_shipPosX < 0xA0) {
 			// Left arrow "<<v"
 			drawFontBankString(dst, pitch, width, height,
-				viewX + 0xA6 + px, viewY + 0x92 + py, "<<v");
+				viewportX + 0xA6 + px, viewportY + 0x92 + py, "<<v");
 		} else {
 			// Right arrow "<<u"
 			drawFontBankString(dst, pitch, width, height,
-				viewX + 0xA8 - px, viewY + 0x93 - py, "<<u");
+				viewportX + 0xA8 - px, viewportY + 0x93 - py, "<<u");
 		}
 	} else {
 		// Target reticle — "<<w" at projected (0xA9, 0x9A), blinks on (frame & 4)
-		if ((_frameCounter & 4) == 0) {
+		if ((_gameCounter & 4) == 0) {
 			int16 projX = 0, projY = 0;
 			projectGameplayPoint(projX, projY);
 			int16 drawX = (int16)(0xA9 - (projX >> 2));
 			int16 drawY = (int16)(0x9A - (projY >> 2));
 			drawFontBankString(dst, pitch, width, height,
-				viewX + drawX, viewY + drawY, "<<w");
+				viewportX + drawX, viewportY + drawY, "<<w");
 		}
 	}
 }
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 68d9c2f404a..7486d22755d 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -143,6 +143,71 @@ int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 t
 	return result;
 }
 
+int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *filename, int32 targetCounter, int32 &localFrame) {
+	localFrame = 0;
+	if (targetCounter <= 0)
+		return 0;
+
+	ScummFile *file = vm->instantiateScummFile();
+	if (!vm->openFile(*file, Common::Path(filename))) {
+		delete file;
+		return -1;
+	}
+
+	int32 result = -1;
+	if (file->size() >= 8) {
+		file->readUint32BE();
+		file->readUint32BE();
+
+		int32 frameIndex = 0;
+		while (file->pos() + 8 <= file->size()) {
+			const int32 chunkOffset = (int32)file->pos();
+			const uint32 chunkTag = file->readUint32BE();
+			const int32 chunkSize = (int32)file->readUint32BE();
+			const int32 chunkDataOffset = (int32)file->pos();
+			const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
+			if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
+				break;
+
+			if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
+				const int32 frameEnd = chunkDataOffset + chunkSize;
+				while (file->pos() + 8 <= frameEnd) {
+					const int32 subChunkOffset = (int32)file->pos();
+					const uint32 subChunkTag = file->readUint32BE();
+					const int32 subChunkSize = (int32)file->readUint32BE();
+					const int32 subChunkDataOffset = (int32)file->pos();
+					const int32 nextSubChunkOffset = subChunkDataOffset + subChunkSize + ((subChunkSize & 1) ? 1 : 0);
+					if (nextSubChunkOffset < subChunkOffset || nextSubChunkOffset > frameEnd)
+						break;
+
+					if (subChunkTag == MKTAG('G', 'A', 'M', 'E') && subChunkSize >= 8) {
+						const uint32 opcode = file->readUint32BE();
+						const int32 counter = (int32)file->readUint32BE();
+						if (opcode == 0x0B && counter >= targetCounter) {
+							localFrame = frameIndex;
+							result = chunkOffset;
+							break;
+						}
+					}
+
+					file->seek(nextSubChunkOffset, SEEK_SET);
+				}
+
+				if (result >= 0)
+					break;
+
+				frameIndex++;
+			}
+
+			file->seek(nextChunkOffset, SEEK_SET);
+		}
+	}
+
+	file->close();
+	delete file;
+	return result;
+}
+
 // ---------------------------------------------------------------------------
 // Game flow (matching original at 0x15597)
 // ---------------------------------------------------------------------------
@@ -891,6 +956,7 @@ bool InsaneRebel1::runLevel8() {
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
 		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+		_inputAxisDeltaX = 0;
 		_avgInputX = 0;
 		_avgInputY = 0;
 
@@ -900,24 +966,37 @@ bool InsaneRebel1::runLevel8() {
 		_walkerBranchChoice = 0;
 
 		int route = 0;
+		int32 routeStartFrame = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;
 			_pendingRouteIndex = -1;
-			playInteractiveVideo(kLevel8Routes[route]);
+			_pendingRouteStartFrame = routeStartFrame;
+			_pendingRouteCutoverFrame = -1;
+			playInteractiveVideo(kLevel8Routes[route], routeStartFrame);
 			if (_vm->shouldQuit())
 				return false;
 
 			if (_health < 0)
 				break;
 
+			if (_walkerHealth <= 0)
+				break;
+
 			if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
 				break;
 
+			// RunLevel8Flow uses PlayAnmFile(..., g_frameCounter, 1, -1)
+			// when it branches from one walker route to another. That wrapper
+			// keeps the current ANM alive for seven more gameplay frames, then
+			// opens the destination route while preserving the active state.
+			routeStartFrame = _pendingRouteStartFrame;
 			route = _pendingRouteIndex;
 		}
 
 		_levelRouteIndex = -1;
 		_pendingRouteIndex = -1;
+		_pendingRouteStartFrame = 0;
+		_pendingRouteCutoverFrame = -1;
 
 		if (_health >= 0) {
 			playCinematic("LVL8/L8END.ANM");
@@ -1747,7 +1826,6 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	if (!resumingRoute)
 		clearBit(0);
 	_interactiveVideoActive = true;
-	_levelRouteChoice = 0;
 	if (!resumingRoute) {
 		_onFootInitialized = false;  // Reset so each segment triggers counter==0 init
 		resetFrameObjectState();
@@ -1772,6 +1850,23 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 			debug(1, "RA1 L7 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
 				_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
 		}
+	} else if (_currentLevel == 7 && resumingRoute) {
+		videoOffset = findAnimFrameChunkOffsetByGameCounter(_vm, filename, startFrame, videoStartFrame);
+		if (videoOffset < 0) {
+			debug(1, "RA1 L8 resume: route=%d timelineFrame=%d GAME counter lookup failed",
+				_levelRouteIndex, (int)startFrame);
+			videoStartFrame = startFrame;
+			videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
+		}
+		if (videoOffset < 0) {
+			debug(1, "RA1 L8 resume: route=%d timelineFrame=%d localFrame=%d offset lookup failed",
+				_levelRouteIndex, (int)startFrame, (int)videoStartFrame);
+			videoStartFrame = 0;
+			videoOffset = 0;
+		} else {
+			debug(1, "RA1 L8 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
+				_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
+		}
 	}
 
 	// Center mouse, hide system cursor (we draw our own), lock mouse to window
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 3a2df42c5e4..451b3a0506b 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -37,6 +37,14 @@
 
 namespace Scumm {
 
+enum {
+	kRA1PresentationBorder = 4,
+	kRA1PresentationScreenWidth = 320,
+	kRA1PresentationScreenHeight = 200,
+	kRA1PresentationWidth = kRA1PresentationScreenWidth - kRA1PresentationBorder * 2,
+	kRA1PresentationHeight = kRA1PresentationScreenHeight - kRA1PresentationBorder * 2
+};
+
 static void ra1ApplyCenteredFetchPlacement(InsaneRebel1 *rebel1, int width, int height, int &left, int &top) {
 	int16 centerX = (int16)(left + (width >> 1));
 	int16 centerY = (int16)(top + (height >> 1));
@@ -66,6 +74,8 @@ void SmushPlayerRebel1::initGamePlayerFields() {
 	_ra1CleanFrame = nullptr;
 	_ra1CleanFrameSize = 0;
 	_ra1HasCleanFrame = false;
+	_ra1PresentationBuffer = nullptr;
+	_ra1PresentationBufferSize = 0;
 	_ra1ObjOverlayData = nullptr;
 	_ra1ObjOverlayDataSize = 0;
 	_ra1ObjOverlayCodec = 0;
@@ -85,6 +95,9 @@ void SmushPlayerRebel1::destroyGamePlayerFields() {
 	free(_ra1CleanFrame);
 	_ra1CleanFrame = nullptr;
 	_ra1CleanFrameSize = 0;
+	free(_ra1PresentationBuffer);
+	_ra1PresentationBuffer = nullptr;
+	_ra1PresentationBufferSize = 0;
 }
 
 void SmushPlayerRebel1::resetGameVideoState() {
@@ -120,6 +133,10 @@ void SmushPlayerRebel1::releaseGameVideoState() {
 	_ra1CleanFrame = nullptr;
 	_ra1CleanFrameSize = 0;
 	_ra1HasCleanFrame = false;
+
+	free(_ra1PresentationBuffer);
+	_ra1PresentationBuffer = nullptr;
+	_ra1PresentationBufferSize = 0;
 }
 
 bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -295,30 +312,51 @@ void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, in
 	} while (--height);
 }
 /**
- * RA1 codec 21: Skip/copy line codec (FUN_10D41).
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41). Clip copy runs without
+ * changing source X; stored cockpit patches can legitimately start offscreen.
  */
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch + left;
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufWidth, int bufHeight) {
 	for (int row = 0; row < height; row++) {
-		uint16 lineSize = READ_LE_UINT16(src);
+		const uint16 lineSize = READ_LE_UINT16(src);
 		const byte *lineData = src + 2;
 		const byte *lineEnd = lineData + lineSize;
-		byte *dstRow = dst;
-		int remaining = width;
-		while (remaining > 0 && lineData < lineEnd) {
-			if (lineData + 2 > lineEnd) break;
-			uint16 skip = READ_LE_UINT16(lineData); lineData += 2;
-			dstRow += skip; remaining -= skip;
-			if (remaining <= 0) break;
-			if (lineData + 2 > lineEnd) break;
-			uint16 copyLen = READ_LE_UINT16(lineData) + 1; lineData += 2;
-			int toCopy = MIN<int>(copyLen, remaining);
-			if (lineData + toCopy > lineEnd) toCopy = (int)(lineEnd - lineData);
-			if (toCopy > 0) { memcpy(dstRow, lineData, toCopy); lineData += toCopy; dstRow += toCopy; remaining -= toCopy; }
-			if (copyLen > toCopy) lineData += (copyLen - toCopy);
+		const int dstY = top + row;
+		int srcX = 0;
+
+		while (srcX < width && lineData < lineEnd) {
+			if (lineData + 2 > lineEnd)
+				break;
+			const uint16 skip = READ_LE_UINT16(lineData);
+			lineData += 2;
+			srcX += skip;
+			if (srcX >= width)
+				break;
+
+			if (lineData + 2 > lineEnd)
+				break;
+			const int copyLen = READ_LE_UINT16(lineData) + 1;
+			lineData += 2;
+
+			const int readableLen = MIN<int>(copyLen, (int)(lineEnd - lineData));
+			const int dstStartX = left + srcX;
+			const int dstEndX = dstStartX + readableLen;
+			if (readableLen > 0 && dstY >= 0 && dstY < bufHeight) {
+				const int clippedStartX = MAX(dstStartX, 0);
+				const int clippedEndX = MIN(dstEndX, bufWidth);
+				if (clippedStartX < clippedEndX) {
+					const int srcSkipX = clippedStartX - dstStartX;
+					memcpy(dst + dstY * pitch + clippedStartX,
+						lineData + srcSkipX, clippedEndX - clippedStartX);
+				}
+			}
+
+			lineData += readableLen;
+			srcX += copyLen;
+			if (readableLen < copyLen)
+				break;
 		}
 		src += lineSize + 2;
-		dst += pitch;
 	}
 }
 
@@ -505,12 +543,23 @@ bool SmushPlayerRebel1::handleGameDimensionOverride(int codec, int width, int he
 }
 
 bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+	_ra1FrameSourceSkipY = 0;
+
 	// RA1 additive codec (SKIP_RLE) and scatter (RA1_SCATTER) use absolute
 	// positions — they must NOT be clipped/adjusted.
 	if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
 		return false;
+
+	// RA1 codec 21 is source-X sensitive: generic left clipping would reduce
+	// the destination width without skipping the corresponding source columns.
+	// Keep only the global FOBJ offset here and let the codec clip each run.
+	if (codec == SMUSH_CODEC_LINE_UPDATE) {
+		left += _fobjOffsetX;
+		top += _fobjOffsetY;
+		return false;
+	}
+
 	int sourceSkipY = 0;
-	_ra1FrameSourceSkipY = 0;
 	adjustFrameCoords(left, top, width, height, pitch, &sourceSkipY);
 	if (codec == SMUSH_CODEC_RLE_ALT) {
 		_ra1FrameSourceSkipY = sourceSkipY;
@@ -541,7 +590,8 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 			dataSize, param, parm2, codec);
 		return true;
 	case SMUSH_CODEC_LINE_UPDATE:
-		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch);
+		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch,
+			pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight);
 		return true;
 	case SMUSH_CODEC_SKIP_RLE: {
 		const int bufWidth = pitch;
@@ -939,20 +989,46 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
 	if (_dst == nullptr || _width <= 0 || _height <= 0)
 		return;
 
+	if (!_insane || !static_cast<InsaneRebel1 *>(_insane)->isInteractiveVideoActive() ||
+			_vm->_screenWidth != kRA1PresentationScreenWidth ||
+			_vm->_screenHeight != kRA1PresentationScreenHeight) {
+		SmushPlayer::handleGameUpdateScreen(src, srcPitch, width, height);
+		return;
+	}
+
 	int ra1ViewX = _ra1ViewportOffsetX;
 	int ra1ViewY = _ra1ViewportOffsetY;
 
-	const int srcX = CLIP(_scrollX + ra1ViewX, 0, _width - 1);
-	const int srcY = CLIP(_scrollY + ra1ViewY, 0, _height - 1);
+	const int srcX = CLIP(_scrollX + ra1ViewX + kRA1PresentationBorder, 0, _width - 1);
+	const int srcY = CLIP(_scrollY + ra1ViewY + kRA1PresentationBorder, 0, _height - 1);
 
-	int frameWidth = MIN(_width - srcX, _vm->_screenWidth);
-	int frameHeight = MIN(_height - srcY, _vm->_screenHeight);
+	int frameWidth = MIN<int>(_width - srcX, kRA1PresentationWidth);
+	int frameHeight = MIN<int>(_height - srcY, kRA1PresentationHeight);
 	if (frameWidth <= 0 || frameHeight <= 0)
 		return;
 
+	const int presentationSize = kRA1PresentationScreenWidth * kRA1PresentationScreenHeight;
+	if (_ra1PresentationBuffer == nullptr || _ra1PresentationBufferSize < presentationSize) {
+		byte *newPresentationBuffer = (byte *)realloc(_ra1PresentationBuffer, presentationSize);
+		if (newPresentationBuffer == nullptr)
+			return;
+		_ra1PresentationBuffer = newPresentationBuffer;
+		_ra1PresentationBufferSize = presentationSize;
+	}
+	memset(_ra1PresentationBuffer, 0, presentationSize);
+
+	// ResetPlaybackViewport() (0x20A53) initializes the interactive draw window
+	// to (4,4,312,192), leaving a black presentation frame around cockpit scenes.
 	const byte *dst = _dst + srcY * _width + srcX;
+	byte *presentationDst = _ra1PresentationBuffer +
+		kRA1PresentationBorder * kRA1PresentationScreenWidth + kRA1PresentationBorder;
+	for (int y = 0; y < frameHeight; y++) {
+		memcpy(presentationDst + y * kRA1PresentationScreenWidth,
+			dst + y * _width, frameWidth);
+	}
 
-	SmushPlayer::handleGameUpdateScreen(dst, _width, frameWidth, frameHeight);
+	SmushPlayer::handleGameUpdateScreen(_ra1PresentationBuffer,
+		kRA1PresentationScreenWidth, kRA1PresentationScreenWidth, kRA1PresentationScreenHeight);
 }
 
 SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index e53fbea39a1..3cd067fc794 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -68,6 +68,10 @@ private:
 	int32 _ra1CleanFrameSize;
 	bool _ra1HasCleanFrame;
 
+	// RA1 interactive movies present a 312x192 viewport inside a black 320x200 frame.
+	byte *_ra1PresentationBuffer;
+	int32 _ra1PresentationBufferSize;
+
 	// RA1 OBJ overlay FOBJ — cockpit drawn once frame 0, re-rendered every frame
 	byte *_ra1ObjOverlayData;
 	int32 _ra1ObjOverlayDataSize;


Commit: e11c594c4f08c09d9c2ed37d26156d0bd6c5eb1d
    https://github.com/scummvm/scummvm/commit/e11c594c4f08c09d9c2ed37d26156d0bd6c5eb1d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:00+02:00

Commit Message:
SCUMM: RA1: Fix invisible cockpit in level 8 sections

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/smush/smush_player.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 6d8233ab8ea..3a8f20ab72a 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -500,6 +500,8 @@ void InsaneRebel1::checkDynamicLevelBranch() {
 		if (!_vm->_smushVideoShouldFinish &&
 			_pendingRouteCutoverFrame >= 0 &&
 			routeFrame >= (uint32)_pendingRouteCutoverFrame) {
+			if (_player)
+				_player->setPreserveGameVideoStateOnRelease(true);
 			_vm->_smushVideoShouldFinish = true;
 			debug(1, "RA1 L%d cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
 				_currentLevel + 1, _levelRouteIndex, _pendingRouteIndex,
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 183ec966e64..f2597c684de 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1519,20 +1519,22 @@ void InsaneRebel1::renderLevel8Overlay(byte *dst, int pitch, int width, int heig
 	}
 
 	if (inDirectionalPhase) {
-		// Directional arrows — projected from (0,0) with parallax compensation
+		// Directional arrows — DrawStringEx(..., flags=0x81) at:
+		// left:  (0xA6 - projX / 4, 0x92 - projY / 4)
+		// right: (0xA8 - projX / 4, 0x93 - projY / 4)
 		int16 projX = 0, projY = 0;
 		projectGameplayPoint(projX, projY);
-		int16 px = (int16)(-(projX >> 2));
-		int16 py = (int16)(-(projY >> 2));
+		const int16 parallaxX = (int16)(projX >> 2);
+		const int16 parallaxY = (int16)(projY >> 2);
 
 		if (_shipPosX < 0xA0) {
 			// Left arrow "<<v"
 			drawFontBankString(dst, pitch, width, height,
-				viewportX + 0xA6 + px, viewportY + 0x92 + py, "<<v");
+				viewportX + 0xA6 - parallaxX, viewportY + 0x92 - parallaxY, "<<v");
 		} else {
 			// Right arrow "<<u"
 			drawFontBankString(dst, pitch, width, height,
-				viewportX + 0xA8 - px, viewportY + 0x93 - py, "<<u");
+				viewportX + 0xA8 - parallaxX, viewportY + 0x93 - parallaxY, "<<u");
 		}
 	} else {
 		// Target reticle — "<<w" at projected (0xA9, 0x9A), blinks on (frame & 4)
diff --git a/engines/scumm/smush/smush_player.cpp b/engines/scumm/smush/smush_player.cpp
index 0da86094c01..795a90be08f 100644
--- a/engines/scumm/smush/smush_player.cpp
+++ b/engines/scumm/smush/smush_player.cpp
@@ -270,6 +270,7 @@ SmushPlayer::SmushPlayer(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insa
 	_fastForwardFromFrame = 0;
 	_fastForwardToFrame = 0;
 	_preserveVideoStateOnNextPlay = false;
+	_preserveGameVideoStateOnRelease = false;
 
 	_IACTpos = 0;
 	_speed = -1;
@@ -391,7 +392,10 @@ void SmushPlayer::release() {
 	free(_specialBuffer);
 	_specialBuffer = nullptr;
 
-	releaseGameVideoState();
+	const bool preserveGameVideoState = _preserveGameVideoStateOnRelease;
+	_preserveGameVideoStateOnRelease = false;
+	if (!preserveGameVideoState)
+		releaseGameVideoState();
 	if (!shouldPreserveFrameBuffer()) {
 		free(_frameBuffer);
 		_frameBuffer = nullptr;
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index 0ec4bd9311e..d0bdd39ff8b 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -202,6 +202,7 @@ private:
 	uint32 _fastForwardFromFrame;
 	uint32 _fastForwardToFrame;
 	bool _preserveVideoStateOnNextPlay;
+	bool _preserveGameVideoStateOnRelease;
 
 	Audio::SoundHandle *_IACTchannel;
 	Audio::QueuingAudioStream *_IACTstream;
@@ -265,6 +266,7 @@ public:
 	void ensureMultiFont();
 	bool isFastForwardingCurrentFrame() const;
 	void setPreserveVideoStateOnNextPlay(bool preserve) { _preserveVideoStateOnNextPlay = preserve; }
+	void setPreserveGameVideoStateOnRelease(bool preserve) { _preserveGameVideoStateOnRelease = preserve; }
 	void setFastForwardFromFrame(uint32 frame) { _fastForwardFromFrame = frame; }
 	void setFastForwardToFrame(uint32 frame) { _fastForwardToFrame = frame; }
 


Commit: 6e7020314e6f1c1caba25813d9c16d3776116d67
    https://github.com/scummvm/scummvm/commit/6e7020314e6f1c1caba25813d9c16d3776116d67
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:00+02:00

Commit Message:
SCUMM: RA1: Trigger damage in level 8

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 3a8f20ab72a..8147bed73d6 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -315,52 +315,67 @@ inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 p
 	}
 }
 
-inline bool hasLevel8PerspectiveHazardRoute0(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+// Named translations of the original Level 8 route collision helpers
+// FUN_12FE1/FUN_130C9/FUN_13195. The names are new to the port.
+inline bool hasLevel8WalkerHazardRoute0(uint16 frame, int16 viewX, int16 viewY) {
 	switch (frame) {
 	case 0x00CD:
-		return perspectiveX < 0x29;
 	case 0x00EF:
-		return perspectiveY < 0x0F;
+		return viewX <= 0x28;
 	case 0x0294:
+		return viewY >= 0x0F;
+	case 0x03A2:
+		return viewX >= 0x18;
 	case 0x04BE:
 	case 0x076C:
-		return perspectiveX < 0x29 && perspectiveY < 0x20;
-	case 0x03A2:
-		return perspectiveX < 0x18;
+		return viewX <= 0x28 && viewY <= 0x1F;
 	case 0x05C9:
 	case 0x085A:
 	case 0x096F:
-		return perspectiveY < 0x20;
+		return viewY <= 0x1F;
 	default:
 		return false;
 	}
 }
 
-inline bool hasLevel8PerspectiveHazardRoute1(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8WalkerHazardRoute1(uint16 frame, int16 viewX, int16 viewY) {
 	switch (frame) {
 	case 0x0189:
-		return perspectiveY < 0x0F;
+		return viewY >= 0x0F;
 	case 0x0297:
-		return perspectiveX < 0x18;
+		return viewX >= 0x18;
 	case 0x03B3:
 	case 0x0661:
-		return perspectiveX < 0x29 && perspectiveY < 0x20;
+		return viewX <= 0x28 && viewY <= 0x1F;
 	case 0x04BE:
 	case 0x074F:
 	case 0x0864:
-		return perspectiveY < 0x20;
+		return viewY <= 0x1F;
 	default:
 		return false;
 	}
 }
 
-inline bool hasLevel8PerspectiveHazardRoute2(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
+inline bool hasLevel8WalkerHazardRoute2(uint16 frame, int16 viewX, int16 viewY) {
 	switch (frame) {
 	case 0x00BB:
-		return perspectiveX < 0x29 && perspectiveY < 0x20;
+		return viewX <= 0x28 && viewY <= 0x1F;
 	case 0x01A9:
 	case 0x02BE:
-		return perspectiveY < 0x20;
+		return viewY <= 0x1F;
+	default:
+		return false;
+	}
+}
+
+inline bool hasLevel8WalkerPlayerHit(int route, uint16 frame, int16 viewX, int16 viewY) {
+	switch (CLIP<int>(route, 0, 2)) {
+	case 0:
+		return hasLevel8WalkerHazardRoute0(frame, viewX, viewY);
+	case 1:
+		return hasLevel8WalkerHazardRoute1(frame, viewX, viewY);
+	case 2:
+		return hasLevel8WalkerHazardRoute2(frame, viewX, viewY);
 	default:
 		return false;
 	}
@@ -1142,14 +1157,47 @@ void InsaneRebel1::updateTurretPhysics() {
 	_damageFlags = 0;
 }
 
-// updateAsteroidPhysics — FUN_1CDA7 (0x1CDA7). Opcode 0x0B handler.
+// New port helpers for the palette-flash block that is inline in FUN_1CDA7.
+void InsaneRebel1::restoreScreenFlashPalette() {
+	if (!_screenFlashBasePaletteValid)
+		return;
+
+	if (_player)
+		_player->setPalette(_screenFlashBasePalette);
+	_screenFlashBasePaletteValid = false;
+}
+
+void InsaneRebel1::updateScreenFlashPalette() {
+	if (!_player) {
+		_screenFlashBasePaletteValid = false;
+		return;
+	}
+
+	if (_screenFlash <= 0) {
+		restoreScreenFlashPalette();
+		return;
+	}
+
+	if (!_screenFlashBasePaletteValid) {
+		memcpy(_screenFlashBasePalette, _player->getVideoPalette(), sizeof(_screenFlashBasePalette));
+		_screenFlashBasePaletteValid = true;
+	}
+
+	byte flashPalette[0x300];
+	const int blend = CLIP<int>(8 - _screenFlash, 0, 8);
+	for (int i = 0; i < 0x300; i++)
+		flashPalette[i] = (byte)(0xFF - (((0xFF - _screenFlashBasePalette[i]) * blend) >> 3));
+	_player->setPalette(flashPalette);
+}
+
+// updateGameOp0BPhysics — FUN_1CDA7 (0x1CDA7). GAME opcode 0x0B handler.
 // Uses 10-frame input history averaging instead of accumulators.
 // Ship position = averaged input + center offset.
 // Viewport = second history buffer for smooth camera scrolling.
-void InsaneRebel1::updateAsteroidPhysics() {
+void InsaneRebel1::updateGameOp0BPhysics() {
 	// Control feel tweak: original uses full 10-sample average in FUN_1CDA7.
 	// We keep the same pipeline but average over fewer samples for responsiveness.
-	const int kAsteroidSmoothWindow = 2;
+	const int kGameOp0BSmoothWindow = 2;
 
 	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
@@ -1168,22 +1216,12 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
 		_damageFlags |= 0x20;
 
+	bool level8WalkerPlayerHit = false;
 	if (_currentLevel == 7) {
 		const uint16 walkerFrame = (uint16)_gameCounter;
-		bool walkerHazard = false;
-		switch (CLIP<int>(_levelRouteIndex, 0, 2)) {
-		case 0:
-			walkerHazard = hasLevel8PerspectiveHazardRoute0(walkerFrame, _perspectiveX, _perspectiveY);
-			break;
-		case 1:
-			walkerHazard = hasLevel8PerspectiveHazardRoute1(walkerFrame, _perspectiveX, _perspectiveY);
-			break;
-		case 2:
-			walkerHazard = hasLevel8PerspectiveHazardRoute2(walkerFrame, _perspectiveX, _perspectiveY);
-			break;
-		}
-
-		if (walkerHazard)
+		level8WalkerPlayerHit = hasLevel8WalkerPlayerHit(_levelRouteIndex, walkerFrame,
+			_perspectiveX, _perspectiveY);
+		if (level8WalkerPlayerHit)
 			_damageFlags |= 0x20;
 	}
 
@@ -1193,15 +1231,18 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	}
 
 	// Damage application (FUN_1CDA7 lines 20-41)
+	// Original 0x0B mapping: 0x80 -> +0x0F, 0x40 -> +0x11, 0x20 -> +0x13.
 	// No cooldown — all three damage types can stack each frame
 	if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
+		const int16 oldHealth = _health;
+		const byte appliedDamageFlags = _damageFlags;
 		_screenFlash = 5;
 		if (_damageFlags & 0x80)
-			_health -= _tuning.shot;
-		if (_damageFlags & 0x40)
 			_health -= _tuning.miss;
-		if (_damageFlags & 0x20)
+		if (_damageFlags & 0x40)
 			_health -= _tuning.wham;
+		if (_damageFlags & 0x20)
+			_health -= _tuning.shot;
 		if (_health < 0) {
 			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
 			if (_damageFlags & 0x80)
@@ -1209,7 +1250,14 @@ void InsaneRebel1::updateAsteroidPhysics() {
 			else
 				_deathCauseIndicator = 1;
 		}
-		_prevDamageFlags = _damageFlags;
+		// FUN_1CDA7 dispatches g_sfxDamageHit, initialized from SYS/BOOM.SAD.
+		playSfx(kSfxBoom, 127, 0);
+		if (level8WalkerPlayerHit) {
+			debug(1, "RA1 L8 player hit by walker: route=%d frame=%u view=(%d,%d) flags=0x%02x health=%d->%d",
+				CLIP<int>(_levelRouteIndex, 0, 2), (unsigned)(uint16)_gameCounter,
+				_perspectiveX, _perspectiveY, appliedDamageFlags, oldHealth, _health);
+		}
+		_prevDamageFlags = appliedDamageFlags;
 		_damageFlags = 0;
 	}
 
@@ -1227,6 +1275,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
 		_screenFlash--;
 		_screenShakeEnabled = (_screenFlash > 0);
 	}
+	updateScreenFlashPalette();
 
 	// --- Cursor and perspective smoothing (FUN_1CDA7) ---
 	// _inputHistory* maps to 0x7580/0x7594, _viewHistory* to 0x75A8/0x75BC.
@@ -1265,8 +1314,11 @@ void InsaneRebel1::updateAsteroidPhysics() {
 	}
 	_inputAxisDeltaX = inputX;
 
-	debug("RA1 asteroid input: source=%s axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+	debug("RA1 GAME 0x0B input: frame=%d source=%s view=(%d,%d) health=%d prevFlags=0x%02x axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+		_gameCounter,
 		inputSourceName,
+		_perspectiveX, _perspectiveY,
+		_health, _prevDamageFlags,
 		_joystickAxisX, _joystickAxisY,
 		_vm->_mouse.x, _vm->_mouse.y,
 		_vm->getActionState(kScummActionInsaneLeft),
@@ -1285,13 +1337,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	int sumInputX = 0;
 	int sumInputY = 0;
-	for (int i = 0; i < kAsteroidSmoothWindow; i++) {
+	for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
 		sumInputX += _inputHistoryX[i];
 		sumInputY += _inputHistoryY[i];
 	}
 
-	_avgInputX = (int16)(sumInputX / kAsteroidSmoothWindow);
-	_avgInputY = (int16)(-sumInputY / kAsteroidSmoothWindow);
+	_avgInputX = (int16)(sumInputX / kGameOp0BSmoothWindow);
+	_avgInputY = (int16)(-sumInputY / kGameOp0BSmoothWindow);
 	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
 	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
 
@@ -1307,13 +1359,13 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	int sumViewX = 0;
 	int sumViewY = 0;
-	for (int i = 0; i < kAsteroidSmoothWindow; i++) {
+	for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
 		sumViewX += _viewHistoryX[i];
 		sumViewY += _viewHistoryY[i];
 	}
 
-	int16 avgViewX = (int16)(sumViewX / kAsteroidSmoothWindow);
-	int16 avgViewY = (int16)(sumViewY / kAsteroidSmoothWindow);
+	int16 avgViewX = (int16)(sumViewX / kGameOp0BSmoothWindow);
+	int16 avgViewY = (int16)(sumViewY / kGameOp0BSmoothWindow);
 	_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
 	resetProjectionTable();
@@ -1380,7 +1432,7 @@ void InsaneRebel1::updateAsteroidPhysics() {
 
 	checkDynamicLevelBranch();
 
-	debug(7, "RA1 asteroid: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
+	debug(7, "RA1 GAME 0x0B: pos=(%d,%d) avg=(%d,%d) view=(%d,%d) health=%d flash=%d",
 		_shipPosX, _shipPosY, _avgInputX, _avgInputY,
 		_perspectiveX, _perspectiveY, _health, _screenFlash);
 }
@@ -1744,7 +1796,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	case 0x0B:
 		_activeGameOpcode = 0x0B;
 		_frameGameOpcodeMask |= (1u << 0x0B);
-		// Asteroid/surface per-frame handler (FUN_1CDA7).
+		// GAME 0x0B per-frame handler (FUN_1CDA7).
 		// field1 = frame counter, field2 = max frames
 		_gameCounter = param1;
 		if (subSize >= 20) {
@@ -1761,9 +1813,9 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			}
 		}
 		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
-		if (!_asteroidPhysicsUpdatedThisFrame) {
-			updateAsteroidPhysics();
-			_asteroidPhysicsUpdatedThisFrame = true;
+		if (!_gameOp0BPhysicsUpdatedThisFrame) {
+			updateGameOp0BPhysics();
+			_gameOp0BPhysicsUpdatedThisFrame = true;
 			syncViewportOffset(true);
 		}
 		break;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 9dc75ef8307..9d7dd390fb6 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -206,6 +206,8 @@ void InsaneRebel1::resetGameplayFlagsFromTuning() {
 }
 
 InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
+	Insane::_vm = scumm;
+
 	_screenWidth = 384;
 	_screenHeight = 242;
 
@@ -226,7 +228,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_posAccumY = 0;
 	_driftParam = 0;
 
-	_difficulty = 0;  // Easy by default
+	// Original startup initializes the options difficulty/tuning index to 1.
+	_difficulty = 1;
 	loadTuningForLevel(0);
 
 	_perspectiveX = 0;
@@ -268,7 +271,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_activeGameOpcode = 0;
 	_frameGameOpcodeMask = 0;
 	_frameDispatchFlags = 0;
-	_asteroidPhysicsUpdatedThisFrame = false;
+	_gameOp0BPhysicsUpdatedThisFrame = false;
 
 	_health = kMaxHealth;
 	_lives = 3;
@@ -283,6 +286,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_screenFlash = 0;
 	_frameCounter = 0;
 	_screenShakeEnabled = false;
+	memset(_screenFlashBasePalette, 0, sizeof(_screenFlashBasePalette));
+	_screenFlashBasePaletteValid = false;
 	_deathCauseIndicator = 0;
 	_hudRenderFlag = 0;
 	_hudDirtyFlag = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 53e7bb55573..6c8117ee2e3 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -270,8 +270,10 @@ private:
 	};
 	InputSource _activeInputSource;
 
-	// 0x0B handler physics update (asteroid/surface levels)
-	void updateAsteroidPhysics();
+	// GAME opcode 0x0B physics update (scrolling cockpit/surface levels)
+	void updateGameOp0BPhysics();
+	void updateScreenFlashPalette();
+	void restoreScreenFlashPalette();
 
 	// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
 	void updateOnFootPhysics();
@@ -303,7 +305,7 @@ private:
 	uint16 _activeGameOpcode;
 	uint32 _frameGameOpcodeMask;
 	uint16 _frameDispatchFlags;
-	bool _asteroidPhysicsUpdatedThisFrame;
+	bool _gameOp0BPhysicsUpdatedThisFrame;
 
 	// Difficulty (0=easy, 1=normal, 2=hard) — matches original DAT_22BC
 	int _difficulty;
@@ -317,9 +319,9 @@ private:
 		int16 slide;     // +0x09: cross-axis coupling
 		int16 drift;     // +0x0B: drift/turbulence multiplier
 		int16 snap;      // +0x0D: hit radius for shooting targets
-		int16 miss;      // +0x0F: obstacle collision damage (0x0B bit 0x40)
-		int16 wham;      // +0x11: light/wall damage
-		int16 shot;      // +0x13: heavy/projectile damage
+		int16 miss;      // +0x0F: first damage value; FUN_1CDA7 uses it for bit 0x80
+		int16 wham;      // +0x11: second damage value; FUN_1CDA7 uses it for bit 0x40
+		int16 shot;      // +0x13: third damage value; FUN_1CDA7 uses it for bit 0x20
 		int16 kill;      // +0x15: score per target kill
 		int16 time;      // +0x17: survival bonus (added every 32 frames)
 		int16 levelPts;  // +0x19: chapter completion bonus (RunChapterCompleteSummaryScreen)
@@ -345,6 +347,8 @@ private:
 	int16 _screenFlash;          // 0x7736: screen flash timer on hit
 	uint32 _frameCounter;        // 0x7740: global frame counter
 	bool _screenShakeEnabled;    // 0x41AC: when true, SetCameraOffset adds ±2 random jitter
+	byte _screenFlashBasePalette[0x300];
+	bool _screenFlashBasePaletteValid;
 	byte _deathCauseIndicator;   // 0x772E: non-zero = player died; selects death animation variant
 	byte _hudRenderFlag;         // 0x7600: 0xFF when HUD should render (set by combat mode handlers)
 	byte _hudDirtyFlag;          // 0x7601: 0xFF after HUD redraw (set by renderHUD)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index f2597c684de..9433b49b761 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -365,7 +365,7 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	_frameGameOpcodeMask = 0;
 	_frameDispatchFlags = 0;
-	_asteroidPhysicsUpdatedThisFrame = false;
+	_gameOp0BPhysicsUpdatedThisFrame = false;
 
 	if (_interactiveVideoActive && _player) {
 		const bool usePerspectiveViewport =
@@ -443,7 +443,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	_hudRenderFlag = 0xFF;
 
 	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
-	const bool asteroidMode = hasFrameGameOpcode(0x0B) ||
+	const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
 		(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
 	const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
 		(!haveFrameGameOpcodes &&
@@ -453,17 +453,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
 		(!haveFrameGameOpcodes &&
 			(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
-	if (asteroidMode) {
-		// First-person asteroid/surface handler — opcode 0x0B (FUN_1CDA7).
-		if (!_asteroidPhysicsUpdatedThisFrame) {
-			updateAsteroidPhysics();
-			_asteroidPhysicsUpdatedThisFrame = true;
+	if (gameOp0BMode) {
+		// GAME 0x0B scrolling cockpit/surface handler — FUN_1CDA7.
+		if (!_gameOp0BPhysicsUpdatedThisFrame) {
+			updateGameOp0BPhysics();
+			_gameOp0BPhysicsUpdatedThisFrame = true;
 			syncViewportOffset(true);
 		}
 
 		// DOS 0x0B loops test health after each frontend frame and leave the
 		// interactive movie as soon as it drops below zero. Mirror that here so
-		// asteroid/surface chapters transition to their retry/death clips like
+		// GAME 0x0B chapters transition to their retry/death clips like
 		// the other gameplay families do.
 		if (_health < 0) {
 			_fireCooldown = _playerFired ? 1 : 0;
@@ -517,9 +517,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// before HUD/screen copy so overlays and final crop observe the same camera.
 	// On-foot mode uses SetCameraOffset(0,0) — no viewport crop.
 	if (_player) {
-		if (onFootMode || (!asteroidMode && !turretMode && !flightMode)) {
+		if (onFootMode || (!gameOp0BMode && !turretMode && !flightMode)) {
 			syncViewportOffset(false);
-		} else if (!asteroidMode) {
+		} else if (!gameOp0BMode) {
 			syncViewportOffset(true);
 		}
 	}
@@ -1367,8 +1367,10 @@ void InsaneRebel1::updateLevel8WalkerState() {
 		return;
 	}
 
-	// FUN_12fe1/FUN_130c9/FUN_13195 walker collision damage is applied from
-	// updateAsteroidPhysics(), where the 0x0B damage flags are consumed.
+	// FUN_12FE1/FUN_130C9/FUN_13195 test whether the walker shot hits the
+	// player. The port synthesizes that player-damage flag in updateGameOp0BPhysics(),
+	// where the 0x0B damage flags are consumed. This is unrelated to _walkerHealth,
+	// which is the boss health displayed by the Level 8 overlay.
 	int route = CLIP(_levelRouteIndex, 0, 2);
 	uint16 fc = (uint16)_gameCounter;
 


Commit: 47e86512bc8dcba7157bd4749c591fcbec00d686
    https://github.com/scummvm/scummvm/commit/47e86512bc8dcba7157bd4749c591fcbec00d686
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:01+02:00

Commit Message:
SCUMM: RA1: Improve level 8 comments

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 8147bed73d6..88d30ac5298 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1221,6 +1221,8 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 		const uint16 walkerFrame = (uint16)_gameCounter;
 		level8WalkerPlayerHit = hasLevel8WalkerPlayerHit(_levelRouteIndex, walkerFrame,
 			_perspectiveX, _perspectiveY);
+		// RunLevel8Flow sets damage flag 0x20 when the AT-AT route contact
+		// helper hits the player; boss damage uses _walkerHealth separately.
 		if (level8WalkerPlayerHit)
 			_damageFlags |= 0x20;
 	}
@@ -1231,18 +1233,18 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	}
 
 	// Damage application (FUN_1CDA7 lines 20-41)
-	// Original 0x0B mapping: 0x80 -> +0x0F, 0x40 -> +0x11, 0x20 -> +0x13.
+	// Original 0x0B mapping: 0x80 -> +0x13, 0x40 -> +0x0F, 0x20 -> +0x11.
 	// No cooldown — all three damage types can stack each frame
 	if (_damageFlags != 0 && _health >= 0 && _deathTimer < 1) {
 		const int16 oldHealth = _health;
 		const byte appliedDamageFlags = _damageFlags;
 		_screenFlash = 5;
 		if (_damageFlags & 0x80)
-			_health -= _tuning.miss;
+			_health -= _tuning.shot;
 		if (_damageFlags & 0x40)
-			_health -= _tuning.wham;
+			_health -= _tuning.miss;
 		if (_damageFlags & 0x20)
-			_health -= _tuning.shot;
+			_health -= _tuning.wham;
 		if (_health < 0) {
 			_deathTimer = 15;  // 0x0F — shorter than Level 1's 30
 			if (_damageFlags & 0x80)
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 6c8117ee2e3..74e99fbee89 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -319,9 +319,9 @@ private:
 		int16 slide;     // +0x09: cross-axis coupling
 		int16 drift;     // +0x0B: drift/turbulence multiplier
 		int16 snap;      // +0x0D: hit radius for shooting targets
-		int16 miss;      // +0x0F: first damage value; FUN_1CDA7 uses it for bit 0x80
-		int16 wham;      // +0x11: second damage value; FUN_1CDA7 uses it for bit 0x40
-		int16 shot;      // +0x13: third damage value; FUN_1CDA7 uses it for bit 0x20
+		int16 miss;      // +0x0F: light/scripted damage; FUN_1CDA7 uses it for bit 0x40
+		int16 wham;      // +0x11: medium/contact damage; FUN_1CDA7 uses it for bit 0x20
+		int16 shot;      // +0x13: heavy/projectile damage; FUN_1CDA7 uses it for bit 0x80
 		int16 kill;      // +0x15: score per target kill
 		int16 time;      // +0x17: survival bonus (added every 32 frames)
 		int16 levelPts;  // +0x19: chapter completion bonus (RunChapterCompleteSummaryScreen)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 9433b49b761..57e3ee12c63 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1367,8 +1367,8 @@ void InsaneRebel1::updateLevel8WalkerState() {
 		return;
 	}
 
-	// FUN_12FE1/FUN_130C9/FUN_13195 test whether the walker shot hits the
-	// player. The port synthesizes that player-damage flag in updateGameOp0BPhysics(),
+	// FUN_12FE1/FUN_130C9/FUN_13195 test whether the route-specific walker
+	// contact hazards hit the player. The port synthesizes that damage flag in updateGameOp0BPhysics(),
 	// where the 0x0B damage flags are consumed. This is unrelated to _walkerHealth,
 	// which is the boss health displayed by the Level 8 overlay.
 	int route = CLIP(_levelRouteIndex, 0, 2);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 7486d22755d..240497668d1 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -915,7 +915,10 @@ bool InsaneRebel1::runLevel8() {
 
 	_currentLevel = 7;
 	loadLevelSprites(8);
-	loadTuningForLevel(7);
+	// RunLevel8Flow starts L8PLAY.ANM with initLevelFlag=10, so the walker
+	// chapter uses the original "8" tuning row while still keeping chapter
+	// index 7 for assets and Level 8-specific runtime logic.
+	loadTuningForLevel(10);
 
 	beginLevelTitleOverlay(7);
 	playCinematic("LVL8/L8INTRO.ANM");


Commit: 357f9ca51e0e7d4b54f890afd27a5afd36d22413
    https://github.com/scummvm/scummvm/commit/357f9ca51e0e7d4b54f890afd27a5afd36d22413
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:01+02:00

Commit Message:
SCUMM: RA1: Render SMUSH_CODEC_SKIP_RLE correctly

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 451b3a0506b..7754e319101 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -596,8 +596,12 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 	case SMUSH_CODEC_SKIP_RLE: {
 		const int bufWidth = pitch;
 		const int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+		// Codec 23 uses the high byte of the FOBJ codec word as the palette
+		// band for its additive delta. The event-mask path may subtract 0x10
+		// from this value before decoding, which Level 8 uses for the walker
+		// armor layers.
 		smushDecodeRA1AdditiveLineUpdate(_dst, src, left, top, width, height,
-			pitch, bufWidth, bufHeight, 0);
+			pitch, bufWidth, bufHeight, param);
 		return true;
 	}
 	default:


Commit: 876fcfa145943c996cb6f6edcfed51017b2ebf26
    https://github.com/scummvm/scummvm/commit/876fcfa145943c996cb6f6edcfed51017b2ebf26
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:01+02:00

Commit Message:
SCUMM: RA1: Avoid trapping ScummVM menu during gameplay

Changed paths:
    engines/scumm/insane/rebel1/menu.cpp


diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 4f7ef736b5d..eb9e7f17472 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -37,6 +37,12 @@ const int kRA1LevelSelectRowsPerCol = 8;
 const int kRA1NumLevels = 15;
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
+	// Global ScummVM dialogs pause the engine while their modal event loop runs.
+	// Do not consume those mouse/key events as RA1 gameplay/menu input, or the
+	// dialog buttons cannot receive clicks while an interactive video is active.
+	if (_vm->isPaused())
+		return false;
+
 	if (event.type == Common::EVENT_MOUSEMOVE && !_mouseRecentering) {
 		_activeInputSource = kInputSourceMouse;
 	}


Commit: a264d2921ca2c8aa1eeea783d32b3e7556dac6ff
    https://github.com/scummvm/scummvm/commit/a264d2921ca2c8aa1eeea783d32b3e7556dac6ff
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:02+02:00

Commit Message:
SCUMM: RA1: Implement movement and damage for level 1 part 2

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 88d30ac5298..d64f8e02d4a 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -47,24 +47,11 @@ inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue,
 const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
 const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
 
-// LVL1 stage-2 0x5D damage/event codes. The gameplay stream exposes low record ids
-// (6..18), while the recovered outer loop compares the post-latch state against the
-// later translated values seen in the executable. Accept both representations.
-inline bool isL1Stage2DamageLatch(uint16 code) {
+// Level 15 final approach 0x5D damage/event codes consumed by
+// RunLevel1GameLoop. The latch stores the raw GAME parameter; no translation is
+// performed by HandleGameOp5D_SegmentLinkLatch.
+inline bool isLevel15FinalDamageLatch(uint16 code) {
 	switch (code) {
-	case 0x0006:
-	case 0x0007:
-	case 0x0008:
-	case 0x0009:
-	case 0x000A:
-	case 0x000B:
-	case 0x000C:
-	case 0x000D:
-	case 0x000E:
-	case 0x000F:
-	case 0x0010:
-	case 0x0011:
-	case 0x0012:
 	case 0x0049:
 	case 0x004B:
 	case 0x004E:
@@ -81,7 +68,7 @@ inline bool isL1Stage2DamageLatch(uint16 code) {
 	}
 }
 
-inline bool isL1Stage2SweepDamage(uint16 frameCounter, int16 perspectiveX) {
+inline bool hasLevel15FinalSweepDamage(uint16 frameCounter, int16 perspectiveX) {
 	switch (frameCounter) {
 	case 0x0034:
 	case 0x00ED:
@@ -910,8 +897,7 @@ void InsaneRebel1::updateShipPhysics() {
 	// RA1 FUN_1B297-style latches from GAME opcodes:
 	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (projectile-like hit)
-	if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
-		isL1Stage2DamageLatch(_gameLatch5D)))
+	if (_gameLatch5D == 0xFFFF)
 		_damageFlags |= 0x40;
 	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
 		_damageFlags |= 0x80;
@@ -990,6 +976,67 @@ void InsaneRebel1::updateShipPhysics() {
 		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
 }
 
+// Port helper: FUN_1E6A7 computes this direction bucket inline before applying
+// the current frame's movement update.
+void InsaneRebel1::updateTurretShipDirection(int16 offsetY) {
+	int dir = 0;
+	if (_flyControlMode == 2) {
+		if (_rollAccum > 0x380) dir = 4;
+		else if (_rollAccum > 0x280) dir = 3;
+		else if (_rollAccum > 0x180) dir = 2;
+		else if (_rollAccum > 0x80) dir = 1;
+		else if (_rollAccum > -0x80) dir = 0;
+		else if (_rollAccum > -0x180) dir = 5;
+		else if (_rollAccum > -0x280) dir = 6;
+		else if (_rollAccum > -0x380) dir = 7;
+		else dir = 8;
+	} else {
+		if (_rollAccum > 0x380) dir = 8;
+		else if (_rollAccum > 0x280) dir = 7;
+		else if (_rollAccum > 0x180) dir = 6;
+		else if (_rollAccum > 0x80) dir = 5;
+		else if (_rollAccum > -0x80) dir = 4;
+		else if (_rollAccum > -0x180) dir = 3;
+		else if (_rollAccum > -0x280) dir = 2;
+		else if (_rollAccum > -0x380) dir = 1;
+		else dir = 0;
+
+		if (offsetY < -0x1E)
+			dir += 0x12;
+		else if (offsetY < 0x1E)
+			dir += 9;
+	}
+
+	const RA1SpriteBank *shipBank = &_shipBank;
+	if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
+		shipBank = &_shipBankAlt;
+	if (shipBank->numSprites > 0)
+		_shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
+}
+
+void InsaneRebel1::getCollisionShipCenter(int16 &x, int16 &y) const {
+	// Original 0x0D/0x0E collision compares projected script zones against the
+	// drawn ship center (base center + g_shipOffset). This port draws into a
+	// 384x242 source buffer and later crops by the camera offset, so convert that
+	// source-buffer anchor to the visible screen-space point used by the projected
+	// zones.
+	//
+	// In Level 1 part 2, HandleGameOp0A_TurretVariant reuses _shipPos for the
+	// targeting cursor, so collision must read the movement accumulator instead.
+	if (_currentLevel == 0 && _flyControlMode == 2) {
+		x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
+		y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
+	} else {
+		x = _shipPosX;
+		y = _shipPosY;
+	}
+
+	if (_interactiveVideoActive) {
+		x = (int16)(x - _perspectiveX);
+		y = (int16)(y - _perspectiveY);
+	}
+}
+
 // updateTurretPhysics — FUN_1E6A7 (0x1E6A7), opcode 0x08 path.
 // Stage-2 cockpit mode uses different smoothing/clamps than FUN_1DEB5.
 void InsaneRebel1::updateTurretPhysics() {
@@ -1001,16 +1048,9 @@ void InsaneRebel1::updateTurretPhysics() {
 	const int32 counter = _gameCounter;
 	const uint16 modeFlags = _frameDispatchFlags;
 
-	// RA1 latches consumed by handler family in FUN_1B297.
-	if (_currentLevel == 0 && _flyControlMode == 2 && isL1Stage2SweepDamage((uint16)counter, _perspectiveX))
-		_damageFlags |= 0x20;
-	if (_gameLatch5D == 0xFFFF || (_currentLevel == 0 && _flyControlMode == 2 &&
-		isL1Stage2DamageLatch(_gameLatch5D)))
+	if (_gameLatch5D == 0xFFFF)
 		_damageFlags |= 0x40;
-	if (_gameLatch5F != 0 &&
-		((_currentLevel == 0 && _flyControlMode == 2)
-			? (_vm->_rnd.getRandomNumber(2) == 0)
-			: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
+	if (_gameLatch5F != 0 && _vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)
 		_damageFlags |= 0x80;
 
 	if (counter == 0) {
@@ -1023,6 +1063,19 @@ void InsaneRebel1::updateTurretPhysics() {
 		_damageCooldown = 0;
 	}
 
+	// GAME 0x0A appears before 0x08 in L1PLAY2.ANM. The original therefore
+	// draws shots/targeting and the ship from this pre-physics center, then
+	// updates the camera offset at the end of 0x08 for the final viewport copy.
+	const int16 preMoveOffsetX = (int16)(_posAccumX >> 8);
+	const int16 preMoveOffsetY = (int16)(_posAccumY >> 8);
+	if (_currentLevel == 0 && _flyControlMode == 2) {
+		_turretFrameShipOffsetX = preMoveOffsetX;
+		_turretFrameShipOffsetY = preMoveOffsetY;
+		_turretFrameShipCenterX = (int16)(kRA1CenterX + preMoveOffsetX);
+		_turretFrameShipCenterY = (int16)(kRA1CenterY + preMoveOffsetY);
+		_turretFrameShipCenterValid = true;
+	}
+
 	// Damage gate from FUN_1E6A7.
 	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
 		if (_damageFlags == 0x80)
@@ -1049,6 +1102,8 @@ void InsaneRebel1::updateTurretPhysics() {
 	if (_health < 0 && _deathTimer > 0)
 		_deathTimer--;
 
+	updateTurretShipDirection(preMoveOffsetY);
+
 	// FUN_1E6A7 movement gate: counter > 8 or flags bit 0x40.
 	if (counter > 8 || (modeFlags & 0x40)) {
 		// FUN_1E6A7 consumes DAT_756C/DAT_756E from the shared input bridge,
@@ -1105,40 +1160,6 @@ void InsaneRebel1::updateTurretPhysics() {
 	// main flight handler, derived directly from roll.
 	rebuildProjectionTable((int16)(-(_rollAccum >> 9)), 0x0D);
 
-	// Direction bucket synthesis from FUN_1E6A7.
-	int dir = 0;
-	if (_flyControlMode == 2) {
-		if (_rollAccum > 0x380) dir = 4;
-		else if (_rollAccum > 0x280) dir = 3;
-		else if (_rollAccum > 0x180) dir = 2;
-		else if (_rollAccum > 0x80) dir = 1;
-		else if (_rollAccum > -0x80) dir = 0;
-		else if (_rollAccum > -0x180) dir = 5;
-		else if (_rollAccum > -0x280) dir = 6;
-		else if (_rollAccum > -0x380) dir = 7;
-		else dir = 8;
-	} else {
-		if (_rollAccum > 0x380) dir = 8;
-		else if (_rollAccum > 0x280) dir = 7;
-		else if (_rollAccum > 0x180) dir = 6;
-		else if (_rollAccum > 0x80) dir = 5;
-		else if (_rollAccum > -0x80) dir = 4;
-		else if (_rollAccum > -0x180) dir = 3;
-		else if (_rollAccum > -0x280) dir = 2;
-		else if (_rollAccum > -0x380) dir = 1;
-		else dir = 0;
-
-		if (offsetY < -0x1E)
-			dir += 0x12;
-		else if (offsetY < 0x1E)
-			dir += 9;
-	}
-	const RA1SpriteBank *shipBank = &_shipBank;
-	if (_currentLevel == 0 && _flyControlMode == 2 && _shipBankAlt.numSprites > 0)
-		shipBank = &_shipBankAlt;
-	if (shipBank->numSprites > 0)
-		_shipDirIndex = CLIP<int16>((int16)dir, 0, shipBank->numSprites - 1);
-
 	// Regeneration + survival bonus via FUN_1BB0E call in this path.
 	if ((_frameCounter & 0x1F) == 0) {
 		if (_health >= 0 && _health < kMaxHealth)
@@ -1202,19 +1223,24 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
+	const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
+
 	if (_gameLatch5D == 0xFFFF ||
 		(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
-		(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)))
+		(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
+		(level15FinalPhase && isLevel15FinalDamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
 	if (_gameLatch5F != 0 &&
-		((_currentLevel == 3 || _currentLevel == 9)
+		((_currentLevel == 3 || _currentLevel == 9 || level15FinalPhase)
 			? (_vm->_rnd.getRandomNumber(2) == 0)
 			: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
 		_damageFlags |= 0x80;
 
 	if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
 		_damageFlags |= 0x20;
+	if (level15FinalPhase && hasLevel15FinalSweepDamage((uint16)_gameCounter, _perspectiveX))
+		_damageFlags |= 0x20;
 
 	bool level8WalkerPlayerHit = false;
 	if (_currentLevel == 7) {
@@ -1725,39 +1751,43 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			// Apply 0x0D immediately so it sees the same pre-physics ship state as
 			// the other per-frame GAME latches. Deferring this until after 0x07/0x09
 			// movement made right-wall hits fire too early when steering into a wall.
-			const bool suppressDirectionalDamage = (_damageFlags & 0x10) != 0;
+			int16 collisionShipX = _shipPosX;
+			int16 collisionShipY = _shipPosY;
+			getCollisionShipCenter(collisionShipX, collisionShipY);
+
+			const bool suppressDirectionalDamage = (_frameDispatchFlags & 0x10) != 0;
 			const byte oldDirectionalFlags = _damageFlags & 0x0F;
 			if (_health >= 0) {
-				if (_shipPosX < _corridorLeftX) {
-					_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+				if (collisionShipX < _corridorLeftX) {
+					_posAccumX = (int32)(_corridorLeftX + _perspectiveX - kRA1CenterX) * 0x100;
 					if (!suppressDirectionalDamage) {
 						if (_rollAccum < 0x100)
 							_rollAccum = 0x100;
 						_damageFlags |= 0x04;
 					}
 				}
-				if (_shipPosX > _corridorRightX) {
-					_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+				if (collisionShipX > _corridorRightX) {
+					_posAccumX = (int32)(_corridorRightX + _perspectiveX - kRA1CenterX) * 0x100;
 					if (!suppressDirectionalDamage) {
 						if (_rollAccum > -0x100)
 							_rollAccum = -0x100;
 						_damageFlags |= 0x02;
 					}
 				}
-				if (_shipPosY < _corridorTopY) {
-					_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+				if (collisionShipY < _corridorTopY) {
+					_posAccumY = (int32)(_corridorTopY + _perspectiveY - kRA1CenterY) * 0x100 + 0x100;
 					if (!suppressDirectionalDamage)
 						_damageFlags |= 0x01;
 				}
-				if (_shipPosY > _corridorBottomY) {
-					_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+				if (collisionShipY > _corridorBottomY) {
+					_posAccumY = (int32)(_corridorBottomY + _perspectiveY - kRA1CenterY) * 0x100 - 0x100;
 					if (!suppressDirectionalDamage)
 						_damageFlags |= 0x08;
 				}
 			}
 			if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
 				debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
-					_shipPosX, _shipPosY,
+					collisionShipX, collisionShipY,
 					_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
 					_damageFlags, suppressDirectionalDamage ? 1 : 0);
 			}
@@ -1777,6 +1807,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			int16 zoneTop = (int16)b.readUint32BE();
 			int16 zoneWidth = (int16)b.readUint32BE();
 			int16 zoneHeight = (int16)b.readUint32BE();
+			const int16 rawZoneLeft = zoneLeft;
+			const int16 rawZoneTop = zoneTop;
 
 			int16 centerX = zoneLeft + zoneWidth / 2;
 			int16 centerY = zoneTop + zoneHeight / 2;
@@ -1786,12 +1818,22 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			zoneTop = centerY - zoneHeight / 2;
 			int16 zoneRight = zoneLeft + zoneWidth;
 			int16 zoneBottom = zoneTop + zoneHeight;
-			if (_shipPosX > zoneLeft && _shipPosX < zoneRight &&
-				_shipPosY > zoneTop && _shipPosY < zoneBottom) {
+			int16 collisionShipX = _shipPosX;
+			int16 collisionShipY = _shipPosY;
+			getCollisionShipCenter(collisionShipX, collisionShipY);
+
+			if (_health >= 0 &&
+				collisionShipX > zoneLeft && collisionShipX < zoneRight &&
+				collisionShipY > zoneTop && collisionShipY < zoneBottom) {
 				_damageFlags |= 0x10;
+				debug(1, "RA1 0x0E hit: ship=(%d,%d) zone=[%d,%d]-[%d,%d] raw=[%d,%d]+(%d,%d) cam=(%d,%d) flags=0x%02x",
+					collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+					rawZoneLeft, rawZoneTop, zoneWidth, zoneHeight,
+					_perspectiveX, _perspectiveY, _damageFlags);
 			}
-			debug(7, "RA1 GAME 0x0E: zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
-				zoneLeft, zoneTop, zoneRight, zoneBottom, _perspectiveX, _perspectiveY, _damageFlags);
+			debug(7, "RA1 GAME 0x0E: ship=(%d,%d) zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
+				collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+				_perspectiveX, _perspectiveY, _damageFlags);
 		}
 		break;
 
@@ -1914,7 +1956,7 @@ void InsaneRebel1::processShot() {
 
 	// Shot origin depends on game mode:
 	// On-foot: character position (g_shipOffsetX + g_perspectiveX)
-	// Turret: perspective-adjusted center
+	// Turret: ship center
 	// Flight: cursor position
 	const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
 	int16 originX, originY;
@@ -1922,8 +1964,7 @@ void InsaneRebel1::processShot() {
 		originX = _onFootCharX + kOnFootCenterX;
 		originY = _onFootCharY + kOnFootCenterY;
 	} else if (turretMode) {
-		originX = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
-		originY = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+		getTurretShipCenter(originX, originY);
 	} else {
 		originX = _shipPosX;
 		originY = _shipPosY;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 9d7dd390fb6..b9599e1194c 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -226,6 +226,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_liftSmooth = 0;
 	_posAccumX = 0;
 	_posAccumY = 0;
+	_turretFrameShipOffsetX = 0;
+	_turretFrameShipOffsetY = 0;
+	_turretFrameShipCenterX = kRA1CenterX;
+	_turretFrameShipCenterY = kRA1CenterY;
+	_turretFrameShipCenterValid = false;
 	_driftParam = 0;
 
 	// Original startup initializes the options difficulty/tuning index to 1.
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 74e99fbee89..0e874041d17 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -147,6 +147,8 @@ private:
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
 	void updateTurretPhysics();
+	void updateTurretShipDirection(int16 offsetY);
+	void getCollisionShipCenter(int16 &x, int16 &y) const;
 	void preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick = nullptr);
 	void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
 	void resetProjectionTable();
@@ -161,6 +163,7 @@ private:
 	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
 							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
+	void getTurretShipCenter(int16 &x, int16 &y) const;
 	void renderLevel8Overlay(byte *dst, int pitch, int width, int height,
 		int viewportX, int viewportY);
 	void updateLevel8WalkerState();
@@ -231,6 +234,11 @@ private:
 	// _74C2/_74C6: position accumulators (32-bit), pixel offset = accum >> 8
 	int32 _posAccumX;
 	int32 _posAccumY;
+	int16 _turretFrameShipOffsetX;
+	int16 _turretFrameShipOffsetY;
+	int16 _turretFrameShipCenterX;
+	int16 _turretFrameShipCenterY;
+	bool _turretFrameShipCenterValid;
 
 	// Per-frame drift bias from GAME 0x07 field3
 	int16 _driftParam;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 57e3ee12c63..f56592dafa1 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -359,6 +359,27 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 	}
 }
 
+void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
+	if (_currentLevel == 0 && _flyControlMode == 2) {
+		// Port helper: original Level 1 part 2 keeps g_perspectiveX/Y at the
+		// screen center and draws the overhead ship at g_shipOffsetX/Y from
+		// FUN_1E6A7. In this port _perspectiveX/Y stores the clamped camera
+		// offset passed to SetCameraOffset(), so recover the ship center from
+		// the movement accumulators instead.
+		if (_turretFrameShipCenterValid) {
+			x = _turretFrameShipCenterX;
+			y = _turretFrameShipCenterY;
+		} else {
+			x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
+			y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
+		}
+		return;
+	}
+
+	x = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
+	y = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+}
+
 // procPreRendering — Sets viewport window offset (FUN_224FD at 0x224FD).
 // RA1 decodes FOBJs at chunk coordinates, then displays a scrolled 320x200
 // window inside the 384x242 framebuffer.
@@ -366,6 +387,7 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	_frameGameOpcodeMask = 0;
 	_frameDispatchFlags = 0;
 	_gameOp0BPhysicsUpdatedThisFrame = false;
+	_turretFrameShipCenterValid = false;
 
 	if (_interactiveVideoActive && _player) {
 		const bool usePerspectiveViewport =
@@ -555,10 +577,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		// FUN_1D79C (GAME 0x0A) owns the cursor center in stage-2 turret mode.
 		// The preceding overlay/shot pass uses the previous frame's cursor; the
 		// handler then publishes the next cursor position from the current
-		// ship-offset and camera state.
+		// pre-physics ship offset. In L1PLAY2 the following 0x08 handler updates
+		// the camera afterward, so source-space anchors and the final viewport
+		// crop intentionally observe different moments in the frame.
 		if (turretTargetingMode) {
-			const int16 shipOffsetX = (int16)(_posAccumX >> 8);
-			const int16 shipOffsetY = (int16)(_posAccumY >> 8);
+			const bool useTurretFrameCenter =
+				(_currentLevel == 0 && _flyControlMode == 2 && _turretFrameShipCenterValid);
+			const int16 shipOffsetX = useTurretFrameCenter ?
+				_turretFrameShipOffsetX : (int16)(_posAccumX >> 8);
+			const int16 shipOffsetY = useTurretFrameCenter ?
+				_turretFrameShipOffsetY : (int16)(_posAccumY >> 8);
 			_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
 			_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));
 		} else if (flightVariantTargetingMode) {
@@ -776,9 +804,14 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	const bool onFootMode = (effectiveOpcode == 0x19 || effectiveOpcode == 0x1A);
 	const bool turretMode = (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A);
 	const bool flightVariantMode = (effectiveOpcode == 0x09);
-	const int shipBaseX = turretMode ? (kRA1CenterX + (_perspectiveX - 0x20)) : _shipPosX;
-	const int shipBaseY = turretMode ? (kRA1CenterY + (_perspectiveY - 0x17))
-		: (flightVariantMode ? _shipPosY : (overlayY + _shipPosY));
+	int shipBaseX = _shipPosX;
+	int shipBaseY = flightVariantMode ? _shipPosY : (overlayY + _shipPosY);
+	if (turretMode) {
+		int16 centerX, centerY;
+		getTurretShipCenter(centerX, centerY);
+		shipBaseX = centerX;
+		shipBaseY = centerY;
+	}
 
 	for (int i = 0; i < kMaxShotSlots; i++) {
 		if (_shotSlots[i].timer > 0 && _shotSlots[i].timer <= spritesPerSet) {
@@ -1142,15 +1175,16 @@ void InsaneRebel1::renderShip(byte *dst, int pitch, int width, int height) {
 
 	const RA1Sprite &spr = shipBank->sprites[_shipDirIndex];
 
-	// In 0x08/0x0A turret handlers, _shipPos holds targeting/cursor state, while
-	// the ship sprite is still anchored from camera perspective + ship drift
-	// offsets. Flight handlers already store the ship sprite center in _shipPos.
+	// In 0x08/0x0A turret handlers, _shipPos holds targeting/cursor state.
+	// Flight handlers already store the ship sprite center in _shipPos.
 	int shipScreenX = _shipPosX;
 	int shipScreenY = _shipPosY;
 	const uint16 effectiveOpcode = getEffectiveGameOpcode();
 	if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
-		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
-		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+		int16 centerX, centerY;
+		getTurretShipCenter(centerX, centerY);
+		shipScreenX = centerX;
+		shipScreenY = centerY;
 	}
 
 	int drawX = shipScreenX - spr.width / 2;
@@ -1174,8 +1208,10 @@ void InsaneRebel1::renderExplosions(byte *dst, int pitch, int width, int height)
 	int shipScreenY = overlayY + _shipPosY;
 	const uint16 effectiveOpcode = getEffectiveGameOpcode();
 	if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
-		shipScreenX = kRA1CenterX + (_perspectiveX - 0x20);
-		shipScreenY = kRA1CenterY + (_perspectiveY - 0x17);
+		int16 centerX, centerY;
+		getTurretShipCenter(centerX, centerY);
+		shipScreenX = centerX;
+		shipScreenY = centerY;
 	}
 
 	// --- Death shake explosions (FUN_1DEB5 LAB_1e0e3) ---


Commit: b45fc2804caa742cda17130b54b94cb33a1f73a9
    https://github.com/scummvm/scummvm/commit/b45fc2804caa742cda17130b54b94cb33a1f73a9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:02+02:00

Commit Message:
SCUMM: RA1: Allow destroying targets in level 1 part 2

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index d64f8e02d4a..189fcda8345 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1992,13 +1992,18 @@ void InsaneRebel1::processShot() {
 }
 
 // checkTargetHit — FUN_1C0EF (0x1C0EF). AABB target detection with snap tolerance.
-// The original compares target bounds against the cursor after
-// UnprojectScreenPoint(), then reprojects the snapped cursor center after a hit.
+// The original compares target bounds against the cursor after UnprojectScreenPoint()
+// because g_shipPos is a screen-space cursor. Most handlers in this port draw the
+// cursor into the 384x242 source buffer before the viewport crop, so their cursor is
+// already in the same target space as raw FOBJ bounds. Opcode 0x0B remains screen-space
+// and still needs the original project/unproject conversion.
 void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom) {
 	int16 snap = _tuning.snap;
+	const bool screenSpaceCursor = (getEffectiveGameOpcode() == 0x0B);
 	int16 curX = getGameplayCursorX();
 	int16 curY = getGameplayCursorY();
-	unprojectGameplayPoint(curX, curY);
+	if (screenSpaceCursor)
+		unprojectGameplayPoint(curX, curY);
 	const int slot = _targetCount;
 
 	if (slot < kMaxTargetBoxes) {
@@ -2028,6 +2033,13 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 		if (curX > left - snap && curX < right + snap &&
 			curY > top - snap && curY < bottom + snap) {
 			_targetProximity = 2;  // On-target
+			if (snap > 0) {
+				int16 snappedX = (left + right) / 2;
+				int16 snappedY = (top + bottom) / 2;
+				if (screenSpaceCursor)
+					projectGameplayPoint(snappedX, snappedY);
+				setGameplayCursor(snappedX, snappedY);
+			}
 
 			// DOS uses g_recentKillObjectIdPlus1 as a frame-wide latch. Once one
 			// target is hit this frame, overlapping FOBJ layers must not consume the
@@ -2079,15 +2091,6 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						const int sfxPan = CLIP((hitCenterX - kRA1CenterX) * 127 / kRA1CenterX, -127, 127);
 						playSfx(kSfxExplode, 127, sfxPan);
 
-						// Match FUN_1C0EF: snap in unprojected space, then project back
-						// into the current gameplay window before rendering the pointer.
-						if (snap > 0) {
-							int16 snappedX = (left + right) / 2;
-							int16 snappedY = (top + bottom) / 2;
-							projectGameplayPoint(snappedX, snappedY);
-							setGameplayCursor(snappedX, snappedY);
-						}
-
 						debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
 							targetIdx, gi, _gostSlots[gi].posX, _gostSlots[gi].posY,
 							_score, _killCount, _bangBank.numSprites);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 240497668d1..d6ce21e3f13 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -334,6 +334,7 @@ bool InsaneRebel1::runLevel1() {
 		_pathBranchEnabled = true;
 		_rightPathSelected = false;
 		_flyControlMode = 1;
+		loadTuningForLevel(0);
 
 		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
 		if (_vm->shouldQuit())
@@ -354,6 +355,10 @@ bool InsaneRebel1::runLevel1() {
 				return false;
 
 			while (!_vm->shouldQuit()) {
+				// RunLevel1Flow calls L1PLAY2 with gameplay selector 1. This is
+				// the "1B" tuning row: snap/kill values are enabled and the
+				// lock/fire text overlay is suppressed.
+				loadTuningForLevel(1);
 				_flyControlMode = 2;
 				_turretEmitterLeftX = 10;
 				_turretEmitterLeftY = -5;


Commit: 361d886ac75b54b9babbad365de8dcaecdd59e31
    https://github.com/scummvm/scummvm/commit/361d886ac75b54b9babbad365de8dcaecdd59e31
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:02+02:00

Commit Message:
SCUMM: RA1: Implement damage in level 2

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 189fcda8345..ba14a752bbf 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -83,6 +83,38 @@ inline bool hasLevel15FinalSweepDamage(uint16 frameCounter, int16 perspectiveX)
 	}
 }
 
+inline bool isLevel2DamageLatch(uint16 code) {
+	switch (code) {
+	case 0x0003:
+	case 0x0009:
+	case 0x000A:
+	case 0x000D:
+	case 0x0012:
+	case 0x0015:
+		return true;
+	default:
+		return false;
+	}
+}
+
+// Level 2 asteroid-contact helper from FUN_00012d70. RunLevel2Flow calls it
+// once per frontend frame and raises damage flag 0x20 when the current asteroid
+// pass intersects the camera position.
+inline bool hasLevel2AsteroidImpact(uint16 frameCounter, int16 perspectiveX, int16 perspectiveY) {
+	switch (frameCounter) {
+	case 0x0071:
+	case 0x0271:
+		return perspectiveX >= 0x28;
+	case 0x011E:
+		return perspectiveY >= 0x1F;
+	case 0x01E1:
+	case 0x02ED:
+		return perspectiveX > 0x17;
+	default:
+		return false;
+	}
+}
+
 const int16 kLevel7BranchFrames[6][6] = {
 	{ -1,  78, 267, 398, 556, 630 },
 	{ -1, 187, 376, 507, 665, 739 },
@@ -1224,8 +1256,10 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
 	const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
+	bool level2AsteroidHit = false;
 
 	if (_gameLatch5D == 0xFFFF ||
+		(_currentLevel == 1 && isLevel2DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
@@ -1242,6 +1276,12 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	if (level15FinalPhase && hasLevel15FinalSweepDamage((uint16)_gameCounter, _perspectiveX))
 		_damageFlags |= 0x20;
 
+	if (_currentLevel == 1) {
+		level2AsteroidHit = hasLevel2AsteroidImpact((uint16)_gameCounter, _perspectiveX, _perspectiveY);
+		if (level2AsteroidHit)
+			_damageFlags |= 0x20;
+	}
+
 	bool level8WalkerPlayerHit = false;
 	if (_currentLevel == 7) {
 		const uint16 walkerFrame = (uint16)_gameCounter;
@@ -1280,6 +1320,12 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 		}
 		// FUN_1CDA7 dispatches g_sfxDamageHit, initialized from SYS/BOOM.SAD.
 		playSfx(kSfxBoom, 127, 0);
+		if (_currentLevel == 1) {
+			debug(1, "RA1 L2 player hit: frame=%u view=(%d,%d) latch=%u asteroid=%d flags=0x%02x health=%d->%d",
+				(unsigned)(uint16)_gameCounter, _perspectiveX, _perspectiveY,
+				(unsigned)_gameLatch5D, level2AsteroidHit ? 1 : 0,
+				appliedDamageFlags, oldHealth, _health);
+		}
 		if (level8WalkerPlayerHit) {
 			debug(1, "RA1 L8 player hit by walker: route=%d frame=%u view=(%d,%d) flags=0x%02x health=%d->%d",
 				CLIP<int>(_levelRouteIndex, 0, 2), (unsigned)(uint16)_gameCounter,


Commit: 03b63a2b5b31e15af93154469c6d7e8debe093a1
    https://github.com/scummvm/scummvm/commit/03b63a2b5b31e15af93154469c6d7e8debe093a1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:03+02:00

Commit Message:
SCUMM: RA1: Add missing damage sounds

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index ba14a752bbf..900f59e558f 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -956,6 +956,8 @@ void InsaneRebel1::updateShipPhysics() {
 
 		_prevDamageFlags = _damageFlags;
 		_damageCooldown = kDamageCooldownInit;
+		// HandleGameOp07_ShipFlight dispatches g_sfxDamageHit here.
+		playSfx(kSfxBoom, 127, 0);
 		_screenFlash = 3;
 	}
 
@@ -1125,6 +1127,8 @@ void InsaneRebel1::updateTurretPhysics() {
 
 		_prevDamageFlags = _damageFlags;
 		_damageCooldown = kDamageCooldownInit;
+		// HandleGameOp08_TurretFlight dispatches g_sfxDamageHit here.
+		playSfx(kSfxBoom, 127, 0);
 		_screenFlash = 3;
 	}
 
@@ -1611,6 +1615,7 @@ void InsaneRebel1::updateOnFootPhysics() {
 		}
 		_prevDamageFlags = _damageFlags;
 		_damageCooldown = 3;
+		playSfx(kSfxBoom, 127, 0);
 		_screenFlash = 5;
 	}
 
@@ -1634,6 +1639,7 @@ void InsaneRebel1::updateOnFootPhysics() {
 		_screenFlash--;
 		_screenShakeEnabled = (_screenFlash > 0);
 	}
+	updateScreenFlashPalette();
 
 	_damageFlags = 0;
 	_frameCounter++;


Commit: b31651a76c4e48c5a88e5c27a7bbe8fb6259eac8
    https://github.com/scummvm/scummvm/commit/b31651a76c4e48c5a88e5c27a7bbe8fb6259eac8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:03+02:00

Commit Message:
SCUMM: RA1: Implement level 13

Changed paths:
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index b9599e1194c..0412b1a8a9b 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -363,6 +363,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_gostSlotIdx = 0;
 	_killCount = 0;
 	_lastHitTarget = 0;
+	resetEnemyShotSlots();
 	_protectedTargetA = 0;
 	_protectedTargetB = 0;
 	_shieldGenHitsA = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 0e874041d17..1da9652da5a 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -163,6 +163,8 @@ private:
 	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
 							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
+	void resetEnemyShotSlots();
+	void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
 	void getTurretShipCenter(int16 &x, int16 &y) const;
 	void renderLevel8Overlay(byte *dst, int pitch, int width, int height,
 		int viewportX, int viewportY);
@@ -203,6 +205,7 @@ private:
 	RA1SpriteBank _techFontBank;  // SYS/TECHFONT.NUT — targeting glyph layer ("<<" markers)
 	RA1SpriteBank _bangBank;      // LxBANG.NUT — impact/explosion sprites (10 frames)
 	RA1SpriteBank _laserBank;     // LxLASER.NUT — laser/shot effect sprites
+	RA1SpriteBank _enemyLaserBank; // LxLASR2.NUT — incoming projectile sprites
 	SmushFont *_menuFont;         // Use engine text renderer for correct TALKFONT character mapping
 
 	// RA1 screen dimensions (384x242)
@@ -482,6 +485,19 @@ private:
 	int16 _killCount;        // 0x75D0: targets destroyed this stage
 	int16 _lastHitTarget;    // 0x75D6: recent-kill latch, allows at most one hit per frame
 
+	// Incoming enemy projectile slots used by Level 13 RunLevel13Flow.
+	static const int kMaxEnemyShotSlots = 5;
+	struct EnemyShotSlot {
+		int16 timer;
+		int16 startX;
+		int16 startY;
+		int16 targetX;
+		int16 targetY;
+		int16 direction;
+		uint16 flags;
+	};
+	EnemyShotSlot _enemyShotSlots[kMaxEnemyShotSlots];
+
 	// Protected target IDs — 0x7732/0x7734 in original
 	// Targets listed here can be hit repeatedly (no event mask toggle).
 	// Used by Level 4 (shield generators) and Level 15 (torpedo targets).
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index f56592dafa1..039e4c10f17 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -241,6 +241,41 @@ int ra1ShotDirectionBucket(int dir) {
 	return 10;
 }
 
+// QuantizeDirectionWithAxisFlags from RunLevel13Flow: returns one of nine
+// direction sprites and publishes DrawFobjGlyph flip flags.
+int ra1ProjectileDirectionWithFlags(int16 srcX, int16 srcY, int16 dstX, int16 dstY, uint16 &flags) {
+	int dx = srcX - dstX;
+	int dy = dstY - srcY;
+	flags = 0;
+
+	if (dy < 0) {
+		dy = -dy;
+		flags |= 0x4000;
+	}
+	if (dx < 0) {
+		dx = -dx;
+		flags |= 0x2000;
+	}
+
+	if (dx * 10 < dy)
+		return 0;
+	if (dx * 10 < dy * 3)
+		return 1;
+	if (dx * 2 < dy)
+		return 2;
+	if (dx * 5 < dy * 4)
+		return 3;
+	if (dx * 4 < dy * 5)
+		return 4;
+	if (dx < dy * 2)
+		return 5;
+	if (dx * 3 < dy * 10)
+		return 6;
+	if (dx < dy * 10)
+		return 7;
+	return 8;
+}
+
 struct RA1ShotEmitterPair {
 	int16 x1;
 	int16 y1;
@@ -602,6 +637,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		_lastHitTarget = 0;
 	}
 
+	if (_currentLevel == 12)
+		renderLevel13EnemyShots(renderBitmap, pitch, width, height);
+
 	renderExplosions(renderBitmap, pitch, width, height);
 
 	// Level 8 (Imperial Walkers) — walker-specific state update + UI overlay.
@@ -771,6 +809,80 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	}
 }
 
+void InsaneRebel1::resetEnemyShotSlots() {
+	memset(_enemyShotSlots, 0, sizeof(_enemyShotSlots));
+}
+
+// Port helper for Level 13 RunLevel13Flow. The original stores this state in
+// five local stack arrays around the L13PLAY frontend loop, not in a named function.
+void InsaneRebel1::renderLevel13EnemyShots(byte *dst, int pitch, int width, int height) {
+	if (_currentLevel != 12 || !_interactiveVideoActive || _health < 0)
+		return;
+
+	const int targetCount = CLIP<int>(_prevTargetCount, 0, kMaxTargetBoxes);
+	const uint16 effectiveOpcode = getEffectiveGameOpcode();
+
+	int16 playerX = getGameplayCursorX();
+	int16 playerY = getGameplayCursorY();
+	if (effectiveOpcode == 0x08 || effectiveOpcode == 0x0A) {
+		getTurretShipCenter(playerX, playerY);
+	} else if (effectiveOpcode == 0x0B) {
+		unprojectGameplayPoint(playerX, playerY);
+	}
+
+	for (int i = 0; i < targetCount; i++) {
+		if (_targetBoxX[i] - 7 < playerX && playerX < _targetBoxX[i] + 7 &&
+			_targetBoxY[i] - 7 < playerY && playerY < _targetBoxY[i] + 7) {
+			_damageFlags |= 0x10;
+		}
+	}
+
+	if (targetCount > 0 && _vm->_rnd.getRandomNumber(19) == 0) {
+		for (int i = 0; i < kMaxEnemyShotSlots; i++) {
+			EnemyShotSlot &slot = _enemyShotSlots[i];
+			if (slot.timer != 0)
+				continue;
+
+			const int targetIdx = _vm->_rnd.getRandomNumber(targetCount - 1);
+			slot.startX = _targetBoxX[targetIdx];
+			slot.startY = _targetBoxY[targetIdx];
+			slot.targetX = playerX;
+			slot.targetY = playerY;
+			slot.timer = 0x0F;
+			slot.direction = ra1ProjectileDirectionWithFlags(slot.startX, slot.startY,
+				slot.targetX, slot.targetY, slot.flags);
+			playSfx(kSfxBlast, 127, 0);
+			break;
+		}
+	}
+
+	for (int i = 0; i < kMaxEnemyShotSlots; i++) {
+		EnemyShotSlot &slot = _enemyShotSlots[i];
+		if (slot.timer == 0)
+			continue;
+
+		const int progress = 0x0F - slot.timer;
+		const int drawX = slot.startX + ((slot.targetX - slot.startX) * progress) / 10;
+		const int drawY = slot.startY + ((slot.targetY - slot.startY) * progress) / 10;
+
+		if (_enemyLaserBank.numSprites > 0) {
+			const int baseSprite = (drawY < 0x50) ? 0x12 : ((drawY < 0xA0) ? 9 : 0);
+			const int spriteIdx = baseSprite + slot.direction;
+			if (spriteIdx >= 0 && spriteIdx < _enemyLaserBank.numSprites) {
+				renderSpriteWithFlags(dst, pitch, width, height, drawX, drawY,
+					_enemyLaserBank.sprites[spriteIdx], slot.flags | 0x3);
+			}
+		}
+
+		slot.timer--;
+		if (drawX - 0x0F < playerX && playerX < drawX + 0x0F &&
+			drawY - 7 < playerY && playerY < drawY + 7) {
+			_damageFlags |= 0x80;
+			slot.timer = 0;
+		}
+	}
+}
+
 // renderLaserShots — FUN_1CDA7/FUN_1D79C/HandleGameOp1A shot visual path.
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
 	const char kRA1TorpedoTrailLeft[] = "<<&";
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index d6ce21e3f13..bf826e05f9a 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1471,6 +1471,7 @@ bool InsaneRebel1::runLevel13() {
 
 	_currentLevel = 12;
 	loadLevelSprites(13);
+	loadRA1Nut("LVL13/L13LASR2.NUT", _enemyLaserBank);
 	loadTuningForLevel(12);
 
 	beginLevelTitleOverlay(12);
@@ -1514,6 +1515,7 @@ bool InsaneRebel1::runLevel13() {
 		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
 		_avgInputX = 0;
 		_avgInputY = 0;
+		resetEnemyShotSlots();
 
 		playInteractiveVideo("LVL13/L13PLAY.ANM");
 		if (_vm->shouldQuit())


Commit: dfeb60086737f9b54068429fa39f62a7bbb5d6ae
    https://github.com/scummvm/scummvm/commit/dfeb60086737f9b54068429fa39f62a7bbb5d6ae
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:03+02:00

Commit Message:
SCUMM: RA1: Implement level 12

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 900f59e558f..956abfb3a1b 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -296,6 +296,25 @@ inline bool isLevel10DamageLatch(uint16 code) {
 	return true;
 }
 
+inline bool isLevel12DamageLatch(uint16 code) {
+	// RunLevel12Flow treats these 0x5D latch values as safe; every other value
+	// raises damage flag 0x40. This helper is intentionally the inverse of the
+	// original branch shape so the update path reads as "is damage".
+	switch (code) {
+	case 0x0000:
+	case 0x0037:
+	case 0x003E:
+	case 0x0059:
+	case 0x00AC:
+	case 0x00C3:
+	case 0x00C5:
+	case 0x00C7:
+		return false;
+	default:
+		return true;
+	}
+}
+
 inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
 	switch (frame) {
 	case 0x006A:
@@ -1267,10 +1286,11 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 		(_currentLevel == 3 && isLevel4DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
+		(_currentLevel == 11 && isLevel12DamageLatch(_gameLatch5D)) ||
 		(level15FinalPhase && isLevel15FinalDamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
 	if (_gameLatch5F != 0 &&
-		((_currentLevel == 3 || _currentLevel == 9 || level15FinalPhase)
+		((_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
 			? (_vm->_rnd.getRandomNumber(2) == 0)
 			: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
 		_damageFlags |= 0x80;
@@ -1450,6 +1470,29 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 
 	_frameCounter++;
 
+	if (_currentLevel == 11) {
+		// RunLevel12Flow checks DAT_761A bits 0x20, 0x08, and 0x02 at frame
+		// 0x550. These map to destroyed object IDs 211, 213, and 215 in the
+		// port's one-based frame-object helper. On failure the original keeps
+		// pumping L12PLAY until frame 0x564, plays L12RETRY, then restarts
+		// L12PLAY without resetting health.
+		if (_levelGameplayPhase == 0 && _frameCounter == 0x550) {
+			const bool target211Destroyed = isFrameObjectPrimarySet(211);
+			const bool target213Destroyed = isFrameObjectPrimarySet(213);
+			const bool target215Destroyed = isFrameObjectPrimarySet(215);
+			if (!target211Destroyed || !target213Destroyed || !target215Destroyed) {
+				_levelGameplayPhase = 1;
+				debug(1, "RA1 L12 retry armed: frame=0x%04x targets=(%d,%d,%d)",
+					_frameCounter,
+					target211Destroyed ? 1 : 0,
+					target213Destroyed ? 1 : 0,
+					target215Destroyed ? 1 : 0);
+			}
+		}
+		if (_levelGameplayPhase == 1 && _frameCounter >= 0x564)
+			_vm->_smushVideoShouldFinish = true;
+	}
+
 	// Level 4 Phase 2: enable torpedo mode at frame 0x3E and finish as
 	// soon as the torpedo registers a hit. The DOS loop exits on killCount.
 	if (_currentLevel == 3 && _levelGameplayPhase == 2) {
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index bf826e05f9a..b652de4a118 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1411,38 +1411,52 @@ bool InsaneRebel1::runLevel12() {
 		_screenFlash = 0;
 		_screenShakeEnabled = false;
 		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
 		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
 
-		playInteractiveVideo("LVL12/L12PLAY.ANM");
-		if (_vm->shouldQuit())
-			return false;
+		while (!_vm->shouldQuit()) {
+			_frameCounter = 0;
+			_gameCounter = 0;
+			_activeGameOpcode = 0;
+			_gameLatch5D = 0;
+			_gameLatch5F = 0;
+			_damageFlags = 0;
+			resetGameplayFlagsFromTuning();
+			_targetCount = 0;
+			_prevTargetCount = 0;
+			_lastHitTarget = 0;
+			_shipPosX = kRA1CenterX;
+			_shipPosY = kRA1CenterY;
+			_shipDirIndex = 17;
+			_rollAccum = 0;
+			_liftSmooth = 0;
+			_posAccumX = 0;
+			_posAccumY = 0;
+			_perspectiveX = 0;
+			_perspectiveY = 0;
+			_levelGameplayPhase = 0;
+			memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+			memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+			memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+			memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+			_avgInputX = 0;
+			_avgInputY = 0;
+
+			playInteractiveVideo("LVL12/L12PLAY.ANM");
+			if (_vm->shouldQuit())
+				return false;
+
+			if (_levelGameplayPhase == 1) {
+				playCinematic("LVL12/L12RETRY.ANM");
+				if (_vm->shouldQuit())
+					return false;
+				if (_health < 0)
+					break;
+				continue;
+			}
+
+			if (_health < 0)
+				break;
 
-		if (_health >= 0) {
 			playCinematic("LVL12/L12END.ANM");
 			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)12);
 			return !_vm->shouldQuit();


Commit: c079896afde4b2887c3e14cb8d985e7ed800e0bd
    https://github.com/scummvm/scummvm/commit/c079896afde4b2887c3e14cb8d985e7ed800e0bd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:04+02:00

Commit Message:
SCUMM: RA1: Implement level 14

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 956abfb3a1b..9b2f80536c4 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -315,6 +315,18 @@ inline bool isLevel12DamageLatch(uint16 code) {
 	}
 }
 
+inline bool isLevel14Phase2DamageLatch(uint16 code) {
+	switch (code) {
+	case 0x0008:
+	case 0x000E:
+	case 0x0010:
+	case 0x0012:
+		return true;
+	default:
+		return false;
+	}
+}
+
 inline bool hasLevel6PerspectiveHazard(uint16 frame, int16 perspectiveX, int16 perspectiveY) {
 	switch (frame) {
 	case 0x006A:
@@ -473,6 +485,42 @@ bool InsaneRebel1::isFrameObjectPrimarySet(int16 objectId) const {
 	return (_frameObjectState[byteIndex] & bit) != 0;
 }
 
+// Port helpers for RunLevel14Flow. The original checks DAT_7614..7616 and
+// DAT_7605..7606 inline, then increments a local 60-frame completion counter.
+bool InsaneRebel1::areLevel14Phase1TargetsDestroyed() const {
+	return isFrameObjectPrimarySet(168) &&
+		isFrameObjectPrimarySet(169) &&
+		isFrameObjectPrimarySet(170) &&
+		isFrameObjectPrimarySet(171) &&
+		isFrameObjectPrimarySet(172) &&
+		isFrameObjectPrimarySet(173) &&
+		isFrameObjectPrimarySet(174) &&
+		isFrameObjectPrimarySet(175) &&
+		isFrameObjectPrimarySet(176) &&
+		isFrameObjectPrimarySet(177) &&
+		isFrameObjectPrimarySet(178) &&
+		isFrameObjectPrimarySet(179) &&
+		isFrameObjectPrimarySet(180) &&
+		isFrameObjectPrimarySet(181) &&
+		isFrameObjectPrimarySet(182) &&
+		isFrameObjectPrimarySet(183);
+}
+
+bool InsaneRebel1::areLevel14Phase2TargetsDestroyed() const {
+	return isFrameObjectPrimarySet(45) &&
+		isFrameObjectPrimarySet(46) &&
+		isFrameObjectPrimarySet(47) &&
+		isFrameObjectPrimarySet(48) &&
+		isFrameObjectPrimarySet(49) &&
+		isFrameObjectPrimarySet(50) &&
+		isFrameObjectPrimarySet(51) &&
+		isFrameObjectPrimarySet(52) &&
+		isFrameObjectPrimarySet(53) &&
+		isFrameObjectPrimarySet(54) &&
+		isFrameObjectPrimarySet(55) &&
+		isFrameObjectPrimarySet(56);
+}
+
 bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
 		int codec, uint8 &ra1Param) {
 	if (!_interactiveVideoActive)
@@ -1279,6 +1327,8 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
 	const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
+	const bool level14Phase1 = (_currentLevel == 13 && _levelGameplayPhase == 1);
+	const bool level14Phase2 = (_currentLevel == 13 && _levelGameplayPhase == 2);
 	bool level2AsteroidHit = false;
 
 	if (_gameLatch5D == 0xFFFF ||
@@ -1287,13 +1337,21 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 		(_currentLevel == 5 && isLevel6DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 9 && isLevel10DamageLatch(_gameLatch5D)) ||
 		(_currentLevel == 11 && isLevel12DamageLatch(_gameLatch5D)) ||
+		(level14Phase2 && isLevel14Phase2DamageLatch(_gameLatch5D)) ||
 		(level15FinalPhase && isLevel15FinalDamageLatch(_gameLatch5D)))
 		_damageFlags |= 0x40;
-	if (_gameLatch5F != 0 &&
-		((_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
-			? (_vm->_rnd.getRandomNumber(2) == 0)
-			: (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0)))
-		_damageFlags |= 0x80;
+	if (_gameLatch5F != 0 && !level14Phase2) {
+		bool randomProjectileHit = false;
+		if (level14Phase1)
+			randomProjectileHit = (_vm->_rnd.getRandomNumber(3) == 0);
+		else if (_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
+			randomProjectileHit = (_vm->_rnd.getRandomNumber(2) == 0);
+		else
+			randomProjectileHit = (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0);
+
+		if (randomProjectileHit)
+			_damageFlags |= 0x80;
+	}
 
 	if (_currentLevel == 5 && hasLevel6PerspectiveHazard((uint16)_frameCounter, _perspectiveX, _perspectiveY))
 		_damageFlags |= 0x20;
@@ -1493,6 +1551,15 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 			_vm->_smushVideoShouldFinish = true;
 	}
 
+	if (_currentLevel == 13 && (_levelGameplayPhase == 1 || _levelGameplayPhase == 2)) {
+		const bool targetsDestroyed = (_levelGameplayPhase == 1) ?
+			areLevel14Phase1TargetsDestroyed() : areLevel14Phase2TargetsDestroyed();
+		if (targetsDestroyed && _level14SuccessFrames < 0x3C)
+			_level14SuccessFrames++;
+		if (_level14SuccessFrames >= 0x3C)
+			_vm->_smushVideoShouldFinish = true;
+	}
+
 	// Level 4 Phase 2: enable torpedo mode at frame 0x3E and finish as
 	// soon as the torpedo registers a hit. The DOS loop exits on killCount.
 	if (_currentLevel == 3 && _levelGameplayPhase == 2) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 0412b1a8a9b..ebb258dc15b 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -307,6 +307,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_pendingRouteCutoverFrame = -1;
 	_levelGameplayPhase = 0;
 	_level5SuccessFramesRemaining = 0;
+	_level14SuccessFrames = 0;
 	_menuActive = false;
 	_introTextActive = false;
 	_introTextStartFrame = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 1da9652da5a..2fb95157f02 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -175,6 +175,8 @@ private:
 	void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
 	void applyFrameObjectHitState(int16 targetIdx);
 	bool isFrameObjectPrimarySet(int16 objectId) const;
+	bool areLevel14Phase1TargetsDestroyed() const;
+	bool areLevel14Phase2TargetsDestroyed() const;
 
 	// Shooting pipeline — FUN_1CCA0 (0x1CCA0) shot spawner,
 	// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
@@ -508,6 +510,7 @@ private:
 	int16 _shieldGenHitsA;   // Hits on _protectedTargetA
 	int16 _shieldGenHitsB;   // Hits on _protectedTargetB
 	int16 _level5SuccessFramesRemaining; // DOS RunLevel5Flow: 20-frame hold after the third kill
+	int16 _level14SuccessFrames; // RunLevel14Flow: 60-frame hold after required targets are destroyed
 
 	// Level 15 torpedo success latch. The original derives this from
 	// g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index b652de4a118..46aa308e005 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1600,7 +1600,8 @@ bool InsaneRebel1::runLevel14() {
 		_posAccumY = 0;
 		_perspectiveX = 0;
 		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
+		_levelGameplayPhase = 1;
+		_level14SuccessFrames = 0;
 		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
 		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
 		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
@@ -1620,6 +1621,8 @@ bool InsaneRebel1::runLevel14() {
 			_gameLatch5F = 0;
 			resetGameplayFlagsFromTuning();
 			_killCount = 0;
+			_levelGameplayPhase = 2;
+			_level14SuccessFrames = 0;
 
 			playInteractiveVideo("LVL14/L14PLAY2.ANM");
 			if (_vm->shouldQuit())


Commit: e94cfd3f51ec3546e9ca4f071f4f5bf342a9cf28
    https://github.com/scummvm/scummvm/commit/e94cfd3f51ec3546e9ca4f071f4f5bf342a9cf28
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:04+02:00

Commit Message:
SCUMM: RA1: Implement level 11 hits overlay

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9b2f80536c4..110fd12f947 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1326,6 +1326,7 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
 	//   0x5F non-zero + RNG -> bit 0x80 (scripted random hit)
+	const bool level15Phase1 = (_currentLevel == 14 && _levelGameplayPhase == 1);
 	const bool level15FinalPhase = (_currentLevel == 14 && _levelGameplayPhase == 2);
 	const bool level14Phase1 = (_currentLevel == 13 && _levelGameplayPhase == 1);
 	const bool level14Phase2 = (_currentLevel == 13 && _levelGameplayPhase == 2);
@@ -1342,9 +1343,12 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 		_damageFlags |= 0x40;
 	if (_gameLatch5F != 0 && !level14Phase2) {
 		bool randomProjectileHit = false;
+		// Original level loops spell these fixed-probability cases separately.
+		// The shared 0x0B path collapses them into one branch.
 		if (level14Phase1)
 			randomProjectileHit = (_vm->_rnd.getRandomNumber(3) == 0);
-		else if (_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 || level15FinalPhase)
+		else if (_currentLevel == 3 || _currentLevel == 9 || _currentLevel == 11 ||
+				level15Phase1 || level15FinalPhase)
 			randomProjectileHit = (_vm->_rnd.getRandomNumber(2) == 0);
 		else
 			randomProjectileHit = (_vm->_rnd.getRandomNumber((uint16)(_gameLatch5F - 1)) == 0);
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 2fb95157f02..4173a1da385 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -163,6 +163,7 @@ private:
 	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
 							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
+	void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
 	void resetEnemyShotSlots();
 	void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
 	void getTurretShipCenter(int16 &x, int16 &y) const;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 039e4c10f17..140751dab2c 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -642,6 +642,9 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	renderExplosions(renderBitmap, pitch, width, height);
 
+	if (_currentLevel == 10)
+		renderLevel11HitsOverlay(renderBitmap, pitch, width, height);
+
 	// Level 8 (Imperial Walkers) — walker-specific state update + UI overlay.
 	// In the original, RunLevel8Flow runs the walker logic inline in the per-frame
 	// game loop. We call it from procPostRendering when _currentLevel == 7.
@@ -809,6 +812,18 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	}
 }
 
+// renderLevel11HitsOverlay — RunLevel11Flow (0x19F9F) calls FormatAndDrawText
+// each L11PLAY frame with "<<HITS %02d" at (0x119, 0x16). This helper is a
+// ScummVM-side extraction; the original keeps the draw call inline in the loop.
+void InsaneRebel1::renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height) {
+	if (_hudFontBank.numSprites <= 0 && _techFontBank.numSprites <= 0)
+		return;
+
+	char hitsStr[16];
+	Common::sprintf_s(hitsStr, "<<HITS %02d", (int)_killCount);
+	drawFontBankString(dst, pitch, width, height, 0x119, 0x16, hitsStr);
+}
+
 void InsaneRebel1::resetEnemyShotSlots() {
 	memset(_enemyShotSlots, 0, sizeof(_enemyShotSlots));
 }


Commit: 2efc01e65543bfc4206ca5f30efed28f44b1bd8f
    https://github.com/scummvm/scummvm/commit/2efc01e65543bfc4206ca5f30efed28f44b1bd8f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:04+02:00

Commit Message:
SCUMM: RA1: Implement level 14 splice

Changed paths:
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index ebb258dc15b..934de8c438e 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -306,6 +306,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_pendingRouteStartFrame = 0;
 	_pendingRouteCutoverFrame = -1;
 	_levelGameplayPhase = 0;
+	_level14Play2BSplicePending = false;
+	_level14Play2BSpliced = false;
+	_level14Play2BSpliceFrame = 0;
 	_level5SuccessFramesRemaining = 0;
 	_level14SuccessFrames = 0;
 	_menuActive = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 4173a1da385..a9ced908ece 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -163,6 +163,7 @@ private:
 	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
 							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
+	void handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame);
 	void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
 	void resetEnemyShotSlots();
 	void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
@@ -408,6 +409,10 @@ private:
 	int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
 	int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
 	int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
+	// RunLevel14Flow queues L14PLY2B at L14PLAY2 maxFrame-0x0F.
+	bool _level14Play2BSplicePending;
+	bool _level14Play2BSpliced;
+	int32 _level14Play2BSpliceFrame;
 
 	// Main menu / options state
 	void runOptionsMenu();
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 140751dab2c..622e92b3cf6 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -641,6 +641,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		renderLevel13EnemyShots(renderBitmap, pitch, width, height);
 
 	renderExplosions(renderBitmap, pitch, width, height);
+	handleLevel14Play2BSplice(curFrame, maxFrame);
 
 	if (_currentLevel == 10)
 		renderLevel11HitsOverlay(renderBitmap, pitch, width, height);
@@ -741,6 +742,31 @@ void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height)
 	_lastHitTarget = 0;
 }
 
+// handleLevel14Play2BSplice — RunLevel14Flow (0x1ACD1) queues L14PLY2B.ANM
+// from inside the L14PLAY2 loop when the current clip reaches maxFrame - 0x0F.
+// This helper is a ScummVM-side extraction; the original keeps the PlayAnmFile
+// call inline and passes the old L14PLAY2 timeline frame to the ANM frame gate.
+void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
+	if (_currentLevel != 13 || _levelGameplayPhase != 2 || _level14Play2BSpliced ||
+			_level14Play2BSplicePending || maxFrame < 0x0F)
+		return;
+
+	if (curFrame != maxFrame - 0x0F)
+		return;
+
+	_level14Play2BSpliced = true;
+	_level14Play2BSplicePending = true;
+	_level14Play2BSpliceFrame = curFrame;
+
+	// Original after PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860, maxFrame-0x0F, 1, -1):
+	//   g_extendedPhaseFlags &= 0xFA;
+	//   DAT_7604 &= 0xBF;
+	// In the port's object-state storage those are the first and third primary bytes.
+	_frameObjectState[0] &= 0xFA;
+	_frameObjectState[2] &= 0xBF;
+	_vm->_smushVideoShouldFinish = true;
+}
+
 // renderGostScorePopup — Per-kill score glyph from RenderGostOverlaySlots (0x1C9CD).
 // Maps kill score to tech-font glyph and draws it rising upward from the kill position.
 void InsaneRebel1::renderGostScorePopup(byte *dst, int pitch, int width, int height,
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 46aa308e005..8f3ea478862 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1558,7 +1558,8 @@ bool InsaneRebel1::runLevel13() {
 
 // Level 14 flow (RunLevel14Flow, 0x1ACB0): Surface Cannon
 // Two interactive phases: L14PLAY (targeting cannons) + L14PLAY2 (exhaust port approach).
-// Original: L14INTRO → L14PLAY → L14PLAY2 → L14END/L14NEW/L14DEATH
+// Original: L14INTRO → L14PLAY → L14PLAY2 → optional L14PLY2B splice
+//   → L14END/L14NEW/L14DEATH
 bool InsaneRebel1::runLevel14() {
 	debug(1, "InsaneRebel1: Running level 14");
 
@@ -1623,10 +1624,26 @@ bool InsaneRebel1::runLevel14() {
 			_killCount = 0;
 			_levelGameplayPhase = 2;
 			_level14SuccessFrames = 0;
+			_level14Play2BSplicePending = false;
+			_level14Play2BSpliced = false;
+			_level14Play2BSpliceFrame = 0;
 
 			playInteractiveVideo("LVL14/L14PLAY2.ANM");
 			if (_vm->shouldQuit())
 				return false;
+
+			if (_health >= 0 && _level14Play2BSplicePending) {
+				const int32 spliceFrame = _level14Play2BSpliceFrame;
+				_level14Play2BSplicePending = false;
+
+				// DOS queues L14PLY2B from the L14PLAY2 loop with startFrame
+				// equal to L14PLAY2's old timeline frame. L14PLY2B itself is the
+				// continuation clip, so the port starts it from frame 0 but uses
+				// the non-zero frame argument to preserve gameplay/video state.
+				playInteractiveVideo("LVL14/L14PLY2B.ANM", spliceFrame);
+				if (_vm->shouldQuit())
+					return false;
+			}
 		}
 
 		if (_health >= 0) {
@@ -1894,6 +1911,13 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 			debug(1, "RA1 L8 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
 				_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
 		}
+	} else if (_currentLevel == 13 && resumingRoute) {
+		// RunLevel14Flow calls PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860,
+		// oldMaxFrame-0x0F, 1, -1). That frame number belongs to L14PLAY2's
+		// timeline; L14PLY2B is already the continuation clip and starts at its
+		// matching lead-in frame. Preserve the current state, but do not seek.
+		debug(1, "RA1 L14 splice: L14PLAY2 timelineFrame=%d -> L14PLY2B frame 0",
+			(int)startFrame);
 	}
 
 	// Center mouse, hide system cursor (we draw our own), lock mouse to window


Commit: 6f800ee60d3ffca852940061bb2f6e69ae5f50e4
    https://github.com/scummvm/scummvm/commit/6f800ee60d3ffca852940061bb2f6e69ae5f50e4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00

Commit Message:
SCUMM: RA1: Implement level 15 summary and scoring

Changed paths:
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 934de8c438e..c831798568f 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -316,6 +316,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_introTextStartFrame = 0;
 	_introTextEndFrame = 0;
 	_introTextLevel = 0;
+	_level15SummaryActive = false;
+	_level15SummaryTargetBonus = 0;
 	_menuConfirmed = false;
 	_menuSelection = 0;
 	_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index a9ced908ece..b931a413d56 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -307,6 +307,10 @@ private:
 	int _introTextLevel;         // index into kLevelTitles
 	void beginLevelTitleOverlay(int level);
 	void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
+	bool _level15SummaryActive;
+	int _level15SummaryTargetBonus;
+	void beginLevel15SummaryOverlay(int targetBonus);
+	void drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
 
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 622e92b3cf6..fd2400b0f39 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -187,6 +187,30 @@ const RA1SpriteBank &selectLayerBank(const RA1SpriteBank &titleBank,
 	return (titleBank.numSprites > 0) ? titleBank : hudBank;
 }
 
+void drawCenteredRebel1String(InsaneRebel1 *rebel1, byte *dst, int pitch, int width, int height,
+		int centerX, int y, const char *text, int maxChars = -1) {
+	if (!rebel1 || !text)
+		return;
+
+	const char *drawText = text;
+	char clipped[96];
+	if (maxChars >= 0) {
+		int len = 0;
+		while (text[len] != '\0' && len < maxChars && len < (int)sizeof(clipped) - 1) {
+			clipped[len] = text[len];
+			len++;
+		}
+		clipped[len] = '\0';
+		drawText = clipped;
+	}
+
+	if (drawText[0] == '\0')
+		return;
+
+	const int textW = rebel1->getFontBankStringWidth(drawText);
+	rebel1->drawFontBankString(dst, pitch, width, height, centerX - textW / 2, y, drawText);
+}
+
 int getBankSpaceHeight(const RA1SpriteBank &bank) {
 	// In FUN_221B7 line advance is derived from the layer's space-glyph height (+4).
 	// With current NUT decoding we approximate that using the '!' glyph (index 0).
@@ -487,6 +511,14 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		drawLevelTitleOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
 	}
 
+	if (_level15SummaryActive && renderBitmap) {
+		int w = _player ? _player->_width : _screenWidth;
+		int h = _player ? _player->_height : _screenHeight;
+		if (w == 0) w = _screenWidth;
+		if (h == 0) h = _screenHeight;
+		drawLevel15SummaryOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
+	}
+
 	if (!_interactiveVideoActive || !renderBitmap)
 		return;
 
@@ -1217,6 +1249,53 @@ void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int he
 	drawFontBankString(dst, pitch, width, height, centerX - subtitleW / 2, 25, subtitle);
 }
 
+void InsaneRebel1::beginLevel15SummaryOverlay(int targetBonus) {
+	_level15SummaryActive = true;
+	_level15SummaryTargetBonus = targetBonus;
+}
+
+// drawLevel15SummaryOverlay — RunChapterCompleteSummaryScreen (0x15E42), used by
+// RunLevel1GameLoop's L15END1 path. This is a ScummVM-side extraction of the
+// original summary draw loop; the score update is done by runLevel15().
+void InsaneRebel1::drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height,
+		int32 curFrame, int32 maxFrame) {
+	if (!_level15SummaryActive || !dst || maxFrame <= 0)
+		return;
+
+	const int32 revealBaseFrame = maxFrame - 0x122;
+	const int32 stopFrame = maxFrame - 0xA5;
+	if (curFrame < revealBaseFrame || curFrame >= stopFrame)
+		return;
+
+	const int centerX = width / 2;
+	const int titleChars = MAX<int>(0, (int)(curFrame - revealBaseFrame));
+	drawCenteredRebel1String(this, dst, pitch, width, height,
+		centerX, 5, "Chapter Complete", titleChars);
+
+	if (revealBaseFrame + 0x0F < curFrame) {
+		char completionText[40];
+		Common::sprintf_s(completionText, "Completion bonus: %d", (int)_tuning.levelPts);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x19, completionText);
+	}
+
+	if (revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
+		char accuracyText[48];
+		char bonusText[32];
+		Common::sprintf_s(accuracyText, "Target Accuracy: %d percent",
+			((int)_killCount * 100) / 0x58);
+		Common::sprintf_s(bonusText, "Bonus: %d", _level15SummaryTargetBonus);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part I");
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, accuracyText);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
+	}
+
+	if (revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part II");
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, "Torpedo on mark");
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, "Bonus: 10000");
+	}
+}
+
 // drawFontBankString — FUN_221B7 (0x221B7), partial parity:
 // supports '<'/'>' layer markup and layer-2 space handling used by RA1 HUD/targeting strings.
 void InsaneRebel1::drawFontBankString(byte *dst, int pitch, int width, int height, int x, int y, const char *text) {
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 8f3ea478862..c3480e719d7 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1754,7 +1754,16 @@ bool InsaneRebel1::runLevel15() {
 		}
 
 		if (_health >= 0) {
+			int targetBonus = 0;
+			if (_killCount > 0x57)
+				targetBonus = 1000;
+			if (_killCount > 0x14)
+				targetBonus += (_killCount - 0x14) * _tuning.bonus;
+
+			beginLevel15SummaryOverlay(targetBonus);
 			playCinematic("LVL15/L15END1.ANM");
+			_level15SummaryActive = false;
+			_score += _tuning.levelPts + targetBonus + 10000;
 			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
 			return !_vm->shouldQuit();
 		}


Commit: 988fc5be0addabf8a3ad4349a926d846a8747238
    https://github.com/scummvm/scummvm/commit/988fc5be0addabf8a3ad4349a926d846a8747238
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00

Commit Message:
SCUMM: RA1: Implement generic level summary and scoring

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 110fd12f947..f4e779b0b2d 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1056,7 +1056,7 @@ void InsaneRebel1::updateShipPhysics() {
 	// --- Path branching detection ---
 	// Original (FUN_1B297): at GAME counter 394 (0x18A), sets nextSceneA=0x67/nextSceneB=0x69.
 	// After this point, drift goes strongly negative (pushing ship left for the hard path).
-	// If ship is right of center, player chose the right/easy path → switch to L1PLAY1R.
+	// If ship is right of center, player chose the hard branch → switch to L1PLAY1R.
 	// Keep this as a one-shot decision: once threshold is reached, lock path.
 	if (_pathBranchEnabled && _gameCounter >= kPathBranchCounter) {
 		if (_shipPosX > kRA1CenterX) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index c831798568f..bec456bd16e 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -316,8 +316,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_introTextStartFrame = 0;
 	_introTextEndFrame = 0;
 	_introTextLevel = 0;
-	_level15SummaryActive = false;
-	_level15SummaryTargetBonus = 0;
+	memset(&_chapterSummary, 0, sizeof(_chapterSummary));
 	_menuConfirmed = false;
 	_menuSelection = 0;
 	_menuFrameCounter = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index b931a413d56..77855815881 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -140,6 +140,11 @@ private:
 	// Play a passive cinematic (no game callback, skippable)
 	// startFrame > 0: fast-forward (decode without display) to that frame
 	void playCinematic(const char *filename, int32 startFrame = 0);
+	void playChapterCompleteCinematic(const char *filename, int16 unlockedChapter,
+		int revealOffsetFromEnd, int stopOffsetFromEnd,
+		const char *bonusLabel1 = nullptr, const char *detailText1 = nullptr, int bonusValue1 = 0,
+		const char *bonusLabel2 = nullptr, const char *detailText2 = nullptr, int bonusValue2 = 0,
+		int passwordIndex = 0);
 
 	// Play interactive gameplay video (with ship physics + HUD)
 	void playInteractiveVideo(const char *filename, int32 startFrame = 0);
@@ -307,10 +312,28 @@ private:
 	int _introTextLevel;         // index into kLevelTitles
 	void beginLevelTitleOverlay(int level);
 	void drawLevelTitleOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
-	bool _level15SummaryActive;
-	int _level15SummaryTargetBonus;
-	void beginLevel15SummaryOverlay(int targetBonus);
-	void drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
+
+	static const int kChapterSummaryTextSize = 80;
+	struct ChapterSummaryState {
+		bool active;
+		int revealOffsetFromEnd;
+		int stopOffsetFromEnd;
+		bool hasBonus1;
+		bool hasBonus2;
+		char bonusLabel1[kChapterSummaryTextSize];
+		char detailText1[kChapterSummaryTextSize];
+		int bonusValue1;
+		char bonusLabel2[kChapterSummaryTextSize];
+		char detailText2[kChapterSummaryTextSize];
+		int bonusValue2;
+		int passwordIndex;
+	};
+	ChapterSummaryState _chapterSummary;
+	void beginChapterSummaryOverlay(int revealOffsetFromEnd, int stopOffsetFromEnd,
+		const char *bonusLabel1, const char *detailText1, int bonusValue1,
+		const char *bonusLabel2, const char *detailText2, int bonusValue2,
+		int passwordIndex);
+	void drawChapterSummaryOverlay(byte *dst, int pitch, int width, int height, int32 curFrame, int32 maxFrame);
 
 	// Control mode (from GAME opcode 0x5E)
 	int16 _flyControlMode;
@@ -407,7 +430,7 @@ private:
 	static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
 	int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
 	bool _pathBranchEnabled;     // True when branching is active for this video
-	bool _rightPathSelected;     // True if player chose the right/easy path
+	bool _rightPathSelected;     // True if player branched into L1PLAY1R
 	int _levelRouteIndex;        // Current mid-level route/segment for branching levels
 	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
 	int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index fd2400b0f39..9cb9c810bfb 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -511,12 +511,12 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		drawLevelTitleOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
 	}
 
-	if (_level15SummaryActive && renderBitmap) {
+	if (_chapterSummary.active && renderBitmap) {
 		int w = _player ? _player->_width : _screenWidth;
 		int h = _player ? _player->_height : _screenHeight;
 		if (w == 0) w = _screenWidth;
 		if (h == 0) h = _screenHeight;
-		drawLevel15SummaryOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
+		drawChapterSummaryOverlay(renderBitmap, w, w, h, curFrame, maxFrame);
 	}
 
 	if (!_interactiveVideoActive || !renderBitmap)
@@ -1249,21 +1249,48 @@ void InsaneRebel1::drawLevelTitleOverlay(byte *dst, int pitch, int width, int he
 	drawFontBankString(dst, pitch, width, height, centerX - subtitleW / 2, 25, subtitle);
 }
 
-void InsaneRebel1::beginLevel15SummaryOverlay(int targetBonus) {
-	_level15SummaryActive = true;
-	_level15SummaryTargetBonus = targetBonus;
+void InsaneRebel1::beginChapterSummaryOverlay(int revealOffsetFromEnd, int stopOffsetFromEnd,
+		const char *bonusLabel1, const char *detailText1, int bonusValue1,
+		const char *bonusLabel2, const char *detailText2, int bonusValue2,
+		int passwordIndex) {
+	memset(&_chapterSummary, 0, sizeof(_chapterSummary));
+	_chapterSummary.active = true;
+	_chapterSummary.revealOffsetFromEnd = revealOffsetFromEnd;
+	_chapterSummary.stopOffsetFromEnd = stopOffsetFromEnd;
+	_chapterSummary.bonusValue1 = bonusValue1;
+	_chapterSummary.bonusValue2 = bonusValue2;
+	_chapterSummary.passwordIndex = passwordIndex;
+
+	if (bonusLabel1 && detailText1) {
+		_chapterSummary.hasBonus1 = true;
+		Common::strlcpy(_chapterSummary.bonusLabel1, bonusLabel1, sizeof(_chapterSummary.bonusLabel1));
+		Common::strlcpy(_chapterSummary.detailText1, detailText1, sizeof(_chapterSummary.detailText1));
+	}
+	if (bonusLabel2 && detailText2) {
+		_chapterSummary.hasBonus2 = true;
+		Common::strlcpy(_chapterSummary.bonusLabel2, bonusLabel2, sizeof(_chapterSummary.bonusLabel2));
+		Common::strlcpy(_chapterSummary.detailText2, detailText2, sizeof(_chapterSummary.detailText2));
+	}
 }
 
-// drawLevel15SummaryOverlay — RunChapterCompleteSummaryScreen (0x15E42), used by
-// RunLevel1GameLoop's L15END1 path. This is a ScummVM-side extraction of the
-// original summary draw loop; the score update is done by runLevel15().
-void InsaneRebel1::drawLevel15SummaryOverlay(byte *dst, int pitch, int width, int height,
+// The DOS table is stored as 15 XOR-0xAA encoded, 20-byte passcode slots at
+// DS:0x00A4. RunChapterCompleteSummaryScreen indexes it with passwordIndex-1.
+static const char *const kChapterCompletePasswords[] = {
+	"FALCON", "BIGGS", "ACKBAR", "ANOAT", "KAIBURR",
+	"FORNAX", "YUZZEM", "MYNOCK", "BESPIN", "BRIGIA",
+	"DAGOBAH", "KESSEL", "GREEDO", "MIMBAN", "ORGANA"
+};
+
+// drawChapterSummaryOverlay — RunChapterCompleteSummaryScreen (0x15E42), shared by
+// the RA1 runlevel flows that call it. This helper is a ScummVM-side extraction;
+// the original pumps frontend frames from the runlevel after queueing the END ANM.
+void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, int height,
 		int32 curFrame, int32 maxFrame) {
-	if (!_level15SummaryActive || !dst || maxFrame <= 0)
+	if (!_chapterSummary.active || !dst || maxFrame <= 0)
 		return;
 
-	const int32 revealBaseFrame = maxFrame - 0x122;
-	const int32 stopFrame = maxFrame - 0xA5;
+	const int32 revealBaseFrame = maxFrame - _chapterSummary.revealOffsetFromEnd;
+	const int32 stopFrame = maxFrame - _chapterSummary.stopOffsetFromEnd;
 	if (curFrame < revealBaseFrame || curFrame >= stopFrame)
 		return;
 
@@ -1278,21 +1305,31 @@ void InsaneRebel1::drawLevel15SummaryOverlay(byte *dst, int pitch, int width, in
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x19, completionText);
 	}
 
-	if (revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
-		char accuracyText[48];
+	if (_chapterSummary.hasBonus1 &&
+			revealBaseFrame + 0x28 < curFrame && curFrame < revealBaseFrame + 0x46) {
+		char bonusText[32];
+		Common::sprintf_s(bonusText, "Bonus: %d", _chapterSummary.bonusValue1);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, _chapterSummary.bonusLabel1);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, _chapterSummary.detailText1);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
+	}
+
+	if (_chapterSummary.hasBonus2 &&
+			revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
 		char bonusText[32];
-		Common::sprintf_s(accuracyText, "Target Accuracy: %d percent",
-			((int)_killCount * 100) / 0x58);
-		Common::sprintf_s(bonusText, "Bonus: %d", _level15SummaryTargetBonus);
-		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part I");
-		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, accuracyText);
+		Common::sprintf_s(bonusText, "Bonus: %d", _chapterSummary.bonusValue2);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, _chapterSummary.bonusLabel2);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, _chapterSummary.detailText2);
 		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, bonusText);
 	}
 
-	if (revealBaseFrame + 0x55 < curFrame && curFrame < revealBaseFrame + 0x73) {
-		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x32, "Part II");
-		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x46, "Torpedo on mark");
-		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x5A, "Bonus: 10000");
+	if (_chapterSummary.passwordIndex > 0 &&
+			_chapterSummary.passwordIndex <= ARRAYSIZE(kChapterCompletePasswords) &&
+			revealBaseFrame + 10 < curFrame) {
+		char passwordText[40];
+		Common::sprintf_s(passwordText, "Password: %s",
+			kChapterCompletePasswords[_chapterSummary.passwordIndex - 1]);
+		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
 	}
 }
 
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index c3480e719d7..f42e283fff3 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -208,6 +208,22 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
 	return result;
 }
 
+static void formatTargetAccuracy(char *dst, size_t dstSize, int kills, int targetCount, bool perfectText) {
+	if (perfectText && kills >= targetCount)
+		Common::sprintf_s(dst, dstSize, "Target Accuracy: Perfect");
+	else
+		Common::sprintf_s(dst, dstSize, "Target Accuracy: %d percent", (kills * 100) / targetCount);
+}
+
+static int calculateThresholdBonus(int kills, int perfectThreshold, int perKillThreshold, int perKillBonus) {
+	int bonus = 0;
+	if (kills > perfectThreshold)
+		bonus += 1000;
+	if (kills > perKillThreshold)
+		bonus += (kills - perKillThreshold) * perKillBonus;
+	return bonus;
+}
+
 // ---------------------------------------------------------------------------
 // Game flow (matching original at 0x15597)
 // ---------------------------------------------------------------------------
@@ -232,6 +248,23 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
 	_introTextActive = false;
 }
 
+// The original runlevel flows repeat this pattern around RunChapterCompleteSummaryScreen:
+// queue the END ANM, draw the summary while the frontend pumps, then award points/unlock.
+void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlockedChapter,
+		int revealOffsetFromEnd, int stopOffsetFromEnd,
+		const char *bonusLabel1, const char *detailText1, int bonusValue1,
+		const char *bonusLabel2, const char *detailText2, int bonusValue2,
+		int passwordIndex) {
+	beginChapterSummaryOverlay(revealOffsetFromEnd, stopOffsetFromEnd,
+		bonusLabel1, detailText1, bonusValue1,
+		bonusLabel2, detailText2, bonusValue2,
+		passwordIndex);
+	playCinematic(filename);
+	_chapterSummary.active = false;
+	_score += _tuning.levelPts + bonusValue1 + bonusValue2;
+	_maxChapterUnlocked = MAX(_maxChapterUnlocked, unlockedChapter);
+}
+
 void InsaneRebel1::clearVideoBuffer() {
 	if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
 		return;
@@ -277,7 +310,7 @@ void InsaneRebel1::playIntroSequence() {
 //   2. L1HANGAR.ANM — Full hangar departure cutscene (782 frames, flags 0x0420)
 //   3. L1CU1.ANM — Pre-flight cutscene (flags 0x0400)
 //   4. L1PLAY1L.ANM — Stage 1 flight, hard/left path (788 frames)
-//      At frame 394, if player steers right → L1PLAY1R (easy path, 396 frames)
+//      At frame 394, if player steers right → L1PLAY1R (hard path, 396 frames)
 //   5. L1CU2.ANM — Mid-level cutscene
 //   6. L1PLAY2.ANM — Stage 2 turret
 //      If score < 5 (0x75D0): L1RETRY → retry Stage 2
@@ -378,8 +411,14 @@ bool InsaneRebel1::runLevel1() {
 					break;
 
 				if (_killCount > 4) {
-					playCinematic("LVL1/L1END.ANM");
-					_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)1);
+					const char *pathText = _rightPathSelected ? "Path Taken: Hard" : "Path Taken: Easy";
+					const int pathBonus = _rightPathSelected ? _tuning.bonus * 3 : 0;
+					char accuracyText[80];
+					formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x0E, true);
+					const int targetBonus = calculateThresholdBonus(_killCount, 0x0D, 5, _tuning.bonus);
+					playChapterCompleteCinematic("LVL1/L1END.ANM", 1, 0x78, 5,
+						"Part I", pathText, pathBonus,
+						"Part II", accuracyText, targetBonus);
 					return !_vm->shouldQuit();
 				}
 
@@ -451,8 +490,10 @@ bool InsaneRebel1::runLevel2() {
 			return false;
 
 		if (_health >= 0) {
-			playCinematic("LVL2/L2END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)2);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x18, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x17, 0x0C, _tuning.bonus);
+			playChapterCompleteCinematic("LVL2/L2END.ANM", 2, 0x69, 10, " ", accuracyText, bonus);
 			return !_vm->shouldQuit();
 		}
 
@@ -514,8 +555,8 @@ bool InsaneRebel1::runLevel3() {
 			return false;
 
 		if (_health >= 0) {
-			playCinematic("LVL3/L3END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)3);
+			playChapterCompleteCinematic("LVL3/L3END.ANM", 3, 0x69, 5,
+				nullptr, nullptr, 0, nullptr, nullptr, 0, 3);
 			return !_vm->shouldQuit();
 		}
 
@@ -597,8 +638,10 @@ bool InsaneRebel1::runLevel4() {
 
 		if (_health >= 0) {
 			// L4END1 = torpedo hit, L4END2 = torpedo missed
-			playCinematic((_killCount != 0) ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)4);
+			const bool torpedoHit = (_killCount != 0);
+			playChapterCompleteCinematic(torpedoHit ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM",
+				4, 0x69, 5, " ", torpedoHit ? "Torpedo Hit" : "Torpedo Missed",
+				torpedoHit ? _tuning.bonus : 0);
 			return !_vm->shouldQuit();
 		}
 
@@ -717,8 +760,16 @@ bool InsaneRebel1::runLevel5() {
 			return false;
 
 		if (_health >= 0) {
-			playCinematic("LVL5/L5END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)5);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 100, false);
+			int bonus = 0;
+			if (_killCount >= 0x1A)
+				bonus += (_killCount - 0x19) * _tuning.bonus;
+			if (_killCount >= 0x33)
+				bonus += (_killCount - 0x32) * _tuning.bonus;
+			if (_killCount >= 0x4C)
+				bonus += (_killCount - 0x4B) * _tuning.bonus;
+			playChapterCompleteCinematic("LVL5/L5END.ANM", 5, 0x69, 5, " ", accuracyText, bonus);
 			return !_vm->shouldQuit();
 		}
 
@@ -781,8 +832,11 @@ bool InsaneRebel1::runLevel6() {
 			return false;
 
 		if (_health >= 0) {
-			playCinematic("LVL6/L6END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)6);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x27, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
+			playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
+				" ", accuracyText, bonus, nullptr, nullptr, 0, 6);
 			return !_vm->shouldQuit();
 		}
 
@@ -889,8 +943,10 @@ bool InsaneRebel1::runLevel7() {
 		_pendingRouteCutoverFrame = -1;
 
 		if (_health >= 0) {
-			playCinematic("LVL7/L7END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)7);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x33, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x32, 0x14, _tuning.bonus);
+			playChapterCompleteCinematic("LVL7/L7END.ANM", 7, 0x69, 5, " ", accuracyText, bonus);
 			return !_vm->shouldQuit();
 		}
 
@@ -1007,8 +1063,7 @@ bool InsaneRebel1::runLevel8() {
 		_pendingRouteCutoverFrame = -1;
 
 		if (_health >= 0) {
-			playCinematic("LVL8/L8END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)8);
+			playChapterCompleteCinematic("LVL8/L8END.ANM", 8, 0x5F, 5);
 			return !_vm->shouldQuit();
 		}
 
@@ -1199,8 +1254,7 @@ bool InsaneRebel1::runLevel9() {
 			if (_health < 0)
 				break;
 
-			playCinematic("LVL9/L9END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)9);
+			playChapterCompleteCinematic("LVL9/L9END.ANM", 9, 0x69, 5);
 			return !_vm->shouldQuit();
 		}
 
@@ -1273,8 +1327,11 @@ bool InsaneRebel1::runLevel10() {
 			return false;
 
 		if (_health >= 0) {
-			playCinematic("LVL10/L10END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)10);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x3E, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x3D, 0x32, _tuning.bonus);
+			playChapterCompleteCinematic("LVL10/L10END.ANM", 10, 0x4B, 5,
+				" ", accuracyText, bonus, nullptr, nullptr, 0, 10);
 			return !_vm->shouldQuit();
 		}
 
@@ -1366,8 +1423,10 @@ bool InsaneRebel1::runLevel11() {
 		}
 
 		if (_health >= 0) {
-			playCinematic("LVL11/L11END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)11);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x60, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x5F, 0x0F, _tuning.bonus);
+			playChapterCompleteCinematic("LVL11/L11END.ANM", 11, 0x69, 5, " ", accuracyText, bonus);
 			return !_vm->shouldQuit();
 		}
 
@@ -1457,8 +1516,10 @@ bool InsaneRebel1::runLevel12() {
 			if (_health < 0)
 				break;
 
-			playCinematic("LVL12/L12END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)12);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x57, 0x46, _tuning.bonus);
+			playChapterCompleteCinematic("LVL12/L12END.ANM", 12, 0x69, 5, " ", accuracyText, bonus);
 			return !_vm->shouldQuit();
 		}
 
@@ -1536,8 +1597,10 @@ bool InsaneRebel1::runLevel13() {
 			return false;
 
 		if (_health >= 0) {
-			playCinematic("LVL13/L13END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)13);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x146, true);
+			const int bonus = calculateThresholdBonus(_killCount, 0x145, 0x14, _tuning.bonus);
+			playChapterCompleteCinematic("LVL13/L13END.ANM", 13, 0x69, 5, " ", accuracyText, bonus);
 			return !_vm->shouldQuit();
 		}
 
@@ -1647,8 +1710,8 @@ bool InsaneRebel1::runLevel14() {
 		}
 
 		if (_health >= 0) {
-			playCinematic("LVL14/L14END.ANM");
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)14);
+			playChapterCompleteCinematic("LVL14/L14END.ANM", 14, 0x69, 5,
+				nullptr, nullptr, 0, nullptr, nullptr, 0, 14);
 			return !_vm->shouldQuit();
 		}
 
@@ -1760,11 +1823,11 @@ bool InsaneRebel1::runLevel15() {
 			if (_killCount > 0x14)
 				targetBonus += (_killCount - 0x14) * _tuning.bonus;
 
-			beginLevel15SummaryOverlay(targetBonus);
-			playCinematic("LVL15/L15END1.ANM");
-			_level15SummaryActive = false;
-			_score += _tuning.levelPts + targetBonus + 10000;
-			_maxChapterUnlocked = MAX(_maxChapterUnlocked, (int16)15);
+			char accuracyText[80];
+			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, false);
+			playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
+				"Part I", accuracyText, targetBonus,
+				"Part II", "Torpedo on mark", 10000, 15);
 			return !_vm->shouldQuit();
 		}
 


Commit: 2b08e14c07cd682e0725633de60cbc2634253588
    https://github.com/scummvm/scummvm/commit/2b08e14c07cd682e0725633de60cbc2634253588
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00

Commit Message:
SCUMM: RA1: Add initial joystick support

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/metaengine.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index f4e779b0b2d..078bd883f7d 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -30,7 +30,8 @@ namespace Scumm {
 
 inline int16 applyRebel1AnalogDeadzone(int16 axisValue) {
 	const int deadZone = MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000;
-	return (ABS(axisValue) <= deadZone) ? 0 : axisValue;
+	const int axis = axisValue;
+	return (ABS(axis) <= deadZone) ? 0 : axisValue;
 }
 
 inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue, int16 axisMax) {
@@ -746,6 +747,21 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		(_vm->getActionState(kScummActionInsaneUp) ? 1 : 0) -
 		(_vm->getActionState(kScummActionInsaneDown) ? 1 : 0);
 
+	if (joyX != 0 || joyY != 0) {
+		_activeInputSource = kInputSourceJoystickDigital;
+
+		if (usedJoystick)
+			*usedJoystick = true;
+
+		inputX = joyX * 127;
+		inputY = joyY * 127;
+
+		if (_optControlsYFlip)
+			inputY = -inputY;
+
+		return;
+	}
+
 	if (_activeInputSource == kInputSourceJoystickAnalog) {
 		if (usedJoystick)
 			*usedJoystick = true;
@@ -764,12 +780,12 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		return;
 	}
 
-	if (_activeInputSource == kInputSourceJoystickDigital || joyX != 0 || joyY != 0) {
+	if (_activeInputSource == kInputSourceJoystickDigital) {
 		if (usedJoystick)
 			*usedJoystick = true;
 
-		inputX = joyX * 127;
-		inputY = joyY * 127;
+		inputX = 0;
+		inputY = 0;
 
 		if (_optControlsYFlip)
 			inputY = -inputY;
@@ -901,9 +917,17 @@ void InsaneRebel1::updateShipPhysics() {
 	// not raw mouse coordinates. Reuse the same centered-axis law here.
 	int16 inputX = 0;
 	int16 inputY = 0;
-	preprocessMouseAxes(inputX, inputY);
+	bool usedJoystick = false;
+	preprocessMouseAxes(inputX, inputY, &usedJoystick);
+	const int16 rawInputX = inputX;
+	const int16 rawInputY = inputY;
 	inputX = CLIP<int16>(inputX, -127, 127);
 	inputY = CLIP<int16>(inputY, -127, 127);
+	const char *inputSourceName = "mouse";
+	if (_activeInputSource == kInputSourceJoystickAnalog)
+		inputSourceName = "joystick-analog";
+	else if (_activeInputSource == kInputSourceJoystickDigital)
+		inputSourceName = "joystick-dpad";
 
 	// --- Step 2: Roll accumulator (_74CA) ---
 	// Normal mode: accumulate; mode 0x10: snap to input
@@ -992,6 +1016,18 @@ void InsaneRebel1::updateShipPhysics() {
 	if (_shipBank.numSprites > 0)
 		_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
 
+	debug(1, "RA1 ship input: frame=%d source=%s usedJoystick=%d raw=(%d,%d) clipped=(%d,%d) storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d) roll=%d lift=%d pos=(%d,%d) view=(%d,%d) dir=%d level=%d mode=%d opcode=0x%X",
+		_gameCounter, inputSourceName, usedJoystick,
+		rawInputX, rawInputY, inputX, inputY,
+		_joystickAxisX, _joystickAxisY,
+		_vm->getActionState(kScummActionInsaneLeft),
+		_vm->getActionState(kScummActionInsaneRight),
+		_vm->getActionState(kScummActionInsaneUp),
+		_vm->getActionState(kScummActionInsaneDown),
+		_rollAccum, _liftSmooth,
+		_shipPosX, _shipPosY, _perspectiveX, _perspectiveY, _shipDirIndex,
+		_currentLevel, _flyControlMode, _activeGameOpcode);
+
 	// --- Step 9: Damage/event bit synthesis + damage processing ---
 	// RA1 FUN_1B297-style latches from GAME opcodes:
 	//   0x5D latch 0xFFFF -> bit 0x40 (obstacle/contact)
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index eb9e7f17472..fd46706176d 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -35,6 +35,161 @@ namespace Scumm {
 const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
 const int kRA1LevelSelectRowsPerCol = 8;
 const int kRA1NumLevels = 15;
+const int kRA1MenuAxisThreshold = Common::JOYAXIS_MAX / 2;
+const uint32 kRA1JoystickAxisEscGuardMs = 250;
+
+static int getRebel1MenuAxisDirection(int16 axisValue) {
+	if (axisValue >= kRA1MenuAxisThreshold)
+		return 1;
+	if (axisValue <= -kRA1MenuAxisThreshold)
+		return -1;
+	return 0;
+}
+
+static void setRebel1Volume(ScummEngine_v7 *vm, int &volume, int delta) {
+	volume = CLIP<int>(volume + delta, 0, 127);
+	vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
+		(volume * Audio::Mixer::kMaxChannelVolume) / 127);
+	ConfMan.setInt("music_volume", (volume * 256) / 127);
+	ConfMan.setInt("sfx_volume", (volume * 256) / 127);
+	ConfMan.setInt("speech_volume", (volume * 256) / 127);
+}
+
+static const char *getRebel1ActionName(Common::CustomEventType customType) {
+	switch (customType) {
+	case kScummActionInsaneUp:
+		return "up";
+	case kScummActionInsaneDown:
+		return "down";
+	case kScummActionInsaneLeft:
+		return "left";
+	case kScummActionInsaneRight:
+		return "right";
+	case kScummActionInsaneAttack:
+		return "attack";
+	case kScummActionInsaneSwitch:
+		return "switch";
+	default:
+		return "other";
+	}
+}
+
+static const char *getRebel1BackendAxisName(Common::CustomEventType customType) {
+	switch (customType) {
+	case kScummBackendActionRebel1AxisUp:
+		return "stick-up";
+	case kScummBackendActionRebel1AxisDown:
+		return "stick-down";
+	case kScummBackendActionRebel1AxisLeft:
+		return "stick-left";
+	case kScummBackendActionRebel1AxisRight:
+		return "stick-right";
+	default:
+		return "other-axis";
+	}
+}
+
+static int16 normalizeRebel1MappedAxisPosition(int16 axisPosition) {
+	// Custom backend axis events are half-axis magnitudes. Keymapper takes
+	// ABS(rawPosition), but int16 -32768 cannot be represented as +32768.
+	return axisPosition == Common::JOYAXIS_MIN ? Common::JOYAXIS_MAX : axisPosition;
+}
+
+bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
+	if (!_menuActive || _highScoresActive)
+		return false;
+
+	if (_levelSelectActive) {
+		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
+		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
+
+		switch (action) {
+		case kScummActionInsaneUp:
+			row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
+			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+			return true;
+		case kScummActionInsaneDown:
+			row = (row + 1) % kRA1LevelSelectRowsPerCol;
+			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
+			return true;
+		case kScummActionInsaneLeft:
+			if (col > 0)
+				_levelSelectSel -= kRA1LevelSelectRowsPerCol;
+			return true;
+		case kScummActionInsaneRight:
+			if (col < 1)
+				_levelSelectSel += kRA1LevelSelectRowsPerCol;
+			return true;
+		case kScummActionInsaneAttack:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	if (_optionsActive) {
+		switch (action) {
+		case kScummActionInsaneUp:
+			_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
+			return true;
+		case kScummActionInsaneDown:
+			_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
+			return true;
+		case kScummActionInsaneLeft:
+			if (_optionsSel == 6)
+				setRebel1Volume(_vm, _optVolume, -5);
+			return true;
+		case kScummActionInsaneRight:
+			if (_optionsSel == 6)
+				setRebel1Volume(_vm, _optVolume, 5);
+			return true;
+		case kScummActionInsaneAttack:
+			_menuConfirmed = true;
+			_vm->_smushVideoShouldFinish = true;
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	switch (action) {
+	case kScummActionInsaneUp:
+		_menuSelection = (_menuSelection + 4) % 5;
+		return true;
+	case kScummActionInsaneDown:
+		_menuSelection = (_menuSelection + 1) % 5;
+		return true;
+	case kScummActionInsaneAttack:
+		_menuConfirmed = true;
+		_vm->_smushVideoShouldFinish = true;
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool InsaneRebel1::handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY) {
+	if (!_menuActive || _highScoresActive)
+		return false;
+
+	// RA1 maps stick axes to backend axis events for analog gameplay.
+	// Menus still need edge-triggered digital navigation from those same axes.
+	// Match the original raw-input convention: positive Y is stick-down in
+	// menus, and the Y-flip option reverses that interpretation.
+	const int oldX = getRebel1MenuAxisDirection(oldAxisX);
+	const int oldY = getRebel1MenuAxisDirection(oldAxisY);
+	const int newX = getRebel1MenuAxisDirection(_joystickAxisX);
+	const int newY = getRebel1MenuAxisDirection(_joystickAxisY);
+
+	if (newY != oldY && newY != 0)
+		return handleControllerMenuAction((newY > 0) != _optControlsYFlip ? kScummActionInsaneDown : kScummActionInsaneUp);
+	if (newX != oldX && newX != 0)
+		return handleControllerMenuAction(newX > 0 ? kScummActionInsaneRight : kScummActionInsaneLeft);
+
+	return false;
+}
 
 bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	// Global ScummVM dialogs pause the engine while their modal event loop runs.
@@ -47,29 +202,84 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		_activeInputSource = kInputSourceMouse;
 	}
 
+	if (event.type == Common::EVENT_JOYAXIS_MOTION) {
+		_lastJoystickAxisEventTime = _vm->_system->getMillis();
+		debug(1, "RA1 input raw-joy-axis: axis=%d pos=%d menu=%d gameplay=%d storedAxis=(%d,%d)",
+			event.joystick.axis, event.joystick.position,
+			_menuActive, _interactiveVideoActive && !_menuActive,
+			_joystickAxisX, _joystickAxisY);
+	}
+
+	if (event.type == Common::EVENT_JOYBUTTON_DOWN || event.type == Common::EVENT_JOYBUTTON_UP) {
+		debug(1, "RA1 input raw-joy-button: button=%d pressed=%d menu=%d gameplay=%d storedAxis=(%d,%d)",
+			event.joystick.button, event.type == Common::EVENT_JOYBUTTON_DOWN,
+			_menuActive, _interactiveVideoActive && !_menuActive,
+			_joystickAxisX, _joystickAxisY);
+	}
+
 	if (event.type == Common::EVENT_CUSTOM_BACKEND_ACTION_AXIS) {
 		_activeInputSource = kInputSourceJoystickAnalog;
+		_lastJoystickAxisEventTime = _vm->_system->getMillis();
+		const int16 oldAxisX = _joystickAxisX;
+		const int16 oldAxisY = _joystickAxisY;
+		const int16 axisPosition = normalizeRebel1MappedAxisPosition(event.joystick.position);
 
 		switch (event.customType) {
 		case kScummBackendActionRebel1AxisUp:
-			if (event.joystick.position == 0 && _joystickAxisY < 0)
+			if (event.joystick.position == 0 && _joystickAxisY > 0) {
+				debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+					getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+				return true;
+			}
+			_joystickAxisY = -axisPosition;
+			debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+				getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+				oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+				_menuActive, _interactiveVideoActive && !_menuActive);
+			if (handleControllerMenuAxis(oldAxisX, oldAxisY))
 				return true;
-			_joystickAxisY = event.joystick.position;
 			return true;
 		case kScummBackendActionRebel1AxisDown:
-			if (event.joystick.position == 0 && _joystickAxisY > 0)
+			if (event.joystick.position == 0 && _joystickAxisY < 0) {
+				debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+					getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+				return true;
+			}
+			_joystickAxisY = axisPosition;
+			debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+				getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+				oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+				_menuActive, _interactiveVideoActive && !_menuActive);
+			if (handleControllerMenuAxis(oldAxisX, oldAxisY))
 				return true;
-			_joystickAxisY = -event.joystick.position;
 			return true;
 		case kScummBackendActionRebel1AxisLeft:
-			if (event.joystick.position == 0 && _joystickAxisX > 0)
+			if (event.joystick.position == 0 && _joystickAxisX > 0) {
+				debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+					getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+				return true;
+			}
+			_joystickAxisX = -axisPosition;
+			debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+				getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+				oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+				_menuActive, _interactiveVideoActive && !_menuActive);
+			if (handleControllerMenuAxis(oldAxisX, oldAxisY))
 				return true;
-			_joystickAxisX = -event.joystick.position;
 			return true;
 		case kScummBackendActionRebel1AxisRight:
-			if (event.joystick.position == 0 && _joystickAxisX < 0)
+			if (event.joystick.position == 0 && _joystickAxisX < 0) {
+				debug(1, "RA1 input mapped-axis ignored-reset: %s pos=0 current=(%d,%d)",
+					getRebel1BackendAxisName(event.customType), _joystickAxisX, _joystickAxisY);
+				return true;
+			}
+			_joystickAxisX = axisPosition;
+			debug(1, "RA1 input mapped-axis: %s pos=%d rawPos=%d old=(%d,%d) new=(%d,%d) menu=%d gameplay=%d",
+				getRebel1BackendAxisName(event.customType), axisPosition, event.joystick.position,
+				oldAxisX, oldAxisY, _joystickAxisX, _joystickAxisY,
+				_menuActive, _interactiveVideoActive && !_menuActive);
+			if (handleControllerMenuAxis(oldAxisX, oldAxisY))
 				return true;
-			_joystickAxisX = event.joystick.position;
 			return true;
 		default:
 			break;
@@ -80,6 +290,15 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END) {
 		const bool pressed = (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START);
 
+		debug(1, "RA1 input mapped-action: action=%s custom=%u pressed=%d menu=%d gameplay=%d storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d)",
+			getRebel1ActionName(event.customType), event.customType, pressed,
+			_menuActive, _interactiveVideoActive && !_menuActive,
+			_joystickAxisX, _joystickAxisY,
+			_vm->getActionState(kScummActionInsaneLeft),
+			_vm->getActionState(kScummActionInsaneRight),
+			_vm->getActionState(kScummActionInsaneUp),
+			_vm->getActionState(kScummActionInsaneDown));
+
 		if (pressed &&
 			(event.customType == kScummActionInsaneUp ||
 			 event.customType == kScummActionInsaneDown ||
@@ -93,91 +312,8 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 			return true;
 		}
 
-		if (_menuActive && !_highScoresActive && pressed) {
-			if (_levelSelectActive) {
-				int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
-				int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
-
-				switch (event.customType) {
-				case kScummActionInsaneUp:
-					row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
-					_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
-					return true;
-				case kScummActionInsaneDown:
-					row = (row + 1) % kRA1LevelSelectRowsPerCol;
-					_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
-					return true;
-				case kScummActionInsaneLeft:
-					if (col > 0)
-						_levelSelectSel -= kRA1LevelSelectRowsPerCol;
-					return true;
-				case kScummActionInsaneRight:
-					if (col < 1)
-						_levelSelectSel += kRA1LevelSelectRowsPerCol;
-					return true;
-				case kScummActionInsaneAttack:
-					_menuConfirmed = true;
-					_vm->_smushVideoShouldFinish = true;
-					return true;
-				default:
-					break;
-				}
-			}
-
-			if (_optionsActive) {
-				switch (event.customType) {
-				case kScummActionInsaneUp:
-					_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
-					return true;
-				case kScummActionInsaneDown:
-					_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
-					return true;
-				case kScummActionInsaneLeft:
-					if (_optionsSel == 6) {
-						_optVolume = MAX(0, _optVolume - 5);
-						_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
-							(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
-						ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
-						ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
-						ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
-					}
-					return true;
-				case kScummActionInsaneRight:
-					if (_optionsSel == 6) {
-						_optVolume = MIN(127, _optVolume + 5);
-						_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
-							(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
-						ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
-						ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
-						ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
-					}
-					return true;
-				case kScummActionInsaneAttack:
-					_menuConfirmed = true;
-					_vm->_smushVideoShouldFinish = true;
-					return true;
-				default:
-					break;
-				}
-			}
-
-			if (!_optionsActive && !_levelSelectActive) {
-				switch (event.customType) {
-				case kScummActionInsaneUp:
-					_menuSelection = (_menuSelection + 4) % 5;
-					return true;
-				case kScummActionInsaneDown:
-					_menuSelection = (_menuSelection + 1) % 5;
-					return true;
-				case kScummActionInsaneAttack:
-					_menuConfirmed = true;
-					_vm->_smushVideoShouldFinish = true;
-					return true;
-				default:
-					break;
-				}
-			}
-		}
+		if (pressed && handleControllerMenuAction((ScummAction)event.customType))
+			return true;
 
 		if (_interactiveVideoActive && !_menuActive && event.customType == kScummActionInsaneAttack) {
 			_playerFired = pressed;
@@ -238,26 +374,14 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		case Common::KEYCODE_LEFT:
 		case Common::KEYCODE_a:
 			// Volume down when on volume row (row 6)
-			if (_optionsSel == 6) {
-				_optVolume = MAX(0, _optVolume - 5);
-				_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
-					(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
-				ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
-				ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
-				ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
-			}
+			if (_optionsSel == 6)
+				setRebel1Volume(_vm, _optVolume, -5);
 			return true;
 		case Common::KEYCODE_RIGHT:
 		case Common::KEYCODE_d:
 			// Volume up when on volume row (row 6)
-			if (_optionsSel == 6) {
-				_optVolume = MIN(127, _optVolume + 5);
-				_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType,
-					(_optVolume * Audio::Mixer::kMaxChannelVolume) / 127);
-				ConfMan.setInt("music_volume", (_optVolume * 256) / 127);
-				ConfMan.setInt("sfx_volume", (_optVolume * 256) / 127);
-				ConfMan.setInt("speech_volume", (_optVolume * 256) / 127);
-			}
+			if (_optionsSel == 6)
+				setRebel1Volume(_vm, _optVolume, 5);
 			return true;
 		case Common::KEYCODE_RETURN:
 		case Common::KEYCODE_KP_ENTER:
@@ -329,9 +453,30 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		return true;
 	}
 
+	if (event.type == Common::EVENT_MAINMENU && _interactiveVideoActive && !_menuActive) {
+		const uint32 now = _vm->_system->getMillis();
+		const uint32 elapsedSinceAxis = _lastJoystickAxisEventTime ? now - _lastJoystickAxisEventTime : 0xffffffffu;
+		debug(1, "RA1 input mainmenu-event: gameplay=1 elapsedSinceAxis=%u storedAxis=(%d,%d)",
+			elapsedSinceAxis, _joystickAxisX, _joystickAxisY);
+		if (elapsedSinceAxis <= kRA1JoystickAxisEscGuardMs) {
+			debug(1, "RA1 input ignored mainmenu event after recent joystick axis movement (%u ms)", elapsedSinceAxis);
+			return true;
+		}
+	}
+
 	if (event.type == Common::EVENT_KEYDOWN && _player) {
 		if (_interactiveVideoActive && !_menuActive &&
 			event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+			const uint32 now = _vm->_system->getMillis();
+			const uint32 elapsedSinceAxis = _lastJoystickAxisEventTime ? now - _lastJoystickAxisEventTime : 0xffffffffu;
+			debug(1, "RA1 input keydown-escape: gameplay=1 ascii=%d flags=0x%x repeat=%d elapsedSinceAxis=%u storedAxis=(%d,%d)",
+				event.kbd.ascii, event.kbd.flags, event.kbdRepeat,
+				elapsedSinceAxis, _joystickAxisX, _joystickAxisY);
+			if (elapsedSinceAxis <= kRA1JoystickAxisEscGuardMs) {
+				debug(1, "RA1 input ignored ESC after recent joystick axis movement (%u ms)", elapsedSinceAxis);
+				return true;
+			}
+
 			debug("Rebel1: ESC pressed during gameplay - opening ScummVM menu");
 			const bool wasPaused = _player->_paused;
 			if (!wasPaused)
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index bec456bd16e..28d4efc4e49 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -263,6 +263,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_mouseRecentering = false;
 	_joystickAxisX = 0;
 	_joystickAxisY = 0;
+	_lastJoystickAxisEventTime = 0;
 	_level2JoystickFilteredX = 0;
 	_level2JoystickFilteredY = 0;
 	_activeInputSource = kInputSourceMouse;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 77855815881..06803f81abb 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -281,6 +281,7 @@ private:
 	bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
 	int16 _joystickAxisX;   // Rebel-specific left-stick X captured from keymapper axis events
 	int16 _joystickAxisY;   // Rebel-specific left-stick Y captured from keymapper axis events
+	uint32 _lastJoystickAxisEventTime;
 	int16 _level2JoystickFilteredX; // Smoothed Level 2 analog X input
 	int16 _level2JoystickFilteredY; // Smoothed Level 2 analog Y input
 	enum InputSource {
@@ -443,6 +444,8 @@ private:
 
 	// Main menu / options state
 	void runOptionsMenu();
+	bool handleControllerMenuAction(ScummAction action);
+	bool handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY);
 	bool _menuActive;
 	bool _menuConfirmed;
 	int _menuSelection; // 0..4 maps to return values 1..5
diff --git a/engines/scumm/metaengine.cpp b/engines/scumm/metaengine.cpp
index 4cc3fa5cb8b..5c8c0025a02 100644
--- a/engines/scumm/metaengine.cpp
+++ b/engines/scumm/metaengine.cpp
@@ -1128,21 +1128,25 @@ Common::KeymapArray ScummMetaEngine::initKeymaps(const char *target) const {
 		act = new Action("RA1STICKUP", _("Stick up"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisUp);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_Y-");
+		act->addDefaultInputMapping("JOY_RIGHT_STICK_Y-");
 		rebel1Keymap->addAction(act);
 
 		act = new Action("RA1STICKDOWN", _("Stick down"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisDown);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_Y+");
+		act->addDefaultInputMapping("JOY_RIGHT_STICK_Y+");
 		rebel1Keymap->addAction(act);
 
 		act = new Action("RA1STICKLEFT", _("Stick left"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisLeft);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_X-");
+		act->addDefaultInputMapping("JOY_RIGHT_STICK_X-");
 		rebel1Keymap->addAction(act);
 
 		act = new Action("RA1STICKRIGHT", _("Stick right"));
 		act->setCustomBackendActionAxisEvent(kScummBackendActionRebel1AxisRight);
 		act->addDefaultInputMapping("JOY_LEFT_STICK_X+");
+		act->addDefaultInputMapping("JOY_RIGHT_STICK_X+");
 		rebel1Keymap->addAction(act);
 
 		act = new Action("RA1FIRE", _("Fire / select"));


Commit: d0cb0eea1f2971419806453a68a58b41d586d08d
    https://github.com/scummvm/scummvm/commit/d0cb0eea1f2971419806453a68a58b41d586d08d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:05+02:00

Commit Message:
SCUMM: RA1: Improve menu code

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


diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index fd46706176d..22e5d5e0fcc 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -36,6 +36,12 @@ const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
 const int kRA1LevelSelectRowsPerCol = 8;
 const int kRA1NumLevels = 15;
 const int kRA1MenuAxisThreshold = Common::JOYAXIS_MAX / 2;
+const int kRA1MenuLogicalWidth = 0x140;
+const int kRA1MenuFrameX = 0x32;
+const int kRA1MenuFrameW = 0xdc;
+const int kRA1MenuFrameH = 0x0f;
+const int kRA1MenuRowH = 0x0f;
+const byte kRA1MenuFrameColor = 0xdf;
 const uint32 kRA1JoystickAxisEscGuardMs = 250;
 
 static int getRebel1MenuAxisDirection(int16 axisValue) {
@@ -95,6 +101,29 @@ static int16 normalizeRebel1MappedAxisPosition(int16 axisPosition) {
 	return axisPosition == Common::JOYAXIS_MIN ? Common::JOYAXIS_MAX : axisPosition;
 }
 
+static int getRebel1MenuCenteredX(int textWidth) {
+	return (kRA1MenuLogicalWidth - textWidth) / 2;
+}
+
+static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int x, int y, int w) {
+	if (!dst || width <= 0 || height <= 0)
+		return;
+
+	const int leftX = CLIP(x, 0, width - 1);
+	const int rightX = CLIP(x + w - 1, 0, width - 1);
+	const int topY = CLIP(y, 0, height - 1);
+	const int bottomY = CLIP(y + kRA1MenuFrameH - 1, 0, height - 1);
+
+	for (int px = leftX; px <= rightX; px++) {
+		dst[topY * pitch + px] = kRA1MenuFrameColor;
+		dst[bottomY * pitch + px] = kRA1MenuFrameColor;
+	}
+	for (int py = topY; py <= bottomY; py++) {
+		dst[py * pitch + leftX] = kRA1MenuFrameColor;
+		dst[py * pitch + rightX] = kRA1MenuFrameColor;
+	}
+}
+
 bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 	if (!_menuActive || _highScoresActive)
 		return false;
@@ -390,7 +419,7 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 			_vm->_smushVideoShouldFinish = true;
 			return true;
 		case Common::KEYCODE_ESCAPE:
-			_optionsSel = kOptionsItemCount - 1;  // Back
+			_optionsSel = 0;
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
@@ -532,7 +561,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		// Original renders over O1SCORE.ANM. Title appears after frame 20,
 		// entries fade in one per frame. We show all immediately.
 		const int titleW = getTalkTextWidth("TOP PILOTS");
-		drawTalkText((width - titleW) / 2, 10, "TOP PILOTS");
+		drawTalkText(getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
 
 		for (int i = 0; i < kHighScoreCount; i++) {
 			const int y = 25 + i * 14;
@@ -553,8 +582,8 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		// --- Options submenu (matching original RunGameOptionsMenu) ---
 		const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
 
-		const int titleW = getTalkTextWidth("GAME OPTIONS");
-		drawTalkText((width - titleW) / 2, 30, "GAME OPTIONS");
+		const int titleW = getTitleTextWidth("GAME OPTIONS");
+		drawTitleText(getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
 
 		// Build dynamic option strings for each row
 		char diffLine[64], volLine[64];
@@ -563,49 +592,32 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 
 		const char *optItems[kOptionsItemCount] = {
 			"EXIT MENU",
+			_optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
 			_optMusicEnabled  ? "MUSIC IS ON"             : "MUSIC IS OFF",
 			_optSfxEnabled    ? "SFX AND VOICE ARE ON"    : "SFX AND VOICE ARE OFF",
 			_optTextEnabled   ? "DIALOGUE TEXT IS ON"      : "DIALOGUE TEXT IS OFF",
 			_optControlsYFlip ? "CONTROLS ARE Y-FLIPPED"  : "CONTROLS ARE NORMAL",
-			_turbulenceEnabled ? "TURBULENCE IS ON"        : "TURBULENCE IS OFF",
 			volLine,
-			diffLine,
-			"BACK"
+			diffLine
 		};
 
-		const int menuY = 44;
-		const int rowH = 14;
-
 		for (int i = 0; i < kOptionsItemCount; i++) {
 			const int textW = getTalkTextWidth(optItems[i]);
-			const int textX = (width - textW) / 2;
-			const int y = menuY + i * rowH;
-			drawTalkText(textX, y + 1, optItems[i]);
-
-			if (i == _optionsSel) {
-				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-				int bracketWidth = textW + 12;
-				int leftX = CLIP(textX - 6, 0, width - 1);
-				int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
-				int topY = CLIP(y - 1, 0, height - 1);
-				int bottomY = CLIP(y + rowH - 2, 0, height - 1);
-				for (int x = leftX; x <= rightX; x++) {
-					dst[topY * pitch + x] = highlightColor;
-					dst[bottomY * pitch + x] = highlightColor;
-				}
-				for (int py = topY; py <= bottomY; py++) {
-					dst[py * pitch + leftX] = highlightColor;
-					dst[py * pitch + rightX] = highlightColor;
-				}
-			}
+			const int textX = getRebel1MenuCenteredX(textW);
+			const int y = 0x2d + i * kRA1MenuRowH;
+			drawTalkText(textX, y, optItems[i]);
+
+			if (i == _optionsSel)
+				drawRebel1MenuFrame(dst, pitch, width, height,
+					kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x1d, kRA1MenuFrameW);
 		}
 		return;
 	}
 
 	if (_levelSelectActive) {
-		// --- Level select submenu (two-column layout) ---
-		const int titleW = getTalkTextWidth("LEVEL SELECT");
-		drawTalkText((width - titleW) / 2, 30, "LEVEL SELECT");
+		// --- ScummVM level select submenu, styled like the original frontend menus ---
+		const int titleW = getTitleTextWidth("LEVEL SELECT");
+		drawTitleText(getRebel1MenuCenteredX(titleW), 15, "LEVEL SELECT");
 
 		const char *kLevelItems[kRA1LevelSelectItemCount] = {
 			" 1 TRAINING",
@@ -626,36 +638,24 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 			"BACK"
 		};
 
-		const int menuY = 50;
-		const int rowH = 14;
-		const int leftColX = 8;
-		const int rightColX = width / 2 + 4;
+		const int menuY = 0x2d;
+		const int leftFrameX = 20;
+		const int rightFrameX = 170;
+		const int columnW = 130;
 
 		for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
 			const int col = i / kRA1LevelSelectRowsPerCol;
 			const int row = i % kRA1LevelSelectRowsPerCol;
-			const int textX = (col == 0) ? leftColX : rightColX;
-			const int y = menuY + row * rowH;
+			const int frameX = (col == 0) ? leftFrameX : rightFrameX;
+			const int y = menuY + row * kRA1MenuRowH;
 			const int textW = getTalkTextWidth(kLevelItems[i]);
+			const int textX = frameX + (columnW - textW) / 2;
 
-			drawTalkText(textX, y + 1, kLevelItems[i]);
-
-			if (i == _levelSelectSel) {
-				byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-				int bracketWidth = textW + 12;
-				int leftX = CLIP(textX - 6, 0, width - 1);
-				int rightX = CLIP(leftX + bracketWidth, 0, width - 1);
-				int topY = CLIP(y - 1, 0, height - 1);
-				int bottomY = CLIP(y + rowH - 2, 0, height - 1);
-				for (int x = leftX; x <= rightX; x++) {
-					dst[topY * pitch + x] = highlightColor;
-					dst[bottomY * pitch + x] = highlightColor;
-				}
-				for (int py = topY; py <= bottomY; py++) {
-					dst[py * pitch + leftX] = highlightColor;
-					dst[py * pitch + rightX] = highlightColor;
-				}
-			}
+			drawTalkText(textX, y, kLevelItems[i]);
+
+			if (i == _levelSelectSel)
+				drawRebel1MenuFrame(dst, pitch, width, height,
+					frameX, row * kRA1MenuRowH + 0x2c, columnW);
 		}
 		return;
 	}
@@ -671,52 +671,20 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 
 	// Center title
 	const int titleW = getTitleTextWidth("MAIN MENU");
-	const int titleX = (width - titleW) / 2;
-	drawTitleText(titleX, 36, "MAIN MENU");
+	const int titleX = getRebel1MenuCenteredX(titleW);
+	drawTitleText(titleX, 30, "MAIN MENU");
 
 	// Draw menu items centered horizontally
-	const int menuY = 60;
-	const int rowH = 16;
-
 	for (int i = 0; i < 5; i++) {
 		const int textW = getTalkTextWidth(kMenuItems[i]);
-		const int textX = (width - textW) / 2;
-		const int y = menuY + i * rowH;
-
-		drawTalkText(textX, y + 1, kMenuItems[i]);
-
-		// Selection highlight box — flashing border (FUN_004292d0 pattern from RA2)
-		if (i == _menuSelection) {
-			// Flash between two palette colors every 8 frames
-			byte highlightColor = ((_menuFrameCounter / 8) & 1) ? 248 : 240;
-
-			int bracketWidth = textW + 12;
-			int bracketHeight = rowH;
-			int leftX = textX - 6;
-			int rightX = leftX + bracketWidth;
-			int topY = y - 1;
-			int bottomY = y + bracketHeight - 2;
-
-			// Clamp
-			if (leftX < 0) leftX = 0;
-			if (rightX >= width) rightX = width - 1;
-			if (topY < 0) topY = 0;
-			if (bottomY >= height) bottomY = height - 1;
-
-			// Draw rectangle border (4 lines)
-			for (int x = leftX; x <= rightX && x < width; x++) {
-				if (topY >= 0 && topY < height)
-					dst[topY * pitch + x] = highlightColor;
-				if (bottomY >= 0 && bottomY < height)
-					dst[bottomY * pitch + x] = highlightColor;
-			}
-			for (int py = topY; py <= bottomY && py < height; py++) {
-				if (leftX >= 0 && leftX < width)
-					dst[py * pitch + leftX] = highlightColor;
-				if (rightX >= 0 && rightX < width)
-					dst[py * pitch + rightX] = highlightColor;
-			}
-		}
+		const int textX = getRebel1MenuCenteredX(textW);
+		const int y = 0x3c + i * kRA1MenuRowH;
+
+		drawTalkText(textX, y, kMenuItems[i]);
+
+		if (i == _menuSelection)
+			drawRebel1MenuFrame(dst, pitch, width, height,
+				kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x2c, kRA1MenuFrameW);
 	}
 }
 
@@ -759,37 +727,34 @@ void InsaneRebel1::runOptionsMenu() {
 
 		if (_menuConfirmed) {
 			switch (_optionsSel) {
-			case 0: // EXIT MENU (same as BACK)
+			case 0: // EXIT MENU
 				_optionsActive = false;
 				return;
-			case 1: // Toggle music
+			case 1: // Toggle Rookie One gender
+				_optRookieOneFemale = !_optRookieOneFemale;
+				break;
+			case 2: // Toggle music
 				_optMusicEnabled = !_optMusicEnabled;
 				_vm->_mixer->muteSoundType(Audio::Mixer::kMusicSoundType, !_optMusicEnabled);
 				break;
-			case 2: // Toggle SFX + Voice
+			case 3: // Toggle SFX + Voice
 				_optSfxEnabled = !_optSfxEnabled;
 				_vm->_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, !_optSfxEnabled);
 				_vm->_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, !_optSfxEnabled);
 				break;
-			case 3: // Toggle dialogue text
+			case 4: // Toggle dialogue text
 				_optTextEnabled = !_optTextEnabled;
 				ConfMan.setBool("subtitles", _optTextEnabled);
 				break;
-			case 4: // Toggle Y-flip controls
+			case 5: // Toggle Y-flip controls
 				_optControlsYFlip = !_optControlsYFlip;
 				break;
-			case 5: // Toggle turbulence
-				_turbulenceEnabled = !_turbulenceEnabled;
-				break;
 			case 6: // Volume — adjusted via left/right in notifyEvent
 				break;
 			case 7: // Cycle difficulty
 				_difficulty = (_difficulty + 1) % 3;
 				loadTuningForLevel(0);
 				break;
-			case 8: // BACK
-				_optionsActive = false;
-				return;
 			}
 		}
 	}
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 28d4efc4e49..2815119bd10 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -326,9 +326,10 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_levelSelectActive = false;
 	_levelSelectSel = 0;
 	_startLevel = 1;
-	_turbulenceEnabled = false;
+	_turbulenceEnabled = true;
 
 	// Options — read initial state from ScummVM mixer
+	_optRookieOneFemale = false;
 	_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
 	_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
 	_optTextEnabled = true;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 06803f81abb..7a5c1a7cbc3 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -452,14 +452,15 @@ private:
 	int _menuFrameCounter;
 
 	// Options submenu state — RunGameOptionsMenu (0x14B42)
-	static const int kOptionsItemCount = 9; // 8 options + BACK
+	static const int kOptionsItemCount = 8;
 	bool _optionsActive;     // True when showing options instead of main menu
-	int _optionsSel;         // 0..8 selected option row
+	int _optionsSel;         // 0..7 selected option row
 	bool _levelSelectActive; // True when showing level-select submenu
 	int _levelSelectSel;     // 0=Level1 ... N-1=Back
 	int _startLevel;         // 1-based start level for "Start New Game"
 
 	// Per-option state (matching original RunGameOptionsMenu globals)
+	bool _optRookieOneFemale; // DAT_22c3: Rookie One gender
 	bool _optMusicEnabled;    // DAT_22b7: music on/off
 	bool _optSfxEnabled;      // DAT_22b8: sfx+voice on/off
 	bool _optTextEnabled;     // DAT_22b9: dialogue text on/off


Commit: b96a94011eaaf281921030f28576d22a2fc69e44
    https://github.com/scummvm/scummvm/commit/b96a94011eaaf281921030f28576d22a2fc69e44
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:06+02:00

Commit Message:
SCUMM: RA1: Allow switching back to original input style

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/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 078bd883f7d..795917c5231 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -47,6 +47,12 @@ inline int16 smoothRebel1Op0BAnalogInput(int16 inputValue, int16 &filteredValue,
 
 const int16 kRA1Op09AimXScale[5] = { 0, 44, 88, 128, 165 };
 const int16 kRA1Op09AimYScale[5] = { 256, 252, 240, 221, 196 };
+const int kRA1DosMouseCenterX = 0x140;
+const int kRA1DosMouseCenterY = 100;
+const int kRA1DosMouseSafeLeft = 0x0AA;
+const int kRA1DosMouseSafeRight = 0x1D6;
+const int kRA1DosMouseSafeTop = 0x32;
+const int kRA1DosMouseSafeBottom = 0x96;
 
 // Level 15 final approach 0x5D damage/event codes consumed by
 // RunLevel1GameLoop. The latch stores the raw GAME parameter; no translation is
@@ -729,8 +735,9 @@ void InsaneRebel1::updateFlightVariantCursor() {
 // preprocessMouseAxes — FUN_231BE (0x231BE) centered-axis output law, adapted to
 // ScummVM's absolute 320x200 mouse space.
 // Preserve the DOS bias/offset persistence and one-frame jump latch from
-// FUN_231BE, but avoid hard recentring the host mouse into the DOS safe window.
-// The actual frame-averaging behavior stays untouched.
+// FUN_231BE. Original-input mode also emulates FUN_23115's DOS mouse recenter;
+// gameplay hides and locks the cursor, so this follows the original without
+// exposing a visible pointer jump.
 void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick) {
 	if (usedJoystick)
 		*usedJoystick = false;
@@ -759,6 +766,7 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		if (_optControlsYFlip)
 			inputY = -inputY;
 
+		_mouseVirtualValid = false;
 		return;
 	}
 
@@ -777,6 +785,7 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		if (_optControlsYFlip)
 			inputY = -inputY;
 
+		_mouseVirtualValid = false;
 		return;
 	}
 
@@ -790,19 +799,60 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		if (_optControlsYFlip)
 			inputY = -inputY;
 
+		_mouseVirtualValid = false;
 		return;
 	}
 
 	int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
 	int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
-	const int16 rawX = (int16)(logicalX << 1);
-	const int16 rawY = logicalY;
-	const int16 deltaX = (int16)(logicalX - kRA1CenterX);
-	const int16 deltaY = (int16)(logicalY - kRA1CenterY);
-	const int16 normX = (int16)(((int32)deltaX * 127) / 160);
-	const int16 normY = (int16)(((int32)deltaY * 127) / 100);
-	int16 biasX = (int16)((rawX + _mouseOffsetX - 0x140) >> 2);
-	int16 biasY = (int16)((rawY + _mouseOffsetY - 100) >> 1);
+	int16 rawX = (int16)(logicalX << 1);
+	int16 rawY = logicalY;
+
+	if (!_optEnhancedControls) {
+		if (!_mouseVirtualValid) {
+			_mouseVirtualRawX = rawX;
+			_mouseVirtualRawY = rawY;
+			_mouseVirtualValid = true;
+		} else {
+			_mouseVirtualRawX = (int16)CLIP<int>(
+				_mouseVirtualRawX + ((logicalX - _mouseVirtualPrevLogicalX) << 1),
+				-32768, 32767);
+			_mouseVirtualRawY = (int16)CLIP<int>(
+				_mouseVirtualRawY + (logicalY - _mouseVirtualPrevLogicalY),
+				-32768, 32767);
+		}
+		_mouseVirtualPrevLogicalX = logicalX;
+		_mouseVirtualPrevLogicalY = logicalY;
+
+		rawX = _mouseVirtualRawX;
+		rawY = _mouseVirtualRawY;
+
+		if (rawX < kRA1DosMouseSafeLeft || rawX > kRA1DosMouseSafeRight ||
+			rawY < kRA1DosMouseSafeTop || rawY > kRA1DosMouseSafeBottom) {
+			_mouseOffsetX = (int16)CLIP<int>(
+				(int)_mouseOffsetX + rawX - kRA1DosMouseCenterX, -32768, 32767);
+			_mouseOffsetY = (int16)CLIP<int>(
+				(int)_mouseOffsetY + rawY - kRA1DosMouseCenterY, -32768, 32767);
+			rawX = kRA1DosMouseCenterX;
+			rawY = kRA1DosMouseCenterY;
+			_mouseVirtualRawX = rawX;
+			_mouseVirtualRawY = rawY;
+			_mouseVirtualPrevLogicalX = kRA1CenterX;
+			_mouseVirtualPrevLogicalY = kRA1CenterY;
+			_vm->_mouse.x = kRA1CenterX;
+			_vm->_mouse.y = kRA1CenterY;
+			smush_warpMouse(kRA1CenterX, kRA1CenterY, -1);
+			debug(2, "RA1 original input virtual recenter: offset=(%d,%d) mouse=(%d,%d)",
+				_mouseOffsetX, _mouseOffsetY, logicalX, logicalY);
+		}
+	} else {
+		_mouseVirtualValid = false;
+	}
+
+	const int16 normX = (int16)(((int32)(logicalX - kRA1CenterX) * 127) / 160);
+	const int16 normY = (int16)(((int32)(logicalY - kRA1CenterY) * 127) / 100);
+	int16 biasX = (int16)((rawX + _mouseOffsetX - kRA1DosMouseCenterX) >> 2);
+	int16 biasY = (int16)((rawY + _mouseOffsetY - kRA1DosMouseCenterY) >> 1);
 
 	if (biasY < 0x65) {
 		const bool largeJump =
@@ -825,8 +875,11 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		_mouseBiasLatch = true;
 	}
 
-	const int16 scaledX = (int16)(normX + biasX);
-	const int16 scaledY = (int16)(normY + biasY);
+	// In original mouse mode, FUN_2309C centers the joystick contribution when
+	// joystick is disabled, so FUN_231BE's mouse output is just the bias term.
+	// Enhanced mode keeps ScummVM's pre-existing centered mouse axis plus bias.
+	const int16 scaledX = _optEnhancedControls ? (int16)(normX + biasX) : biasX;
+	const int16 scaledY = _optEnhancedControls ? (int16)(normY + biasY) : biasY;
 
 	_mouseBiasX = biasX;
 	_mouseBiasY = biasY;
@@ -941,7 +994,8 @@ void InsaneRebel1::updateShipPhysics() {
 
 	// --- Step 4: Position accumulator deltas ---
 	// X delta: drift + slide coupling - cross-coupling
-	int32 rng = _turbulenceEnabled ? (int32)_vm->_rnd.getRandomNumber(199) : 100;  // 0-199, centered at 100
+	const bool originalTurbulence = !_optEnhancedControls;
+	int32 rng = originalTurbulence ? (int32)_vm->_rnd.getRandomNumber(199) : 100;  // RandScaleByte(200), centered at 100
 	int32 crossTermX;
 	if (_liftSmooth < 0)
 		crossTermX = ((int32)_tuning.lift * _liftSmooth * _rollAccum) >> 11;
@@ -1016,8 +1070,9 @@ void InsaneRebel1::updateShipPhysics() {
 	if (_shipBank.numSprites > 0)
 		_shipDirIndex = CLIP<int16>((int16)(vComponent + hComponent), 0, _shipBank.numSprites - 1);
 
-	debug(1, "RA1 ship input: frame=%d source=%s usedJoystick=%d raw=(%d,%d) clipped=(%d,%d) storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d) roll=%d lift=%d pos=(%d,%d) view=(%d,%d) dir=%d level=%d mode=%d opcode=0x%X",
-		_gameCounter, inputSourceName, usedJoystick,
+	debug(1, "RA1 ship input: frame=%d source=%s controls=%s turbulence=%d usedJoystick=%d raw=(%d,%d) clipped=(%d,%d) storedAxis=(%d,%d) actionState(L,R,U,D)=(%d,%d,%d,%d) roll=%d lift=%d pos=(%d,%d) view=(%d,%d) dir=%d level=%d mode=%d opcode=0x%X",
+		_gameCounter, inputSourceName,
+		_optEnhancedControls ? "enhanced" : "original", originalTurbulence, usedJoystick,
 		rawInputX, rawInputY, inputX, inputY,
 		_joystickAxisX, _joystickAxisY,
 		_vm->getActionState(kScummActionInsaneLeft),
@@ -1256,15 +1311,16 @@ void InsaneRebel1::updateTurretPhysics() {
 		const int16 rawInputX = inputX;
 		const int16 rawInputY = inputY;
 
-		if (usedJoystick) {
+		if (usedJoystick && _optEnhancedControls) {
 			// First-person turret/cockpit stages are noticeably more sensitive on
 			// joystick than on mouse, so damp only the joystick-driven input here.
 			inputX /= 2;
 			inputY /= 2;
 		}
 
-		debug("RA1 turret input: source=%s mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d mode=%d opcode=0x%X",
+		debug("RA1 turret input: source=%s controls=%s mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d mode=%d opcode=0x%X",
 			usedJoystick ? "joystick-actions" : "mouse-path",
+			_optEnhancedControls ? "enhanced" : "original",
 			_vm->_mouse.x, _vm->_mouse.y,
 			_vm->getActionState(kScummActionInsaneLeft),
 			_vm->getActionState(kScummActionInsaneRight),
@@ -1355,9 +1411,9 @@ void InsaneRebel1::updateScreenFlashPalette() {
 // Ship position = averaged input + center offset.
 // Viewport = second history buffer for smooth camera scrolling.
 void InsaneRebel1::updateGameOp0BPhysics() {
-	// Control feel tweak: original uses full 10-sample average in FUN_1CDA7.
-	// We keep the same pipeline but average over fewer samples for responsiveness.
-	const int kGameOp0BSmoothWindow = 2;
+	// Original FUN_1CDA7 uses the full 10-sample history. Enhanced controls keep
+	// the same pipeline but average fewer samples for responsiveness.
+	const int gameOp0BSmoothWindow = _optEnhancedControls ? 2 : kInputHistorySize;
 
 	// RA1 FUN_1B297-style per-frame latches for 0x0B sections:
 	//   0x5D latch 0xFFFF -> bit 0x40 (scripted obstacle/contact)
@@ -1491,7 +1547,7 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	else if (_activeInputSource == kInputSourceJoystickDigital)
 		inputSourceName = "joystick-dpad";
 
-	if (usedJoystick) {
+	if (usedJoystick && _optEnhancedControls) {
 		// The 0x0B first-person handler is shared by multiple RA1 stages. Smooth
 		// analog stick input over time so these sections keep full reach without
 		// feeling hyper-sensitive, while leaving mouse behavior untouched.
@@ -1510,9 +1566,11 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	}
 	_inputAxisDeltaX = inputX;
 
-	debug("RA1 GAME 0x0B input: frame=%d source=%s view=(%d,%d) health=%d prevFlags=0x%02x axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
+	debug("RA1 GAME 0x0B input: frame=%d source=%s controls=%s window=%d view=(%d,%d) health=%d prevFlags=0x%02x axis=(%d,%d) mouse=(%d,%d) actions(L,R,U,D)=(%d,%d,%d,%d) raw=(%d,%d) final=(%d,%d) level=%d opcode=0x%X",
 		_gameCounter,
 		inputSourceName,
+		_optEnhancedControls ? "enhanced" : "original",
+		gameOp0BSmoothWindow,
 		_perspectiveX, _perspectiveY,
 		_health, _prevDamageFlags,
 		_joystickAxisX, _joystickAxisY,
@@ -1533,13 +1591,13 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 
 	int sumInputX = 0;
 	int sumInputY = 0;
-	for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
+	for (int i = 0; i < gameOp0BSmoothWindow; i++) {
 		sumInputX += _inputHistoryX[i];
 		sumInputY += _inputHistoryY[i];
 	}
 
-	_avgInputX = (int16)(sumInputX / kGameOp0BSmoothWindow);
-	_avgInputY = (int16)(-sumInputY / kGameOp0BSmoothWindow);
+	_avgInputX = (int16)(sumInputX / gameOp0BSmoothWindow);
+	_avgInputY = (int16)(-sumInputY / gameOp0BSmoothWindow);
 	_avgInputX = CLIP<int16>(_avgInputX, -0xA0, 0xA0);
 	_avgInputY = CLIP<int16>(_avgInputY, -0x46, 0x41);
 
@@ -1555,13 +1613,13 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 
 	int sumViewX = 0;
 	int sumViewY = 0;
-	for (int i = 0; i < kGameOp0BSmoothWindow; i++) {
+	for (int i = 0; i < gameOp0BSmoothWindow; i++) {
 		sumViewX += _viewHistoryX[i];
 		sumViewY += _viewHistoryY[i];
 	}
 
-	int16 avgViewX = (int16)(sumViewX / kGameOp0BSmoothWindow);
-	int16 avgViewY = (int16)(sumViewY / kGameOp0BSmoothWindow);
+	int16 avgViewX = (int16)(sumViewX / gameOp0BSmoothWindow);
+	int16 avgViewY = (int16)(sumViewY / gameOp0BSmoothWindow);
 	_perspectiveX = CLIP<int16>((int16)((avgViewX >> 1) + 0x20), 0, 0x40);
 	_perspectiveY = CLIP<int16>((int16)((avgViewY >> 1) + 0x17), 0, 0x2E);
 	resetProjectionTable();
@@ -1844,6 +1902,11 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 		_mousePrevBiasY = 0;
 		_mouseBiasLatch = false;
 		_mouseRecentering = false;
+		_mouseVirtualRawX = kRA1DosMouseCenterX;
+		_mouseVirtualRawY = kRA1DosMouseCenterY;
+		_mouseVirtualPrevLogicalX = kRA1CenterX;
+		_mouseVirtualPrevLogicalY = kRA1CenterY;
+		_mouseVirtualValid = false;
 		_level2JoystickFilteredX = 0;
 		_level2JoystickFilteredY = 0;
 
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 22e5d5e0fcc..d491e849b2b 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -167,11 +167,11 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 			_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
 			return true;
 		case kScummActionInsaneLeft:
-			if (_optionsSel == 6)
+			if (_optionsSel == 7)
 				setRebel1Volume(_vm, _optVolume, -5);
 			return true;
 		case kScummActionInsaneRight:
-			if (_optionsSel == 6)
+			if (_optionsSel == 7)
 				setRebel1Volume(_vm, _optVolume, 5);
 			return true;
 		case kScummActionInsaneAttack:
@@ -402,14 +402,14 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 			return true;
 		case Common::KEYCODE_LEFT:
 		case Common::KEYCODE_a:
-			// Volume down when on volume row (row 6)
-			if (_optionsSel == 6)
+			// Volume down when on volume row (row 7)
+			if (_optionsSel == 7)
 				setRebel1Volume(_vm, _optVolume, -5);
 			return true;
 		case Common::KEYCODE_RIGHT:
 		case Common::KEYCODE_d:
-			// Volume up when on volume row (row 6)
-			if (_optionsSel == 6)
+			// Volume up when on volume row (row 7)
+			if (_optionsSel == 7)
 				setRebel1Volume(_vm, _optVolume, 5);
 			return true;
 		case Common::KEYCODE_RETURN:
@@ -596,7 +596,8 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 			_optMusicEnabled  ? "MUSIC IS ON"             : "MUSIC IS OFF",
 			_optSfxEnabled    ? "SFX AND VOICE ARE ON"    : "SFX AND VOICE ARE OFF",
 			_optTextEnabled   ? "DIALOGUE TEXT IS ON"      : "DIALOGUE TEXT IS OFF",
-			_optControlsYFlip ? "CONTROLS ARE Y-FLIPPED"  : "CONTROLS ARE NORMAL",
+			_optEnhancedControls ? "INPUT STYLE ENHANCED" : "INPUT STYLE ORIGINAL",
+			_optControlsYFlip ? "Y AXIS IS INVERTED"      : "Y AXIS IS NORMAL",
 			volLine,
 			diffLine
 		};
@@ -746,12 +747,15 @@ void InsaneRebel1::runOptionsMenu() {
 				_optTextEnabled = !_optTextEnabled;
 				ConfMan.setBool("subtitles", _optTextEnabled);
 				break;
-			case 5: // Toggle Y-flip controls
+			case 5: // Toggle enhanced/original input style
+				_optEnhancedControls = !_optEnhancedControls;
+				break;
+			case 6: // Toggle Y-flip controls
 				_optControlsYFlip = !_optControlsYFlip;
 				break;
-			case 6: // Volume — adjusted via left/right in notifyEvent
+			case 7: // Volume — adjusted via left/right in notifyEvent
 				break;
-			case 7: // Cycle difficulty
+			case 8: // Cycle difficulty
 				_difficulty = (_difficulty + 1) % 3;
 				loadTuningForLevel(0);
 				break;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 2815119bd10..39861afee1f 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -261,6 +261,11 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_mousePrevBiasY = 0;
 	_mouseBiasLatch = false;
 	_mouseRecentering = false;
+	_mouseVirtualRawX = 0x140;
+	_mouseVirtualRawY = 100;
+	_mouseVirtualPrevLogicalX = kRA1CenterX;
+	_mouseVirtualPrevLogicalY = kRA1CenterY;
+	_mouseVirtualValid = false;
 	_joystickAxisX = 0;
 	_joystickAxisY = 0;
 	_lastJoystickAxisEventTime = 0;
@@ -326,13 +331,13 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_levelSelectActive = false;
 	_levelSelectSel = 0;
 	_startLevel = 1;
-	_turbulenceEnabled = true;
 
 	// Options — read initial state from ScummVM mixer
 	_optRookieOneFemale = false;
 	_optMusicEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kMusicSoundType);
 	_optSfxEnabled = !_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSFXSoundType);
 	_optTextEnabled = true;
+	_optEnhancedControls = true;
 	_optControlsYFlip = false;
 	_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
 
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 7a5c1a7cbc3..c324a590dbf 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -279,6 +279,11 @@ private:
 	int16 _mousePrevBiasY; // 0x976E: previous-frame biasY
 	bool _mouseBiasLatch;  // 0x4486: one-frame large-jump latch
 	bool _mouseRecentering; // 0x976D: suppress recursive updates during warp
+	int16 _mouseVirtualRawX; // Virtual DOS mouse X used by original-input recentering
+	int16 _mouseVirtualRawY; // Virtual DOS mouse Y used by original-input recentering
+	int16 _mouseVirtualPrevLogicalX;
+	int16 _mouseVirtualPrevLogicalY;
+	bool _mouseVirtualValid;
 	int16 _joystickAxisX;   // Rebel-specific left-stick X captured from keymapper axis events
 	int16 _joystickAxisY;   // Rebel-specific left-stick Y captured from keymapper axis events
 	uint32 _lastJoystickAxisEventTime;
@@ -452,9 +457,9 @@ private:
 	int _menuFrameCounter;
 
 	// Options submenu state — RunGameOptionsMenu (0x14B42)
-	static const int kOptionsItemCount = 8;
+	static const int kOptionsItemCount = 9;
 	bool _optionsActive;     // True when showing options instead of main menu
-	int _optionsSel;         // 0..7 selected option row
+	int _optionsSel;         // 0..8 selected option row
 	bool _levelSelectActive; // True when showing level-select submenu
 	int _levelSelectSel;     // 0=Level1 ... N-1=Back
 	int _startLevel;         // 1-based start level for "Start New Game"
@@ -464,9 +469,9 @@ private:
 	bool _optMusicEnabled;    // DAT_22b7: music on/off
 	bool _optSfxEnabled;      // DAT_22b8: sfx+voice on/off
 	bool _optTextEnabled;     // DAT_22b9: dialogue text on/off
+	bool _optEnhancedControls; // ScummVM option: current responsive controls vs original control law
 	bool _optControlsYFlip;   // DAT_22be: Y-axis inversion
 	int  _optVolume;          // DAT_22c1: master volume 0..127
-	bool _turbulenceEnabled;  // Random per-frame jitter in deltaX (original has it on)
 
 	// High scores / TOP PILOTS display — data at DS:0x1D0
 	static const int kHighScoreCount = 10;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index f42e283fff3..6f091cf8a9f 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1994,6 +1994,11 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 
 	// Center mouse, hide system cursor (we draw our own), lock mouse to window
 	smush_warpMouse(160, 100, -1);
+	_mouseVirtualRawX = 0x140;
+	_mouseVirtualRawY = 100;
+	_mouseVirtualPrevLogicalX = kRA1CenterX;
+	_mouseVirtualPrevLogicalY = kRA1CenterY;
+	_mouseVirtualValid = false;
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
 


Commit: 559b588ebf7d9407479f23127c4020ff16e31b44
    https://github.com/scummvm/scummvm/commit/559b588ebf7d9407479f23127c4020ff16e31b44
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:06+02:00

Commit Message:
SCUMM: RA1: Implement missing chunks

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra1.h


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 7754e319101..4d828c8356c 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -61,6 +61,64 @@ static void ra1ApplyCenteredFetchPlacement(InsaneRebel1 *rebel1, int width, int
 	top -= ((projectedTop - top) >> 2);
 }
 
+static bool ra1EnsureBuffer(byte *&buffer, int32 &bufferSize, int32 neededSize) {
+	if (neededSize <= 0)
+		return false;
+	if (buffer != nullptr && bufferSize >= neededSize)
+		return true;
+
+	byte *newBuffer = (byte *)realloc(buffer, neededSize);
+	if (newBuffer == nullptr)
+		return false;
+
+	buffer = newBuffer;
+	bufferSize = neededSize;
+	return true;
+}
+
+static void ra1CopyFadeRun(byte *dst, const byte *src, int srcPitch, int width, int height,
+		int dstPos, int srcPos, int count) {
+	const int frameSize = width * height;
+	if (dstPos < 0 || srcPos < 0 || count <= 0 || dstPos >= frameSize || srcPos >= frameSize)
+		return;
+
+	count = MIN(count, frameSize - dstPos);
+	count = MIN(count, frameSize - srcPos);
+	while (count > 0) {
+		const int srcX = srcPos % width;
+		const int dstX = dstPos % width;
+		const int run = MIN(count, width - srcX);
+		const int rowRun = MIN(run, width - dstX);
+		memcpy(dst + dstPos, src + (srcPos / width) * srcPitch + srcX, rowRun);
+		srcPos += rowRun;
+		dstPos += rowRun;
+		count -= rowRun;
+	}
+}
+
+static void ra1RememberDisplayedFrame(byte *&buffer, int32 &bufferSize, int &storedWidth,
+		int &storedHeight, bool &valid, const byte *src, int pitch, int width, int height) {
+	if (src == nullptr || width <= 0 || height <= 0)
+		return;
+
+	const int32 neededSize = width * height;
+	if (!ra1EnsureBuffer(buffer, bufferSize, neededSize))
+		return;
+
+	if (buffer == src && pitch == width) {
+		// Already displaying the retained FADE buffer.
+	} else if (pitch == width) {
+		memcpy(buffer, src, neededSize);
+	} else {
+		for (int y = 0; y < height; ++y)
+			memcpy(buffer + y * width, src + y * pitch, width);
+	}
+
+	storedWidth = width;
+	storedHeight = height;
+	valid = true;
+}
+
 SmushPlayerRebel1::SmushPlayerRebel1(ScummEngine_v7 *scumm, IMuseDigital *imuseDigital, Insane *insane)
 	: SmushPlayer(scumm, imuseDigital, insane) {
 	initGamePlayerFields();
@@ -86,6 +144,12 @@ void SmushPlayerRebel1::initGamePlayerFields() {
 	_ra1ViewportOffsetX = 0;
 	_ra1ViewportOffsetY = 0;
 	_ra1FrameSourceSkipY = 0;
+	_ra1FadeFrame = nullptr;
+	_ra1FadeFrameSize = 0;
+	_ra1FadeFrameWidth = 0;
+	_ra1FadeFrameHeight = 0;
+	_ra1FadeFrameValid = false;
+	_ra1UseFadeFrame = false;
 }
 
 void SmushPlayerRebel1::destroyGamePlayerFields() {
@@ -98,6 +162,9 @@ void SmushPlayerRebel1::destroyGamePlayerFields() {
 	free(_ra1PresentationBuffer);
 	_ra1PresentationBuffer = nullptr;
 	_ra1PresentationBufferSize = 0;
+	free(_ra1FadeFrame);
+	_ra1FadeFrame = nullptr;
+	_ra1FadeFrameSize = 0;
 }
 
 void SmushPlayerRebel1::resetGameVideoState() {
@@ -112,6 +179,7 @@ void SmushPlayerRebel1::resetGameVideoState() {
 	_ra1ObjOverlayHeight = 0;
 	_ra1ViewportOffsetX = 0;
 	_ra1ViewportOffsetY = 0;
+	_ra1UseFadeFrame = false;
 }
 
 void SmushPlayerRebel1::releaseGameVideoState() {
@@ -137,6 +205,14 @@ void SmushPlayerRebel1::releaseGameVideoState() {
 	free(_ra1PresentationBuffer);
 	_ra1PresentationBuffer = nullptr;
 	_ra1PresentationBufferSize = 0;
+
+	free(_ra1FadeFrame);
+	_ra1FadeFrame = nullptr;
+	_ra1FadeFrameSize = 0;
+	_ra1FadeFrameWidth = 0;
+	_ra1FadeFrameHeight = 0;
+	_ra1FadeFrameValid = false;
+	_ra1UseFadeFrame = false;
 }
 
 bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStream &b) {
@@ -250,7 +326,140 @@ SmushFont *SmushPlayerRebel1::getGameFont(int font) {
 }
 
 void SmushPlayerRebel1::adjustGamePalette() {
+	for (int i = 0; i < ARRAYSIZE(_pal); ++i)
+		_shiftedDeltaPal[i] = _pal[i] << 7;
+	memset(_deltaPal, 0, sizeof(_deltaPal));
+	_pal[0] = _pal[1] = _pal[2] = 0;
+}
+
+void SmushPlayerRebel1::ra1HandleDeltaPalette(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize < 4) {
+		b.skip(subSize);
+		return;
+	}
+
+	const uint32 command = b.readUint32BE();
+	const int32 payloadBytes = subSize - 4;
+
+	if (command == 0 || command == 2) {
+		_deltaPal[0] = 0;
+		_shiftedDeltaPal[0] = 0;
+		int32 remaining = payloadBytes;
+		if (remaining >= 2) {
+			// The original loop starts at palette component 1, leaving component
+			// 0 black and ignoring the first delta word in the XPAL payload.
+			b.skip(2);
+			remaining -= 2;
+		}
+
+		for (int i = 1; i < ARRAYSIZE(_pal); ++i) {
+			_shiftedDeltaPal[i] = _pal[i] << 7;
+			if (remaining >= 2) {
+				_deltaPal[i] = b.readSint16LE();
+				remaining -= 2;
+			} else {
+				_deltaPal[i] = 0;
+			}
+		}
+
+		if (remaining > 0)
+			b.skip(remaining);
+
+		// Command 2 in the DOS dispatcher first restores the palette state before
+		// loading a new delta table. ScummVM keeps the active palette in _pal, so
+		// marking it dirty is the corresponding visible-side effect.
+		if (command == 2)
+			setDirtyColors(0, 255);
+		return;
+	}
+
+	if (payloadBytes > 0)
+		b.skip(payloadBytes);
+
+	for (int i = 1; i < ARRAYSIZE(_pal); ++i) {
+		_shiftedDeltaPal[i] += _deltaPal[i];
+		_pal[i] = CLIP<int32>(_shiftedDeltaPal[i] >> 7, 0, 255);
+	}
 	_pal[0] = _pal[1] = _pal[2] = 0;
+	setDirtyColors(0, 255);
+}
+
+void SmushPlayerRebel1::ra1HandleFade(int32 subSize, Common::SeekableReadStream &b) {
+	if (subSize <= 24 || _dst == nullptr || _width <= 0 || _height <= 0) {
+		b.skip(subSize);
+		return;
+	}
+
+	byte *fadeData = (byte *)malloc(subSize);
+	if (fadeData == nullptr) {
+		b.skip(subSize);
+		return;
+	}
+	b.read(fadeData, subSize);
+
+	int fadeWidth = kRA1PresentationScreenWidth;
+	int fadeHeight = kRA1PresentationScreenHeight;
+	if (subSize >= 16 && READ_BE_UINT32(fadeData) == MKTAG('F','D','H','D')) {
+		const int headerWidth = READ_LE_UINT16(fadeData + 12);
+		const int headerHeight = READ_LE_UINT16(fadeData + 14);
+		if (headerWidth > 0 && headerHeight > 0) {
+			fadeWidth = headerWidth;
+			fadeHeight = headerHeight;
+		}
+	}
+
+	fadeWidth = MIN(fadeWidth, MIN(_vm->_screenWidth, _width - _scrollX));
+	fadeHeight = MIN(fadeHeight, MIN(_vm->_screenHeight, _height - _scrollY));
+	if (fadeWidth <= 0 || fadeHeight <= 0) {
+		free(fadeData);
+		return;
+	}
+
+	const int32 fadeFrameSize = fadeWidth * fadeHeight;
+	if (!ra1EnsureBuffer(_ra1FadeFrame, _ra1FadeFrameSize, fadeFrameSize)) {
+		free(fadeData);
+		return;
+	}
+
+	if (!_ra1FadeFrameValid ||
+			_ra1FadeFrameWidth != fadeWidth || _ra1FadeFrameHeight != fadeHeight) {
+		memset(_ra1FadeFrame, 0, fadeFrameSize);
+		_ra1FadeFrameValid = true;
+	}
+	_ra1FadeFrameWidth = fadeWidth;
+	_ra1FadeFrameHeight = fadeHeight;
+
+	const byte *control = fadeData + 24;
+	int32 remaining = subSize - 24;
+	const byte *src = _dst + _scrollY * _width + _scrollX;
+	int srcPos = 0;
+	int dstPos = 0;
+
+	while (remaining > 0 && dstPos < fadeFrameSize && srcPos < fadeFrameSize) {
+		byte op = *control++;
+		remaining--;
+
+		int count = op & 0x7F;
+		if (count == 0) {
+			if (remaining < 2)
+				break;
+			count = READ_LE_UINT16(control);
+			control += 2;
+			remaining -= 2;
+		}
+
+		if (op & 0x80) {
+			srcPos += count;
+			dstPos += count;
+		} else {
+			ra1CopyFadeRun(_ra1FadeFrame, src, _width, fadeWidth, fadeHeight, dstPos, srcPos, count);
+			srcPos += count;
+			dstPos += count;
+		}
+	}
+
+	_ra1UseFadeFrame = true;
+	free(fadeData);
 }
 
 bool SmushPlayerRebel1::handleGameAnimHeader(byte *headerContent) {
@@ -734,7 +943,8 @@ static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize)
 
 		if (subType == MKTAG('F', 'R', 'M', 'E'))
 			break;
-		if (subType == MKTAG('G', 'A', 'M', 'E')) {
+		if (subType == MKTAG('G', 'A', 'M', 'E') ||
+				subType == MKTAG('G', 'A', 'M', '2')) {
 			b.seek(frameStart, SEEK_SET);
 			return true;
 		}
@@ -843,7 +1053,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 			handleTextResource(subType, subSize, b);
 			break;
 		case MKTAG('X','P','A','L'):
-			handleDeltaPalette(subSize, b);
+			ra1HandleDeltaPalette(subSize, b);
 			break;
 		case MKTAG('I','A','C','T'):
 			handleIACT(subSize, b);
@@ -860,7 +1070,8 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 		case MKTAG('G','O','S','T'):
 			handleGameGost(subSize, b);
 			break;
-		case MKTAG('G','A','M','E'): {
+		case MKTAG('G','A','M','E'):
+		case MKTAG('G','A','M','2'): {
 			InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
 			rebel1->handleGameChunk(subSize, b);
 			break;
@@ -880,6 +1091,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 
 					bool recognized = (embTag == MKTAG('F','O','B','J') ||
 					                   embTag == MKTAG('G','A','M','E') ||
+					                   embTag == MKTAG('G','A','M','2') ||
 					                   embTag == MKTAG('P','S','A','D'));
 
 					if (!recognized || embSize > (uint32)embRemaining) {
@@ -903,7 +1115,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 							_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
 							_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
 						}
-					} else if (embTag == MKTAG('G','A','M','E')) {
+					} else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
 						Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
 						InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
 						rebel1->handleGameChunk(embSize, embStream);
@@ -925,8 +1137,9 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 			frameSize = 0;
 			continue;
 		}
-		case MKTAG('G','A','M','2'):
 		case MKTAG('F','A','D','E'):
+			ra1HandleFade(subSize, b);
+			break;
 		case MKTAG('S','E','G','A'):
 		case MKTAG('A','D','L',' '):
 		case MKTAG('A','D','L','2'):
@@ -993,21 +1206,37 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
 	if (_dst == nullptr || _width <= 0 || _height <= 0)
 		return;
 
+	const bool useFadeFrame = _ra1UseFadeFrame && _ra1FadeFrameValid && _ra1FadeFrame != nullptr;
+	if (useFadeFrame) {
+		src = _ra1FadeFrame;
+		srcPitch = _ra1FadeFrameWidth;
+		width = MIN(width, _ra1FadeFrameWidth);
+		height = MIN(height, _ra1FadeFrameHeight);
+	}
+
 	if (!_insane || !static_cast<InsaneRebel1 *>(_insane)->isInteractiveVideoActive() ||
 			_vm->_screenWidth != kRA1PresentationScreenWidth ||
 			_vm->_screenHeight != kRA1PresentationScreenHeight) {
 		SmushPlayer::handleGameUpdateScreen(src, srcPitch, width, height);
+		ra1RememberDisplayedFrame(_ra1FadeFrame, _ra1FadeFrameSize,
+			_ra1FadeFrameWidth, _ra1FadeFrameHeight, _ra1FadeFrameValid,
+			src, srcPitch, width, height);
+		_ra1UseFadeFrame = false;
 		return;
 	}
 
 	int ra1ViewX = _ra1ViewportOffsetX;
 	int ra1ViewY = _ra1ViewportOffsetY;
 
-	const int srcX = CLIP(_scrollX + ra1ViewX + kRA1PresentationBorder, 0, _width - 1);
-	const int srcY = CLIP(_scrollY + ra1ViewY + kRA1PresentationBorder, 0, _height - 1);
+	const byte *sourceBase = useFadeFrame ? src : _dst;
+	const int sourcePitch = useFadeFrame ? srcPitch : _width;
+	const int sourceWidth = useFadeFrame ? width : _width;
+	const int sourceHeight = useFadeFrame ? height : _height;
+	const int srcX = useFadeFrame ? 0 : CLIP(_scrollX + ra1ViewX + kRA1PresentationBorder, 0, sourceWidth - 1);
+	const int srcY = useFadeFrame ? 0 : CLIP(_scrollY + ra1ViewY + kRA1PresentationBorder, 0, sourceHeight - 1);
 
-	int frameWidth = MIN<int>(_width - srcX, kRA1PresentationWidth);
-	int frameHeight = MIN<int>(_height - srcY, kRA1PresentationHeight);
+	int frameWidth = MIN<int>(sourceWidth - srcX, kRA1PresentationWidth);
+	int frameHeight = MIN<int>(sourceHeight - srcY, kRA1PresentationHeight);
 	if (frameWidth <= 0 || frameHeight <= 0)
 		return;
 
@@ -1023,16 +1252,21 @@ void SmushPlayerRebel1::handleGameUpdateScreen(const byte *src, int srcPitch, in
 
 	// ResetPlaybackViewport() (0x20A53) initializes the interactive draw window
 	// to (4,4,312,192), leaving a black presentation frame around cockpit scenes.
-	const byte *dst = _dst + srcY * _width + srcX;
+	const byte *dst = sourceBase + srcY * sourcePitch + srcX;
 	byte *presentationDst = _ra1PresentationBuffer +
 		kRA1PresentationBorder * kRA1PresentationScreenWidth + kRA1PresentationBorder;
 	for (int y = 0; y < frameHeight; y++) {
 		memcpy(presentationDst + y * kRA1PresentationScreenWidth,
-			dst + y * _width, frameWidth);
+			dst + y * sourcePitch, frameWidth);
 	}
 
 	SmushPlayer::handleGameUpdateScreen(_ra1PresentationBuffer,
 		kRA1PresentationScreenWidth, kRA1PresentationScreenWidth, kRA1PresentationScreenHeight);
+	ra1RememberDisplayedFrame(_ra1FadeFrame, _ra1FadeFrameSize,
+		_ra1FadeFrameWidth, _ra1FadeFrameHeight, _ra1FadeFrameValid,
+		_ra1PresentationBuffer, kRA1PresentationScreenWidth,
+		kRA1PresentationScreenWidth, kRA1PresentationScreenHeight);
+	_ra1UseFadeFrame = false;
 }
 
 SmushFont *SmushPlayerRebel1::ra1GetFont(int font) {
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index 3cd067fc794..5d54cfd0b1a 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -60,6 +60,8 @@ protected:
 
 private:
 	void ra1HandleGost(int32 subSize, Common::SeekableReadStream &b);
+	void ra1HandleDeltaPalette(int32 subSize, Common::SeekableReadStream &b);
+	void ra1HandleFade(int32 subSize, Common::SeekableReadStream &b);
 	SmushFont *ra1GetFont(int font);
 	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
 
@@ -85,6 +87,15 @@ private:
 	int _ra1ViewportOffsetX;
 	int _ra1ViewportOffsetY;
 	int _ra1FrameSourceSkipY;
+
+	// RA1 FADE chunks update the visible 320x200 screen through a sparse
+	// copy mask, separate from the decoded frame buffer.
+	byte *_ra1FadeFrame;
+	int32 _ra1FadeFrameSize;
+	int _ra1FadeFrameWidth;
+	int _ra1FadeFrameHeight;
+	bool _ra1FadeFrameValid;
+	bool _ra1UseFadeFrame;
 };
 
 } // End of namespace Scumm


Commit: c47a0c95a4998167a1feed43024f286d8b8f1ca5
    https://github.com/scummvm/scummvm/commit/c47a0c95a4998167a1feed43024f286d8b8f1ca5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:06+02:00

Commit Message:
SCUMM: RA1: Improve HandleGameOp19 accuracy

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 795917c5231..91c31357ed1 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -90,6 +90,18 @@ inline bool hasLevel15FinalSweepDamage(uint16 frameCounter, int16 perspectiveX)
 	}
 }
 
+inline bool ra1DispatcherHudOnlyWhenDisabled(uint32 opcode) {
+	switch (opcode) {
+	case 0x07:
+	case 0x08:
+	case 0x0B:
+	case 0x1A:
+		return true;
+	default:
+		return false;
+	}
+}
+
 inline bool isLevel2DamageLatch(uint16 code) {
 	switch (code) {
 	case 0x0003:
@@ -1724,7 +1736,8 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 }
 
 
-// updateOnFootPhysics — HandleGameOp19_OnFootSequence (0x19) + HandleGameOp1A_OnFootVariant (0x1A).
+// updateOnFootPhysics — ScummVM-side glue for original on-foot GAME handlers
+// HandleGameOp19_OnFootSequence (0x19) and HandleGameOp1A_OnFootVariant (0x1A).
 // On-foot handler for Level 9 (Stormtroopers). Character walks left/right, crosshair tracks mouse.
 //
 // Original has TWO separate variable pairs:
@@ -1735,7 +1748,9 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 const int16 kOnFootCenterX = 0xA3;  // g_perspectiveX in HandleGameOp19
 const int16 kOnFootCenterY = 0x82;  // g_perspectiveY in HandleGameOp19
 
-void InsaneRebel1::updateOnFootPhysics() {
+// Port split matching HandleGameOp19_OnFootSequence. The helper name is new to
+// this implementation; the original code dispatches the opcode handler directly.
+void InsaneRebel1::updateOnFootSequence() {
 	// --- First-frame initialization (0x19 counter==0) ---
 	if (!_onFootInitialized) {
 		_onFootInitialized = true;
@@ -1796,15 +1811,6 @@ void InsaneRebel1::updateOnFootPhysics() {
 			_shipDirIndex = 4;  // Walk left
 	}
 
-	// --- 0x1A: Crosshair positioning (HandleGameOp1A_OnFootVariant) ---
-	// shipPosX/Y = mouse_input + crosshair_center + character_offset
-	int16 inputX = 0, inputY = 0;
-	preprocessMouseAxes(inputX, inputY);
-	inputX = CLIP<int16>(inputX, -100, 100);
-	int16 inputYNeg = CLIP<int16>((int16)(-inputY), -0x4B, 0x0F);
-	_shipPosX = inputX + kOnFootCenterX + _onFootCharX;
-	_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
-
 	// --- Scripted damage latches → damageFlags (matching FUN_1B297 pattern) ---
 	// GAME 0x5D/0x5F set latches; convert to damage flags before the check.
 	if (_gameLatch5D == 0xFFFF)
@@ -1814,9 +1820,10 @@ void InsaneRebel1::updateOnFootPhysics() {
 		_damageFlags |= 0x80;
 
 	// --- Damage handling (from HandleGameOp19_OnFootSequence) ---
-	// On-foot uses single tuning value (DAT_00001b29 offset = miss) for all damage types.
+	// On-foot damage uses the same heavy-damage tuning byte as ship shot/collision
+	// damage in the original, not the miss penalty.
 	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer < 1) {
-		_health -= _tuning.miss;
+		_health -= _tuning.shot;
 		if (_health < 0) {
 			_deathTimer = 15;
 			_deathCauseIndicator = (_damageFlags & 0x80) ? 2 : 1;
@@ -1826,6 +1833,32 @@ void InsaneRebel1::updateOnFootPhysics() {
 		playSfx(kSfxBoom, 127, 0);
 		_screenFlash = 5;
 	}
+}
+
+// Port split matching HandleGameOp1A_OnFootVariant. The helper name is new to
+// this implementation; the original code dispatches the opcode handler directly.
+void InsaneRebel1::updateOnFootAimVariant() {
+	// --- 0x1A: Crosshair positioning (HandleGameOp1A_OnFootVariant) ---
+	// shipPosX/Y = mouse_input + crosshair_center + character_offset
+	int16 inputX = 0, inputY = 0;
+	preprocessMouseAxes(inputX, inputY);
+	inputX = CLIP<int16>(inputX, -100, 100);
+	int16 inputYNeg = CLIP<int16>((int16)(-inputY), -0x4B, 0x0F);
+	_shipPosX = inputX + kOnFootCenterX + _onFootCharX;
+	_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
+}
+
+void InsaneRebel1::updateOnFootPhysics() {
+	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
+	const bool sequenceOpcode =
+		hasFrameGameOpcode(0x19) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x19);
+	const bool aimOpcode =
+		hasFrameGameOpcode(0x1A) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x1A);
+
+	if (sequenceOpcode)
+		updateOnFootSequence();
+	if (aimOpcode)
+		updateOnFootAimVariant();
 
 	if (_damageCooldown > 0)
 		_damageCooldown--;
@@ -1870,10 +1903,31 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	uint32 opcode = b.readUint32BE();
 	uint32 param1 = b.readUint32BE();
 
+	// FUN_1BE1B applies two global gates before the opcode switch. Bit 0 of
+	// g_combatModeFlags skips gameplay dispatch entirely; bit 5 of g_hudDisableFlags
+	// suppresses the handlers while still requesting HUD refresh for a few opcodes.
+	if (_gameplayFlags75ff & 1) {
+		debug(7, "RA1 GAME 0x%02x: skipped by combat mode flags=0x%02x",
+			opcode, _gameplayFlags75ff);
+		return;
+	}
+	if (_gameplayFlags75fe & 0x20) {
+		if (ra1DispatcherHudOnlyWhenDisabled(opcode))
+			_hudRenderFlag = 0xFF;
+		debug(7, "RA1 GAME 0x%02x: skipped by HUD disable flags=0x%02x",
+			opcode, _gameplayFlags75fe);
+		return;
+	}
+
 	switch (opcode) {
 	case 0x5E:
 		// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
 		// This is not a pure control-mode assignment.
+		if (_frameDispatchFlags & 0x40) {
+			debug(7, "RA1 GAME 0x5E: reset suppressed by dispatch flags=0x%02x",
+				_frameDispatchFlags);
+			break;
+		}
 		_damageFlags = 0;
 		_prevDamageFlags = 0;
 		_damageCooldown = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index c324a590dbf..d285f398e2d 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -303,6 +303,8 @@ private:
 
 	// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
 	void updateOnFootPhysics();
+	void updateOnFootSequence();
+	void updateOnFootAimVariant();
 	int16 _onFootCharX;      // Character draw X (g_shipOffsetX in original)
 	int16 _onFootCharY;      // Character draw Y (g_shipOffsetY in original)
 	int16 _onFootAnimCounter; // DAT_0000828a: fire animation counter


Commit: e62919da5be6076fb748291ed2fa3907326216f3
    https://github.com/scummvm/scummvm/commit/e62919da5be6076fb748291ed2fa3907326216f3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:07+02:00

Commit Message:
SCUMM: RA1: Refactor laser rendering

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


diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 9cb9c810bfb..7b552325774 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -418,6 +418,54 @@ void renderSpriteWithFlags(byte *dst, int pitch, int width, int height,
 	}
 }
 
+// ScummVM-side helper only: the original keeps this shot-sprite math inline in
+// several GAME handlers. It is collapsed here because the direction/lerp/render
+// sequence is identical for one-beam shot sprites.
+void renderAimedShotSprite(byte *dst, int pitch, int width, int height,
+		const RA1SpriteBank &laserBank, int startX, int startY, int targetX, int targetY,
+		int lerp) {
+	if (laserBank.numSprites <= 0)
+		return;
+
+	const int dir = ra1ShotDirection((int16)startX, (int16)startY, (int16)targetX, (int16)targetY);
+	const int sprIdx = MIN<int>(ABS(dir), laserBank.numSprites - 1);
+	const uint32 flags = 0x83 | ((dir < 0) ? 0x2000 : 0);
+	const int interpX = startX + (((targetX - startX) * lerp) >> 3);
+	const int interpY = startY + (((targetY - startY) * lerp) >> 3);
+
+	renderSpriteWithFlags(dst, pitch, width, height,
+		interpX, interpY, laserBank.sprites[sprIdx], flags);
+}
+
+void renderAimedShotPair(byte *dst, int pitch, int width, int height,
+		const RA1SpriteBank &laserBank, int start1X, int start1Y, int start2X, int start2Y,
+		int targetX, int targetY, int lerp) {
+	renderAimedShotSprite(dst, pitch, width, height, laserBank,
+		start1X, start1Y, targetX, targetY, lerp);
+	renderAimedShotSprite(dst, pitch, width, height, laserBank,
+		start2X, start2Y, targetX, targetY, lerp);
+}
+
+// ScummVM-side helper for the 0x0B fallback edge-beam path. The original keeps
+// the bucket lookup inline with the renderer, but both left/right beams share it.
+void renderBucketedShotSprite(byte *dst, int pitch, int width, int height,
+		const RA1SpriteBank &laserBank, int startX, int startY, int targetX, int targetY,
+		int lerp, int frame, uint32 flags) {
+	if (laserBank.numSprites <= 0)
+		return;
+
+	const int dir = ra1ShotDirection((int16)startX, (int16)startY, (int16)targetX, (int16)targetY);
+	const int sprIdx = frame + ra1ShotDirectionBucket(dir);
+	if (sprIdx < 0 || sprIdx >= laserBank.numSprites)
+		return;
+
+	const int interpX = startX + (((targetX - startX) * lerp) >> 3);
+	const int interpY = startY + (((targetY - startY) * lerp) >> 3);
+
+	renderSpriteWithFlags(dst, pitch, width, height,
+		interpX, interpY, laserBank.sprites[sprIdx], flags);
+}
+
 void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
 	if (_currentLevel == 0 && _flyControlMode == 2) {
 		// Port helper: original Level 1 part 2 keeps g_perspectiveX/Y at the
@@ -1021,16 +1069,10 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 				const int srcY = _shotSlots[i].centerY;
 				const int dstX = _shotSlots[i].posX;
 				const int dstY = _shotSlots[i].posY;
-				const int dir = ra1ShotDirection((int16)srcX, (int16)srcY,
-					(int16)dstX, (int16)dstY);
-				const int sprIdx = MIN<int>(ABS(dir), _laserBank.numSprites - 1);
-				const uint32 flags = 0x83 | ((dir < 0) ? 0x2000 : 0);
 				const int onFootLerp = kOnFootShotLerp[timer];
-				const int interpX = srcX + (((dstX - srcX) * onFootLerp) >> 3);
-				const int interpY = srcY + (((dstY - srcY) * onFootLerp) >> 3);
 
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interpX, interpY, _laserBank.sprites[sprIdx], flags);
+				renderAimedShotSprite(dst, pitch, width, height, _laserBank,
+					srcX, srcY, dstX, dstY, onFootLerp);
 				continue;
 			}
 
@@ -1065,21 +1107,8 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 				if (!haveEmitters)
 					continue;
 
-				const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
-				const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
-				const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
-				const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
-				const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
-				const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
-				const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
-				const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
-				const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
-				const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
-
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+				renderAimedShotPair(dst, pitch, width, height, _laserBank,
+					start1X, start1Y, start2X, start2Y, targetX, targetY, lerp);
 				continue;
 			}
 
@@ -1099,17 +1128,6 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 				const int start1Y = shipBaseY + emit.y1;
 				const int start2X = shipBaseX + emit.x2;
 				const int start2Y = shipBaseY + emit.y2;
-				const int dir1 = ra1ShotDirection((int16)start1X, (int16)start1Y, (int16)targetX, (int16)targetY);
-				const int dir2 = ra1ShotDirection((int16)start2X, (int16)start2Y, (int16)targetX, (int16)targetY);
-				const int sprIdx1 = MIN<int>(ABS(dir1), _laserBank.numSprites - 1);
-				const int sprIdx2 = MIN<int>(ABS(dir2), _laserBank.numSprites - 1);
-				const uint32 flags1 = 0x83 | ((dir1 < 0) ? 0x2000 : 0);
-				const uint32 flags2 = 0x83 | ((dir2 < 0) ? 0x2000 : 0);
-				const int interp1X = start1X + (((targetX - start1X) * lerp) >> 3);
-				const int interp1Y = start1Y + (((targetY - start1Y) * lerp) >> 3);
-				const int interp2X = start2X + (((targetX - start2X) * lerp) >> 3);
-				const int interp2Y = start2Y + (((targetY - start2Y) * lerp) >> 3);
-
 				if (_currentLevel == 4) {
 					debug(1, "RA1 op09 shotRender: frame=%d timer=%d shipBase=(%d,%d) target=(%d,%d) emit1=(%d,%d) emit2=(%d,%d) dir=%d variant=%d mode=%d",
 						_gameCounter, timer, shipBaseX, shipBaseY, targetX, targetY,
@@ -1117,10 +1135,8 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 						_shotSlots[i].variant, _flyControlMode);
 				}
 
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interp1X, interp1Y, _laserBank.sprites[sprIdx1], flags1);
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interp2X, interp2Y, _laserBank.sprites[sprIdx2], flags2);
+				renderAimedShotPair(dst, pitch, width, height, _laserBank,
+					start1X, start1Y, start2X, start2Y, targetX, targetY, lerp);
 				continue;
 			}
 
@@ -1159,25 +1175,10 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 
 			const int startLeftX = overlayX + leftStartX;
 			const int startRightX = overlayX + rightStartX;
-			const int dirLeft = ra1ShotDirection((int16)startLeftX, (int16)leftStartY, (int16)targetX, (int16)targetY);
-			const int dirRight = ra1ShotDirection((int16)startRightX, (int16)rightStartY, (int16)targetX, (int16)targetY);
-			const int bucketLeft = ra1ShotDirectionBucket(dirLeft);
-			const int bucketRight = ra1ShotDirectionBucket(dirRight);
-			const int sprIdxLeft = frame + bucketLeft;
-			const int sprIdxRight = frame + bucketRight;
-			const int interpLeftX = startLeftX + (((targetX - startLeftX) * lerp) >> 3);
-			const int interpLeftY = leftStartY + (((targetY - leftStartY) * lerp) >> 3);
-			const int interpRightX = startRightX + (((targetX - startRightX) * lerp) >> 3);
-			const int interpRightY = rightStartY + (((targetY - rightStartY) * lerp) >> 3);
-
-			if (sprIdxLeft >= 0 && sprIdxLeft < _laserBank.numSprites) {
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interpLeftX, interpLeftY, _laserBank.sprites[sprIdxLeft], leftFlags);
-			}
-			if (sprIdxRight >= 0 && sprIdxRight < _laserBank.numSprites) {
-				renderSpriteWithFlags(dst, pitch, width, height,
-					interpRightX, interpRightY, _laserBank.sprites[sprIdxRight], rightFlags);
-			}
+			renderBucketedShotSprite(dst, pitch, width, height, _laserBank,
+				startLeftX, leftStartY, targetX, targetY, lerp, frame, leftFlags);
+			renderBucketedShotSprite(dst, pitch, width, height, _laserBank,
+				startRightX, rightStartY, targetX, targetY, lerp, frame, rightFlags);
 		}
 	}
 }


Commit: 7c868c8408efbceb325954f075364699dce27dc5
    https://github.com/scummvm/scummvm/commit/7c868c8408efbceb325954f075364699dce27dc5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:07+02:00

Commit Message:
SCUMM: RA1: Implement password selection correctly

Changed paths:
    engines/scumm/insane/rebel1/menu.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index d491e849b2b..1d6d033016a 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -32,6 +32,7 @@
 
 namespace Scumm {
 
+const int kRA1MainMenuItemCount = 6;
 const int kRA1LevelSelectItemCount = 16;  // 15 levels + BACK
 const int kRA1LevelSelectRowsPerCol = 8;
 const int kRA1NumLevels = 15;
@@ -43,6 +44,11 @@ const int kRA1MenuFrameH = 0x0f;
 const int kRA1MenuRowH = 0x0f;
 const byte kRA1MenuFrameColor = 0xdf;
 const uint32 kRA1JoystickAxisEscGuardMs = 250;
+// Original picker traversal uses fixed max indices, not strlen(): passcodes
+// stop at 0x1d and high-score names stop at 0x37.
+const char kRA1TextEntryPickerChars[] = "^`_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+const int kRA1PasscodePickerCount = 0x1d + 1;
+const int kRA1NamePickerCount = 0x37 + 1;
 
 static int getRebel1MenuAxisDirection(int16 axisValue) {
 	if (axisValue >= kRA1MenuAxisThreshold)
@@ -105,6 +111,68 @@ static int getRebel1MenuCenteredX(int textWidth) {
 	return (kRA1MenuLogicalWidth - textWidth) / 2;
 }
 
+static const char *getRebel1TextEntryPickerChars(bool) {
+	return kRA1TextEntryPickerChars;
+}
+
+static int getRebel1TextEntryPickerCount(bool passcodeMode) {
+	return passcodeMode ? kRA1PasscodePickerCount : kRA1NamePickerCount;
+}
+
+static int getRebel1PasscodeDifficulty(int passwordIndex) {
+	return (passwordIndex - 1) % 3;
+}
+
+static int getRebel1PasscodeStartLevel(int passwordIndex) {
+	// Original main-menu control flow groups passcodes by difficulty:
+	// 1-3 resume after chapter 3, 4-6 after chapter 6, 7-9 after chapter 10,
+	// 10-12 after chapter 14, and 13-15 jump to the final ending sequence.
+	switch ((passwordIndex - 1) / 3) {
+	case 0:
+		return 4;
+	case 1:
+		return 7;
+	case 2:
+		return 11;
+	case 3:
+		return 15;
+	case 4:
+		return kRA1NumLevels + 1;
+	default:
+		return 0;
+	}
+}
+
+static char normalizeRebel1PasscodeChar(char c) {
+	if (c >= 'a' && c <= 'z')
+		return c - ('a' - 'A');
+	return c;
+}
+
+static bool isRebel1TextEntryChar(bool passcodeMode, char c) {
+	if (passcodeMode) {
+		c = normalizeRebel1PasscodeChar(c);
+		return c >= 'A' && c <= 'Z';
+	}
+
+	return c == ' ' || c == '_' ||
+		(c >= 'A' && c <= 'Z') ||
+		(c >= 'a' && c <= 'z');
+}
+
+static void appendRebel1TextEntryChar(char *buffer, int bufferSize, int maxChars, bool passcodeMode, char c) {
+	if (!buffer || bufferSize <= 0)
+		return;
+
+	c = passcodeMode ? normalizeRebel1PasscodeChar(c) : c;
+	const int len = strlen(buffer);
+	if (len >= maxChars || len >= bufferSize - 1)
+		return;
+
+	buffer[len] = c;
+	buffer[len + 1] = '\0';
+}
+
 static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int x, int y, int w) {
 	if (!dst || width <= 0 || height <= 0)
 		return;
@@ -124,10 +192,135 @@ static void drawRebel1MenuFrame(byte *dst, int pitch, int width, int height, int
 	}
 }
 
+void InsaneRebel1::beginTextEntry(bool passcodeMode) {
+	_textEntryActive = true;
+	_textEntryPasscodeMode = passcodeMode;
+	_textEntryDone = false;
+	_textEntryCanceled = false;
+	_textEntryPickerIndex = 3; // First letter in the original picker string.
+	_textEntryPickerOffsetX = 0;
+	// The DOS buffers include a leading '<' font marker and cap strlen() at 8.
+	_textEntryMaxChars = 8;
+	_textEntryBuffer[0] = '\0';
+
+	if (passcodeMode && _maxChapterUnlocked > 0) {
+		const char *password = getChapterCompletePassword(_maxChapterUnlocked);
+		if (password) {
+			Common::strlcpy(_textEntryBuffer, password, sizeof(_textEntryBuffer));
+			_textEntryPickerIndex = 1;
+		}
+	}
+}
+
+void InsaneRebel1::finishTextEntry(bool canceled) {
+	_textEntryCanceled = canceled;
+	if (!canceled)
+		_textEntryDone = true;
+	_textEntryActive = false;
+	_vm->_smushVideoShouldFinish = true;
+}
+
+void InsaneRebel1::selectTextEntryChar() {
+	if (!_textEntryActive)
+		return;
+
+	const char *pickerChars = getRebel1TextEntryPickerChars(_textEntryPasscodeMode);
+	const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+	if (_textEntryPickerIndex < 0 || _textEntryPickerIndex >= pickerCount)
+		return;
+
+	const char ch = pickerChars[_textEntryPickerIndex];
+	if (ch == '^') {
+		const int len = strlen(_textEntryBuffer);
+		if (len > 0)
+			_textEntryBuffer[len - 1] = '\0';
+	} else if (ch == '`') {
+		finishTextEntry(false);
+	} else if (ch == '_') {
+		appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+			_textEntryMaxChars, _textEntryPasscodeMode, ' ');
+	} else {
+		appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+			_textEntryMaxChars, _textEntryPasscodeMode, ch);
+	}
+}
+
+bool InsaneRebel1::handleTextEntryAction(ScummAction action) {
+	if (!_textEntryActive)
+		return false;
+
+	const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+	switch (action) {
+	case kScummActionInsaneLeft:
+		_textEntryPickerIndex = (_textEntryPickerIndex + pickerCount - 1) % pickerCount;
+		_textEntryPickerOffsetX = -7;
+		return true;
+	case kScummActionInsaneRight:
+		_textEntryPickerIndex = (_textEntryPickerIndex + 1) % pickerCount;
+		_textEntryPickerOffsetX = 7;
+		return true;
+	case kScummActionInsaneAttack:
+		selectTextEntryChar();
+		return true;
+	default:
+		return false;
+	}
+}
+
+bool InsaneRebel1::handleTextEntryKey(const Common::Event &event) {
+	if (!_textEntryActive || event.type != Common::EVENT_KEYDOWN)
+		return false;
+
+	const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+	switch (event.kbd.keycode) {
+	case Common::KEYCODE_LEFT:
+		_textEntryPickerIndex = (_textEntryPickerIndex + pickerCount - 1) % pickerCount;
+		_textEntryPickerOffsetX = -7;
+		return true;
+	case Common::KEYCODE_RIGHT:
+		_textEntryPickerIndex = (_textEntryPickerIndex + 1) % pickerCount;
+		_textEntryPickerOffsetX = 7;
+		return true;
+	case Common::KEYCODE_RETURN:
+	case Common::KEYCODE_KP_ENTER:
+		finishTextEntry(false);
+		return true;
+	case Common::KEYCODE_ESCAPE:
+		finishTextEntry(true);
+		return true;
+	case Common::KEYCODE_BACKSPACE: {
+		const int len = strlen(_textEntryBuffer);
+		if (len > 0)
+			_textEntryBuffer[len - 1] = '\0';
+		return true;
+	}
+	case Common::KEYCODE_SPACE:
+		appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+			_textEntryMaxChars, _textEntryPasscodeMode, ' ');
+		return true;
+	default:
+		break;
+	}
+
+	char ch = (char)event.kbd.ascii;
+	if (ch == '\0')
+		ch = (char)event.kbd.keycode;
+	if (isRebel1TextEntryChar(_textEntryPasscodeMode, ch)) {
+		appendRebel1TextEntryChar(_textEntryBuffer, sizeof(_textEntryBuffer),
+			_textEntryMaxChars, _textEntryPasscodeMode, ch);
+		return true;
+	}
+
+	return true;
+}
+
 bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 	if (!_menuActive || _highScoresActive)
 		return false;
 
+	if (_textEntryActive)
+		return handleTextEntryAction(action);
+
 	if (_levelSelectActive) {
 		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
 		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
@@ -185,10 +378,10 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 
 	switch (action) {
 	case kScummActionInsaneUp:
-		_menuSelection = (_menuSelection + 4) % 5;
+		_menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
 		return true;
 	case kScummActionInsaneDown:
-		_menuSelection = (_menuSelection + 1) % 5;
+		_menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
 		return true;
 	case kScummActionInsaneAttack:
 		_menuConfirmed = true;
@@ -350,6 +543,9 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		}
 	}
 
+	if (_menuActive && _textEntryActive && event.type == Common::EVENT_KEYDOWN)
+		return handleTextEntryKey(event);
+
 	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
 		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
 		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
@@ -432,11 +628,11 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		switch (event.kbd.keycode) {
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_w:
-			_menuSelection = (_menuSelection + 4) % 5;
+			_menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
 			return true;
 		case Common::KEYCODE_DOWN:
 		case Common::KEYCODE_s:
-			_menuSelection = (_menuSelection + 1) % 5;
+			_menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
 			return true;
 		case Common::KEYCODE_RETURN:
 		case Common::KEYCODE_KP_ENTER:
@@ -449,12 +645,13 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 		case Common::KEYCODE_3:
 		case Common::KEYCODE_4:
 		case Common::KEYCODE_5:
+		case Common::KEYCODE_6:
 			_menuSelection = event.kbd.keycode - Common::KEYCODE_1;
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
 		case Common::KEYCODE_ESCAPE:
-			_menuSelection = 4;
+			_menuSelection = kRA1MainMenuItemCount - 1;
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
@@ -556,6 +753,11 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 		drawFontBankString(dst, pitch, width, height, x, y, text);
 	};
 
+	if (_textEntryActive) {
+		renderTextEntryOverlay(dst, pitch, width, height);
+		return;
+	}
+
 	if (_highScoresActive) {
 		// --- TOP PILOTS high score display ---
 		// Original renders over O1SCORE.ANM. Title appears after frame 20,
@@ -662,9 +864,10 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	}
 
 	// --- Main menu ---
-	const char *kMenuItems[5] = {
+	const char *kMenuItems[kRA1MainMenuItemCount] = {
 		"START NEW GAME",
 		"GAME OPTIONS",
+		"ENTER PASSCODE",
 		"LEVEL SELECT",
 		"CONTINUE DEMO",
 		"EXIT TO DOS"
@@ -676,7 +879,7 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	drawTitleText(titleX, 30, "MAIN MENU");
 
 	// Draw menu items centered horizontally
-	for (int i = 0; i < 5; i++) {
+	for (int i = 0; i < kRA1MainMenuItemCount; i++) {
 		const int textW = getTalkTextWidth(kMenuItems[i]);
 		const int textX = getRebel1MenuCenteredX(textW);
 		const int y = 0x3c + i * kRA1MenuRowH;
@@ -689,6 +892,45 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	}
 }
 
+void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int height) {
+	auto makeTalkText = [](const char *text) {
+		Common::String out("<");
+		out += text;
+		return out;
+	};
+	auto drawCenteredTalkText = [&](int y, const char *text) {
+		Common::String styled = makeTalkText(text);
+		const int textW = getFontBankStringWidth(styled.c_str());
+		drawFontBankString(dst, pitch, width, height,
+			getRebel1MenuCenteredX(textW), y, styled.c_str());
+	};
+	auto drawCenteredRawChar = [&](int centerX, int y, char ch) {
+		char text[2] = { ch, '\0' };
+		const int textW = getFontBankStringWidth(text);
+		drawFontBankString(dst, pitch, width, height, centerX - textW / 2, y, text);
+	};
+
+	if (_textEntryPasscodeMode) {
+		drawCenteredTalkText(0x4b, "ENTER PASSCODE");
+		drawCenteredTalkText(0x5f, _textEntryBuffer);
+	} else {
+		char scoreText[40];
+		Common::sprintf_s(scoreText, "SCORE: %ld", (long)_score);
+		drawCenteredTalkText(0x37, scoreText);
+		drawCenteredTalkText(0x4b, "NEW HIGH SCORE");
+		drawCenteredTalkText(0x5f, _textEntryBuffer);
+	}
+
+	const char *pickerChars = getRebel1TextEntryPickerChars(_textEntryPasscodeMode);
+	const int pickerCount = getRebel1TextEntryPickerCount(_textEntryPasscodeMode);
+	const int prevIndex = (_textEntryPickerIndex + pickerCount - 1) % pickerCount;
+	const int nextIndex = (_textEntryPickerIndex + 1) % pickerCount;
+	drawCenteredRawChar(0x91 + _textEntryPickerOffsetX, 0x6e, pickerChars[prevIndex]);
+	drawCenteredRawChar(0xa0 + _textEntryPickerOffsetX, 0x6e, pickerChars[_textEntryPickerIndex]);
+	drawCenteredRawChar(0xaf + _textEntryPickerOffsetX, 0x6e, pickerChars[nextIndex]);
+	_textEntryPickerOffsetX = 0;
+}
+
 int InsaneRebel1::runMainMenu() {
 	debug(1, "InsaneRebel1: Main menu");
 
@@ -702,13 +944,84 @@ int InsaneRebel1::runMainMenu() {
 		_menuActive = false;
 
 		if (_vm->shouldQuit())
-			return 5;
+			return kRA1MainMenuItemCount;
 
 		if (_menuConfirmed)
 			return _menuSelection + 1;
 	}
 
-	return 5;
+	return kRA1MainMenuItemCount;
+}
+
+int InsaneRebel1::runPasscodeEntryDialog() {
+	beginTextEntry(true);
+
+	while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+	}
+
+	if (_vm->shouldQuit() || _textEntryCanceled)
+		return 0;
+
+	for (int i = 1; i <= kRA1NumLevels; i++) {
+		const char *password = getChapterCompletePassword(i);
+		if (password && !scumm_stricmp(_textEntryBuffer, password)) {
+			const int targetLevel = getRebel1PasscodeStartLevel(i);
+			if (targetLevel == 0)
+				return 0;
+
+			_difficulty = getRebel1PasscodeDifficulty(i);
+			_maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, i);
+			if (targetLevel <= kRA1NumLevels)
+				_startLevel = targetLevel;
+			debug(1, "RA1 passcode accepted: slot=%d password=%s difficulty=%d target=%d",
+				i, password, _difficulty, targetLevel);
+			return targetLevel;
+		}
+	}
+
+	debug(1, "RA1 passcode rejected: '%s'", _textEntryBuffer);
+	return 0;
+}
+
+bool InsaneRebel1::runHighScoreNameEntry() {
+	int slot = 0;
+	while (slot < kHighScoreCount && _highScores[slot].score >= _score)
+		slot++;
+	if (slot >= kHighScoreCount)
+		return false;
+
+	for (int i = kHighScoreCount - 1; i > slot; i--)
+		_highScores[i] = _highScores[i - 1];
+
+	_highScores[slot].score = _score;
+	_highScores[slot].difficulty = _difficulty;
+	Common::strlcpy(_highScores[slot].name, "<", sizeof(_highScores[slot].name));
+	_highScoreEntryIndex = slot;
+
+	beginTextEntry(false);
+	while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
+		_menuActive = true;
+		_menuConfirmed = false;
+		_menuFrameCounter = 0;
+		clearVideoBuffer();
+		playCinematic("OPEN/O1OPTION.ANM");
+		_menuActive = false;
+	}
+
+	Common::String storedName("<");
+	storedName += _textEntryBuffer;
+	Common::strlcpy(_highScores[slot].name, storedName.c_str(), sizeof(_highScores[slot].name));
+	_highScores[slot].difficulty = _difficulty;
+	_highScoreEntryIndex = -1;
+	debug(1, "RA1 high score inserted: slot=%d name=%s score=%ld difficulty=%d",
+		slot, _highScores[slot].name, (long)_highScores[slot].score, _highScores[slot].difficulty);
+	return true;
 }
 
 void InsaneRebel1::runOptionsMenu() {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 39861afee1f..efa8e64e700 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -354,6 +354,15 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		_highScores[i].difficulty = kDefaultScores[i].difficulty;
 	}
 	_highScoresActive = false;
+	_textEntryActive = false;
+	_textEntryPasscodeMode = false;
+	_textEntryDone = false;
+	_textEntryCanceled = false;
+	_textEntryPickerIndex = 0;
+	_textEntryPickerOffsetX = 0;
+	_textEntryMaxChars = 0;
+	_highScoreEntryIndex = -1;
+	memset(_textEntryBuffer, 0, sizeof(_textEntryBuffer));
 
 	// Shooting/targeting state
 	_playerFired = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d285f398e2d..05f88d954c4 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -113,7 +113,8 @@ private:
 	void clearVideoBuffer();
 
 	// Main menu loop on O1OPTION.ANM background (0x15968)
-	// Returns: 1=Start New Game, 2=Game Options, 3=Level Select, 4=Continue Demo, 5=Exit
+	// Returns: 1=Start New Game, 2=Game Options, 3=Enter Passcode,
+	// 4=Level Select, 5=Continue Demo, 6=Exit
 	int runMainMenu();
 	int runLevelSelectMenu();
 
@@ -161,6 +162,7 @@ private:
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
+	void renderTextEntryOverlay(byte *dst, int pitch, int width, int height);
 	void renderExplosions(byte *dst, int pitch, int width, int height);
 	void renderTargetBoxes(byte *dst, int pitch, int width, int height);
 	void renderTargeting(byte *dst, int pitch, int width, int height);
@@ -402,7 +404,7 @@ private:
 	byte _deathCauseIndicator;   // 0x772E: non-zero = player died; selects death animation variant
 	byte _hudRenderFlag;         // 0x7600: 0xFF when HUD should render (set by combat mode handlers)
 	byte _hudDirtyFlag;          // 0x7601: 0xFF after HUD redraw (set by renderHUD)
-	int16 _maxChapterUnlocked;   // 0x7730: highest chapter with valid passcode (0=none, set on completion)
+	int16 _maxChapterUnlocked;   // 0x7730: highest unlocked passcode slot (0=none)
 
 	static const int16 kMaxHealth = 98;
 	static const int16 kDeathTimerInit = 30;
@@ -451,11 +453,19 @@ private:
 
 	// Main menu / options state
 	void runOptionsMenu();
+	int runPasscodeEntryDialog();
+	bool runHighScoreNameEntry();
 	bool handleControllerMenuAction(ScummAction action);
 	bool handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY);
+	bool handleTextEntryAction(ScummAction action);
+	bool handleTextEntryKey(const Common::Event &event);
+	void beginTextEntry(bool passcodeMode);
+	void finishTextEntry(bool canceled);
+	void selectTextEntryChar();
+	const char *getChapterCompletePassword(int passwordIndex) const;
 	bool _menuActive;
 	bool _menuConfirmed;
-	int _menuSelection; // 0..4 maps to return values 1..5
+	int _menuSelection; // 0..5 maps to return values 1..6
 	int _menuFrameCounter;
 
 	// Options submenu state — RunGameOptionsMenu (0x14B42)
@@ -486,6 +496,18 @@ private:
 	bool _highScoresActive;  // True when showing TOP PILOTS overlay
 	void showHighScores();
 
+	// Character picker shared by RunPasscodeEntryDialog and RunHighScoreNameEntry.
+	static const int kTextEntryBufferSize = 20;
+	bool _textEntryActive;
+	bool _textEntryPasscodeMode;
+	bool _textEntryDone;
+	bool _textEntryCanceled;
+	int _textEntryPickerIndex;
+	int _textEntryPickerOffsetX;
+	int _textEntryMaxChars;
+	int _highScoreEntryIndex;
+	char _textEntryBuffer[kTextEntryBufferSize];
+
 	// Shooting state — FUN_1CCA0 (0x1CCA0)
 	bool _playerFired;       // 0x7570: current fire-button state
 	int16 _fireCooldown;     // 0x757C: previous-frame fire-button state (edge gate)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 7b552325774..b051339717b 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1274,14 +1274,21 @@ void InsaneRebel1::beginChapterSummaryOverlay(int revealOffsetFromEnd, int stopO
 	}
 }
 
-// The DOS table is stored as 15 XOR-0xAA encoded, 20-byte passcode slots at
-// DS:0x00A4. RunChapterCompleteSummaryScreen indexes it with passwordIndex-1.
+// ScummVM keeps the passcodes in clear text. The DOS table is XOR-0xAA
+// encoded in 15 20-byte slots at DS:0x00A4.
 static const char *const kChapterCompletePasswords[] = {
 	"FALCON", "BIGGS", "ACKBAR", "ANOAT", "KAIBURR",
 	"FORNAX", "YUZZEM", "MYNOCK", "BESPIN", "BRIGIA",
 	"DAGOBAH", "KESSEL", "GREEDO", "MIMBAN", "ORGANA"
 };
 
+const char *InsaneRebel1::getChapterCompletePassword(int passwordIndex) const {
+	if (passwordIndex < 1 || passwordIndex > (int)ARRAYSIZE(kChapterCompletePasswords))
+		return nullptr;
+
+	return kChapterCompletePasswords[passwordIndex - 1];
+}
+
 // drawChapterSummaryOverlay — RunChapterCompleteSummaryScreen (0x15E42), shared by
 // the RA1 runlevel flows that call it. This helper is a ScummVM-side extraction;
 // the original pumps frontend frames from the runlevel after queueing the END ANM.
@@ -1325,12 +1332,13 @@ void InsaneRebel1::drawChapterSummaryOverlay(byte *dst, int pitch, int width, in
 	}
 
 	if (_chapterSummary.passwordIndex > 0 &&
-			_chapterSummary.passwordIndex <= ARRAYSIZE(kChapterCompletePasswords) &&
 			revealBaseFrame + 10 < curFrame) {
-		char passwordText[40];
-		Common::sprintf_s(passwordText, "Password: %s",
-			kChapterCompletePasswords[_chapterSummary.passwordIndex - 1]);
-		drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
+		const char *password = getChapterCompletePassword(_chapterSummary.passwordIndex);
+		if (password) {
+			char passwordText[40];
+			Common::sprintf_s(passwordText, "Password: %s", password);
+			drawCenteredRebel1String(this, dst, pitch, width, height, centerX, 0x73, passwordText);
+		}
 	}
 }
 
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 6f091cf8a9f..08b2d4991e5 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -262,7 +262,8 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
 	playCinematic(filename);
 	_chapterSummary.active = false;
 	_score += _tuning.levelPts + bonusValue1 + bonusValue2;
-	_maxChapterUnlocked = MAX(_maxChapterUnlocked, unlockedChapter);
+	if (passwordIndex != 0)
+		_maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, passwordIndex);
 }
 
 void InsaneRebel1::clearVideoBuffer() {
@@ -556,7 +557,7 @@ bool InsaneRebel1::runLevel3() {
 
 		if (_health >= 0) {
 			playChapterCompleteCinematic("LVL3/L3END.ANM", 3, 0x69, 5,
-				nullptr, nullptr, 0, nullptr, nullptr, 0, 3);
+				nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 1);
 			return !_vm->shouldQuit();
 		}
 
@@ -836,7 +837,7 @@ bool InsaneRebel1::runLevel6() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x27, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
 			playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
-				" ", accuracyText, bonus, nullptr, nullptr, 0, 6);
+				" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 4);
 			return !_vm->shouldQuit();
 		}
 
@@ -1331,7 +1332,7 @@ bool InsaneRebel1::runLevel10() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x3E, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x3D, 0x32, _tuning.bonus);
 			playChapterCompleteCinematic("LVL10/L10END.ANM", 10, 0x4B, 5,
-				" ", accuracyText, bonus, nullptr, nullptr, 0, 10);
+				" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 7);
 			return !_vm->shouldQuit();
 		}
 
@@ -1711,7 +1712,7 @@ bool InsaneRebel1::runLevel14() {
 
 		if (_health >= 0) {
 			playChapterCompleteCinematic("LVL14/L14END.ANM", 14, 0x69, 5,
-				nullptr, nullptr, 0, nullptr, nullptr, 0, 14);
+				nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 10);
 			return !_vm->shouldQuit();
 		}
 
@@ -1827,7 +1828,7 @@ bool InsaneRebel1::runLevel15() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, false);
 			playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
 				"Part I", accuracyText, targetBonus,
-				"Part II", "Torpedo on mark", 10000, 15);
+				"Part II", "Torpedo on mark", 10000, _difficulty + 13);
 			return !_vm->shouldQuit();
 		}
 
@@ -1867,6 +1868,27 @@ void InsaneRebel1::runGame() {
 		&InsaneRebel1::runLevel14,
 		&InsaneRebel1::runLevel15
 	};
+	const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
+	auto runLevelsFrom = [&](int startLevel) {
+		const int firstLevel = CLIP<int>(startLevel, 1, numLevels);
+		bool completed = true;
+		_health = kMaxHealth;
+		_lives = 3;
+		_score = 0;
+		_prevScore = 0;
+
+		for (int level = firstLevel;
+			 level <= numLevels && completed && !_vm->shouldQuit();
+			 ++level) {
+			completed = (this->*kLevelRunners[level - 1])();
+			if (completed && level < numLevels)
+				_startLevel = level + 1;
+		}
+
+		if (!_vm->shouldQuit())
+			runHighScoreNameEntry();
+		_currentLevel = 0;
+	};
 
 	// Play intro sequence (logo + opening)
 	playIntroSequence();
@@ -1882,18 +1904,7 @@ void InsaneRebel1::runGame() {
 		switch (menuResult) {
 		case 1: {
 			// START NEW GAME — sequential play from _startLevel
-			const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
-			const int startLevel = CLIP<int>(_startLevel, 1, numLevels);
-			bool completed = true;
-
-			for (int level = startLevel;
-				 level <= numLevels && completed && !_vm->shouldQuit();
-				 ++level) {
-				completed = (this->*kLevelRunners[level - 1])();
-				if (completed && level < numLevels)
-					_startLevel = level + 1;
-			}
-			_currentLevel = 0;
+			runLevelsFrom(_startLevel);
 			break;
 		}
 		case 2:
@@ -1901,23 +1912,48 @@ void InsaneRebel1::runGame() {
 			runOptionsMenu();
 			break;
 		case 3: {
-			// Level Select — launch directly
+			// Enter Passcode — original RunPasscodeEntryDialog starts the
+			// game from the decoded chapter when a valid passcode is entered.
+			const int passcodeLevel = runPasscodeEntryDialog();
+			if (passcodeLevel >= 1 && passcodeLevel <= numLevels)
+				runLevelsFrom(passcodeLevel);
+			else if (passcodeLevel == numLevels + 1) {
+				_health = kMaxHealth;
+				_lives = 3;
+				_score = 0;
+				_prevScore = 0;
+				playCinematic("FIN/FNFINAL.ANM");
+				if (!_vm->shouldQuit())
+					runHighScoreNameEntry();
+				_currentLevel = 0;
+			}
+			break;
+		}
+		case 4: {
+			// Level Select — ScummVM-only direct launcher, kept separate from
+			// original passcode-start flow so it does not force sequential play.
 			int selectedLevel = runLevelSelectMenu();
-			if (selectedLevel >= 1 && selectedLevel <= (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]))) {
+			if (selectedLevel >= 1 && selectedLevel <= numLevels) {
 				_startLevel = selectedLevel;
+				_health = kMaxHealth;
+				_lives = 3;
+				_score = 0;
+				_prevScore = 0;
 				(this->*kLevelRunners[selectedLevel - 1])();
+				if (!_vm->shouldQuit())
+					runHighScoreNameEntry();
 				_currentLevel = 0;
 			}
 			break;
 		}
-		case 4:
+		case 5:
 			// CONTINUE DEMO — attract mode.
 			// Original shows TOP PILOTS (O1SCORE.ANM) then loops O1OPEN.ANM.
 			showHighScores();
 			if (!_vm->shouldQuit())
 				playCinematic("OPEN/O1OPEN.ANM");
 			break;
-		case 5:
+		case 6:
 			// Exit
 			return;
 		default:


Commit: d9df04cc213130d49b54d77cecaa4ac7244bc9d4
    https://github.com/scummvm/scummvm/commit/d9df04cc213130d49b54d77cecaa4ac7244bc9d4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:07+02:00

Commit Message:
SCUMM: RA1: Add missing cutscenes

Changed paths:
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 05f88d954c4..66bcb96ef76 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -141,6 +141,7 @@ private:
 	// Play a passive cinematic (no game callback, skippable)
 	// startFrame > 0: fast-forward (decode without display) to that frame
 	void playCinematic(const char *filename, int32 startFrame = 0);
+	void playLevelTransitionCutscene(int level);
 	void playChapterCompleteCinematic(const char *filename, int16 unlockedChapter,
 		int revealOffsetFromEnd, int stopOffsetFromEnd,
 		const char *bonusLabel1 = nullptr, const char *detailText1 = nullptr, int bonusValue1 = 0,
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 08b2d4991e5..fc67a0ec0d6 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -266,6 +266,38 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
 		_maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, passwordIndex);
 }
 
+void InsaneRebel1::playLevelTransitionCutscene(int level) {
+	switch (level) {
+	case 4:
+		// Original successor path 0x6d7d, reached after chapter 3 and by the
+		// FALCON/BIGGS/WEDGE passcode group. This is separate from RunLevel4Flow
+		// (0x6ee4), which starts with LVL4/L4INTRO.ANM.
+		playCinematic("CUT1/C1BLOCK.ANM");
+		if (_vm->shouldQuit())
+			break;
+		playCinematic("CUT1/C1DARTH1.ANM");
+		if (_vm->shouldQuit())
+			break;
+		playCinematic("CUT1/C1C3PO.ANM");
+		if (_vm->shouldQuit())
+			break;
+		playCinematic("CUT1/C1DARTH2.ANM");
+		break;
+	case 7:
+		// Original successor path 0x7d52, reached after chapter 6 and by the
+		// chapter-6 passcode group, before RunLevel7Flow (0x7dcc).
+		playCinematic("CUT2/C2CUT2.ANM");
+		break;
+	case 11:
+		// Original successor path 0x9f3a, reached after chapter 10 and by the
+		// chapter-10 passcode group, before RunLevel11Flow (0x9f9f).
+		playCinematic("CUT3/C3BOOM.ANM");
+		break;
+	default:
+		break;
+	}
+}
+
 void InsaneRebel1::clearVideoBuffer() {
 	if (_vm->_screenWidth <= 0 || _vm->_screenHeight <= 0)
 		return;
@@ -1872,6 +1904,7 @@ void InsaneRebel1::runGame() {
 	auto runLevelsFrom = [&](int startLevel) {
 		const int firstLevel = CLIP<int>(startLevel, 1, numLevels);
 		bool completed = true;
+		int lastCompletedLevel = 0;
 		_health = kMaxHealth;
 		_lives = 3;
 		_score = 0;
@@ -1880,11 +1913,20 @@ void InsaneRebel1::runGame() {
 		for (int level = firstLevel;
 			 level <= numLevels && completed && !_vm->shouldQuit();
 			 ++level) {
+			playLevelTransitionCutscene(level);
+			if (_vm->shouldQuit())
+				break;
+
 			completed = (this->*kLevelRunners[level - 1])();
-			if (completed && level < numLevels)
-				_startLevel = level + 1;
+			if (completed) {
+				lastCompletedLevel = level;
+				if (level < numLevels)
+					_startLevel = level + 1;
+			}
 		}
 
+		if (!_vm->shouldQuit() && completed && lastCompletedLevel == numLevels)
+			playCinematic("FIN/FNFINAL.ANM");
 		if (!_vm->shouldQuit())
 			runHighScoreNameEntry();
 		_currentLevel = 0;
@@ -1930,20 +1972,11 @@ void InsaneRebel1::runGame() {
 			break;
 		}
 		case 4: {
-			// Level Select — ScummVM-only direct launcher, kept separate from
-			// original passcode-start flow so it does not force sequential play.
+			// Level Select — ScummVM-only start point. Continue through the
+			// original successor flow so post-level cinematics still play.
 			int selectedLevel = runLevelSelectMenu();
-			if (selectedLevel >= 1 && selectedLevel <= numLevels) {
-				_startLevel = selectedLevel;
-				_health = kMaxHealth;
-				_lives = 3;
-				_score = 0;
-				_prevScore = 0;
-				(this->*kLevelRunners[selectedLevel - 1])();
-				if (!_vm->shouldQuit())
-					runHighScoreNameEntry();
-				_currentLevel = 0;
-			}
+			if (selectedLevel >= 1 && selectedLevel <= numLevels)
+				runLevelsFrom(selectedLevel);
 			break;
 		}
 		case 5:


Commit: b55c73fea517249fc047f3b3c810863957457f61
    https://github.com/scummvm/scummvm/commit/b55c73fea517249fc047f3b3c810863957457f61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:08+02:00

Commit Message:
SCUMM: RA1: Avoid crash in handleGameAdjustCoords

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 4d828c8356c..acca9e27cc2 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -759,6 +759,15 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
 	if (codec == SMUSH_CODEC_SKIP_RLE || codec == SMUSH_CODEC_RA1_SCATTER)
 		return false;
 
+	// RA1 block codecs are column-major tile streams, not row-prefixed RLE
+	// streams. Preserve the source data and let smushDecodeRA1Block() consume
+	// offscreen tiles while clipping destination pixels.
+	if (codec == SMUSH_CODEC_RA1_DELTA || codec == SMUSH_CODEC_RA1_BLOCK) {
+		left += _fobjOffsetX;
+		top += _fobjOffsetY;
+		return false;
+	}
+
 	// RA1 codec 21 is source-X sensitive: generic left clipping would reduce
 	// the destination width without skipping the corresponding source columns.
 	// Keep only the global FOBJ offset here and let the codec clip each run.


Commit: c29ef27990f72892388ee0242b41f1348fbb1a2f
    https://github.com/scummvm/scummvm/commit/c29ef27990f72892388ee0242b41f1348fbb1a2f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:08+02:00

Commit Message:
SCUMM: RA1: Fix level 14 frame object state checks

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 91c31357ed1..7d8ac754669 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -504,40 +504,33 @@ bool InsaneRebel1::isFrameObjectPrimarySet(int16 objectId) const {
 	return (_frameObjectState[byteIndex] & bit) != 0;
 }
 
+bool InsaneRebel1::areFrameObjectPrimaryBitsSet(int byteIndex, byte mask) const {
+	if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+		return false;
+
+	return (_frameObjectState[byteIndex] & mask) == mask;
+}
+
+void InsaneRebel1::clearFrameObjectPrimaryBits(int byteIndex, byte mask) {
+	if (byteIndex < 0 || byteIndex >= 0x96 || byteIndex >= kFrameObjectStateBytes)
+		return;
+
+	_frameObjectState[byteIndex] &= ~mask;
+}
+
 // Port helpers for RunLevel14Flow. The original checks DAT_7614..7616 and
 // DAT_7605..7606 inline, then increments a local 60-frame completion counter.
 bool InsaneRebel1::areLevel14Phase1TargetsDestroyed() const {
-	return isFrameObjectPrimarySet(168) &&
-		isFrameObjectPrimarySet(169) &&
-		isFrameObjectPrimarySet(170) &&
-		isFrameObjectPrimarySet(171) &&
-		isFrameObjectPrimarySet(172) &&
-		isFrameObjectPrimarySet(173) &&
-		isFrameObjectPrimarySet(174) &&
-		isFrameObjectPrimarySet(175) &&
-		isFrameObjectPrimarySet(176) &&
-		isFrameObjectPrimarySet(177) &&
-		isFrameObjectPrimarySet(178) &&
-		isFrameObjectPrimarySet(179) &&
-		isFrameObjectPrimarySet(180) &&
-		isFrameObjectPrimarySet(181) &&
-		isFrameObjectPrimarySet(182) &&
-		isFrameObjectPrimarySet(183);
+	// g_gameplayPhaseFlags starts at primary byte 0; DAT_7614 is byte 0x12.
+	return areFrameObjectPrimaryBitsSet(0x12, 0x01) &&
+		areFrameObjectPrimaryBitsSet(0x13, 0xFF) &&
+		areFrameObjectPrimaryBitsSet(0x14, 0xFE);
 }
 
 bool InsaneRebel1::areLevel14Phase2TargetsDestroyed() const {
-	return isFrameObjectPrimarySet(45) &&
-		isFrameObjectPrimarySet(46) &&
-		isFrameObjectPrimarySet(47) &&
-		isFrameObjectPrimarySet(48) &&
-		isFrameObjectPrimarySet(49) &&
-		isFrameObjectPrimarySet(50) &&
-		isFrameObjectPrimarySet(51) &&
-		isFrameObjectPrimarySet(52) &&
-		isFrameObjectPrimarySet(53) &&
-		isFrameObjectPrimarySet(54) &&
-		isFrameObjectPrimarySet(55) &&
-		isFrameObjectPrimarySet(56);
+	// Phase 2 checks DAT_7605 low nibble and all of DAT_7606.
+	return areFrameObjectPrimaryBitsSet(0x03, 0x0F) &&
+		areFrameObjectPrimaryBitsSet(0x04, 0xFF);
 }
 
 bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top, int16 width, int16 height,
@@ -1714,15 +1707,15 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	}
 
 	// Level 15 Phase 2: enable torpedo at frame 0x18A, expose the protected
-	// target IDs used by the original flow, and finish when object-state bit
-	// 0x7602 & 2 becomes set.
+	// target IDs used by the original flow, and finish when
+	// g_gameplayPhaseFlags & 2 becomes set.
 	if (_currentLevel == 14 && _levelGameplayPhase == 2) {
 		if (_frameCounter == 0x18A) {
 			_gameplayFlags75ff |= 2;
 			_protectedTargetA = 0x67;
 			_protectedTargetB = 0x69;
 		}
-		if (isFrameObjectPrimarySet(7)) {
+		if (areFrameObjectPrimaryBitsSet(0, 0x02)) {
 			_torpedoFired = true;
 			_vm->_smushVideoShouldFinish = true;
 		}
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 66bcb96ef76..600a8567a28 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -185,6 +185,8 @@ private:
 	void updateGostSlotPosition(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
 	void applyFrameObjectHitState(int16 targetIdx);
 	bool isFrameObjectPrimarySet(int16 objectId) const;
+	bool areFrameObjectPrimaryBitsSet(int byteIndex, byte mask) const;
+	void clearFrameObjectPrimaryBits(int byteIndex, byte mask);
 	bool areLevel14Phase1TargetsDestroyed() const;
 	bool areLevel14Phase2TargetsDestroyed() const;
 
@@ -580,7 +582,7 @@ private:
 	int16 _level14SuccessFrames; // RunLevel14Flow: 60-frame hold after required targets are destroyed
 
 	// Level 15 torpedo success latch. The original derives this from
-	// g_gameplayPhaseFlags bit 1, which is the primary object-state bit for object 7.
+	// g_gameplayPhaseFlags bit 1, which is _frameObjectState primary byte 0 bit 0x02.
 	bool _torpedoFired;
 
 	// Level 8 walker-specific state — RunLevel8Flow (0x18546)
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index b051339717b..5943b1c6b91 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -841,9 +841,10 @@ void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
 	// Original after PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860, maxFrame-0x0F, 1, -1):
 	//   g_extendedPhaseFlags &= 0xFA;
 	//   DAT_7604 &= 0xBF;
-	// In the port's object-state storage those are the first and third primary bytes.
-	_frameObjectState[0] &= 0xFA;
-	_frameObjectState[2] &= 0xBF;
+	// g_extendedPhaseFlags is primary byte 1 because g_gameplayPhaseFlags starts
+	// at byte 0 of the original frame-object state array.
+	clearFrameObjectPrimaryBits(1, 0x05);
+	clearFrameObjectPrimaryBits(2, 0x40);
 	_vm->_smushVideoShouldFinish = true;
 }
 


Commit: e5324c60805d9b798ae568e76726c25fb6f465d0
    https://github.com/scummvm/scummvm/commit/e5324c60805d9b798ae568e76726c25fb6f465d0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:08+02:00

Commit Message:
SCUMM: RA1: Correct level names and tuning selectors

Changed paths:
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index efa8e64e700..90cd418cf80 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -40,115 +40,115 @@ namespace Scumm {
 // 21 sub-levels x 3 difficulties x 13 fields
 // Fields: roll, lift, slide, drift, snap, miss, wham, shot, kill, time, levelPts, bonus, flags
 const int16 kTuningTable[21][3][13] = {
-	// Sub-level 0: "1A" (Flight Training - canyon flight)
+	// Sub-level 0: "1A" (Flight Training part 1)
 	{
 		{ 100, 100,  60, 110,   0,   0,  15,   0,   0,   5,  500,  100, 2048 },  // Easy
 		{ 100, 105,  60, 115,   0,   0,  25,   0,   0,   5, 1000,  200, 2048 },  // Normal
 		{ 105, 110,  65, 120,   0,   0,  30,   0,   0,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Sub-level 1: "1B" (Flight Training - asteroid flight)
+	// Sub-level 1: "1B" (Flight Training part 2)
 	{
 		{ 100,  16, 120,   0,   7,   0,  15,   0,  25,   5,  500,  100, 3072 },  // Easy
 		{ 100,  18, 120,   0,   5,   0,  20,   0,  50,   5, 1000,  200, 3072 },  // Normal
 		{ 100,  20, 150,   0,   1,   0,  25,   0,  75,  10, 1500,  500, 3074 },  // Hard
 	},
-	// Sub-level 2: "2" (Planet Kolaador)
+	// Sub-level 2: "2" (Asteroid Field Training)
 	{
 		{   0,   0,   0,   0,   4,  15,  25,   0,  25,  10,  500,  100, 2048 },  // Easy
 		{   0,   0,   0,   0,   2,  18,  30,   0,  50,  10, 1000,  200, 2048 },  // Normal
 		{   0,   0,   0,   0,   0,  20,  35,   0,  75,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Sub-level 3: "3" (Star Destroyer Attack)
+	// Sub-level 3: "3" (Planet Kolaador)
 	{
 		{  70, 100, 150,  90,   0,   0,  20,   0,   0,   5, 1000,  100, 2048 },  // Easy
 		{  72, 105, 155, 105,   0,   0,  25,   0,   0,   5, 2000,  200, 2048 },  // Normal
 		{  75, 110, 160, 110,   0,   0,  28,   0,   0,  10, 3000,  500, 2050 },  // Hard
 	},
-	// Sub-level 4: "4A" (Tatooine Attack)
+	// Sub-level 4: "4A" (Star Destroyer Attack part 1)
 	{
 		{   0,   0,   0,   0,   2,  11,   0,   4,  25,   5,  500,  750, 2048 },  // Easy
 		{   0,   0,   0,   0,   1,  25,   0,   6,  50,   5, 1000, 1500, 2048 },  // Normal
 		{   0,   0,   0,   0,   1,  28,   0,   6,  75,  10, 1500, 2000, 2050 },  // Hard
 	},
-	// Sub-level 5: "4B" (Tatooine Attack part 2)
+	// Sub-level 5: "4B" (Star Destroyer Attack part 2)
 	{
 		{   0,   0,   0,   0,   3,  20,   0,   2,  50,   5,  500,  750, 2064 },  // Easy
 		{   0,   0,   0,   0,   1,  25,   0,   5, 100,   5, 1000, 1500, 2064 },  // Normal
 		{   0,   0,   0,   0,   1,  28,   0,   6, 200,  10, 1500, 2000, 2064 },  // Hard
 	},
-	// Sub-level 6: "5A" (Imperial Probe Droids - speeder)
+	// Sub-level 6: "5A" (Tatooine Attack part 1)
 	{
 		{  70, 150,  50,  25,  10,   0,  20,   0,  25,   5,  500,   15, 3072 },  // Easy
 		{  72, 165, 155,  30,   8,   0,  30,   0,  50,   5, 1000,   30, 3072 },  // Normal
 		{ 110, 190,  55,  65,   3,   0,  33,   0,  75,  10, 1500,   75, 3074 },  // Hard
 	},
-	// Sub-level 7: "5B" (Imperial Walkers)
+	// Sub-level 7: "5B" (Tatooine Attack part 2)
 	{
 		{   0,   0,   0,   0,   5,   0,   0,   2,  25,   0,  500,   15, 2048 },  // Easy
 		{   0,   0,   0,   0,   3,   0,   0,   5,  50,   5, 1000,   30, 2048 },  // Normal
 		{   0,   0,   0,   0,   1,   0,   0,   6,  75,  10, 1500,   75, 2050 },  // Hard
 	},
-	// Sub-level 8: "6" (Stormtroopers)
+	// Sub-level 8: "6" (Asteroid Field Chase)
 	{
 		{   0,   0,   0,   0,   2,  20,  20,   0,  25,   5,  500,  100, 2048 },  // Easy
 		{   0,   0,   0,   0,   1,  25,  30,   0,  50,   5, 1000,  200, 2048 },  // Normal
 		{   0,   0,   0,   0,   0,  28,  33,   0,  75,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Sub-level 9: "7" (Protect Rebel Transport)
+	// Sub-level 9: "7" (Imperial Probe Droids)
 	{
 		{ 100, 150, 150,  25,   7,   0,  12,   2,  50,   5,  500,  100, 3072 },  // Easy
 		{ 100, 160, 200,  35,   4,   0,  30,   4, 100,   5, 1000,  200, 3072 },  // Normal
 		{ 100, 180, 250,  50,   3,   0,  33,   5, 100,  10, 1500,  500, 3074 },  // Hard
 	},
-	// Sub-level 10: "8" (Death Star surface)
+	// Sub-level 10: "8" (Imperial Walkers)
 	{
 		{   0,   0,   0,   0,   0,   0,  30,   0,  25,   0, 1000,  100, 3074 },  // Easy
 		{   0,   0,   0,   0,   0,   0,  36,   0,  50,   0, 2000,  200, 3074 },  // Normal
 		{   0,   0,   0,   0,   0,   0,  39,   0,  75,   0, 3000,  500, 3074 },  // Hard
 	},
-	// Sub-level 11: "9A" (Death Star turrets part 1)
+	// Sub-level 11: "9A" (Stormtroopers part 1)
 	{
 		{   0,   0,   0,   0,   4,   0,   0,  15,  25,   0, 1000,  100, 3074 },  // Easy
 		{   0,   0,   0,   0,   2,   0,   0,  25,  50,   0, 2000,  200, 3078 },  // Normal
 		{   0,   0,   0,   0,   0,   0,   0,  30,  75,   0, 3000,  500, 3078 },  // Hard
 	},
-	// Sub-level 12: "9B" (Death Star turrets part 2)
+	// Sub-level 12: "9B" (Stormtroopers part 2)
 	{
 		{   0,   0,   0,   0,   0,   0,   0,  15,  25,   0, 1000,  100, 3098 },  // Easy
 		{   0,   0,   0,   0,   0,   0,   0,  25,  50,   0, 2000,  200, 3098 },  // Normal
 		{   0,   0,   0,   0,   0,   0,   0,  30,  75,   0, 3000,  500, 3098 },  // Hard
 	},
-	// Sub-level 13: "10" (Death Star trench approach)
+	// Sub-level 13: "10" (Protect Rebel Transport)
 	{
 		{   0,   0,   0,   0,   3,  10,   0,   5,  25,   5,  500,  200, 2048 },  // Easy
 		{   0,   0,   0,   0,   1,  16,   0,   5,  50,   5, 1000,  400, 2048 },  // Normal
 		{   0,   0,   0,   0,   0,  18,   0,   7,  75,  10, 1500, 1000, 2050 },  // Hard
 	},
-	// Sub-level 14: "11" (Death Star trench - speeder)
+	// Sub-level 14: "11" (Yavin Training)
 	{
 		{  70, 150, 150,  25,  12,   0,  30,   0,  50,   5,  500,  200, 3072 },  // Easy
 		{  72, 165, 155,  30,   7,   0,  36,   0,  50,   5, 1000,  400, 3072 },  // Normal
 		{  75, 170, 160,  33,   3,   0,  39,   0,  75,  10, 1500, 1000, 3074 },  // Hard
 	},
-	// Sub-level 15: "12" (Death Star trench run)
+	// Sub-level 15: "12" (TIE Attack)
 	{
 		{   0,   0,   0,   0,   4,  13,   0,   5,  25,   5,  500,  100, 2048 },  // Easy
 		{   0,   0,   0,   0,   2,  20,   0,   5,  50,   5, 1000,  200, 2048 },  // Normal
 		{   0,   0,   0,   0,   0,  23,   0,   5,  75,  10, 1500,  500, 2050 },  // Hard
 	},
-	// Sub-level 16: "13" (Asteroid belt chase)
+	// Sub-level 16: "13" (Death Star Surface)
 	{
 		{ 100,  16, 120,   0,  20,   0,  35,   8,  75,   5,  500,  100, 3072 },  // Easy
 		{ 100,  18, 120,   0,  18,   0,  36,  10, 100,   5, 1000,  200, 3072 },  // Normal
 		{ 100,  20, 150,   0,  15,   0,  39,  12, 200,  10, 1500,  500, 3074 },  // Hard
 	},
-	// Sub-level 17: "14A" (Star Destroyer attack 2 part 1)
+	// Sub-level 17: "14A" (Surface Cannon part 1)
 	{
 		{   0,   0,   0,   0,   0,  20,  35,   8,  25,   0, 1000,  100, 2048 },  // Easy
 		{   0,   0,   0,   0,   0,  27,  36,  12,  50,   0, 2000,  200, 2048 },  // Normal
 		{   0,   0,   0,   0,   0,  28,  39,  12,  75,   0, 3000,  500, 2050 },  // Hard
 	},
-	// Sub-level 18: "14B" (Star Destroyer attack 2 part 2)
+	// Sub-level 18: "14B" (Surface Cannon part 2)
 	{
 		{   0,   0,   0,   0,  10,  20,  35,   8,  25,   0, 1000,  100, 2048 },  // Easy
 		{   0,   0,   0,   0,   5,  25,  36,  10,  50,   0, 2000,  200, 2048 },  // Normal
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index fc67a0ec0d6..2b371a8ae1d 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -613,7 +613,7 @@ bool InsaneRebel1::runLevel4() {
 
 	_currentLevel = 3;
 	loadLevelSprites(4);
-	// DOS RunLevel4Flow launches L4PLAY1/2.ANM with gameplay selector 4.
+	// DOS RunLevel4Flow launches L4PLAY1.ANM with selector 4 and L4PLAY2.ANM with selector 5.
 	loadTuningForLevel(4);
 
 	beginLevelTitleOverlay(3);
@@ -622,6 +622,7 @@ bool InsaneRebel1::runLevel4() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
+		loadTuningForLevel(4);
 		_flyControlMode = 1;
 		_health = kMaxHealth;
 		_damageFlags = 0;
@@ -661,6 +662,7 @@ bool InsaneRebel1::runLevel4() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
+			loadTuningForLevel(5);
 			resetGameplayFlagsFromTuning();
 			_killCount = 0;
 			_levelGameplayPhase = 2;
@@ -902,7 +904,8 @@ bool InsaneRebel1::runLevel7() {
 
 	_currentLevel = 6;
 	loadLevelSprites(7);
-	loadTuningForLevel(6);
+	// DOS RunLevel7Flow starts L7PLAY1.ANM with initLevelFlag=9.
+	loadTuningForLevel(9);
 
 	beginLevelTitleOverlay(6);
 	playCinematic("LVL7/L7INTRO.ANM");
@@ -1124,7 +1127,8 @@ bool InsaneRebel1::runLevel9() {
 
 	_currentLevel = 8;
 	loadLevelSprites(9);
-	loadTuningForLevel(8);
+	// DOS RunLevel9Flow alternates selectors 0x0B and 0x0C across its playable routes.
+	loadTuningForLevel(0x0B);
 
 	beginLevelTitleOverlay(8);
 	playCinematic("LVL9/L9INTRO.ANM");
@@ -1132,6 +1136,7 @@ bool InsaneRebel1::runLevel9() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
+		loadTuningForLevel(0x0B);
 		_flyControlMode = 0;
 		_health = kMaxHealth;
 		_damageFlags = 0;
@@ -1173,6 +1178,7 @@ bool InsaneRebel1::runLevel9() {
 		_avgInputY = 0;
 
 		while (!_vm->shouldQuit()) {
+			loadTuningForLevel(0x0B);
 			playInteractiveVideo("LVL9/L9PLAY1.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1185,6 +1191,7 @@ bool InsaneRebel1::runLevel9() {
 
 			_shipPosX = kRA1CenterX;
 			_posAccumX = 0;
+			loadTuningForLevel(0x0C);
 			playInteractiveVideo("LVL9/L9PLAY2.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1208,6 +1215,7 @@ bool InsaneRebel1::runLevel9() {
 			if (_vm->shouldQuit())
 				return false;
 
+			loadTuningForLevel(0x0B);
 			playInteractiveVideo("LVL9/L9PLAY3A.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1228,6 +1236,7 @@ bool InsaneRebel1::runLevel9() {
 
 			_shipPosX = kRA1CenterX;
 			_posAccumX = 0;
+			loadTuningForLevel(0x0C);
 			playInteractiveVideo("LVL9/L9PLAY4.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1245,6 +1254,7 @@ bool InsaneRebel1::runLevel9() {
 				if (_vm->shouldQuit())
 					return false;
 
+				loadTuningForLevel(0x0B);
 				playInteractiveVideo("LVL9/L9PLAY5.ANM");
 				if (_vm->shouldQuit())
 					return false;
@@ -1257,6 +1267,7 @@ bool InsaneRebel1::runLevel9() {
 
 				_shipPosX = kRA1CenterX;
 				_posAccumX = 0;
+				loadTuningForLevel(0x0C);
 				playInteractiveVideo("LVL9/L9PLAY6.ANM");
 				if (_vm->shouldQuit())
 					return false;
@@ -1281,6 +1292,7 @@ bool InsaneRebel1::runLevel9() {
 					return false;
 			}
 
+			loadTuningForLevel(0x0B);
 			playInteractiveVideo("LVL9/L9PLAY7.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1311,7 +1323,8 @@ bool InsaneRebel1::runLevel10() {
 
 	_currentLevel = 9;
 	loadLevelSprites(10);
-	loadTuningForLevel(9);
+	// DOS RunLevel10Flow starts L10PLAY.ANM with initLevelFlag=0x0D.
+	loadTuningForLevel(0x0D);
 
 	beginLevelTitleOverlay(9);
 	playCinematic("LVL10/L10INTRO.ANM");
@@ -1391,7 +1404,9 @@ bool InsaneRebel1::runLevel11() {
 
 	_currentLevel = 10;
 	loadLevelSprites(11);
-	loadTuningForLevel(10);
+	// DOS RunLevel11Flow starts L11PLAY.ANM with initLevelFlag=0x0E.
+	// Row 10 has zero roll/slide tuning, which prevents horizontal aiming.
+	loadTuningForLevel(0x0E);
 
 	beginLevelTitleOverlay(10);
 	playCinematic("LVL11/L11INTRO.ANM");
@@ -1486,7 +1501,8 @@ bool InsaneRebel1::runLevel12() {
 
 	_currentLevel = 11;
 	loadLevelSprites(12);
-	loadTuningForLevel(11);
+	// DOS RunLevel12Flow starts L12PLAY.ANM with initLevelFlag=0x0F.
+	loadTuningForLevel(0x0F);
 
 	beginLevelTitleOverlay(11);
 	playCinematic("LVL12/L12INTRO.ANM");
@@ -1506,6 +1522,7 @@ bool InsaneRebel1::runLevel12() {
 		_killCount = 0;
 
 		while (!_vm->shouldQuit()) {
+			loadTuningForLevel(0x0F);
 			_frameCounter = 0;
 			_gameCounter = 0;
 			_activeGameOpcode = 0;
@@ -1580,7 +1597,8 @@ bool InsaneRebel1::runLevel13() {
 	_currentLevel = 12;
 	loadLevelSprites(13);
 	loadRA1Nut("LVL13/L13LASR2.NUT", _enemyLaserBank);
-	loadTuningForLevel(12);
+	// DOS RunLevel13Flow starts L13PLAY.ANM with initLevelFlag=0x10.
+	loadTuningForLevel(0x10);
 
 	beginLevelTitleOverlay(12);
 	playCinematic("LVL13/L13INTRO.ANM");
@@ -1661,7 +1679,8 @@ bool InsaneRebel1::runLevel14() {
 
 	_currentLevel = 13;
 	loadLevelSprites(14);
-	loadTuningForLevel(13);
+	// DOS RunLevel14Flow uses selector 0x11 for L14PLAY and 0x12 for L14PLAY2.
+	loadTuningForLevel(0x11);
 
 	beginLevelTitleOverlay(13);
 	playCinematic("LVL14/L14INTRO.ANM");
@@ -1669,6 +1688,7 @@ bool InsaneRebel1::runLevel14() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
+		loadTuningForLevel(0x11);
 		_flyControlMode = 1;
 		_health = kMaxHealth;
 		_damageFlags = 0;
@@ -1716,6 +1736,7 @@ bool InsaneRebel1::runLevel14() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
+			loadTuningForLevel(0x12);
 			resetGameplayFlagsFromTuning();
 			_killCount = 0;
 			_levelGameplayPhase = 2;
@@ -1772,7 +1793,8 @@ bool InsaneRebel1::runLevel15() {
 
 	_currentLevel = 14;
 	loadLevelSprites(15);
-	loadTuningForLevel(14);
+	// DOS RunLevel1GameLoop uses selector 0x13 for L15PLAY1 and 0x14 for L15PLAY2.
+	loadTuningForLevel(0x13);
 
 	beginLevelTitleOverlay(14);
 	playCinematic("LVL15/L15INTRO.ANM");
@@ -1780,6 +1802,7 @@ bool InsaneRebel1::runLevel15() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
+		loadTuningForLevel(0x13);
 		_flyControlMode = 1;
 		_health = kMaxHealth;
 		_damageFlags = 0;
@@ -1834,6 +1857,7 @@ bool InsaneRebel1::runLevel15() {
 			_activeGameOpcode = 0;
 			_gameLatch5D = 0;
 			_gameLatch5F = 0;
+			loadTuningForLevel(0x14);
 			resetGameplayFlagsFromTuning();
 			_killCount = 0;
 			_torpedoFired = false;


Commit: 571e6bbc83c05d1a84e97c3d789d5ba6d6508858
    https://github.com/scummvm/scummvm/commit/571e6bbc83c05d1a84e97c3d789d5ba6d6508858
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:09+02:00

Commit Message:
SCUMM: RA1: Improve level 13 controls

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/render.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 7d8ac754669..9371c359031 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -739,10 +739,10 @@ void InsaneRebel1::updateFlightVariantCursor() {
 
 // preprocessMouseAxes — FUN_231BE (0x231BE) centered-axis output law, adapted to
 // ScummVM's absolute 320x200 mouse space.
-// Preserve the DOS bias/offset persistence and one-frame jump latch from
-// FUN_231BE. Original-input mode also emulates FUN_23115's DOS mouse recenter;
-// gameplay hides and locks the cursor, so this follows the original without
-// exposing a visible pointer jump.
+// Original-input mode preserves the DOS bias/offset persistence and one-frame
+// jump latch from FUN_231BE, plus FUN_23115's DOS mouse recenter behavior.
+// Enhanced controls are ScummVM-only: they bypass the original recentering state
+// and expose a stable absolute centered mouse axis instead.
 void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick) {
 	if (usedJoystick)
 		*usedJoystick = false;
@@ -810,52 +810,64 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 
 	int16 logicalX = (int16)CLIP<int>(_vm->_mouse.x, 0, 319);
 	int16 logicalY = (int16)CLIP<int>(_vm->_mouse.y, 0, 199);
+
+	if (_optEnhancedControls) {
+		_mouseVirtualValid = false;
+		_mouseBiasLatch = false;
+		_mouseBiasX = 0;
+		_mouseBiasY = 0;
+		_mousePrevBiasX = 0;
+		_mousePrevBiasY = 0;
+
+		inputX = (int16)CLIP<int32>(((int32)(logicalX - kRA1CenterX) * 127) / kRA1CenterX, -127, 127);
+		inputY = (int16)CLIP<int32>(((int32)(logicalY - kRA1CenterY) * 127) / kRA1CenterY, -127, 127);
+
+		if (_optControlsYFlip)
+			inputY = -inputY;
+
+		return;
+	}
+
 	int16 rawX = (int16)(logicalX << 1);
 	int16 rawY = logicalY;
 
-	if (!_optEnhancedControls) {
-		if (!_mouseVirtualValid) {
-			_mouseVirtualRawX = rawX;
-			_mouseVirtualRawY = rawY;
-			_mouseVirtualValid = true;
-		} else {
-			_mouseVirtualRawX = (int16)CLIP<int>(
-				_mouseVirtualRawX + ((logicalX - _mouseVirtualPrevLogicalX) << 1),
-				-32768, 32767);
-			_mouseVirtualRawY = (int16)CLIP<int>(
-				_mouseVirtualRawY + (logicalY - _mouseVirtualPrevLogicalY),
-				-32768, 32767);
-		}
-		_mouseVirtualPrevLogicalX = logicalX;
-		_mouseVirtualPrevLogicalY = logicalY;
-
-		rawX = _mouseVirtualRawX;
-		rawY = _mouseVirtualRawY;
-
-		if (rawX < kRA1DosMouseSafeLeft || rawX > kRA1DosMouseSafeRight ||
-			rawY < kRA1DosMouseSafeTop || rawY > kRA1DosMouseSafeBottom) {
-			_mouseOffsetX = (int16)CLIP<int>(
-				(int)_mouseOffsetX + rawX - kRA1DosMouseCenterX, -32768, 32767);
-			_mouseOffsetY = (int16)CLIP<int>(
-				(int)_mouseOffsetY + rawY - kRA1DosMouseCenterY, -32768, 32767);
-			rawX = kRA1DosMouseCenterX;
-			rawY = kRA1DosMouseCenterY;
-			_mouseVirtualRawX = rawX;
-			_mouseVirtualRawY = rawY;
-			_mouseVirtualPrevLogicalX = kRA1CenterX;
-			_mouseVirtualPrevLogicalY = kRA1CenterY;
-			_vm->_mouse.x = kRA1CenterX;
-			_vm->_mouse.y = kRA1CenterY;
-			smush_warpMouse(kRA1CenterX, kRA1CenterY, -1);
-			debug(2, "RA1 original input virtual recenter: offset=(%d,%d) mouse=(%d,%d)",
-				_mouseOffsetX, _mouseOffsetY, logicalX, logicalY);
-		}
+	if (!_mouseVirtualValid) {
+		_mouseVirtualRawX = rawX;
+		_mouseVirtualRawY = rawY;
+		_mouseVirtualValid = true;
 	} else {
-		_mouseVirtualValid = false;
+		_mouseVirtualRawX = (int16)CLIP<int>(
+			_mouseVirtualRawX + ((logicalX - _mouseVirtualPrevLogicalX) << 1),
+			-32768, 32767);
+		_mouseVirtualRawY = (int16)CLIP<int>(
+			_mouseVirtualRawY + (logicalY - _mouseVirtualPrevLogicalY),
+			-32768, 32767);
+	}
+	_mouseVirtualPrevLogicalX = logicalX;
+	_mouseVirtualPrevLogicalY = logicalY;
+
+	rawX = _mouseVirtualRawX;
+	rawY = _mouseVirtualRawY;
+
+	if (rawX < kRA1DosMouseSafeLeft || rawX > kRA1DosMouseSafeRight ||
+		rawY < kRA1DosMouseSafeTop || rawY > kRA1DosMouseSafeBottom) {
+		_mouseOffsetX = (int16)CLIP<int>(
+			(int)_mouseOffsetX + rawX - kRA1DosMouseCenterX, -32768, 32767);
+		_mouseOffsetY = (int16)CLIP<int>(
+			(int)_mouseOffsetY + rawY - kRA1DosMouseCenterY, -32768, 32767);
+		rawX = kRA1DosMouseCenterX;
+		rawY = kRA1DosMouseCenterY;
+		_mouseVirtualRawX = rawX;
+		_mouseVirtualRawY = rawY;
+		_mouseVirtualPrevLogicalX = kRA1CenterX;
+		_mouseVirtualPrevLogicalY = kRA1CenterY;
+		_vm->_mouse.x = kRA1CenterX;
+		_vm->_mouse.y = kRA1CenterY;
+		smush_warpMouse(kRA1CenterX, kRA1CenterY, -1);
+		debug(2, "RA1 original input virtual recenter: offset=(%d,%d) mouse=(%d,%d)",
+			_mouseOffsetX, _mouseOffsetY, logicalX, logicalY);
 	}
 
-	const int16 normX = (int16)(((int32)(logicalX - kRA1CenterX) * 127) / 160);
-	const int16 normY = (int16)(((int32)(logicalY - kRA1CenterY) * 127) / 100);
 	int16 biasX = (int16)((rawX + _mouseOffsetX - kRA1DosMouseCenterX) >> 2);
 	int16 biasY = (int16)((rawY + _mouseOffsetY - kRA1DosMouseCenterY) >> 1);
 
@@ -880,12 +892,6 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		_mouseBiasLatch = true;
 	}
 
-	// In original mouse mode, FUN_2309C centers the joystick contribution when
-	// joystick is disabled, so FUN_231BE's mouse output is just the bias term.
-	// Enhanced mode keeps ScummVM's pre-existing centered mouse axis plus bias.
-	const int16 scaledX = _optEnhancedControls ? (int16)(normX + biasX) : biasX;
-	const int16 scaledY = _optEnhancedControls ? (int16)(normY + biasY) : biasY;
-
 	_mouseBiasX = biasX;
 	_mouseBiasY = biasY;
 	_mousePrevBiasX = biasX;
@@ -931,8 +937,8 @@ void InsaneRebel1::preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJ
 		_mouseOffsetY -= 4;
 	}
 
-	inputX = CLIP<int16>(scaledX, -0xA0, 0xA0);
-	inputY = CLIP<int16>(scaledY, -127, 127);
+	inputX = CLIP<int16>(biasX, -0xA0, 0xA0);
+	inputY = CLIP<int16>(biasY, -127, 127);
 
 	// Controls Y-flip option (DAT_22be in original)
 	if (_optControlsYFlip)
@@ -1260,18 +1266,16 @@ void InsaneRebel1::updateTurretPhysics() {
 		_damageCooldown = 0;
 	}
 
-	// GAME 0x0A appears before 0x08 in L1PLAY2.ANM. The original therefore
+	// GAME 0x0A appears before 0x08 in turret/combat ANMs. The original
 	// draws shots/targeting and the ship from this pre-physics center, then
 	// updates the camera offset at the end of 0x08 for the final viewport copy.
 	const int16 preMoveOffsetX = (int16)(_posAccumX >> 8);
 	const int16 preMoveOffsetY = (int16)(_posAccumY >> 8);
-	if (_currentLevel == 0 && _flyControlMode == 2) {
-		_turretFrameShipOffsetX = preMoveOffsetX;
-		_turretFrameShipOffsetY = preMoveOffsetY;
-		_turretFrameShipCenterX = (int16)(kRA1CenterX + preMoveOffsetX);
-		_turretFrameShipCenterY = (int16)(kRA1CenterY + preMoveOffsetY);
-		_turretFrameShipCenterValid = true;
-	}
+	_turretFrameShipOffsetX = preMoveOffsetX;
+	_turretFrameShipOffsetY = preMoveOffsetY;
+	_turretFrameShipCenterX = (int16)(kRA1CenterX + preMoveOffsetX);
+	_turretFrameShipCenterY = (int16)(kRA1CenterY + preMoveOffsetY);
+	_turretFrameShipCenterValid = true;
 
 	// Damage gate from FUN_1E6A7.
 	if (_damageFlags != 0 && _damageCooldown == 0 && _health >= 0 && _deathTimer <= 0) {
@@ -1316,9 +1320,9 @@ void InsaneRebel1::updateTurretPhysics() {
 		const int16 rawInputX = inputX;
 		const int16 rawInputY = inputY;
 
-		if (usedJoystick && _optEnhancedControls) {
-			// First-person turret/cockpit stages are noticeably more sensitive on
-			// joystick than on mouse, so damp only the joystick-driven input here.
+		if (usedJoystick && _optEnhancedControls && _flyControlMode == 2) {
+			// ScummVM-only concession for Level 1 part 2. The original 0x08 handler
+			// uses raw axes directly; do not damp Level 13's surface controls.
 			inputX /= 2;
 			inputY /= 2;
 		}
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 5943b1c6b91..54b5b2b96cf 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -467,24 +467,18 @@ void renderBucketedShotSprite(byte *dst, int pitch, int width, int height,
 }
 
 void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
-	if (_currentLevel == 0 && _flyControlMode == 2) {
-		// Port helper: original Level 1 part 2 keeps g_perspectiveX/Y at the
-		// screen center and draws the overhead ship at g_shipOffsetX/Y from
-		// FUN_1E6A7. In this port _perspectiveX/Y stores the clamped camera
-		// offset passed to SetCameraOffset(), so recover the ship center from
-		// the movement accumulators instead.
-		if (_turretFrameShipCenterValid) {
-			x = _turretFrameShipCenterX;
-			y = _turretFrameShipCenterY;
-		} else {
-			x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
-			y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
-		}
+	// Port helper: original FUN_1E6A7 keeps g_perspectiveX/Y at screen center
+	// and draws the ship at g_perspective + g_shipOffset. In this port
+	// _perspectiveX/Y stores the clamped camera offset passed to SetCameraOffset(),
+	// so recover the ship center from the movement accumulators instead.
+	if (_turretFrameShipCenterValid) {
+		x = _turretFrameShipCenterX;
+		y = _turretFrameShipCenterY;
 		return;
 	}
 
-	x = (int16)(kRA1CenterX + (_perspectiveX - 0x20));
-	y = (int16)(kRA1CenterY + (_perspectiveY - 0x17));
+	x = (int16)(kRA1CenterX + (int16)(_posAccumX >> 8));
+	y = (int16)(kRA1CenterY + (int16)(_posAccumY >> 8));
 }
 
 // procPreRendering — Sets viewport window offset (FUN_224FD at 0x224FD).
@@ -689,18 +683,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		renderGostSlots(renderBitmap, pitch, width, height);
 		renderTargeting(renderBitmap, pitch, width, height);
 
-		// FUN_1D79C (GAME 0x0A) owns the cursor center in stage-2 turret mode.
+		// FUN_1D79C (GAME 0x0A) owns the cursor center in turret/combat mode.
 		// The preceding overlay/shot pass uses the previous frame's cursor; the
 		// handler then publishes the next cursor position from the current
-		// pre-physics ship offset. In L1PLAY2 the following 0x08 handler updates
-		// the camera afterward, so source-space anchors and the final viewport
-		// crop intentionally observe different moments in the frame.
+		// pre-physics ship offset. The following 0x08 handler updates the camera
+		// afterward, so source-space anchors and the final viewport crop
+		// intentionally observe different moments in the frame.
 		if (turretTargetingMode) {
-			const bool useTurretFrameCenter =
-				(_currentLevel == 0 && _flyControlMode == 2 && _turretFrameShipCenterValid);
-			const int16 shipOffsetX = useTurretFrameCenter ?
+			const int16 shipOffsetX = _turretFrameShipCenterValid ?
 				_turretFrameShipOffsetX : (int16)(_posAccumX >> 8);
-			const int16 shipOffsetY = useTurretFrameCenter ?
+			const int16 shipOffsetY = _turretFrameShipCenterValid ?
 				_turretFrameShipOffsetY : (int16)(_posAccumY >> 8);
 			_shipPosX = (int16)(kRA1CenterX + shipOffsetX);
 			_shipPosY = (int16)((kRA1CenterY + shipOffsetY - 0x23) - (shipOffsetY >> 3));


Commit: 298177ad654608568c5c3ebcd1bfb5471a1cc945
    https://github.com/scummvm/scummvm/commit/298177ad654608568c5c3ebcd1bfb5471a1cc945
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:09+02:00

Commit Message:
SCUMM: RA1: Render HUD only during active gameplay

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9371c359031..e38cc3f1625 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1899,6 +1899,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 	uint32 opcode = b.readUint32BE();
 	uint32 param1 = b.readUint32BE();
+	_frameHasGameChunk = true;
 
 	// FUN_1BE1B applies two global gates before the opcode switch. Bit 0 of
 	// g_combatModeFlags skips gameplay dispatch entirely; bit 5 of g_hudDisableFlags
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 90cd418cf80..3e60a5271e1 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -281,6 +281,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_turretEmitterRightY = 0;
 	_activeGameOpcode = 0;
 	_frameGameOpcodeMask = 0;
+	_frameHasGameChunk = false;
 	_frameDispatchFlags = 0;
 	_gameOp0BPhysicsUpdatedThisFrame = false;
 
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 600a8567a28..05a57f7ea22 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -86,6 +86,7 @@ public:
 
 	void handleGameChunk(int32 subSize, Common::SeekableReadStream &b);
 	bool isInteractiveVideoActive() const { return _interactiveVideoActive; }
+	void setFrameHasGameChunk(bool hasGameChunk) { _frameHasGameChunk = hasGameChunk; }
 	int getCurrentLevel() const { return _currentLevel; }
 	uint16 getActiveGameOpcode() const { return _activeGameOpcode; }
 	uint16 getEffectiveGameOpcode() const;
@@ -359,6 +360,7 @@ private:
 	// Kept for legacy call sites; frame-accurate dispatch uses _frameGameOpcodeMask.
 	uint16 _activeGameOpcode;
 	uint32 _frameGameOpcodeMask;
+	bool _frameHasGameChunk;
 	uint16 _frameDispatchFlags;
 	bool _gameOp0BPhysicsUpdatedThisFrame;
 
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 54b5b2b96cf..bf043391e19 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -487,14 +487,16 @@ void InsaneRebel1::getTurretShipCenter(int16 &x, int16 &y) const {
 void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	_frameGameOpcodeMask = 0;
 	_frameDispatchFlags = 0;
+	_hudRenderFlag = 0;
 	_gameOp0BPhysicsUpdatedThisFrame = false;
 	_turretFrameShipCenterValid = false;
 
 	if (_interactiveVideoActive && _player) {
 		const bool usePerspectiveViewport =
-			_activeGameOpcode == 0x07 || _activeGameOpcode == 0x08 ||
-			_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
-			_activeGameOpcode == 0x0B;
+			_frameHasGameChunk &&
+			(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x08 ||
+			 _activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
+			 _activeGameOpcode == 0x0B);
 		// Only gameplay handlers that actually execute FUN_224FD own the scrolling
 		// 320x200 window inside the 384x242 buffer. Interactive movies with no
 		// GAME stream (for example LVL4/L4PLAY2.ANM) keep a static camera.
@@ -570,19 +572,23 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	if (height == 0) height = _screenHeight;
 	int pitch = width;
 
-	// Set HUD render flag when interactive gameplay is active (0x7600)
-	_hudRenderFlag = 0xFF;
+	if (!_frameHasGameChunk) {
+		syncViewportOffset(false);
+		_fireCooldown = _playerFired ? 1 : 0;
+		return;
+	}
 
 	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
+	const bool allowImplicitGameplayMode = _frameHasGameChunk && !haveFrameGameOpcodes;
 	const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
-		(!haveFrameGameOpcodes && _activeGameOpcode == 0x0B);
+		(allowImplicitGameplayMode && _activeGameOpcode == 0x0B);
 	const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
-		(!haveFrameGameOpcodes &&
+		(allowImplicitGameplayMode &&
 			(_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
 	const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
-		(!haveFrameGameOpcodes && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
+		(allowImplicitGameplayMode && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
 	const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
-		(!haveFrameGameOpcodes &&
+		(allowImplicitGameplayMode &&
 			(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
 	if (gameOp0BMode) {
 		// GAME 0x0B scrolling cockpit/surface handler — FUN_1CDA7.
@@ -661,16 +667,16 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const bool hasTargetingPipeline =
 		hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
 		hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
-		(!haveFrameGameOpcodes &&
+		(allowImplicitGameplayMode &&
 			(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
 			 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
 	if (hasTargetingPipeline) {
 		const bool flightVariantTargetingMode =
 			hasFrameGameOpcode(0x09) ||
-			(!haveFrameGameOpcodes && _activeGameOpcode == 0x09);
+			(allowImplicitGameplayMode && _activeGameOpcode == 0x09);
 		const bool turretTargetingMode =
 			hasFrameGameOpcode(0x0A) ||
-			(!haveFrameGameOpcodes && _activeGameOpcode == 0x0A);
+			(allowImplicitGameplayMode && _activeGameOpcode == 0x0A);
 		renderTargetBoxes(renderBitmap, pitch, width, height);
 		processShot();
 		renderLaserShots(renderBitmap, pitch, width, height);
@@ -728,7 +734,8 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		renderLevel8Overlay(renderBitmap, pitch, width, height, viewportX, viewportY);
 	}
 
-	renderHUD(renderBitmap, pitch, width, height);
+	if (gameOp0BMode || onFootMode || turretMode || flightMode || _hudRenderFlag != 0)
+		renderHUD(renderBitmap, pitch, width, height);
 	_fireCooldown = _playerFired ? 1 : 0;
 }
 
@@ -830,6 +837,11 @@ void InsaneRebel1::handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame) {
 	_level14Play2BSplicePending = true;
 	_level14Play2BSpliceFrame = curFrame;
 
+	// DOS queues the continuation from inside the active playback loop, so the
+	// STOR/FTCH video state remains live across the L14PLAY2 -> L14PLY2B jump.
+	if (_player)
+		_player->setPreserveGameVideoStateOnRelease(true);
+
 	// Original after PlayAnmFile("LVL14/L14PLY2B.ANM", 0x860, maxFrame-0x0F, 1, -1):
 	//   g_extendedPhaseFlags &= 0xFA;
 	//   DAT_7604 &= 0xBF;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 2b371a8ae1d..29bec475b90 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -235,6 +235,7 @@ void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
 	debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
+	restoreScreenFlashPalette();
 	_interactiveVideoActive = false;
 	_vm->_smushVideoShouldFinish = false;
 	splayer->setCurVideoFlags(0x28);  // Cinematic mode + buffer preserve
@@ -2032,6 +2033,7 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
+	restoreScreenFlashPalette();
 	if (!resumingRoute)
 		clearBit(0);
 	_interactiveVideoActive = true;
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index acca9e27cc2..ef27996d58f 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -242,7 +242,8 @@ bool SmushPlayerRebel1::handleGameFetch(int32 subSize, Common::SeekableReadStrea
 				// directly — no projection-based FTCH placement. Level 4 phase 2
 				// also stores a full-width screen-space patch (320x180) that DOS
 				// restores without the centered 1/4 projection warp.
-				if (fullWidthStoredPatch || gameOp == 0x19 || gameOp == 0x1A) {
+				if (fullWidthStoredPatch || gameOp == 0x0B ||
+						gameOp == 0x19 || gameOp == 0x1A) {
 					left += _ra1ViewportOffsetX;
 					top += _ra1ViewportOffsetY;
 				} else {
@@ -976,13 +977,14 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 	if (_insane) {
 		InsaneRebel1 *rebel1 = static_cast<InsaneRebel1 *>(_insane);
 		bool interactive = rebel1->isInteractiveVideoActive();
+		const bool frameHasGameChunk = interactive && ra1FrameHasGameChunk(b, frameSize);
+		rebel1->setFrameHasGameChunk(frameHasGameChunk);
 		const uint16 activeOpcode = rebel1->getActiveGameOpcode();
 		bool forceClear = interactive &&
 			(activeOpcode == 0x0B ||
 			 (activeOpcode == 0 && rebel1->getCurrentLevel() == 1));
 
-		preserveFrameHistory = interactive && !forceClear &&
-			ra1FrameHasGameChunk(b, frameSize);
+		preserveFrameHistory = interactive && !forceClear && frameHasGameChunk;
 	}
 
 	// Restore clean frame for delta source


Commit: 24f60c962aba4f57035f15deb87d4b61d240a509
    https://github.com/scummvm/scummvm/commit/24f60c962aba4f57035f15deb87d4b61d240a509
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:09+02:00

Commit Message:
SCUMM: RA1: Improve torpedo mode rendering

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index e38cc3f1625..90022a4cafa 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1715,6 +1715,10 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 	// g_gameplayPhaseFlags & 2 becomes set.
 	if (_currentLevel == 14 && _levelGameplayPhase == 2) {
 		if (_frameCounter == 0x18A) {
+			// Original writes the 16-bit flags word: g_hudDisableFlags |= 0x210.
+			// Low byte bit 0x10 suppresses normal hit feedback in the torpedo phase;
+			// high byte bit 0x02 switches targeting/shot rendering to torpedoes.
+			_gameplayFlags75fe |= 0x10;
 			_gameplayFlags75ff |= 2;
 			_protectedTargetA = 0x67;
 			_protectedTargetB = 0x69;
@@ -1732,6 +1736,18 @@ void InsaneRebel1::updateGameOp0BPhysics() {
 		_perspectiveX, _perspectiveY, _health, _screenFlash);
 }
 
+bool InsaneRebel1::isTorpedoModeActive() const {
+	if ((_gameplayFlags75ff & 0x2) == 0)
+		return false;
+
+	// The original high-byte flag is only intentionally armed by the level 4
+	// torpedo run and the level 15 exhaust-port run. Gate the port's rendering
+	// and shot behavior to those phases so stale route/retry state cannot turn
+	// ordinary laser sections into torpedo mode.
+	return (_currentLevel == 3 && _levelGameplayPhase == 2) ||
+		(_currentLevel == 14 && _levelGameplayPhase == 2);
+}
+
 
 // updateOnFootPhysics — ScummVM-side glue for original on-foot GAME handlers
 // HandleGameOp19_OnFootSequence (0x19) and HandleGameOp1A_OnFootVariant (0x1A).
@@ -2289,14 +2305,15 @@ void InsaneRebel1::processShot() {
 
 	const int16 cursorX = getGameplayCursorX();
 	const int16 cursorY = getGameplayCursorY();
-	_shotSlots[slot].timer = (_gameplayFlags75ff & 0x2) ? 2 : 5;
+	const bool torpedoMode = isTorpedoModeActive();
+	_shotSlots[slot].timer = torpedoMode ? 2 : 5;
 	_shotSlots[slot].posX = cursorX;
 	_shotSlots[slot].posY = cursorY;
 	_shotSlots[slot].centerX = originX;
 	_shotSlots[slot].centerY = originY;
 	_shotSlots[slot].variant = _shotAlternator;
 	_shotAlternator = 1 - _shotAlternator;
-	playSfx((_gameplayFlags75ff & 0x2) ? kSfxAlert : kSfxLaserShot, 127, 0);
+	playSfx(torpedoMode ? kSfxAlert : kSfxLaserShot, 127, 0);
 
 	if (effectiveOpcode == 0x09 || _currentLevel == 4) {
 		debug(1, "RA1 shot: opcode=0x%02x frame=%d slot=%d cursor=(%d,%d) origin=(%d,%d) dir=%d mode=%d",
@@ -2406,7 +2423,8 @@ void InsaneRebel1::checkTargetHit(int16 targetIdx, int16 left, int16 top, int16
 						int16 hitCenterY = (top + bottom) / 2;
 						projectGameplayPoint(hitCenterX, hitCenterY);
 						const int sfxPan = CLIP((hitCenterX - kRA1CenterX) * 127 / kRA1CenterX, -127, 127);
-						playSfx(kSfxExplode, 127, sfxPan);
+						if ((_gameplayFlags75fe & 0x10) == 0)
+							playSfx(kSfxExplode, 127, sfxPan);
 
 						debug(3, "RA1 HIT: target=%d gost=%d pos=(%d,%d) score=%d kills=%d bangSprites=%d",
 							targetIdx, gi, _gostSlots[gi].posX, _gostSlots[gi].posY,
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 05a57f7ea22..8ba2f6576ad 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -193,6 +193,7 @@ private:
 
 	// Shooting pipeline — FUN_1CCA0 (0x1CCA0) shot spawner,
 	// FUN_1C0EF (0x1C0EF) target detection, FUN_1C940 (0x1C940) shot processing
+	bool isTorpedoModeActive() const;
 	void processShot();
 	void checkTargetHit(int16 targetIdx, int16 left, int16 top, int16 right, int16 bottom);
 
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index bf043391e19..f02ad7fb121 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -773,14 +773,14 @@ void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height
 // The original does not draw a hardcoded pixel cross; it renders glyph markers
 // whose state depends on _targetProximity.
 void InsaneRebel1::renderTargeting(byte *dst, int pitch, int width, int height) {
-	const char kRA1TorpedoIndicator[] = "<d";
+	const char kRA1TorpedoIndicator[] = "<<d";
 	const RA1SpriteBank &markerBank = (_techFontBank.numSprites > 0) ? _techFontBank : _hudFontBank;
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const int overlayY = ra1OverlayViewOffsetY(this);
 	if (markerBank.numSprites > 0) {
 		// FUN_1CB22 can switch marker sets via DAT_75FF bit 1.
 		// Baseline RA1 targeting uses '^' and animation e..h.
-		const bool altMarkerSet = (_gameplayFlags75ff & 0x2) != 0;
+		const bool altMarkerSet = isTorpedoModeActive();
 
 		// DAT_75FF bit 2 suppresses the fixed lock/readiness overlay.
 		if ((_gameplayFlags75ff & 0x4) == 0) {
@@ -879,6 +879,9 @@ void InsaneRebel1::renderGostScorePopup(byte *dst, int pitch, int width, int hei
 // renderGostSlots — FUN_1C9CD (0x1C9CD). Hit explosion animations at target positions.
 // Renders explosion sprites from bangBank + per-kill score popup glyphs.
 void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height) {
+	if ((_gameplayFlags75fe & 0x10) != 0)
+		return;
+
 	if (_bangBank.numSprites <= 0) {
 		// Warn if there are active GOST slots but no bang sprites to render
 		for (int i = 0; i < kMaxGostSlots; i++) {
@@ -1013,7 +1016,7 @@ void InsaneRebel1::renderLevel13EnemyShots(byte *dst, int pitch, int width, int
 void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height) {
 	const char kRA1TorpedoTrailLeft[] = "<<&";
 	const char kRA1TorpedoTrailRight[] = "<<'";
-	const bool torpedoMode = (_gameplayFlags75ff & 0x2) != 0;
+	const bool torpedoMode = isTorpedoModeActive();
 
 	if (_laserBank.numSprites <= 0 && !torpedoMode)
 		return;


Commit: ec4cff3a96833e1e9fad5d5bf443a2815708f79f
    https://github.com/scummvm/scummvm/commit/ec4cff3a96833e1e9fad5d5bf443a2815708f79f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:10+02:00

Commit Message:
SCUMM: RA1: Fix level 9 on-foot targeting

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 90022a4cafa..facc373470e 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -549,16 +549,33 @@ bool InsaneRebel1::handleFrameObjectTarget(int16 objectId, int16 left, int16 top
 
 	const byte bit = (byte)(0x80 >> (bitIndex & 7));
 	const int altIndex = byteIndex + 0x96;
-	const bool primarySet = (_frameObjectState[byteIndex] & bit) != 0;
-	const bool secondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
 	const int16 right = left + width;
 	const int16 bottom = top + height;
 
+	if (objectId >= 0x280 && _frameObjectHitRevealPending) {
+		// DispatchSmushFrameChunks keeps a one-object latch after a target hit.
+		// The following high-id FOBJ clears its hidden bit and becomes the
+		// replacement/death frame. This is used heavily by Level 9 troopers.
+		_frameObjectState[byteIndex] &= ~bit;
+		_frameObjectHitRevealPending = false;
+		debug(5, "RA1 FOBJ reveal: object=%d frameObjectByte=%d bit=0x%02x",
+			objectId, byteIndex, bit);
+	}
+
+	const bool primarySet = (_frameObjectState[byteIndex] & bit) != 0;
+	const bool secondarySet = (altIndex < kFrameObjectStateBytes) && ((_frameObjectState[altIndex] & bit) != 0);
+
 	if (objectId > 0 && objectId < 0x280) {
-		if (!primarySet || secondarySet)
+		if (!primarySet || secondarySet) {
+			const int16 previousKillCount = _killCount;
 			checkTargetHit(objectId - 1, left, top, right, bottom);
-		else
+			const bool updatedPrimarySet = (_frameObjectState[byteIndex] & bit) != 0;
+			_frameObjectHitRevealPending =
+				(_killCount != previousKillCount) && !primarySet && updatedPrimarySet;
+		} else {
+			_frameObjectHitRevealPending = false;
 			updateGostSlotPosition(objectId - 1, left, top, right, bottom);
+		}
 	}
 
 	const bool updatedPrimarySet = (_frameObjectState[byteIndex] & bit) != 0;
@@ -1749,7 +1766,7 @@ bool InsaneRebel1::isTorpedoModeActive() const {
 }
 
 
-// updateOnFootPhysics — ScummVM-side glue for original on-foot GAME handlers
+// ScummVM-side splits for the original on-foot GAME handlers:
 // HandleGameOp19_OnFootSequence (0x19) and HandleGameOp1A_OnFootVariant (0x1A).
 // On-foot handler for Level 9 (Stormtroopers). Character walks left/right, crosshair tracks mouse.
 //
@@ -1763,7 +1780,7 @@ const int16 kOnFootCenterY = 0x82;  // g_perspectiveY in HandleGameOp19
 
 // Port split matching HandleGameOp19_OnFootSequence. The helper name is new to
 // this implementation; the original code dispatches the opcode handler directly.
-void InsaneRebel1::updateOnFootSequence() {
+void InsaneRebel1::initOnFootSequence() {
 	// --- First-frame initialization (0x19 counter==0) ---
 	if (!_onFootInitialized) {
 		_onFootInitialized = true;
@@ -1780,8 +1797,14 @@ void InsaneRebel1::updateOnFootSequence() {
 		_perspectiveY = 0;
 		resetProjectionTable();
 	}
+}
 
-	// --- 0x19: Character walk animation + damage ---
+// Port split matching HandleGameOp19_OnFootSequence. The helper name is new to
+// this implementation; the original code dispatches the opcode handler directly.
+void InsaneRebel1::updateOnFootSequence() {
+	initOnFootSequence();
+
+	// --- 0x19: Post-draw character walk animation + damage ---
 	// Track fire button for animation
 	if (!_playerFired)
 		_onFootAnimCounter = 0;
@@ -1801,19 +1824,16 @@ void InsaneRebel1::updateOnFootSequence() {
 		// Right edge: snap to center, step character right
 		_shipDirIndex = 15;
 		_onFootCharX += 0x3A;
-	} else if (_onFootAnimCounter < 5 && !_playerFired) {
-		// Aim direction from crosshair toward character (QuantizeDirection8Way)
-		int16 dx = _shipPosX - (_onFootCharX + kOnFootCenterX);
-		int16 aimDir = 0;
-		if (dx > 30)
-			aimDir = 4;
-		else if (dx > 10)
-			aimDir = 2;
-		else if (dx < -30)
-			aimDir = -4;
-		else if (dx < -10)
-			aimDir = -2;
-		_shipDirIndex = CLIP<int16>(aimDir + 15, 11, 19);
+	} else if (_onFootAnimCounter < 5 && !(_playerFired && _fireCooldown == 0)) {
+		// Original calls QuantizeDirection8Way with the cursor and character
+		// center, but the DOS on-foot axis is mirrored relative to the screen
+		// coordinates used by this port. Use the visual screen-space vector so
+		// L9PILOT.NUT poses 11..14 aim left and 16..19 aim right.
+		const int16 centerX = _onFootCharX + kOnFootCenterX;
+		const int16 centerY = _onFootCharY + kOnFootCenterY;
+		const int16 aimDir = CLIP<int16>(
+			(int16)ra1ShotDirection(centerX, centerY, _shipPosX, _shipPosY), -4, 4);
+		_shipDirIndex = aimDir + 15;
 	} else {
 		// Walking based on mouse input direction
 		int16 inputX = 0, inputY = 0;
@@ -1861,18 +1881,7 @@ void InsaneRebel1::updateOnFootAimVariant() {
 	_shipPosY = inputYNeg + kOnFootCenterY + _onFootCharY - 0x32;
 }
 
-void InsaneRebel1::updateOnFootPhysics() {
-	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
-	const bool sequenceOpcode =
-		hasFrameGameOpcode(0x19) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x19);
-	const bool aimOpcode =
-		hasFrameGameOpcode(0x1A) || (!haveFrameGameOpcodes && _activeGameOpcode == 0x1A);
-
-	if (sequenceOpcode)
-		updateOnFootSequence();
-	if (aimOpcode)
-		updateOnFootAimVariant();
-
+void InsaneRebel1::finishOnFootFrame() {
 	if (_damageCooldown > 0)
 		_damageCooldown--;
 
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 3e60a5271e1..b90af71f368 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -385,6 +385,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_gostSlotIdx = 0;
 	_killCount = 0;
 	_lastHitTarget = 0;
+	_frameObjectHitRevealPending = false;
 	resetEnemyShotSlots();
 	_protectedTargetA = 0;
 	_protectedTargetB = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 8ba2f6576ad..83fbb415459 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -63,6 +63,8 @@ static const int16 kRA1MaxY = 180;
 static const int16 kRA1FocalX = 43;
 static const int16 kRA1FocalY = 25;
 
+int ra1ShotDirection(int16 x1, int16 y1, int16 x2, int16 y2);
+
 /**
  * Star Wars: Rebel Assault (RA1) game logic.
  * Adapts RA2 Handler 7 (ship flight) physics for RA1's 384x242 resolution.
@@ -172,6 +174,8 @@ private:
 	void renderGostScorePopup(byte *dst, int pitch, int width, int height,
 							  int16 centerX, int16 centerY, int16 frame);
 	void renderLaserShots(byte *dst, int pitch, int width, int height);
+	void renderShotOverlayPipeline(byte *dst, int pitch, int width, int height,
+		bool drawTargetBoxes);
 	void handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame);
 	void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
 	void resetEnemyShotSlots();
@@ -309,9 +313,10 @@ private:
 	void restoreScreenFlashPalette();
 
 	// 0x19/0x1A on-foot handler (Level 9 Stormtroopers)
-	void updateOnFootPhysics();
+	void initOnFootSequence();
 	void updateOnFootSequence();
 	void updateOnFootAimVariant();
+	void finishOnFootFrame();
 	int16 _onFootCharX;      // Character draw X (g_shipOffsetX in original)
 	int16 _onFootCharY;      // Character draw Y (g_shipOffsetY in original)
 	int16 _onFootAnimCounter; // DAT_0000828a: fire animation counter
@@ -558,6 +563,7 @@ private:
 
 	int16 _killCount;        // 0x75D0: targets destroyed this stage
 	int16 _lastHitTarget;    // 0x75D6: recent-kill latch, allows at most one hit per frame
+	bool _frameObjectHitRevealPending; // DispatchSmushFrameChunks local_14 high-id reveal latch
 
 	// Incoming enemy projectile slots used by Level 13 RunLevel13Flow.
 	static const int kMaxEnemyShotSlots = 5;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index f02ad7fb121..3702b7029b3 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -490,6 +490,7 @@ void InsaneRebel1::procPreRendering(byte *renderBitmap) {
 	_hudRenderFlag = 0;
 	_gameOp0BPhysicsUpdatedThisFrame = false;
 	_turretFrameShipCenterValid = false;
+	_frameObjectHitRevealPending = false;
 
 	if (_interactiveVideoActive && _player) {
 		const bool usePerspectiveViewport =
@@ -582,14 +583,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	const bool allowImplicitGameplayMode = _frameHasGameChunk && !haveFrameGameOpcodes;
 	const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
 		(allowImplicitGameplayMode && _activeGameOpcode == 0x0B);
-	const bool onFootMode = hasFrameGameOpcode(0x19) || hasFrameGameOpcode(0x1A) ||
-		(allowImplicitGameplayMode &&
-			(_activeGameOpcode == 0x19 || _activeGameOpcode == 0x1A));
+	const bool onFootSequenceMode = hasFrameGameOpcode(0x19) ||
+		(allowImplicitGameplayMode && _activeGameOpcode == 0x19);
+	const bool onFootAimMode = hasFrameGameOpcode(0x1A) ||
+		(allowImplicitGameplayMode && _activeGameOpcode == 0x1A);
+	const bool onFootMode = onFootSequenceMode || onFootAimMode;
 	const bool turretMode = hasFrameGameOpcode(0x08) || hasFrameGameOpcode(0x0A) ||
 		(allowImplicitGameplayMode && (_activeGameOpcode == 0x08 || _activeGameOpcode == 0x0A));
 	const bool flightMode = hasFrameGameOpcode(0x07) || hasFrameGameOpcode(0x09) ||
 		(allowImplicitGameplayMode &&
 			(_activeGameOpcode == 0x07 || _activeGameOpcode == 0x09));
+	bool shotOverlayHandled = false;
 	if (gameOp0BMode) {
 		// GAME 0x0B scrolling cockpit/surface handler — FUN_1CDA7.
 		if (!_gameOp0BPhysicsUpdatedThisFrame) {
@@ -609,22 +613,59 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		}
 	} else if (onFootMode) {
 		// On-foot handler — opcodes 0x19/0x1A (Level 9 Stormtroopers)
-		updateOnFootPhysics();
-
-		if (_health < 0 && _deathTimer < 2) {
+		if (_currentLevel == 8 && onFootAimMode && !onFootSequenceMode && _killCount > 0) {
 			_fireCooldown = _playerFired ? 1 : 0;
 			_vm->_smushVideoShouldFinish = true;
 			return;
 		}
 
-		// Draw character sprite at on-foot position (DrawFobjGlyph with flag 0x80)
-		if (_shipBank.numSprites > 0 && _shipDirIndex >= 0 &&
+		if (onFootSequenceMode)
+			initOnFootSequence();
+
+		// Original LVL9 frames dispatch GAME 0x1A before 0x19. That means the
+		// shot overlay uses the current crosshair/pose and is then covered by the
+		// character DrawFobjGlyph from 0x19; the 0x19 pose update is for the next
+		// frame. Keep that ordering explicit here.
+		if (onFootAimMode) {
+			shotOverlayHandled = true;
+			if (_health >= 0) {
+				// HandleGameOp5A_ObjectOrSceneTrigger can snap g_shipPos to the
+				// target center before the shot overlay runs. The ScummVM player
+				// defers GAME 0x1A until after FOBJ dispatch, so preserve that
+				// snap instead of immediately replacing it with raw input again.
+				const bool preserveTargetSnap = (_targetProximity == 2 && _tuning.snap > 0);
+				const int16 snappedTargetX = _shipPosX;
+				const int16 snappedTargetY = _shipPosY;
+
+				updateOnFootAimVariant();
+				if (preserveTargetSnap) {
+					_shipPosX = snappedTargetX;
+					_shipPosY = snappedTargetY;
+				}
+				renderShotOverlayPipeline(renderBitmap, pitch, width, height, false);
+			}
+		}
+
+		// Draw character sprite at on-foot position (DrawFobjGlyph with flag 0x80).
+		// GAME 0x1A-only selector clips reuse the targeting handler but do not
+		// draw the walking character.
+		if (onFootSequenceMode && _shipBank.numSprites > 0 && _shipDirIndex >= 0 &&
 			_shipDirIndex < _shipBank.numSprites) {
 			const RA1Sprite &spr = _shipBank.sprites[_shipDirIndex];
 			int drawX = _onFootCharX + spr.xoffs;
 			int drawY = _onFootCharY + spr.yoffs;
 			renderSprite(renderBitmap, pitch, width, height, drawX, drawY, spr);
 		}
+
+		if (onFootSequenceMode)
+			updateOnFootSequence();
+		finishOnFootFrame();
+
+		if (_health < 0 && _deathTimer < 2) {
+			_fireCooldown = _playerFired ? 1 : 0;
+			_vm->_smushVideoShouldFinish = true;
+			return;
+		}
 	} else {
 		// Dispatch movement path by GAME handler family:
 		//   0x08/0x0A -> FUN_1E6A7/FUN_1D79C (turret/cockpit)
@@ -665,11 +706,12 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	// in handlers 0x09/0x0A/0x0B/0x1A. LVL1 stage-2 works because the stream emits
 	// both 0x0A and 0x08 in the same frame, not because 0x08 owns the overlay path.
 	const bool hasTargetingPipeline =
-		hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
-		hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
-		(allowImplicitGameplayMode &&
+		!shotOverlayHandled &&
+		(hasFrameGameOpcode(0x09) || hasFrameGameOpcode(0x0A) ||
+		 hasFrameGameOpcode(0x0B) || hasFrameGameOpcode(0x1A) ||
+		 (allowImplicitGameplayMode &&
 			(_activeGameOpcode == 0x09 || _activeGameOpcode == 0x0A ||
-			 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A));
+			 _activeGameOpcode == 0x0B || _activeGameOpcode == 0x1A)));
 	if (hasTargetingPipeline) {
 		const bool flightVariantTargetingMode =
 			hasFrameGameOpcode(0x09) ||
@@ -677,17 +719,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		const bool turretTargetingMode =
 			hasFrameGameOpcode(0x0A) ||
 			(allowImplicitGameplayMode && _activeGameOpcode == 0x0A);
-		renderTargetBoxes(renderBitmap, pitch, width, height);
-		processShot();
-		renderLaserShots(renderBitmap, pitch, width, height);
-		// Timer decrement AFTER rendering (original decrements inside the render loop).
-		// This ensures timer==5 first frame is rendered with gun barrel offset and lerp=1.
-		for (int i = 0; i < kMaxShotSlots; i++) {
-			if (_shotSlots[i].timer > 0)
-				_shotSlots[i].timer--;
-		}
-		renderGostSlots(renderBitmap, pitch, width, height);
-		renderTargeting(renderBitmap, pitch, width, height);
+		renderShotOverlayPipeline(renderBitmap, pitch, width, height, true);
 
 		// FUN_1D79C (GAME 0x0A) owns the cursor center in turret/combat mode.
 		// The preceding overlay/shot pass uses the previous frame's cursor; the
@@ -705,7 +737,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		} else if (flightVariantTargetingMode) {
 			updateFlightVariantCursor();
 		}
-	} else {
+	} else if (!shotOverlayHandled) {
 		// Keep lock/target accumulators quiescent when current handler doesn't
 		// execute FUN_1C940/FUN_1CCA0/FUN_1C9CD/FUN_1CB22.
 		_targetProximity = 0;
@@ -739,6 +771,30 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 	_fireCooldown = _playerFired ? 1 : 0;
 }
 
+// ScummVM helper that groups the common shot/lock overlay calls used by the
+// original GAME handlers. GAME 0x1A uses the same pipeline without the target
+// box draw; 0x09/0x0A/0x0B include DrawTargetIndicators first.
+void InsaneRebel1::renderShotOverlayPipeline(byte *dst, int pitch, int width, int height,
+		bool drawTargetBoxes) {
+	if (drawTargetBoxes) {
+		renderTargetBoxes(dst, pitch, width, height);
+	} else {
+		_prevTargetCount = _targetCount;
+		_targetCount = 0;
+	}
+
+	processShot();
+	renderLaserShots(dst, pitch, width, height);
+	// Timer decrement AFTER rendering (original decrements inside the render loop).
+	// This ensures timer==5 first frame is rendered with gun barrel offset and lerp=1.
+	for (int i = 0; i < kMaxShotSlots; i++) {
+		if (_shotSlots[i].timer > 0)
+			_shotSlots[i].timer--;
+	}
+	renderGostSlots(dst, pitch, width, height);
+	renderTargeting(dst, pitch, width, height);
+}
+
 // renderTargetBoxes — FUN_1C940 (0x1C940). Per-target green box overlays.
 // Original gates on g_hudDisableFlags (0x75FE) bit 1: skip when set (Hard difficulty).
 void InsaneRebel1::renderTargetBoxes(byte *dst, int pitch, int width, int height) {
@@ -882,35 +938,25 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 	if ((_gameplayFlags75fe & 0x10) != 0)
 		return;
 
-	if (_bangBank.numSprites <= 0) {
-		// Warn if there are active GOST slots but no bang sprites to render
-		for (int i = 0; i < kMaxGostSlots; i++) {
-			if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
-				debug(1, "RA1 renderGostSlots: bangBank empty but gost slot %d active (target=%d frame=%d)",
-					i, _gostSlots[i].targetId, _gostSlots[i].frame);
-				_gostSlots[i].targetId = 0;  // Clear stale slot
-			}
-		}
-		return;
-	}
-
 	const int overlayX = ra1OverlayViewOffsetX(this);
 	const bool projectGostMarkers = (getEffectiveGameOpcode() == 0x0B);
 	for (int i = 0; i < kMaxGostSlots; i++) {
 		if (_gostSlots[i].targetId != 0 && _gostSlots[i].frame < 10) {
-			int sprIdx = _gostSlots[i].frame;
-			if (sprIdx >= _bangBank.numSprites)
-				sprIdx = _bangBank.numSprites - 1;
-
-			const RA1Sprite &spr = _bangBank.sprites[sprIdx];
 			int16 centerX = _gostSlots[i].posX;
 			int16 centerY = _gostSlots[i].posY;
 			if (projectGostMarkers)
 				centerX = (int16)(centerX - _perspectiveX);
 
-			int drawX = overlayX + centerX - spr.width / 2;
-			int drawY = centerY - spr.height / 2;
-			renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+			if (_bangBank.numSprites > 0) {
+				int sprIdx = _gostSlots[i].frame;
+				if (sprIdx >= _bangBank.numSprites)
+					sprIdx = _bangBank.numSprites - 1;
+
+				const RA1Sprite &spr = _bangBank.sprites[sprIdx];
+				int drawX = overlayX + centerX - spr.width / 2;
+				int drawY = centerY - spr.height / 2;
+				renderSprite(dst, pitch, width, height, drawX, drawY, spr);
+			}
 
 			// Per-kill score popup glyph — RenderGostOverlaySlots (0x1CA35)
 			// Suppressed when DAT_75FF bit 3 is set.
@@ -920,8 +966,6 @@ void InsaneRebel1::renderGostSlots(byte *dst, int pitch, int width, int height)
 			}
 
 			_gostSlots[i].frame++;
-			if (_gostSlots[i].frame >= 10)
-				_gostSlots[i].targetId = 0;  // Animation complete
 		}
 	}
 }
@@ -1027,14 +1071,15 @@ void InsaneRebel1::renderLaserShots(byte *dst, int pitch, int width, int height)
 	// DAT_2413: on-foot lerp table (timer 5 = 1, not 0 like flight mode).
 	const int kOnFootShotLerp[6] = { 0, 8, 7, 6, 4, 1 };
 	// DAT_240e: gun barrel X offset indexed by shipDirIndex (for timer==5 first frame).
+	// Indices 11..19 are the active firing poses from L9PILOT.NUT.
 	const int16 kOnFootGunBarrelX[20] = {
-		4, 0, 8, 8, 7, 6, 4, 1, 0, 0,
+		0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 		0, -56, -47, -23, -13, 0, 13, 30, 54, 59
 	};
 	// DAT_2420: gun barrel Y offset indexed by shipDirIndex (for timer==5 first frame).
 	const int16 kOnFootGunBarrelY[20] = {
-		0, 0, -56, -47, -23, -13, 0, 13, 30, 54,
-		59, -3, -19, -24, -30, -28, -30, -29, -20, -5
+		0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+		0, -3, -19, -24, -30, -28, -30, -29, -20, -5
 	};
 	const int spritesPerSet = 5;
 	const int overlayX = ra1OverlayViewOffsetX(this);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 29bec475b90..ba0978cad89 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1125,6 +1125,30 @@ bool InsaneRebel1::runLevel9() {
 	const int randPath1 = _vm->_rnd.getRandomNumber(1);
 	const int randPath2 = _vm->_rnd.getRandomNumber(1);
 	const int randPath3 = _vm->_rnd.getRandomNumber(1);
+	auto playLevel9PathSelector = [&](const char *filename) {
+		while (!_vm->shouldQuit()) {
+			// DOS zeros g_shipOffsetX around the 0x1A-only selector clips.
+			// Keep the selector cursor centered instead of inheriting the last
+			// walking offset from the previous stormtrooper segment.
+			_onFootCharX = 0;
+			_onFootCharY = 0;
+			_shipPosX = kRA1CenterX;
+			_shipPosY = kRA1CenterY;
+			_posAccumX = 0;
+			_posAccumY = 0;
+			_killCount = 0;
+			_lastHitTarget = 0;
+
+			playInteractiveVideo(filename);
+			if (_vm->shouldQuit() || _health < 0)
+				return -1;
+			if (_killCount > 0)
+				return (_shipPosX < kRA1CenterX) ? 0 : 1;
+
+			debug(1, "RA1 L9 selector '%s' ended without target hit; replaying", filename);
+		}
+		return -1;
+	};
 
 	_currentLevel = 8;
 	loadLevelSprites(9);
@@ -1190,17 +1214,16 @@ bool InsaneRebel1::runLevel9() {
 			if (_vm->shouldQuit())
 				return false;
 
-			_shipPosX = kRA1CenterX;
-			_posAccumX = 0;
 			loadTuningForLevel(0x0C);
-			playInteractiveVideo("LVL9/L9PLAY2.ANM");
+			const int side1 = playLevel9PathSelector("LVL9/L9PLAY2.ANM");
 			if (_vm->shouldQuit())
 				return false;
 			if (_health < 0)
 				break;
+			if (side1 < 0)
+				return false;
 
 			_gameplayFlags75fe |= 4;
-			const int side1 = (_shipPosX < kRA1CenterX) ? 0 : 1;
 			playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1235,17 +1258,16 @@ bool InsaneRebel1::runLevel9() {
 			if (_vm->shouldQuit())
 				return false;
 
-			_shipPosX = kRA1CenterX;
-			_posAccumX = 0;
 			loadTuningForLevel(0x0C);
-			playInteractiveVideo("LVL9/L9PLAY4.ANM");
+			const int side2 = playLevel9PathSelector("LVL9/L9PLAY4.ANM");
 			if (_vm->shouldQuit())
 				return false;
 			if (_health < 0)
 				break;
+			if (side2 < 0)
+				return false;
 
 			_gameplayFlags75fe |= 4;
-			const int side2 = (_shipPosX < kRA1CenterX) ? 0 : 1;
 			playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
 			if (_vm->shouldQuit())
 				return false;
@@ -1266,17 +1288,16 @@ bool InsaneRebel1::runLevel9() {
 				if (_vm->shouldQuit())
 					return false;
 
-				_shipPosX = kRA1CenterX;
-				_posAccumX = 0;
 				loadTuningForLevel(0x0C);
-				playInteractiveVideo("LVL9/L9PLAY6.ANM");
+				const int side3 = playLevel9PathSelector("LVL9/L9PLAY6.ANM");
 				if (_vm->shouldQuit())
 					return false;
 				if (_health < 0)
 					break;
+				if (side3 < 0)
+					return false;
 
 				_gameplayFlags75fe |= 4;
-				const int side3 = (_shipPosX < kRA1CenterX) ? 0 : 1;
 				if (side3 == randPath3) {
 					playCinematic("LVL9/L9CUT6A.ANM");
 					if (_vm->shouldQuit())


Commit: 8565fdbda2b4c89a1ca897a1f8292aa131f6ac16
    https://github.com/scummvm/scummvm/commit/8565fdbda2b4c89a1ca897a1f8292aa131f6ac16
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:10+02:00

Commit Message:
SCUMM: RA1: Seed random path selection deterministically

Changed paths:
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index ba0978cad89..2c20befca89 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1122,9 +1122,18 @@ bool InsaneRebel1::runLevel8() {
 bool InsaneRebel1::runLevel9() {
 	debug(1, "InsaneRebel1: Running level 9");
 
-	const int randPath1 = _vm->_rnd.getRandomNumber(1);
-	const int randPath2 = _vm->_rnd.getRandomNumber(1);
-	const int randPath3 = _vm->_rnd.getRandomNumber(1);
+	// DOS RunLevel9Flow calls RandScaleByte(2) three times before the intro.
+	// That helper advances a byte seed with seed = seed * 9 + 0x35 and returns
+	// (2 * seed) >> 8. Do not use ScummVM's session RNG here: it can turn the
+	// original right-side route into the capture/restart branch.
+	uint8 originalRouteSeed = 0;
+	auto getOriginalRouteBit = [&originalRouteSeed]() {
+		originalRouteSeed = (uint8)(originalRouteSeed * 9 + 0x35);
+		return (2 * originalRouteSeed) >> 8;
+	};
+	const int randPath1 = getOriginalRouteBit();
+	const int randPath2 = getOriginalRouteBit();
+	const int randPath3 = getOriginalRouteBit();
 	auto playLevel9PathSelector = [&](const char *filename) {
 		while (!_vm->shouldQuit()) {
 			// DOS zeros g_shipOffsetX around the 0x1A-only selector clips.


Commit: aa24c708399928fc64219b1a32b58cb47c6a09c6
    https://github.com/scummvm/scummvm/commit/aa24c708399928fc64219b1a32b58cb47c6a09c6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:10+02:00

Commit Message:
SCUMM: RA1: Allow navigation in level 7

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/render.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index facc373470e..9df570a6031 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -621,21 +621,28 @@ void InsaneRebel1::resetProjectionTable() {
 	rebuildProjectionTable(0, 1);
 }
 
-void InsaneRebel1::checkDynamicLevelBranch() {
+void InsaneRebel1::checkDynamicLevelBranch(int32 curFrame) {
 	if (!_interactiveVideoActive || _levelRouteIndex < 0)
 		return;
 
 	if ((_currentLevel == 6 || _currentLevel == 7) && _pendingRouteIndex >= 0) {
-		const uint32 routeFrame = (_currentLevel == 7) ? (uint32)_gameCounter : _frameCounter;
+		const uint32 routeFrame = (_currentLevel == 6 && curFrame >= 0) ?
+			(uint32)curFrame : (uint32)_gameCounter;
 		if (!_vm->_smushVideoShouldFinish &&
 			_pendingRouteCutoverFrame >= 0 &&
 			routeFrame >= (uint32)_pendingRouteCutoverFrame) {
-			if (_player)
+			// Level 7 route switches open the destination ANM as a fresh file.
+			// Keep Rebel runtime state, but do not carry SMUSH decoder state
+			// from the previous route into the new file.
+			if (_player && _currentLevel != 6)
 				_player->setPreserveGameVideoStateOnRelease(true);
 			_vm->_smushVideoShouldFinish = true;
-			debug(1, "RA1 L%d cutover: route=%d -> %d at frame=%u (resumeTimelineFrame=%d)",
+			const int32 resumeFrame = (_currentLevel == 6 && _pendingRouteStartFrame < 0) ?
+				0 : _pendingRouteStartFrame;
+			debug(1, "RA1 L%d cutover: route=%d -> %d at %s=%u (resumeFrame=%d)",
 				_currentLevel + 1, _levelRouteIndex, _pendingRouteIndex,
-				(unsigned)routeFrame, (int)_pendingRouteStartFrame);
+				_currentLevel == 6 ? "localFrame" : "frame",
+				(unsigned)routeFrame, (int)resumeFrame);
 		}
 		return;
 	}
@@ -644,23 +651,48 @@ void InsaneRebel1::checkDynamicLevelBranch() {
 		return;
 
 	if (_currentLevel == 6) {
+		// RunLevel7Flow compares the branch table against g_frameCounter. The
+		// playback callback writes that value from the ANM-local frame index,
+		// not from the decoded GAME counter embedded in these non-linear files.
+		if (curFrame < 0)
+			return;
+		const uint32 routeFrame = (uint32)curFrame;
+		// GAME 0x09 publishes its branch-tested position in g_shipPosX.
+		// ScummVM keeps the drawn ship center and the 0x09 aim cursor split,
+		// so compare the effective gameplay cursor here.
+		const int16 branchX = getGameplayCursorX();
 		const int route = CLIP<int>(_levelRouteIndex, 0, 5);
 		for (int nextRoute = 1; nextRoute < 6; ++nextRoute) {
 			const int triggerFrame = kLevel7BranchFrames[route][nextRoute];
-			if (triggerFrame <= 0 || nextRoute == route || _frameCounter != (uint32)(triggerFrame - 1))
+			if (triggerFrame <= 0)
+				continue;
+
+			const uint32 decisionFrame = (uint32)(triggerFrame - 1);
+			if (routeFrame + 0x1E == decisionFrame) {
+				_level7WarningFrames = 0x1E;
+				_level7WarningThreshold = kLevel7BranchThreshold[nextRoute];
+			}
+
+			if (routeFrame != decisionFrame)
 				continue;
 
 			const bool takeBranch = (kLevel7BranchDir[nextRoute] > 0)
-				? (_shipPosX > kLevel7BranchThreshold[nextRoute])
-				: (_shipPosX < kLevel7BranchThreshold[nextRoute]);
-			if (!takeBranch)
+				? (branchX > kLevel7BranchThreshold[nextRoute])
+				: (branchX < kLevel7BranchThreshold[nextRoute]);
+			if (!takeBranch) {
+				if (routeFrame == decisionFrame)
+					debug(1, "RA1 L7 branch miss: route=%d candidate=%d localFrame=%u gameFrame=%d shipX=%d dir=%d threshold=%d",
+						route, nextRoute, (unsigned)routeFrame, (int)_gameCounter, branchX,
+						kLevel7BranchDir[nextRoute], kLevel7BranchThreshold[nextRoute]);
 				continue;
+			}
 
 			_pendingRouteIndex = nextRoute;
-			_pendingRouteCutoverFrame = (int32)_frameCounter + 7;
-			_pendingRouteStartFrame = _pendingRouteCutoverFrame;
-			debug(1, "RA1 L7 branch: route=%d -> %d at frame=%u shipX=%d resumeTimelineFrame=%d cutoverFrame=%d",
-				route, nextRoute, (unsigned)_frameCounter, _shipPosX,
+			_pendingRouteCutoverFrame = (int32)routeFrame + 7;
+			_pendingRouteStartFrame = (int32)routeFrame;
+			_level7WarningFrames = 0;
+			debug(1, "RA1 L7 branch: route=%d -> %d at localFrame=%u gameFrame=%d decisionFrame=%u shipX=%d resumeSourceFrame=%d cutoverFrame=%d",
+				route, nextRoute, (unsigned)routeFrame, (int)_gameCounter, (unsigned)decisionFrame, branchX,
 				(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
 			return;
 		}
@@ -1188,7 +1220,8 @@ void InsaneRebel1::updateShipPhysics() {
 		_pathBranchEnabled = false;
 	}
 
-	checkDynamicLevelBranch();
+	if (_currentLevel != 6)
+		checkDynamicLevelBranch();
 
 	debug(7, "RA1 ship: pos=(%d,%d) roll=%d lift=%d accX=%d accY=%d dir=%d health=%d corridor=[%d,%d]-[%d,%d]",
 		_shipPosX, _shipPosY, _rollAccum, _liftSmooth,
@@ -1235,11 +1268,10 @@ void InsaneRebel1::updateTurretShipDirection(int16 offsetY) {
 }
 
 void InsaneRebel1::getCollisionShipCenter(int16 &x, int16 &y) const {
-	// Original 0x0D/0x0E collision compares projected script zones against the
-	// drawn ship center (base center + g_shipOffset). This port draws into a
-	// 384x242 source buffer and later crops by the camera offset, so convert that
-	// source-buffer anchor to the visible screen-space point used by the projected
-	// zones.
+	// Original 0x0D/0x0E collision compares script zones transformed by
+	// FUN_2248C against the source-buffer ship center (base center +
+	// g_shipOffset). Keep this in the same 384x242 space; the final viewport
+	// crop is only a presentation step.
 	//
 	// In Level 1 part 2, HandleGameOp0A_TurretVariant reuses _shipPos for the
 	// targeting cursor, so collision must read the movement accumulator instead.
@@ -1250,11 +1282,6 @@ void InsaneRebel1::getCollisionShipCenter(int16 &x, int16 &y) const {
 		x = _shipPosX;
 		y = _shipPosY;
 	}
-
-	if (_interactiveVideoActive) {
-		x = (int16)(x - _perspectiveX);
-		y = (int16)(y - _perspectiveY);
-	}
 }
 
 // updateTurretPhysics — FUN_1E6A7 (0x1E6A7), opcode 0x08 path.
@@ -2083,7 +2110,10 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 			int16 centerX = corridorLeft + corridorWidth / 2;
 			int16 centerY = corridorTop + corridorHeight / 2;
-			projectGameplayPoint(centerX, centerY);
+			// DOS FUN_1C54D calls FUN_2248C here, which adds the current
+			// camera offset to the scripted rectangle center before testing it
+			// against the source-buffer ship center.
+			unprojectGameplayPoint(centerX, centerY);
 
 			_corridorLeftX = centerX - corridorWidth / 2;
 			_corridorTopY = centerY - corridorHeight / 2;
@@ -2101,7 +2131,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 			const byte oldDirectionalFlags = _damageFlags & 0x0F;
 			if (_health >= 0) {
 				if (collisionShipX < _corridorLeftX) {
-					_posAccumX = (int32)(_corridorLeftX + _perspectiveX - kRA1CenterX) * 0x100;
+					_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
 					if (!suppressDirectionalDamage) {
 						if (_rollAccum < 0x100)
 							_rollAccum = 0x100;
@@ -2109,7 +2139,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 					}
 				}
 				if (collisionShipX > _corridorRightX) {
-					_posAccumX = (int32)(_corridorRightX + _perspectiveX - kRA1CenterX) * 0x100;
+					_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
 					if (!suppressDirectionalDamage) {
 						if (_rollAccum > -0x100)
 							_rollAccum = -0x100;
@@ -2117,12 +2147,12 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 					}
 				}
 				if (collisionShipY < _corridorTopY) {
-					_posAccumY = (int32)(_corridorTopY + _perspectiveY - kRA1CenterY) * 0x100 + 0x100;
+					_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
 					if (!suppressDirectionalDamage)
 						_damageFlags |= 0x01;
 				}
 				if (collisionShipY > _corridorBottomY) {
-					_posAccumY = (int32)(_corridorBottomY + _perspectiveY - kRA1CenterY) * 0x100 - 0x100;
+					_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
 					if (!suppressDirectionalDamage)
 						_damageFlags |= 0x08;
 				}
@@ -2154,7 +2184,8 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 			int16 centerX = zoneLeft + zoneWidth / 2;
 			int16 centerY = zoneTop + zoneHeight / 2;
-			projectGameplayPoint(centerX, centerY);
+			// Same transform as opcode 0x0D/FUN_1C54D.
+			unprojectGameplayPoint(centerX, centerY);
 
 			zoneLeft = centerX - zoneWidth / 2;
 			zoneTop = centerY - zoneHeight / 2;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index b90af71f368..f3d856b65d8 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -312,6 +312,8 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_pendingRouteIndex = -1;
 	_pendingRouteStartFrame = 0;
 	_pendingRouteCutoverFrame = -1;
+	_level7WarningFrames = 0;
+	_level7WarningThreshold = 0;
 	_levelGameplayPhase = 0;
 	_level14Play2BSplicePending = false;
 	_level14Play2BSpliced = false;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 83fbb415459..c7dc42932ae 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -162,7 +162,7 @@ private:
 	void preprocessMouseAxes(int16 &inputX, int16 &inputY, bool *usedJoystick = nullptr);
 	void rebuildProjectionTable(int16 curveStep, int16 curveExtent);
 	void resetProjectionTable();
-	void checkDynamicLevelBranch();
+	void checkDynamicLevelBranch(int32 curFrame = -1);
 	void renderShip(byte *dst, int pitch, int width, int height);
 	void renderHUD(byte *dst, int pitch, int width, int height);
 	void renderMainMenuOverlay(byte *dst, int pitch, int width, int height);
@@ -177,6 +177,7 @@ private:
 	void renderShotOverlayPipeline(byte *dst, int pitch, int width, int height,
 		bool drawTargetBoxes);
 	void handleLevel14Play2BSplice(int32 curFrame, int32 maxFrame);
+	void renderLevel7RouteOverlays(byte *dst, int pitch, int width, int height);
 	void renderLevel11HitsOverlay(byte *dst, int pitch, int width, int height);
 	void resetEnemyShotSlots();
 	void renderLevel13EnemyShots(byte *dst, int pitch, int width, int height);
@@ -408,7 +409,7 @@ private:
 	int16 _damageCooldown;       // 0x74D8: invulnerability timer (10 frames after hit)
 	int16 _deathTimer;           // 0x756A: death animation countdown (30 on death)
 	int16 _screenFlash;          // 0x7736: screen flash timer on hit
-	uint32 _frameCounter;        // 0x7740: global frame counter
+	uint32 _frameCounter;        // Gameplay handler frame accumulator
 	bool _screenShakeEnabled;    // 0x41AC: when true, SetCameraOffset adds ±2 random jitter
 	byte _screenFlashBasePalette[0x300];
 	bool _screenFlashBasePaletteValid;
@@ -449,13 +450,15 @@ private:
 	// Original sets nextSceneA/nextSceneB when GAME 0x07 counter == 394 (0x18A).
 	// We check ship position at that counter value to decide left vs right path.
 	static const int32 kPathBranchCounter = 394;  // GAME 0x07 field1 value
-	int32 _gameCounter;          // GAME 0x07 field1 — the original's _DAT_7740
+	int32 _gameCounter;          // GAME chunk field1/logical counter
 	bool _pathBranchEnabled;     // True when branching is active for this video
 	bool _rightPathSelected;     // True if player branched into L1PLAY1R
 	int _levelRouteIndex;        // Current mid-level route/segment for branching levels
 	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
-	int32 _pendingRouteStartFrame; // Resume frame for branch-driven route switches
+	int32 _pendingRouteStartFrame; // Resume/frame-gate target for branch-driven route switches
 	int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
+	int16 _level7WarningFrames;  // RunLevel7Flow local_38: 30-frame branch warning countdown
+	int16 _level7WarningThreshold; // Collapses RunLevel7Flow local_40 into its DAT_2361 threshold
 	int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
 	// RunLevel14Flow queues L14PLY2B at L14PLAY2 maxFrame-0x0F.
 	bool _level14Play2BSplicePending;
diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index 3702b7029b3..a390fc6006f 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -581,6 +581,7 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 
 	const bool haveFrameGameOpcodes = (_frameGameOpcodeMask != 0);
 	const bool allowImplicitGameplayMode = _frameHasGameChunk && !haveFrameGameOpcodes;
+
 	const bool gameOp0BMode = hasFrameGameOpcode(0x0B) ||
 		(allowImplicitGameplayMode && _activeGameOpcode == 0x0B);
 	const bool onFootSequenceMode = hasFrameGameOpcode(0x19) ||
@@ -747,9 +748,17 @@ void InsaneRebel1::procPostRendering(byte *renderBitmap, int32 codecparam, int32
 		_lastHitTarget = 0;
 	}
 
+	// RunLevel7Flow tests route branches after PumpFrontendFrame, using the
+	// decoded logical route timeline and updated GAME 0x09 cursor state.
+	if (_currentLevel == 6)
+		checkDynamicLevelBranch(curFrame);
+
 	if (_currentLevel == 12)
 		renderLevel13EnemyShots(renderBitmap, pitch, width, height);
 
+	if (_currentLevel == 6)
+		renderLevel7RouteOverlays(renderBitmap, pitch, width, height);
+
 	renderExplosions(renderBitmap, pitch, width, height);
 	handleLevel14Play2BSplice(curFrame, maxFrame);
 
@@ -986,6 +995,70 @@ void InsaneRebel1::resetEnemyShotSlots() {
 	memset(_enemyShotSlots, 0, sizeof(_enemyShotSlots));
 }
 
+// Port helper for Level 7 RunLevel7Flow. The original keeps this warning and
+// single incoming projectile slot inline in the L7PLAY frontend loop.
+void InsaneRebel1::renderLevel7RouteOverlays(byte *dst, int pitch, int width, int height) {
+	if (_currentLevel != 6 || !_interactiveVideoActive || _health < 0)
+		return;
+
+	if (_level7WarningFrames != 0) {
+		const int16 oldWarningFrames = _level7WarningFrames;
+		_level7WarningFrames--;
+
+		if ((oldWarningFrames & 7) == 0)
+			playSfx(kSfxLockOn, 127, 0);
+
+		const char *warningText = nullptr;
+		const int16 warningX = getGameplayCursorX();
+		if (_level7WarningFrames < 0x0B) {
+			warningText = (_level7WarningThreshold < warningX) ? "<<_" : "<<`";
+		} else if ((_level7WarningFrames & 4) != 0) {
+			warningText = "<<`_";
+		}
+
+		if (warningText != nullptr)
+			drawFontBankString(dst, pitch, width, height, 0x11C, 0x16, warningText);
+	}
+
+	const int targetCount = CLIP<int>(_prevTargetCount, 0, kMaxTargetBoxes);
+	EnemyShotSlot &slot = _enemyShotSlots[0];
+
+	int16 playerX = (int16)(_shipPosX - kRA1CenterX + _perspectiveX);
+	int16 playerY = (int16)(_shipPosY - kRA1CenterY + _perspectiveY);
+	unprojectGameplayPoint(playerX, playerY);
+
+	if (slot.timer == 0 && targetCount > 0 && _vm->_rnd.getRandomNumber(14) == 0) {
+		const int targetIdx = _vm->_rnd.getRandomNumber(targetCount - 1);
+		slot.startX = _targetBoxX[targetIdx];
+		slot.startY = _targetBoxY[targetIdx];
+		slot.targetX = playerX;
+		slot.targetY = playerY;
+		slot.timer = 7;
+		slot.direction = ra1ProjectileDirectionWithFlags(slot.startX, slot.startY,
+			slot.targetX, slot.targetY, slot.flags);
+		playSfx(kSfxBlast, 127, 0);
+	}
+
+	if (slot.timer == 0)
+		return;
+
+	const int progress = 9 - slot.timer;
+	const int drawX = slot.startX + (((slot.targetX - slot.startX) * progress) >> 2);
+	const int drawY = slot.startY + (((slot.targetY - slot.startY) * progress) >> 2);
+	const RA1SpriteBank &laserBank = (_enemyLaserBank.numSprites > 0) ? _enemyLaserBank : _laserBank;
+	if (slot.direction >= 0 && slot.direction < laserBank.numSprites) {
+		renderSpriteWithFlags(dst, pitch, width, height, drawX, drawY,
+			laserBank.sprites[slot.direction], slot.flags | 0x3);
+	}
+
+	slot.timer--;
+	if (drawX - 0x14 < playerX && playerX < drawX + 0x14 &&
+		drawY - 0x1E < playerY && playerY < drawY + 0x1E) {
+		_damageFlags |= 0x80;
+		slot.timer = 0;
+	}
+}
+
 // Port helper for Level 13 RunLevel13Flow. The original stores this state in
 // five local stack arrays around the L13PLAY frontend loop, not in a named function.
 void InsaneRebel1::renderLevel13EnemyShots(byte *dst, int pitch, int width, int height) {
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 2c20befca89..d85c5a75ea2 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -31,77 +31,6 @@
 
 namespace Scumm {
 
-struct RA1Level7ResumeSegment {
-	int16 timelineStart;
-	int16 timelineEnd;
-	int16 localStart;
-};
-
-const RA1Level7ResumeSegment kLevel7ResumeSegments[6][4] = {
-	{
-		{    0,  638,   0 },
-		{ 1416, 1468, 639 },
-		{   -1,   -1,  -1 },
-		{   -1,   -1,  -1 }
-	},
-	{
-		{   80,  639, 189 },
-		{  691,  879,   0 },
-		{ 1416, 1468, 749 },
-		{   -1,   -1,  -1 }
-	},
-	{
-		{   80,  639, 189 },
-		{  777,  879,  86 },
-		{  880,  965,   0 },
-		{ 1416, 1468, 749 }
-	},
-	{
-		{  398,  639, 284 },
-		{ 1132, 1415,   0 },
-		{ 1416, 1468, 526 },
-		{   -1,   -1,  -1 }
-	},
-	{
-		{  398,  639, 143 },
-		{  966, 1076,   0 },
-		{ 1384, 1415, 111 },
-		{ 1416, 1468, 385 }
-	},
-	{
-		{   80,  639, 114 },
-		{  821,  879,  55 },
-		{ 1077, 1131,   0 },
-		{ 1416, 1468, 674 }
-	}
-};
-
-int32 mapLevel7TimelineFrameToLocal(int route, int32 timelineFrame) {
-	if (timelineFrame <= 0)
-		return 0;
-
-	const int clampedRoute = CLIP<int>(route, 0, ARRAYSIZE(kLevel7ResumeSegments) - 1);
-	const RA1Level7ResumeSegment *segments = kLevel7ResumeSegments[clampedRoute];
-
-	for (int i = 0; i < ARRAYSIZE(kLevel7ResumeSegments[0]); ++i) {
-		const RA1Level7ResumeSegment &segment = segments[i];
-		if (segment.timelineStart < 0)
-			break;
-		if (timelineFrame < segment.timelineStart)
-			return segment.localStart;
-		if (timelineFrame <= segment.timelineEnd)
-			return segment.localStart + (timelineFrame - segment.timelineStart);
-	}
-
-	for (int i = ARRAYSIZE(kLevel7ResumeSegments[0]) - 1; i >= 0; --i) {
-		const RA1Level7ResumeSegment &segment = segments[i];
-		if (segment.timelineStart >= 0)
-			return segment.localStart;
-	}
-
-	return 0;
-}
-
 int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 targetFrame) {
 	if (targetFrame <= 0)
 		return 0;
@@ -905,6 +834,7 @@ bool InsaneRebel1::runLevel7() {
 
 	_currentLevel = 6;
 	loadLevelSprites(7);
+	loadRA1Nut("LVL7/L7LASER2.NUT", _enemyLaserBank);
 	// DOS RunLevel7Flow starts L7PLAY1.ANM with initLevelFlag=9.
 	loadTuningForLevel(9);
 
@@ -929,6 +859,7 @@ bool InsaneRebel1::runLevel7() {
 		_gameLatch5D = 0;
 		_gameLatch5F = 0;
 		resetGameplayFlagsFromTuning();
+		_driftParam = 0x19;
 		_killCount = 0;
 		_targetCount = 0;
 		_prevTargetCount = 0;
@@ -949,13 +880,17 @@ bool InsaneRebel1::runLevel7() {
 		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
 		_avgInputX = 0;
 		_avgInputY = 0;
+		resetEnemyShotSlots();
+		_level7WarningFrames = 0;
+		_level7WarningThreshold = 0;
 
 		int route = 0;
 		int32 routeStartFrame = 0;
+		int32 routeSourceFrame = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;
 			_pendingRouteIndex = -1;
-			_pendingRouteStartFrame = routeStartFrame;
+			_pendingRouteStartFrame = routeSourceFrame;
 			_pendingRouteCutoverFrame = -1;
 			playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
 			if (_vm->shouldQuit())
@@ -964,20 +899,23 @@ bool InsaneRebel1::runLevel7() {
 			if (_health < 0)
 				break;
 
-			if (_pendingRouteIndex < 0 || _pendingRouteIndex == route)
+			if (_pendingRouteIndex < 0)
 				break;
 
-			// RunLevel7Flow arms the next route inline, lets the current route run for
-			// seven more gameplay frames, then opens the destination ANM while keeping
-			// the existing video state alive.
-			routeStartFrame = _pendingRouteStartFrame;
 			route = _pendingRouteIndex;
+			routeSourceFrame = _pendingRouteStartFrame;
+			// DOS does not seek the destination route ANM here. The ANM-local
+			// decision frame is used by the playback gate/cutoff, while
+			// L7PLAY2..6 open from their first frame.
+			routeStartFrame = 0;
 		}
 
 		_levelRouteIndex = -1;
 		_pendingRouteIndex = -1;
 		_pendingRouteStartFrame = 0;
 		_pendingRouteCutoverFrame = -1;
+		_level7WarningFrames = 0;
+		_level7WarningThreshold = 0;
 
 		if (_health >= 0) {
 			char accuracyText[80];
@@ -2053,7 +1991,10 @@ void InsaneRebel1::runGame() {
 // Play interactive gameplay video (with ship physics + HUD).
 void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
 	debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
-	const bool resumingRoute = (startFrame > 0);
+	const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
+	const bool resumingRoute = startFrame > 0;
+	const bool preserveRuntimeState = resumingRoute || level7RouteSplice;
+	const bool preserveVideoState = resumingRoute && !level7RouteSplice;
 	int32 videoStartFrame = 0;
 	int32 videoOffset = 0;
 
@@ -2064,10 +2005,10 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
 	restoreScreenFlashPalette();
-	if (!resumingRoute)
+	if (!preserveRuntimeState)
 		clearBit(0);
 	_interactiveVideoActive = true;
-	if (!resumingRoute) {
+	if (!preserveRuntimeState) {
 		_onFootInitialized = false;  // Reset so each segment triggers counter==0 init
 		resetFrameObjectState();
 	}
@@ -2075,21 +2016,24 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	// Route resumes stay in the same gameplay flow in the original executable.
 	// Preserve the previous video/runtime state, but keep the destination clip
 	// fully interactive from its first visible frame.
-	splayer->setPreserveVideoStateOnNextPlay(resumingRoute);
+	splayer->setPreserveVideoStateOnNextPlay(preserveVideoState);
 	splayer->setCurVideoFlags(0x28);
 	splayer->setFastForwardFromFrame(0);
 	splayer->setFastForwardToFrame(0);
-	if (_currentLevel == 6 && resumingRoute) {
-		videoStartFrame = mapLevel7TimelineFrameToLocal(_levelRouteIndex, startFrame);
+	if (_currentLevel == 6 && level7RouteSplice) {
+		// DOS opens the route ANM from the beginning, then the armed frame gate
+		// suppresses destination local frame 0 because the L7 arm-frame table is 1.
+		videoStartFrame = 1;
 		videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
 		if (videoOffset < 0) {
-			debug(1, "RA1 L7 resume: route=%d timelineFrame=%d localFrame=%d offset lookup failed",
-				_levelRouteIndex, (int)startFrame, (int)videoStartFrame);
+			debug(1, "RA1 L7 route switch: route=%d gateFrame=%d offset lookup failed",
+				_levelRouteIndex, (int)videoStartFrame);
 			videoStartFrame = 0;
 			videoOffset = 0;
 		} else {
-			debug(1, "RA1 L7 resume: route=%d timelineFrame=%d -> localFrame=%d offset=0x%x",
-				_levelRouteIndex, (int)startFrame, (int)videoStartFrame, (unsigned)videoOffset);
+			debug(1, "RA1 L7 route switch: route=%d decisionLocalFrame=%d opens destination at gateFrame=%d offset=0x%x",
+				_levelRouteIndex, (int)_pendingRouteStartFrame,
+				(int)videoStartFrame, (unsigned)videoOffset);
 		}
 	} else if (_currentLevel == 7 && resumingRoute) {
 		videoOffset = findAnimFrameChunkOffsetByGameCounter(_vm, filename, startFrame, videoStartFrame);
@@ -2117,13 +2061,17 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 			(int)startFrame);
 	}
 
-	// Center mouse, hide system cursor (we draw our own), lock mouse to window
-	smush_warpMouse(160, 100, -1);
-	_mouseVirtualRawX = 0x140;
-	_mouseVirtualRawY = 100;
-	_mouseVirtualPrevLogicalX = kRA1CenterX;
-	_mouseVirtualPrevLogicalY = kRA1CenterY;
-	_mouseVirtualValid = false;
+	// Center mouse, hide system cursor (we draw our own), lock mouse to window.
+	// Level 7 route splices happen inside one original gameplay loop, so keep
+	// the current input state instead of recentering between route clips.
+	if (!level7RouteSplice) {
+		smush_warpMouse(160, 100, -1);
+		_mouseVirtualRawX = 0x140;
+		_mouseVirtualRawY = 100;
+		_mouseVirtualPrevLogicalX = kRA1CenterX;
+		_mouseVirtualPrevLogicalY = kRA1CenterY;
+		_mouseVirtualValid = false;
+	}
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index ef27996d58f..3c9c4c71e96 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -972,6 +972,7 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 	uint8 *audioChunk = nullptr;
 	_skipNext = false;
 	handleGameFrameStart();
+	const bool fastForwarding = isFastForwardingCurrentFrame();
 
 	bool preserveFrameHistory = false;
 	if (_insane) {
@@ -1083,8 +1084,10 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 			break;
 		case MKTAG('G','A','M','E'):
 		case MKTAG('G','A','M','2'): {
-			InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
-			rebel1->handleGameChunk(subSize, b);
+			if (!fastForwarding) {
+				InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+				rebel1->handleGameChunk(subSize, b);
+			}
 			break;
 		}
 		case MKTAG('O','B','J','\0'): {
@@ -1127,9 +1130,11 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 							_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
 						}
 					} else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
-						Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
-						InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
-						rebel1->handleGameChunk(embSize, embStream);
+						if (!fastForwarding) {
+							Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+							InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+							rebel1->handleGameChunk(embSize, embStream);
+						}
 					} else if (embTag == MKTAG('P','S','A','D')) {
 						if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
 							uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
@@ -1200,10 +1205,10 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 		_ra1HasCleanFrame = false;
 	}
 
-	if (_insanity)
+	if (_insanity && !fastForwarding)
 		_insane->procPostRendering(_dst, 0, 0, 0, _frame, _nbframes-1);
 
-	if (_width != 0 && _height != 0)
+	if (_width != 0 && _height != 0 && !fastForwarding)
 		updateScreen();
 
 	_frame++;


Commit: 10feabc9dab1f70402e9b6a9d2a8e4240e47f7ca
    https://github.com/scummvm/scummvm/commit/10feabc9dab1f70402e9b6a9d2a8e4240e47f7ca
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:11+02:00

Commit Message:
SCUMM: RA1: Refine navigation in level 7

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 9df570a6031..98ed443d600 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -690,10 +690,12 @@ void InsaneRebel1::checkDynamicLevelBranch(int32 curFrame) {
 			_pendingRouteIndex = nextRoute;
 			_pendingRouteCutoverFrame = (int32)routeFrame + 7;
 			_pendingRouteStartFrame = (int32)routeFrame;
+			_pendingRouteVideoStartFrame = 1 + (_pendingRouteCutoverFrame - _pendingRouteStartFrame);
 			_level7WarningFrames = 0;
-			debug(1, "RA1 L7 branch: route=%d -> %d at localFrame=%u gameFrame=%d decisionFrame=%u shipX=%d resumeSourceFrame=%d cutoverFrame=%d",
+			debug(1, "RA1 L7 branch: route=%d -> %d at localFrame=%u gameFrame=%d decisionFrame=%u shipX=%d resumeSourceFrame=%d cutoverFrame=%d destFrame=%d",
 				route, nextRoute, (unsigned)routeFrame, (int)_gameCounter, (unsigned)decisionFrame, branchX,
-				(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame);
+				(int)_pendingRouteStartFrame, (int)_pendingRouteCutoverFrame,
+				(int)_pendingRouteVideoStartFrame);
 			return;
 		}
 	}
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index f3d856b65d8..c6686439014 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -312,6 +312,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_pendingRouteIndex = -1;
 	_pendingRouteStartFrame = 0;
 	_pendingRouteCutoverFrame = -1;
+	_pendingRouteVideoStartFrame = 0;
 	_level7WarningFrames = 0;
 	_level7WarningThreshold = 0;
 	_levelGameplayPhase = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index c7dc42932ae..d714c77588b 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -457,6 +457,7 @@ private:
 	int _pendingRouteIndex;      // Next route requested by original frame-branch logic
 	int32 _pendingRouteStartFrame; // Resume/frame-gate target for branch-driven route switches
 	int32 _pendingRouteCutoverFrame; // Delayed inline route splice frame (branchFrame + 7)
+	int32 _pendingRouteVideoStartFrame; // L7 destination ANM-local frame after gate adjustment
 	int16 _level7WarningFrames;  // RunLevel7Flow local_38: 30-frame branch warning countdown
 	int16 _level7WarningThreshold; // Collapses RunLevel7Flow local_40 into its DAT_2361 threshold
 	int _levelGameplayPhase;     // Level-local interactive phase (e.g. LVL4 PLAY1 vs PLAY2)
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index d85c5a75ea2..b5a95396492 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -887,11 +887,13 @@ bool InsaneRebel1::runLevel7() {
 		int route = 0;
 		int32 routeStartFrame = 0;
 		int32 routeSourceFrame = 0;
+		int32 routeVideoStartFrame = 0;
 		while (!_vm->shouldQuit()) {
 			_levelRouteIndex = route;
 			_pendingRouteIndex = -1;
 			_pendingRouteStartFrame = routeSourceFrame;
 			_pendingRouteCutoverFrame = -1;
+			_pendingRouteVideoStartFrame = routeVideoStartFrame;
 			playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
 			if (_vm->shouldQuit())
 				return false;
@@ -904,9 +906,10 @@ bool InsaneRebel1::runLevel7() {
 
 			route = _pendingRouteIndex;
 			routeSourceFrame = _pendingRouteStartFrame;
+			routeVideoStartFrame = _pendingRouteVideoStartFrame;
 			// DOS does not seek the destination route ANM here. The ANM-local
-			// decision frame is used by the playback gate/cutoff, while
-			// L7PLAY2..6 open from their first frame.
+			// decision frame is used by the playback gate/cutoff; ScummVM
+			// starts at the already-advanced gate target after the cutover.
 			routeStartFrame = 0;
 		}
 
@@ -914,6 +917,7 @@ bool InsaneRebel1::runLevel7() {
 		_pendingRouteIndex = -1;
 		_pendingRouteStartFrame = 0;
 		_pendingRouteCutoverFrame = -1;
+		_pendingRouteVideoStartFrame = 0;
 		_level7WarningFrames = 0;
 		_level7WarningThreshold = 0;
 
@@ -2022,16 +2026,18 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	splayer->setFastForwardToFrame(0);
 	if (_currentLevel == 6 && level7RouteSplice) {
 		// DOS opens the route ANM from the beginning, then the armed frame gate
-		// suppresses destination local frame 0 because the L7 arm-frame table is 1.
-		videoStartFrame = 1;
+		// suppresses until the adjusted target. With ScummVM's delayed cutover,
+		// the destination must advance by the source tail already displayed.
+		videoStartFrame = (_pendingRouteVideoStartFrame > 0) ?
+			_pendingRouteVideoStartFrame : 1;
 		videoOffset = findAnimFrameChunkOffset(_vm, filename, videoStartFrame);
 		if (videoOffset < 0) {
-			debug(1, "RA1 L7 route switch: route=%d gateFrame=%d offset lookup failed",
+			debug(1, "RA1 L7 route switch: route=%d destinationFrame=%d offset lookup failed",
 				_levelRouteIndex, (int)videoStartFrame);
 			videoStartFrame = 0;
 			videoOffset = 0;
 		} else {
-			debug(1, "RA1 L7 route switch: route=%d decisionLocalFrame=%d opens destination at gateFrame=%d offset=0x%x",
+			debug(1, "RA1 L7 route switch: route=%d decisionLocalFrame=%d opens destination at localFrame=%d offset=0x%x",
 				_levelRouteIndex, (int)_pendingRouteStartFrame,
 				(int)videoStartFrame, (unsigned)videoOffset);
 		}


Commit: aacaf1cdd711a45fdc7d21f4d3bd157ee25d28d6
    https://github.com/scummvm/scummvm/commit/aacaf1cdd711a45fdc7d21f4d3bd157ee25d28d6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:11+02:00

Commit Message:
SCUMM: RA1: Deduplicate runLevel handlers

Changed paths:
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d714c77588b..b8b1678ffc0 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -150,6 +150,14 @@ private:
 		const char *bonusLabel1 = nullptr, const char *detailText1 = nullptr, int bonusValue1 = 0,
 		const char *bonusLabel2 = nullptr, const char *detailText2 = nullptr, int bonusValue2 = 0,
 		int passwordIndex = 0);
+	bool playDeathOrRetry(const char *retryVideo, const char *gameOverVideo);
+	void resetLevelDamageState();
+	void resetLevelFrameState();
+	void resetLevelTargetingState(bool resetKillCount = true);
+	void resetLevelFlightState(int16 shipDirIndex = 17);
+	void resetLevelInputHistory(bool resetAxisDeltaX = false);
+	void resetLevelAttemptState(int16 flyControlMode, int16 gameplayPhase,
+		int16 shipDirIndex = 17, bool resetAxisDeltaX = false);
 
 	// Play interactive gameplay video (with ship physics + HUD)
 	void playInteractiveVideo(const char *filename, int32 startFrame = 0);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index b5a95396492..80da8bfcbfa 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -196,6 +196,81 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
 		_maxChapterUnlocked = MAX<int16>(_maxChapterUnlocked, passwordIndex);
 }
 
+bool InsaneRebel1::playDeathOrRetry(const char *retryVideo, const char *gameOverVideo) {
+	if (_lives > 0) {
+		playCinematic(retryVideo);
+		if (_vm->shouldQuit())
+			return false;
+		_lives--;
+		return true;
+	}
+
+	playCinematic(gameOverVideo);
+	return false;
+}
+
+void InsaneRebel1::resetLevelDamageState() {
+	_health = kMaxHealth;
+	_damageFlags = 0;
+	_prevDamageFlags = 0;
+	_damageCooldown = 0;
+	_deathTimer = 0;
+	_screenFlash = 0;
+	_screenShakeEnabled = false;
+	_deathCauseIndicator = 0;
+}
+
+void InsaneRebel1::resetLevelFrameState() {
+	_frameCounter = 0;
+	_gameCounter = 0;
+	_activeGameOpcode = 0;
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+}
+
+void InsaneRebel1::resetLevelTargetingState(bool resetKillCount) {
+	if (resetKillCount)
+		_killCount = 0;
+	_targetCount = 0;
+	_prevTargetCount = 0;
+	_lastHitTarget = 0;
+}
+
+void InsaneRebel1::resetLevelFlightState(int16 shipDirIndex) {
+	_shipPosX = kRA1CenterX;
+	_shipPosY = kRA1CenterY;
+	_shipDirIndex = shipDirIndex;
+	_rollAccum = 0;
+	_liftSmooth = 0;
+	_posAccumX = 0;
+	_posAccumY = 0;
+	_perspectiveX = 0;
+	_perspectiveY = 0;
+}
+
+void InsaneRebel1::resetLevelInputHistory(bool resetAxisDeltaX) {
+	memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+	memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+	memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+	memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+	if (resetAxisDeltaX)
+		_inputAxisDeltaX = 0;
+	_avgInputX = 0;
+	_avgInputY = 0;
+}
+
+void InsaneRebel1::resetLevelAttemptState(int16 flyControlMode, int16 gameplayPhase,
+		int16 shipDirIndex, bool resetAxisDeltaX) {
+	_flyControlMode = flyControlMode;
+	resetLevelDamageState();
+	resetLevelFrameState();
+	resetGameplayFlagsFromTuning();
+	resetLevelTargetingState();
+	resetLevelFlightState(shipDirIndex);
+	_levelGameplayPhase = gameplayPhase;
+	resetLevelInputHistory(resetAxisDeltaX);
+}
+
 void InsaneRebel1::playLevelTransitionCutscene(int level) {
 	switch (level) {
 	case 4:
@@ -301,32 +376,10 @@ bool InsaneRebel1::runLevel1() {
 	while (!_vm->shouldQuit()) {
 		bool stage2Started = false;
 
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
+		resetLevelDamageState();
+		resetLevelFrameState();
+		resetLevelTargetingState();
+		resetLevelFlightState();
 		_pathBranchEnabled = true;
 		_rightPathSelected = false;
 		_flyControlMode = 1;
@@ -398,15 +451,8 @@ bool InsaneRebel1::runLevel1() {
 		if (_vm->shouldQuit())
 			return false;
 
-		if (_lives <= 0) {
-			playCinematic("LVL1/L1DEATH.ANM");
+		if (!playDeathOrRetry("LVL1/L1NEW.ANM", "LVL1/L1DEATH.ANM"))
 			return false;
-		}
-
-		playCinematic("LVL1/L1NEW.ANM");
-		if (_vm->shouldQuit())
-			return false;
-		_lives--;
 	}
 
 	return false;
@@ -427,26 +473,10 @@ bool InsaneRebel1::runLevel2() {
 
 	while (!_vm->shouldQuit()) {
 		_flyControlMode = 0;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelDamageState();
+		resetLevelFrameState();
+		resetLevelInputHistory();
 		_killCount = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
 
 		playInteractiveVideo("LVL2/L2PLAY.ANM");
 		if (_vm->shouldQuit())
@@ -460,15 +490,9 @@ bool InsaneRebel1::runLevel2() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL2/L2NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL2/L2NEW.ANM", "LVL2/L2DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL2/L2DEATH.ANM");
 		return false;
 	}
 
@@ -490,28 +514,12 @@ bool InsaneRebel1::runLevel3() {
 
 	while (!_vm->shouldQuit()) {
 		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
+		resetLevelDamageState();
+		resetLevelFrameState();
 		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelInputHistory();
 
 		playInteractiveVideo("LVL3/L3PLAY.ANM");
 		if (_vm->shouldQuit())
@@ -523,15 +531,9 @@ bool InsaneRebel1::runLevel3() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL3/L3NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL3/L3NEW.ANM", "LVL3/L3DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL3/L3DEATH.ANM");
 		return false;
 	}
 
@@ -554,19 +556,8 @@ bool InsaneRebel1::runLevel4() {
 	while (!_vm->shouldQuit()) {
 		loadTuningForLevel(4);
 		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
+		resetLevelDamageState();
+		resetLevelFrameState();
 		_pendingRouteStartFrame = 0;
 		resetGameplayFlagsFromTuning();
 		_killCount = 0;
@@ -610,15 +601,9 @@ bool InsaneRebel1::runLevel4() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL4/L4NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL4/L4NEW.ANM", "LVL4/L4DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL4/L4DEATH.ANM");
 		return false;
 	}
 
@@ -641,57 +626,17 @@ bool InsaneRebel1::runLevel5() {
 	while (!_vm->shouldQuit()) {
 		loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
 		loadTuningForLevel(6);
-		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 1;
+		resetLevelAttemptState(1, 1);
 		_level5SuccessFramesRemaining = 0x14;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
 
 		playInteractiveVideo("LVL5/L5PLAY.ANM");
 		if (_vm->shouldQuit())
 			return false;
 
 		if (_health < 0) {
-			if (_lives > 0) {
-				playCinematic("LVL5/L5NEW.ANM");
-				if (_vm->shouldQuit())
-					return false;
-				_lives--;
+			if (playDeathOrRetry("LVL5/L5NEW.ANM", "LVL5/L5DEATH.ANM"))
 				continue;
-			}
 
-			playCinematic("LVL5/L5DEATH.ANM");
 			return false;
 		}
 
@@ -738,15 +683,9 @@ bool InsaneRebel1::runLevel5() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL5/L5NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL5/L5NEW.ANM", "LVL5/L5DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL5/L5DEATH.ANM");
 		return false;
 	}
 
@@ -769,28 +708,12 @@ bool InsaneRebel1::runLevel6() {
 
 	while (!_vm->shouldQuit()) {
 		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
+		resetLevelDamageState();
+		resetLevelFrameState();
 		resetGameplayFlagsFromTuning();
 		_killCount = 0;
 		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelInputHistory();
 
 		playInteractiveVideo("LVL6/L6PLAY.ANM");
 		if (_vm->shouldQuit())
@@ -805,15 +728,9 @@ bool InsaneRebel1::runLevel6() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL6/L6NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL6/L6NEW.ANM", "LVL6/L6DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL6/L6DEATH.ANM");
 		return false;
 	}
 
@@ -844,42 +761,8 @@ bool InsaneRebel1::runLevel7() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
-		_flyControlMode = 3;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
+		resetLevelAttemptState(3, 0);
 		_driftParam = 0x19;
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
 		resetEnemyShotSlots();
 		_level7WarningFrames = 0;
 		_level7WarningThreshold = 0;
@@ -929,15 +812,9 @@ bool InsaneRebel1::runLevel7() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL7/L7NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL7/L7NEW.ANM", "LVL7/L7DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL7/L7DEATH.ANM");
 		return false;
 	}
 
@@ -966,42 +843,7 @@ bool InsaneRebel1::runLevel8() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
-		_flyControlMode = 3;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_inputAxisDeltaX = 0;
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelAttemptState(3, 0, 17, true);
 
 		// Walker-specific state — RunLevel8Flow (0x18546)
 		_walkerHealth = 100;
@@ -1046,15 +888,9 @@ bool InsaneRebel1::runLevel8() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL8/L8NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL8/L8NEW.ANM", "LVL8/L8DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL8/L8DEATH.ANM");
 		return false;
 	}
 
@@ -1113,45 +949,11 @@ bool InsaneRebel1::runLevel9() {
 
 	while (!_vm->shouldQuit()) {
 		loadTuningForLevel(0x0B);
-		_flyControlMode = 0;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 15;  // On-foot center direction
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
+		resetLevelAttemptState(0, 0, 15);  // On-foot center direction
 		_onFootCharX = 0;
 		_onFootCharY = 0;
 		_onFootAnimCounter = 0;
 		_onFootInitialized = false;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
 
 		while (!_vm->shouldQuit()) {
 			loadTuningForLevel(0x0B);
@@ -1276,15 +1078,9 @@ bool InsaneRebel1::runLevel9() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL9/L9NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL9/L9NEW.ANM", "LVL9/L9DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL9/L9DEATH.ANM");
 		return false;
 	}
 
@@ -1305,41 +1101,7 @@ bool InsaneRebel1::runLevel10() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
-		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelAttemptState(1, 0);
 
 		playInteractiveVideo("LVL10/L10PLAY.ANM");
 		if (_vm->shouldQuit())
@@ -1354,15 +1116,9 @@ bool InsaneRebel1::runLevel10() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL10/L10NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL10/L10NEW.ANM", "LVL10/L10DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL10/L10DEATH.ANM");
 		return false;
 	}
 
@@ -1387,41 +1143,7 @@ bool InsaneRebel1::runLevel11() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
-		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelAttemptState(1, 0);
 		_turretEmitterLeftX = 25;
 		_turretEmitterLeftY = 15;
 
@@ -1451,15 +1173,9 @@ bool InsaneRebel1::runLevel11() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL11/L11NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL11/L11NEW.ANM", "LVL11/L11DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL11/L11DEATH.ANM");
 		return false;
 	}
 
@@ -1484,44 +1200,18 @@ bool InsaneRebel1::runLevel12() {
 
 	while (!_vm->shouldQuit()) {
 		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
+		resetLevelDamageState();
 		_killCount = 0;
 
 		while (!_vm->shouldQuit()) {
 			loadTuningForLevel(0x0F);
-			_frameCounter = 0;
-			_gameCounter = 0;
-			_activeGameOpcode = 0;
-			_gameLatch5D = 0;
-			_gameLatch5F = 0;
+			resetLevelFrameState();
 			_damageFlags = 0;
 			resetGameplayFlagsFromTuning();
-			_targetCount = 0;
-			_prevTargetCount = 0;
-			_lastHitTarget = 0;
-			_shipPosX = kRA1CenterX;
-			_shipPosY = kRA1CenterY;
-			_shipDirIndex = 17;
-			_rollAccum = 0;
-			_liftSmooth = 0;
-			_posAccumX = 0;
-			_posAccumY = 0;
-			_perspectiveX = 0;
-			_perspectiveY = 0;
+			resetLevelTargetingState(false);
+			resetLevelFlightState();
 			_levelGameplayPhase = 0;
-			memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-			memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-			memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-			memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-			_avgInputX = 0;
-			_avgInputY = 0;
+			resetLevelInputHistory();
 
 			playInteractiveVideo("LVL12/L12PLAY.ANM");
 			if (_vm->shouldQuit())
@@ -1546,15 +1236,9 @@ bool InsaneRebel1::runLevel12() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL12/L12NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL12/L12NEW.ANM", "LVL12/L12DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL12/L12DEATH.ANM");
 		return false;
 	}
 
@@ -1579,41 +1263,7 @@ bool InsaneRebel1::runLevel13() {
 		return false;
 
 	while (!_vm->shouldQuit()) {
-		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelAttemptState(1, 0);
 		resetEnemyShotSlots();
 
 		playInteractiveVideo("LVL13/L13PLAY.ANM");
@@ -1628,15 +1278,9 @@ bool InsaneRebel1::runLevel13() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL13/L13NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL13/L13NEW.ANM", "LVL13/L13DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL13/L13DEATH.ANM");
 		return false;
 	}
 
@@ -1662,42 +1306,8 @@ bool InsaneRebel1::runLevel14() {
 
 	while (!_vm->shouldQuit()) {
 		loadTuningForLevel(0x11);
-		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 1;
+		resetLevelAttemptState(1, 1);
 		_level14SuccessFrames = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
 
 		// Phase 1: targeting surface cannons
 		playInteractiveVideo("LVL14/L14PLAY.ANM");
@@ -1742,15 +1352,9 @@ bool InsaneRebel1::runLevel14() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL14/L14NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL14/L14NEW.ANM", "LVL14/L14DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL14/L14DEATH.ANM");
 		return false;
 	}
 
@@ -1776,41 +1380,7 @@ bool InsaneRebel1::runLevel15() {
 
 	while (!_vm->shouldQuit()) {
 		loadTuningForLevel(0x13);
-		_flyControlMode = 1;
-		_health = kMaxHealth;
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_deathCauseIndicator = 0;
-		_frameCounter = 0;
-		_gameCounter = 0;
-		_activeGameOpcode = 0;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		resetGameplayFlagsFromTuning();
-		_killCount = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		_lastHitTarget = 0;
-		_shipPosX = kRA1CenterX;
-		_shipPosY = kRA1CenterY;
-		_shipDirIndex = 17;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		_perspectiveX = 0;
-		_perspectiveY = 0;
-		_levelGameplayPhase = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_avgInputX = 0;
-		_avgInputY = 0;
+		resetLevelAttemptState(1, 0);
 
 		// Phase 1: trench run
 		_levelGameplayPhase = 1;
@@ -1861,15 +1431,9 @@ bool InsaneRebel1::runLevel15() {
 			return !_vm->shouldQuit();
 		}
 
-		if (_lives > 0) {
-			playCinematic("LVL15/L15NEW.ANM");
-			if (_vm->shouldQuit())
-				return false;
-			_lives--;
+		if (playDeathOrRetry("LVL15/L15NEW.ANM", "LVL15/L15DEATH.ANM"))
 			continue;
-		}
 
-		playCinematic("LVL15/L15DEATH.ANM");
 		return false;
 	}
 


Commit: 625f0e9324f088731fac3122d01cf0aedc2763e4
    https://github.com/scummvm/scummvm/commit/625f0e9324f088731fac3122d01cf0aedc2763e4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:11+02:00

Commit Message:
SCUMM: RA1: Extract codecs

Changed paths:
  A engines/scumm/smush/rebel/codec_ra1.cpp
  A engines/scumm/smush/rebel/codec_ra1.h
    engines/scumm/insane/rebel1/levels.cpp
    engines/scumm/module.mk
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index cbe0b1afda2..705cf871353 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -25,38 +25,10 @@
 #include "scumm/scumm_v7.h"
 #include "scumm/file.h"
 #include "scumm/insane/rebel1/rebel.h"
+#include "scumm/smush/rebel/codec_ra1.h"
 
 namespace Scumm {
 
-// From smush/codec1.cpp
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch);
-
-void decodeBomp(byte *dst, const byte *src, int width, int height, int pitch) {
-	while (height--) {
-		byte *dstNext = dst + pitch;
-		const byte *srcNext = src + 2 + READ_LE_UINT16(src);
-		src += 2;
-		int len = width;
-		byte *d = dst;
-		do {
-			int offs = READ_LE_UINT16(src); src += 2;
-			d += offs;
-			len -= offs;
-			if (len <= 0)
-				break;
-			int w = READ_LE_UINT16(src) + 1; src += 2;
-			len -= w;
-			if (len < 0)
-				w += len;
-			memcpy(d, src, w);
-			src += w;
-			d += w;
-		} while (len > 0);
-		dst = dstNext;
-		src = srcNext;
-	}
-}
-
 void resetSpriteBank(RA1SpriteBank &bank) {
 	delete[] bank.sprites;
 	bank.sprites = nullptr;
@@ -175,17 +147,21 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 
 		int pixelCount = bank.sprites[i].width * bank.sprites[i].height;
 		const byte *fobjData = data + fobjOffset + 22;
+		const uint32 fobjSize = READ_BE_UINT32(data + fobjOffset + 4);
+		const int fobjDataSize = (fobjSize >= 14) ? (int)(fobjSize - 14) : 0;
 
 		if (codec == 21) {
 			bank.sprites[i].data = decPtr;
-			decodeBomp(decPtr, fobjData, bank.sprites[i].width,
-					   bank.sprites[i].height, bank.sprites[i].width);
+			smushDecodeRA1SkipCopy(decPtr, fobjData, 0, 0, bank.sprites[i].width,
+				bank.sprites[i].height, bank.sprites[i].width, bank.sprites[i].width,
+				bank.sprites[i].height, fobjDataSize);
 		} else if (codec == 1) {
 			// RA1 codec 1 in NUTs (e.g. LVL2/L2LASER.NUT): RLE where color 0 is transparent.
 			// Decode into a zero-cleared sprite buffer so skipped pixels stay transparent.
 			bank.sprites[i].data = decPtr;
 			smushDecodeRA1Transparent(decPtr, fobjData, 0, 0,
-				bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width);
+				bank.sprites[i].width, bank.sprites[i].height, bank.sprites[i].width,
+				fobjDataSize);
 		} else {
 			bank.sprites[i].width = 0;
 			bank.sprites[i].height = 0;
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index ea6cc308dc7..fedb366f729 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -152,6 +152,7 @@ MODULE_OBJS += \
 	smush/codec37.o \
 	smush/codec47.o \
 	smush/smush_player.o \
+	smush/rebel/codec_ra1.o \
 	smush/rebel/codec_ra2.o \
 	smush/rebel/smush_multi_font.o \
 	smush/rebel/smush_player_ra1.o \
diff --git a/engines/scumm/smush/rebel/codec_ra1.cpp b/engines/scumm/smush/rebel/codec_ra1.cpp
new file mode 100644
index 00000000000..e9c0a0ba9e9
--- /dev/null
+++ b/engines/scumm/smush/rebel/codec_ra1.cpp
@@ -0,0 +1,324 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Rebel Assault 1 SMUSH video codecs
+
+#include "scumm/smush/rebel/codec_ra1.h"
+
+#include "common/endian.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+
+namespace Scumm {
+
+/**
+ * RA1 codec 1: RLE with transparency on pixel 0.
+ * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
+ * allowing the background (restored via FTCH) to show through.
+ */
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize) {
+	if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+		return;
+
+	const byte *srcEnd = src + dataSize;
+	dst += top * pitch;
+	while (height-- > 0 && srcEnd - src >= 2) {
+		const int lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+		byte *rowDst = dst + left;
+		int remaining = width;
+
+		while (remaining > 0 && lineData < lineEnd) {
+			byte code = *lineData++;
+			int num = (code >> 1) + 1;
+			if (num > remaining)
+				num = remaining;
+
+			if (code & 1) {
+				if (lineData >= lineEnd)
+					break;
+				byte color = *lineData++;
+				if (color != 0)
+					memset(rowDst, color, num);
+			} else {
+				const int readable = MIN<int>(num, lineEnd - lineData);
+				for (int j = 0; j < readable; j++) {
+					byte c = lineData[j];
+					if (c != 0)
+						rowDst[j] = c;
+				}
+				lineData += readable;
+				if (readable < num)
+					break;
+			}
+			rowDst += num;
+			remaining -= num;
+		}
+
+		const int rowSize = 2 + lineSize;
+		if (rowSize > srcEnd - src)
+			break;
+		src += rowSize;
+		dst += pitch;
+	}
+}
+
+/**
+ * RA1 codec 21: Skip/copy line codec (FUN_10D41). Clip copy runs without
+ * changing source X; stored cockpit patches can legitimately start offscreen.
+ */
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufWidth, int bufHeight, int dataSize) {
+	if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+		return;
+
+	const byte *srcEnd = src + dataSize;
+	for (int row = 0; row < height && srcEnd - src >= 2; row++) {
+		const uint16 lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+		const int dstY = top + row;
+		int srcX = 0;
+
+		while (srcX < width && lineData < lineEnd) {
+			if (lineData + 2 > lineEnd)
+				break;
+			const uint16 skip = READ_LE_UINT16(lineData);
+			lineData += 2;
+			srcX += skip;
+			if (srcX >= width)
+				break;
+
+			if (lineData + 2 > lineEnd)
+				break;
+			const int copyLen = READ_LE_UINT16(lineData) + 1;
+			lineData += 2;
+
+			const int readableLen = MIN<int>(copyLen, (int)(lineEnd - lineData));
+			const int dstStartX = left + srcX;
+			const int dstEndX = dstStartX + readableLen;
+			if (readableLen > 0 && dstY >= 0 && dstY < bufHeight) {
+				const int clippedStartX = MAX(dstStartX, 0);
+				const int clippedEndX = MIN(dstEndX, bufWidth);
+				if (clippedStartX < clippedEndX) {
+					const int srcSkipX = clippedStartX - dstStartX;
+					memcpy(dst + dstY * pitch + clippedStartX,
+						lineData + srcSkipX, clippedEndX - clippedStartX);
+				}
+			}
+
+			lineData += readableLen;
+			srcX += copyLen;
+			if (readableLen < copyLen)
+				break;
+		}
+
+		const int rowSize = 2 + lineSize;
+		if (rowSize > srcEnd - src)
+			break;
+		src += rowSize;
+	}
+}
+
+/**
+ * RA1 codec 23: Additive line-update overlay (FUN_10B40).
+ */
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufWidth, int bufHeight, uint8 paletteBase, int dataSize) {
+	if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || dataSize <= 0)
+		return;
+
+	const uint8 colorDelta = (uint8)(paletteBase - 0x30);
+	const byte *srcEnd = src + dataSize;
+	for (int row = 0; row < height && srcEnd - src >= 2; row++) {
+		const uint16 lineSize = READ_LE_UINT16(src);
+		const byte *lineData = src + 2;
+		const byte *lineEnd = lineData + MIN<int>(lineSize, srcEnd - lineData);
+		const int dstY = top + row;
+		int srcX = 0;
+		while (srcX < width && lineData < lineEnd) {
+			const int skip = *lineData++;
+			srcX += skip;
+			if (srcX >= width || lineData >= lineEnd)
+				break;
+			const int runLength = (int)(*lineData++);
+			const int dstStartX = left + srcX;
+			const int dstEndX = dstStartX + runLength;
+			if (dstY >= 0 && dstY < bufHeight) {
+				const int clippedStartX = MAX(dstStartX, 0);
+				const int clippedEndX = MIN(dstEndX, bufWidth);
+				if (clippedStartX < clippedEndX) {
+					byte *dstPixel = dst + dstY * pitch + clippedStartX;
+					for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
+						*dstPixel = (byte)(*dstPixel + colorDelta);
+				}
+			}
+			srcX += runLength;
+		}
+
+		const int rowSize = 2 + lineSize;
+		if (rowSize > srcEnd - src)
+			break;
+		src += rowSize;
+	}
+}
+
+/**
+ * RA1 codec 2: Scatter/point draw (FUN_110D7).
+ */
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
+	if (dst == nullptr || src == nullptr || pitch <= 0 || dataSize <= 0)
+		return;
+
+	int curX = left;
+	int curY = top;
+	while (dataSize >= 4) {
+		int16 dx = (int16)READ_LE_UINT16(src);
+		uint8 dy = src[2];
+		uint8 pixel = src[3];
+		src += 4;
+		dataSize -= 4;
+		curX += dx;
+		curY += dy;
+		if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight)
+			dst[curY * pitch + curX] = pixel;
+	}
+}
+
+// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
+static uint8 s_ra1C4Tbl[2][256][16];
+static uint16 s_ra1C4Param = 0xFFFF;
+
+static void ra1Codec4GenTiles(uint16 param1) {
+	uint8 *dst = &s_ra1C4Tbl[0][0][0];
+	for (int i = 1; i < 16; i += 2) {
+		for (int k = 0; k < 16; k++) {
+			int j = i + param1, l = k + param1;
+			int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+			if (j == m || l == m) {
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
+			} else {
+				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
+				*dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
+				*dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
+			}
+		}
+	}
+	for (int i = 0; i < 16; i += 2) {
+		for (int k = 0; k < 16; k++) {
+			int j = i + param1, l = k + param1;
+			int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
+			if (m == j || m == l) {
+				*dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
+				*dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
+				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
+				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
+			} else {
+				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
+				*dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
+				*dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
+			}
+		}
+	}
+}
+
+static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
+	uint8 *dst = &s_ra1C4Tbl[1][0][0];
+	int loop = param2 * 8;
+	if (param2 > 256 || remaining < loop)
+		return false;
+	for (int i = 0; i < loop; i++) {
+		byte c = *src++;
+		remaining--;
+		*dst++ = (c >> 4) + clr;
+		*dst++ = (c & 0xF) + clr;
+	}
+	return true;
+}
+
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec) {
+	if (dst == nullptr || src == nullptr || width <= 0 || height <= 0 || pitch <= 0 || bufHeight <= 0 || dataSize <= 0)
+		return;
+
+	const int mx = pitch;
+	const int my = bufHeight;
+	if (s_ra1C4Param != param) {
+		ra1Codec4GenTiles(param);
+		s_ra1C4Param = param;
+	}
+	int remaining = dataSize;
+	const byte *data = src;
+	if (parm2 > 0) {
+		if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
+			warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
+			return;
+		}
+	}
+	for (int j = 0; j < width; j += 4) {
+		byte mask = 0, bits = 0;
+		int x = left + j;
+		for (int i = 0; i < height; i += 4) {
+			int y = top + i;
+			int bit = 0;
+			if (parm2 > 0) {
+				if (bits == 0) {
+					if (remaining < 1)
+						return;
+					mask = *data++;
+					remaining--;
+					bits = 8;
+				}
+				bit = !!(mask & 0x80);
+				mask <<= 1;
+				bits--;
+			}
+			if (remaining < 1)
+				return;
+			byte idx = *data++;
+			remaining--;
+			if (bit == 0 && idx == 0x80 && codec != 5)
+				continue;
+			if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
+				continue;
+			const byte *gs = &s_ra1C4Tbl[bit][idx][0];
+			if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
+				for (int k = 0; k < 4; k++, gs += 4)
+					memcpy(dst + x + (y + k) * pitch, gs, 4);
+			} else {
+				for (int k = 0; k < 4; k++)
+					for (int l = 0; l < 4; l++, gs++) {
+						int yo = y + k, xo = x + l;
+						if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
+							*(dst + yo * pitch + xo) = *gs;
+					}
+			}
+		}
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/codec_ra1.h b/engines/scumm/smush/rebel/codec_ra1.h
new file mode 100644
index 00000000000..b509120323b
--- /dev/null
+++ b/engines/scumm/smush/rebel/codec_ra1.h
@@ -0,0 +1,40 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_REBEL_CODEC_RA1_H
+#define SCUMM_SMUSH_REBEL_CODEC_RA1_H
+
+#include "common/scummsys.h"
+
+namespace Scumm {
+
+void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch, int dataSize);
+void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufWidth, int bufHeight, int dataSize);
+void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufWidth, int bufHeight, uint8 paletteBase, int dataSize);
+void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize);
+void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
+		int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec);
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index 3c9c4c71e96..bb5527bff00 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -29,6 +29,7 @@
 
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/codec_ra1.h"
 #include "scumm/smush/smush_font.h"
 #include "scumm/smush/rebel/codec_ra2.h"
 #include "scumm/smush/rebel/smush_player_ra1.h"
@@ -483,251 +484,6 @@ void SmushPlayerRebel1::handleGameParseNextFrame() {
 	processDispatches(_smushAudioSampleRate / _speed);
 }
 
-// Forward declarations for RA1 codec functions (defined in smush_player.cpp and codec1.cpp)
-/**
- * RA1 codec 1: RLE with transparency on pixel 0.
- * Same BOMP encoding as smushDecodeRLE but pixel value 0 is not written,
- * allowing the background (restored via FTCH) to show through.
- */
-void smushDecodeRA1Transparent(byte *dst, const byte *src, int left, int top, int width, int height, int pitch) {
-	dst += top * pitch;
-	do {
-		byte *rowDst = dst + left;
-		const byte *lineData = src + 2;
-		int remaining = width;
-
-		while (remaining > 0) {
-			byte code = *lineData++;
-			byte num = (code >> 1) + 1;
-			if (num > remaining)
-				num = remaining;
-			if (code & 1) {
-				byte color = *lineData++;
-				if (color != 0)
-					memset(rowDst, color, num);
-			} else {
-				for (int j = 0; j < num; j++) {
-					byte c = lineData[j];
-					if (c != 0)
-						rowDst[j] = c;
-				}
-				lineData += num;
-			}
-			rowDst += num;
-			remaining -= num;
-		}
-
-		src += READ_LE_UINT16(src) + 2;
-		dst += pitch;
-	} while (--height);
-}
-/**
- * RA1 codec 21: Skip/copy line codec (FUN_10D41). Clip copy runs without
- * changing source X; stored cockpit patches can legitimately start offscreen.
- */
-void smushDecodeRA1SkipCopy(byte *dst, const byte *src, int left, int top, int width, int height,
-		int pitch, int bufWidth, int bufHeight) {
-	for (int row = 0; row < height; row++) {
-		const uint16 lineSize = READ_LE_UINT16(src);
-		const byte *lineData = src + 2;
-		const byte *lineEnd = lineData + lineSize;
-		const int dstY = top + row;
-		int srcX = 0;
-
-		while (srcX < width && lineData < lineEnd) {
-			if (lineData + 2 > lineEnd)
-				break;
-			const uint16 skip = READ_LE_UINT16(lineData);
-			lineData += 2;
-			srcX += skip;
-			if (srcX >= width)
-				break;
-
-			if (lineData + 2 > lineEnd)
-				break;
-			const int copyLen = READ_LE_UINT16(lineData) + 1;
-			lineData += 2;
-
-			const int readableLen = MIN<int>(copyLen, (int)(lineEnd - lineData));
-			const int dstStartX = left + srcX;
-			const int dstEndX = dstStartX + readableLen;
-			if (readableLen > 0 && dstY >= 0 && dstY < bufHeight) {
-				const int clippedStartX = MAX(dstStartX, 0);
-				const int clippedEndX = MIN(dstEndX, bufWidth);
-				if (clippedStartX < clippedEndX) {
-					const int srcSkipX = clippedStartX - dstStartX;
-					memcpy(dst + dstY * pitch + clippedStartX,
-						lineData + srcSkipX, clippedEndX - clippedStartX);
-				}
-			}
-
-			lineData += readableLen;
-			srcX += copyLen;
-			if (readableLen < copyLen)
-				break;
-		}
-		src += lineSize + 2;
-	}
-}
-
-/**
- * RA1 codec 23: Additive line-update overlay (FUN_10B40).
- */
-void smushDecodeRA1AdditiveLineUpdate(byte *dst, const byte *src, int left, int top, int width, int height,
-		int pitch, int bufWidth, int bufHeight, uint8 paletteBase) {
-	const uint8 colorDelta = (uint8)(paletteBase - 0x30);
-	for (int row = 0; row < height; row++) {
-		const uint16 lineSize = READ_LE_UINT16(src);
-		const byte *lineData = src + 2;
-		const byte *lineEnd = lineData + lineSize;
-		const int dstY = top + row;
-		int srcX = 0;
-		while (srcX < width && lineData < lineEnd) {
-			const int skip = *lineData++; srcX += skip;
-			if (srcX >= width || lineData >= lineEnd) break;
-			const int runLength = (int)(*lineData++);
-			const int dstStartX = left + srcX;
-			const int dstEndX = dstStartX + runLength;
-			if (dstY >= 0 && dstY < bufHeight) {
-				const int clippedStartX = MAX(dstStartX, 0);
-				const int clippedEndX = MIN(dstEndX, bufWidth);
-				if (clippedStartX < clippedEndX) {
-					byte *dstPixel = dst + dstY * pitch + clippedStartX;
-					for (int x = clippedStartX; x < clippedEndX; x++, dstPixel++)
-						*dstPixel = (byte)(*dstPixel + colorDelta);
-				}
-			}
-			srcX += runLength;
-		}
-		src += lineSize + 2;
-	}
-}
-
-/**
- * RA1 codec 2: Scatter/point draw (FUN_110D7).
- */
-void smushDecodeRA1Scatter(byte *dst, const byte *src, int left, int top, int bufWidth, int bufHeight, int pitch, int dataSize) {
-	int curX = left;
-	int curY = top;
-	while (dataSize >= 4) {
-		int16 dx = (int16)READ_LE_UINT16(src);
-		uint8 dy = src[2];
-		uint8 pixel = src[3];
-		src += 4; dataSize -= 4;
-		curX += dx; curY += dy;
-		if (curX >= 0 && curY >= 0 && curX < bufWidth && curY < bufHeight)
-			dst[curY * pitch + curX] = pixel;
-	}
-}
-
-// RA1 codec 4/5: block-based dithered codec with 4x4 tile lookup tables
-static uint8 s_ra1C4Tbl[2][256][16];
-static uint16 s_ra1C4Param = 0xFFFF;
-
-static void ra1Codec4GenTiles(uint16 param1) {
-	uint8 *dst = &s_ra1C4Tbl[0][0][0];
-	for (int i = 1; i < 16; i += 2) {
-		for (int k = 0; k < 16; k++) {
-			int j = i + param1, l = k + param1;
-			int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
-			if (j == m || l == m) {
-				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
-				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = j;
-				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = j;
-				*dst++ = l; *dst++ = l; *dst++ = j; *dst++ = l;
-			} else {
-				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
-				*dst++ = m; *dst++ = m; *dst++ = n; *dst++ = j;
-				*dst++ = o; *dst++ = o; *dst++ = m; *dst++ = n;
-				*dst++ = l; *dst++ = l; *dst++ = o; *dst++ = m;
-			}
-		}
-	}
-	for (int i = 0; i < 16; i += 2) {
-		for (int k = 0; k < 16; k++) {
-			int j = i + param1, l = k + param1;
-			int m = (j + l) / 2, n = (j + m) / 2, o = (l + m) / 2;
-			if (m == j || m == l) {
-				*dst++ = j; *dst++ = j; *dst++ = l; *dst++ = j;
-				*dst++ = j; *dst++ = j; *dst++ = j; *dst++ = l;
-				*dst++ = l; *dst++ = j; *dst++ = l; *dst++ = l;
-				*dst++ = j; *dst++ = l; *dst++ = j; *dst++ = l;
-			} else {
-				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
-				*dst++ = j; *dst++ = j; *dst++ = n; *dst++ = m;
-				*dst++ = n; *dst++ = n; *dst++ = m; *dst++ = o;
-				*dst++ = m; *dst++ = m; *dst++ = o; *dst++ = l;
-			}
-		}
-	}
-}
-
-static bool ra1Codec4LoadTiles(const byte *&src, int &remaining, uint16 param2, uint8 clr) {
-	uint8 *dst = &s_ra1C4Tbl[1][0][0];
-	int loop = param2 * 8;
-	if (param2 > 256 || remaining < loop)
-		return false;
-	for (int i = 0; i < loop; i++) {
-		byte c = *src++;
-		remaining--;
-		*dst++ = (c >> 4) + clr;
-		*dst++ = (c & 0xF) + clr;
-	}
-	return true;
-}
-
-void smushDecodeRA1Block(byte *dst, const byte *src, int left, int top, int width, int height,
-						 int pitch, int bufHeight, int dataSize, uint8 param, uint16 parm2, int codec) {
-	const int mx = pitch;
-	const int my = bufHeight;
-	if (s_ra1C4Param != param) {
-		ra1Codec4GenTiles(param);
-		s_ra1C4Param = param;
-	}
-	int remaining = dataSize;
-	const byte *data = src;
-	if (parm2 > 0) {
-		if (!ra1Codec4LoadTiles(data, remaining, parm2, param)) {
-			warning("smushDecodeRA1Block: not enough data for tile load (parm2=%d)", parm2);
-			return;
-		}
-	}
-	for (int j = 0; j < width; j += 4) {
-		byte mask = 0, bits = 0;
-		int x = left + j;
-		for (int i = 0; i < height; i += 4) {
-			int y = top + i;
-			int bit = 0;
-			if (parm2 > 0) {
-				if (bits == 0) {
-					if (remaining < 1) return;
-					mask = *data++; remaining--; bits = 8;
-				}
-				bit = !!(mask & 0x80);
-				mask <<= 1; bits--;
-			}
-			if (remaining < 1) return;
-			byte idx = *data++; remaining--;
-			if (bit == 0 && idx == 0x80 && codec != 5)
-				continue;
-			if (y >= my || (y + 4) < 0 || (x + 4) < 0 || x >= mx)
-				continue;
-			const byte *gs = &s_ra1C4Tbl[bit][idx][0];
-			if (y >= 0 && x >= 0 && (y + 4) <= my && (x + 4) <= mx) {
-				for (int k = 0; k < 4; k++, gs += 4)
-					memcpy(dst + x + (y + k) * pitch, gs, 4);
-			} else {
-				for (int k = 0; k < 4; k++)
-					for (int l = 0; l < 4; l++, gs++) {
-						int yo = y + k, xo = x + l;
-						if (yo >= 0 && yo < my && xo >= 0 && xo < mx)
-							*(dst + yo * pitch + xo) = *gs;
-					}
-			}
-		}
-	}
-}
-
 bool SmushPlayerRebel1::handleGameFrameBufferSelect(int codec, int width, int height) {
 	// RA1 sub-fullscreen frames render into _specialBuffer at their (left, top) offset position.
 	int bufSize = 384 * 242;
@@ -793,7 +549,7 @@ bool SmushPlayerRebel1::handleGameAdjustCoords(int codec, int &left, int &top, i
 bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int left, int top, int width, int height, int pitch, int dataSize, uint8 param, uint16 parm2) {
 	switch (codec) {
 	case SMUSH_CODEC_RLE:
-		smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch);
+		smushDecodeRA1Transparent(_dst, src, left, top, width, height, pitch, dataSize);
 		return true;
 	case SMUSH_CODEC_RLE_ALT:
 		src = smushSkipRLELines(src, dataSize, _ra1FrameSourceSkipY);
@@ -810,7 +566,7 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		return true;
 	case SMUSH_CODEC_LINE_UPDATE:
 		smushDecodeRA1SkipCopy(_dst, src, left, top, width, height, pitch,
-			pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight);
+			pitch, (_dst == _specialBuffer) ? _height : _vm->_screenHeight, dataSize);
 		return true;
 	case SMUSH_CODEC_SKIP_RLE: {
 		const int bufWidth = pitch;
@@ -820,7 +576,7 @@ bool SmushPlayerRebel1::handleGameCodecDecode(int codec, const uint8 *src, int l
 		// from this value before decoding, which Level 8 uses for the walker
 		// armor layers.
 		smushDecodeRA1AdditiveLineUpdate(_dst, src, left, top, width, height,
-			pitch, bufWidth, bufHeight, param);
+			pitch, bufWidth, bufHeight, param, dataSize);
 		return true;
 	}
 	default:


Commit: 0e4cfe0eed25a91c0e26cf4641aa2b257f98d476
    https://github.com/scummvm/scummvm/commit/0e4cfe0eed25a91c0e26cf4641aa2b257f98d476
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:12+02:00

Commit Message:
SCUMM: RA1: Split handleFrame

Changed paths:
    engines/scumm/smush/rebel/smush_player_ra1.cpp
    engines/scumm/smush/rebel/smush_player_ra1.h


diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index bb5527bff00..e33221d76ee 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -723,9 +723,175 @@ static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize)
 	return false;
 }
 
+void SmushPlayerRebel1::ra1HandleFrameAudioChunk(int32 subSize, Common::SeekableReadStream &b) {
+	if (_compressedFileMode || isFastForwardingCurrentFrame())
+		return;
+
+	uint8 *audioChunk = (uint8 *)malloc(subSize + 8);
+	if (audioChunk == nullptr)
+		return;
+
+	b.seek(-8, SEEK_CUR);
+	b.read(audioChunk, subSize + 8);
+	feedAudio(audioChunk, 0, 127, 0, 0);
+	free(audioChunk);
+}
+
+void SmushPlayerRebel1::ra1HandleGameFrameChunk(int32 subSize, Common::SeekableReadStream &b, bool fastForwarding) {
+	if (!fastForwarding && _insane) {
+		InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
+		rebel1->handleGameChunk(subSize, b);
+	}
+}
+
+void SmushPlayerRebel1::ra1HandleObjOverlayFrameChunk(int32 objDataSize, Common::SeekableReadStream &b, bool fastForwarding) {
+	if (objDataSize <= 0)
+		return;
+
+	byte *objBuf = (byte *)malloc(objDataSize);
+	if (objBuf == nullptr)
+		return;
+
+	b.read(objBuf, objDataSize);
+
+	int32 objPos = 0;
+	while (objPos + 8 < objDataSize) {
+		uint32 embTag = READ_BE_UINT32(objBuf + objPos);
+		uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
+		int32 embRemaining = objDataSize - objPos - 8;
+
+		bool recognized = (embTag == MKTAG('F','O','B','J') ||
+		                   embTag == MKTAG('G','A','M','E') ||
+		                   embTag == MKTAG('G','A','M','2') ||
+		                   embTag == MKTAG('P','S','A','D'));
+
+		if (!recognized || embSize > (uint32)embRemaining) {
+			objPos++;
+			continue;
+		}
+
+		if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
+			Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+			handleFrameObject(embSize, embStream);
+
+			if (_ra1ObjOverlayData == nullptr ||
+			    (int32)embSize > _ra1ObjOverlayDataSize) {
+				free(_ra1ObjOverlayData);
+				_ra1ObjOverlayDataSize = embSize;
+				_ra1ObjOverlayData = (byte *)malloc(embSize);
+				memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
+				_ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
+				_ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
+				_ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
+				_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
+				_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
+			}
+		} else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
+			Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
+			ra1HandleGameFrameChunk(embSize, embStream, fastForwarding);
+		} else if (embTag == MKTAG('P','S','A','D')) {
+			if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
+				uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
+				if (audioBuf == nullptr)
+					break;
+				memcpy(audioBuf, objBuf + objPos, embSize + 8);
+				feedAudio(audioBuf, 0, 127, 0, 0);
+				free(audioBuf);
+			}
+		}
+
+		objPos += 8 + embSize;
+		if (embSize & 1)
+			objPos++;
+	}
+
+	free(objBuf);
+}
+
+bool SmushPlayerRebel1::ra1HandleUnknownFrameChunk(uint32 subType, int32 subSize) {
+	// Original FUN_1FDBC: unknown uppercase tag -> silently stop
+	byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
+	byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
+	if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
+	    tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
+		debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
+		return true;
+	}
+
+	error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
+	return false;
+}
+
+bool SmushPlayerRebel1::ra1DispatchFrameChunk(uint32 subType, int32 subSize, int32 &frameSize,
+		Common::SeekableReadStream &b, bool fastForwarding) {
+	switch (subType) {
+	case MKTAG('N','P','A','L'):
+		handleNewPalette(subSize, b);
+		break;
+	case MKTAG('F','O','B','J'):
+		handleFrameObject(subSize, b);
+		break;
+	case MKTAG('Z','F','O','B'):
+		handleZlibFrameObject(subSize, b);
+		break;
+	case MKTAG('P','S','A','D'):
+	case MKTAG('P','V','O','C'):
+		ra1HandleFrameAudioChunk(subSize, b);
+		break;
+	case MKTAG('T','R','E','S'):
+	case MKTAG('T','E','X','T'):
+		handleTextResource(subType, subSize, b);
+		break;
+	case MKTAG('X','P','A','L'):
+		ra1HandleDeltaPalette(subSize, b);
+		break;
+	case MKTAG('I','A','C','T'):
+		handleIACT(subSize, b);
+		break;
+	case MKTAG('S','T','O','R'):
+		handleStore(subSize, b);
+		break;
+	case MKTAG('F','T','C','H'):
+		handleFetch(subSize, b);
+		break;
+	case MKTAG('S','K','I','P'):
+		_insane->procSKIP(subSize, b);
+		break;
+	case MKTAG('G','O','S','T'):
+		handleGameGost(subSize, b);
+		break;
+	case MKTAG('G','A','M','E'):
+	case MKTAG('G','A','M','2'):
+		ra1HandleGameFrameChunk(subSize, b, fastForwarding);
+		break;
+	case MKTAG('O','B','J','\0'):
+		ra1HandleObjOverlayFrameChunk(frameSize - 8, b, fastForwarding);
+		frameSize = 0;
+		return true;
+	case MKTAG('F','A','D','E'):
+		ra1HandleFade(subSize, b);
+		break;
+	case MKTAG('S','E','G','A'):
+	case MKTAG('A','D','L',' '):
+	case MKTAG('A','D','L','2'):
+	case MKTAG('S','B','L',' '):
+	case MKTAG('S','B','L','2'):
+	case MKTAG('P','S','D','2'):
+		debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame: skipping chunk %s (%d bytes)", tag2str(subType), subSize);
+		break;
+	default:
+		if (ra1HandleUnknownFrameChunk(subType, subSize)) {
+			frameSize = 0;
+			return true;
+		}
+		break;
+	}
+
+	return false;
+}
+
 void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream &b) {
 	debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame(%d)", _frame);
-	uint8 *audioChunk = nullptr;
 	_skipNext = false;
 	handleGameFrameStart();
 	const bool fastForwarding = isFastForwardingCurrentFrame();
@@ -795,144 +961,8 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 			break;
 		}
 
-		switch (subType) {
-		case MKTAG('N','P','A','L'):
-			handleNewPalette(subSize, b);
-			break;
-		case MKTAG('F','O','B','J'):
-			handleFrameObject(subSize, b);
-			break;
-		case MKTAG('Z','F','O','B'):
-			handleZlibFrameObject(subSize, b);
-			break;
-		case MKTAG('P','S','A','D'):
-		case MKTAG('P','V','O','C'):
-			if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
-				audioChunk = (uint8 *)malloc(subSize + 8);
-				b.seek(-8, SEEK_CUR);
-				b.read(audioChunk, subSize + 8);
-				feedAudio(audioChunk, 0, 127, 0, 0);
-				free(audioChunk);
-				audioChunk = nullptr;
-			}
-			break;
-		case MKTAG('T','R','E','S'):
-		case MKTAG('T','E','X','T'):
-			handleTextResource(subType, subSize, b);
-			break;
-		case MKTAG('X','P','A','L'):
-			ra1HandleDeltaPalette(subSize, b);
-			break;
-		case MKTAG('I','A','C','T'):
-			handleIACT(subSize, b);
-			break;
-		case MKTAG('S','T','O','R'):
-			handleStore(subSize, b);
-			break;
-		case MKTAG('F','T','C','H'):
-			handleFetch(subSize, b);
-			break;
-		case MKTAG('S','K','I','P'):
-			_insane->procSKIP(subSize, b);
-			break;
-		case MKTAG('G','O','S','T'):
-			handleGameGost(subSize, b);
-			break;
-		case MKTAG('G','A','M','E'):
-		case MKTAG('G','A','M','2'): {
-			if (!fastForwarding) {
-				InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
-				rebel1->handleGameChunk(subSize, b);
-			}
-			break;
-		}
-		case MKTAG('O','B','J','\0'): {
-			// RA1 object overlay chunk: variable-size header + embedded FOBJ/GAME/PSAD.
-			int32 objDataSize = frameSize - 8;
-			if (objDataSize > 0) {
-				byte *objBuf = (byte *)malloc(objDataSize);
-				b.read(objBuf, objDataSize);
-
-				int32 objPos = 0;
-				while (objPos + 8 < objDataSize) {
-					uint32 embTag = READ_BE_UINT32(objBuf + objPos);
-					uint32 embSize = READ_BE_UINT32(objBuf + objPos + 4);
-					int32 embRemaining = objDataSize - objPos - 8;
-
-					bool recognized = (embTag == MKTAG('F','O','B','J') ||
-					                   embTag == MKTAG('G','A','M','E') ||
-					                   embTag == MKTAG('G','A','M','2') ||
-					                   embTag == MKTAG('P','S','A','D'));
-
-					if (!recognized || embSize > (uint32)embRemaining) {
-						objPos++;
-						continue;
-					}
-
-					if (embTag == MKTAG('F','O','B','J') && embSize >= 14) {
-						Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
-						handleFrameObject(embSize, embStream);
-
-						if (_ra1ObjOverlayData == nullptr ||
-						    (int32)embSize > _ra1ObjOverlayDataSize) {
-							free(_ra1ObjOverlayData);
-							_ra1ObjOverlayDataSize = embSize;
-							_ra1ObjOverlayData = (byte *)malloc(embSize);
-							memcpy(_ra1ObjOverlayData, objBuf + objPos + 8, embSize);
-							_ra1ObjOverlayCodec = objBuf[objPos + 8] & 0xFF;
-							_ra1ObjOverlayLeft = (int16)READ_LE_UINT16(objBuf + objPos + 10);
-							_ra1ObjOverlayTop = (int16)READ_LE_UINT16(objBuf + objPos + 12);
-							_ra1ObjOverlayWidth = READ_LE_UINT16(objBuf + objPos + 14);
-							_ra1ObjOverlayHeight = READ_LE_UINT16(objBuf + objPos + 16);
-						}
-					} else if (embTag == MKTAG('G','A','M','E') || embTag == MKTAG('G','A','M','2')) {
-						if (!fastForwarding) {
-							Common::MemoryReadStream embStream(objBuf + objPos + 8, embSize);
-							InsaneRebel1 *rebel1 = (InsaneRebel1 *)_insane;
-							rebel1->handleGameChunk(embSize, embStream);
-						}
-					} else if (embTag == MKTAG('P','S','A','D')) {
-						if (!_compressedFileMode && !isFastForwardingCurrentFrame()) {
-							uint8 *audioBuf = (uint8 *)malloc(embSize + 8);
-							memcpy(audioBuf, objBuf + objPos, embSize + 8);
-							feedAudio(audioBuf, 0, 127, 0, 0);
-							free(audioBuf);
-						}
-					}
-
-					objPos += 8 + embSize;
-					if (embSize & 1)
-						objPos++;
-				}
-				free(objBuf);
-			}
-			frameSize = 0;
+		if (ra1DispatchFrameChunk(subType, subSize, frameSize, b, fastForwarding))
 			continue;
-		}
-		case MKTAG('F','A','D','E'):
-			ra1HandleFade(subSize, b);
-			break;
-		case MKTAG('S','E','G','A'):
-		case MKTAG('A','D','L',' '):
-		case MKTAG('A','D','L','2'):
-		case MKTAG('S','B','L',' '):
-		case MKTAG('S','B','L','2'):
-		case MKTAG('P','S','D','2'):
-			debugC(DEBUG_SMUSH, "SmushPlayerRebel1::handleFrame: skipping chunk %s (%d bytes)", tag2str(subType), subSize);
-			break;
-		default: {
-			// Original FUN_1FDBC: unknown uppercase tag → silently stop
-			byte tb0 = (subType >> 24) & 0xFF, tb1 = (subType >> 16) & 0xFF;
-			byte tb2 = (subType >> 8) & 0xFF, tb3 = subType & 0xFF;
-			if (tb0 > 0x40 && tb0 < 0x5B && tb1 > 0x40 && tb1 < 0x5B &&
-			    tb2 > 0x40 && tb2 < 0x5B && tb3 > 0x40 && tb3 < 0x5B) {
-				debug(5, "RA1: unknown uppercase tag %s at frame %d, stopping frame parse", tag2str(subType), _frame);
-				frameSize = 0;
-				continue;
-			}
-			error("Unknown frame subChunk found : %s, %d", tag2str(subType), subSize);
-		}
-		}
 
 		frameSize -= subSize + 8;
 		b.seek(subOffset + subSize, SEEK_SET);
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.h b/engines/scumm/smush/rebel/smush_player_ra1.h
index 5d54cfd0b1a..ff1dc1befdc 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.h
+++ b/engines/scumm/smush/rebel/smush_player_ra1.h
@@ -64,6 +64,12 @@ private:
 	void ra1HandleFade(int32 subSize, Common::SeekableReadStream &b);
 	SmushFont *ra1GetFont(int font);
 	void ra1HandleText(int32 subSize, Common::SeekableReadStream &b);
+	void ra1HandleFrameAudioChunk(int32 subSize, Common::SeekableReadStream &b);
+	void ra1HandleGameFrameChunk(int32 subSize, Common::SeekableReadStream &b, bool fastForwarding);
+	void ra1HandleObjOverlayFrameChunk(int32 objDataSize, Common::SeekableReadStream &b, bool fastForwarding);
+	bool ra1HandleUnknownFrameChunk(uint32 subType, int32 subSize);
+	bool ra1DispatchFrameChunk(uint32 subType, int32 subSize, int32 &frameSize,
+		Common::SeekableReadStream &b, bool fastForwarding);
 
 	// RA1 clean frame buffer for delta source restoration
 	byte *_ra1CleanFrame;


Commit: 1c4b90088378d95b36f3d6d3b2ab28a91bdd7ffd
    https://github.com/scummvm/scummvm/commit/1c4b90088378d95b36f3d6d3b2ab28a91bdd7ffd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:12+02:00

Commit Message:
SCUMM: RA1: Split playInteractiveVideo

Changed paths:
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/runlevels.cpp


diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index b8b1678ffc0..4c7baa881d4 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -161,6 +161,12 @@ private:
 
 	// Play interactive gameplay video (with ship physics + HUD)
 	void playInteractiveVideo(const char *filename, int32 startFrame = 0);
+	void resetInteractiveVideoAudio();
+	void setupInteractiveVideoState(int32 startFrame);
+	void resolveSeek(const char *filename, int32 startFrame, int32 &videoOffset, int32 &videoStartFrame);
+	void captureInteractiveVideoInput();
+	void releaseInteractiveVideoInput();
+	void playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame);
 	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
 	void loadLevelSprites(int level);
 	void updateShipPhysics();
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 80da8bfcbfa..2e6334daca2 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1556,19 +1556,16 @@ void InsaneRebel1::runGame() {
 	}
 }
 
-// Play interactive gameplay video (with ship physics + HUD).
-void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
-	debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+void InsaneRebel1::resetInteractiveVideoAudio() {
+	terminateAudio();
+	initAudio(_audioSampleRate);
+}
+
+void InsaneRebel1::setupInteractiveVideoState(int32 startFrame) {
 	const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
 	const bool resumingRoute = startFrame > 0;
 	const bool preserveRuntimeState = resumingRoute || level7RouteSplice;
 	const bool preserveVideoState = resumingRoute && !level7RouteSplice;
-	int32 videoStartFrame = 0;
-	int32 videoOffset = 0;
-
-	// Stop any leftover audio from previous video
-	terminateAudio();
-	initAudio(_audioSampleRate);
 
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
@@ -1588,6 +1585,15 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	splayer->setCurVideoFlags(0x28);
 	splayer->setFastForwardFromFrame(0);
 	splayer->setFastForwardToFrame(0);
+}
+
+void InsaneRebel1::resolveSeek(const char *filename, int32 startFrame, int32 &videoOffset, int32 &videoStartFrame) {
+	const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
+	const bool resumingRoute = startFrame > 0;
+
+	videoStartFrame = 0;
+	videoOffset = 0;
+
 	if (_currentLevel == 6 && level7RouteSplice) {
 		// DOS opens the route ANM from the beginning, then the armed frame gate
 		// suppresses until the adjusted target. With ScummVM's delayed cutover,
@@ -1630,6 +1636,10 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 		debug(1, "RA1 L14 splice: L14PLAY2 timelineFrame=%d -> L14PLY2B frame 0",
 			(int)startFrame);
 	}
+}
+
+void InsaneRebel1::captureInteractiveVideoInput() {
+	const bool level7RouteSplice = (_currentLevel == 6 && _levelRouteIndex > 0);
 
 	// Center mouse, hide system cursor (we draw our own), lock mouse to window.
 	// Level 7 route splices happen inside one original gameplay loop, so keep
@@ -1644,11 +1654,29 @@ void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame)
 	}
 	CursorMan.showMouse(false);
 	g_system->lockMouse(true);
+}
+
+void InsaneRebel1::releaseInteractiveVideoInput() {
+	g_system->lockMouse(false);
+}
 
-	splayer->play(filename, 12, videoOffset, videoStartFrame);
+void InsaneRebel1::playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame) {
+	_vm->_splayer->play(filename, 12, videoOffset, videoStartFrame);
 	_interactiveVideoActive = false;
+}
 
-	g_system->lockMouse(false);
+// Play interactive gameplay video (with ship physics + HUD).
+void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
+	debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+	int32 videoStartFrame = 0;
+	int32 videoOffset = 0;
+
+	resetInteractiveVideoAudio();
+	setupInteractiveVideoState(startFrame);
+	resolveSeek(filename, startFrame, videoOffset, videoStartFrame);
+	captureInteractiveVideoInput();
+	playInteractiveVideoFile(filename, videoOffset, videoStartFrame);
+	releaseInteractiveVideoInput();
 }
 
 } // End of namespace Scumm


Commit: 3fc5813aa35acd8579e551c4d209062e6de7e97a
    https://github.com/scummvm/scummvm/commit/3fc5813aa35acd8579e551c4d209062e6de7e97a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:12+02:00

Commit Message:
SCUMM: RA1: Refactor menu handling and rendering

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


diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 1d6d033016a..c7628ea5fea 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -149,6 +149,60 @@ static char normalizeRebel1PasscodeChar(char c) {
 	return c;
 }
 
+static RA1MenuCommand getRebel1MenuCommandFromAction(ScummAction action) {
+	switch (action) {
+	case kScummActionInsaneUp:
+		return kRA1MenuCommandUp;
+	case kScummActionInsaneDown:
+		return kRA1MenuCommandDown;
+	case kScummActionInsaneLeft:
+		return kRA1MenuCommandLeft;
+	case kScummActionInsaneRight:
+		return kRA1MenuCommandRight;
+	case kScummActionInsaneAttack:
+		return kRA1MenuCommandAccept;
+	default:
+		return kRA1MenuCommandNone;
+	}
+}
+
+static RA1MenuCommand getRebel1MenuCommandFromKey(const Common::KeyState &kbd) {
+	switch (kbd.keycode) {
+	case Common::KEYCODE_UP:
+	case Common::KEYCODE_w:
+		return kRA1MenuCommandUp;
+	case Common::KEYCODE_DOWN:
+	case Common::KEYCODE_s:
+		return kRA1MenuCommandDown;
+	case Common::KEYCODE_LEFT:
+	case Common::KEYCODE_a:
+		return kRA1MenuCommandLeft;
+	case Common::KEYCODE_RIGHT:
+	case Common::KEYCODE_d:
+		return kRA1MenuCommandRight;
+	case Common::KEYCODE_RETURN:
+	case Common::KEYCODE_KP_ENTER:
+	case Common::KEYCODE_SPACE:
+		return kRA1MenuCommandAccept;
+	case Common::KEYCODE_ESCAPE:
+		return kRA1MenuCommandCancel;
+	case Common::KEYCODE_1:
+		return kRA1MenuCommandSelect1;
+	case Common::KEYCODE_2:
+		return kRA1MenuCommandSelect2;
+	case Common::KEYCODE_3:
+		return kRA1MenuCommandSelect3;
+	case Common::KEYCODE_4:
+		return kRA1MenuCommandSelect4;
+	case Common::KEYCODE_5:
+		return kRA1MenuCommandSelect5;
+	case Common::KEYCODE_6:
+		return kRA1MenuCommandSelect6;
+	default:
+		return kRA1MenuCommandNone;
+	}
+}
+
 static bool isRebel1TextEntryChar(bool passcodeMode, char c) {
 	if (passcodeMode) {
 		c = normalizeRebel1PasscodeChar(c);
@@ -314,35 +368,38 @@ bool InsaneRebel1::handleTextEntryKey(const Common::Event &event) {
 	return true;
 }
 
-bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
-	if (!_menuActive || _highScoresActive)
+bool InsaneRebel1::handleMenuCommand(RA1MenuCommand command) {
+	if (!_menuActive || _highScoresActive || command == kRA1MenuCommandNone)
 		return false;
 
 	if (_textEntryActive)
-		return handleTextEntryAction(action);
+		return false;
 
 	if (_levelSelectActive) {
 		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
 		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
 
-		switch (action) {
-		case kScummActionInsaneUp:
+		switch (command) {
+		case kRA1MenuCommandUp:
 			row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
 			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
 			return true;
-		case kScummActionInsaneDown:
+		case kRA1MenuCommandDown:
 			row = (row + 1) % kRA1LevelSelectRowsPerCol;
 			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
 			return true;
-		case kScummActionInsaneLeft:
+		case kRA1MenuCommandLeft:
 			if (col > 0)
 				_levelSelectSel -= kRA1LevelSelectRowsPerCol;
 			return true;
-		case kScummActionInsaneRight:
+		case kRA1MenuCommandRight:
 			if (col < 1)
 				_levelSelectSel += kRA1LevelSelectRowsPerCol;
 			return true;
-		case kScummActionInsaneAttack:
+		case kRA1MenuCommandCancel:
+			_levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
+			// fall through
+		case kRA1MenuCommandAccept:
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
@@ -352,22 +409,25 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 	}
 
 	if (_optionsActive) {
-		switch (action) {
-		case kScummActionInsaneUp:
+		switch (command) {
+		case kRA1MenuCommandUp:
 			_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
 			return true;
-		case kScummActionInsaneDown:
+		case kRA1MenuCommandDown:
 			_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
 			return true;
-		case kScummActionInsaneLeft:
+		case kRA1MenuCommandLeft:
 			if (_optionsSel == 7)
 				setRebel1Volume(_vm, _optVolume, -5);
 			return true;
-		case kScummActionInsaneRight:
+		case kRA1MenuCommandRight:
 			if (_optionsSel == 7)
 				setRebel1Volume(_vm, _optVolume, 5);
 			return true;
-		case kScummActionInsaneAttack:
+		case kRA1MenuCommandCancel:
+			_optionsSel = 0;
+			// fall through
+		case kRA1MenuCommandAccept:
 			_menuConfirmed = true;
 			_vm->_smushVideoShouldFinish = true;
 			return true;
@@ -376,14 +436,27 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 		}
 	}
 
-	switch (action) {
-	case kScummActionInsaneUp:
+	switch (command) {
+	case kRA1MenuCommandUp:
 		_menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
 		return true;
-	case kScummActionInsaneDown:
+	case kRA1MenuCommandDown:
 		_menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
 		return true;
-	case kScummActionInsaneAttack:
+	case kRA1MenuCommandCancel:
+		_menuSelection = kRA1MainMenuItemCount - 1;
+		// fall through
+	case kRA1MenuCommandAccept:
+		_menuConfirmed = true;
+		_vm->_smushVideoShouldFinish = true;
+		return true;
+	case kRA1MenuCommandSelect1:
+	case kRA1MenuCommandSelect2:
+	case kRA1MenuCommandSelect3:
+	case kRA1MenuCommandSelect4:
+	case kRA1MenuCommandSelect5:
+	case kRA1MenuCommandSelect6:
+		_menuSelection = command - kRA1MenuCommandSelect1;
 		_menuConfirmed = true;
 		_vm->_smushVideoShouldFinish = true;
 		return true;
@@ -392,6 +465,16 @@ bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
 	}
 }
 
+bool InsaneRebel1::handleControllerMenuAction(ScummAction action) {
+	if (!_menuActive || _highScoresActive)
+		return false;
+
+	if (_textEntryActive)
+		return handleTextEntryAction(action);
+
+	return handleMenuCommand(getRebel1MenuCommandFromAction(action));
+}
+
 bool InsaneRebel1::handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY) {
 	if (!_menuActive || _highScoresActive)
 		return false;
@@ -406,9 +489,9 @@ bool InsaneRebel1::handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY) {
 	const int newY = getRebel1MenuAxisDirection(_joystickAxisY);
 
 	if (newY != oldY && newY != 0)
-		return handleControllerMenuAction((newY > 0) != _optControlsYFlip ? kScummActionInsaneDown : kScummActionInsaneUp);
+		return handleMenuCommand((newY > 0) != _optControlsYFlip ? kRA1MenuCommandDown : kRA1MenuCommandUp);
 	if (newX != oldX && newX != 0)
-		return handleControllerMenuAction(newX > 0 ? kScummActionInsaneRight : kScummActionInsaneLeft);
+		return handleMenuCommand(newX > 0 ? kRA1MenuCommandRight : kRA1MenuCommandLeft);
 
 	return false;
 }
@@ -546,119 +629,9 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	if (_menuActive && _textEntryActive && event.type == Common::EVENT_KEYDOWN)
 		return handleTextEntryKey(event);
 
-	if (_menuActive && _levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
-		int col = _levelSelectSel / kRA1LevelSelectRowsPerCol;
-		int row = _levelSelectSel % kRA1LevelSelectRowsPerCol;
-		switch (event.kbd.keycode) {
-		case Common::KEYCODE_UP:
-		case Common::KEYCODE_w:
-			row = (row + kRA1LevelSelectRowsPerCol - 1) % kRA1LevelSelectRowsPerCol;
-			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
-			return true;
-		case Common::KEYCODE_DOWN:
-		case Common::KEYCODE_s:
-			row = (row + 1) % kRA1LevelSelectRowsPerCol;
-			_levelSelectSel = col * kRA1LevelSelectRowsPerCol + row;
-			return true;
-		case Common::KEYCODE_LEFT:
-		case Common::KEYCODE_a:
-			if (col > 0)
-				_levelSelectSel -= kRA1LevelSelectRowsPerCol;
-			return true;
-		case Common::KEYCODE_RIGHT:
-		case Common::KEYCODE_d:
-			if (col < 1)
-				_levelSelectSel += kRA1LevelSelectRowsPerCol;
-			return true;
-		case Common::KEYCODE_RETURN:
-		case Common::KEYCODE_KP_ENTER:
-		case Common::KEYCODE_SPACE:
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_ESCAPE:
-			_levelSelectSel = kRA1LevelSelectItemCount - 1; // Back
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		default:
-			break;
-		}
-	}
-
-	if (_menuActive && _optionsActive && event.type == Common::EVENT_KEYDOWN) {
-		switch (event.kbd.keycode) {
-		case Common::KEYCODE_UP:
-		case Common::KEYCODE_w:
-			_optionsSel = (_optionsSel + kOptionsItemCount - 1) % kOptionsItemCount;
-			return true;
-		case Common::KEYCODE_DOWN:
-		case Common::KEYCODE_s:
-			_optionsSel = (_optionsSel + 1) % kOptionsItemCount;
-			return true;
-		case Common::KEYCODE_LEFT:
-		case Common::KEYCODE_a:
-			// Volume down when on volume row (row 7)
-			if (_optionsSel == 7)
-				setRebel1Volume(_vm, _optVolume, -5);
-			return true;
-		case Common::KEYCODE_RIGHT:
-		case Common::KEYCODE_d:
-			// Volume up when on volume row (row 7)
-			if (_optionsSel == 7)
-				setRebel1Volume(_vm, _optVolume, 5);
-			return true;
-		case Common::KEYCODE_RETURN:
-		case Common::KEYCODE_KP_ENTER:
-		case Common::KEYCODE_SPACE:
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_ESCAPE:
-			_optionsSel = 0;
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		default:
-			break;
-		}
-	}
-
-	if (_menuActive && !_optionsActive && !_levelSelectActive && event.type == Common::EVENT_KEYDOWN) {
-		switch (event.kbd.keycode) {
-		case Common::KEYCODE_UP:
-		case Common::KEYCODE_w:
-			_menuSelection = (_menuSelection + kRA1MainMenuItemCount - 1) % kRA1MainMenuItemCount;
-			return true;
-		case Common::KEYCODE_DOWN:
-		case Common::KEYCODE_s:
-			_menuSelection = (_menuSelection + 1) % kRA1MainMenuItemCount;
-			return true;
-		case Common::KEYCODE_RETURN:
-		case Common::KEYCODE_KP_ENTER:
-		case Common::KEYCODE_SPACE:
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_1:
-		case Common::KEYCODE_2:
-		case Common::KEYCODE_3:
-		case Common::KEYCODE_4:
-		case Common::KEYCODE_5:
-		case Common::KEYCODE_6:
-			_menuSelection = event.kbd.keycode - Common::KEYCODE_1;
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		case Common::KEYCODE_ESCAPE:
-			_menuSelection = kRA1MainMenuItemCount - 1;
-			_menuConfirmed = true;
-			_vm->_smushVideoShouldFinish = true;
-			return true;
-		default:
-			break;
-		}
-	}
+	if (_menuActive && event.type == Common::EVENT_KEYDOWN &&
+		handleMenuCommand(getRebel1MenuCommandFromKey(event.kbd)))
+		return true;
 
 	// Shooting: mouse button during interactive gameplay — FUN_1CCA0 (0x1CCA0)
 	if (_interactiveVideoActive && !_menuActive) {
@@ -731,138 +704,127 @@ bool InsaneRebel1::notifyEvent(const Common::Event &event) {
 	return false;
 }
 
-void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
-	_menuFrameCounter++;
-	auto makeTalkText = [](const char *text) {
-		Common::String out("<");
-		out += text;
-		return out;
-	};
-	auto getTalkTextWidth = [&](const char *text) {
-		Common::String styled = makeTalkText(text);
-		return getFontBankStringWidth(styled.c_str());
-	};
-	auto drawTalkText = [&](int x, int y, const char *text) {
-		Common::String styled = makeTalkText(text);
-		drawFontBankString(dst, pitch, width, height, x, y, styled.c_str());
-	};
-	auto getTitleTextWidth = [&](const char *text) {
-		return getFontBankStringWidth(text);
-	};
-	auto drawTitleText = [&](int x, int y, const char *text) {
-		drawFontBankString(dst, pitch, width, height, x, y, text);
-	};
+int InsaneRebel1::getMenuTalkTextWidth(const char *text) {
+	Common::String styled("<");
+	styled += text;
+	return getFontBankStringWidth(styled.c_str());
+}
 
-	if (_textEntryActive) {
-		renderTextEntryOverlay(dst, pitch, width, height);
-		return;
-	}
+void InsaneRebel1::drawMenuTalkText(byte *dst, int pitch, int width, int height,
+		int x, int y, const char *text) {
+	Common::String styled("<");
+	styled += text;
+	drawFontBankString(dst, pitch, width, height, x, y, styled.c_str());
+}
 
-	if (_highScoresActive) {
-		// --- TOP PILOTS high score display ---
-		// Original renders over O1SCORE.ANM. Title appears after frame 20,
-		// entries fade in one per frame. We show all immediately.
-		const int titleW = getTalkTextWidth("TOP PILOTS");
-		drawTalkText(getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
-
-		for (int i = 0; i < kHighScoreCount; i++) {
-			const int y = 25 + i * 14;
-			// Name (left side)
-			drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
-			// Score + difficulty glyph (right side) — original format "<%ld %c"
-			// Difficulty byte 0/1/2 + 0x7B = '{','|','}' tech font glyphs (easy/normal/hard)
-			char scoreLine[32];
-			Common::sprintf_s(scoreLine, "<%ld %c",
-				(long)_highScores[i].score,
-				(char)(_highScores[i].difficulty + 0x7B));
-			drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
-		}
-		return;
+void InsaneRebel1::drawMenuTitleText(byte *dst, int pitch, int width, int height,
+		int x, int y, const char *text) {
+	drawFontBankString(dst, pitch, width, height, x, y, text);
+}
+
+void InsaneRebel1::renderHighScoresOverlay(byte *dst, int pitch, int width, int height) {
+	// --- TOP PILOTS high score display ---
+	// Original renders over O1SCORE.ANM. Title appears after frame 20,
+	// entries fade in one per frame. We show all immediately.
+	const int titleW = getMenuTalkTextWidth("TOP PILOTS");
+	drawMenuTalkText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 10, "TOP PILOTS");
+
+	for (int i = 0; i < kHighScoreCount; i++) {
+		const int y = 25 + i * 14;
+		// Name (left side)
+		drawFontBankString(dst, pitch, width, height, 40, y, _highScores[i].name);
+		// Score + difficulty glyph (right side) — original format "<%ld %c"
+		// Difficulty byte 0/1/2 + 0x7B = '{','|','}' tech font glyphs (easy/normal/hard)
+		char scoreLine[32];
+		Common::sprintf_s(scoreLine, "<%ld %c",
+			(long)_highScores[i].score,
+			(char)(_highScores[i].difficulty + 0x7B));
+		drawFontBankString(dst, pitch, width, height, 220, y, scoreLine);
 	}
+}
 
-	if (_optionsActive) {
-		// --- Options submenu (matching original RunGameOptionsMenu) ---
-		const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
-
-		const int titleW = getTitleTextWidth("GAME OPTIONS");
-		drawTitleText(getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
-
-		// Build dynamic option strings for each row
-		char diffLine[64], volLine[64];
-		Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
-		Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
-
-		const char *optItems[kOptionsItemCount] = {
-			"EXIT MENU",
-			_optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
-			_optMusicEnabled  ? "MUSIC IS ON"             : "MUSIC IS OFF",
-			_optSfxEnabled    ? "SFX AND VOICE ARE ON"    : "SFX AND VOICE ARE OFF",
-			_optTextEnabled   ? "DIALOGUE TEXT IS ON"      : "DIALOGUE TEXT IS OFF",
-			_optEnhancedControls ? "INPUT STYLE ENHANCED" : "INPUT STYLE ORIGINAL",
-			_optControlsYFlip ? "Y AXIS IS INVERTED"      : "Y AXIS IS NORMAL",
-			volLine,
-			diffLine
-		};
-
-		for (int i = 0; i < kOptionsItemCount; i++) {
-			const int textW = getTalkTextWidth(optItems[i]);
-			const int textX = getRebel1MenuCenteredX(textW);
-			const int y = 0x2d + i * kRA1MenuRowH;
-			drawTalkText(textX, y, optItems[i]);
-
-			if (i == _optionsSel)
-				drawRebel1MenuFrame(dst, pitch, width, height,
-					kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x1d, kRA1MenuFrameW);
-		}
-		return;
+void InsaneRebel1::renderOptionsOverlay(byte *dst, int pitch, int width, int height) {
+	// --- Options submenu (matching original RunGameOptionsMenu) ---
+	const char *kDiffNames[3] = { "EASY", "NORMAL", "HARD" };
+
+	const int titleW = getFontBankStringWidth("GAME OPTIONS");
+	drawMenuTitleText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 15, "GAME OPTIONS");
+
+	// Build dynamic option strings for each row
+	char diffLine[64], volLine[64];
+	Common::sprintf_s(diffLine, "DIFFICULTY IS %s", kDiffNames[CLIP(_difficulty, 0, 2)]);
+	Common::sprintf_s(volLine, "VOLUME AT %d PERCENT", (_optVolume * 100) / 127);
+
+	const char *optItems[kOptionsItemCount] = {
+		"EXIT MENU",
+		_optRookieOneFemale ? "ROOKIE1 IS FEMALE" : "ROOKIE1 IS MALE",
+		_optMusicEnabled  ? "MUSIC IS ON"             : "MUSIC IS OFF",
+		_optSfxEnabled    ? "SFX AND VOICE ARE ON"    : "SFX AND VOICE ARE OFF",
+		_optTextEnabled   ? "DIALOGUE TEXT IS ON"      : "DIALOGUE TEXT IS OFF",
+		_optEnhancedControls ? "INPUT STYLE ENHANCED" : "INPUT STYLE ORIGINAL",
+		_optControlsYFlip ? "Y AXIS IS INVERTED"      : "Y AXIS IS NORMAL",
+		volLine,
+		diffLine
+	};
+
+	for (int i = 0; i < kOptionsItemCount; i++) {
+		const int textW = getMenuTalkTextWidth(optItems[i]);
+		const int textX = getRebel1MenuCenteredX(textW);
+		const int y = 0x2d + i * kRA1MenuRowH;
+		drawMenuTalkText(dst, pitch, width, height, textX, y, optItems[i]);
+
+		if (i == _optionsSel)
+			drawRebel1MenuFrame(dst, pitch, width, height,
+				kRA1MenuFrameX, (i + 1) * kRA1MenuRowH + 0x1d, kRA1MenuFrameW);
 	}
+}
 
-	if (_levelSelectActive) {
-		// --- ScummVM level select submenu, styled like the original frontend menus ---
-		const int titleW = getTitleTextWidth("LEVEL SELECT");
-		drawTitleText(getRebel1MenuCenteredX(titleW), 15, "LEVEL SELECT");
-
-		const char *kLevelItems[kRA1LevelSelectItemCount] = {
-			" 1 TRAINING",
-			" 2 ASTEROIDS",
-			" 3 KOLAADOR",
-			" 4 STAR DESTR",
-			" 5 TATOOINE",
-			" 6 AST CHASE",
-			" 7 PROBES",
-			" 8 WALKERS",
-			" 9 TROOPERS",
-			"10 TRANSPORT",
-			"11 YAVIN",
-			"12 TIE ATK",
-			"13 DS SURFACE",
-			"14 CANNON",
-			"15 DS TRENCH",
-			"BACK"
-		};
-
-		const int menuY = 0x2d;
-		const int leftFrameX = 20;
-		const int rightFrameX = 170;
-		const int columnW = 130;
-
-		for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
-			const int col = i / kRA1LevelSelectRowsPerCol;
-			const int row = i % kRA1LevelSelectRowsPerCol;
-			const int frameX = (col == 0) ? leftFrameX : rightFrameX;
-			const int y = menuY + row * kRA1MenuRowH;
-			const int textW = getTalkTextWidth(kLevelItems[i]);
-			const int textX = frameX + (columnW - textW) / 2;
-
-			drawTalkText(textX, y, kLevelItems[i]);
-
-			if (i == _levelSelectSel)
-				drawRebel1MenuFrame(dst, pitch, width, height,
-					frameX, row * kRA1MenuRowH + 0x2c, columnW);
-		}
-		return;
+void InsaneRebel1::renderLevelSelectOverlay(byte *dst, int pitch, int width, int height) {
+	// --- ScummVM level select submenu, styled like the original frontend menus ---
+	const int titleW = getFontBankStringWidth("LEVEL SELECT");
+	drawMenuTitleText(dst, pitch, width, height, getRebel1MenuCenteredX(titleW), 15, "LEVEL SELECT");
+
+	const char *kLevelItems[kRA1LevelSelectItemCount] = {
+		" 1 TRAINING",
+		" 2 ASTEROIDS",
+		" 3 KOLAADOR",
+		" 4 STAR DESTR",
+		" 5 TATOOINE",
+		" 6 AST CHASE",
+		" 7 PROBES",
+		" 8 WALKERS",
+		" 9 TROOPERS",
+		"10 TRANSPORT",
+		"11 YAVIN",
+		"12 TIE ATK",
+		"13 DS SURFACE",
+		"14 CANNON",
+		"15 DS TRENCH",
+		"BACK"
+	};
+
+	const int menuY = 0x2d;
+	const int leftFrameX = 20;
+	const int rightFrameX = 170;
+	const int columnW = 130;
+
+	for (int i = 0; i < kRA1LevelSelectItemCount; i++) {
+		const int col = i / kRA1LevelSelectRowsPerCol;
+		const int row = i % kRA1LevelSelectRowsPerCol;
+		const int frameX = (col == 0) ? leftFrameX : rightFrameX;
+		const int y = menuY + row * kRA1MenuRowH;
+		const int textW = getMenuTalkTextWidth(kLevelItems[i]);
+		const int textX = frameX + (columnW - textW) / 2;
+
+		drawMenuTalkText(dst, pitch, width, height, textX, y, kLevelItems[i]);
+
+		if (i == _levelSelectSel)
+			drawRebel1MenuFrame(dst, pitch, width, height,
+				frameX, row * kRA1MenuRowH + 0x2c, columnW);
 	}
+}
 
+void InsaneRebel1::renderMainMenuItems(byte *dst, int pitch, int width, int height) {
 	// --- Main menu ---
 	const char *kMenuItems[kRA1MainMenuItemCount] = {
 		"START NEW GAME",
@@ -874,17 +836,17 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	};
 
 	// Center title
-	const int titleW = getTitleTextWidth("MAIN MENU");
+	const int titleW = getFontBankStringWidth("MAIN MENU");
 	const int titleX = getRebel1MenuCenteredX(titleW);
-	drawTitleText(titleX, 30, "MAIN MENU");
+	drawMenuTitleText(dst, pitch, width, height, titleX, 30, "MAIN MENU");
 
 	// Draw menu items centered horizontally
 	for (int i = 0; i < kRA1MainMenuItemCount; i++) {
-		const int textW = getTalkTextWidth(kMenuItems[i]);
+		const int textW = getMenuTalkTextWidth(kMenuItems[i]);
 		const int textX = getRebel1MenuCenteredX(textW);
 		const int y = 0x3c + i * kRA1MenuRowH;
 
-		drawTalkText(textX, y, kMenuItems[i]);
+		drawMenuTalkText(dst, pitch, width, height, textX, y, kMenuItems[i]);
 
 		if (i == _menuSelection)
 			drawRebel1MenuFrame(dst, pitch, width, height,
@@ -892,17 +854,36 @@ void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int he
 	}
 }
 
+void InsaneRebel1::renderMainMenuOverlay(byte *dst, int pitch, int width, int height) {
+	_menuFrameCounter++;
+
+	if (_textEntryActive) {
+		renderTextEntryOverlay(dst, pitch, width, height);
+		return;
+	}
+
+	if (_highScoresActive) {
+		renderHighScoresOverlay(dst, pitch, width, height);
+		return;
+	}
+
+	if (_optionsActive) {
+		renderOptionsOverlay(dst, pitch, width, height);
+		return;
+	}
+
+	if (_levelSelectActive) {
+		renderLevelSelectOverlay(dst, pitch, width, height);
+		return;
+	}
+
+	renderMainMenuItems(dst, pitch, width, height);
+}
+
 void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int height) {
-	auto makeTalkText = [](const char *text) {
-		Common::String out("<");
-		out += text;
-		return out;
-	};
 	auto drawCenteredTalkText = [&](int y, const char *text) {
-		Common::String styled = makeTalkText(text);
-		const int textW = getFontBankStringWidth(styled.c_str());
-		drawFontBankString(dst, pitch, width, height,
-			getRebel1MenuCenteredX(textW), y, styled.c_str());
+		const int textW = getMenuTalkTextWidth(text);
+		drawMenuTalkText(dst, pitch, width, height, getRebel1MenuCenteredX(textW), y, text);
 	};
 	auto drawCenteredRawChar = [&](int centerX, int y, char ch) {
 		char text[2] = { ch, '\0' };
@@ -931,17 +912,28 @@ void InsaneRebel1::renderTextEntryOverlay(byte *dst, int pitch, int width, int h
 	_textEntryPickerOffsetX = 0;
 }
 
+void InsaneRebel1::playMenuBackground() {
+	_menuActive = true;
+	_menuConfirmed = false;
+	_menuFrameCounter = 0;
+	clearVideoBuffer();
+	playCinematic("OPEN/O1OPTION.ANM");
+	_menuActive = false;
+}
+
+bool InsaneRebel1::runTextEntryMenuLoop() {
+	while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled)
+		playMenuBackground();
+
+	return !_vm->shouldQuit() && !_textEntryCanceled;
+}
+
 int InsaneRebel1::runMainMenu() {
 	debug(1, "InsaneRebel1: Main menu");
 
 	_menuSelection = 0;
 	while (!_vm->shouldQuit()) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
+		playMenuBackground();
 
 		if (_vm->shouldQuit())
 			return kRA1MainMenuItemCount;
@@ -956,16 +948,7 @@ int InsaneRebel1::runMainMenu() {
 int InsaneRebel1::runPasscodeEntryDialog() {
 	beginTextEntry(true);
 
-	while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
-	}
-
-	if (_vm->shouldQuit() || _textEntryCanceled)
+	if (!runTextEntryMenuLoop())
 		return 0;
 
 	for (int i = 1; i <= kRA1NumLevels; i++) {
@@ -1005,14 +988,7 @@ bool InsaneRebel1::runHighScoreNameEntry() {
 	_highScoreEntryIndex = slot;
 
 	beginTextEntry(false);
-	while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
-	}
+	runTextEntryMenuLoop();
 
 	Common::String storedName("<");
 	storedName += _textEntryBuffer;
@@ -1029,12 +1005,7 @@ void InsaneRebel1::runOptionsMenu() {
 	_optionsActive = true;
 
 	while (!_vm->shouldQuit()) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
+		playMenuBackground();
 
 		if (_vm->shouldQuit())
 			break;
@@ -1083,12 +1054,7 @@ int InsaneRebel1::runLevelSelectMenu() {
 	_levelSelectActive = true;
 
 	while (!_vm->shouldQuit()) {
-		_menuActive = true;
-		_menuConfirmed = false;
-		_menuFrameCounter = 0;
-		clearVideoBuffer();
-		playCinematic("OPEN/O1OPTION.ANM");
-		_menuActive = false;
+		playMenuBackground();
 
 		if (_vm->shouldQuit())
 			break;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 4c7baa881d4..fde8876fd59 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -33,6 +33,22 @@ namespace Scumm {
 class ScummEngine_v7;
 class SmushFont;
 
+enum RA1MenuCommand {
+	kRA1MenuCommandNone = 0,
+	kRA1MenuCommandUp,
+	kRA1MenuCommandDown,
+	kRA1MenuCommandLeft,
+	kRA1MenuCommandRight,
+	kRA1MenuCommandAccept,
+	kRA1MenuCommandCancel,
+	kRA1MenuCommandSelect1,
+	kRA1MenuCommandSelect2,
+	kRA1MenuCommandSelect3,
+	kRA1MenuCommandSelect4,
+	kRA1MenuCommandSelect5,
+	kRA1MenuCommandSelect6
+};
+
 // Simple sprite bank for RA1 NUT files (ANIM v1 with odd-alignment padding).
 // Separate from NutRenderer to avoid modifying shared NUT parsing code.
 struct RA1Sprite {
@@ -484,10 +500,13 @@ private:
 	void runOptionsMenu();
 	int runPasscodeEntryDialog();
 	bool runHighScoreNameEntry();
+	bool handleMenuCommand(RA1MenuCommand command);
 	bool handleControllerMenuAction(ScummAction action);
 	bool handleControllerMenuAxis(int16 oldAxisX, int16 oldAxisY);
 	bool handleTextEntryAction(ScummAction action);
 	bool handleTextEntryKey(const Common::Event &event);
+	void playMenuBackground();
+	bool runTextEntryMenuLoop();
 	void beginTextEntry(bool passcodeMode);
 	void finishTextEntry(bool canceled);
 	void selectTextEntryChar();
@@ -524,6 +543,13 @@ private:
 	HighScoreEntry _highScores[kHighScoreCount];
 	bool _highScoresActive;  // True when showing TOP PILOTS overlay
 	void showHighScores();
+	int getMenuTalkTextWidth(const char *text);
+	void drawMenuTalkText(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+	void drawMenuTitleText(byte *dst, int pitch, int width, int height, int x, int y, const char *text);
+	void renderHighScoresOverlay(byte *dst, int pitch, int width, int height);
+	void renderOptionsOverlay(byte *dst, int pitch, int width, int height);
+	void renderLevelSelectOverlay(byte *dst, int pitch, int width, int height);
+	void renderMainMenuItems(byte *dst, int pitch, int width, int height);
 
 	// Character picker shared by RunPasscodeEntryDialog and RunHighScoreNameEntry.
 	static const int kTextEntryBufferSize = 20;


Commit: 9abca5775a71d67b7c12b4822bee474702d547e0
    https://github.com/scummvm/scummvm/commit/9abca5775a71d67b7c12b4822bee474702d547e0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:13+02:00

Commit Message:
SCUMM: RA: Extract common audio helpers

Changed paths:
  A engines/scumm/insane/rebel/rebel_audio.cpp
  A engines/scumm/insane/rebel/rebel_audio.h
  A engines/scumm/smush/rebel/smush_player_rebel.cpp
    engines/scumm/insane/rebel1/audio.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/insane/rebel2/audio.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h
    engines/scumm/module.mk
    engines/scumm/smush/rebel/smush_player_ra2.cpp
    engines/scumm/smush/smush_player.h


diff --git a/engines/scumm/insane/rebel/rebel_audio.cpp b/engines/scumm/insane/rebel/rebel_audio.cpp
new file mode 100644
index 00000000000..b06a58fd884
--- /dev/null
+++ b/engines/scumm/insane/rebel/rebel_audio.cpp
@@ -0,0 +1,209 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/mixer.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/smush_player.h"
+#include "scumm/insane/rebel/rebel_audio.h"
+
+namespace Scumm {
+
+RebelAudio::RebelAudio() : _vm(nullptr), _sampleRate(11025) {
+	for (int i = 0; i < kMaxTracks; i++) {
+		_streams[i] = nullptr;
+		_trackActive[i] = false;
+	}
+}
+
+void RebelAudio::init(ScummEngine_v7 *vm, int sampleRate) {
+	_vm = vm;
+	_sampleRate = sampleRate;
+	for (int i = 0; i < kMaxTracks; i++) {
+		_streams[i] = nullptr;
+		_trackActive[i] = false;
+	}
+}
+
+void RebelAudio::terminate() {
+	if (!_vm)
+		return;
+
+	for (int i = 0; i < kMaxTracks; i++) {
+		if (_trackActive[i]) {
+			_vm->_mixer->stopHandle(_handles[i]);
+			_trackActive[i] = false;
+		}
+		if (_streams[i]) {
+			_streams[i]->finish();
+			_streams[i] = nullptr;
+		}
+	}
+}
+
+void RebelAudio::queueData(int trackIdx, const uint8 *data, int32 size, int volume, int pan) {
+	if (!_vm || trackIdx < 0 || trackIdx >= kMaxTracks || size <= 0 || !data)
+		return;
+
+	if (!_streams[trackIdx]) {
+		debug(1, "RebelAudio: Creating audio stream for track %d at %d Hz", trackIdx, _sampleRate);
+		_streams[trackIdx] = Audio::makeQueuingAudioStream(_sampleRate, false);
+		_trackActive[trackIdx] = true;
+		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handles[trackIdx],
+			_streams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
+			DisposeAfterUse::NO);
+	}
+
+	byte *audioCopy = (byte *)malloc(size);
+	if (!audioCopy)
+		return;
+	memcpy(audioCopy, data, size);
+
+	_streams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+
+	const int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
+	const int scaledPan = (pan * 127) / 128;
+	_vm->_mixer->setChannelVolume(_handles[trackIdx], scaledVolume);
+	_vm->_mixer->setChannelBalance(_handles[trackIdx], scaledPan);
+}
+
+void RebelAudio::processFrame(SmushPlayer *player, int16 feedSize) {
+	if (!player)
+		return;
+
+	if (player->_smushTracksNeedInit) {
+		player->_smushTracksNeedInit = false;
+		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
+			player->_smushDispatch[i].fadeRemaining = 0;
+			player->_smushDispatch[i].fadeVolume = 0;
+			player->_smushDispatch[i].fadeSampleRate = 0;
+			player->_smushDispatch[i].elapsedAudio = 0;
+			player->_smushDispatch[i].audioLength = 0;
+		}
+	}
+
+	for (int i = 0; i < player->_smushNumTracks; i++) {
+		SmushPlayer::SmushAudioTrack &track = player->_smushTracks[i];
+		SmushPlayer::SmushAudioDispatch &dispatch = player->_smushDispatch[i];
+
+		if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
+			continue;
+
+		bool isPlayableTrack =
+			((track.flags & TRK_TYPE_MASK) == IS_SPEECH && player->isChanActive(CHN_SPEECH)) ||
+			((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && player->isChanActive(CHN_BKGMUS)) ||
+			((track.flags & TRK_TYPE_MASK) == IS_SFX && player->isChanActive(CHN_OTHER));
+
+		if (!isPlayableTrack)
+			continue;
+
+		int baseVolume;
+		switch (track.flags & TRK_TYPE_MASK) {
+		case IS_SFX:
+			baseVolume = (player->_smushTrackVols[1] * track.volume) >> 7;
+			break;
+		case IS_BKG_MUSIC:
+			baseVolume = (player->_smushTrackVols[3] * track.volume) >> 7;
+			break;
+		case IS_SPEECH:
+			baseVolume = (player->_smushTrackVols[2] * track.volume) >> 7;
+			break;
+		default:
+			baseVolume = track.volume;
+			break;
+		}
+		int mixVolume = baseVolume * player->_smushTrackVols[0] / 127;
+
+		if (track.state == TRK_STATE_FADING) {
+			dispatch.headerPtr = track.dataBuf;
+			dispatch.dataBuf = track.subChunkPtr;
+			dispatch.dataSize = track.dataSize;
+			dispatch.currentOffset = 0;
+			dispatch.audioLength = 0;
+			track.state = TRK_STATE_PLAYING;
+		}
+
+		if (track.state != TRK_STATE_INACTIVE) {
+			int32 tmpFeedSize = feedSize;
+
+			while (tmpFeedSize > 0) {
+				int32 mixInFrameCount = dispatch.currentOffset;
+
+				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
+					if (dispatch.audioRemaining < 0)
+						dispatch.audioRemaining = 0;
+
+					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
+
+					if (dispatch.sampleRate > 0 && player->_smushAudioSampleRate > 0) {
+						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / player->_smushAudioSampleRate;
+						if (mixInFrameCount > maxFrames)
+							mixInFrameCount = maxFrames;
+					}
+
+					if (offset + mixInFrameCount > dispatch.dataSize)
+						mixInFrameCount = dispatch.dataSize - offset;
+
+					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
+						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
+						if (mixInFrameCount <= 0) {
+							track.state = TRK_STATE_ENDING;
+							break;
+						}
+					}
+
+					if (mixInFrameCount > 0) {
+						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
+							break;
+
+						queueData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
+
+						dispatch.currentOffset -= mixInFrameCount;
+						dispatch.audioRemaining += mixInFrameCount;
+
+						if (dispatch.sampleRate > 0) {
+							int32 consumedFeed = mixInFrameCount * player->_smushAudioSampleRate / dispatch.sampleRate;
+							tmpFeedSize -= consumedFeed;
+						} else {
+							tmpFeedSize -= mixInFrameCount;
+						}
+					}
+				}
+
+				if (dispatch.currentOffset <= 0) {
+					if (!player->processAudioCodes(i, tmpFeedSize, mixVolume))
+						break;
+					if (dispatch.currentOffset <= 0)
+						break;
+				} else if (tmpFeedSize <= 0) {
+					break;
+				}
+			}
+		}
+
+		track.audioRemaining = dispatch.audioRemaining;
+		dispatch.state = track.state;
+	}
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel/rebel_audio.h b/engines/scumm/insane/rebel/rebel_audio.h
new file mode 100644
index 00000000000..c20dace1e5e
--- /dev/null
+++ b/engines/scumm/insane/rebel/rebel_audio.h
@@ -0,0 +1,60 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_INSANE_REBEL_AUDIO_H
+#define SCUMM_INSANE_REBEL_AUDIO_H
+
+#include "audio/mixer.h"
+#include "common/scummsys.h"
+
+namespace Audio {
+class QueuingAudioStream;
+}
+
+namespace Scumm {
+
+class ScummEngine_v7;
+class SmushPlayer;
+
+class RebelAudio {
+public:
+	RebelAudio();
+
+	void init(ScummEngine_v7 *vm, int sampleRate);
+	void terminate();
+	int sampleRate() const { return _sampleRate; }
+
+	void queueData(int trackIdx, const uint8 *data, int32 size, int volume, int pan);
+	void processFrame(SmushPlayer *player, int16 feedSize);
+
+private:
+	static const int kMaxTracks = 4;
+
+	ScummEngine_v7 *_vm;
+	Audio::QueuingAudioStream *_streams[kMaxTracks];
+	Audio::SoundHandle _handles[kMaxTracks];
+	bool _trackActive[kMaxTracks];
+	int _sampleRate;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/insane/rebel1/audio.cpp b/engines/scumm/insane/rebel1/audio.cpp
index 6c1540aa368..bb807f9df2d 100644
--- a/engines/scumm/insane/rebel1/audio.cpp
+++ b/engines/scumm/insane/rebel1/audio.cpp
@@ -48,24 +48,11 @@ const char *const kRA1SfxFiles[8] = {
 // ---------------------------------------------------------------------------
 
 void InsaneRebel1::initAudio(int sampleRate) {
-	_audioSampleRate = sampleRate;
-	for (int i = 0; i < kMaxAudioTracks; i++) {
-		_audioStreams[i] = nullptr;
-		_audioTrackActive[i] = false;
-	}
+	_audio.init(_vm, sampleRate);
 }
 
 void InsaneRebel1::terminateAudio() {
-	for (int i = 0; i < kMaxAudioTracks; i++) {
-		if (_audioTrackActive[i]) {
-			_vm->_mixer->stopHandle(_audioHandles[i]);
-			_audioTrackActive[i] = false;
-		}
-		if (_audioStreams[i]) {
-			_audioStreams[i]->finish();
-			_audioStreams[i] = nullptr;
-		}
-	}
+	_audio.terminate();
 
 	for (int i = 0; i < kNumSfx; i++) {
 		_vm->_mixer->stopHandle(_sfxHandles[i]);
@@ -73,150 +60,11 @@ void InsaneRebel1::terminateAudio() {
 }
 
 void InsaneRebel1::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
-	if (trackIdx < 0 || trackIdx >= kMaxAudioTracks || size <= 0 || !data)
-		return;
-
-	if (!_audioStreams[trackIdx]) {
-		debug(1, "InsaneRebel1: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
-		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
-		_audioTrackActive[trackIdx] = true;
-		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
-								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
-								DisposeAfterUse::NO);
-	}
-
-	byte *audioCopy = (byte *)malloc(size);
-	if (!audioCopy)
-		return;
-	memcpy(audioCopy, data, size);
-
-	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
-	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
-	int scaledPan = (pan * 127) / 128;
-	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
-	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+	_audio.queueData(trackIdx, data, size, volume, pan);
 }
 
 void InsaneRebel1::processAudioFrame(int16 feedSize) {
-	if (!_player)
-		return;
-
-	SmushPlayer *sp = _player;
-
-	if (sp->_smushTracksNeedInit) {
-		sp->_smushTracksNeedInit = false;
-		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
-			sp->_smushDispatch[i].fadeRemaining = 0;
-			sp->_smushDispatch[i].fadeVolume = 0;
-			sp->_smushDispatch[i].fadeSampleRate = 0;
-			sp->_smushDispatch[i].elapsedAudio = 0;
-			sp->_smushDispatch[i].audioLength = 0;
-		}
-	}
-
-	for (int i = 0; i < sp->_smushNumTracks; i++) {
-		SmushPlayer::SmushAudioTrack &track = sp->_smushTracks[i];
-		SmushPlayer::SmushAudioDispatch &dispatch = sp->_smushDispatch[i];
-
-		if (track.state == TRK_STATE_INACTIVE || !track.blockPtr)
-			continue;
-
-		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && sp->isChanActive(CHN_SPEECH)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && sp->isChanActive(CHN_BKGMUS)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && sp->isChanActive(CHN_OTHER));
-
-		if (!isPlayableTrack)
-			continue;
-
-		int baseVolume;
-		switch (track.flags & TRK_TYPE_MASK) {
-		case IS_SFX:
-			baseVolume = (sp->_smushTrackVols[1] * track.volume) >> 7;
-			break;
-		case IS_BKG_MUSIC:
-			baseVolume = (sp->_smushTrackVols[3] * track.volume) >> 7;
-			break;
-		case IS_SPEECH:
-			baseVolume = (sp->_smushTrackVols[2] * track.volume) >> 7;
-			break;
-		default:
-			baseVolume = track.volume;
-			break;
-		}
-		int mixVolume = baseVolume * sp->_smushTrackVols[0] / 127;
-
-		// Handle FADING -> PLAYING transition
-		if (track.state == TRK_STATE_FADING) {
-			dispatch.headerPtr = track.dataBuf;
-			dispatch.dataBuf = track.subChunkPtr;
-			dispatch.dataSize = track.dataSize;
-			dispatch.currentOffset = 0;
-			dispatch.audioLength = 0;
-			track.state = TRK_STATE_PLAYING;
-		}
-
-		if (track.state != TRK_STATE_INACTIVE) {
-			int32 tmpFeedSize = feedSize;
-
-			while (tmpFeedSize > 0) {
-				int32 mixInFrameCount = dispatch.currentOffset;
-
-				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
-					if (dispatch.audioRemaining < 0)
-						dispatch.audioRemaining = 0;
-
-					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
-					if (dispatch.sampleRate > 0 && sp->_smushAudioSampleRate > 0) {
-						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / sp->_smushAudioSampleRate;
-						if (mixInFrameCount > maxFrames)
-							mixInFrameCount = maxFrames;
-					}
-
-					if (offset + mixInFrameCount > dispatch.dataSize)
-						mixInFrameCount = dispatch.dataSize - offset;
-
-					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
-						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
-						if (mixInFrameCount <= 0) {
-							track.state = TRK_STATE_ENDING;
-							break;
-						}
-					}
-
-					if (mixInFrameCount > 0) {
-						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize)
-							break;
-
-						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
-						dispatch.currentOffset -= mixInFrameCount;
-						dispatch.audioRemaining += mixInFrameCount;
-
-						if (dispatch.sampleRate > 0) {
-							int32 consumedFeed = mixInFrameCount * sp->_smushAudioSampleRate / dispatch.sampleRate;
-							tmpFeedSize -= consumedFeed;
-						} else {
-							tmpFeedSize -= mixInFrameCount;
-						}
-					}
-				}
-
-				if (dispatch.currentOffset <= 0) {
-					if (!sp->processAudioCodes(i, tmpFeedSize, mixVolume))
-						break;
-					if (dispatch.currentOffset <= 0)
-						break;
-				} else if (tmpFeedSize <= 0) {
-					break;
-				}
-			}
-		}
-
-		track.audioRemaining = dispatch.audioRemaining;
-		dispatch.state = track.state;
-	}
+	_audio.processFrame(_player, feedSize);
 }
 
 void InsaneRebel1::loadSfx() {
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index fde8876fd59..d358202f8ef 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -26,6 +26,7 @@
 #include "audio/mixer.h"
 #include "common/events.h"
 #include "scumm/insane/insane.h"
+#include "scumm/insane/rebel/rebel_audio.h"
 #include "scumm/smush/rebel/smush_player_ra1.h"
 
 namespace Scumm {
@@ -452,12 +453,8 @@ private:
 	static const int16 kDeathTimerInit = 30;
 	static const int16 kDamageCooldownInit = 10;
 
-	// Audio state (same structure as RA2)
-	static const int kMaxAudioTracks = 4;
-	Audio::QueuingAudioStream *_audioStreams[kMaxAudioTracks];
-	Audio::SoundHandle _audioHandles[kMaxAudioTracks];
-	bool _audioTrackActive[kMaxAudioTracks];
-	int _audioSampleRate;
+	// Streamed SMUSH audio
+	RebelAudio _audio;
 	static const int kNumSfx = 8;
 	enum SfxSlot {
 		kSfxLaserShot = 0,
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 2e6334daca2..6ed8cbf89fb 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -1557,8 +1557,9 @@ void InsaneRebel1::runGame() {
 }
 
 void InsaneRebel1::resetInteractiveVideoAudio() {
+	const int sampleRate = _audio.sampleRate();
 	terminateAudio();
-	initAudio(_audioSampleRate);
+	initAudio(sampleRate);
 }
 
 void InsaneRebel1::setupInteractiveVideoState(int32 startFrame) {
diff --git a/engines/scumm/insane/rebel2/audio.cpp b/engines/scumm/insane/rebel2/audio.cpp
index 14ace0f131a..cddd81a69c0 100644
--- a/engines/scumm/insane/rebel2/audio.cpp
+++ b/engines/scumm/insane/rebel2/audio.cpp
@@ -40,65 +40,18 @@ namespace Scumm {
 
 // initAudio -- Initialize audio system for RA2.
 void InsaneRebel2::initAudio(int sampleRate) {
-	_audioSampleRate = sampleRate;
-	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
-		_audioStreams[i] = nullptr;
-		_audioTrackActive[i] = false;
-	}
+	_audio.init(_vm, sampleRate);
 }
 
 // terminateAudio -- Stop all tracks and release audio streams.
 void InsaneRebel2::terminateAudio() {
-	for (int i = 0; i < kRA2MaxAudioTracks; i++) {
-		if (_audioTrackActive[i]) {
-			_vm->_mixer->stopHandle(_audioHandles[i]);
-			_audioTrackActive[i] = false;
-		}
-		if (_audioStreams[i]) {
-			_audioStreams[i]->finish();
-			_audioStreams[i] = nullptr;
-		}
-	}
+	_audio.terminate();
 }
 
 // queueAudioData -- Queue raw PCM data for playback on a track.
 // Creates the queuing stream on first use. RA2 audio is 8-bit unsigned mono.
 void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int volume, int pan) {
-	if (trackIdx < 0 || trackIdx >= kRA2MaxAudioTracks || size <= 0 || !data) {
-		debug(5, "InsaneRebel2::queueAudioData: Invalid params trackIdx=%d size=%d data=%p", trackIdx, size, (void*)data);
-		return;
-	}
-
-	debug(5, "InsaneRebel2::queueAudioData: trackIdx=%d size=%d volume=%d pan=%d", trackIdx, size, volume, pan);
-
-	// Create audio stream if not already active
-	if (!_audioStreams[trackIdx]) {
-		// RA2 audio is 8-bit unsigned mono at the track's sample rate
-		debug("InsaneRebel2: Creating audio stream for track %d at %d Hz", trackIdx, _audioSampleRate);
-		_audioStreams[trackIdx] = Audio::makeQueuingAudioStream(_audioSampleRate, false);
-		_audioTrackActive[trackIdx] = true;
-		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioHandles[trackIdx],
-								_audioStreams[trackIdx], -1, Audio::Mixer::kMaxChannelVolume, 0,
-								DisposeAfterUse::NO);
-	}
-
-	debug(6, "InsaneRebel2: Queueing %d bytes to track %d (vol=%d)", size, trackIdx, volume);
-
-	// Copy the audio data since queueBuffer may need to own it
-	byte *audioCopy = (byte *)malloc(size);
-	if (!audioCopy) {
-		return;
-	}
-	memcpy(audioCopy, data, size);
-
-	// Queue the audio data - RA2 SMUSH audio is 8-bit unsigned mono
-	_audioStreams[trackIdx]->queueBuffer(audioCopy, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
-
-	// Apply volume and pan to the channel
-	int scaledVolume = (volume * Audio::Mixer::kMaxChannelVolume) / 127;
-	int scaledPan = (pan * 127) / 128;  // Convert -128..127 to -127..127
-	_vm->_mixer->setChannelVolume(_audioHandles[trackIdx], scaledVolume);
-	_vm->_mixer->setChannelBalance(_audioHandles[trackIdx], scaledPan);
+	_audio.queueData(trackIdx, data, size, volume, pan);
 }
 
 //
@@ -109,164 +62,7 @@ void InsaneRebel2::queueAudioData(int trackIdx, uint8 *data, int32 size, int vol
 // iMUSE is null.
 //
 void InsaneRebel2::processAudioFrame(int16 feedSize) {
-	if (!_player) {
-		return;
-	}
-
-	// Initialize dispatch data if needed (normally done in processDispatches for iMUSE games)
-	if (_player->_smushTracksNeedInit) {
-		_player->_smushTracksNeedInit = false;
-		for (int i = 0; i < SMUSH_MAX_TRACKS; i++) {
-			_player->_smushDispatch[i].fadeRemaining = 0;
-			_player->_smushDispatch[i].fadeVolume = 0;
-			_player->_smushDispatch[i].fadeSampleRate = 0;
-			_player->_smushDispatch[i].elapsedAudio = 0;
-			_player->_smushDispatch[i].audioLength = 0;
-		}
-	}
-
-	// Access SmushPlayer's audio track data (InsaneRebel2 is a friend class)
-	// Only iterate over actually allocated tracks (not SMUSH_MAX_TRACKS)
-	for (int i = 0; i < _player->_smushNumTracks; i++) {
-		SmushPlayer::SmushAudioTrack &track = _player->_smushTracks[i];
-		SmushPlayer::SmushAudioDispatch &dispatch = _player->_smushDispatch[i];
-
-		if (track.state == TRK_STATE_INACTIVE) {
-			continue;
-		}
-
-		// Skip tracks that don't have valid buffer pointers yet
-		// Note: dispatch.dataBuf is set when transitioning from FADING to PLAYING,
-		// so tracks in FADING state won't have it set yet - that's OK, they'll be
-		// transitioned below and then processed
-		if (!track.blockPtr) {
-			debug(5, "InsaneRebel2: Skipping track %d - blockPtr=%p state=%d",
-				  i, (void*)track.blockPtr, track.state);
-			continue;
-		}
-
-		// Check if this track type should be played
-		bool isPlayableTrack = ((track.flags & TRK_TYPE_MASK) == IS_SPEECH && _player->isChanActive(CHN_SPEECH)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_BKG_MUSIC && _player->isChanActive(CHN_BKGMUS)) ||
-							   ((track.flags & TRK_TYPE_MASK) == IS_SFX && _player->isChanActive(CHN_OTHER));
-
-		if (!isPlayableTrack) {
-			continue;
-		}
-
-		// Calculate base volume for this track type
-		int baseVolume;
-		switch (track.flags & TRK_TYPE_MASK) {
-		case IS_SFX:
-			baseVolume = (_player->_smushTrackVols[1] * track.volume) >> 7;
-			break;
-		case IS_BKG_MUSIC:
-			baseVolume = (_player->_smushTrackVols[3] * track.volume) >> 7;
-			break;
-		case IS_SPEECH:
-			baseVolume = (_player->_smushTrackVols[2] * track.volume) >> 7;
-			break;
-		default:
-			baseVolume = track.volume;
-			break;
-		}
-		int mixVolume = baseVolume * _player->_smushTrackVols[0] / 127;
-
-		// Handle track state transitions: FADING -> PLAYING
-		if (track.state == TRK_STATE_FADING) {
-			dispatch.headerPtr = track.dataBuf;
-			dispatch.dataBuf = track.subChunkPtr;
-			dispatch.dataSize = track.dataSize;
-			dispatch.currentOffset = 0;
-			dispatch.audioLength = 0;
-			track.state = TRK_STATE_PLAYING;
-		}
-
-		// Process audio for this track
-		if (track.state != TRK_STATE_INACTIVE) {
-			int32 tmpFeedSize = feedSize;
-
-			while (tmpFeedSize > 0) {
-				int32 mixInFrameCount = dispatch.currentOffset;
-
-				// Use dispatch.dataBuf and dispatch.dataSize which are set consistently
-				// when the track transitions from FADING to PLAYING, and audioRemaining
-				// is calculated relative to these values by processAudioCodes
-				if (mixInFrameCount > 0 && dispatch.dataBuf && dispatch.dataSize > 0) {
-					// Ensure audioRemaining is non-negative for proper circular buffer access
-					if (dispatch.audioRemaining < 0) {
-						debug(5, "InsaneRebel2: Resetting negative audioRemaining=%d for track %d", dispatch.audioRemaining, i);
-						dispatch.audioRemaining = 0;
-					}
-					int32 offset = dispatch.audioRemaining % dispatch.dataSize;
-
-					// Limit to feed size proportional to sample rate
-					if (dispatch.sampleRate > 0 && _player->_smushAudioSampleRate > 0) {
-						int32 maxFrames = dispatch.sampleRate * tmpFeedSize / _player->_smushAudioSampleRate;
-						if (mixInFrameCount > maxFrames) {
-							mixInFrameCount = maxFrames;
-						}
-					}
-
-					// Don't read past the buffer
-					if (offset + mixInFrameCount > dispatch.dataSize) {
-						mixInFrameCount = dispatch.dataSize - offset;
-					}
-
-					// Make sure we don't exceed available data
-					if (dispatch.audioRemaining + mixInFrameCount > track.availableSize) {
-						mixInFrameCount = track.availableSize - dispatch.audioRemaining;
-						if (mixInFrameCount <= 0) {
-							// Track is ending - no more data
-							track.state = TRK_STATE_ENDING;
-							break;
-						}
-					}
-
-					if (mixInFrameCount > 0) {
-						// Safety check: verify the pointer and offset are within bounds
-						if (!dispatch.dataBuf || offset < 0 || offset + mixInFrameCount > dispatch.dataSize) {
-							debug(1, "InsaneRebel2: Invalid audio buffer access track=%d dataBuf=%p offset=%d mixInFrameCount=%d dataSize=%d",
-								  i, (void*)dispatch.dataBuf, offset, mixInFrameCount, dispatch.dataSize);
-							break;
-						}
-
-						// Queue audio data directly to our audio streams
-						queueAudioData(i, &dispatch.dataBuf[offset], mixInFrameCount, mixVolume, track.pan);
-
-						// Update dispatch state
-						dispatch.currentOffset -= mixInFrameCount;
-						dispatch.audioRemaining += mixInFrameCount;
-
-						// Calculate how much feed time was consumed
-						if (dispatch.sampleRate > 0) {
-							int32 consumedFeed = mixInFrameCount * _player->_smushAudioSampleRate / dispatch.sampleRate;
-							tmpFeedSize -= consumedFeed;
-						} else {
-							tmpFeedSize -= mixInFrameCount;
-						}
-					}
-				}
-
-				// If currentOffset is depleted, process audio codes to get more
-				if (dispatch.currentOffset <= 0) {
-					// processAudioCodes returns true if there's more audio, false if done
-					if (!_player->processAudioCodes(i, tmpFeedSize, mixVolume)) {
-						break;
-					}
-					// If still no offset after processing codes, we're done
-					if (dispatch.currentOffset <= 0) {
-						break;
-					}
-				} else if (tmpFeedSize <= 0) {
-					break;
-				}
-			}
-		}
-
-		track.audioRemaining = dispatch.audioRemaining;
-		dispatch.state = track.state;
-	}
+	_audio.processFrame(_player, feedSize);
 }
 
 // ---------------------------------------------------------------------------
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 42fe279ad97..ba9e1b5ed92 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -370,11 +370,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_hudOverlay2Nut = nullptr;   // DAT_0047fe80 - Secondary HUD overlay
 
 	// Initialize audio system for RA2 (since we don't use iMUSE)
-	_audioSampleRate = 11025;  // RA2 audio is 11025 Hz, not 22050 Hz
-	for (i = 0; i < kRA2MaxAudioTracks; i++) {
-		_audioStreams[i] = nullptr;
-		_audioTrackActive[i] = false;
-	}
+	initAudio(11025);  // RA2 audio is 11025 Hz, not 22050 Hz
 
 	// Initialize and load sound effects (SYSTM/*.SAD files)
 	for (i = 0; i < kRA2NumSfx; i++) {
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 8b4d436768d..bf19ea2af37 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -27,6 +27,7 @@
 #include "scumm/smush/smush_player.h"
 
 #include "scumm/insane/insane.h"
+#include "scumm/insane/rebel/rebel_audio.h"
 
 #include "common/keyboard.h"
 #include "common/list.h"
@@ -1222,12 +1223,7 @@ public:
 	// ---------------------------------------------------------------------------
 	// RA2 doesn't use iMUSE -- audio is handled directly through the mixer.
 
-	static const int kRA2MaxAudioTracks = 4;
-
-	Audio::QueuingAudioStream *_audioStreams[kRA2MaxAudioTracks];
-	Audio::SoundHandle _audioHandles[kRA2MaxAudioTracks];
-	bool _audioTrackActive[kRA2MaxAudioTracks];
-	int _audioSampleRate;
+	RebelAudio _audio;
 
 	// Initialize audio system for RA2
 	void initAudio(int sampleRate);
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index fedb366f729..b77e4a876a9 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -133,6 +133,7 @@ MODULE_OBJS += \
 	insane/insane_enemy.o \
 	insane/insane_scenes.o \
 	insane/insane_iact.o \
+	insane/rebel/rebel_audio.o \
 	insane/rebel1/rebel.o \
 	insane/rebel1/audio.o \
 	insane/rebel1/iact.o \
@@ -155,6 +156,7 @@ MODULE_OBJS += \
 	smush/rebel/codec_ra1.o \
 	smush/rebel/codec_ra2.o \
 	smush/rebel/smush_multi_font.o \
+	smush/rebel/smush_player_rebel.o \
 	smush/rebel/smush_player_ra1.o \
 	smush/rebel/smush_player_ra2.o
 
diff --git a/engines/scumm/smush/rebel/smush_player_ra2.cpp b/engines/scumm/smush/rebel/smush_player_ra2.cpp
index 2610822cdee..fe8165bde42 100644
--- a/engines/scumm/smush/rebel/smush_player_ra2.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra2.cpp
@@ -488,12 +488,6 @@ void SmushPlayerRebel2::handleLoad(int32 subSize, Common::SeekableReadStream &b)
 	}
 }
 
-void SmushPlayer::ensureMultiFont() {
-	if (!_multiFont) {
-		_multiFont = new SmushMultiFont(_vm, this, true);
-	}
-}
-
 /**
  * RA2-specific text rendering using SmushMultiFont for inline font switching.
  */
@@ -557,35 +551,6 @@ void SmushPlayerRebel2::ra2SelectFrameBuffer(int width, int height) {
 	}
 }
 
-/**
- * Apply RA2 FOBJ position offsets and clamp to buffer bounds.
- * Modifies left, top, width, height in place.
- * When srcSkipY is non-null, outputs the number of source rows to skip
- * when top is clipped from negative (for codecs with row-size prefixes).
- */
-void SmushPlayer::adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
-	left += _fobjOffsetX;
-	top += _fobjOffsetY;
-
-	int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
-	if (top < 0) {
-		if (srcSkipY)
-			*srcSkipY = -top;
-		height += top;
-		top = 0;
-	}
-	if (left < 0) {
-		width += left;
-		left = 0;
-	}
-	if (top + height > bufHeight) {
-		height = bufHeight - top;
-	}
-	if (left + width > pitch) {
-		width = pitch - left;
-	}
-}
-
 /**
  * Dispatch to RA2-specific codec functions.
  * Returns true if the codec was handled, false for standard codecs.
@@ -632,37 +597,6 @@ void SmushPlayerRebel2::ra2StoreFobjData(int codec, const byte *data, int32 data
 	_storeFrame = false;
 }
 
-/**
- * Cache the most recent frame FOBJ for GOST re-rendering.
- */
-void SmushPlayer::rememberLastFobj(int codec, const byte *data, int32 dataSize,
-									  int left, int top, int width, int height) {
-	if (dataSize <= 0) {
-		_hasFrameFobjForGost = false;
-		return;
-	}
-
-	byte *newData = (byte *)realloc(_lastFobjData, dataSize);
-	if (newData == nullptr) {
-		warning("SmushPlayer::rememberLastFobj: Failed to allocate %d bytes", dataSize);
-		free(_lastFobjData);
-		_lastFobjData = nullptr;
-		_lastFobjDataSize = 0;
-		_hasFrameFobjForGost = false;
-		return;
-	}
-
-	_lastFobjData = newData;
-	memcpy(_lastFobjData, data, dataSize);
-	_lastFobjDataSize = dataSize;
-	_lastFobjCodec = codec;
-	_lastFobjLeft = left;
-	_lastFobjTop = top;
-	_lastFobjWidth = width;
-	_lastFobjHeight = height;
-	_hasFrameFobjForGost = true;
-}
-
 /**
  * RA2 GOST chunk handler.
  * Re-renders the most recent frame FOBJ at the supplied ghost position.
@@ -800,33 +734,4 @@ void SmushPlayerRebel2::handleGameProcessAudio(int16 feedSize) {
 	}
 }
 
-// Masked region management — used by InsaneRebel2
-
-void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
-	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
-		if (*it == rect) {
-			return; // Already exists
-		}
-	}
-	_maskedRegions.push_back(rect);
-}
-
-void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
-	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
-		if (*it == rect) {
-			_maskedRegions.erase(it);
-			return;
-		}
-	}
-}
-
-void SmushPlayer::clearMaskedRegions() {
-	_maskedRegions.clear();
-}
-
-void SmushPlayer::setScrollOffset(int x, int y) {
-	_scrollX = MAX(0, x);
-	_scrollY = MAX(0, y);
-}
-
 } // End of namespace Scumm
diff --git a/engines/scumm/smush/rebel/smush_player_rebel.cpp b/engines/scumm/smush/rebel/smush_player_rebel.cpp
new file mode 100644
index 00000000000..681c3e283cf
--- /dev/null
+++ b/engines/scumm/smush/rebel/smush_player_rebel.cpp
@@ -0,0 +1,110 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/rect.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/smush_multi_font.h"
+#include "scumm/smush/smush_player.h"
+
+namespace Scumm {
+
+void SmushPlayer::ensureMultiFont() {
+	if (!_multiFont)
+		_multiFont = new SmushMultiFont(_vm, this, true);
+}
+
+void SmushPlayer::addMaskedRegion(const Common::Rect &rect) {
+	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+		if (*it == rect)
+			return;
+	}
+	_maskedRegions.push_back(rect);
+}
+
+void SmushPlayer::removeMaskedRegion(const Common::Rect &rect) {
+	for (Common::List<Common::Rect>::iterator it = _maskedRegions.begin(); it != _maskedRegions.end(); ++it) {
+		if (*it == rect) {
+			_maskedRegions.erase(it);
+			return;
+		}
+	}
+}
+
+void SmushPlayer::clearMaskedRegions() {
+	_maskedRegions.clear();
+}
+
+void SmushPlayer::setScrollOffset(int x, int y) {
+	_scrollX = MAX(0, x);
+	_scrollY = MAX(0, y);
+}
+
+void SmushPlayer::adjustFrameCoords(int &left, int &top, int &width, int &height, int pitch, int *srcSkipY) {
+	left += _fobjOffsetX;
+	top += _fobjOffsetY;
+
+	int bufHeight = (_dst == _specialBuffer) ? _height : _vm->_screenHeight;
+	if (top < 0) {
+		if (srcSkipY)
+			*srcSkipY = -top;
+		height += top;
+		top = 0;
+	}
+	if (left < 0) {
+		width += left;
+		left = 0;
+	}
+	if (top + height > bufHeight)
+		height = bufHeight - top;
+	if (left + width > pitch)
+		width = pitch - left;
+}
+
+void SmushPlayer::rememberLastFobj(int codec, const byte *data, int32 dataSize,
+		int left, int top, int width, int height) {
+	if (dataSize <= 0) {
+		_hasFrameFobjForGost = false;
+		return;
+	}
+
+	byte *newData = (byte *)realloc(_lastFobjData, dataSize);
+	if (newData == nullptr) {
+		warning("SmushPlayer::rememberLastFobj: Failed to allocate %d bytes", dataSize);
+		free(_lastFobjData);
+		_lastFobjData = nullptr;
+		_lastFobjDataSize = 0;
+		_hasFrameFobjForGost = false;
+		return;
+	}
+
+	_lastFobjData = newData;
+	memcpy(_lastFobjData, data, dataSize);
+	_lastFobjDataSize = dataSize;
+	_lastFobjCodec = codec;
+	_lastFobjLeft = left;
+	_lastFobjTop = top;
+	_lastFobjWidth = width;
+	_lastFobjHeight = height;
+	_hasFrameFobjForGost = true;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/smush/smush_player.h b/engines/scumm/smush/smush_player.h
index d0bdd39ff8b..4f967932c5a 100644
--- a/engines/scumm/smush/smush_player.h
+++ b/engines/scumm/smush/smush_player.h
@@ -102,6 +102,7 @@ class SmushDeltaBlocksDecoder;
 class SmushDeltaGlyphsDecoder;
 class IMuseDigital;
 class Insane;
+class RebelAudio;
 class SmushPlayerRebel1;
 class SmushPlayerRebel2;
 
@@ -109,6 +110,7 @@ class SmushPlayer {
 	friend class Insane;
 	friend class InsaneRebel1;
 	friend class InsaneRebel2;
+	friend class RebelAudio;
 	friend class SmushPlayerRebel1;
 	friend class SmushPlayerRebel2;
 	friend class SmushMultiFont;


Commit: 088825f21788afc474d36f7adec8655ad6ac0b61
    https://github.com/scummvm/scummvm/commit/088825f21788afc474d36f7adec8655ad6ac0b61
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:13+02:00

Commit Message:
SCUMM: RA1: Extract common iterator code

Changed paths:
  A engines/scumm/smush/rebel/anim_ra1.h
    engines/scumm/insane/rebel1/levels.cpp
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/smush/rebel/smush_player_ra1.cpp


diff --git a/engines/scumm/insane/rebel1/levels.cpp b/engines/scumm/insane/rebel1/levels.cpp
index 705cf871353..f32b598aee8 100644
--- a/engines/scumm/insane/rebel1/levels.cpp
+++ b/engines/scumm/insane/rebel1/levels.cpp
@@ -25,6 +25,7 @@
 #include "scumm/scumm_v7.h"
 #include "scumm/file.h"
 #include "scumm/insane/rebel1/rebel.h"
+#include "scumm/smush/rebel/anim_ra1.h"
 #include "scumm/smush/rebel/codec_ra1.h"
 
 namespace Scumm {
@@ -86,45 +87,25 @@ bool InsaneRebel1::loadRA1Nut(const char *filename, RA1SpriteBank &bank) {
 	// Pass 1: Parse ANIM chunks properly and collect FRME->FOBJ offsets in-order.
 	uint32 decodedSize = 0;
 	uint16 foundSprites = 0;
-	uint32 chunkOffset = 0;
-	while (chunkOffset + 8 <= animSize && foundSprites < expectedSprites) {
-		uint32 chunkTag = READ_BE_UINT32(data + chunkOffset);
-		uint32 chunkSize = READ_BE_UINT32(data + chunkOffset + 4);
-		uint32 chunkDataOffset = chunkOffset + 8;
-		uint32 chunkEnd = chunkDataOffset + chunkSize;
-		if (chunkEnd > animSize)
-			break;
-
-		if (chunkTag == MKTAG('F','R','M','E')) {
-			uint32 subOffset = chunkDataOffset;
-			while (subOffset + 8 <= chunkEnd) {
-				uint32 subTag = READ_BE_UINT32(data + subOffset);
-				uint32 subSize = READ_BE_UINT32(data + subOffset + 4);
-				uint32 subDataOffset = subOffset + 8;
-				uint32 subEnd = subDataOffset + subSize;
-				if (subEnd > chunkEnd)
-					break;
-
-				if (subTag == MKTAG('F','O','B','J') && subOffset + 22 <= animSize) {
-					uint16 w = READ_LE_UINT16(data + subOffset + 14);
-					uint16 h = READ_LE_UINT16(data + subOffset + 16);
+	RA1AnimChunkIterator chunks(data, animSize);
+	RA1AnimChunk chunk;
+	while (chunks.next(chunk) && foundSprites < expectedSprites) {
+		if (chunk.tag == MKTAG('F','R','M','E')) {
+			RA1AnimChunkIterator subChunks(data, (uint32)chunk.dataOffset, (uint32)chunk.endOffset);
+			RA1AnimChunk subChunk;
+			while (subChunks.next(subChunk)) {
+				if (subChunk.tag == MKTAG('F','O','B','J') && subChunk.size >= 14) {
+					uint16 w = READ_LE_UINT16(data + subChunk.dataOffset + 6);
+					uint16 h = READ_LE_UINT16(data + subChunk.dataOffset + 8);
 					decodedSize += (uint32)w * (uint32)h;
-					fobjOffsets[foundSprites] = subOffset;
+					fobjOffsets[foundSprites] = (uint32)subChunk.offset;
 					break;
 				}
-
-				subOffset = subEnd;
-				if (subSize & 1)
-					subOffset++;
 			}
 			// Always increment for every FRME to preserve char-to-glyph alignment.
 			// Empty FRMEs (no FOBJ) keep fobjOffsets[i] = 0, decoded as blank sprites.
 			foundSprites++;
 		}
-
-		chunkOffset = chunkEnd;
-		if (chunkSize & 1)
-			chunkOffset++;
 	}
 
 	bank.decodedData = (byte *)calloc(decodedSize ? decodedSize : 1, 1);
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 6ed8cbf89fb..6c1b16854ce 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -26,6 +26,7 @@
 
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/anim_ra1.h"
 #include "scumm/smush/smush_player.h"
 #include "scumm/insane/rebel1/rebel.h"
 
@@ -44,26 +45,22 @@ int32 findAnimFrameChunkOffset(ScummEngine_v7 *vm, const char *filename, int32 t
 	int32 result = -1;
 	if (file->size() >= 8) {
 		file->readUint32BE();
-		file->readUint32BE();
+		const uint32 animSize = file->readUint32BE();
+		const int64 animEnd = MIN<int64>((int64)file->pos() + animSize, file->size());
 
 		int32 frameIndex = 0;
-		while (file->pos() + 8 <= file->size()) {
-			const int32 chunkOffset = (int32)file->pos();
-			const uint32 chunkTag = file->readUint32BE();
-			const int32 chunkSize = (int32)file->readUint32BE();
-			const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
-			if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
-				break;
-
-			if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
+		RA1AnimStreamChunkIterator chunks(*file, animEnd);
+		RA1AnimChunk chunk;
+		while (chunks.next(chunk)) {
+			if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
 				if (frameIndex == targetFrame) {
-					result = chunkOffset;
+					result = (int32)chunk.offset;
 					break;
 				}
 				frameIndex++;
 			}
 
-			file->seek(nextChunkOffset, SEEK_SET);
+			chunks.skip(chunk);
 		}
 	}
 
@@ -86,40 +83,28 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
 	int32 result = -1;
 	if (file->size() >= 8) {
 		file->readUint32BE();
-		file->readUint32BE();
+		const uint32 animSize = file->readUint32BE();
+		const int64 animEnd = MIN<int64>((int64)file->pos() + animSize, file->size());
 
 		int32 frameIndex = 0;
-		while (file->pos() + 8 <= file->size()) {
-			const int32 chunkOffset = (int32)file->pos();
-			const uint32 chunkTag = file->readUint32BE();
-			const int32 chunkSize = (int32)file->readUint32BE();
-			const int32 chunkDataOffset = (int32)file->pos();
-			const int32 nextChunkOffset = chunkOffset + 8 + chunkSize + ((chunkSize & 1) ? 1 : 0);
-			if (nextChunkOffset < chunkOffset || nextChunkOffset > file->size())
-				break;
-
-			if (chunkTag == MKTAG('F', 'R', 'M', 'E')) {
-				const int32 frameEnd = chunkDataOffset + chunkSize;
-				while (file->pos() + 8 <= frameEnd) {
-					const int32 subChunkOffset = (int32)file->pos();
-					const uint32 subChunkTag = file->readUint32BE();
-					const int32 subChunkSize = (int32)file->readUint32BE();
-					const int32 subChunkDataOffset = (int32)file->pos();
-					const int32 nextSubChunkOffset = subChunkDataOffset + subChunkSize + ((subChunkSize & 1) ? 1 : 0);
-					if (nextSubChunkOffset < subChunkOffset || nextSubChunkOffset > frameEnd)
-						break;
-
-					if (subChunkTag == MKTAG('G', 'A', 'M', 'E') && subChunkSize >= 8) {
+		RA1AnimStreamChunkIterator chunks(*file, animEnd);
+		RA1AnimChunk chunk;
+		while (chunks.next(chunk)) {
+			if (chunk.tag == MKTAG('F', 'R', 'M', 'E')) {
+				RA1AnimStreamChunkIterator subChunks(*file, chunk.endOffset);
+				RA1AnimChunk subChunk;
+				while (subChunks.next(subChunk)) {
+					if (subChunk.tag == MKTAG('G', 'A', 'M', 'E') && subChunk.size >= 8) {
 						const uint32 opcode = file->readUint32BE();
 						const int32 counter = (int32)file->readUint32BE();
 						if (opcode == 0x0B && counter >= targetCounter) {
 							localFrame = frameIndex;
-							result = chunkOffset;
+							result = (int32)chunk.offset;
 							break;
 						}
 					}
 
-					file->seek(nextSubChunkOffset, SEEK_SET);
+					subChunks.skip(subChunk);
 				}
 
 				if (result >= 0)
@@ -128,7 +113,7 @@ int32 findAnimFrameChunkOffsetByGameCounter(ScummEngine_v7 *vm, const char *file
 				frameIndex++;
 			}
 
-			file->seek(nextChunkOffset, SEEK_SET);
+			chunks.skip(chunk);
 		}
 	}
 
diff --git a/engines/scumm/smush/rebel/anim_ra1.h b/engines/scumm/smush/rebel/anim_ra1.h
new file mode 100644
index 00000000000..fea359322cb
--- /dev/null
+++ b/engines/scumm/smush/rebel/anim_ra1.h
@@ -0,0 +1,177 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCUMM_SMUSH_REBEL_ANIM_RA1_H
+#define SCUMM_SMUSH_REBEL_ANIM_RA1_H
+
+#include "common/endian.h"
+#include "common/stream.h"
+
+namespace Scumm {
+
+struct RA1AnimChunk {
+	uint32 tag;
+	uint32 size;
+	int64 offset;
+	int64 dataOffset;
+	int64 endOffset;
+	int64 nextOffset;
+	const byte *data;
+};
+
+class RA1AnimChunkIterator {
+public:
+	RA1AnimChunkIterator(const byte *data, uint32 size)
+		: _data(data), _offset(0), _endOffset(size) {}
+
+	RA1AnimChunkIterator(const byte *data, uint32 startOffset, uint32 endOffset)
+		: _data(data), _offset(startOffset), _endOffset(endOffset) {
+		if (_offset > _endOffset)
+			_offset = _endOffset;
+	}
+
+	bool next(RA1AnimChunk &chunk) {
+		if (_data == nullptr || _offset + 8 > _endOffset)
+			return false;
+
+		const uint32 size = READ_BE_UINT32(_data + _offset + 4);
+		const uint32 dataOffset = _offset + 8;
+		if (size > _endOffset - dataOffset) {
+			_offset = _endOffset;
+			return false;
+		}
+
+		chunk.tag = READ_BE_UINT32(_data + _offset);
+		chunk.size = size;
+		chunk.offset = _offset;
+		chunk.dataOffset = dataOffset;
+		chunk.endOffset = dataOffset + size;
+		chunk.nextOffset = chunk.endOffset + (size & 1);
+		if (chunk.nextOffset > _endOffset)
+			chunk.nextOffset = _endOffset;
+		chunk.data = _data + dataOffset;
+
+		_offset = (uint32)chunk.nextOffset;
+		return true;
+	}
+
+private:
+	const byte *_data;
+	uint32 _offset;
+	uint32 _endOffset;
+};
+
+class RA1AnimStreamChunkIterator {
+public:
+	RA1AnimStreamChunkIterator(Common::SeekableReadStream &stream, int64 endOffset)
+		: _stream(stream), _endOffset(endOffset) {}
+
+	bool next(RA1AnimChunk &chunk) {
+		if (_stream.pos() + 8 > _endOffset)
+			return false;
+
+		const int64 chunkOffset = _stream.pos();
+		const uint32 tag = _stream.readUint32BE();
+		const uint32 size = _stream.readUint32BE();
+		const int64 dataOffset = _stream.pos();
+		const int64 endOffset = dataOffset + size;
+		if (endOffset < dataOffset || endOffset > _endOffset)
+			return false;
+
+		chunk.tag = tag;
+		chunk.size = size;
+		chunk.offset = chunkOffset;
+		chunk.dataOffset = dataOffset;
+		chunk.endOffset = endOffset;
+		chunk.nextOffset = endOffset + (size & 1);
+		if (chunk.nextOffset > _endOffset)
+			chunk.nextOffset = _endOffset;
+		chunk.data = nullptr;
+		return true;
+	}
+
+	void skip(const RA1AnimChunk &chunk) {
+		_stream.seek(chunk.nextOffset, SEEK_SET);
+	}
+
+private:
+	Common::SeekableReadStream &_stream;
+	int64 _endOffset;
+};
+
+class RA1FrameChunkIterator {
+public:
+	RA1FrameChunkIterator(Common::SeekableReadStream &stream, int32 &remaining)
+		: _stream(stream), _remaining(remaining) {}
+
+	bool next(RA1AnimChunk &chunk) {
+		while (_remaining > 1) {
+			if ((_stream.pos() & 1) && _remaining > 0) {
+				const byte pad = _stream.readByte();
+				if (pad == 0) {
+					_remaining--;
+				} else {
+					_stream.seek(-1, SEEK_CUR);
+				}
+			}
+
+			if (_remaining < 8) {
+				_stream.skip(_remaining);
+				_remaining = 0;
+				return false;
+			}
+
+			const int64 chunkOffset = _stream.pos();
+			chunk.tag = _stream.readUint32BE();
+			chunk.size = _stream.readUint32BE();
+			chunk.offset = chunkOffset;
+			chunk.dataOffset = _stream.pos();
+			chunk.endOffset = chunk.dataOffset + chunk.size;
+			chunk.nextOffset = chunk.endOffset;
+			chunk.data = nullptr;
+			return true;
+		}
+
+		if (_remaining == 1) {
+			_stream.skip(1);
+			_remaining = 0;
+		}
+		return false;
+	}
+
+	bool fits(const RA1AnimChunk &chunk) const {
+		return _remaining >= 8 && chunk.size <= (uint32)(_remaining - 8);
+	}
+
+	void skip(const RA1AnimChunk &chunk) {
+		const int32 consumed = fits(chunk) ? (int32)chunk.size + 8 : _remaining;
+		_remaining -= consumed;
+		_stream.seek(chunk.endOffset, SEEK_SET);
+	}
+
+private:
+	Common::SeekableReadStream &_stream;
+	int32 &_remaining;
+};
+
+} // End of namespace Scumm
+
+#endif
diff --git a/engines/scumm/smush/rebel/smush_player_ra1.cpp b/engines/scumm/smush/rebel/smush_player_ra1.cpp
index e33221d76ee..d04d673155f 100644
--- a/engines/scumm/smush/rebel/smush_player_ra1.cpp
+++ b/engines/scumm/smush/rebel/smush_player_ra1.cpp
@@ -29,6 +29,7 @@
 
 #include "scumm/file.h"
 #include "scumm/scumm_v7.h"
+#include "scumm/smush/rebel/anim_ra1.h"
 #include "scumm/smush/rebel/codec_ra1.h"
 #include "scumm/smush/smush_font.h"
 #include "scumm/smush/rebel/codec_ra2.h"
@@ -690,33 +691,20 @@ void SmushPlayerRebel1::handleFrameObject(int32 subSize, Common::SeekableReadStr
 static bool ra1FrameHasGameChunk(Common::SeekableReadStream &b, int32 frameSize) {
 	const int64 frameStart = b.pos();
 	int32 remaining = frameSize;
-
-	while (remaining > 1) {
-		if ((b.pos() & 1) && remaining > 0) {
-			const byte pad = b.readByte();
-			if (pad == 0) {
-				remaining--;
-			} else {
-				b.seek(-1, SEEK_CUR);
-			}
-		}
-		if (remaining < 8)
+	RA1FrameChunkIterator chunks(b, remaining);
+	RA1AnimChunk chunk;
+	while (chunks.next(chunk)) {
+		if (chunk.tag == MKTAG('F', 'R', 'M', 'E'))
 			break;
-
-		const uint32 subType = b.readUint32BE();
-		const int32 subSize = b.readUint32BE();
-		const int64 subDataPos = b.pos();
-
-		if (subType == MKTAG('F', 'R', 'M', 'E'))
-			break;
-		if (subType == MKTAG('G', 'A', 'M', 'E') ||
-				subType == MKTAG('G', 'A', 'M', '2')) {
+		if (chunk.tag == MKTAG('G', 'A', 'M', 'E') ||
+				chunk.tag == MKTAG('G', 'A', 'M', '2')) {
 			b.seek(frameStart, SEEK_SET);
 			return true;
 		}
 
-		remaining -= subSize + 8;
-		b.seek(subDataPos + subSize, SEEK_SET);
+		if (!chunks.fits(chunk))
+			break;
+		chunks.skip(chunk);
 	}
 
 	b.seek(frameStart, SEEK_SET);
@@ -928,44 +916,21 @@ void SmushPlayerRebel1::handleFrame(int32 frameSize, Common::SeekableReadStream
 			memset(_dst, 0, _width * _height);
 	}
 
-	while (frameSize > 0) {
-		// RA1 exits when <=1 byte remains
-		if (frameSize <= 1) {
-			if (frameSize == 1)
-				b.skip(1);
-			break;
-		}
-
-		// RA1 top-of-loop alignment (FUN_1FDBC)
-		if ((b.pos() & 1) && frameSize > 0) {
-			byte peek = b.readByte();
-			if (peek == 0) {
-				frameSize--;
-			} else {
-				b.seek(-1, SEEK_CUR);
-			}
-		}
-
-		if (frameSize < 8) {
-			b.skip(frameSize);
-			break;
-		}
-
-		uint32 subType = b.readUint32BE();
-		int32 subSize = b.readUint32BE();
-		int32 subOffset = b.pos();
+	RA1FrameChunkIterator chunks(b, frameSize);
+	RA1AnimChunk chunk;
+	while (chunks.next(chunk)) {
+		const int32 subSize = (int32)chunk.size;
 
 		// Guard against consuming next frame marker
-		if (subType == MKTAG('F','R','M','E')) {
-			b.seek(-8, SEEK_CUR);
+		if (chunk.tag == MKTAG('F','R','M','E')) {
+			b.seek(chunk.offset, SEEK_SET);
 			break;
 		}
 
-		if (ra1DispatchFrameChunk(subType, subSize, frameSize, b, fastForwarding))
+		if (ra1DispatchFrameChunk(chunk.tag, subSize, frameSize, b, fastForwarding))
 			continue;
 
-		frameSize -= subSize + 8;
-		b.seek(subOffset + subSize, SEEK_SET);
+		chunks.skip(chunk);
 		// RA1 uses top-of-loop alignment, not bottom-of-loop padding
 	}
 


Commit: 915b9dab5f5df540c617ef674b2ca15ec2ab7224
    https://github.com/scummvm/scummvm/commit/915b9dab5f5df540c617ef674b2ca15ec2ab7224
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:13+02:00

Commit Message:
SCUMM: RA1: Refactor handleGameOpcode

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


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 98ed443d600..c89961c50a4 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -1945,6 +1945,331 @@ void InsaneRebel1::procIACT(byte *renderBitmap, int32 codecparam, int32 setupsan
 void InsaneRebel1::procSKIP(int32 subSize, Common::SeekableReadStream &b) {
 }
 
+void InsaneRebel1::handleGameOpcode5EReset(uint32 param1) {
+	// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
+	// This is not a pure control-mode assignment.
+	if (_frameDispatchFlags & 0x40) {
+		debug(7, "RA1 GAME 0x5E: reset suppressed by dispatch flags=0x%02x",
+			_frameDispatchFlags);
+		return;
+	}
+
+	_damageFlags = 0;
+	_prevDamageFlags = 0;
+	_damageCooldown = 0;
+	_deathTimer = 0;
+	_screenFlash = 0;
+	_screenShakeEnabled = false;
+	_gameLatch5D = 0;
+	_gameLatch5F = 0;
+	_driftParam = 0;
+	_rollAccum = 0;
+	_liftSmooth = 0;
+	_posAccumX = 0;
+	_posAccumY = 0;
+	memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
+	memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
+	memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
+	memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
+	_inputAxisDeltaX = 0;
+	_avgInputX = 0;
+	_avgInputY = 0;
+	_mouseOffsetX = 0;
+	_mouseOffsetY = 0;
+	_mouseBiasX = 0;
+	_mouseBiasY = 0;
+	_mousePrevBiasX = 0;
+	_mousePrevBiasY = 0;
+	_mouseBiasLatch = false;
+	_mouseRecentering = false;
+	_mouseVirtualRawX = kRA1DosMouseCenterX;
+	_mouseVirtualRawY = kRA1DosMouseCenterY;
+	_mouseVirtualPrevLogicalX = kRA1CenterX;
+	_mouseVirtualPrevLogicalY = kRA1CenterY;
+	_mouseVirtualValid = false;
+	_level2JoystickFilteredX = 0;
+	_level2JoystickFilteredY = 0;
+
+	_playerFired = false;
+	_fireCooldown = 0;
+	memset(_shotSlots, 0, sizeof(_shotSlots));
+	_shotAlternator = 0;
+	_shotSideToggle = false;
+	_targetProximity = 0;
+	_prevTargetProx = 0;
+	_targetAnimCounter = 0;
+	_targetCount = 0;
+	_prevTargetCount = 0;
+	memset(_targetBoxX, 0, sizeof(_targetBoxX));
+	memset(_targetBoxY, 0, sizeof(_targetBoxY));
+	memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
+	memset(_gostSlots, 0, sizeof(_gostSlots));
+	_gostSlotIdx = 0;
+	_killCount = 0;
+	_lastHitTarget = 0;
+
+	// Field1 == 0 corresponds to baseline recenter behavior in the original.
+	if ((int32)param1 == 0) {
+		_perspectiveX = 0x20;
+		_perspectiveY = 0x17;
+		_shipPosX = kRA1CenterX;
+		_shipPosY = kRA1CenterY;
+	}
+
+	_activeGameOpcode = 0;
+	_frameGameOpcodeMask = 0;
+	_frameDispatchFlags = 0;
+	resetProjectionTable();
+	if ((int32)param1 == 0)
+		syncViewportOffset(true);
+
+	// Original RunLevel8Flow initializes its separate g_level8HitboxBuffer
+	// after the first L8PLAY runtime reset. We fold that mask into the
+	// secondary half of _frameObjectState; route resumes preserve it.
+	if (_currentLevel == 7)
+		memset(_frameObjectState + 150, 0xFF, 150);
+
+	debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
+}
+
+void InsaneRebel1::handleGameOpcode5DLinkLatch(uint32 param1) {
+	if ((uint16)param1 == 0xFFFF) {
+		_gameLatch5D = 0xFFFF;
+	} else if (param1 > 0) {
+		const int bitIndex = (int)param1 - 1;
+		const int byteIndex = bitIndex >> 3;
+		if (byteIndex >= 0 && byteIndex < 0x96 &&
+			(_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+			_gameLatch5D = (uint16)param1;
+		}
+	}
+
+	debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
+}
+
+void InsaneRebel1::handleGameOpcode5FRandomHitLatch(uint32 param1) {
+	if (param1 > 0) {
+		const int bitIndex = (int)param1 - 1;
+		const int byteIndex = bitIndex >> 3;
+		if (byteIndex >= 0 && byteIndex < 0x96 &&
+			(_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
+			_gameLatch5F = (uint16)param1;
+		}
+	}
+
+	debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
+}
+
+void InsaneRebel1::handleGameOpcode07ShipFlight(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+	_activeGameOpcode = 0x07;
+	_frameGameOpcodeMask |= (1u << 0x07);
+	// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
+	// f1 is the original's _DAT_7740 (game frame counter)
+	// f3 is the drift/wind parameter combined with tuning table
+	_gameCounter = param1;
+	if (subSize >= 20) {
+		b.readUint32BE(); // f2 (max frames, unused in physics)
+		_driftParam = (int16)(int32)b.readUint32BE();
+		b.readUint32BE(); // f4 (unused in original assembly)
+		debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
+	}
+}
+
+void InsaneRebel1::handleGameOpcode0DCorridor(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+	// Corridor boundaries: per-frame flight corridor
+	// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
+	// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
+	if (subSize < 20)
+		return;
+
+	int16 corridorLeft = (int16)param1;
+	int16 corridorTop = (int16)b.readUint32BE();
+	int16 corridorWidth = (int16)b.readUint32BE();
+	int16 corridorHeight = (int16)b.readUint32BE();
+
+	int16 centerX = corridorLeft + corridorWidth / 2;
+	int16 centerY = corridorTop + corridorHeight / 2;
+	// DOS FUN_1C54D calls FUN_2248C here, which adds the current
+	// camera offset to the scripted rectangle center before testing it
+	// against the source-buffer ship center.
+	unprojectGameplayPoint(centerX, centerY);
+
+	_corridorLeftX = centerX - corridorWidth / 2;
+	_corridorTopY = centerY - corridorHeight / 2;
+	_corridorRightX = _corridorLeftX + corridorWidth;
+	_corridorBottomY = _corridorTopY + corridorHeight;
+
+	// Apply 0x0D immediately so it sees the same pre-physics ship state as
+	// the other per-frame GAME latches. Deferring this until after 0x07/0x09
+	// movement made right-wall hits fire too early when steering into a wall.
+	int16 collisionShipX = _shipPosX;
+	int16 collisionShipY = _shipPosY;
+	getCollisionShipCenter(collisionShipX, collisionShipY);
+
+	const bool suppressDirectionalDamage = (_frameDispatchFlags & 0x10) != 0;
+	const byte oldDirectionalFlags = _damageFlags & 0x0F;
+	if (_health >= 0) {
+		if (collisionShipX < _corridorLeftX) {
+			_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
+			if (!suppressDirectionalDamage) {
+				if (_rollAccum < 0x100)
+					_rollAccum = 0x100;
+				_damageFlags |= 0x04;
+			}
+		}
+		if (collisionShipX > _corridorRightX) {
+			_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
+			if (!suppressDirectionalDamage) {
+				if (_rollAccum > -0x100)
+					_rollAccum = -0x100;
+				_damageFlags |= 0x02;
+			}
+		}
+		if (collisionShipY < _corridorTopY) {
+			_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
+			if (!suppressDirectionalDamage)
+				_damageFlags |= 0x01;
+		}
+		if (collisionShipY > _corridorBottomY) {
+			_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
+			if (!suppressDirectionalDamage)
+				_damageFlags |= 0x08;
+		}
+	}
+	if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
+		debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
+			collisionShipX, collisionShipY,
+			_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
+			_damageFlags, suppressDirectionalDamage ? 1 : 0);
+	}
+
+	debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
+		corridorLeft, corridorTop, corridorWidth, corridorHeight,
+		_perspectiveX, _perspectiveY,
+		_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
+}
+
+void InsaneRebel1::handleGameOpcode0EZone(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+	// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
+	// Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
+	if (subSize < 20)
+		return;
+
+	int16 zoneLeft = (int16)param1;
+	int16 zoneTop = (int16)b.readUint32BE();
+	int16 zoneWidth = (int16)b.readUint32BE();
+	int16 zoneHeight = (int16)b.readUint32BE();
+	const int16 rawZoneLeft = zoneLeft;
+	const int16 rawZoneTop = zoneTop;
+
+	int16 centerX = zoneLeft + zoneWidth / 2;
+	int16 centerY = zoneTop + zoneHeight / 2;
+	// Same transform as opcode 0x0D/FUN_1C54D.
+	unprojectGameplayPoint(centerX, centerY);
+
+	zoneLeft = centerX - zoneWidth / 2;
+	zoneTop = centerY - zoneHeight / 2;
+	int16 zoneRight = zoneLeft + zoneWidth;
+	int16 zoneBottom = zoneTop + zoneHeight;
+	int16 collisionShipX = _shipPosX;
+	int16 collisionShipY = _shipPosY;
+	getCollisionShipCenter(collisionShipX, collisionShipY);
+
+	if (_health >= 0 &&
+		collisionShipX > zoneLeft && collisionShipX < zoneRight &&
+		collisionShipY > zoneTop && collisionShipY < zoneBottom) {
+		_damageFlags |= 0x10;
+		debug(1, "RA1 0x0E hit: ship=(%d,%d) zone=[%d,%d]-[%d,%d] raw=[%d,%d]+(%d,%d) cam=(%d,%d) flags=0x%02x",
+			collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+			rawZoneLeft, rawZoneTop, zoneWidth, zoneHeight,
+			_perspectiveX, _perspectiveY, _damageFlags);
+	}
+	debug(7, "RA1 GAME 0x0E: ship=(%d,%d) zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
+		collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
+		_perspectiveX, _perspectiveY, _damageFlags);
+}
+
+void InsaneRebel1::handleGameOpcode0BFirstPerson(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+	_activeGameOpcode = 0x0B;
+	_frameGameOpcodeMask |= (1u << 0x0B);
+	// GAME 0x0B per-frame handler (FUN_1CDA7).
+	// field1 = frame counter, field2 = max frames
+	_gameCounter = param1;
+	if (subSize >= 20) {
+		uint32 maxFrames = b.readUint32BE(); // field2 (max frames)
+		b.readUint32BE(); // field3
+		b.readUint32BE(); // field4
+
+		// RA1 scripts drive progression with GAME counters. Finish 0x0B-driven
+		// interactive videos once the script counter reaches the terminal frame.
+		if (_interactiveVideoActive && maxFrames > 0 &&
+			_gameCounter >= (int32)maxFrames - 1) {
+			_vm->_smushVideoShouldFinish = true;
+			debug(1, "RA1: finishing 0x0B interactive video at counter=%d/%u", _gameCounter, maxFrames);
+		}
+	}
+	debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
+	if (!_gameOp0BPhysicsUpdatedThisFrame) {
+		updateGameOp0BPhysics();
+		_gameOp0BPhysicsUpdatedThisFrame = true;
+		syncViewportOffset(true);
+	}
+}
+
+void InsaneRebel1::handleGameOpcode5ATarget(int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+	// Target detection — HandleGameOp5A (0x1C0EF). AABB from video stream.
+	// Original checks event mask: if target already killed, skip to GOST update.
+	if (subSize < 24)
+		return;
+
+	int16 targetIdx = (int16)param1;
+	int16 left = (int16)b.readUint32BE();
+	int16 top = (int16)b.readUint32BE();
+	int16 w = (int16)b.readUint32BE();
+	int16 h = (int16)b.readUint32BE();
+	int16 right = left + w;
+	int16 bottom = top + h;
+
+	if (targetIdx >= 0) {
+		const int byteIdx = targetIdx >> 3;
+		if (byteIdx >= 0 && byteIdx < 0x96 && byteIdx < kFrameObjectStateBytes) {
+			const byte bit = (byte)(0x80 >> (targetIdx & 7));
+			const int altIdx = byteIdx + 0x96;
+			const bool primarySet = (_frameObjectState[byteIdx] & bit) != 0;
+			const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
+				((_frameObjectState[altIdx] & bit) != 0);
+
+			if (!primarySet || secondarySet) {
+				checkTargetHit(targetIdx, left, top, right, bottom);
+			} else {
+				updateGostSlotPosition(targetIdx, left, top, right, bottom);
+			}
+		} else {
+			checkTargetHit(targetIdx, left, top, right, bottom);
+		}
+	}
+	debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
+		targetIdx, left, top, right, bottom, _targetProximity);
+}
+
+void InsaneRebel1::handleGameCounterOpcode(uint32 opcode, int32 subSize, Common::SeekableReadStream &b, uint32 param1) {
+	_activeGameOpcode = (uint16)opcode;
+	_frameGameOpcodeMask |= (1u << opcode);
+	_gameCounter = param1;
+	if (subSize >= 20) {
+		uint32 param2 = b.readUint32BE();
+		uint32 param3 = b.readUint32BE();
+		uint32 param4 = b.readUint32BE();
+		if (opcode == 0x09 && _currentLevel == 4) {
+			debug(1, "RA1 GAME 0x09: counter=%d params=(%d,%d,%d) opcodeMask=0x%08x",
+				_gameCounter, param2, param3, param4, _frameGameOpcodeMask);
+		} else {
+			debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
+				opcode, _gameCounter, param2, param3, param4);
+		}
+	}
+}
+
 // handleGameChunk — FUN_1BE1B (0x1BE1B). Central GAME opcode dispatcher.
 // Reads 7x32-bit BE integers from GAME chunk, routes to per-opcode handlers.
 void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b) {
@@ -1973,305 +2298,35 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 
 	switch (opcode) {
 	case 0x5E:
-		// RA1 dispatcher inline reset/init path (FUN_1BE1B case 0x5E).
-		// This is not a pure control-mode assignment.
-		if (_frameDispatchFlags & 0x40) {
-			debug(7, "RA1 GAME 0x5E: reset suppressed by dispatch flags=0x%02x",
-				_frameDispatchFlags);
-			break;
-		}
-		_damageFlags = 0;
-		_prevDamageFlags = 0;
-		_damageCooldown = 0;
-		_deathTimer = 0;
-		_screenFlash = 0;
-		_screenShakeEnabled = false;
-		_gameLatch5D = 0;
-		_gameLatch5F = 0;
-		_driftParam = 0;
-		_rollAccum = 0;
-		_liftSmooth = 0;
-		_posAccumX = 0;
-		_posAccumY = 0;
-		memset(_inputHistoryX, 0, sizeof(_inputHistoryX));
-		memset(_inputHistoryY, 0, sizeof(_inputHistoryY));
-		memset(_viewHistoryX, 0, sizeof(_viewHistoryX));
-		memset(_viewHistoryY, 0, sizeof(_viewHistoryY));
-		_inputAxisDeltaX = 0;
-		_avgInputX = 0;
-		_avgInputY = 0;
-		_mouseOffsetX = 0;
-		_mouseOffsetY = 0;
-		_mouseBiasX = 0;
-		_mouseBiasY = 0;
-		_mousePrevBiasX = 0;
-		_mousePrevBiasY = 0;
-		_mouseBiasLatch = false;
-		_mouseRecentering = false;
-		_mouseVirtualRawX = kRA1DosMouseCenterX;
-		_mouseVirtualRawY = kRA1DosMouseCenterY;
-		_mouseVirtualPrevLogicalX = kRA1CenterX;
-		_mouseVirtualPrevLogicalY = kRA1CenterY;
-		_mouseVirtualValid = false;
-		_level2JoystickFilteredX = 0;
-		_level2JoystickFilteredY = 0;
-
-		// Shooting/targeting reset
-		_playerFired = false;
-		_fireCooldown = 0;
-		memset(_shotSlots, 0, sizeof(_shotSlots));
-		_shotAlternator = 0;
-		_shotSideToggle = false;
-		_targetProximity = 0;
-		_prevTargetProx = 0;
-		_targetAnimCounter = 0;
-		_targetCount = 0;
-		_prevTargetCount = 0;
-		memset(_targetBoxX, 0, sizeof(_targetBoxX));
-		memset(_targetBoxY, 0, sizeof(_targetBoxY));
-		memset(_targetBoxVariant, 0, sizeof(_targetBoxVariant));
-		memset(_gostSlots, 0, sizeof(_gostSlots));
-		_gostSlotIdx = 0;
-		_killCount = 0;
-		_lastHitTarget = 0;
-
-		// Field1 == 0 corresponds to baseline recenter behavior in the original.
-		if ((int32)param1 == 0) {
-			_perspectiveX = 0x20;
-			_perspectiveY = 0x17;
-			_shipPosX = kRA1CenterX;
-			_shipPosY = kRA1CenterY;
-		}
-
-		_activeGameOpcode = 0;
-		_frameGameOpcodeMask = 0;
-		_frameDispatchFlags = 0;
-		resetProjectionTable();
-		if ((int32)param1 == 0)
-			syncViewportOffset(true);
-
-		// Original RunLevel8Flow initializes its separate g_level8HitboxBuffer
-		// after the first L8PLAY runtime reset. We fold that mask into the
-		// secondary half of _frameObjectState; route resumes preserve it.
-		if (_currentLevel == 7)
-			memset(_frameObjectState + 150, 0xFF, 150);
-
-		debug(5, "RA1 GAME 0x5E: reset state field1=%d mode=%d", (int32)param1, (int)_flyControlMode);
+		handleGameOpcode5EReset(param1);
 		break;
 
 	case 0x5D:
-		if ((uint16)param1 == 0xFFFF) {
-			_gameLatch5D = 0xFFFF;
-		} else if (param1 > 0) {
-			const int bitIndex = (int)param1 - 1;
-			const int byteIndex = bitIndex >> 3;
-			if (byteIndex >= 0 && byteIndex < 0x96 &&
-				(_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
-				_gameLatch5D = (uint16)param1;
-			}
-		}
-		debug(5, "RA1 GAME 0x5D (link/event latch) param=%u", _gameLatch5D);
+		handleGameOpcode5DLinkLatch(param1);
 		break;
 
 	case 0x5F:
-		if (param1 > 0) {
-			const int bitIndex = (int)param1 - 1;
-			const int byteIndex = bitIndex >> 3;
-			if (byteIndex >= 0 && byteIndex < 0x96 &&
-				(_frameObjectState[byteIndex] & (byte)(0x80 >> (bitIndex & 7))) == 0) {
-				_gameLatch5F = (uint16)param1;
-			}
-		}
-		debug(5, "RA1 GAME 0x5F (random-hit latch) param=%u", _gameLatch5F);
+		handleGameOpcode5FRandomHitLatch(param1);
 		break;
 
 	case 0x07:
-		_activeGameOpcode = 0x07;
-		_frameGameOpcodeMask |= (1u << 0x07);
-		// Per-frame corridor data: f1=frame counter, f2=max frames, f3=drift bias, f4=unused
-		// f1 is the original's _DAT_7740 (game frame counter)
-		// f3 is the drift/wind parameter combined with tuning table
-		_gameCounter = param1;
-		if (subSize >= 20) {
-			b.readUint32BE(); // f2 (max frames, unused in physics)
-			_driftParam = (int16)(int32)b.readUint32BE();
-			b.readUint32BE(); // f4 (unused in original assembly)
-			debug(7, "RA1 GAME 0x07: counter=%d driftParam=%d", _gameCounter, _driftParam);
-		}
+		handleGameOpcode07ShipFlight(subSize, b, param1);
 		break;
 
 	case 0x0D:
-		// Corridor boundaries: per-frame flight corridor
-		// Original params: left, top, WIDTH, HEIGHT (not right/bottom!)
-		// FUN_1C54D computes center = (left+width/2, top+height/2), transforms, then checks edges.
-		if (subSize >= 20) {
-			int16 corridorLeft = (int16)param1;
-			int16 corridorTop = (int16)b.readUint32BE();
-			int16 corridorWidth = (int16)b.readUint32BE();
-			int16 corridorHeight = (int16)b.readUint32BE();
-
-			int16 centerX = corridorLeft + corridorWidth / 2;
-			int16 centerY = corridorTop + corridorHeight / 2;
-			// DOS FUN_1C54D calls FUN_2248C here, which adds the current
-			// camera offset to the scripted rectangle center before testing it
-			// against the source-buffer ship center.
-			unprojectGameplayPoint(centerX, centerY);
-
-			_corridorLeftX = centerX - corridorWidth / 2;
-			_corridorTopY = centerY - corridorHeight / 2;
-			_corridorRightX = _corridorLeftX + corridorWidth;
-			_corridorBottomY = _corridorTopY + corridorHeight;
-
-			// Apply 0x0D immediately so it sees the same pre-physics ship state as
-			// the other per-frame GAME latches. Deferring this until after 0x07/0x09
-			// movement made right-wall hits fire too early when steering into a wall.
-			int16 collisionShipX = _shipPosX;
-			int16 collisionShipY = _shipPosY;
-			getCollisionShipCenter(collisionShipX, collisionShipY);
-
-			const bool suppressDirectionalDamage = (_frameDispatchFlags & 0x10) != 0;
-			const byte oldDirectionalFlags = _damageFlags & 0x0F;
-			if (_health >= 0) {
-				if (collisionShipX < _corridorLeftX) {
-					_posAccumX = (int32)(_corridorLeftX - kRA1CenterX) * 0x100;
-					if (!suppressDirectionalDamage) {
-						if (_rollAccum < 0x100)
-							_rollAccum = 0x100;
-						_damageFlags |= 0x04;
-					}
-				}
-				if (collisionShipX > _corridorRightX) {
-					_posAccumX = (int32)(_corridorRightX - kRA1CenterX) * 0x100;
-					if (!suppressDirectionalDamage) {
-						if (_rollAccum > -0x100)
-							_rollAccum = -0x100;
-						_damageFlags |= 0x02;
-					}
-				}
-				if (collisionShipY < _corridorTopY) {
-					_posAccumY = (int32)(_corridorTopY - kRA1CenterY) * 0x100 + 0x100;
-					if (!suppressDirectionalDamage)
-						_damageFlags |= 0x01;
-				}
-				if (collisionShipY > _corridorBottomY) {
-					_posAccumY = (int32)(_corridorBottomY - kRA1CenterY) * 0x100 - 0x100;
-					if (!suppressDirectionalDamage)
-						_damageFlags |= 0x08;
-				}
-			}
-			if ((_damageFlags & 0x0F) != oldDirectionalFlags) {
-				debug(1, "RA1 0x0D hit: ship=(%d,%d) corridor=[%d,%d]-[%d,%d] flags=0x%02x zoneSuppressed=%d",
-					collisionShipX, collisionShipY,
-					_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY,
-					_damageFlags, suppressDirectionalDamage ? 1 : 0);
-			}
-
-			debug(5, "RA1 GAME 0x0D: raw=[%d,%d]+(%d,%d) cam=(%d,%d) transformed=[%d,%d]-[%d,%d]",
-				corridorLeft, corridorTop, corridorWidth, corridorHeight,
-				_perspectiveX, _perspectiveY,
-				_corridorLeftX, _corridorTopY, _corridorRightX, _corridorBottomY);
-		}
+		handleGameOpcode0DCorridor(subSize, b, param1);
 		break;
 
 	case 0x0E:
-		// Secondary collision zone (FUN_1C6E9): AABB test, sets damageFlags bit 4 (0x10)
-		// Original params: left, top, WIDTH, HEIGHT (same as 0x0D)
-		if (subSize >= 20) {
-			int16 zoneLeft = (int16)param1;
-			int16 zoneTop = (int16)b.readUint32BE();
-			int16 zoneWidth = (int16)b.readUint32BE();
-			int16 zoneHeight = (int16)b.readUint32BE();
-			const int16 rawZoneLeft = zoneLeft;
-			const int16 rawZoneTop = zoneTop;
-
-			int16 centerX = zoneLeft + zoneWidth / 2;
-			int16 centerY = zoneTop + zoneHeight / 2;
-			// Same transform as opcode 0x0D/FUN_1C54D.
-			unprojectGameplayPoint(centerX, centerY);
-
-			zoneLeft = centerX - zoneWidth / 2;
-			zoneTop = centerY - zoneHeight / 2;
-			int16 zoneRight = zoneLeft + zoneWidth;
-			int16 zoneBottom = zoneTop + zoneHeight;
-			int16 collisionShipX = _shipPosX;
-			int16 collisionShipY = _shipPosY;
-			getCollisionShipCenter(collisionShipX, collisionShipY);
-
-			if (_health >= 0 &&
-				collisionShipX > zoneLeft && collisionShipX < zoneRight &&
-				collisionShipY > zoneTop && collisionShipY < zoneBottom) {
-				_damageFlags |= 0x10;
-				debug(1, "RA1 0x0E hit: ship=(%d,%d) zone=[%d,%d]-[%d,%d] raw=[%d,%d]+(%d,%d) cam=(%d,%d) flags=0x%02x",
-					collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
-					rawZoneLeft, rawZoneTop, zoneWidth, zoneHeight,
-					_perspectiveX, _perspectiveY, _damageFlags);
-			}
-			debug(7, "RA1 GAME 0x0E: ship=(%d,%d) zone=[%d,%d]-[%d,%d] cam=(%d,%d) flags=0x%02x",
-				collisionShipX, collisionShipY, zoneLeft, zoneTop, zoneRight, zoneBottom,
-				_perspectiveX, _perspectiveY, _damageFlags);
-		}
+		handleGameOpcode0EZone(subSize, b, param1);
 		break;
 
 	case 0x0B:
-		_activeGameOpcode = 0x0B;
-		_frameGameOpcodeMask |= (1u << 0x0B);
-		// GAME 0x0B per-frame handler (FUN_1CDA7).
-		// field1 = frame counter, field2 = max frames
-		_gameCounter = param1;
-		if (subSize >= 20) {
-			uint32 maxFrames = b.readUint32BE(); // field2 (max frames)
-			b.readUint32BE(); // field3
-			b.readUint32BE(); // field4
-
-			// RA1 scripts drive progression with GAME counters. Finish 0x0B-driven
-			// interactive videos once the script counter reaches the terminal frame.
-			if (_interactiveVideoActive && maxFrames > 0 &&
-				_gameCounter >= (int32)maxFrames - 1) {
-				_vm->_smushVideoShouldFinish = true;
-				debug(1, "RA1: finishing 0x0B interactive video at counter=%d/%u", _gameCounter, maxFrames);
-			}
-		}
-		debug(7, "RA1 GAME 0x0B: counter=%d", _gameCounter);
-		if (!_gameOp0BPhysicsUpdatedThisFrame) {
-			updateGameOp0BPhysics();
-			_gameOp0BPhysicsUpdatedThisFrame = true;
-			syncViewportOffset(true);
-		}
+		handleGameOpcode0BFirstPerson(subSize, b, param1);
 		break;
 
 	case 0x5A:
-		// Target detection — HandleGameOp5A (0x1C0EF). AABB from video stream.
-		// Original checks event mask: if target already killed, skip to GOST update.
-		if (subSize >= 24) {
-			int16 targetIdx = (int16)param1;
-			int16 left = (int16)b.readUint32BE();
-			int16 top = (int16)b.readUint32BE();
-			int16 w = (int16)b.readUint32BE();
-			int16 h = (int16)b.readUint32BE();
-			int16 right = left + w;
-			int16 bottom = top + h;
-
-			if (targetIdx >= 0) {
-				const int byteIdx = targetIdx >> 3;
-				if (byteIdx >= 0 && byteIdx < 0x96 && byteIdx < kFrameObjectStateBytes) {
-					const byte bit = (byte)(0x80 >> (targetIdx & 7));
-					const int altIdx = byteIdx + 0x96;
-					const bool primarySet = (_frameObjectState[byteIdx] & bit) != 0;
-					const bool secondarySet = (altIdx < kFrameObjectStateBytes) &&
-						((_frameObjectState[altIdx] & bit) != 0);
-
-					if (!primarySet || secondarySet) {
-						checkTargetHit(targetIdx, left, top, right, bottom);
-					} else {
-						updateGostSlotPosition(targetIdx, left, top, right, bottom);
-					}
-				} else {
-					checkTargetHit(targetIdx, left, top, right, bottom);
-				}
-			}
-			debug(5, "RA1 GAME 0x5A: target=%d rect=[%d,%d]-[%d,%d] prox=%d",
-				targetIdx, left, top, right, bottom, _targetProximity);
-		}
+		handleGameOpcode5ATarget(subSize, b, param1);
 		break;
 
 	case 0x08:
@@ -2279,21 +2334,7 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b)
 	case 0x0A:
 	case 0x19:
 	case 0x1A:
-		_activeGameOpcode = (uint16)opcode;
-		_frameGameOpcodeMask |= (1u << opcode);
-		_gameCounter = param1;
-		if (subSize >= 20) {
-			uint32 param2 = b.readUint32BE();
-			uint32 param3 = b.readUint32BE();
-			uint32 param4 = b.readUint32BE();
-			if (opcode == 0x09 && _currentLevel == 4) {
-				debug(1, "RA1 GAME 0x09: counter=%d params=(%d,%d,%d) opcodeMask=0x%08x",
-					_gameCounter, param2, param3, param4, _frameGameOpcodeMask);
-			} else {
-				debug(5, "RA1 GAME 0x%02x: counter=%d params=(%d,%d,%d)",
-					opcode, _gameCounter, param2, param3, param4);
-			}
-		}
+		handleGameCounterOpcode(opcode, subSize, b, param1);
 		break;
 
 	default:
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index d358202f8ef..155dc39da4f 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -186,6 +186,15 @@ private:
 	void playInteractiveVideoFile(const char *filename, int32 videoOffset, int32 videoStartFrame);
 	bool loadRA1Nut(const char *filename, RA1SpriteBank &bank);
 	void loadLevelSprites(int level);
+	void handleGameOpcode5EReset(uint32 param1);
+	void handleGameOpcode5DLinkLatch(uint32 param1);
+	void handleGameOpcode5FRandomHitLatch(uint32 param1);
+	void handleGameOpcode07ShipFlight(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+	void handleGameOpcode0DCorridor(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+	void handleGameOpcode0EZone(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+	void handleGameOpcode0BFirstPerson(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+	void handleGameOpcode5ATarget(int32 subSize, Common::SeekableReadStream &b, uint32 param1);
+	void handleGameCounterOpcode(uint32 opcode, int32 subSize, Common::SeekableReadStream &b, uint32 param1);
 	void updateShipPhysics();
 	void updateTurretPhysics();
 	void updateTurretShipDirection(int16 offsetY);


Commit: 54fe61ff8d06de23e232c15b9784e0cffeef2cf3
    https://github.com/scummvm/scummvm/commit/54fe61ff8d06de23e232c15b9784e0cffeef2cf3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:14+02:00

Commit Message:
SCUMM: RA1: Render number of pilots left correctly

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


diff --git a/engines/scumm/insane/rebel1/render.cpp b/engines/scumm/insane/rebel1/render.cpp
index a390fc6006f..c5397079d76 100644
--- a/engines/scumm/insane/rebel1/render.cpp
+++ b/engines/scumm/insane/rebel1/render.cpp
@@ -1692,28 +1692,50 @@ void InsaneRebel1::renderHUD(byte *dst, int pitch, int width, int height) {
 		hudOriginY = ra1Player()->_ra1ViewportOffsetY;
 	}
 
-	int hudX = hudOriginX + bar.xoffs;
-	int hudY = hudOriginY + bar.yoffs;
+	const int hudX = hudOriginX + bar.xoffs;
+	const int hudY = hudOriginY + bar.yoffs;
+	// FUN_1BBCB draws DISPLAY.NUT at x=5 with a 4..315 clip rect. The HUD
+	// masks/text below use the unshifted screen-space coordinate origin.
+	const int hudPlateX = hudX + 5;
+	const int hudPlateY = hudY;
 
 	// DOS RA1 draws the HUD plate through DrawFobjGlyph(..., flags=0x181),
 	// which selects the opaque blit path. Keep zero-valued pixels black instead
 	// of treating them as transparent.
 	if (bar.data && bar.width > 0 && bar.height > 0) {
-		int drawX = hudX, drawY = hudY, drawW = bar.width, drawH = bar.height;
+		int drawX = hudPlateX, drawY = hudPlateY, drawW = bar.width, drawH = bar.height;
 		int srcOffX = 0, srcOffY = 0;
+		const int clipLeft = hudX + 4;
+		const int clipTop = hudY;
+		const int clipRight = hudX + 315;
+		const int clipBottom = hudY + 19;
+		if (drawX < clipLeft) {
+			srcOffX = clipLeft - drawX;
+			drawW -= srcOffX;
+			drawX = clipLeft;
+		}
+		if (drawY < clipTop) {
+			srcOffY = clipTop - drawY;
+			drawH -= srcOffY;
+			drawY = clipTop;
+		}
+		if (drawX + drawW - 1 > clipRight) drawW = clipRight - drawX + 1;
+		if (drawY + drawH - 1 > clipBottom) drawH = clipBottom - drawY + 1;
 		if (drawX < 0) { srcOffX = -drawX; drawW += drawX; drawX = 0; }
 		if (drawY < 0) { srcOffY = -drawY; drawH += drawY; drawY = 0; }
 		if (drawX + drawW > width) drawW = width - drawX;
 		if (drawY + drawH > height) drawH = height - drawY;
 
-		for (int iy = 0; iy < drawH; iy++) {
-			const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
-			byte *d = dst + (drawY + iy) * pitch + drawX;
-			memcpy(d, s, drawW);
+		if (drawW > 0 && drawH > 0) {
+			for (int iy = 0; iy < drawH; iy++) {
+				const byte *s = bar.data + (srcOffY + iy) * bar.width + srcOffX;
+				byte *d = dst + (drawY + iy) * pitch + drawX;
+				memcpy(d, s, drawW);
+			}
 		}
 
 		debug(5, "RA1 HUD: drawn at (%d,%d) size=%dx%d",
-			hudX, hudY, bar.width, bar.height);
+			hudPlateX, hudPlateY, bar.width, bar.height);
 	}
 
 	// Draw health bar from FUN_1BBCB (0x1BBCB) + FUN_21D66 (0x21D66):


Commit: dee67a5319a51c39ad0e9141c260057047cbdd7b
    https://github.com/scummvm/scummvm/commit/dee67a5319a51c39ad0e9141c260057047cbdd7b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:14+02:00

Commit Message:
SCUMM: RA1: Add basic save/load support

Changed paths:
  A engines/scumm/insane/rebel1/saveload.cpp
    engines/scumm/insane/rebel1/menu.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel1/runlevels.cpp
    engines/scumm/module.mk
    engines/scumm/saveload.cpp


diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index c7628ea5fea..43f7c341a06 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -922,20 +922,20 @@ void InsaneRebel1::playMenuBackground() {
 }
 
 bool InsaneRebel1::runTextEntryMenuLoop() {
-	while (!_vm->shouldQuit() && !_textEntryDone && !_textEntryCanceled)
+	while (!shouldAbortGameFlow() && !_textEntryDone && !_textEntryCanceled)
 		playMenuBackground();
 
-	return !_vm->shouldQuit() && !_textEntryCanceled;
+	return !shouldAbortGameFlow() && !_textEntryCanceled;
 }
 
 int InsaneRebel1::runMainMenu() {
 	debug(1, "InsaneRebel1: Main menu");
 
 	_menuSelection = 0;
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		playMenuBackground();
 
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return kRA1MainMenuItemCount;
 
 		if (_menuConfirmed)
@@ -1004,10 +1004,10 @@ void InsaneRebel1::runOptionsMenu() {
 	_optionsSel = 0;
 	_optionsActive = true;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		playMenuBackground();
 
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			break;
 
 		if (_menuConfirmed) {
@@ -1053,10 +1053,10 @@ int InsaneRebel1::runLevelSelectMenu() {
 	_levelSelectSel = CLIP(_startLevel - 1, 0, kRA1NumLevels - 1);
 	_levelSelectActive = true;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		playMenuBackground();
 
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			break;
 
 		if (_menuConfirmed) {
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index c6686439014..875a305541e 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -274,6 +274,9 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_activeInputSource = kInputSourceMouse;
 
 	_currentLevel = 0;
+	_resumeLevel = 1;
+	_activeSaveSlot = -1;
+	_loadRequested = false;
 	_flyControlMode = 0;
 	_turretEmitterLeftX = 0;
 	_turretEmitterLeftY = 0;
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index 155dc39da4f..22f93bd3e49 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -126,6 +126,9 @@ public:
 
 	// Game flow (matching original at 0x15597)
 	void runGame();
+	Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave = false);
+	Common::Error loadGameState(int slot, bool startupLoad = false);
+	bool shouldAbortGameFlow() const { return _vm->shouldQuit() || _loadRequested; }
 
 private:
 	// Intro sequence: O1LOGO → O1OPEN (0x155ef-0x158f8)
@@ -168,6 +171,19 @@ private:
 		const char *bonusLabel2 = nullptr, const char *detailText2 = nullptr, int bonusValue2 = 0,
 		int passwordIndex = 0);
 	bool playDeathOrRetry(const char *retryVideo, const char *gameOverVideo);
+	void autosaveProgress();
+	int getAutosaveTargetSlot() const;
+	int getCurrentSaveLevel() const;
+	struct SaveState {
+		int resumeLevel;
+		int lives;
+		int score;
+		int prevScore;
+		int difficulty;
+		int maxChapterUnlocked;
+	};
+	Common::Error writeSaveState(int slot, const Common::String &desc, const SaveState &state) const;
+	bool readSaveState(int slot, SaveState &state, Common::String *desc = nullptr) const;
 	void resetLevelDamageState();
 	void resetLevelFrameState();
 	void resetLevelTargetingState(bool resetKillCount = true);
@@ -365,6 +381,9 @@ private:
 
 	// Current level index (0-based: 0=LVL1, 1=LVL2, etc.)
 	int _currentLevel;
+	int _resumeLevel;             // 1-based level used when saving/restoring RA1 progress
+	int _activeSaveSlot;          // Last manually saved or loaded slot; -1 means use autosave slot
+	bool _loadRequested;          // Runtime load requested while unwinding the current video flow
 
 	// Intro title overlay (RunTwoLineTextSplash from original)
 	bool _introTextActive;
@@ -461,6 +480,7 @@ private:
 	static const int16 kMaxHealth = 98;
 	static const int16 kDeathTimerInit = 30;
 	static const int16 kDamageCooldownInit = 10;
+	enum { kNumLevels = 15 };
 
 	// Streamed SMUSH audio
 	RebelAudio _audio;
diff --git a/engines/scumm/insane/rebel1/runlevels.cpp b/engines/scumm/insane/rebel1/runlevels.cpp
index 6c1b16854ce..8dffb1c5a27 100644
--- a/engines/scumm/insane/rebel1/runlevels.cpp
+++ b/engines/scumm/insane/rebel1/runlevels.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/system.h"
 
 #include "graphics/cursorman.h"
@@ -147,6 +148,9 @@ static int calculateThresholdBonus(int kills, int perfectThreshold, int perKillT
 // startFrame > 0: fast-forward (decode without display/audio) to that frame.
 void InsaneRebel1::playCinematic(const char *filename, int32 startFrame) {
 	debug(1, "InsaneRebel1::playCinematic('%s', startFrame=%d)", filename, startFrame);
+	if (shouldAbortGameFlow())
+		return;
+
 	SmushPlayer *splayer = _vm->_splayer;
 	_player = splayer;
 	restoreScreenFlashPalette();
@@ -184,7 +188,7 @@ void InsaneRebel1::playChapterCompleteCinematic(const char *filename, int16 unlo
 bool InsaneRebel1::playDeathOrRetry(const char *retryVideo, const char *gameOverVideo) {
 	if (_lives > 0) {
 		playCinematic(retryVideo);
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 		_lives--;
 		return true;
@@ -263,13 +267,13 @@ void InsaneRebel1::playLevelTransitionCutscene(int level) {
 		// FALCON/BIGGS/WEDGE passcode group. This is separate from RunLevel4Flow
 		// (0x6ee4), which starts with LVL4/L4INTRO.ANM.
 		playCinematic("CUT1/C1BLOCK.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			break;
 		playCinematic("CUT1/C1DARTH1.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			break;
 		playCinematic("CUT1/C1C3PO.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			break;
 		playCinematic("CUT1/C1DARTH2.ANM");
 		break;
@@ -315,7 +319,7 @@ void InsaneRebel1::playIntroSequence() {
 
 	// LucasArts logo (original: PUSH 0x57cc, CALL FUN_1BA32 with flags 0x0420)
 	playCinematic("OPEN/O1LOGO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return;
 	clearVideoBuffer();
 
@@ -351,14 +355,14 @@ bool InsaneRebel1::runLevel1() {
 
 	beginLevelTitleOverlay(0);
 	playCinematic("LVL1/L1HANGAR.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
 	playCinematic("LVL1/L1CU1.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		bool stage2Started = false;
 
 		resetLevelDamageState();
@@ -371,24 +375,24 @@ bool InsaneRebel1::runLevel1() {
 		loadTuningForLevel(0);
 
 		playInteractiveVideo("LVL1/L1PLAY1L.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_rightPathSelected && _health >= 0) {
 			_pathBranchEnabled = false;
 			_flyControlMode = 1;
 			playInteractiveVideo("LVL1/L1PLAY1R.ANM", 0x187);
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 		}
 		_pathBranchEnabled = false;
 
 		if (_health >= 0) {
 			playCinematic("LVL1/L1CU2.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
-			while (!_vm->shouldQuit()) {
+			while (!shouldAbortGameFlow()) {
 				// RunLevel1Flow calls L1PLAY2 with gameplay selector 1. This is
 				// the "1B" tuning row: snap/kill values are enabled and the
 				// lock/fire text overlay is suppressed.
@@ -405,7 +409,7 @@ bool InsaneRebel1::runLevel1() {
 				stage2Started = true;
 
 				playInteractiveVideo("LVL1/L1PLAY2.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 
 				if (_health < 0)
@@ -420,11 +424,11 @@ bool InsaneRebel1::runLevel1() {
 					playChapterCompleteCinematic("LVL1/L1END.ANM", 1, 0x78, 5,
 						"Part I", pathText, pathBonus,
 						"Part II", accuracyText, targetBonus);
-					return !_vm->shouldQuit();
+					return !shouldAbortGameFlow();
 				}
 
 				playCinematic("LVL1/L1RETRY.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 			}
 		}
@@ -433,7 +437,7 @@ bool InsaneRebel1::runLevel1() {
 			playCinematic("LVL1/L1CRASHB.ANM");
 		else
 			playCinematic("LVL1/L1CRASHA.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (!playDeathOrRetry("LVL1/L1NEW.ANM", "LVL1/L1DEATH.ANM"))
@@ -453,10 +457,10 @@ bool InsaneRebel1::runLevel2() {
 
 	beginLevelTitleOverlay(1);
 	playCinematic("LVL2/L2INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		_flyControlMode = 0;
 		resetLevelDamageState();
 		resetLevelFrameState();
@@ -464,7 +468,7 @@ bool InsaneRebel1::runLevel2() {
 		_killCount = 0;
 
 		playInteractiveVideo("LVL2/L2PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -472,7 +476,7 @@ bool InsaneRebel1::runLevel2() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x18, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x17, 0x0C, _tuning.bonus);
 			playChapterCompleteCinematic("LVL2/L2END.ANM", 2, 0x69, 10, " ", accuracyText, bonus);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL2/L2NEW.ANM", "LVL2/L2DEATH.ANM"))
@@ -494,10 +498,10 @@ bool InsaneRebel1::runLevel3() {
 
 	beginLevelTitleOverlay(2);
 	playCinematic("LVL3/L3INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		_flyControlMode = 1;
 		resetLevelDamageState();
 		resetLevelFrameState();
@@ -507,13 +511,13 @@ bool InsaneRebel1::runLevel3() {
 		resetLevelInputHistory();
 
 		playInteractiveVideo("LVL3/L3PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
 			playChapterCompleteCinematic("LVL3/L3END.ANM", 3, 0x69, 5,
 				nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 1);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL3/L3NEW.ANM", "LVL3/L3DEATH.ANM"))
@@ -535,10 +539,10 @@ bool InsaneRebel1::runLevel4() {
 
 	beginLevelTitleOverlay(3);
 	playCinematic("LVL4/L4INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		loadTuningForLevel(4);
 		_flyControlMode = 1;
 		resetLevelDamageState();
@@ -559,7 +563,7 @@ bool InsaneRebel1::runLevel4() {
 		playInteractiveVideo("LVL4/L4PLAY1.ANM");
 		_protectedTargetA = 0;
 		_protectedTargetB = 0;
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -573,7 +577,7 @@ bool InsaneRebel1::runLevel4() {
 			_killCount = 0;
 			_levelGameplayPhase = 2;
 			playInteractiveVideo("LVL4/L4PLAY2.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 		}
 
@@ -583,7 +587,7 @@ bool InsaneRebel1::runLevel4() {
 			playChapterCompleteCinematic(torpedoHit ? "LVL4/L4END1.ANM" : "LVL4/L4END2.ANM",
 				4, 0x69, 5, " ", torpedoHit ? "Torpedo Hit" : "Torpedo Missed",
 				torpedoHit ? _tuning.bonus : 0);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL4/L4NEW.ANM", "LVL4/L4DEATH.ANM"))
@@ -605,17 +609,17 @@ bool InsaneRebel1::runLevel5() {
 
 	beginLevelTitleOverlay(4);
 	playCinematic("LVL5/L5INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		loadRA1Nut("LVL5/L5LASER.NUT", _laserBank);
 		loadTuningForLevel(6);
 		resetLevelAttemptState(1, 1);
 		_level5SuccessFramesRemaining = 0x14;
 
 		playInteractiveVideo("LVL5/L5PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health < 0) {
@@ -629,7 +633,7 @@ bool InsaneRebel1::runLevel5() {
 			if (_lives > 0) {
 				_lives--;
 				playCinematic("LVL5/L5RETRY.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 				continue;
 			}
@@ -639,7 +643,7 @@ bool InsaneRebel1::runLevel5() {
 		}
 
 		playCinematic("LVL5/L5BINTRO.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		loadRA1Nut("LVL5/L5LASER2.NUT", _laserBank);
@@ -651,7 +655,7 @@ bool InsaneRebel1::runLevel5() {
 		_levelGameplayPhase = 2;
 		_level5SuccessFramesRemaining = 0;
 		playInteractiveVideo("LVL5/L5PLAY2.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -665,7 +669,7 @@ bool InsaneRebel1::runLevel5() {
 			if (_killCount >= 0x4C)
 				bonus += (_killCount - 0x4B) * _tuning.bonus;
 			playChapterCompleteCinematic("LVL5/L5END.ANM", 5, 0x69, 5, " ", accuracyText, bonus);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL5/L5NEW.ANM", "LVL5/L5DEATH.ANM"))
@@ -688,10 +692,10 @@ bool InsaneRebel1::runLevel6() {
 
 	beginLevelTitleOverlay(5);
 	playCinematic("LVL6/L6INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		_flyControlMode = 1;
 		resetLevelDamageState();
 		resetLevelFrameState();
@@ -701,7 +705,7 @@ bool InsaneRebel1::runLevel6() {
 		resetLevelInputHistory();
 
 		playInteractiveVideo("LVL6/L6PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -710,7 +714,7 @@ bool InsaneRebel1::runLevel6() {
 			const int bonus = calculateThresholdBonus(_killCount, 0x26, 0x0C, _tuning.bonus);
 			playChapterCompleteCinematic("LVL6/L6END.ANM", 6, 0x4B, 5,
 				" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 4);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL6/L6NEW.ANM", "LVL6/L6DEATH.ANM"))
@@ -742,10 +746,10 @@ bool InsaneRebel1::runLevel7() {
 
 	beginLevelTitleOverlay(6);
 	playCinematic("LVL7/L7INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		resetLevelAttemptState(3, 0);
 		_driftParam = 0x19;
 		resetEnemyShotSlots();
@@ -756,14 +760,14 @@ bool InsaneRebel1::runLevel7() {
 		int32 routeStartFrame = 0;
 		int32 routeSourceFrame = 0;
 		int32 routeVideoStartFrame = 0;
-		while (!_vm->shouldQuit()) {
+		while (!shouldAbortGameFlow()) {
 			_levelRouteIndex = route;
 			_pendingRouteIndex = -1;
 			_pendingRouteStartFrame = routeSourceFrame;
 			_pendingRouteCutoverFrame = -1;
 			_pendingRouteVideoStartFrame = routeVideoStartFrame;
 			playInteractiveVideo(kLevel7Segments[route], routeStartFrame);
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (_health < 0)
@@ -794,7 +798,7 @@ bool InsaneRebel1::runLevel7() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x33, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x32, 0x14, _tuning.bonus);
 			playChapterCompleteCinematic("LVL7/L7END.ANM", 7, 0x69, 5, " ", accuracyText, bonus);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL7/L7NEW.ANM", "LVL7/L7DEATH.ANM"))
@@ -824,10 +828,10 @@ bool InsaneRebel1::runLevel8() {
 
 	beginLevelTitleOverlay(7);
 	playCinematic("LVL8/L8INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		resetLevelAttemptState(3, 0, 17, true);
 
 		// Walker-specific state — RunLevel8Flow (0x18546)
@@ -837,13 +841,13 @@ bool InsaneRebel1::runLevel8() {
 
 		int route = 0;
 		int32 routeStartFrame = 0;
-		while (!_vm->shouldQuit()) {
+		while (!shouldAbortGameFlow()) {
 			_levelRouteIndex = route;
 			_pendingRouteIndex = -1;
 			_pendingRouteStartFrame = routeStartFrame;
 			_pendingRouteCutoverFrame = -1;
 			playInteractiveVideo(kLevel8Routes[route], routeStartFrame);
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (_health < 0)
@@ -870,7 +874,7 @@ bool InsaneRebel1::runLevel8() {
 
 		if (_health >= 0) {
 			playChapterCompleteCinematic("LVL8/L8END.ANM", 8, 0x5F, 5);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL8/L8NEW.ANM", "LVL8/L8DEATH.ANM"))
@@ -898,7 +902,7 @@ bool InsaneRebel1::runLevel9() {
 	const int randPath2 = getOriginalRouteBit();
 	const int randPath3 = getOriginalRouteBit();
 	auto playLevel9PathSelector = [&](const char *filename) {
-		while (!_vm->shouldQuit()) {
+		while (!shouldAbortGameFlow()) {
 			// DOS zeros g_shipOffsetX around the 0x1A-only selector clips.
 			// Keep the selector cursor centered instead of inheriting the last
 			// walking offset from the previous stormtrooper segment.
@@ -912,7 +916,7 @@ bool InsaneRebel1::runLevel9() {
 			_lastHitTarget = 0;
 
 			playInteractiveVideo(filename);
-			if (_vm->shouldQuit() || _health < 0)
+			if (shouldAbortGameFlow() || _health < 0)
 				return -1;
 			if (_killCount > 0)
 				return (_shipPosX < kRA1CenterX) ? 0 : 1;
@@ -929,10 +933,10 @@ bool InsaneRebel1::runLevel9() {
 
 	beginLevelTitleOverlay(8);
 	playCinematic("LVL9/L9INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		loadTuningForLevel(0x0B);
 		resetLevelAttemptState(0, 0, 15);  // On-foot center direction
 		_onFootCharX = 0;
@@ -940,21 +944,21 @@ bool InsaneRebel1::runLevel9() {
 		_onFootAnimCounter = 0;
 		_onFootInitialized = false;
 
-		while (!_vm->shouldQuit()) {
+		while (!shouldAbortGameFlow()) {
 			loadTuningForLevel(0x0B);
 			playInteractiveVideo("LVL9/L9PLAY1.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
 				break;
 
 			playCinematic("LVL9/L9CUT1.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			loadTuningForLevel(0x0C);
 			const int side1 = playLevel9PathSelector("LVL9/L9PLAY2.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
 				break;
@@ -963,42 +967,42 @@ bool InsaneRebel1::runLevel9() {
 
 			_gameplayFlags75fe |= 4;
 			playCinematic(side1 == 0 ? "LVL9/L9PLAY2A.ANM" : "LVL9/L9PLAY2B.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (side1 == randPath1) {
 				playCinematic("LVL9/L9CUT2A.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 				continue;
 			}
 
 			playCinematic("LVL9/L9CUT2B.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			loadTuningForLevel(0x0B);
 			playInteractiveVideo("LVL9/L9PLAY3A.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
 				break;
 
 			if (_killCount < 15) {
 				playInteractiveVideo("LVL9/L9PLAY3B.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 				if (_health < 0)
 					break;
 			}
 
 			playCinematic("LVL9/L9CUT3.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			loadTuningForLevel(0x0C);
 			const int side2 = playLevel9PathSelector("LVL9/L9PLAY4.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
 				break;
@@ -1007,28 +1011,28 @@ bool InsaneRebel1::runLevel9() {
 
 			_gameplayFlags75fe |= 4;
 			playCinematic(side2 == 0 ? "LVL9/L9PLAY4A.ANM" : "LVL9/L9PLAY4B.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (side2 == randPath2) {
 				playCinematic(side2 == 0 ? "LVL9/L9CUT4AX.ANM" : "LVL9/L9CUT4B.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 
 				loadTuningForLevel(0x0B);
 				playInteractiveVideo("LVL9/L9PLAY5.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 				if (_health < 0)
 					break;
 
 				playCinematic("LVL9/L9CUT5.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 
 				loadTuningForLevel(0x0C);
 				const int side3 = playLevel9PathSelector("LVL9/L9PLAY6.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 				if (_health < 0)
 					break;
@@ -1038,29 +1042,29 @@ bool InsaneRebel1::runLevel9() {
 				_gameplayFlags75fe |= 4;
 				if (side3 == randPath3) {
 					playCinematic("LVL9/L9CUT6A.ANM");
-					if (_vm->shouldQuit())
+					if (shouldAbortGameFlow())
 						return false;
 					continue;
 				}
 
 				playCinematic("LVL9/L9CUT6B.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 			} else {
 				playCinematic(side2 == 0 ? "LVL9/L9CUT4A.ANM" : "LVL9/L9CUT4BX.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 			}
 
 			loadTuningForLevel(0x0B);
 			playInteractiveVideo("LVL9/L9PLAY7.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 			if (_health < 0)
 				break;
 
 			playChapterCompleteCinematic("LVL9/L9END.ANM", 9, 0x69, 5);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL9/L9NEW.ANM", "LVL9/L9DEATH.ANM"))
@@ -1082,14 +1086,14 @@ bool InsaneRebel1::runLevel10() {
 
 	beginLevelTitleOverlay(9);
 	playCinematic("LVL10/L10INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		resetLevelAttemptState(1, 0);
 
 		playInteractiveVideo("LVL10/L10PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -1098,7 +1102,7 @@ bool InsaneRebel1::runLevel10() {
 			const int bonus = calculateThresholdBonus(_killCount, 0x3D, 0x32, _tuning.bonus);
 			playChapterCompleteCinematic("LVL10/L10END.ANM", 10, 0x4B, 5,
 				" ", accuracyText, bonus, nullptr, nullptr, 0, _difficulty + 7);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL10/L10NEW.ANM", "LVL10/L10DEATH.ANM"))
@@ -1124,17 +1128,17 @@ bool InsaneRebel1::runLevel11() {
 
 	beginLevelTitleOverlay(10);
 	playCinematic("LVL11/L11INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		resetLevelAttemptState(1, 0);
 		_turretEmitterLeftX = 25;
 		_turretEmitterLeftY = 15;
 
-		while (!_vm->shouldQuit()) {
+		while (!shouldAbortGameFlow()) {
 			playInteractiveVideo("LVL11/L11PLAY.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (_health < 0)
@@ -1146,7 +1150,7 @@ bool InsaneRebel1::runLevel11() {
 
 			// Not enough kills — retry
 			playCinematic("LVL11/L11RETRY.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 		}
 
@@ -1155,7 +1159,7 @@ bool InsaneRebel1::runLevel11() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x60, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x5F, 0x0F, _tuning.bonus);
 			playChapterCompleteCinematic("LVL11/L11END.ANM", 11, 0x69, 5, " ", accuracyText, bonus);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL11/L11NEW.ANM", "LVL11/L11DEATH.ANM"))
@@ -1180,15 +1184,15 @@ bool InsaneRebel1::runLevel12() {
 
 	beginLevelTitleOverlay(11);
 	playCinematic("LVL12/L12INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		_flyControlMode = 1;
 		resetLevelDamageState();
 		_killCount = 0;
 
-		while (!_vm->shouldQuit()) {
+		while (!shouldAbortGameFlow()) {
 			loadTuningForLevel(0x0F);
 			resetLevelFrameState();
 			_damageFlags = 0;
@@ -1199,12 +1203,12 @@ bool InsaneRebel1::runLevel12() {
 			resetLevelInputHistory();
 
 			playInteractiveVideo("LVL12/L12PLAY.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (_levelGameplayPhase == 1) {
 				playCinematic("LVL12/L12RETRY.ANM");
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 				if (_health < 0)
 					break;
@@ -1218,7 +1222,7 @@ bool InsaneRebel1::runLevel12() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x58, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x57, 0x46, _tuning.bonus);
 			playChapterCompleteCinematic("LVL12/L12END.ANM", 12, 0x69, 5, " ", accuracyText, bonus);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL12/L12NEW.ANM", "LVL12/L12DEATH.ANM"))
@@ -1244,15 +1248,15 @@ bool InsaneRebel1::runLevel13() {
 
 	beginLevelTitleOverlay(12);
 	playCinematic("LVL13/L13INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		resetLevelAttemptState(1, 0);
 		resetEnemyShotSlots();
 
 		playInteractiveVideo("LVL13/L13PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -1260,7 +1264,7 @@ bool InsaneRebel1::runLevel13() {
 			formatTargetAccuracy(accuracyText, sizeof(accuracyText), _killCount, 0x146, true);
 			const int bonus = calculateThresholdBonus(_killCount, 0x145, 0x14, _tuning.bonus);
 			playChapterCompleteCinematic("LVL13/L13END.ANM", 13, 0x69, 5, " ", accuracyText, bonus);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL13/L13NEW.ANM", "LVL13/L13DEATH.ANM"))
@@ -1286,17 +1290,17 @@ bool InsaneRebel1::runLevel14() {
 
 	beginLevelTitleOverlay(13);
 	playCinematic("LVL14/L14INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		loadTuningForLevel(0x11);
 		resetLevelAttemptState(1, 1);
 		_level14SuccessFrames = 0;
 
 		// Phase 1: targeting surface cannons
 		playInteractiveVideo("LVL14/L14PLAY.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
@@ -1314,7 +1318,7 @@ bool InsaneRebel1::runLevel14() {
 			_level14Play2BSpliceFrame = 0;
 
 			playInteractiveVideo("LVL14/L14PLAY2.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			if (_health >= 0 && _level14Play2BSplicePending) {
@@ -1326,7 +1330,7 @@ bool InsaneRebel1::runLevel14() {
 				// continuation clip, so the port starts it from frame 0 but uses
 				// the non-zero frame argument to preserve gameplay/video state.
 				playInteractiveVideo("LVL14/L14PLY2B.ANM", spliceFrame);
-				if (_vm->shouldQuit())
+				if (shouldAbortGameFlow())
 					return false;
 			}
 		}
@@ -1334,7 +1338,7 @@ bool InsaneRebel1::runLevel14() {
 		if (_health >= 0) {
 			playChapterCompleteCinematic("LVL14/L14END.ANM", 14, 0x69, 5,
 				nullptr, nullptr, 0, nullptr, nullptr, 0, _difficulty + 10);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL14/L14NEW.ANM", "LVL14/L14DEATH.ANM"))
@@ -1360,23 +1364,23 @@ bool InsaneRebel1::runLevel15() {
 
 	beginLevelTitleOverlay(14);
 	playCinematic("LVL15/L15INTRO.ANM");
-	if (_vm->shouldQuit())
+	if (shouldAbortGameFlow())
 		return false;
 
-	while (!_vm->shouldQuit()) {
+	while (!shouldAbortGameFlow()) {
 		loadTuningForLevel(0x13);
 		resetLevelAttemptState(1, 0);
 
 		// Phase 1: trench run
 		_levelGameplayPhase = 1;
 		playInteractiveVideo("LVL15/L15PLAY1.ANM");
-		if (_vm->shouldQuit())
+		if (shouldAbortGameFlow())
 			return false;
 
 		if (_health >= 0) {
 			// Torpedo lock cutscene
 			playCinematic("LVL15/L15INTR2.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 
 			// Phase 2: final approach and torpedo shot. The DOS flow enables
@@ -1392,7 +1396,7 @@ bool InsaneRebel1::runLevel15() {
 			_levelGameplayPhase = 2;
 
 			playInteractiveVideo("LVL15/L15PLAY2.ANM");
-			if (_vm->shouldQuit())
+			if (shouldAbortGameFlow())
 				return false;
 		}
 
@@ -1413,7 +1417,7 @@ bool InsaneRebel1::runLevel15() {
 			playChapterCompleteCinematic("LVL15/L15END1.ANM", 15, 0x122, 0xA5,
 				"Part I", accuracyText, targetBonus,
 				"Part II", "Torpedo on mark", 10000, _difficulty + 13);
-			return !_vm->shouldQuit();
+			return !shouldAbortGameFlow();
 		}
 
 		if (playDeathOrRetry("LVL15/L15NEW.ANM", "LVL15/L15DEATH.ANM"))
@@ -1447,52 +1451,85 @@ void InsaneRebel1::runGame() {
 		&InsaneRebel1::runLevel15
 	};
 	const int numLevels = (int)(sizeof(kLevelRunners) / sizeof(kLevelRunners[0]));
-	auto runLevelsFrom = [&](int startLevel) {
-		const int firstLevel = CLIP<int>(startLevel, 1, numLevels);
-		bool completed = true;
-		int lastCompletedLevel = 0;
-		_health = kMaxHealth;
-		_lives = 3;
-		_score = 0;
-		_prevScore = 0;
-
-		for (int level = firstLevel;
-			 level <= numLevels && completed && !_vm->shouldQuit();
-			 ++level) {
-			playLevelTransitionCutscene(level);
-			if (_vm->shouldQuit())
-				break;
+	auto runLevelsFrom = [&](int startLevel, bool resetRunState) {
+		int firstLevel = CLIP<int>(startLevel, 1, numLevels);
+
+		while (!_vm->shouldQuit()) {
+			_loadRequested = false;
+			bool completed = true;
+			int lastCompletedLevel = 0;
 
-			completed = (this->*kLevelRunners[level - 1])();
-			if (completed) {
-				lastCompletedLevel = level;
-				if (level < numLevels)
-					_startLevel = level + 1;
+			_health = kMaxHealth;
+			if (resetRunState) {
+				_lives = 3;
+				_score = 0;
+				_prevScore = 0;
+			}
+			resetRunState = false;
+
+			for (int level = firstLevel;
+				 level <= numLevels && completed && !shouldAbortGameFlow();
+				 ++level) {
+				_resumeLevel = level;
+				playLevelTransitionCutscene(level);
+				if (shouldAbortGameFlow())
+					break;
+
+				completed = (this->*kLevelRunners[level - 1])();
+				if (completed) {
+					lastCompletedLevel = level;
+					if (level < numLevels) {
+						_startLevel = level + 1;
+						_resumeLevel = _startLevel;
+						autosaveProgress();
+					}
+				}
 			}
-		}
 
-		if (!_vm->shouldQuit() && completed && lastCompletedLevel == numLevels)
-			playCinematic("FIN/FNFINAL.ANM");
-		if (!_vm->shouldQuit())
-			runHighScoreNameEntry();
-		_currentLevel = 0;
+			if (_loadRequested) {
+				firstLevel = getCurrentSaveLevel();
+				continue;
+			}
+
+			if (!shouldAbortGameFlow() && completed && lastCompletedLevel == numLevels)
+				playCinematic("FIN/FNFINAL.ANM");
+			if (!shouldAbortGameFlow())
+				runHighScoreNameEntry();
+			_currentLevel = 0;
+			return;
+		}
 	};
 
-	// Play intro sequence (logo + opening)
-	playIntroSequence();
-	if (_vm->shouldQuit())
-		return;
+	bool loadedStartupSave = false;
+	if (ConfMan.hasKey("save_slot")) {
+		const int saveSlot = ConfMan.getInt("save_slot");
+		if (loadGameState(saveSlot, true).getCode() == Common::kNoError) {
+			loadedStartupSave = true;
+			runLevelsFrom(_resumeLevel, false);
+		}
+	}
+
+	if (!loadedStartupSave) {
+		// Play intro sequence (logo + opening)
+		playIntroSequence();
+		if (shouldAbortGameFlow())
+			return;
+	}
 
 	// Main menu → gameplay loop
 	while (!_vm->shouldQuit()) {
 		int menuResult = runMainMenu();
+		if (_loadRequested) {
+			runLevelsFrom(_resumeLevel, false);
+			continue;
+		}
 		if (_vm->shouldQuit())
 			return;
 
 		switch (menuResult) {
 		case 1: {
 			// START NEW GAME — sequential play from _startLevel
-			runLevelsFrom(_startLevel);
+			runLevelsFrom(_startLevel, true);
 			break;
 		}
 		case 2:
@@ -1504,14 +1541,14 @@ void InsaneRebel1::runGame() {
 			// game from the decoded chapter when a valid passcode is entered.
 			const int passcodeLevel = runPasscodeEntryDialog();
 			if (passcodeLevel >= 1 && passcodeLevel <= numLevels)
-				runLevelsFrom(passcodeLevel);
+				runLevelsFrom(passcodeLevel, true);
 			else if (passcodeLevel == numLevels + 1) {
 				_health = kMaxHealth;
 				_lives = 3;
 				_score = 0;
 				_prevScore = 0;
 				playCinematic("FIN/FNFINAL.ANM");
-				if (!_vm->shouldQuit())
+				if (!shouldAbortGameFlow())
 					runHighScoreNameEntry();
 				_currentLevel = 0;
 			}
@@ -1522,14 +1559,14 @@ void InsaneRebel1::runGame() {
 			// original successor flow so post-level cinematics still play.
 			int selectedLevel = runLevelSelectMenu();
 			if (selectedLevel >= 1 && selectedLevel <= numLevels)
-				runLevelsFrom(selectedLevel);
+				runLevelsFrom(selectedLevel, true);
 			break;
 		}
 		case 5:
 			// CONTINUE DEMO — attract mode.
 			// Original shows TOP PILOTS (O1SCORE.ANM) then loops O1OPEN.ANM.
 			showHighScores();
-			if (!_vm->shouldQuit())
+			if (!shouldAbortGameFlow())
 				playCinematic("OPEN/O1OPEN.ANM");
 			break;
 		case 6:
@@ -1654,6 +1691,9 @@ void InsaneRebel1::playInteractiveVideoFile(const char *filename, int32 videoOff
 // Play interactive gameplay video (with ship physics + HUD).
 void InsaneRebel1::playInteractiveVideo(const char *filename, int32 startFrame) {
 	debug(1, "InsaneRebel1::playInteractiveVideo('%s', startFrame=%d)", filename, startFrame);
+	if (shouldAbortGameFlow())
+		return;
+
 	int32 videoStartFrame = 0;
 	int32 videoOffset = 0;
 
diff --git a/engines/scumm/insane/rebel1/saveload.cpp b/engines/scumm/insane/rebel1/saveload.cpp
new file mode 100644
index 00000000000..06849925a7f
--- /dev/null
+++ b/engines/scumm/insane/rebel1/saveload.cpp
@@ -0,0 +1,254 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/ptr.h"
+#include "common/stream.h"
+#include "common/endian.h"
+
+#include "graphics/thumbnail.h"
+
+#include "scumm/scumm.h"
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel1/rebel.h"
+
+namespace Scumm {
+
+const uint32 kRA1SaveTag = MKTAG('R', 'A', '1', 'S');
+const uint32 kRA1SaveVersion = 1;
+const uint32 kRA1ScummSaveVersion = 124;
+
+struct Rebel1SavegameHeader {
+	uint32 type;
+	uint32 size;
+	uint32 version;
+	char name[32];
+};
+
+bool writeRebel1SavegameHeader(Common::WriteStream *out, const Common::String &desc) {
+	Rebel1SavegameHeader hdr;
+	hdr.type = MKTAG('S', 'C', 'V', 'M');
+	hdr.size = 0;
+	hdr.version = kRA1ScummSaveVersion;
+	memset(hdr.name, 0, sizeof(hdr.name));
+	Common::strlcpy(hdr.name, desc.c_str(), sizeof(hdr.name));
+
+	out->writeUint32BE(hdr.type);
+	out->writeUint32LE(hdr.size);
+	out->writeUint32LE(hdr.version);
+	out->write(hdr.name, sizeof(hdr.name));
+	return !out->err();
+}
+
+bool readRebel1SavegameHeader(Common::SeekableReadStream *in, Common::String *desc, uint32 *version) {
+	Rebel1SavegameHeader hdr;
+	hdr.type = in->readUint32BE();
+	hdr.size = in->readUint32LE();
+	hdr.version = in->readUint32LE();
+	in->read(hdr.name, sizeof(hdr.name));
+	if (in->err() || hdr.type != MKTAG('S', 'C', 'V', 'M'))
+		return false;
+
+	if (hdr.version > 0xFFFFFF)
+		hdr.version = SWAP_BYTES_32(hdr.version);
+	if (hdr.version < VER(52) || hdr.version > kRA1ScummSaveVersion)
+		return false;
+
+	hdr.name[sizeof(hdr.name) - 1] = 0;
+	if (desc)
+		*desc = hdr.name;
+	if (version)
+		*version = hdr.version;
+	return true;
+}
+
+int InsaneRebel1::getCurrentSaveLevel() const {
+	return CLIP<int>(_resumeLevel, 1, kNumLevels);
+}
+
+int InsaneRebel1::getAutosaveTargetSlot() const {
+	return _activeSaveSlot >= 0 ? _activeSaveSlot : 0;
+}
+
+Common::Error InsaneRebel1::writeSaveState(int slot, const Common::String &desc, const SaveState &state) const {
+	Common::String filename;
+	Common::SeekableWriteStream *out = _vm->openSaveFileForWriting(slot, false, filename);
+	if (!out)
+		return Common::kWritingFailed;
+
+	writeRebel1SavegameHeader(out, desc);
+	Graphics::saveThumbnail(*out);
+	_vm->saveInfos(out);
+
+	out->writeUint32BE(kRA1SaveTag);
+	out->writeUint32LE(kRA1SaveVersion);
+	out->writeSint16LE((int16)CLIP<int>(state.resumeLevel, 1, kNumLevels));
+	out->writeSint16LE((int16)MAX<int>(state.lives, 0));
+	out->writeSint32LE(state.score);
+	out->writeSint32LE(state.prevScore);
+	out->writeSint16LE((int16)CLIP<int>(state.difficulty, 0, 2));
+	out->writeSint16LE((int16)CLIP<int>(state.maxChapterUnlocked, 0, kNumLevels));
+
+	out->finalize();
+	const bool failed = out->err();
+	delete out;
+
+	if (failed) {
+		warning("RA1: failed to write save '%s'", filename.c_str());
+		return Common::kWritingFailed;
+	}
+
+	debug(1, "RA1: saved slot=%d level=%d lives=%d score=%d desc='%s'",
+		slot, state.resumeLevel, state.lives, state.score, desc.c_str());
+	return Common::kNoError;
+}
+
+bool InsaneRebel1::readSaveState(int slot, SaveState &state, Common::String *desc) const {
+	Common::String filename;
+	Common::ScopedPtr<Common::SeekableReadStream> in(_vm->openSaveFileForReading(slot, false, filename));
+	if (!in)
+		return false;
+
+	uint32 headerVersion = 0;
+	if (!readRebel1SavegameHeader(in.get(), desc, &headerVersion)) {
+		warning("RA1: invalid save header in '%s'", filename.c_str());
+		return false;
+	}
+
+	if (headerVersion >= VER(52) && !Graphics::skipThumbnail(*in)) {
+		warning("RA1: save thumbnail could not be skipped in '%s'", filename.c_str());
+		return false;
+	}
+
+	if (headerVersion >= VER(56)) {
+		SaveStateMetaInfos infos;
+		if (!_vm->loadInfos(in.get(), &infos)) {
+			warning("RA1: save info section could not be found in '%s'", filename.c_str());
+			return false;
+		}
+		_vm->setTotalPlayTime(infos.playtime * 1000);
+	} else {
+		_vm->setTotalPlayTime();
+	}
+
+	if (in->readUint32BE() != kRA1SaveTag) {
+		warning("RA1: missing RA1 save data in '%s'", filename.c_str());
+		return false;
+	}
+
+	const uint32 version = in->readUint32LE();
+	if (version == 0 || version > kRA1SaveVersion) {
+		warning("RA1: unsupported save version %u in '%s'", version, filename.c_str());
+		return false;
+	}
+
+	state.resumeLevel = CLIP<int>(in->readSint16LE(), 1, kNumLevels);
+	state.lives = MAX<int>(in->readSint16LE(), 0);
+	state.score = in->readSint32LE();
+	state.prevScore = in->readSint32LE();
+	state.difficulty = CLIP<int>(in->readSint16LE(), 0, 2);
+	state.maxChapterUnlocked = CLIP<int>(in->readSint16LE(), 0, kNumLevels);
+
+	if (in->err()) {
+		warning("RA1: truncated save data in '%s'", filename.c_str());
+		return false;
+	}
+
+	return true;
+}
+
+Common::Error InsaneRebel1::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
+	if (isAutosave) {
+		autosaveProgress();
+		return Common::kNoError;
+	}
+
+	SaveState state;
+	state.resumeLevel = getCurrentSaveLevel();
+	state.lives = _lives;
+	state.score = _score;
+	state.prevScore = _prevScore;
+	state.difficulty = _difficulty;
+	state.maxChapterUnlocked = _maxChapterUnlocked;
+
+	Common::String saveDesc = desc;
+	if (saveDesc.empty())
+		saveDesc = Common::String::format("Level %d", state.resumeLevel);
+
+	Common::Error result = writeSaveState(slot, saveDesc, state);
+	if (result.getCode() == Common::kNoError)
+		_activeSaveSlot = slot;
+	return result;
+}
+
+Common::Error InsaneRebel1::loadGameState(int slot, bool startupLoad) {
+	SaveState state;
+	if (!readSaveState(slot, state))
+		return Common::kReadingFailed;
+
+	_resumeLevel = state.resumeLevel;
+	_startLevel = state.resumeLevel;
+	_lives = state.lives;
+	_score = state.score;
+	_prevScore = state.prevScore;
+	_difficulty = state.difficulty;
+	_maxChapterUnlocked = state.maxChapterUnlocked;
+	_health = kMaxHealth;
+	_activeSaveSlot = slot;
+
+	loadTuningForLevel(_resumeLevel - 1);
+	if (!startupLoad) {
+		_loadRequested = true;
+		_vm->_smushVideoShouldFinish = true;
+	}
+
+	debug(1, "RA1: loaded slot=%d level=%d lives=%d score=%d", slot, _resumeLevel, _lives, _score);
+	return Common::kNoError;
+}
+
+void InsaneRebel1::autosaveProgress() {
+	SaveState state;
+	state.resumeLevel = getCurrentSaveLevel();
+	state.lives = _lives;
+	state.score = _score;
+	state.prevScore = _prevScore;
+	state.difficulty = _difficulty;
+	state.maxChapterUnlocked = _maxChapterUnlocked;
+
+	const int slot = getAutosaveTargetSlot();
+	SaveState oldState;
+	Common::String oldDesc;
+	if (readSaveState(slot, oldState, &oldDesc)) {
+		if (oldState.resumeLevel > state.resumeLevel ||
+				(oldState.resumeLevel == state.resumeLevel && oldState.lives >= state.lives)) {
+			debug(1, "RA1: skipping autosave slot=%d level=%d lives=%d; existing level=%d lives=%d",
+				slot, state.resumeLevel, state.lives, oldState.resumeLevel, oldState.lives);
+			return;
+		}
+	}
+
+	Common::String desc = oldDesc;
+	if (slot == 0 || desc.empty())
+		desc = "Autosave";
+
+	(void)writeSaveState(slot, desc, state);
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index b77e4a876a9..e7fe13f0263 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -141,6 +141,7 @@ MODULE_OBJS += \
 	insane/rebel1/menu.o \
 	insane/rebel1/render.o \
 	insane/rebel1/runlevels.o \
+	insane/rebel1/saveload.o \
 	insane/rebel2/rebel.o \
 	insane/rebel2/audio.o \
 	insane/rebel2/iact.o \
diff --git a/engines/scumm/saveload.cpp b/engines/scumm/saveload.cpp
index 7d050a27045..55af040617a 100644
--- a/engines/scumm/saveload.cpp
+++ b/engines/scumm/saveload.cpp
@@ -42,6 +42,10 @@
 #include "scumm/he/sprite_he.h"
 #include "scumm/verbs.h"
 
+#ifdef ENABLE_SCUMM_7_8
+#include "scumm/insane/rebel1/rebel.h"
+#endif
+
 #include "backends/audiocd/audiocd.h"
 
 #include "graphics/thumbnail.h"
@@ -76,6 +80,14 @@ struct SaveInfoSection {
 #pragma mark -
 
 Common::Error ScummEngine::loadGameState(int slot) {
+#ifdef ENABLE_SCUMM_7_8
+	if (_game.id == GID_REBEL1) {
+		InsaneRebel1 *rebel = (InsaneRebel1 *)((ScummEngine_v7 *)this)->getInsane();
+		if (rebel)
+			return rebel->loadGameState(slot);
+	}
+#endif
+
 	requestLoad(slot);
 	return Common::kNoError;
 }
@@ -84,6 +96,11 @@ bool ScummEngine::canLoadGameStateCurrently(Common::U32String *msg) {
 	if (!_setupIsComplete)
 		return false;
 
+#ifdef ENABLE_SCUMM_7_8
+	if (_game.id == GID_REBEL1)
+		return true;
+#endif
+
 	// FIXME: For now always allow loading in V0-V3 games
 	// FIXME: Actually, we might wish to support loading in more places.
 	// As long as we are sure it won't cause any problems... Are we
@@ -139,6 +156,14 @@ bool ScummEngine::canLoadGameStateCurrently(Common::U32String *msg) {
 }
 
 Common::Error ScummEngine::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
+#ifdef ENABLE_SCUMM_7_8
+	if (_game.id == GID_REBEL1) {
+		InsaneRebel1 *rebel = (InsaneRebel1 *)((ScummEngine_v7 *)this)->getInsane();
+		if (rebel)
+			return rebel->saveGameState(slot, desc, isAutosave);
+	}
+#endif
+
 	requestSave(slot, desc);
 	return Common::kNoError;
 }
@@ -147,6 +172,11 @@ bool ScummEngine::canSaveGameStateCurrently(Common::U32String *msg) {
 	if (!_setupIsComplete)
 		return false;
 
+#ifdef ENABLE_SCUMM_7_8
+	if (_game.id == GID_REBEL1)
+		return true;
+#endif
+
 	// Disallow saving in v0-v3 games when a 'prequel' to a cutscene is shown.
 	// This is a blank screen with text, and while this is shown, saving should
 	// be disabled, as no room is set.


Commit: 9e6422b20a72b3e6107217c7feba9b0227ecf7f9
    https://github.com/scummvm/scummvm/commit/9e6422b20a72b3e6107217c7feba9b0227ecf7f9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-05-31T16:10:14+02:00

Commit Message:
SCUMM: RA2: Avoid direct OSystem mouse call

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 5746004151c..e9b35a408b6 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2126,7 +2126,6 @@ void InsaneRebel2::renderPostRenderMenuCursor(byte *renderBitmap, int pitch, int
 	const int cursorHeight = 10;
 
 	CursorMan.showMouse(false);
-	_vm->_system->showMouse(false);
 
 	const int cursorX = _vm->_mouse.x;
 	const int cursorY = _vm->_mouse.y;




More information about the Scummvm-git-logs mailing list