[Scummvm-git-logs] scummvm master -> 64d051dc08be08b230344d4d55de734304c9d5d4
neuromancer
noreply at scummvm.org
Sun Jul 26 11:16:46 UTC 2026
This automated email contains information about 18 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
3340929b6c SCUMM: RA2: support for JP release (DOS/Windows)
c02839641b SCUMM: RA2: better implementation of the menu for psx release
8d355b9ef7 SCUMM: RA2: implementation of the level select menu for psx release
e3d7ec9751 SCUMM: RA2: use correct sound for L1 in psx release
8420adc2de SCUMM: RA2: improved movement and gameplay of L1 in psx release
68f27cca05 SCUMM: RA2: added correct explosions and debris among others in L1 in psx release
a5985cf827 SCUMM: RA2: added smoke trails and other effects in L1 in psx release
e182864bf7 SCUMM: RA2: added missing animation and improved controls for L1 in psx release
b9d3731531 SCUMM: RA2: use the original's fire cadence and cockpit details for L1 in psx release
d8041fdd6b SCUMM: RA2: pan the 3D scene with the view in L1 in psx release
1872ad720d SCUMM: RA2: sustain the looping sounds in psx release
22a782936c SCUMM: RA2: emit the debris smoke trails as the original does in psx release
233d70d6b1 SCUMM: RA2: initial code for L2 in psx release
61864ceb26 SCUMM: RA2: correctly implemented sliding effect and enemy waves in L2 in psx release
53a736900d SCUMM: RA2: fix L2 aiming, occlusion and per-part rookie tables in psx release
2a8dc23c1a SCUMM: RA2: fix L2 shot origin, cover occlusion and cutscene order in psx release
79b6a1e895 SCUMM: RA2: draw the L2 rookie with its slot CLUT and fire from the pistol in psx release
64d051dc08 SCUMM: RA2: fixed variable shadow warning in psx release
Commit: 3340929b6c56519c7ba1f790da9b0f4e077a4ee9
https://github.com/scummvm/scummvm/commit/3340929b6c56519c7ba1f790da9b0f4e077a4ee9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: support for JP release (DOS/Windows)
Changed paths:
engines/scumm/charset.cpp
engines/scumm/detection_internal.h
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/insane/rebel2/render.cpp
engines/scumm/smush/rebel/font_rebel2.cpp
engines/scumm/smush/rebel/font_rebel2.h
engines/scumm/smush/rebel/smush_multi_font.cpp
diff --git a/engines/scumm/charset.cpp b/engines/scumm/charset.cpp
index 42910601596..190e18181d0 100644
--- a/engines/scumm/charset.cpp
+++ b/engines/scumm/charset.cpp
@@ -116,7 +116,12 @@ void ScummEngine::loadCJKFont() {
numChar = 2350;
break;
case Common::JA_JPN:
- fontFile = (_game.id == GID_DIG) ? "kanji16.fnt" : "japanese.fnt";
+ if (_game.id == GID_DIG)
+ fontFile = "kanji16.fnt";
+ else if (_game.id == GID_REBEL2)
+ fontFile = "LAUNCH/KANJI.FNT";
+ else
+ fontFile = "japanese.fnt";
numChar = 8192;
break;
case Common::ZH_TWN:
diff --git a/engines/scumm/detection_internal.h b/engines/scumm/detection_internal.h
index 77785688a06..e6b00513396 100644
--- a/engines/scumm/detection_internal.h
+++ b/engines/scumm/detection_internal.h
@@ -249,6 +249,8 @@ static Common::Language detectLanguage(const Common::FSList &fslist, byte id, co
&& searchFSNode(systmList, "GAME.TRS", langFile)
&& trs.open(langFile)) {
switch (trs.size()) {
+ case 46294: // ed4b2312e8f60ad3fdd9d02db38da9a9
+ return Common::JA_JPN;
case 46746: // d9aced0c3fcb8f6a0045dcd4cbf12590
return Common::EN_ANY;
case 48097: // 66353d7250f680b28992459c355caa17
@@ -257,6 +259,8 @@ static Common::Language detectLanguage(const Common::FSList &fslist, byte id, co
return Common::ES_ESP;
case 50094: // 004fb2fd15f84a1f81cc362d73811c9c
return Common::DE_DEU;
+ case 58883: // efffbf955884a87a3be6b8459ba559de
+ return Common::PT_BRA;
case 60976: // c53823d48beca122c45a83d35027a0e7
return Common::FR_FRA;
default:
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 0a3c2dd89ab..0af5ddacdf5 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -2068,6 +2068,7 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
int srcLen = strlen(textStr);
int dstIdx = 0;
int numChars = _rebelMsgFont->getNumChars();
+ const bool useCJK = _vm->_language == Common::JA_JPN;
for (int i = 0; i < srcLen && dstIdx < (int)sizeof(convertedText) - 1; i++) {
byte ch = (byte)textStr[i];
@@ -2104,6 +2105,16 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
}
}
+ uint charLen;
+ decodeRebel2Char(textStr + i, srcLen - i, charLen, useCJK);
+ if (charLen == 2) {
+ if (dstIdx + 2 > (int)sizeof(convertedText) - 1)
+ break;
+ convertedText[dstIdx++] = textStr[i++];
+ convertedText[dstIdx++] = textStr[i];
+ continue;
+ }
+
if (ch >= 'a' && ch <= 'z') {
ch = ch - 'a' + 'A';
}
@@ -2119,7 +2130,7 @@ void InsaneRebel2::iactRebel2Opcode9(byte *renderBitmap, Common::SeekableReadStr
convertedText[dstIdx] = '\0';
if (ConfMan.getBool("subtitles")) {
- Rebel2FontSet fontSet;
+ Rebel2FontSet fontSet(_vm->_language == Common::JA_JPN);
fontSet.numFonts = 1;
fontSet.fonts[0] = _rebelMsgFont;
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index 51bbee2f9cd..c646f42aa59 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -291,17 +291,21 @@ int InsaneRebel2::getMenuStringWidth(const char *str) const {
if (!defaultFont)
return 0;
+ const char *end = str + strlen(str);
+ const bool useCJK = _vm->_language == Common::JA_JPN;
int w = 0;
NutRenderer *curFont = defaultFont;
int dummyColor = 0;
- while (*str) {
+ while (str < end) {
int fc = parseFormatCode(str, dummyColor);
if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); continue; }
if (fc == -2) continue;
- byte c = (byte)*str++;
+ uint charLen;
+ uint16 c = decodeRebel2Char(str, end - str, charLen, useCJK);
+ str += charLen;
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (curFont && c < curFont->getNumChars())
- w += curFont->getCharWidth(c);
+ if (curFont && (c > 0xff || c < curFont->getNumChars()))
+ w += curFont->getCharWidth((byte)c);
}
return w;
}
@@ -315,16 +319,20 @@ void InsaneRebel2::drawMenuString(byte *renderBitmap, const char *str, int x, in
Common::Rect clipRect(0, 0, _vm->_screenWidth, _vm->_screenHeight);
int pitch = _vm->_screenWidth;
+ const char *end = str + strlen(str);
+ const bool useCJK = _vm->_language == Common::JA_JPN;
NutRenderer *curFont = defaultFont;
int curColor = defaultColor;
- while (*str) {
+ while (str < end) {
int fc = parseFormatCode(str, curColor);
if (fc >= 0) { curFont = (fonts[fc] ? fonts[fc] : defaultFont); continue; }
if (fc == -2) continue;
- byte c = (byte)*str++;
+ uint charLen;
+ uint16 c = decodeRebel2Char(str, end - str, charLen, useCJK);
+ str += charLen;
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
- if (!curFont || c >= curFont->getNumChars()) continue;
- int charW = curFont->getCharWidth(c);
+ if (!curFont || (c <= 0xff && c >= curFont->getNumChars())) continue;
+ int charW = curFont->getCharWidth((byte)c);
if (x >= 0 && y >= 0 && charW > 0)
drawRebel2Char(curFont, renderBitmap, clipRect, x, y, pitch, curColor, c);
x += charW;
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index c5447b4de25..d3353bd2482 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -2795,6 +2795,7 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
}
const int textScale = isHiRes() ? 2 : 1;
+ const bool useCJK = _vm->_language == Common::JA_JPN;
int drawY = _textOverlayY * textScale;
int visCount = 0;
@@ -2813,11 +2814,13 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
if (parseRebel2TextOverlayFormat(s, mFont, mColor, fonts, ARRAYSIZE(fonts), defaultFont))
continue;
lineFont = mFont;
- byte c = (byte)*s++;
+ uint charLen;
+ uint16 c = decodeRebel2Char(s, lineEnd - s, charLen, useCJK);
+ s += charLen;
if (c >= 'a' && c <= 'z')
c = c - 'a' + 'A';
- if (mFont && c < mFont->getNumChars())
- lineWidth += mFont->getCharWidth(c);
+ if (mFont && (c > 0xff || c < mFont->getNumChars()))
+ lineWidth += mFont->getCharWidth((byte)c);
lineVisCount++;
}
}
@@ -2831,14 +2834,16 @@ void InsaneRebel2::renderTextOverlay(byte *renderBitmap, int pitch, int width, i
while (s < lineEnd && (visCount + lineCharsDrawn) < displayLen) {
if (parseRebel2TextOverlayFormat(s, curFont, curColor, fonts, ARRAYSIZE(fonts), defaultFont))
continue;
- byte c = (byte)*s++;
+ uint charLen;
+ uint16 c = decodeRebel2Char(s, lineEnd - s, charLen, useCJK);
+ s += charLen;
if (c >= 'a' && c <= 'z')
c = c - 'a' + 'A';
- if (!curFont || c >= curFont->getNumChars()) {
+ if (!curFont || (c <= 0xff && c >= curFont->getNumChars())) {
lineCharsDrawn++;
continue;
}
- int charW = curFont->getCharWidth(c);
+ int charW = curFont->getCharWidth((byte)c);
if (drawX >= 0 && drawY >= 0 && charW > 0) {
drawRebel2Char(curFont, renderBitmap, clipRect, drawX, drawY, pitch, curColor, c);
}
@@ -4168,7 +4173,7 @@ void InsaneRebel2::renderHandler8PovOverlay(byte *renderBitmap, int pitch, int w
if (_rebelHandler != 8 || !renderBitmap || !_smush_talkfontNut || !_smush_povfontNut)
return;
- Rebel2FontSet fontSet;
+ Rebel2FontSet fontSet(_vm->_language == Common::JA_JPN);
fontSet.numFonts = 4;
fontSet.defaultFont = 0;
fontSet.fonts[0] = _smush_talkfontNut;
diff --git a/engines/scumm/smush/rebel/font_rebel2.cpp b/engines/scumm/smush/rebel/font_rebel2.cpp
index 77f2733eeaf..7b903401e63 100644
--- a/engines/scumm/smush/rebel/font_rebel2.cpp
+++ b/engines/scumm/smush/rebel/font_rebel2.cpp
@@ -637,7 +637,7 @@ bool drawRebel2Codec23Sprite(NutRenderer *sprite, byte *buffer, int pitch, int w
return renderer && renderer->drawCodec23Sprite(buffer, pitch, width, height, clipRect, x, y, spriteIdx, scale);
}
-Rebel2FontSet::Rebel2FontSet() : numFonts(0), defaultFont(0) {
+Rebel2FontSet::Rebel2FontSet(bool useCJKMode) : numFonts(0), defaultFont(0), useCJK(useCJKMode) {
memset(fonts, 0, sizeof(fonts));
}
@@ -694,9 +694,28 @@ static bool parseRebel2FormatCode(const char *str, uint len, uint &pos, int &fon
return false;
}
+uint16 decodeRebel2Char(const char *str, uint len, uint &charLen, bool useCJK) {
+ charLen = 0;
+ if (!str || len == 0)
+ return 0;
+
+ const byte lead = (byte)str[0];
+ charLen = 1;
+ if (useCJK && len > 1 && ((lead >= 0x80 && lead <= 0x9f) || (lead >= 0xe0 && lead <= 0xfc))) {
+ charLen = 2;
+ return lead | ((uint16)(byte)str[1] << 8);
+ }
+
+ return lead;
+}
+
int drawRebel2Char(NutRenderer *font, byte *buffer, Common::Rect &clipRect, int x, int y,
- int pitch, int16 col, byte chr) {
- if (!font || chr >= font->getNumChars())
+ int pitch, int16 col, uint16 chr) {
+ if (!font)
+ return 0;
+ if (chr > 0xff)
+ return font->draw2byte(buffer, clipRect, x, y, pitch, col, chr);
+ if (chr >= font->getNumChars())
return 0;
const int charWidth = font->getCharWidth(chr);
@@ -740,13 +759,15 @@ int getRebel2StringWidth(const Rebel2FontSet &fontSet, const char *str, uint len
if (parseRebel2FormatCode(str, len, pos, fontId, color))
continue;
- const byte chr = (byte)str[pos++];
+ uint charLen;
+ const uint16 chr = decodeRebel2Char(str + pos, len - pos, charLen, fontSet.useCJK);
+ pos += charLen;
if (chr == '\n' || chr == '\r')
continue;
NutRenderer *font = fontSet.getFont(fontId);
- if (font && chr < font->getNumChars())
- width += font->getCharWidth(chr);
+ if (font && (chr > 0xff || chr < font->getNumChars()))
+ width += font->getCharWidth((byte)chr);
}
return width;
@@ -762,15 +783,17 @@ int getRebel2StringHeight(const Rebel2FontSet &fontSet, const char *str, uint le
if (parseRebel2FormatCode(str, len, pos, fontId, color))
continue;
- const byte chr = (byte)str[pos++];
+ uint charLen;
+ const uint16 chr = decodeRebel2Char(str + pos, len - pos, charLen, fontSet.useCJK);
+ pos += charLen;
if (chr == '\n') {
NutRenderer *font = fontSet.getFont(fontId);
height += lineHeight ? lineHeight : (font ? font->getFontHeight() : 0);
lineHeight = 0;
} else if (chr != '\r') {
NutRenderer *font = fontSet.getFont(fontId);
- if (font && chr < font->getNumChars())
- lineHeight = MAX<int>(lineHeight, font->getCharHeight(chr));
+ if (font && (chr > 0xff || chr < font->getNumChars()))
+ lineHeight = MAX<int>(lineHeight, font->getCharHeight((byte)chr));
}
}
@@ -785,7 +808,9 @@ static void drawRebel2Substring(const Rebel2FontSet &fontSet, const char *str, u
if (parseRebel2FormatCode(str, len, pos, fontId, color))
continue;
- const byte chr = (byte)str[pos++];
+ uint charLen;
+ const uint16 chr = decodeRebel2Char(str + pos, len - pos, charLen, fontSet.useCJK);
+ pos += charLen;
if (chr == '\n' || chr == '\r')
continue;
diff --git a/engines/scumm/smush/rebel/font_rebel2.h b/engines/scumm/smush/rebel/font_rebel2.h
index 002ba6c38bd..299ff7b0cb7 100644
--- a/engines/scumm/smush/rebel/font_rebel2.h
+++ b/engines/scumm/smush/rebel/font_rebel2.h
@@ -40,8 +40,9 @@ struct Rebel2FontSet {
NutRenderer *fonts[kMaxFonts];
int numFonts;
int defaultFont;
+ bool useCJK;
- Rebel2FontSet();
+ Rebel2FontSet(bool useCJKMode = false);
NutRenderer *getFont(int id) const;
};
@@ -51,8 +52,9 @@ bool drawRebel2Codec23Sprite(NutRenderer *sprite, byte *buffer, int pitch, int w
const Common::Rect &clipRect, int x, int y, int spriteIdx, int scale);
bool drawRebel2Codec45Sprite(NutRenderer *sprite, byte *buffer, int pitch, int width, int height,
const Common::Rect &clipRect, int x, int y, int spriteIdx, int scale);
+uint16 decodeRebel2Char(const char *str, uint len, uint &charLen, bool useCJK);
int drawRebel2Char(NutRenderer *font, byte *buffer, Common::Rect &clipRect, int x, int y,
- int pitch, int16 col, byte chr);
+ int pitch, int16 col, uint16 chr);
int getRebel2StringWidth(const Rebel2FontSet &fontSet, const char *str, uint len);
int getRebel2StringHeight(const Rebel2FontSet &fontSet, const char *str, uint len);
void drawRebel2String(const Rebel2FontSet &fontSet, const char *str, uint len, byte *buffer,
diff --git a/engines/scumm/smush/rebel/smush_multi_font.cpp b/engines/scumm/smush/rebel/smush_multi_font.cpp
index fefa1efb864..8bf8172e884 100644
--- a/engines/scumm/smush/rebel/smush_multi_font.cpp
+++ b/engines/scumm/smush/rebel/smush_multi_font.cpp
@@ -63,7 +63,7 @@ Rebel2FontSet SmushMultiFont::getRebel2FontSet() {
const bool highRes = _vm->_screenWidth >= 640 && _vm->_screenHeight >= 400;
const char *const *ra2Fonts = highRes ? ra2FontsHi : ra2FontsLo;
- Rebel2FontSet fontSet;
+ Rebel2FontSet fontSet(_vm->_language == Common::JA_JPN);
fontSet.numFonts = ARRAYSIZE(ra2FontsLo);
fontSet.defaultFont = CLIP<int>(_defaultFont, 0, fontSet.numFonts - 1);
for (int i = 0; i < fontSet.numFonts; i++) {
Commit: c02839641b1c42aa63b9f63ee748b006c3e5aeaa
https://github.com/scummvm/scummvm/commit/c02839641b1c42aa63b9f63ee748b006c3e5aeaa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: better implementation of the menu for psx release
Changed paths:
engines/scumm/insane/rebel2/psx/audio.cpp
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/menu.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/audio.cpp b/engines/scumm/insane/rebel2/psx/audio.cpp
index 4f99c1904ea..264ac9425a6 100644
--- a/engines/scumm/insane/rebel2/psx/audio.cpp
+++ b/engines/scumm/insane/rebel2/psx/audio.cpp
@@ -22,6 +22,7 @@
#include "audio/decoders/xa.h"
#include "audio/mixer.h"
+#include "common/config-manager.h"
#include "common/endian.h"
#include "common/memstream.h"
#include "common/system.h"
@@ -92,6 +93,9 @@ static bool timeReached(uint32 now, uint32 target) {
}
static int soundBalance(int pan) {
+ // "sound mode: mono" in the options menu collapses the panning.
+ if (ConfMan.hasKey("rebel2_mono") && ConfMan.getBool("rebel2_mono"))
+ return 0;
return CLIP((pan - 64) * 2, -127, 127);
}
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 729dfd9d5b0..24bef8053b4 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -427,6 +427,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const bool cursorWasVisible = CursorMan.isVisible();
CursorMan.showMouse(false);
g_system->warpMouse(160, 120);
+ decoder.setVolume(_settings.videoVolume());
decoder.start();
const uint32 gameplayStartTime = g_system->getMillis();
const Graphics::Surface *background = nullptr;
diff --git a/engines/scumm/insane/rebel2/psx/menu.cpp b/engines/scumm/insane/rebel2/psx/menu.cpp
index 0bbf2d2fbc4..1d720d9cf1e 100644
--- a/engines/scumm/insane/rebel2/psx/menu.cpp
+++ b/engines/scumm/insane/rebel2/psx/menu.cpp
@@ -18,14 +18,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#include "common/config-manager.h"
#include "common/events.h"
#include "common/system.h"
+#include "common/util.h"
#include "graphics/cursorman.h"
#include "graphics/surface.h"
-#include "engines/dialogs.h"
-
#include "scumm/scumm_v7.h"
#include "scumm/insane/rebel2/shared.h"
#include "scumm/insane/rebel2/psx/psx.h"
@@ -33,123 +33,562 @@
namespace Scumm {
-static Rebel2MenuCommand getPSXMenuCommand(const Common::Event &event) {
- if (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat)
- return getRebel2MenuCommand(event.kbd);
- if (event.type != Common::EVENT_CUSTOM_ENGINE_ACTION_START)
- return kRebel2MenuCommandNone;
-
- switch (event.customType) {
- case kScummActionInsaneUp:
- return kRebel2MenuCommandUp;
- case kScummActionInsaneDown:
- return kRebel2MenuCommandDown;
- case kScummActionInsaneAttack:
- return kRebel2MenuCommandAccept;
- case kScummActionInsaneBack:
- case kScummActionInsaneSkip:
- return kRebel2MenuCommandCancel;
- default:
- return kRebel2MenuCommandNone;
- }
+// The menus run one logic step per vertical blank.
+static const int kMenuFrameRate = 60;
+
+// Sound effect ids the menus use.
+enum {
+ kMenuSfxMove = 0x1d,
+ kMenuSfxAdjust = 0x1e,
+ kMenuSfxEnter = 0x1f,
+ kMenuSfxLeave = 0x20,
+ kMenuSfxConfirm = 0x40,
+ kMenuSfxStart = 0x44,
+ kMenuSfxDefaults = 0x45
+};
+
+void RA2PSXSettings::reset() {
+ difficulty = 1;
+ sfx = 0x54;
+ music = 0xc00;
+ movies = 0xc00;
+ mono = false;
}
-Rebel2PSX::MenuResult Rebel2PSX::runMainMenu(const RA2PSXMainMenuUI &ui) {
- Graphics::Surface surface;
- surface.create(_vm->_screenWidth, _vm->_screenHeight, g_system->getScreenFormat());
+void RA2PSXSettings::load() {
+ reset();
+ if (ConfMan.hasKey("rebel2_difficulty"))
+ difficulty = CLIP(ConfMan.getInt("rebel2_difficulty"), 0, 2);
+ if (ConfMan.hasKey("rebel2_mono"))
+ mono = ConfMan.getBool("rebel2_mono");
+ if (ConfMan.hasKey("sfx_volume"))
+ sfx = CLIP(ConfMan.getInt("sfx_volume"), 0, 255) * kSFXMaximum / 255 /
+ kSFXStep * kSFXStep;
+ if (ConfMan.hasKey("music_volume"))
+ music = CLIP(ConfMan.getInt("music_volume"), 0, 255) * kCDMaximum / 255 /
+ kCDStep * kCDStep;
+ if (ConfMan.hasKey("speech_volume"))
+ movies = CLIP(ConfMan.getInt("speech_volume"), 0, 255) * kCDMaximum / 255 /
+ kCDStep * kCDStep;
+}
- const bool cursorWasVisible = CursorMan.isVisible();
- CursorMan.showMouse(true);
- int selection = 0;
- MenuResult result = kMenuQuit;
- bool redraw = true;
+void RA2PSXSettings::save() const {
+ ConfMan.setInt("rebel2_difficulty", difficulty);
+ ConfMan.setBool("rebel2_mono", mono);
+ ConfMan.setInt("sfx_volume", sfx * 255 / kSFXMaximum);
+ ConfMan.setInt("music_volume", MIN<int>(255, music * 255 / kCDMaximum));
+ ConfMan.setInt("speech_volume", MIN<int>(255, movies * 255 / kCDMaximum));
+}
- while (!_vm->shouldQuit()) {
- bool openGlobalMenu = false;
- bool openOptions = false;
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- if (event.type == Common::EVENT_QUIT ||
- event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- _vm->quitGame();
- break;
+void RA2PSXSettings::apply(ScummEngine_v7 *vm) const {
+ save();
+ vm->syncSoundSettings();
+}
+
+byte RA2PSXSettings::videoVolume() const {
+ return (byte)CLIP<int>(movies * 255 / kCDMaximum, 0, 255);
+}
+
+#ifdef USE_TINYGL
+
+// A full screen quad in the GPU's subtract mode: level 0 is untouched, 0xff is black.
+struct RA2PSXMenuFade {
+ RA2PSXMenuFade() : level(0), step(0), active(false) {}
+
+ void fadeIn(int amount) {
+ level = 0xff;
+ step = amount;
+ active = true;
+ }
+
+ void fadeOut(int amount) {
+ level = 0;
+ step = -amount;
+ active = true;
+ }
+
+ // Returns true on the step that completes a fade to black.
+ bool update() {
+ if (!active || !step)
+ return false;
+ level -= step;
+ if (step > 0) {
+ if (level <= 0) {
+ level = 0;
+ step = 0;
+ active = false;
}
+ return false;
+ }
+ if (level < 0x100)
+ return false;
+ level = 0xff;
+ step = 0;
+ return true;
+ }
+
+ void apply(Graphics::Surface &surface) const {
+ if (active && level > 0)
+ subtractRA2PSXRect(surface, Common::Rect(surface.w, surface.h),
+ level, level, level);
+ }
+
+ int level;
+ int step;
+ bool active;
+};
+
+struct RA2PSXMenuEvents {
+ RA2PSXMenuEvents() : up(false), down(false), left(false), right(false), accept(false),
+ cancel(false), globalMenu(false), mouseMoved(false), mouseClicked(false),
+ mouseX(-1), mouseY(-1) {}
+
+ bool up;
+ bool down;
+ bool left;
+ bool right;
+ bool accept;
+ bool cancel;
+ bool globalMenu;
+ bool mouseMoved;
+ bool mouseClicked;
+ int mouseX;
+ int mouseY;
+};
- if (event.type == Common::EVENT_MAINMENU ||
- (event.type == Common::EVENT_KEYDOWN && !event.kbdRepeat &&
- event.kbd.keycode == Common::KEYCODE_ESCAPE)) {
- openGlobalMenu = true;
- continue;
- }
-
- if (event.type == Common::EVENT_MOUSEMOVE ||
- event.type == Common::EVENT_LBUTTONDOWN) {
- const int xOffset = (surface.w - 320) / 2;
- const int yOffset = (surface.h - 240) / 2;
- for (int i = 0; i < 2; ++i) {
- Common::Rect rect = ui.itemRect(i);
- rect.translate(xOffset, yOffset);
- if (!rect.contains(event.mouse.x, event.mouse.y))
- continue;
- if (selection != i) {
- selection = i;
- redraw = true;
- }
- if (event.type == Common::EVENT_LBUTTONDOWN) {
- if (i == 0)
- result = kMenuStart;
- else
- openOptions = true;
- }
+static void pollMenuEvents(ScummEngine_v7 *vm, RA2PSXMenuEvents &events) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ switch (event.type) {
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ vm->quitGame();
+ return;
+ case Common::EVENT_MAINMENU:
+ events.globalMenu = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ events.mouseClicked = true;
+ // fall through
+ case Common::EVENT_MOUSEMOVE:
+ events.mouseMoved = true;
+ events.mouseX = event.mouse.x;
+ events.mouseY = event.mouse.y;
+ break;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbdRepeat)
+ break;
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ events.globalMenu = true;
+ break;
+ case Common::KEYCODE_LEFT:
+ events.left = true;
+ break;
+ case Common::KEYCODE_RIGHT:
+ events.right = true;
+ break;
+ case Common::KEYCODE_SPACE:
+ events.accept = true;
+ break;
+ case Common::KEYCODE_BACKSPACE:
+ events.cancel = true;
+ break;
+ default:
+ switch (getRebel2MenuCommand(event.kbd)) {
+ case kRebel2MenuCommandUp:
+ events.up = true;
+ break;
+ case kRebel2MenuCommandDown:
+ events.down = true;
+ break;
+ case kRebel2MenuCommandAccept:
+ events.accept = true;
+ break;
+ default:
break;
}
+ break;
}
-
- const Rebel2MenuCommand command = getPSXMenuCommand(event);
- const int oldSelection = selection;
- const int commandResult = applyRebel2MenuCommand(command, 2, selection);
- if (selection != oldSelection)
- redraw = true;
- if (commandResult == kRebel2MenuResultCancel)
- openGlobalMenu = true;
- else if (commandResult == 0)
- result = kMenuStart;
- else if (commandResult == 1)
- openOptions = true;
+ break;
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
+ switch (event.customType) {
+ case kScummActionInsaneUp:
+ events.up = true;
+ break;
+ case kScummActionInsaneDown:
+ events.down = true;
+ break;
+ case kScummActionInsaneLeft:
+ events.left = true;
+ break;
+ case kScummActionInsaneRight:
+ events.right = true;
+ break;
+ case kScummActionInsaneAttack:
+ events.accept = true;
+ break;
+ case kScummActionInsaneBack:
+ case kScummActionInsaneSkip:
+ events.cancel = true;
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
}
+ }
+}
+
+static int hitTestMenu(Common::Rect (*itemRect)(int), int count, const Graphics::Surface &surface,
+ int x, int y) {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ for (int item = 0; item < count; ++item) {
+ Common::Rect rect = itemRect(item);
+ rect.translate(xOffset, yOffset);
+ if (rect.contains(x, y))
+ return item;
+ }
+ return -1;
+}
+
+// The options list and its "adjust sound" page; the other four screens stay dimmed.
+void Rebel2PSX::runOptionsMenu(const RA2PSXOptionsUI &ui, RA2PSXSoundPlayer &sound,
+ RA2PSXTinyGLRenderer &renderer) {
+ enum Screen {
+ kScreenMain,
+ kScreenSound
+ };
+ enum Dialog {
+ kDialogNone,
+ kDialogResetConfirm,
+ kDialogDefaultsRestored
+ };
+
+ Graphics::Surface background;
+ background.create(_vm->_screenWidth, _vm->_screenHeight, g_system->getScreenFormat());
+ background.fillRect(Common::Rect(background.w, background.h), 0);
+
+ Screen screen = kScreenMain;
+ Dialog dialog = kDialogNone;
+ int selection = RA2PSXOptionsUI::kItemDifficulty;
+ int soundSelection = RA2PSXOptionsUI::kSoundItemMode;
+ int confirmSelection = 0;
+ int messageFrames = 0;
+ int cloakAngle = 0x800;
+ int logicFrame = -1;
+ bool leaving = false;
+ RA2PSXMenuFade fade;
+ fade.fadeIn(8);
+ // Accumulates across iterations; a press between logic steps must not be lost.
+ RA2PSXMenuEvents events;
+ const uint32 startTime = g_system->getMillis();
+ while (!_vm->shouldQuit()) {
+ pollMenuEvents(_vm, events);
if (_vm->shouldQuit())
break;
- if (result == kMenuStart)
+ if (events.globalMenu) {
+ _vm->openMainMenuDialog();
+ _settings.load();
+ events = RA2PSXMenuEvents();
+ continue;
+ }
+
+ const uint32 elapsed = g_system->getMillis() - startTime;
+ const int targetFrame = (int)((uint64)elapsed * kMenuFrameRate / 1000);
+ if (logicFrame >= targetFrame) {
+ g_system->delayMillis(5);
+ continue;
+ }
+
+ bool fadedOut = false;
+ while (logicFrame < targetFrame) {
+ ++logicFrame;
+ cloakAngle = (cloakAngle + 6) & 0xfff;
+ fadedOut |= fade.update();
+ if (dialog == kDialogDefaultsRestored && --messageFrames <= 0)
+ dialog = kDialogNone;
+ }
+ if (fadedOut && leaving)
+ break;
+
+ sound.update();
+ if (!fade.active && dialog == kDialogResetConfirm) {
+ if (events.left || events.right || events.up || events.down) {
+ confirmSelection ^= 1;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (events.cancel) {
+ dialog = kDialogNone;
+ sound.play(kMenuSfxLeave, 0x7f, 0x40);
+ } else if (events.accept) {
+ if (confirmSelection == 1) {
+ _settings.reset();
+ _settings.apply(_vm);
+ selection = RA2PSXOptionsUI::kItemDifficulty;
+ dialog = kDialogDefaultsRestored;
+ messageFrames = 0x78;
+ sound.play(kMenuSfxDefaults, 0x7f, 0x40);
+ } else {
+ dialog = kDialogNone;
+ sound.play(kMenuSfxLeave, 0x7f, 0x40);
+ }
+ }
+ } else if (!fade.active && dialog == kDialogNone && screen == kScreenMain) {
+ const int hit = hitTestMenu(RA2PSXOptionsUI::mainItemRect,
+ RA2PSXOptionsUI::kItemCount, background, events.mouseX, events.mouseY);
+ if (events.mouseMoved && hit >= 0 && hit != selection &&
+ RA2PSXOptionsUI::isItemAvailable(hit)) {
+ selection = hit;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (events.up || events.down) {
+ const int step = events.down ? 1 : RA2PSXOptionsUI::kItemCount - 1;
+ do {
+ selection = (selection + step) % RA2PSXOptionsUI::kItemCount;
+ } while (!RA2PSXOptionsUI::isItemAvailable(selection));
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (selection == RA2PSXOptionsUI::kItemDifficulty && (events.left || events.right)) {
+ _settings.difficulty = (_settings.difficulty + (events.right ? 1 : 2)) % 3;
+ _settings.save();
+ sound.play(kMenuSfxAdjust, 0x7f, 0x40);
+ }
+ if (events.cancel) {
+ leaving = true;
+ sound.play(kMenuSfxLeave, 0x7f, 0x40);
+ fade.fadeOut(8);
+ } else if (events.accept || (events.mouseClicked && hit == selection)) {
+ switch (selection) {
+ case RA2PSXOptionsUI::kItemAdjustSound:
+ screen = kScreenSound;
+ soundSelection = RA2PSXOptionsUI::kSoundItemMode;
+ sound.play(kMenuSfxEnter, 0x7f, 0x40);
+ break;
+ case RA2PSXOptionsUI::kItemResetSettings:
+ dialog = kDialogResetConfirm;
+ confirmSelection = 0;
+ sound.play(kMenuSfxConfirm, 0x7f, 0x40);
+ break;
+ case RA2PSXOptionsUI::kItemExit:
+ leaving = true;
+ sound.play(kMenuSfxLeave, 0x7f, 0x40);
+ fade.fadeOut(8);
+ break;
+ default:
+ break;
+ }
+ }
+ } else if (!fade.active && dialog == kDialogNone) {
+ const int hit = hitTestMenu(RA2PSXOptionsUI::soundItemRect,
+ RA2PSXOptionsUI::kSoundItemCount, background,
+ events.mouseX, events.mouseY);
+ if (events.mouseMoved && hit >= 0 && hit != soundSelection) {
+ soundSelection = hit;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (events.up || events.down) {
+ soundSelection = (soundSelection + (events.down ? 1 :
+ RA2PSXOptionsUI::kSoundItemCount - 1)) %
+ RA2PSXOptionsUI::kSoundItemCount;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (events.left || events.right) {
+ const int direction = events.right ? 1 : -1;
+ bool changed = true;
+ switch (soundSelection) {
+ case RA2PSXOptionsUI::kSoundItemMode:
+ _settings.mono = !_settings.mono;
+ break;
+ case RA2PSXOptionsUI::kSoundItemEffects:
+ _settings.sfx = CLIP<int>(_settings.sfx +
+ direction * RA2PSXSettings::kSFXStep,
+ 0, RA2PSXSettings::kSFXMaximum);
+ break;
+ case RA2PSXOptionsUI::kSoundItemMusic:
+ _settings.music = CLIP<int>(_settings.music +
+ direction * RA2PSXSettings::kCDStep,
+ 0, RA2PSXSettings::kCDMaximum);
+ break;
+ case RA2PSXOptionsUI::kSoundItemMovies:
+ _settings.movies = CLIP<int>(_settings.movies +
+ direction * RA2PSXSettings::kCDStep,
+ 0, RA2PSXSettings::kCDMaximum);
+ break;
+ default:
+ changed = false;
+ break;
+ }
+ if (changed) {
+ _settings.apply(_vm);
+ sound.play(kMenuSfxAdjust, 0x7f, 0x40);
+ }
+ }
+ if (events.cancel || ((events.accept || (events.mouseClicked && hit == soundSelection)) &&
+ soundSelection == RA2PSXOptionsUI::kSoundItemExit)) {
+ screen = kScreenMain;
+ sound.play(kMenuSfxLeave, 0x7f, 0x40);
+ }
+ }
+
+ events = RA2PSXMenuEvents();
+ renderer.beginFrame(background);
+ RA2PSXMatrix transform;
+ transform.setRotationZ(-0x100);
+ transform.preRotateY(cloakAngle);
+ transform.setTranslation(0, 0, 0x604);
+ renderer.renderTransformedModel(_cloakModel, transform, false);
+
+ Graphics::Surface output;
+ renderer.finishFrame(output);
+ if (screen == kScreenMain)
+ ui.drawMain(output, selection, _settings);
+ else
+ ui.drawSound(output, soundSelection, _settings);
+ if (dialog == kDialogResetConfirm)
+ ui.drawDialog(output, "reset settings", "are you sure?", nullptr, confirmSelection);
+ else if (dialog == kDialogDefaultsRestored)
+ ui.drawDialog(output, nullptr, "default values", "restored", -1);
+ fade.apply(output);
+ g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
+ g_system->updateScreen();
+ }
+
+ background.free();
+ _settings.save();
+ ConfMan.flushToDisk();
+}
+
+Rebel2PSX::MenuResult Rebel2PSX::runMainMenu(const RA2PSXMainMenuUI &ui,
+ const RA2PSXOptionsUI &options) {
+ enum Action {
+ kActionNone,
+ kActionStart,
+ kActionOptions
+ };
+
+ RA2PSXTinyGLRenderer renderer;
+ if (!renderer.init(_vm->_screenWidth, _vm->_screenHeight))
+ return kMenuQuit;
+
+ Graphics::Surface background;
+ background.create(_vm->_screenWidth, _vm->_screenHeight, g_system->getScreenFormat());
+ background.fillRect(Common::Rect(background.w, background.h), 0);
+ ui.drawBackground(background);
+
+ RA2PSXSoundPlayer sound(_vm, _soundBank);
+ const bool cursorWasVisible = CursorMan.isVisible();
+ CursorMan.showMouse(true);
+
+ MenuResult result = kMenuQuit;
+ Action action = kActionNone;
+ int selection = 0;
+ int logoAngle = 0;
+ int logicFrame = -1;
+ int inputDelay = 10;
+ RA2PSXMenuFade fade;
+ fade.fadeIn(0x10);
+ uint32 startTime = g_system->getMillis();
+
+ // Accumulates across iterations; a press between logic steps must not be lost.
+ RA2PSXMenuEvents events;
+ while (!_vm->shouldQuit()) {
+ pollMenuEvents(_vm, events);
+ if (_vm->shouldQuit())
break;
- if (openGlobalMenu) {
+ if (events.globalMenu) {
_vm->openMainMenuDialog();
- redraw = true;
+ _settings.load();
+ events = RA2PSXMenuEvents();
continue;
}
- if (openOptions) {
- GUI::ConfigDialog dialog;
- dialog.runModal();
- g_system->applyBackendSettings();
- _vm->syncSoundSettings();
- redraw = true;
+
+ const uint32 elapsed = g_system->getMillis() - startTime;
+ const int targetFrame = (int)((uint64)elapsed * kMenuFrameRate / 1000);
+ if (logicFrame >= targetFrame) {
+ g_system->delayMillis(5);
continue;
}
- if (redraw) {
- surface.fillRect(Common::Rect(surface.w, surface.h), 0);
- ui.draw(surface, selection);
- g_system->copyRectToScreen(surface.getPixels(), surface.pitch,
- 0, 0, surface.w, surface.h);
- g_system->updateScreen();
- redraw = false;
+ bool fadedOut = false;
+ while (logicFrame < targetFrame) {
+ ++logicFrame;
+ // A flat plate: sweeping edge on to edge on and snapping back reads as a spin.
+ logoAngle = (logoAngle + 0x14) & 0xffff;
+ if (inputDelay)
+ --inputDelay;
+ fadedOut |= fade.update();
}
- g_system->delayMillis(10);
+
+ sound.update();
+ if (fadedOut && action == kActionStart) {
+ result = kMenuStart;
+ break;
+ }
+ if (fadedOut && action == kActionOptions) {
+ runOptionsMenu(options, sound, renderer);
+ if (_vm->shouldQuit())
+ break;
+ action = kActionNone;
+ inputDelay = 0x1e;
+ logicFrame = -1;
+ startTime = g_system->getMillis();
+ fade.fadeIn(0x10);
+ }
+
+ if (action == kActionNone && !inputDelay) {
+ const int hit = hitTestMenu(RA2PSXMainMenuUI::itemRect, 2, background,
+ events.mouseX, events.mouseY);
+ if (events.mouseMoved && hit >= 0 && hit != selection) {
+ selection = hit;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (events.up || events.down) {
+ selection ^= 1;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+ if (events.accept || (events.mouseClicked && hit == selection)) {
+ action = selection == 0 ? kActionStart : kActionOptions;
+ sound.play(selection == 0 ? kMenuSfxStart : kMenuSfxEnter, 0x7f, 0x40);
+ fade.fadeOut(0x10);
+ }
+ }
+
+ events = RA2PSXMenuEvents();
+ renderer.beginFrame(background);
+ RA2PSXMatrix transform;
+ transform.setScale(0x1800, 0x1800, 0x1000);
+ transform.preRotateY((int16)((logoAngle & 0x7ff) - 0x400));
+ transform.setTranslation(0, 0, 0x604);
+ renderer.renderTransformedModel(_logoModel, transform, false);
+
+ Graphics::Surface output;
+ renderer.finishFrame(output);
+ ui.drawForeground(output, selection);
+ fade.apply(output);
+ g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
+ g_system->updateScreen();
}
- surface.free();
+ sound.stopAll();
+ background.free();
CursorMan.showMouse(cursorWasVisible);
return _vm->shouldQuit() ? kMenuQuit : result;
}
+#else
+
+Rebel2PSX::MenuResult Rebel2PSX::runMainMenu(const RA2PSXMainMenuUI &ui,
+ const RA2PSXOptionsUI &options) {
+ (void)ui;
+ (void)options;
+ return kMenuQuit;
+}
+
+#endif
+
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index 1ff211f14b6..a88d3af4bb9 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -35,6 +35,95 @@ static bool readU32(const Common::Array<byte> &data, uint32 offset, uint32 &valu
return true;
}
+// One turn is 4096 PlayStation angle units.
+static const float kRA2PSXAngleScale = 0.0015339807878856412f;
+
+void RA2PSXMatrix::setIdentity() {
+ for (uint row = 0; row < 3; ++row) {
+ for (uint column = 0; column < 3; ++column)
+ rotation[row][column] = row == column ? 1.0f : 0.0f;
+ translation[row] = 0.0f;
+ }
+}
+
+void RA2PSXMatrix::setScale(int x, int y, int z) {
+ setIdentity();
+ rotation[0][0] = x / 4096.0f;
+ rotation[1][1] = y / 4096.0f;
+ rotation[2][2] = z / 4096.0f;
+}
+
+void RA2PSXMatrix::setRotationX(int angle) {
+ const float cosine = cosf(angle * kRA2PSXAngleScale);
+ const float sine = sinf(angle * kRA2PSXAngleScale);
+ setIdentity();
+ rotation[1][1] = cosine;
+ rotation[1][2] = sine;
+ rotation[2][1] = -sine;
+ rotation[2][2] = cosine;
+}
+
+void RA2PSXMatrix::setRotationY(int angle) {
+ const float cosine = cosf(angle * kRA2PSXAngleScale);
+ const float sine = sinf(angle * kRA2PSXAngleScale);
+ setIdentity();
+ rotation[0][0] = cosine;
+ rotation[0][2] = -sine;
+ rotation[2][0] = sine;
+ rotation[2][2] = cosine;
+}
+
+void RA2PSXMatrix::setRotationZ(int angle) {
+ const float cosine = cosf(angle * kRA2PSXAngleScale);
+ const float sine = sinf(angle * kRA2PSXAngleScale);
+ setIdentity();
+ rotation[0][0] = cosine;
+ rotation[0][1] = sine;
+ rotation[1][0] = -sine;
+ rotation[1][1] = cosine;
+}
+
+static void preMultiply(RA2PSXMatrix &matrix, const RA2PSXMatrix &left) {
+ RA2PSXMatrix result;
+ for (uint row = 0; row < 3; ++row) {
+ for (uint column = 0; column < 3; ++column) {
+ float value = 0.0f;
+ for (uint i = 0; i < 3; ++i)
+ value += left.rotation[row][i] * matrix.rotation[i][column];
+ result.rotation[row][column] = value;
+ }
+ float value = left.translation[row];
+ for (uint i = 0; i < 3; ++i)
+ value += left.rotation[row][i] * matrix.translation[i];
+ result.translation[row] = value;
+ }
+ matrix = result;
+}
+
+void RA2PSXMatrix::preRotateX(int angle) {
+ RA2PSXMatrix rotate;
+ rotate.setRotationX(angle);
+ preMultiply(*this, rotate);
+}
+
+void RA2PSXMatrix::preRotateY(int angle) {
+ RA2PSXMatrix rotate;
+ rotate.setRotationY(angle);
+ preMultiply(*this, rotate);
+}
+
+void RA2PSXMatrix::preRotateZ(int angle) {
+ RA2PSXMatrix rotate;
+ rotate.setRotationZ(angle);
+ preMultiply(*this, rotate);
+}
+
+void RA2PSXMatrix::setTranslation(int x, int y, int z) {
+ translation[0] = (float)x;
+ translation[1] = (float)y;
+ translation[2] = (float)z;
+}
+
bool loadRA2PSXTextures(const Common::Array<byte> &data,
Common::Array<RA2PSXTexture> &textures) {
const uint initialCount = textures.size();
@@ -586,6 +675,27 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
const float modelYy = downY * cosine - rightY * sine;
const float modelYz = downZ * cosine - rightZ * sine;
+ RA2PSXMatrix transform;
+ transform.rotation[0][0] = modelXx;
+ transform.rotation[0][1] = modelYx;
+ transform.rotation[0][2] = forwardX;
+ transform.rotation[1][0] = modelXy;
+ transform.rotation[1][1] = modelYy;
+ transform.rotation[1][2] = forwardY;
+ transform.rotation[2][0] = modelXz;
+ transform.rotation[2][1] = modelYz;
+ transform.rotation[2][2] = forwardZ;
+ transform.translation[0] = x;
+ transform.translation[1] = y;
+ transform.translation[2] = z;
+ renderTransformedModel(model, transform, depthTest);
+}
+
+void RA2PSXTinyGLRenderer::renderTransformedModel(const RA2PSXModel &model,
+ const RA2PSXMatrix &transform, bool depthTest) {
+ if (!_context || model.vertices().empty())
+ return;
+
struct ProjectedVertex {
float x;
float y;
@@ -599,9 +709,12 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
const float focalLength = _width * 2.0f;
for (uint i = 0; i < model.vertices().size(); ++i) {
const RA2PSXVertex &vertex = model.vertices()[i];
- const float worldX = x + modelXx * vertex.x + modelYx * vertex.y + forwardX * vertex.z;
- const float worldY = y + modelXy * vertex.x + modelYy * vertex.y + forwardY * vertex.z;
- const float worldZ = z + modelXz * vertex.x + modelYz * vertex.y + forwardZ * vertex.z;
+ const float worldX = transform.translation[0] + transform.rotation[0][0] * vertex.x +
+ transform.rotation[0][1] * vertex.y + transform.rotation[0][2] * vertex.z;
+ const float worldY = transform.translation[1] + transform.rotation[1][0] * vertex.x +
+ transform.rotation[1][1] * vertex.y + transform.rotation[1][2] * vertex.z;
+ const float worldZ = transform.translation[2] + transform.rotation[2][0] * vertex.x +
+ transform.rotation[2][1] * vertex.y + transform.rotation[2][2] * vertex.z;
projected[i].z = worldZ;
projected[i].visible = worldZ > 1.0f;
if (projected[i].visible) {
@@ -641,7 +754,8 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
for (uint faceIndex = 0; faceIndex < projectedFaces.size(); ++faceIndex) {
const ProjectedFace &projectedFace = projectedFaces[faceIndex];
const RA2PSXFace &face = faces[projectedFace.index];
- const float orderingDepth = (z - projectedFace.depth) * 512.0f / model.radius();
+ const float orderingDepth = (transform.translation[2] - projectedFace.depth) *
+ 512.0f / model.radius();
const RA2PSXTexture *texture = model.texture(face.texture);
setFaceState(model, face);
@@ -649,15 +763,15 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
tglBegin(face.vertexCount == 4 ? TGL_QUAD_STRIP : TGL_TRIANGLES);
for (uint vertexIndex = 0; vertexIndex < face.vertexCount; ++vertexIndex) {
setFaceColor(face, vertexIndex,
- modelXx * face.normalX[vertexIndex] +
- modelYx * face.normalY[vertexIndex] +
- forwardX * face.normalZ[vertexIndex],
- modelXy * face.normalX[vertexIndex] +
- modelYy * face.normalY[vertexIndex] +
- forwardY * face.normalZ[vertexIndex],
- modelXz * face.normalX[vertexIndex] +
- modelYz * face.normalY[vertexIndex] +
- forwardZ * face.normalZ[vertexIndex], faceDepth);
+ transform.rotation[0][0] * face.normalX[vertexIndex] +
+ transform.rotation[0][1] * face.normalY[vertexIndex] +
+ transform.rotation[0][2] * face.normalZ[vertexIndex],
+ transform.rotation[1][0] * face.normalX[vertexIndex] +
+ transform.rotation[1][1] * face.normalY[vertexIndex] +
+ transform.rotation[1][2] * face.normalZ[vertexIndex],
+ transform.rotation[2][0] * face.normalX[vertexIndex] +
+ transform.rotation[2][1] * face.normalY[vertexIndex] +
+ transform.rotation[2][2] * face.normalZ[vertexIndex], faceDepth);
if (texture)
tglTexCoord2f((face.u[vertexIndex] + 0.5f) / texture->width,
(face.v[vertexIndex] + 0.5f) / texture->height);
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 36265c007d2..185530431f8 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -152,6 +152,7 @@ bool Rebel2PSX::playVideo(const Common::Path &path, int discNumber, bool version
decoder.close();
return false;
}
+ decoder.setVolume(_settings.videoVolume());
decoder.start();
const bool cursorWasVisible = CursorMan.isVisible();
CursorMan.showMouse(false);
@@ -223,10 +224,22 @@ bool Rebel2PSX::loadGlobalAssets(RA2PSXMainMenuUI &menu) {
RA2PSXArchive archive;
const bool loaded = archive.load(*stream);
delete stream;
+ if (!loaded || !menu.load(archive))
+ return false;
+
+ Common::Array<byte> textureData;
+ Common::Array<byte> logoData;
+ Common::Array<byte> cloakData;
+ if (!archive.getMember("menuTex", textureData) ||
+ !archive.getMember("fOFS/Logo", logoData) || !_logoModel.load(logoData) ||
+ !_logoModel.loadTextures(textureData) ||
+ !archive.getMember("fOFS/Cloak", cloakData) || !_cloakModel.load(cloakData) ||
+ !_cloakModel.loadTextures(textureData))
+ return false;
+
Common::Array<byte> soundData;
Common::Array<byte> soundProjectData;
- return loaded && menu.load(archive) &&
- archive.getMember("SNDsmp", soundData) &&
+ return archive.getMember("SNDsmp", soundData) &&
archive.getMember("sNDdata", soundProjectData) &&
_soundBank.load(soundData, soundProjectData);
}
@@ -266,9 +279,11 @@ Common::Error Rebel2PSX::runGame() {
#ifdef USE_TINYGL
RA2PSXMainMenuUI menu;
RA2PSXMovieText movieText;
+ _settings.load();
if (!loadGlobalAssets(menu))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation menu resources"));
+ const RA2PSXOptionsUI options(menu.textures());
if (!loadMovieTextAssets(movieText))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation movie fonts"));
@@ -279,7 +294,7 @@ Common::Error Rebel2PSX::runGame() {
_("Could not play the PlayStation introduction"));
}
- const MenuResult menuResult = runMainMenu(menu);
+ const MenuResult menuResult = runMainMenu(menu, options);
if (menuResult == kMenuQuit)
return Common::kNoError;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 3f09bb80398..3fc44a29645 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -39,6 +39,7 @@ class ScummEngine_v7;
class RA2PSXLevel1UI;
class RA2PSXMainMenuUI;
class RA2PSXMovieText;
+class RA2PSXOptionsUI;
enum RA2PSXMovieTextSequence {
kRA2PSXMovieTextNone,
@@ -132,12 +133,55 @@ private:
Impl *_impl;
};
+// Options menu settings; sfx steps by 7 up to 0x70, the CD volumes by 0x200 up to 0x1000.
+struct RA2PSXSettings {
+ enum {
+ kSFXMaximum = 0x70,
+ kSFXStep = 7,
+ kCDMaximum = 0x1000,
+ kCDStep = 0x200
+ };
+
+ RA2PSXSettings() { reset(); }
+
+ void reset();
+ void load();
+ void save() const;
+ void apply(ScummEngine_v7 *vm) const;
+ byte videoVolume() const;
+
+ int difficulty;
+ int sfx;
+ int music;
+ int movies;
+ bool mono;
+};
+
struct RA2PSXVertex {
int16 x;
int16 y;
int16 z;
};
+// PlayStation matrix helpers: 4096 angle units per turn, 1/4096 fixed point scales,
+// and pre-rotating multiplies from the left.
+struct RA2PSXMatrix {
+ RA2PSXMatrix() { setIdentity(); }
+
+ void setIdentity();
+ void setScale(int x, int y, int z);
+ void setRotationX(int angle);
+ void setRotationY(int angle);
+ void setRotationZ(int angle);
+ void preRotateX(int angle);
+ void preRotateY(int angle);
+ void preRotateZ(int angle);
+ void setTranslation(int x, int y, int z);
+
+ float rotation[3][3];
+ float translation[3];
+};
+
struct RA2PSXTexture {
Common::String name;
uint16 width;
@@ -197,6 +241,8 @@ public:
void renderPerspectiveModel(const RA2PSXModel &model, float x, float y, float z,
float directionX, float directionY, float directionZ, float roll,
bool depthTest = true);
+ void renderTransformedModel(const RA2PSXModel &model, const RA2PSXMatrix &transform,
+ bool depthTest = true);
void finishFrame(Graphics::Surface &surface);
private:
@@ -248,7 +294,12 @@ private:
const RA2PSXMovieText *movieText = nullptr,
RA2PSXMovieTextSequence textSequence = kRA2PSXMovieTextNone);
bool playIntroSequence(const RA2PSXMovieText &movieText);
- MenuResult runMainMenu(const RA2PSXMainMenuUI &ui);
+ MenuResult runMainMenu(const RA2PSXMainMenuUI &ui, const RA2PSXOptionsUI &options);
+#ifdef USE_TINYGL
+ // TinyGL tracks one context, so the options screen borrows the title screen's renderer.
+ void runOptionsMenu(const RA2PSXOptionsUI &ui, RA2PSXSoundPlayer &sound,
+ RA2PSXTinyGLRenderer &renderer);
+#endif
bool loadGlobalAssets(RA2PSXMainMenuUI &menu);
bool loadMovieTextAssets(RA2PSXMovieText &movieText);
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
@@ -259,6 +310,10 @@ private:
ScummEngine_v7 *_vm;
RA2PSXSoundBank _soundBank;
+ RA2PSXSettings _settings;
+ // The crest on the title screen and the freighter behind the options list.
+ RA2PSXModel _logoModel;
+ RA2PSXModel _cloakModel;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index bc25de6c95b..1b8f87062fd 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -124,48 +124,122 @@ void RA2PSXTextureSet::draw(Graphics::Surface &surface, const char *name,
g = MIN(g, 0xff);
b = MIN(b, 0xff);
const int destX = x + sourceX - sourceLeft;
- if (blend == kBlendAdditive) {
+ if (blend != kBlendOpaque) {
byte destR, destG, destB;
surface.format.colorToRGB(surface.getPixel(destX, destY), destR, destG, destB);
- r = MIN<int>(0xff, r + destR);
- g = MIN<int>(0xff, g + destG);
- b = MIN<int>(0xff, b + destB);
+ if (blend == kBlendAdditive) {
+ r = MIN<int>(0xff, r + destR);
+ g = MIN<int>(0xff, g + destG);
+ b = MIN<int>(0xff, b + destB);
+ } else {
+ r = (r + destR) / 2;
+ g = (g + destG) / 2;
+ b = (b + destB) / 2;
+ }
}
surface.setPixel(destX, destY, surface.format.RGBToColor(r, g, b));
}
}
}
-void RA2PSXTextureSet::drawText(Graphics::Surface &surface, const char *font,
- const char *text, int x, int y) const {
- static const char glyphs[] = "abcdefghijklmnopqrstuvwxyz0123456789%-:.,+/C ";
- static const byte widths[] = {
- 6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
- 6, 6, 6, 6, 6, 6, 8, 6, 7, 6, 6, 4, 6, 6, 6, 6,
- 6, 6, 6, 6, 6, 6, 2, 2, 2, 6, 6, 8, 2
- };
- static_assert(ARRAYSIZE(glyphs) == ARRAYSIZE(widths) + 1,
- "RA2 PSX glyph widths do not match the font map");
+void RA2PSXTextureSet::drawShape(Graphics::Surface &surface, const char *name,
+ int x, int y, const Common::Rect &source, byte r, byte g, byte b) const {
+ const RA2PSXTexture *texture = find(name);
+ if (!texture)
+ return;
- for (; *text; ++text) {
- int glyph = -1;
- for (uint i = 0; i < ARRAYSIZE(widths); ++i) {
- if (*text == glyphs[i]) {
- glyph = i;
- break;
- }
+ const int sourceLeft = MAX<int>(0, source.left);
+ const int sourceTop = MAX<int>(0, source.top);
+ const int sourceRight = MIN<int>(texture->width, source.right);
+ const int sourceBottom = MIN<int>(texture->height, source.bottom);
+ const uint32 color = surface.format.RGBToColor(r, g, b);
+ for (int sourceY = sourceTop; sourceY < sourceBottom; ++sourceY) {
+ const int destY = y + sourceY - source.top;
+ if (destY < 0 || destY >= surface.h)
+ continue;
+ for (int sourceX = sourceLeft; sourceX < sourceRight; ++sourceX) {
+ const int destX = x + sourceX - source.left;
+ if (destX < 0 || destX >= surface.w)
+ continue;
+ if (texture->pixels[sourceY * texture->width + sourceX] & 0x01000000)
+ surface.setPixel(destX, destY, color);
}
+ }
+}
+
+void subtractRA2PSXRect(Graphics::Surface &surface, const Common::Rect &rect,
+ int r, int g, int b) {
+ const int left = MAX<int>(0, rect.left);
+ const int top = MAX<int>(0, rect.top);
+ const int right = MIN<int>(surface.w, rect.right);
+ const int bottom = MIN<int>(surface.h, rect.bottom);
+ for (int y = top; y < bottom; ++y) {
+ for (int x = left; x < right; ++x) {
+ byte destR, destG, destB;
+ surface.format.colorToRGB(surface.getPixel(x, y), destR, destG, destB);
+ surface.setPixel(x, y, surface.format.RGBToColor(MAX<int>(0, destR - r),
+ MAX<int>(0, destG - g), MAX<int>(0, destB - b)));
+ }
+ }
+}
+
+void drawRA2PSXGouraudLine(Graphics::Surface &surface, int x0, int y0, int x1, int y1,
+ const byte *from, const byte *to) {
+ const int steps = MAX(ABS(x1 - x0), ABS(y1 - y0));
+ for (int step = 0; step <= steps; ++step) {
+ const int x = steps ? x0 + (x1 - x0) * step / steps : x0;
+ const int y = steps ? y0 + (y1 - y0) * step / steps : y0;
+ if (x < 0 || x >= surface.w || y < 0 || y >= surface.h)
+ continue;
+ const int r = steps ? from[0] + (to[0] - from[0]) * step / steps : from[0];
+ const int g = steps ? from[1] + (to[1] - from[1]) * step / steps : from[1];
+ const int b = steps ? from[2] + (to[2] - from[2]) * step / steps : from[2];
+ surface.setPixel(x, y, surface.format.RGBToColor(r, g, b));
+ }
+}
+
+static const char kSmallGlyphs[] = "abcdefghijklmnopqrstuvwxyz0123456789%-:.?+/C ";
+static const byte kSmallWidths[] = {
+ 6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 8, 6, 7, 6, 6, 4, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 2, 2, 6, 6, 6, 8, 2
+};
+static_assert(ARRAYSIZE(kSmallGlyphs) == ARRAYSIZE(kSmallWidths) + 1,
+ "RA2 PSX glyph widths do not match the font map");
+
+static int findSmallGlyph(char character) {
+ for (uint i = 0; i < ARRAYSIZE(kSmallWidths); ++i) {
+ if (character == kSmallGlyphs[i])
+ return i;
+ }
+ return -1;
+}
+
+void RA2PSXTextureSet::drawText(Graphics::Surface &surface, const char *font,
+ const char *text, int x, int y, int brightness) const {
+ for (; *text; ++text) {
+ const int glyph = findSmallGlyph(*text);
if (glyph < 0)
continue;
const int sourceX = (glyph % 12) * 8;
const int sourceY = (glyph / 12) * 8;
draw(surface, font, x, y, Common::Rect(sourceX, sourceY,
- sourceX + widths[glyph], sourceY + 8));
- x += widths[glyph] + 2;
+ sourceX + kSmallWidths[glyph], sourceY + 8), brightness);
+ x += kSmallWidths[glyph] + 2;
}
}
+int RA2PSXTextureSet::measureText(const char *text) const {
+ int width = 0;
+ for (; *text; ++text) {
+ const int glyph = findSmallGlyph(*text);
+ if (glyph >= 0)
+ width += kSmallWidths[glyph] + 2;
+ }
+ return width ? width - 2 : 0;
+}
+
struct RA2PSXMovieFont {
const char *texture;
const char *characters;
@@ -350,6 +424,24 @@ static int measureMovieText(const RA2PSXMovieFont &font, const char *text,
return width ? width - spacing : 0;
}
+int RA2PSXTextureSet::measureHeadline(const char *text) const {
+ return measureMovieText(kMovieBigFont, text, 0xffff, 2);
+}
+
+void RA2PSXTextureSet::drawHeadline(Graphics::Surface &surface, const char *text,
+ int x, int y) const {
+ for (; *text; ++text) {
+ RA2PSXMovieGlyph glyph;
+ if (!findMovieGlyph(kMovieBigFont, *text, glyph))
+ continue;
+ const Common::Rect source(glyph.x, glyph.row * kMovieBigFont.rowStep,
+ glyph.x + glyph.width, glyph.row * kMovieBigFont.rowStep + kMovieBigFont.height);
+ drawShape(surface, "FNT24BIG", x + 1, y + 1, source, 0, 0, 0);
+ draw(surface, "FNT24BIG", x, y, source);
+ x += glyph.width + 2;
+ }
+}
+
static int scaleMovieX(int x) {
const int remainder = x % 3;
if (remainder)
@@ -445,20 +537,37 @@ void RA2PSXMovieText::draw(Graphics::Surface &surface,
}
bool RA2PSXMainMenuUI::load(const RA2PSXArchive &archive) {
+ static const char *const required[] = {
+ "BACK_L", "BACK_R", "TITLE", "FNT24BIG", "VOLUMES",
+ "STD_FT2", "STD_FT3", "STD_FT4", "STD_FT5", "STD_FT6"
+ };
+
Common::Array<byte> data;
_textures.clear();
- return archive.getMember("menuTex", data) && _textures.append(data) &&
- _textures.has("BACK_L") && _textures.has("BACK_R") &&
- _textures.has("TITLE") && _textures.has("STD_FT2") &&
- _textures.has("STD_FT4") && _textures.has("STD_FT6");
+ if (!archive.getMember("menuTex", data) || !_textures.append(data))
+ return false;
+ for (uint i = 0; i < ARRAYSIZE(required); ++i) {
+ if (!_textures.has(required[i]))
+ return false;
+ }
+ return true;
+}
+
+void RA2PSXMainMenuUI::drawBackground(Graphics::Surface &surface) const {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ // The original dims both halves of the backdrop to 0x60/0x80.
+ _textures.draw(surface, "BACK_L", xOffset, yOffset, Common::Rect(0, 0, 224, 240), 0x60);
+ _textures.draw(surface, "BACK_R", xOffset + 224, yOffset, Common::Rect(0, 0, 96, 240), 0x60);
}
-void RA2PSXMainMenuUI::draw(Graphics::Surface &surface, int selection) const {
+void RA2PSXMainMenuUI::drawForeground(Graphics::Surface &surface, int selection) const {
const int xOffset = (surface.w - 320) / 2;
const int yOffset = (surface.h - 240) / 2;
- _textures.draw(surface, "BACK_L", xOffset, yOffset, Common::Rect(0, 0, 224, 240));
- _textures.draw(surface, "BACK_R", xOffset + 224, yOffset, Common::Rect(0, 0, 96, 240));
- _textures.draw(surface, "TITLE", xOffset + 72, yOffset + 22, Common::Rect(0, 0, 176, 124));
+ subtractRA2PSXRect(surface, Common::Rect(xOffset + 127, yOffset + 161,
+ xOffset + 193, yOffset + 191), 0x46, 0x46, 0x46);
+ _textures.draw(surface, "TITLE", xOffset + 72, yOffset + 22, Common::Rect(0, 0, 176, 124),
+ 0x80, RA2PSXTextureSet::kBlendAverage);
static const char *const items[] = { "start", "options" };
static const int itemX[] = { 141, 134 };
@@ -472,10 +581,199 @@ void RA2PSXMainMenuUI::draw(Graphics::Surface &surface, int selection) const {
xOffset + 16, yOffset + 212);
}
-Common::Rect RA2PSXMainMenuUI::itemRect(int item) const {
+Common::Rect RA2PSXMainMenuUI::itemRect(int item) {
return Common::Rect(120, 164 + item * 10, 200, 174 + item * 10);
}
+// Row positions from the original widget tables.
+static const int16 kOptionRowY[] = { 70, 85, 100, 115, 130, 145, 160, 190 };
+static const int16 kSoundRowY[] = { 87, 102, 117, 132, 162 };
+
+Common::Rect RA2PSXOptionsUI::mainItemRect(int item) {
+ const int y = kOptionRowY[CLIP<int>(item, 0, ARRAYSIZE(kOptionRowY) - 1)];
+ return Common::Rect(90, y - 1, 230, y + 9);
+}
+
+Common::Rect RA2PSXOptionsUI::soundItemRect(int item) {
+ const int y = kSoundRowY[CLIP<int>(item, 0, ARRAYSIZE(kSoundRowY) - 1)];
+ return Common::Rect(90, y - 1, 230, y + 9);
+}
+
+enum {
+ kOptionStateNormal,
+ kOptionStateValue,
+ kOptionStateSelected,
+ // Ours, not the original's: the plain colour dimmed.
+ kOptionStateUnavailable
+};
+
+bool RA2PSXOptionsUI::isItemAvailable(int item) {
+ switch (item) {
+ case kItemDifficulty:
+ case kItemAdjustSound:
+ case kItemResetSettings:
+ case kItemExit:
+ return true;
+ default:
+ // These four still need their own screens.
+ return false;
+ }
+}
+
+void RA2PSXOptionsUI::drawItem(Graphics::Surface &surface, const char *text, int x, int y,
+ bool centered, int state) const {
+ static const char *const fonts[] = { "STD_FT3", "STD_FT2", "STD_FT4", "STD_FT3" };
+ static const int brightness[] = { 100, 0x80, 0x80, 0x30 };
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ if (centered)
+ x = (320 - _textures.measureText(text)) / 2;
+ _textures.drawText(surface, fonts[state], text, xOffset + x, yOffset + y,
+ brightness[state]);
+}
+
+void RA2PSXOptionsUI::drawMain(Graphics::Surface &surface, int selection,
+ const RA2PSXSettings &settings) const {
+ struct Entry {
+ const char *text;
+ int16 x;
+ bool centered;
+ };
+ static const Entry entries[kItemCount] = {
+ { "difficulty:", 96, false },
+ { "adjust sound", 70, true },
+ { "enter passcode", 70, true },
+ { "memory card", 70, true },
+ { "high scores", 70, true },
+ { "controls", 70, true },
+ { "reset settings", 70, true },
+ { "exit", 70, true }
+ };
+ static const Entry difficulties[] = {
+ { "easy", 182, false },
+ { "medium", 176, false },
+ { "hard", 182, false }
+ };
+
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ subtractRA2PSXRect(surface, Common::Rect(xOffset + 90, yOffset + 63,
+ xOffset + 230, yOffset + 203), 0x20, 0x20, 0x20);
+ _textures.drawHeadline(surface, "Options",
+ xOffset + (320 - _textures.measureHeadline("Options")) / 2, yOffset + 36);
+
+ for (int item = 0; item < kItemCount; ++item) {
+ int state = kOptionStateNormal;
+ if (item == selection)
+ state = kOptionStateSelected;
+ else if (!isItemAvailable(item))
+ state = kOptionStateUnavailable;
+ drawItem(surface, entries[item].text, entries[item].x, kOptionRowY[item],
+ entries[item].centered, state);
+ }
+
+ // The value dims while the difficulty row itself is highlighted.
+ const Entry &value = difficulties[CLIP<int>(settings.difficulty, 0, 2)];
+ drawItem(surface, value.text, value.x, kOptionRowY[kItemDifficulty], false,
+ selection == kItemDifficulty ? kOptionStateValue : kOptionStateNormal);
+}
+
+void RA2PSXOptionsUI::drawVolume(Graphics::Surface &surface, int y, int level) const {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ const int filled = CLIP(level, 0, 8) * 4;
+ if (filled)
+ _textures.draw(surface, "VOLUMES", xOffset + 180, yOffset + y,
+ Common::Rect(0, 0, filled, 8), 0x60);
+ if (filled < 32)
+ _textures.draw(surface, "VOLUMES", xOffset + 180 + filled, yOffset + y,
+ Common::Rect(filled, 8, 32, 16), 0x32);
+}
+
+void RA2PSXOptionsUI::drawSound(Graphics::Surface &surface, int selection,
+ const RA2PSXSettings &settings) const {
+ static const char *const labels[kSoundItemCount] = {
+ "sound mode:", "game f/x:", "game music:", "movies:", "exit"
+ };
+ static const int16 labelX[kSoundItemCount] = { 94, 110, 96, 126, 0 };
+
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ subtractRA2PSXRect(surface, Common::Rect(xOffset + 90, yOffset + 83,
+ xOffset + 230, yOffset + 173), 0x20, 0x20, 0x20);
+ _textures.drawHeadline(surface, "Adjust Sound",
+ xOffset + (320 - _textures.measureHeadline("Adjust Sound")) / 2, yOffset + 37);
+
+ for (int item = 0; item < kSoundItemCount; ++item) {
+ drawItem(surface, labels[item], labelX[item], kSoundRowY[item],
+ item == kSoundItemExit, item == selection ?
+ kOptionStateSelected : kOptionStateNormal);
+ }
+
+ drawItem(surface, settings.mono ? "mono" : "stereo", settings.mono ? 186 : 179, 87,
+ false, selection == kSoundItemMode ? kOptionStateValue : kOptionStateNormal);
+ drawVolume(surface, 102, (settings.sfx + 1) / 14);
+ drawVolume(surface, 117, settings.music / RA2PSXSettings::kCDStep);
+ drawVolume(surface, 132, settings.movies / RA2PSXSettings::kCDStep);
+}
+
+void RA2PSXOptionsUI::drawDialog(Graphics::Surface &surface, const char *headline,
+ const char *question, const char *detail, int selection) const {
+ static const byte kBright[3] = { 0xfc, 0xc8, 0x19 };
+ static const byte kDark[3] = { 0x80, 0x65, 0x0c };
+ static const byte kMessageBright[3] = { 0xff, 0xff, 0xff };
+ static const byte kMessageDark[3] = { 0x80, 0x80, 0x80 };
+
+ const bool message = headline == nullptr;
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ const int height = message ? 0x20 : 0x40;
+ int width = MAX(_textures.measureText(question), detail ? _textures.measureText(detail) : 0);
+ if (!message)
+ width = MAX(width, _textures.measureText(headline));
+ const int left = (320 - width) / 2;
+ const int top = (240 - height) / 2;
+
+ const Common::Rect box(xOffset + left - 6, yOffset + top - 4,
+ xOffset + left - 6 + width + 12, yOffset + top - 4 + height + 8);
+ subtractRA2PSXRect(surface, box, 0xff, 0xff, 0xff);
+
+ const byte *bright = message ? kMessageBright : kBright;
+ const byte *dark = message ? kMessageDark : kDark;
+ const int midX = box.left + (width + 12) / 2;
+ const int midY = box.top + (height + 8) / 2;
+ drawRA2PSXGouraudLine(surface, box.left, box.top, midX, box.top, bright, dark);
+ drawRA2PSXGouraudLine(surface, midX, box.top, box.right, box.top, dark, bright);
+ drawRA2PSXGouraudLine(surface, box.right, box.top, box.right, midY, bright, dark);
+ drawRA2PSXGouraudLine(surface, box.right, midY, box.right, box.bottom, dark, bright);
+ drawRA2PSXGouraudLine(surface, box.right, box.bottom, midX, box.bottom, bright, dark);
+ drawRA2PSXGouraudLine(surface, midX, box.bottom, box.left, box.bottom, dark, bright);
+ drawRA2PSXGouraudLine(surface, box.left, box.bottom, box.left, midY, bright, dark);
+ drawRA2PSXGouraudLine(surface, box.left, midY, box.left, box.top, dark, bright);
+
+ const char *const bodyFont = message ? "STD_FT3" : "STD_FT2";
+ const int questionY = message ? (detail ? 9 : 13) : (detail ? 0x14 : 0x19);
+ if (!message)
+ _textures.drawText(surface, "STD_FT5", headline,
+ xOffset + left + (width - _textures.measureText(headline)) / 2,
+ yOffset + top, 0x80);
+ _textures.drawText(surface, bodyFont, question,
+ xOffset + left + (width - _textures.measureText(question)) / 2,
+ yOffset + top + questionY, 0x80);
+ if (detail)
+ _textures.drawText(surface, bodyFont, detail,
+ xOffset + left + (width - _textures.measureText(detail)) / 2,
+ yOffset + top + (message ? 0x13 : 0x1e), 0x80);
+
+ if (selection >= 0) {
+ _textures.drawText(surface, selection == 1 ? "STD_FT4" : "STD_FT3", "yes",
+ xOffset + left + 10, yOffset + top + 0x32, selection == 1 ? 0x80 : 100);
+ _textures.drawText(surface, selection == 0 ? "STD_FT4" : "STD_FT3", "no",
+ xOffset + left + width - 0x1e, yOffset + top + 0x32,
+ selection == 0 ? 0x80 : 100);
+ }
+}
+
bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
Common::Array<byte> data;
_textures.clear();
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 04d23f629b7..43993e20f73 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -25,7 +25,9 @@ class RA2PSXTextureSet {
public:
enum BlendMode {
kBlendOpaque,
- kBlendAdditive
+ kBlendAdditive,
+ // The GPU's semi-transparency mode 0: half background, half source.
+ kBlendAverage
};
void clear() { _textures.clear(); }
@@ -37,13 +39,25 @@ public:
const Common::Rect &source, int brightness = 0x80,
BlendMode blend = kBlendOpaque) const;
void drawText(Graphics::Surface &surface, const char *font, const char *text,
- int x, int y) const;
+ int x, int y, int brightness = 0x80) const;
+ int measureText(const char *text) const;
+ // The 16 pixel tall menu headline font, drawn over a black drop shadow.
+ void drawHeadline(Graphics::Surface &surface, const char *text, int x, int y) const;
+ int measureHeadline(const char *text) const;
private:
const RA2PSXTexture *find(const char *name) const;
+ void drawShape(Graphics::Surface &surface, const char *name, int x, int y,
+ const Common::Rect &source, byte r, byte g, byte b) const;
Common::Array<RA2PSXTexture> _textures;
};
+// The GPU's semi-transparency mode 2: background minus source.
+void subtractRA2PSXRect(Graphics::Surface &surface, const Common::Rect &rect,
+ int r, int g, int b);
+void drawRA2PSXGouraudLine(Graphics::Surface &surface, int x0, int y0, int x1, int y1,
+ const byte *from, const byte *to);
+
class RA2PSXMovieText {
public:
bool load(Common::SeekableReadStream &executable);
@@ -57,13 +71,62 @@ private:
class RA2PSXMainMenuUI {
public:
bool load(const RA2PSXArchive &archive);
- void draw(Graphics::Surface &surface, int selection) const;
- Common::Rect itemRect(int item) const;
+ // The spinning crest is drawn between these two passes.
+ void drawBackground(Graphics::Surface &surface) const;
+ void drawForeground(Graphics::Surface &surface, int selection) const;
+ static Common::Rect itemRect(int item);
+ const RA2PSXTextureSet &textures() const { return _textures; }
private:
RA2PSXTextureSet _textures;
};
+// Options menu; passcode, memory card, high scores and controls are dimmed for now.
+class RA2PSXOptionsUI {
+public:
+ enum Item {
+ kItemDifficulty,
+ kItemAdjustSound,
+ kItemPasscode,
+ kItemMemoryCard,
+ kItemHighScores,
+ kItemControls,
+ kItemResetSettings,
+ kItemExit,
+ kItemCount
+ };
+
+ enum SoundItem {
+ kSoundItemMode,
+ kSoundItemEffects,
+ kSoundItemMusic,
+ kSoundItemMovies,
+ kSoundItemExit,
+ kSoundItemCount
+ };
+
+ explicit RA2PSXOptionsUI(const RA2PSXTextureSet &textures) : _textures(textures) {}
+
+ static bool isItemAvailable(int item);
+ static Common::Rect mainItemRect(int item);
+ static Common::Rect soundItemRect(int item);
+
+ void drawMain(Graphics::Surface &surface, int selection,
+ const RA2PSXSettings &settings) const;
+ void drawSound(Graphics::Surface &surface, int selection,
+ const RA2PSXSettings &settings) const;
+ // A two or three line box; pass a negative selection for a plain message.
+ void drawDialog(Graphics::Surface &surface, const char *headline,
+ const char *question, const char *detail, int selection) const;
+
+private:
+ void drawItem(Graphics::Surface &surface, const char *text, int x, int y,
+ bool centered, int state) const;
+ void drawVolume(Graphics::Surface &surface, int y, int level) const;
+
+ const RA2PSXTextureSet &_textures;
+};
+
class RA2PSXLevel1UI {
public:
bool load(const RA2PSXArchive &archive);
Commit: 8d355b9ef7b2beeaf45aa9de2f517d19016ac152
https://github.com/scummvm/scummvm/commit/8d355b9ef7b2beeaf45aa9de2f517d19016ac152
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: implementation of the level select menu for psx release
Changed paths:
engines/scumm/insane/rebel2/psx/menu.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/menu.cpp b/engines/scumm/insane/rebel2/psx/menu.cpp
index 1d720d9cf1e..15bc1601ffa 100644
--- a/engines/scumm/insane/rebel2/psx/menu.cpp
+++ b/engines/scumm/insane/rebel2/psx/menu.cpp
@@ -30,6 +30,7 @@
#include "scumm/insane/rebel2/shared.h"
#include "scumm/insane/rebel2/psx/psx.h"
#include "scumm/insane/rebel2/psx/ui.h"
+#include "scumm/insane/rebel2/psx/video.h"
namespace Scumm {
@@ -43,6 +44,7 @@ enum {
kMenuSfxEnter = 0x1f,
kMenuSfxLeave = 0x20,
kMenuSfxConfirm = 0x40,
+ kMenuSfxLocked = 0x21,
kMenuSfxStart = 0x44,
kMenuSfxDefaults = 0x45
};
@@ -53,6 +55,9 @@ void RA2PSXSettings::reset() {
music = 0xc00;
movies = 0xc00;
mono = false;
+ for (int i = 0; i < ARRAYSIZE(unlocked); ++i)
+ unlocked[i] = 1;
+ unlockAll = false;
}
void RA2PSXSettings::load() {
@@ -70,6 +75,12 @@ void RA2PSXSettings::load() {
if (ConfMan.hasKey("speech_volume"))
movies = CLIP(ConfMan.getInt("speech_volume"), 0, 255) * kCDMaximum / 255 /
kCDStep * kCDStep;
+ unlockAll = ConfMan.getBool("rebel2_unlock_all");
+ for (int i = 0; i < ARRAYSIZE(unlocked); ++i) {
+ const Common::String key = Common::String::format("rebel2_psx_chapters%d", i);
+ if (ConfMan.hasKey(key))
+ unlocked[i] = CLIP(ConfMan.getInt(key), 1, 16);
+ }
}
void RA2PSXSettings::save() const {
@@ -78,6 +89,8 @@ void RA2PSXSettings::save() const {
ConfMan.setInt("sfx_volume", sfx * 255 / kSFXMaximum);
ConfMan.setInt("music_volume", MIN<int>(255, music * 255 / kCDMaximum));
ConfMan.setInt("speech_volume", MIN<int>(255, movies * 255 / kCDMaximum));
+ for (int i = 0; i < ARRAYSIZE(unlocked); ++i)
+ ConfMan.setInt(Common::String::format("rebel2_psx_chapters%d", i), unlocked[i]);
}
void RA2PSXSettings::apply(ScummEngine_v7 *vm) const {
@@ -85,6 +98,10 @@ void RA2PSXSettings::apply(ScummEngine_v7 *vm) const {
vm->syncSoundSettings();
}
+int RA2PSXSettings::unlockedChapters() const {
+ return unlockAll ? 16 : CLIP<int>(unlocked[CLIP(difficulty, 0, 2)], 1, 16);
+}
+
byte RA2PSXSettings::videoVolume() const {
return (byte)CLIP<int>(movies * 255 / kCDMaximum, 0, 255);
}
@@ -463,6 +480,198 @@ void Rebel2PSX::runOptionsMenu(const RA2PSXOptionsUI &ui, RA2PSXSoundPlayer &sou
ConfMan.flushToDisk();
}
+int Rebel2PSX::runChapterSelect(const RA2PSXChapterSelectUI &ui) {
+ RA2PSXTinyGLRenderer renderer;
+ if (!renderer.init(_vm->_screenWidth, _vm->_screenHeight))
+ return 0;
+
+ // The tile previews are frames of LEVELSEL.STR, looped for as long as the
+ // screen is up; without them the tiles stay empty.
+ Common::SeekableReadStream *stream = openRawFile("LEVELSEL.STR", 1);
+ RA2PSXStreamDecoder decoder(RA2PSXStreamDecoder::kVersion2);
+ bool previewsReady = stream && decoder.loadStream(stream) &&
+ decoder.setOutputPixelFormat(g_system->getScreenFormat());
+ if (previewsReady) {
+ decoder.setVolume(_settings.videoVolume());
+ decoder.start();
+ } else {
+ decoder.close();
+ }
+
+ Graphics::Surface background;
+ background.create(_vm->_screenWidth, _vm->_screenHeight, g_system->getScreenFormat());
+ background.fillRect(Common::Rect(background.w, background.h), 0);
+
+ RA2PSXSoundPlayer sound(_vm, _soundBank);
+ const bool cursorWasVisible = CursorMan.isVisible();
+ CursorMan.showMouse(true);
+
+ const int unlocked = _settings.unlockedChapters();
+ // Kept in our own surface so it survives restarting the stream.
+ Graphics::Surface previews;
+ bool havePreviews = false;
+ int selection = 0;
+ int chosen = 0;
+ int scroll = 0;
+ int target = 0;
+ int velocity = 0;
+ int direction = 1;
+ int cloakAngle = 0x800;
+ int crestAngle = 0;
+ int logicFrame = -1;
+ bool leaving = false;
+ RA2PSXMenuFade fade;
+ fade.fadeIn(8);
+
+ // Accumulates across iterations; a press between logic steps must not be lost.
+ RA2PSXMenuEvents events;
+ const uint32 startTime = g_system->getMillis();
+ while (!_vm->shouldQuit()) {
+ pollMenuEvents(_vm, events);
+ if (_vm->shouldQuit())
+ break;
+ if (events.globalMenu) {
+ _vm->openMainMenuDialog();
+ events = RA2PSXMenuEvents();
+ continue;
+ }
+
+ if (previewsReady) {
+ while (decoder.needsUpdate()) {
+ const Graphics::Surface *frame = decoder.decodeNextFrame();
+ if (!frame || frame->w < 320 || frame->h < 240)
+ break;
+ if (!previews.getPixels())
+ previews.create(320, 240, frame->format);
+ previews.copyRectToSurface(*frame, 0, 0, Common::Rect(320, 240));
+ havePreviews = true;
+ }
+ // The clip only runs for about twelve seconds, so start it over.
+ if (decoder.endOfVideo()) {
+ decoder.close();
+ stream = openRawFile("LEVELSEL.STR", 1);
+ previewsReady = stream && decoder.loadStream(stream) &&
+ decoder.setOutputPixelFormat(g_system->getScreenFormat());
+ if (previewsReady) {
+ decoder.setVolume(_settings.videoVolume());
+ decoder.start();
+ } else {
+ decoder.close();
+ }
+ }
+ }
+
+ const uint32 elapsed = g_system->getMillis() - startTime;
+ const int targetFrame = (int)((uint64)elapsed * kMenuFrameRate / 1000);
+ if (logicFrame >= targetFrame) {
+ g_system->delayMillis(5);
+ continue;
+ }
+
+ bool fadedOut = false;
+ while (logicFrame < targetFrame) {
+ ++logicFrame;
+ cloakAngle = (cloakAngle + 6) & 0xfff;
+ crestAngle = (crestAngle + 0x14) & 0xfff;
+ fadedOut |= fade.update();
+
+ // The list eases towards the selected row and never overshoots.
+ if (scroll != target) {
+ scroll += (MIN(velocity, 0x8000) >> 12) * direction;
+ if (direction > 0 ? scroll >= target : scroll <= target) {
+ scroll = target;
+ velocity = 0;
+ }
+ }
+ if (velocity) {
+ velocity = velocity * 0xe80 >> 12;
+ if (velocity < 0x1000)
+ velocity = 0x1000;
+ }
+ }
+ if (fadedOut && leaving)
+ break;
+
+ sound.update();
+ if (!fade.active) {
+ int mouseHit = -1;
+ if (events.mouseMoved) {
+ const int xOffset = (background.w - 320) / 2;
+ const int yOffset = (background.h - 240) / 2;
+ for (int chapter = 0; chapter < RA2PSXChapterSelectUI::kChapterCount; ++chapter) {
+ Common::Rect rect = RA2PSXChapterSelectUI::tileRect(chapter, scroll);
+ rect.translate(xOffset, yOffset);
+ if (rect.contains(events.mouseX, events.mouseY)) {
+ mouseHit = chapter;
+ break;
+ }
+ }
+ }
+
+ int step = 0;
+ if (events.down && selection < RA2PSXChapterSelectUI::kChapterCount - 1)
+ step = 1;
+ else if (events.up && selection > 0)
+ step = -1;
+ else if (mouseHit >= 0 && mouseHit != selection)
+ step = mouseHit > selection ? 1 : -1;
+ if (step) {
+ const int rows = mouseHit >= 0 && !events.up && !events.down ?
+ ABS(mouseHit - selection) : 1;
+ selection += step * rows;
+ target += step * rows * RA2PSXChapterSelectUI::kRowPitch;
+ direction = step;
+ velocity += 0x8000;
+ sound.play(kMenuSfxMove, 0x7f, 0x40);
+ }
+
+ const bool activate = events.accept ||
+ (events.mouseClicked && mouseHit == selection);
+ if (events.cancel) {
+ leaving = true;
+ sound.play(kMenuSfxLeave, 0x7f, 0x40);
+ fade.fadeOut(8);
+ } else if (activate) {
+ if (selection >= unlocked) {
+ // The original just refuses a locked chapter with a buzz.
+ sound.play(kMenuSfxLocked, 0x7f, 0x40);
+ } else {
+ chosen = selection + 1;
+ leaving = true;
+ sound.play(kMenuSfxStart, 0x7f, 0x40);
+ fade.fadeOut(8);
+ }
+ }
+ }
+
+ events = RA2PSXMenuEvents();
+ renderer.beginFrame(background);
+ RA2PSXMatrix transform;
+ transform.setRotationZ(-0x100);
+ transform.preRotateY(cloakAngle);
+ transform.setTranslation(0, 0, 0x604);
+ renderer.renderTransformedModel(_cloakModel, transform, false);
+ transform.setScale(0x6f5, 0x6f5, 0x6f5);
+ transform.preRotateY(crestAngle);
+ transform.setTranslation(-0x120, -0xd6, 0x604);
+ renderer.renderTransformedModel(_crestModel, transform, false);
+
+ Graphics::Surface output;
+ renderer.finishFrame(output);
+ ui.draw(output, havePreviews ? &previews : nullptr, scroll, selection, unlocked);
+ fade.apply(output);
+ g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
+ g_system->updateScreen();
+ }
+
+ sound.stopAll();
+ decoder.close();
+ previews.free();
+ background.free();
+ CursorMan.showMouse(cursorWasVisible);
+ return _vm->shouldQuit() ? 0 : chosen;
+}
+
Rebel2PSX::MenuResult Rebel2PSX::runMainMenu(const RA2PSXMainMenuUI &ui,
const RA2PSXOptionsUI &options) {
enum Action {
@@ -589,6 +798,11 @@ Rebel2PSX::MenuResult Rebel2PSX::runMainMenu(const RA2PSXMainMenuUI &ui,
return kMenuQuit;
}
+int Rebel2PSX::runChapterSelect(const RA2PSXChapterSelectUI &ui) {
+ (void)ui;
+ return 0;
+}
+
#endif
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 185530431f8..be7f11be0ff 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -230,11 +230,13 @@ bool Rebel2PSX::loadGlobalAssets(RA2PSXMainMenuUI &menu) {
Common::Array<byte> textureData;
Common::Array<byte> logoData;
Common::Array<byte> cloakData;
+ Common::Array<byte> crestData;
if (!archive.getMember("menuTex", textureData) ||
!archive.getMember("fOFS/Logo", logoData) || !_logoModel.load(logoData) ||
!_logoModel.loadTextures(textureData) ||
!archive.getMember("fOFS/Cloak", cloakData) || !_cloakModel.load(cloakData) ||
- !_cloakModel.loadTextures(textureData))
+ !_cloakModel.loadTextures(textureData) ||
+ !archive.getMember("fOFS/LogoDbl", crestData) || !_crestModel.load(crestData))
return false;
Common::Array<byte> soundData;
@@ -284,6 +286,7 @@ Common::Error Rebel2PSX::runGame() {
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation menu resources"));
const RA2PSXOptionsUI options(menu.textures());
+ const RA2PSXChapterSelectUI chapters(menu.textures());
if (!loadMovieTextAssets(movieText))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation movie fonts"));
@@ -294,8 +297,25 @@ Common::Error Rebel2PSX::runGame() {
_("Could not play the PlayStation introduction"));
}
- const MenuResult menuResult = runMainMenu(menu, options);
- if (menuResult == kMenuQuit)
+ int chapter = 0;
+ while (!chapter && !_vm->shouldQuit()) {
+ if (runMainMenu(menu, options) == kMenuQuit)
+ return Common::kNoError;
+
+ bool backedOut = false;
+ while (!chapter && !backedOut && !_vm->shouldQuit()) {
+ const int picked = runChapterSelect(chapters);
+ if (!picked)
+ backedOut = true;
+ else if (picked > 1)
+ // Only chapter 1 is ported; stay on the list rather than
+ // pretending to start the rest.
+ warning("Rebel Assault II: chapter %d is not implemented yet", picked);
+ else
+ chapter = picked;
+ }
+ }
+ if (_vm->shouldQuit())
return Common::kNoError;
RA2PSXModel enemy;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 3fc44a29645..24f003d10af 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -40,6 +40,7 @@ class RA2PSXLevel1UI;
class RA2PSXMainMenuUI;
class RA2PSXMovieText;
class RA2PSXOptionsUI;
+class RA2PSXChapterSelectUI;
enum RA2PSXMovieTextSequence {
kRA2PSXMovieTextNone,
@@ -149,12 +150,16 @@ struct RA2PSXSettings {
void save() const;
void apply(ScummEngine_v7 *vm) const;
byte videoVolume() const;
+ // The original keeps a per-difficulty count of the chapters reached.
+ int unlockedChapters() const;
int difficulty;
int sfx;
int music;
int movies;
bool mono;
+ int unlocked[3];
+ bool unlockAll;
};
struct RA2PSXVertex {
@@ -300,6 +305,8 @@ private:
void runOptionsMenu(const RA2PSXOptionsUI &ui, RA2PSXSoundPlayer &sound,
RA2PSXTinyGLRenderer &renderer);
#endif
+ // Returns the chosen chapter, 1 to 16, or 0 when the player backs out.
+ int runChapterSelect(const RA2PSXChapterSelectUI &ui);
bool loadGlobalAssets(RA2PSXMainMenuUI &menu);
bool loadMovieTextAssets(RA2PSXMovieText &movieText);
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
@@ -311,9 +318,11 @@ private:
ScummEngine_v7 *_vm;
RA2PSXSoundBank _soundBank;
RA2PSXSettings _settings;
- // The crest on the title screen and the freighter behind the options list.
+ // The crest on the title screen, the freighter behind the options list and
+ // the double sided crest on the chapter select.
RA2PSXModel _logoModel;
RA2PSXModel _cloakModel;
+ RA2PSXModel _crestModel;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index 1b8f87062fd..e22b4617596 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -198,6 +198,13 @@ void drawRA2PSXGouraudLine(Graphics::Surface &surface, int x0, int y0, int x1, i
}
}
+void drawRA2PSXMenuHints(Graphics::Surface &surface, const RA2PSXTextureSet &textures) {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ textures.draw(surface, "BACK", xOffset + 20, yOffset + 216, Common::Rect(0, 0, 46, 11));
+ textures.draw(surface, "SELECT", xOffset + 252, yOffset + 216, Common::Rect(0, 0, 56, 11));
+}
+
static const char kSmallGlyphs[] = "abcdefghijklmnopqrstuvwxyz0123456789%-:.?+/C ";
static const byte kSmallWidths[] = {
6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
@@ -538,7 +545,7 @@ void RA2PSXMovieText::draw(Graphics::Surface &surface,
bool RA2PSXMainMenuUI::load(const RA2PSXArchive &archive) {
static const char *const required[] = {
- "BACK_L", "BACK_R", "TITLE", "FNT24BIG", "VOLUMES",
+ "BACK_L", "BACK_R", "TITLE", "FNT24BIG", "VOLUMES", "BACK", "SELECT",
"STD_FT2", "STD_FT3", "STD_FT4", "STD_FT5", "STD_FT6"
};
@@ -672,6 +679,8 @@ void RA2PSXOptionsUI::drawMain(Graphics::Surface &surface, int selection,
entries[item].centered, state);
}
+ drawRA2PSXMenuHints(surface, _textures);
+
// The value dims while the difficulty row itself is highlighted.
const Entry &value = difficulties[CLIP<int>(settings.difficulty, 0, 2)];
drawItem(surface, value.text, value.x, kOptionRowY[kItemDifficulty], false,
@@ -712,6 +721,7 @@ void RA2PSXOptionsUI::drawSound(Graphics::Surface &surface, int selection,
drawItem(surface, settings.mono ? "mono" : "stereo", settings.mono ? 186 : 179, 87,
false, selection == kSoundItemMode ? kOptionStateValue : kOptionStateNormal);
+ drawRA2PSXMenuHints(surface, _textures);
drawVolume(surface, 102, (settings.sfx + 1) / 14);
drawVolume(surface, 117, settings.music / RA2PSXSettings::kCDStep);
drawVolume(surface, 132, settings.movies / RA2PSXSettings::kCDStep);
@@ -774,6 +784,137 @@ void RA2PSXOptionsUI::drawDialog(Graphics::Surface &surface, const char *headlin
}
}
+// Row positions and label offsets from the original chapter table.
+struct RA2PSXChapterEntry {
+ const char *title;
+ int16 titleX;
+ const char *name;
+ int16 nameX;
+ int16 barWidth;
+};
+
+static const RA2PSXChapterEntry kChapters[RA2PSXChapterSelectUI::kChapterCount] = {
+ { "chapter 1:", 142, "the dreighton triangle", 52, 158 },
+ { "chapter 2:", 110, "the corellia star", 110, 122 },
+ { "chapter 3:", 140, "mining tunnels", 110, 100 },
+ { "chapter 4:", 110, "the mine field", 110, 96 },
+ { "chapter 5:", 140, "interceptor attack", 76, 134 },
+ { "chapter 6:", 110, "the mining facility", 110, 128 },
+ { "chapter 7:", 140, "tie training", 132, 78 },
+ { "chapter 8:", 110, "flight to imdaar", 110, 112 },
+ { "chapter 9:", 140, "the asteroid field", 84, 126 },
+ { "chapter 10:", 110, "speeder bikes", 110, 94 },
+ { "chapter 11:", 136, "aboard the terror", 84, 126 },
+ { "chapter 12:", 110, "the sewer", 110, 76 },
+ { "chapter 13:", 134, "escaping the star destroyer", 12, 198 },
+ { "chapter 14:", 110, "tie attack", 110, 78 },
+ { "chapter 15:", 134, "imdaar alpha", 122, 88 },
+ { "finale:", 110, "the return home", 110, 112 }
+};
+
+int RA2PSXChapterSelectUI::rowY(int chapter, int scroll) {
+ return 91 - scroll + chapter * kRowPitch;
+}
+
+Common::Rect RA2PSXChapterSelectUI::tileRect(int chapter, int scroll) {
+ // Even chapters put the tile on the right, odd ones on the left.
+ const int x = (chapter & 1) ? 20 : 220;
+ const int y = rowY(chapter, scroll);
+ return Common::Rect(x, y, x + kTileWidth, y + kTileHeight);
+}
+
+void RA2PSXChapterSelectUI::drawTile(Graphics::Surface &surface, int chapter, int y,
+ const Graphics::Surface *previews, bool selected, bool unlocked) const {
+ static const byte kIdleCorner[3] = { 0x80, 0x80, 0x80 };
+ static const byte kIdleEdge[3] = { 0xff, 0xff, 0xff };
+ // The original cycles the highlight through an animated CLUT; this uses the
+ // same accent the menu dialogs are drawn with.
+ static const byte kActiveCorner[3] = { 0xfc, 0xc8, 0x19 };
+ static const byte kActiveEdge[3] = { 0x80, 0x65, 0x0c };
+
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ const int left = xOffset + ((chapter & 1) ? 20 : 220);
+ const int top = yOffset + y;
+
+ if (unlocked && previews && previews->w >= 320 && previews->h >= 240) {
+ const int sourceX = (chapter % 4) * kTileWidth;
+ const int sourceY = (chapter / 4) * kTileHeight;
+ Common::Rect source(sourceX, sourceY, sourceX + kTileWidth, sourceY + kTileHeight);
+ int destX = left;
+ int destY = top;
+ if (destY < 0) {
+ source.top -= destY;
+ destY = 0;
+ }
+ source.bottom = MIN<int>(source.bottom, source.top + surface.h - destY);
+ if (source.top < source.bottom && destX >= 0 && destX + kTileWidth <= surface.w)
+ surface.copyRectToSurface(*previews, destX, destY, source);
+ }
+
+ const byte *corner = selected ? kActiveCorner : kIdleCorner;
+ const byte *edge = selected ? kActiveEdge : kIdleEdge;
+ const int midX = left + kTileWidth / 2;
+ const int midY = top + kTileHeight / 2;
+ const int right = left + kTileWidth;
+ const int bottom = top + kTileHeight;
+ drawRA2PSXGouraudLine(surface, left, top, midX, top, corner, edge);
+ drawRA2PSXGouraudLine(surface, midX, top, right, top, edge, corner);
+ drawRA2PSXGouraudLine(surface, right, top, right, midY, corner, edge);
+ drawRA2PSXGouraudLine(surface, right, midY, right, bottom, edge, corner);
+ drawRA2PSXGouraudLine(surface, right, bottom, midX, bottom, corner, edge);
+ drawRA2PSXGouraudLine(surface, midX, bottom, left, bottom, edge, corner);
+ drawRA2PSXGouraudLine(surface, left, bottom, left, midY, corner, edge);
+ drawRA2PSXGouraudLine(surface, left, midY, left, top, edge, corner);
+}
+
+void RA2PSXChapterSelectUI::drawLabel(Graphics::Surface &surface, int chapter, int y,
+ bool selected, bool unlocked) const {
+ const RA2PSXChapterEntry &entry = kChapters[chapter];
+ const char *const font = selected ? "STD_FT4" : "STD_FT2";
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+
+ if (!unlocked) {
+ // Locked chapters only get their number, without the episode name.
+ Common::String text = chapter == kChapterCount - 1 ?
+ Common::String("finale") : Common::String::format("chapter %d", chapter + 1);
+ const int width = _textures.measureText(text.c_str());
+ const int x = (chapter & 1) ? 110 : 210 - width;
+ if (selected)
+ subtractRA2PSXRect(surface, Common::Rect(xOffset + ((chapter & 1) ? 106 : 206 - width),
+ yOffset + y + 40, xOffset + ((chapter & 1) ? 106 : 206 - width) + width + 8,
+ yOffset + y + 54), 0x28, 0x28, 0x28);
+ _textures.drawText(surface, font, text.c_str(), xOffset + x, yOffset + y + 26, 0x7f);
+ return;
+ }
+
+ if (selected)
+ subtractRA2PSXRect(surface, Common::Rect(xOffset + ((chapter & 1) ? 106 : 206 - entry.barWidth),
+ yOffset + y + 17, xOffset + ((chapter & 1) ? 106 : 206 - entry.barWidth) +
+ entry.barWidth + 8, yOffset + y + 44), 0x28, 0x28, 0x28);
+ _textures.drawText(surface, font, entry.title, xOffset + entry.titleX, yOffset + y + 20, 0x7f);
+ _textures.drawText(surface, font, entry.name, xOffset + entry.nameX, yOffset + y + 33, 0x7f);
+}
+
+void RA2PSXChapterSelectUI::draw(Graphics::Surface &surface, const Graphics::Surface *previews,
+ int scroll, int selection, int unlocked) const {
+ const int xOffset = (surface.w - 320) / 2;
+ const int yOffset = (surface.h - 240) / 2;
+ for (int chapter = 0; chapter < kChapterCount; ++chapter) {
+ const int y = rowY(chapter, scroll);
+ if (y < -kRowPitch || y > 240)
+ continue;
+ const bool selected = chapter == selection;
+ const bool open = chapter < unlocked;
+ drawTile(surface, chapter, y, previews, selected, open);
+ drawLabel(surface, chapter, y, selected, open);
+ }
+ // The headline and the button captions sit over the rows.
+ _textures.drawHeadline(surface, "Select Chapter", xOffset + 110, yOffset + 20);
+ drawRA2PSXMenuHints(surface, _textures);
+}
+
bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
Common::Array<byte> data;
_textures.clear();
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 43993e20f73..3733a144acc 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -57,6 +57,8 @@ void subtractRA2PSXRect(Graphics::Surface &surface, const Common::Rect &rect,
int r, int g, int b);
void drawRA2PSXGouraudLine(Graphics::Surface &surface, int x0, int y0, int x1, int y1,
const byte *from, const byte *to);
+// The "back" and "select" button captions along the bottom of every menu page.
+void drawRA2PSXMenuHints(Graphics::Surface &surface, const RA2PSXTextureSet &textures);
class RA2PSXMovieText {
public:
@@ -127,6 +129,33 @@ private:
const RA2PSXTextureSet &_textures;
};
+// The scrolling chapter grid, whose tiles are frames of LEVELSEL.STR diced into
+// a four by four atlas of 80x60 previews.
+class RA2PSXChapterSelectUI {
+public:
+ enum {
+ kChapterCount = 16,
+ kTileWidth = 80,
+ kTileHeight = 60,
+ kRowPitch = 62
+ };
+
+ explicit RA2PSXChapterSelectUI(const RA2PSXTextureSet &textures) : _textures(textures) {}
+
+ void draw(Graphics::Surface &surface, const Graphics::Surface *previews,
+ int scroll, int selection, int unlocked) const;
+ static int rowY(int chapter, int scroll);
+ static Common::Rect tileRect(int chapter, int scroll);
+
+private:
+ void drawTile(Graphics::Surface &surface, int chapter, int y,
+ const Graphics::Surface *previews, bool selected, bool unlocked) const;
+ void drawLabel(Graphics::Surface &surface, int chapter, int y,
+ bool selected, bool unlocked) const;
+
+ const RA2PSXTextureSet &_textures;
+};
+
class RA2PSXLevel1UI {
public:
bool load(const RA2PSXArchive &archive);
Commit: e3d7ec975185750671401ad817a9c2f43a46627d
https://github.com/scummvm/scummvm/commit/e3d7ec975185750671401ad817a9c2f43a46627d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: use correct sound for L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/audio.cpp
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/psx.h
diff --git a/engines/scumm/insane/rebel2/psx/audio.cpp b/engines/scumm/insane/rebel2/psx/audio.cpp
index 264ac9425a6..6d1bb0fb137 100644
--- a/engines/scumm/insane/rebel2/psx/audio.cpp
+++ b/engines/scumm/insane/rebel2/psx/audio.cpp
@@ -25,12 +25,15 @@
#include "common/config-manager.h"
#include "common/endian.h"
#include "common/memstream.h"
+#include "common/ptr.h"
#include "common/system.h"
#include "common/util.h"
#include "scumm/scumm_v7.h"
#include "scumm/insane/rebel2/psx/psx.h"
+#include <math.h>
+
namespace Scumm {
class RA2PSXADSRStream : public Audio::RewindableAudioStream {
@@ -84,6 +87,82 @@ private:
uint32 _sustain;
};
+// A 14 bit bend centred on kNeutralBend, scaled into semitones by the range
+// macro command 0x33 sets.
+static int bendToStep(int bend, int rangeUp, int rangeDown) {
+ const int offset = CLIP(bend, 0, 0x3fff) - RA2PSXSoundPlayer::kNeutralBend;
+ if (!offset)
+ return 0x1000;
+ const int range = offset < 0 ? rangeDown : rangeUp;
+ const double semitones = (double)offset * range / 8192.0;
+ return CLIP((int)(0x1000 * pow(2.0, semitones / 12.0)), 1, 0x8000);
+}
+
+// The SPU bends in place; the mixer cannot, so the step lives in a block the
+// voice keeps a reference to and the stream reads.
+struct RA2PSXPitch {
+ RA2PSXPitch(int initial) : pitch(initial) {}
+ int pitch;
+};
+
+typedef Common::SharedPtr<RA2PSXPitch> RA2PSXPitchRef;
+
+class RA2PSXPitchStream : public Audio::RewindableAudioStream {
+public:
+ RA2PSXPitchStream(Audio::RewindableAudioStream *stream, const RA2PSXPitchRef &pitch)
+ : _stream(stream), _pitch(pitch), _channels(stream->isStereo() ? 2 : 1),
+ _fraction(0x1000), _ended(false) {
+ for (int i = 0; i < 2; ++i)
+ _previous[i] = _current[i] = 0;
+ }
+
+ ~RA2PSXPitchStream() override { delete _stream; }
+
+ int readBuffer(int16 *buffer, const int numSamples) override {
+ int produced = 0;
+ while (produced + _channels <= numSamples) {
+ while (_fraction >= 0x1000) {
+ int16 frame[2] = { 0, 0 };
+ if (_stream->readBuffer(frame, _channels) != _channels) {
+ _ended = true;
+ return produced;
+ }
+ for (int i = 0; i < _channels; ++i) {
+ _previous[i] = _current[i];
+ _current[i] = frame[i];
+ }
+ _fraction -= 0x1000;
+ }
+ for (int i = 0; i < _channels; ++i)
+ buffer[produced++] = (int16)((_previous[i] * (0x1000 - _fraction) +
+ _current[i] * _fraction) >> 12);
+ _fraction += CLIP(_pitch->pitch, 1, 0x3fff);
+ }
+ return produced;
+ }
+
+ bool isStereo() const override { return _channels == 2; }
+ bool endOfData() const override { return _ended || _stream->endOfData(); }
+ int getRate() const override { return _stream->getRate(); }
+
+ bool rewind() override {
+ _fraction = 0x1000;
+ _ended = false;
+ for (int i = 0; i < 2; ++i)
+ _previous[i] = _current[i] = 0;
+ return _stream->rewind();
+ }
+
+private:
+ Audio::RewindableAudioStream *_stream;
+ RA2PSXPitchRef _pitch;
+ int _channels;
+ int _fraction;
+ bool _ended;
+ int16 _previous[2];
+ int16 _current[2];
+};
+
static bool matchesTag(const Common::Array<byte> &data, uint32 offset, const char *tag) {
return offset + 4 <= data.size() && !memcmp(data.data() + offset, tag, 4);
}
@@ -148,14 +227,17 @@ bool RA2PSXSoundBank::load(const Common::Array<byte> &sampleData,
if (macroBase64 < 4 || macroBase64 > projectData.size())
return false;
const uint32 macroBase = (uint32)macroBase64;
- for (uint32 record = macroBase - 4; record + 8 <= projectData.size() &&
+ // Records are (int32 offset relative to the table, uint32 id) pairs; swapping
+ // the two pairs every macro with the previous entry's id.
+ for (uint32 record = macroBase; record + 8 <= projectData.size() &&
macros.size() < 256; record += 8) {
- const int32 relative = (int32)READ_LE_UINT32(projectData.data() + record + 4);
+ const int32 relative = (int32)READ_LE_UINT32(projectData.data() + record);
+ const uint32 id = READ_LE_UINT32(projectData.data() + record + 4);
const int64 target = (int64)macroBase + relative;
- if (target < 0 || target + 8 > (int64)projectData.size() || (target & 3))
+ if (id > 0xffff || target < 0 || target + 8 > (int64)projectData.size() || (target & 3))
break;
Macro macro;
- macro.id = READ_LE_UINT32(projectData.data() + record);
+ macro.id = (uint32)id;
macro.offset = (uint32)target;
macros.push_back(macro);
}
@@ -269,12 +351,14 @@ bool RA2PSXSoundBank::getMacroCommand(uint16 macro, uint16 step, byte *command)
return false;
}
-Audio::RewindableAudioStream *RA2PSXSoundBank::makeStream(uint16 id, uint32 rate,
+Audio::RewindableAudioStream *RA2PSXSoundBank::makeStream(uint16 id, uint32 macroRate,
uint16 adsrId) const {
const Sample *sample = findSample(id);
if (!sample)
return nullptr;
+ const uint32 rate = macroRate ? macroRate : sample->rate;
+
const uint32 size = (uint32)sample->blocks * 16;
byte *copy = (byte *)malloc(size);
if (!copy)
@@ -283,8 +367,7 @@ Audio::RewindableAudioStream *RA2PSXSoundBank::makeStream(uint16 id, uint32 rate
Common::SeekableReadStream *source =
new Common::MemoryReadStream(copy, size, DisposeAfterUse::YES);
- Audio::RewindableAudioStream *stream =
- Audio::makeXAStream(source, rate ? rate : sample->rate);
+ Audio::RewindableAudioStream *stream = Audio::makeXAStream(source, rate);
const ADSR *adsr = findADSR(adsrId);
if (adsr)
stream = new RA2PSXADSRStream(stream, adsr->attack, adsr->decay, adsr->sustain);
@@ -302,7 +385,9 @@ struct RA2PSXSoundPlayer::Impl {
struct Voice {
Voice() : active(false), waiting(false), waitForSampleEnd(false), macroDone(false),
root(false), sound(0), macro(0), step(0), adsr(0xffff), rate(0),
- rateOverride(-1), volume(0), pan(64), priority(0), born(0), readyTick(0),
+ sampleId(0), bendUp(2), bendDown(2),
+ pitch(RA2PSXSoundPlayer::kNeutralBend), volume(0), pan(64), priority(0),
+ born(0), readyTick(0),
startedAt(0), waitUntil(0) {}
bool active;
@@ -314,8 +399,12 @@ struct RA2PSXSoundPlayer::Impl {
uint16 macro;
uint16 step;
uint16 adsr;
+ uint16 sampleId;
+ RA2PSXPitchRef pitchRef;
+ int bendUp;
+ int bendDown;
uint32 rate;
- int rateOverride;
+ int pitch;
int volume;
int pan;
byte priority;
@@ -365,13 +454,19 @@ struct RA2PSXSoundPlayer::Impl {
voices[index] = Voice();
}
- void stopGroup(int index) {
- const SoundId sound = groups[index].sound;
+ // Keyed on the sound, not the group: the group can be gone while voices play.
+ void stopSound(SoundId sound) {
for (int i = 0; i < kVoiceCount; ++i) {
if (voices[i].active && voices[i].sound == sound)
clearVoice(i);
}
- groups[index] = Group();
+ const int group = findGroup(sound);
+ if (group >= 0)
+ groups[group] = Group();
+ }
+
+ void stopGroup(int index) {
+ stopSound(groups[index].sound);
}
int allocateGroup(SoundId sound) {
@@ -433,7 +528,7 @@ struct RA2PSXSoundPlayer::Impl {
}
int startVoice(SoundId sound, uint16 macro, uint16 step, byte priority,
- byte maxVoices, int volume, int pan, int rateOverride, bool root,
+ byte maxVoices, int volume, int pan, int pitch, bool root,
uint32 now, int excluded) {
const int slot = allocateVoice(macro, priority, maxVoices, excluded);
if (slot < 0)
@@ -445,7 +540,7 @@ struct RA2PSXSoundPlayer::Impl {
voice.sound = sound;
voice.macro = macro;
voice.step = step;
- voice.rateOverride = rateOverride;
+ voice.pitch = pitch;
voice.volume = CLIP(volume, 0, 127);
voice.pan = CLIP(pan, 0, 127);
voice.priority = priority;
@@ -470,11 +565,8 @@ struct RA2PSXSoundPlayer::Impl {
}
void finishVoice(int index) {
- const SoundId sound = voices[index].sound;
if (voices[index].root) {
- const int group = findGroup(sound);
- if (group >= 0)
- stopGroup(group);
+ stopSound(voices[index].sound);
return;
}
voices[index].macroDone = true;
@@ -505,8 +597,11 @@ struct RA2PSXSoundPlayer::Impl {
milliseconds = randomState % milliseconds;
}
voice.waitUntil = command[4] ? voice.startedAt + milliseconds : now + milliseconds;
- voice.waitForSampleEnd = command[3] != 0;
+ // Byte 1 bit 0 also ends on the sample; 0xffff means no limit.
+ voice.waitForSampleEnd = (command[1] & 1) != 0;
voice.waiting = milliseconds != 0;
+ if (milliseconds == 0xffff)
+ voice.waitUntil = now + 0xffffff;
if (voice.root && nextCommandEnds(voice))
setGroupExpiry(voice.sound, voice.waitUntil);
if (voice.waiting)
@@ -518,27 +613,21 @@ struct RA2PSXSoundPlayer::Impl {
const uint16 step = READ_LE_UINT16(command + 4);
if (macro != voice.macro) {
startVoice(voice.sound, macro, step, command[6], command[7],
- voice.volume, voice.pan, -1, false, now, index);
+ voice.volume, voice.pan, voice.pitch, false, now, index);
}
break;
}
case 0xc:
voice.adsr = READ_LE_UINT16(command + 1);
break;
+ case 0x33:
+ voice.bendUp = command[1] ? command[1] : 1;
+ voice.bendDown = command[2] ? command[2] : 1;
+ break;
case 0x10: {
vm->_mixer->stopHandle(voice.handle);
- const uint32 rate = voice.rateOverride >= 0 ?
- (uint32)voice.rateOverride : voice.rate;
- if (rate != 0 || voice.rateOverride < 0) {
- Audio::RewindableAudioStream *stream = bank.makeStream(
- READ_LE_UINT16(command + 1), rate, voice.adsr);
- if (stream) {
- vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &voice.handle,
- stream, -1,
- voice.volume * Audio::Mixer::kMaxChannelVolume / 127,
- soundBalance(voice.pan));
- }
- }
+ voice.sampleId = READ_LE_UINT16(command + 1);
+ startSample(index);
break;
}
case 0x1f:
@@ -552,7 +641,22 @@ struct RA2PSXSoundPlayer::Impl {
finishVoice(index);
}
- SoundId play(uint16 sfxId, int volume, int pan, int rate) {
+ void startSample(int index) {
+ Voice &voice = voices[index];
+ if (!voice.sampleId)
+ return;
+ Audio::RewindableAudioStream *stream = bank.makeStream(voice.sampleId, voice.rate,
+ voice.adsr);
+ if (!stream)
+ return;
+ voice.pitchRef = RA2PSXPitchRef(new RA2PSXPitch(
+ bendToStep(voice.pitch, voice.bendUp, voice.bendDown)));
+ stream = new RA2PSXPitchStream(stream, voice.pitchRef);
+ vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &voice.handle, stream, -1,
+ voice.volume * Audio::Mixer::kMaxChannelVolume / 127, soundBalance(voice.pan));
+ }
+
+ SoundId play(uint16 sfxId, int volume, int pan, int pitch) {
uint16 macro;
byte priority;
byte maxVoices;
@@ -565,7 +669,7 @@ struct RA2PSXSoundPlayer::Impl {
const int group = allocateGroup(sound);
const uint32 now = g_system->getMillis();
const int voice = startVoice(sound, macro, 0, priority, maxVoices,
- volume, pan, rate, true, now, -1);
+ volume, pan, pitch, true, now, -1);
if (voice < 0) {
groups[group] = Group();
return kInvalidSoundId;
@@ -632,10 +736,32 @@ struct RA2PSXSoundPlayer::Impl {
}
}
+ void setVolume(SoundId sound, int volume) {
+ volume = CLIP(volume, 0, 127);
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (!voices[i].active || voices[i].sound != sound)
+ continue;
+ voices[i].volume = volume;
+ if (vm->_mixer->isSoundHandleActive(voices[i].handle))
+ vm->_mixer->setChannelVolume(voices[i].handle,
+ volume * Audio::Mixer::kMaxChannelVolume / 127);
+ }
+ }
+
+ void setPitch(SoundId sound, int pitch) {
+ pitch = CLIP(pitch, 0, 0x3fff);
+ for (int i = 0; i < kVoiceCount; ++i) {
+ if (!voices[i].active || voices[i].sound != sound)
+ continue;
+ voices[i].pitch = pitch;
+ if (voices[i].pitchRef)
+ voices[i].pitchRef->pitch =
+ bendToStep(pitch, voices[i].bendUp, voices[i].bendDown);
+ }
+ }
+
void stop(SoundId sound) {
- const int group = findGroup(sound);
- if (group >= 0)
- stopGroup(group);
+ stopSound(sound);
}
void stopAll() {
@@ -656,8 +782,8 @@ RA2PSXSoundPlayer::~RA2PSXSoundPlayer() {
}
RA2PSXSoundPlayer::SoundId RA2PSXSoundPlayer::play(uint16 sfx, int volume,
- int pan, int rate) {
- return _impl->play(sfx, volume, pan, rate);
+ int pan, int pitch) {
+ return _impl->play(sfx, volume, pan, pitch);
}
void RA2PSXSoundPlayer::update() {
@@ -668,6 +794,14 @@ void RA2PSXSoundPlayer::setPan(SoundId sound, int pan) {
_impl->setPan(sound, pan);
}
+void RA2PSXSoundPlayer::setVolume(SoundId sound, int volume) {
+ _impl->setVolume(sound, volume);
+}
+
+void RA2PSXSoundPlayer::setPitch(SoundId sound, int pitch) {
+ _impl->setPitch(sound, pitch);
+}
+
void RA2PSXSoundPlayer::stop(SoundId sound) {
_impl->stop(sound);
}
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 24bef8053b4..fab83729dff 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -40,10 +40,46 @@ namespace Scumm {
#ifdef USE_TINYGL
static const int kLevel1FrameRate = 30;
+// The original pans as (x * 640 / z) / 2 + 0x40, saturating before the edges.
static int getLevel1SoundPan(float screenX) {
- return CLIP<int>((int)(screenX * 127.0f / 320.0f), 0, 127);
+ return CLIP<int>((int)((screenX - 160.0f) * 0.5f) + 64, 0, 127);
}
+enum {
+ kLevel1SfxTieApproachA = 0x19,
+ kLevel1SfxTieApproachB = 0x1a,
+ kLevel1SfxTieExplode = 0x1b,
+ kLevel1SfxTieFire = 0x17,
+ kLevel1SfxPlayerFire = 0x18,
+ kLevel1SfxPlayerHit = 0x36,
+ kLevel1SfxLowShield = 0x2d,
+ kLevel1SfxEngineLeft = 0x32,
+ kLevel1SfxEngineRight = 0x33,
+ kLevel1SfxEngineOutside = 0x41,
+ kLevel1SfxExtraLife = 0x44
+};
+
+// Outside engine base volume, and the per-frame step of the mix cross-fade.
+enum {
+ kLevel1OutsideBase = 0x46,
+ kLevel1MixStep = 2,
+ kLevel1MixMaximum = 0x7f
+};
+
+static int approachMix(int current, int target) {
+ if (current < target)
+ return MIN(target, current + kLevel1MixStep);
+ return MAX(target, current - kLevel1MixStep);
+}
+
+// Points per kill and the extra life thresholds, by difficulty.
+static const int kLevel1KillScore[3] = { 80, 100, 150 };
+static const int kLevel1ExtraLife[3][3] = {
+ { 5000, 5000, 10000 },
+ { 5000, 5000, 30000 },
+ { 10000, 20000, 30000 }
+};
+
struct RA2PSXLevel1Enemy {
RA2PSXLevel1Enemy() : active(false), pattern(0), age(0), lifetime(0), fireFrame(0),
laserFrames(0), startX(0), startY(0), controlX(0), controlY(0), endX(0), endY(0),
@@ -388,6 +424,20 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
RA2PSXLevel1Ship ship;
RA2PSXSoundPlayer soundPlayer(_vm, _soundBank);
RA2PSXSoundPlayer::SoundId approachSounds[3] = {};
+ // A hard left/right pair for the cockpit and a centred one for outside,
+ // cross-faded as the view changes.
+ const RA2PSXSoundPlayer::SoundId engineLeft =
+ soundPlayer.play(kLevel1SfxEngineLeft, 0x28, 0x00);
+ const RA2PSXSoundPlayer::SoundId engineRight =
+ soundPlayer.play(kLevel1SfxEngineRight, 0x28, 0x7f);
+ const RA2PSXSoundPlayer::SoundId engineOutside =
+ soundPlayer.play(kLevel1SfxEngineOutside, kLevel1OutsideBase, 0x40);
+ RA2PSXSoundPlayer::SoundId lowShieldAlarm = RA2PSXSoundPlayer::kInvalidSoundId;
+ int cockpitMix = -1;
+ int outsideMix = -1;
+ const int difficulty = CLIP(_settings.difficulty, 0, 2);
+ int extraLifeStage = 0;
+ int nextExtraLife = kLevel1ExtraLife[difficulty][0];
int aimX = 160;
int aimY = 113;
int aimVelocityX = 0;
@@ -608,6 +658,30 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
left, right, up, down);
}
+ const int cockpitTarget = thirdPersonView ? 0 : kLevel1MixMaximum;
+ const int outsideTarget = thirdPersonView ? kLevel1MixMaximum : 0;
+ if (cockpitMix != cockpitTarget || outsideMix != outsideTarget) {
+ cockpitMix = cockpitMix < 0 ? cockpitTarget :
+ approachMix(cockpitMix, cockpitTarget);
+ outsideMix = outsideMix < 0 ? outsideTarget :
+ approachMix(outsideMix, outsideTarget);
+ soundPlayer.setVolume(engineLeft, cockpitMix);
+ soundPlayer.setVolume(engineRight, cockpitMix);
+ soundPlayer.setVolume(engineOutside, outsideMix);
+ } else if (thirdPersonView) {
+ // Outside, the engine tracks how hard the ship is turning.
+ const int turn = MAX(ABS(ship.velocityX), ABS(ship.velocityY));
+ soundPlayer.setPitch(engineOutside, 0x2000 + turn);
+ soundPlayer.setVolume(engineOutside, kLevel1OutsideBase +
+ turn / (0xa0 - kLevel1OutsideBase));
+ soundPlayer.setPan(engineOutside,
+ CLIP<int>(ship.x * 4096 / 0x479f + 64, 0, 127));
+ }
+
+ // Started once, then left running, as in the original.
+ if (shield <= 31 && lowShieldAlarm == RA2PSXSoundPlayer::kInvalidSoundId)
+ lowShieldAlarm = soundPlayer.play(kLevel1SfxLowShield, 0x50, 0x40);
+
if (logicFrame >= nextSpawnAdjustment) {
nextSpawnAdjustment = logicFrame + 20;
spawnRange = MAX(40, spawnRange - 1);
@@ -628,9 +702,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
}
if (spawnedEnemy >= 0) {
- const uint16 sfx = _vm->_rnd.getRandomNumber(999) < 800 ? 0x19 : 0x1a;
- const int rate = 0x1c18 + _vm->_rnd.getRandomNumber(1999);
- approachSounds[spawnedEnemy] = soundPlayer.play(sfx, 0x5e, 0x40, rate);
+ const uint16 sfx = _vm->_rnd.getRandomNumber(999) < 800 ?
+ kLevel1SfxTieApproachA : kLevel1SfxTieApproachB;
+ const int pitch = 0x1c18 + _vm->_rnd.getRandomNumber(1999);
+ approachSounds[spawnedEnemy] = soundPlayer.play(sfx, 0x5e, 0x40, pitch);
}
spawnDelay = spawnBase + _vm->_rnd.getRandomNumber(spawnRange - 1);
}
@@ -650,21 +725,21 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
soundPlayer.setPan(approachSounds[i], soundPan);
if (enemies[i].age == enemies[i].fireFrame) {
enemies[i].laserFrames = 4;
- soundPlayer.play(0x17, 0x4e, soundPan);
+ soundPlayer.play(kLevel1SfxTieFire, 0x4e, soundPan);
if (_vm->_rnd.getRandomNumber(99) < 38) {
shield = MAX(0, shield - (int)_vm->_rnd.getRandomNumberRng(6, 10));
- soundPlayer.play(0x36, 0x7f, 0x40);
+ soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
}
}
if (enemies[i].age >= enemies[i].lifetime) {
- const int rate = _vm->_rnd.getRandomNumber(0x3fff);
- soundPlayer.play(0x1b, 0x5a, soundPan, rate);
+ const int pitch = _vm->_rnd.getRandomNumber(0x3fff);
+ soundPlayer.play(kLevel1SfxTieExplode, 0x5a, soundPan, pitch);
soundPlayer.stop(approachSounds[i]);
approachSounds[i] = RA2PSXSoundPlayer::kInvalidSoundId;
enemies[i].active = false;
if (_vm->_rnd.getRandomNumber(99) < 18) {
shield = MAX(0, shield - 12);
- soundPlayer.play(0x36, 0x7f, 0x40);
+ soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
}
}
}
@@ -681,7 +756,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
if (!spawnLevel1Shot(shots, aimX, aimY,
thirdPersonView ? &ship : nullptr, shotTargetX, shotTargetY))
continue;
- soundPlayer.play(0x18, 0x3f, 0x40);
+ soundPlayer.play(kLevel1SfxPlayerFire, 0x3f, 0x40);
int hitEnemy = -1;
float hitDistance = 1000000.0f;
for (int i = 0; i < 3; ++i) {
@@ -698,12 +773,18 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
if (hitEnemy >= 0) {
const int soundPan = getLevel1SoundPan(enemies[hitEnemy].x);
- const int rate = _vm->_rnd.getRandomNumber(0x3fff);
- soundPlayer.play(0x1b, 0x5a, soundPan, rate);
+ const int pitch = _vm->_rnd.getRandomNumber(0x3fff);
+ soundPlayer.play(kLevel1SfxTieExplode, 0x5a, soundPan, pitch);
soundPlayer.stop(approachSounds[hitEnemy]);
approachSounds[hitEnemy] = RA2PSXSoundPlayer::kInvalidSoundId;
enemies[hitEnemy].active = false;
- score = MIN(9999999, score + 100);
+ score = MIN(9999999, score + kLevel1KillScore[difficulty]);
+ if (score >= nextExtraLife) {
+ // The original also awards a life; the shared runner owns it.
+ soundPlayer.play(kLevel1SfxExtraLife, 0x7f, 0x40);
+ extraLifeStage = MIN(extraLifeStage + 1, 2);
+ nextExtraLife += kLevel1ExtraLife[difficulty][extraLifeStage];
+ }
for (int i = 0; i < 3; ++i) {
if (explosions[i].frames == 0) {
explosions[i].frames = 10;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 24f003d10af..e8dd3b72a4c 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -74,7 +74,7 @@ private:
class RA2PSXSoundBank {
public:
bool load(const Common::Array<byte> &sampleData, const Common::Array<byte> &projectData);
- Audio::RewindableAudioStream *makeStream(uint16 id, uint32 rate,
+ Audio::RewindableAudioStream *makeStream(uint16 id, uint32 macroRate,
uint16 adsrId = 0xffff) const;
bool getSFX(uint16 id, uint16 ¯o, byte &priority, byte &maxVoices) const;
bool getMacroCommand(uint16 macro, uint16 step, byte *command) const;
@@ -123,9 +123,14 @@ public:
RA2PSXSoundPlayer(ScummEngine_v7 *vm, const RA2PSXSoundBank &bank);
~RA2PSXSoundPlayer();
- SoundId play(uint16 sfx, int volume, int pan, int rate = -1);
+ // Pitch is a 14 bit bend; kNeutralBend leaves the sound at its own rate.
+ enum { kNeutralBend = 0x2000 };
+
+ SoundId play(uint16 sfx, int volume, int pan, int pitch = kNeutralBend);
void update();
void setPan(SoundId sound, int pan);
+ void setVolume(SoundId sound, int volume);
+ void setPitch(SoundId sound, int pitch);
void stop(SoundId sound);
void stopAll();
Commit: 8420adc2deacde75ebc91d83a13fa484ab818952
https://github.com/scummvm/scummvm/commit/8420adc2deacde75ebc91d83a13fa484ab818952
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: improved movement and gameplay of L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/iact.cpp
engines/scumm/insane/rebel2/levels.cpp
engines/scumm/insane/rebel2/menu.cpp
engines/scumm/insane/rebel2/psx/audio.cpp
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/menu.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/video.cpp
engines/scumm/insane/rebel2/render.cpp
diff --git a/engines/scumm/insane/rebel2/iact.cpp b/engines/scumm/insane/rebel2/iact.cpp
index 0af5ddacdf5..3c8b5838065 100644
--- a/engines/scumm/insane/rebel2/iact.cpp
+++ b/engines/scumm/insane/rebel2/iact.cpp
@@ -38,7 +38,7 @@ const int kRA2Handler7DirectInputNumerator = 4;
const int kRA2Handler7DirectInputDenominator = 5;
const int kRA2Handler7MouseTargetRangeX = 0xc0;
-static bool readLevel2BackgroundChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
+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)
diff --git a/engines/scumm/insane/rebel2/levels.cpp b/engines/scumm/insane/rebel2/levels.cpp
index 0aa7e4e5f36..ec758c094c3 100644
--- a/engines/scumm/insane/rebel2/levels.cpp
+++ b/engines/scumm/insane/rebel2/levels.cpp
@@ -33,9 +33,9 @@
namespace Scumm {
-static const int kRebel2GameplayAimCenterX = 160;
-static const int kRebel2GameplayAimCenterY = 100;
-static const uint32 kRebel2GameplayMouseSettleMs = 1000;
+const int kRebel2GameplayAimCenterX = 160;
+const int kRebel2GameplayAimCenterY = 100;
+const uint32 kRebel2GameplayMouseSettleMs = 1000;
struct Rebel2LevelEndParams {
int titleStartBeforeEnd;
@@ -46,7 +46,7 @@ struct Rebel2LevelEndParams {
int errHigh;
};
-static const Rebel2LevelEndParams kRebel2LevelEndParams[16] = {
+const Rebel2LevelEndParams kRebel2LevelEndParams[16] = {
{ 0, 0, -1, -1, -1, -1 },
{ 120, 10, 96, 100, -1, -1 },
{ 120, 10, 80, 94, -1, -1 },
@@ -65,7 +65,7 @@ static const Rebel2LevelEndParams kRebel2LevelEndParams[16] = {
{ 100, 10, 60, 68, 1, 3 }
};
-static void purgeRebel2GameplayInputEvents(Common::EventManager *eventMan) {
+void purgeRebel2GameplayInputEvents(Common::EventManager *eventMan) {
if (!eventMan)
return;
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index c646f42aa59..59cd9529d1c 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -41,7 +41,7 @@
namespace Scumm {
-static void setRebel2MixerVolume(ScummEngine_v7 *vm, int volumeLevel) {
+void setRebel2MixerVolume(ScummEngine_v7 *vm, int volumeLevel) {
const int mixerVolume = CLIP<int>(volumeLevel * 2, 0, (int)Audio::Mixer::kMaxMixerVolume);
vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, mixerVolume);
vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, mixerVolume);
diff --git a/engines/scumm/insane/rebel2/psx/audio.cpp b/engines/scumm/insane/rebel2/psx/audio.cpp
index 6d1bb0fb137..39cba3ff0df 100644
--- a/engines/scumm/insane/rebel2/psx/audio.cpp
+++ b/engines/scumm/insane/rebel2/psx/audio.cpp
@@ -89,7 +89,7 @@ private:
// A 14 bit bend centred on kNeutralBend, scaled into semitones by the range
// macro command 0x33 sets.
-static int bendToStep(int bend, int rangeUp, int rangeDown) {
+int bendToStep(int bend, int rangeUp, int rangeDown) {
const int offset = CLIP(bend, 0, 0x3fff) - RA2PSXSoundPlayer::kNeutralBend;
if (!offset)
return 0x1000;
@@ -163,15 +163,15 @@ private:
int16 _current[2];
};
-static bool matchesTag(const Common::Array<byte> &data, uint32 offset, const char *tag) {
+bool matchesTag(const Common::Array<byte> &data, uint32 offset, const char *tag) {
return offset + 4 <= data.size() && !memcmp(data.data() + offset, tag, 4);
}
-static bool timeReached(uint32 now, uint32 target) {
+bool timeReached(uint32 now, uint32 target) {
return (int32)(now - target) >= 0;
}
-static int soundBalance(int pan) {
+int soundBalance(int pan) {
// "sound mode: mono" in the options menu collapses the panning.
if (ConfMan.hasKey("rebel2_mono") && ConfMan.getBool("rebel2_mono"))
return 0;
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index fab83729dff..4102c542c9f 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -38,12 +38,7 @@
namespace Scumm {
#ifdef USE_TINYGL
-static const int kLevel1FrameRate = 30;
-
-// The original pans as (x * 640 / z) / 2 + 0x40, saturating before the edges.
-static int getLevel1SoundPan(float screenX) {
- return CLIP<int>((int)((screenX - 160.0f) * 0.5f) + 64, 0, 127);
-}
+const int kLevel1FrameRate = 30;
enum {
kLevel1SfxTieApproachA = 0x19,
@@ -66,43 +61,257 @@ enum {
kLevel1MixMaximum = 0x7f
};
-static int approachMix(int current, int target) {
+int approachMix(int current, int target) {
if (current < target)
return MIN(target, current + kLevel1MixStep);
return MAX(target, current - kLevel1MixStep);
}
// Points per kill and the extra life thresholds, by difficulty.
-static const int kLevel1KillScore[3] = { 80, 100, 150 };
-static const int kLevel1ExtraLife[3][3] = {
+const int kLevel1KillScore[3] = { 80, 100, 150 };
+const int kLevel1ExtraLife[3][3] = {
{ 5000, 5000, 10000 },
{ 5000, 5000, 30000 },
{ 10000, 20000, 30000 }
};
+// The TIEs fly five point Hermite splines in world space; angles run 4096 units to
+// the turn and fractions are 1/4096.
+enum {
+ kLevel1SplinePoints = 5,
+ kLevel1SplineStep = 0x40,
+ kLevel1LeadFrames = 30,
+ kLevel1EnemyCount = 3,
+ kLevel1ShotCount = 8,
+ kLevel1ShieldFull = 0x1000,
+ kLevel1LowShield = 0x501,
+ kLevel1FireFacing = 0xf3c,
+ kLevel1HitRadius = 0xc000
+};
+
+const int kLevel1SplineDepth[kLevel1SplinePoints] = { 17000, 14000, 11000, -4000, -8000 };
+
+// Bolt damage by view then difficulty, and the far heavier damage from a TIE that rams.
+const int kLevel1BoltDamage[2][3] = { { 100, 200, 240 }, { 112, 160, 220 } };
+const int kLevel1RamDamage[3] = { 256, 512, 1024 };
+// Per cent chance that a shot is aimed at the player instead of fired straight ahead.
+const int kLevel1AimChance[2][3] = { { 5, 5, 5 }, { 15, 20, 20 } };
+// A TIE that gets this close without being shot collides with the player.
+const int kLevel1RamRadius[2] = { 200, 220 };
+
+// The original truncates its 1/4096 products toward zero.
+int fixedShift12(int value) {
+ return (value < 0 ? value + 0xfff : value) >> 12;
+}
+
+void normalizeVector12(const int *source, int *result) {
+ const int x = source[0];
+ const int y = source[1];
+ const int z = source[2];
+ const double square = (double)x * x + (double)y * y + (double)z * z;
+ const int length = MAX(1, (int)sqrt(square));
+ result[0] = x * 4096 / length;
+ result[1] = y * 4096 / length;
+ result[2] = z * 4096 / length;
+}
+
+// asin(value / 4096) in the PlayStation's 4096 units to the turn.
+int lookupAsinAngle(int value) {
+ return (int)(asin(CLIP(value, 0, 4096) / 4096.0) * 651.8986469044033 + 0.5);
+}
+
+int signedAsinAngle(int value) {
+ return value < 0 ? -lookupAsinAngle(-value) : lookupAsinAngle(value);
+}
+
+struct RA2PSXLevel1Spline {
+ RA2PSXLevel1Spline() {
+ memset(this, 0, sizeof(*this));
+ segmentCount = kLevel1SplinePoints;
+ step = kLevel1SplineStep;
+ }
+
+ int control[kLevel1SplinePoints][3];
+ int tangentA[3];
+ int tangentB[3];
+ int previous[3];
+ int coefficient[3][4];
+ int position[3];
+ int segment;
+ int segmentCount;
+ int t;
+ int step;
+};
+
+void updateSplineTangents(RA2PSXLevel1Spline &spline, int segment) {
+ for (int axis = 0; axis < 3; ++axis)
+ spline.tangentA[axis] = spline.tangentB[axis];
+
+ if (segment == 0) {
+ for (int axis = 0; axis < 3; ++axis) {
+ spline.tangentB[axis] = spline.control[1][axis] - spline.control[0][axis];
+ spline.previous[axis] = spline.tangentB[axis];
+ spline.tangentA[axis] = spline.control[0][axis];
+ }
+ // The original only overwrites x and y, leaving z at the first control point.
+ spline.tangentA[0] = 1;
+ spline.tangentA[1] = 1;
+ } else if (segment == spline.segmentCount - 1) {
+ for (int axis = 0; axis < 3; ++axis)
+ spline.tangentB[axis] = spline.previous[axis];
+ } else {
+ for (int axis = 0; axis < 3; ++axis) {
+ const int old = spline.previous[axis];
+ spline.previous[axis] = spline.control[segment + 1][axis] -
+ spline.control[segment][axis];
+ spline.tangentB[axis] = (old + spline.previous[axis]) / 2;
+ }
+ }
+}
+
+// Hermite basis, one row per source term and one column per power of t.
+const int kLevel1SplineBasis[4][4] = {
+ { 2, -3, 0, 1 },
+ { -2, 3, 0, 0 },
+ { 1, -2, 1, 0 },
+ { 1, -1, 0, 0 }
+};
+
+void buildSplineSegmentCoefficients(RA2PSXLevel1Spline &spline, int segment) {
+ if (segment < 1 || segment >= kLevel1SplinePoints)
+ return;
+ for (int axis = 0; axis < 3; ++axis) {
+ const int source[4] = {
+ spline.control[segment - 1][axis], spline.control[segment][axis],
+ spline.tangentA[axis], spline.tangentB[axis]
+ };
+ for (int power = 0; power < 4; ++power) {
+ int value = 0;
+ for (int term = 0; term < 4; ++term)
+ value += source[term] * kLevel1SplineBasis[term][power];
+ spline.coefficient[axis][power] = value;
+ }
+ }
+}
+
+void advanceSplineObject(RA2PSXLevel1Spline &spline) {
+ const int t1 = spline.t;
+ const int t2 = t1 * t1 >> 12;
+ const int t3 = t1 * t2 >> 12;
+ for (int axis = 0; axis < 3; ++axis)
+ spline.position[axis] = (int16)(((spline.coefficient[axis][0] * t3 +
+ spline.coefficient[axis][1] * t2 +
+ spline.coefficient[axis][2] * t1) >> 12) + spline.coefficient[axis][3]);
+
+ spline.t += spline.step;
+ if (spline.t <= 0xfff)
+ return;
+ spline.t -= 0x1000;
+ if (++spline.segment == spline.segmentCount)
+ return;
+ updateSplineTangents(spline, spline.segment);
+ buildSplineSegmentCoefficients(spline, spline.segment + 1);
+}
+
+void randomizeTieSplineControlPoints(RA2PSXLevel1Spline &spline, int mode,
+ int spread, int base, Common::RandomSource &random) {
+ if (mode < 1 || mode > 3)
+ return;
+ const int offset = base / 2;
+ if (mode != 1) {
+ for (int axis = 0; axis < 2; ++axis)
+ spline.control[1][axis] += (int)random.getRandomNumber(spread * 2 - 1) -
+ spread + offset;
+ }
+ if (mode != 2) {
+ for (int axis = 0; axis < 2; ++axis)
+ spline.control[2][axis] += (int)random.getRandomNumber(spread * 2 - 1) -
+ spread + offset;
+ }
+ if (mode == 1)
+ return;
+ for (int axis = 0; axis < 2; ++axis)
+ spline.control[3][axis] += (int)random.getRandomNumber(spread * 2 - 1) - spread + offset;
+}
+
+// Head on attack run: the far and near ends are picked first, the rest strung between them.
+void initTieSplinePatternA(RA2PSXLevel1Spline &spline, Common::RandomSource &random) {
+ spline = RA2PSXLevel1Spline();
+
+ int last = (int)random.getRandomNumber(349) + 500;
+ if (!random.getRandomBit())
+ last = -last;
+ spline.control[4][0] = last;
+ last = (int)random.getRandomNumber(349) + 500;
+ if (!random.getRandomBit())
+ last = -last;
+ spline.control[4][1] = last;
+ spline.control[4][2] = kLevel1SplineDepth[4];
+
+ const int lead = spline.control[4][0] > 0 ? -kLevel1SplineDepth[0] : kLevel1SplineDepth[0];
+ spline.control[0][0] = (int)random.getRandomNumber(179) * lead / 640;
+ spline.control[0][1] = spline.control[4][1] < 1 ? 0 :
+ (int)random.getRandomNumber(139) * -kLevel1SplineDepth[0] / 640;
+ spline.control[0][2] = kLevel1SplineDepth[0];
+
+ int direction[3];
+ for (int axis = 0; axis < 3; ++axis)
+ direction[axis] = spline.control[0][axis] - spline.control[4][axis];
+ normalizeVector12(direction, direction);
+ for (int point = 3; point >= 1; --point) {
+ const int depth = kLevel1SplineDepth[point];
+ spline.control[point][0] = fixedShift12(direction[0] * depth) + spline.control[4][0];
+ spline.control[point][1] = fixedShift12(direction[1] * depth) + spline.control[4][1];
+ spline.control[point][2] = depth;
+ }
+
+ randomizeTieSplineControlPoints(spline, (int)random.getRandomNumber(2),
+ (int)random.getRandomNumber(499) + 1000, 800, random);
+ updateSplineTangents(spline, 0);
+ buildSplineSegmentCoefficients(spline, 1);
+}
+
+// Loose weave: every control point is scattered across the view at its own depth.
+void initTieSplinePatternB(RA2PSXLevel1Spline &spline, Common::RandomSource &random) {
+ spline = RA2PSXLevel1Spline();
+
+ for (int point = 0; point < kLevel1SplinePoints - 1; ++point) {
+ const int depth = kLevel1SplineDepth[point];
+ spline.control[point][2] = depth;
+ spline.control[point][0] = ((int)random.getRandomNumber(259) - 130) * depth / 640;
+ spline.control[point][1] = ((int)random.getRandomNumber(179) - 90) * depth / 640;
+ }
+
+ const int depth = kLevel1SplineDepth[kLevel1SplinePoints - 1];
+ spline.control[4][2] = depth;
+ int offset = (int)random.getRandomNumber(299) + 800;
+ if ((int)random.getRandomNumber(1999) > 1000)
+ offset = -offset;
+ spline.control[4][0] = offset * depth / 640;
+ offset = (int)random.getRandomNumber(299) + 600;
+ if ((int)random.getRandomNumber(1999) > 1000)
+ offset = -offset;
+ spline.control[4][1] = offset * depth / 640;
+
+ spline.control[0][0] += (int)random.getRandomNumber(2999) - 1500;
+ spline.control[0][1] += (int)random.getRandomNumber(1499) - 750;
+ updateSplineTangents(spline, 0);
+ buildSplineSegmentCoefficients(spline, 1);
+}
+
struct RA2PSXLevel1Enemy {
- RA2PSXLevel1Enemy() : active(false), pattern(0), age(0), lifetime(0), fireFrame(0),
- laserFrames(0), startX(0), startY(0), controlX(0), controlY(0), endX(0), endY(0),
- x(0), y(0), size(0), pitch(0), yaw(0), roll(0) {}
+ RA2PSXLevel1Enemy() : active(false), previousYaw(0), facing(0), fireCountdown(0) {
+ rotation[0] = rotation[1] = rotation[2] = 0;
+ }
bool active;
- int pattern;
- int age;
- int lifetime;
- int fireFrame;
- int laserFrames;
- float startX;
- float startY;
- float controlX;
- float controlY;
- float endX;
- float endY;
- float x;
- float y;
- float size;
- float pitch;
- float yaw;
- float roll;
+ RA2PSXLevel1Spline path;
+ // A copy of the path run 30 frames ahead; the gap gives the heading.
+ RA2PSXLevel1Spline lead;
+ int rotation[3];
+ int previousYaw;
+ int facing;
+ int fireCountdown;
};
struct RA2PSXLevel1Explosion {
@@ -113,6 +322,18 @@ struct RA2PSXLevel1Explosion {
int y;
};
+struct RA2PSXLevel1TieShot {
+ RA2PSXLevel1TieShot() : active(false), distance(0), length(0) {}
+
+ bool active;
+ int origin[2][3];
+ int direction[2][3];
+ int yaw[2];
+ int pitch[2];
+ int distance;
+ int length;
+};
+
struct RA2PSXLevel1Shot {
RA2PSXLevel1Shot() : active(false), progress(0), targetX(0), targetY(0), targetZ(0) {}
@@ -123,6 +344,10 @@ struct RA2PSXLevel1Shot {
float targetX;
float targetY;
float targetZ;
+ // The line the player aims along, sampled once per logic frame for the hit test.
+ float traceStart[3];
+ float trace[3];
+ float previousTrace[3];
};
struct RA2PSXLevel1Ship {
@@ -135,62 +360,199 @@ struct RA2PSXLevel1Ship {
int velocityY;
};
-static const float kLevel1LaserStart[3][3] = {
+const float kLevel1LaserStart[3][3] = {
{ -350.0f, 200.0f, 400.0f },
{ 350.0f, 200.0f, 400.0f },
{ 0.0f, 500.0f, 400.0f }
};
-static const float kLevel1LaserRoll[3] = { -45.0f, 45.0f, 0.0f };
+const float kLevel1LaserRoll[3] = { -45.0f, 45.0f, 0.0f };
-static const float kLevel1ShipLaserStart[3][3] = {
+const float kLevel1ShipLaserStart[3][3] = {
{ -93.0f, 11.0f, -139.0f },
{ 93.0f, 11.0f, -139.0f },
{ 4.0f, 210.0f, -111.0f }
};
-static void updateLevel1Enemy(RA2PSXLevel1Enemy &enemy) {
- const float t = MIN(1.0f, (float)enemy.age / enemy.lifetime);
- const float inverse = 1.0f - t;
- enemy.x = inverse * inverse * enemy.startX + 2.0f * inverse * t * enemy.controlX +
- t * t * enemy.endX;
- enemy.y = inverse * inverse * enemy.startY + 2.0f * inverse * t * enemy.controlY +
- t * t * enemy.endY;
- enemy.size = 5.0f + 72.0f * t * t;
- enemy.yaw = (enemy.controlX - enemy.startX) * 0.18f + sinf(enemy.age * 0.09f) * 18.0f;
- enemy.pitch = -8.0f + sinf(enemy.age * 0.06f) * 10.0f;
- enemy.roll = (enemy.controlY - enemy.startY) * 0.12f + sinf(enemy.age * 0.12f) * 8.0f;
+// The two cannon mounts, in the TIE's own space.
+const int kLevel1TieMuzzle[2][3] = { { -60, 10, -50 }, { 60, 10, -50 } };
+
+void rotateVector(const RA2PSXMatrix &transform, const int *source, int *result) {
+ for (int row = 0; row < 3; ++row) {
+ float value = 0.0f;
+ for (int column = 0; column < 3; ++column)
+ value += transform.rotation[row][column] * source[column];
+ result[row] = (int)value;
+ }
}
-static void spawnLevel1Enemy(RA2PSXLevel1Enemy &enemy, Common::RandomSource &random) {
+void spawnLevel1Enemy(RA2PSXLevel1Enemy &enemy, Common::RandomSource &random) {
enemy = RA2PSXLevel1Enemy();
enemy.active = true;
- enemy.pattern = random.getRandomBit();
- enemy.lifetime = random.getRandomNumberRng(95, 150);
- enemy.fireFrame = enemy.lifetime * random.getRandomNumberRng(45, 70) / 100;
-
- const bool fromLeft = random.getRandomBit();
- if (enemy.pattern == 0) {
- enemy.startX = fromLeft ? -24.0f : 344.0f;
- enemy.startY = (float)random.getRandomNumberRng(25, 175);
- enemy.controlX = (float)random.getRandomNumberRng(95, 225);
- enemy.controlY = (float)random.getRandomNumberRng(35, 170);
- enemy.endX = fromLeft ? (float)random.getRandomNumberRng(210, 390) :
- (float)random.getRandomNumberRngSigned(-70, 110);
+ if (random.getRandomNumber(999) < 500)
+ initTieSplinePatternA(enemy.path, random);
+ else
+ initTieSplinePatternB(enemy.path, random);
+ enemy.lead = enemy.path;
+ enemy.lead.t += enemy.path.step * kLevel1LeadFrames;
+ enemy.rotation[1] = 0x800;
+ enemy.previousYaw = 0x800;
+ enemy.fireCountdown = (int)random.getRandomNumber(14) + 8;
+}
+
+// The TIE points down its own path and banks into the turn; facing measures how squarely
+// that path runs at the player, which is what lets it open fire.
+void orientTieAlongSpline(RA2PSXLevel1Enemy &enemy, int playerX, int playerY) {
+ int delta[3];
+ for (int axis = 0; axis < 3; ++axis)
+ delta[axis] = enemy.path.position[axis] - enemy.lead.position[axis];
+
+ int vector[3] = { 0, delta[1], delta[2] };
+ normalizeVector12(vector, vector);
+ enemy.rotation[0] = signedAsinAngle(vector[1]);
+
+ vector[0] = delta[0];
+ vector[1] = 0;
+ vector[2] = delta[2];
+ normalizeVector12(vector, vector);
+ const int yaw = (int16)(-signedAsinAngle(vector[0]) + 0x800);
+ enemy.rotation[2] = (int16)fixedShift12(((yaw - enemy.previousYaw) * 2 +
+ enemy.rotation[2]) * 0xf00);
+ enemy.rotation[1] = yaw;
+ enemy.previousYaw = yaw;
+
+ for (int axis = 0; axis < 3; ++axis)
+ delta[axis] = enemy.lead.position[axis] - enemy.path.position[axis];
+ normalizeVector12(delta, delta);
+ int toPlayer[3] = {
+ playerX - enemy.path.position[0],
+ playerY - enemy.path.position[1],
+ -enemy.path.position[2]
+ };
+ normalizeVector12(toPlayer, toPlayer);
+ enemy.facing = fixedShift12(delta[0] * toPlayer[0]) + fixedShift12(delta[1] * toPlayer[1]) +
+ fixedShift12(delta[2] * toPlayer[2]);
+}
+
+// pan = (x * 640 / z) / 2 + 0x40, which saturates well before the screen edges.
+int getLevel1WorldPan(const int *position) {
+ if (!position[2])
+ return 0x40;
+ return CLIP<int>((position[0] * 640 / position[2]) / 2 + 0x40, 0, 0x7f);
+}
+
+bool spawnLevel1TieShot(RA2PSXLevel1TieShot *shots, const RA2PSXLevel1Enemy &enemy,
+ bool aimed, bool outsideView, const int *shipPosition, Common::RandomSource &random) {
+ int slot = -1;
+ for (int i = 0; i < kLevel1ShotCount; ++i) {
+ if (!shots[i].active) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0)
+ return false;
+
+ RA2PSXLevel1TieShot &shot = shots[slot];
+ shot = RA2PSXLevel1TieShot();
+ shot.active = true;
+ shot.distance = 0x380;
+ shot.length = 0x100;
+
+ RA2PSXMatrix transform;
+ transform.preRotateX(-enemy.rotation[0]);
+ transform.preRotateY(enemy.rotation[1]);
+ transform.preRotateZ(enemy.rotation[2]);
+
+ // Aimed shots converge on the player; in the cockpit that is a point scattered
+ // across the canopy rather than the camera itself.
+ int target[3];
+ if (outsideView) {
+ for (int axis = 0; axis < 3; ++axis)
+ target[axis] = shipPosition[axis];
} else {
- enemy.startX = (float)random.getRandomNumberRng(35, 285);
- enemy.startY = (float)random.getRandomNumberRng(20, 155);
- enemy.controlX = fromLeft ? (float)random.getRandomNumberRng(180, 310) :
- (float)random.getRandomNumberRng(10, 140);
- enemy.controlY = (float)random.getRandomNumberRng(20, 190);
- enemy.endX = fromLeft ? (float)random.getRandomNumberRngSigned(-80, 80) :
- (float)random.getRandomNumberRng(240, 400);
+ target[0] = (int)random.getRandomNumber(999) - 500;
+ target[1] = (int)random.getRandomNumber(799) - 400;
+ target[2] = 0x280;
+ }
+
+ for (int bolt = 0; bolt < 2; ++bolt) {
+ rotateVector(transform, kLevel1TieMuzzle[bolt], shot.origin[bolt]);
+ for (int axis = 0; axis < 3; ++axis)
+ shot.origin[bolt][axis] += enemy.path.position[axis];
+
+ int *direction = shot.direction[bolt];
+ if (aimed) {
+ for (int axis = 0; axis < 3; ++axis)
+ direction[axis] = enemy.path.position[axis] - target[axis];
+ normalizeVector12(direction, direction);
+ } else {
+ const int forward[3] = { 0, 0, -0x1000 };
+ rotateVector(transform, forward, direction);
+ }
+
+ int vector[3] = { direction[0], 0, direction[2] };
+ normalizeVector12(vector, vector);
+ shot.yaw[bolt] = -signedAsinAngle(vector[0]);
+ vector[0] = 0;
+ vector[1] = direction[1];
+ vector[2] = direction[2];
+ normalizeVector12(vector, vector);
+ shot.pitch[bolt] = signedAsinAngle(vector[1]);
+ }
+ return true;
+}
+
+void getLevel1TieShotPosition(const RA2PSXLevel1TieShot &shot, int bolt, int *position) {
+ for (int axis = 0; axis < 3; ++axis)
+ position[axis] = shot.origin[bolt][axis] - (shot.direction[bolt][axis] * shot.distance >> 12);
+}
+
+// Returns false once the bolt is spent; hit is set only when it reaches the player.
+bool updateLevel1TieShot(RA2PSXLevel1TieShot &shot, bool outsideView,
+ const int *shipPosition, bool &hit) {
+ int position[3];
+ getLevel1TieShotPosition(shot, 1, position);
+ shot.length = MIN(0x1000, shot.length + 0x400);
+ shot.distance += 0x380;
+ hit = false;
+
+ if (!outsideView) {
+ if (position[2] > 0x280)
+ return true;
+ hit = position[0] * position[0] + position[1] * position[1] < 450000;
+ return false;
+ }
+
+ const int dx = position[0] - shipPosition[0];
+ const int dy = position[1] - shipPosition[1];
+ const int dz = position[2] - shipPosition[2];
+ if ((int)sqrt((double)dx * dx + (double)dy * dy + (double)dz * dz) < 500) {
+ hit = true;
+ return false;
+ }
+ return position[2] > 200;
+}
+
+void renderLevel1TieShots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &bolt,
+ const RA2PSXLevel1TieShot *shots) {
+ for (int i = 0; i < kLevel1ShotCount; ++i) {
+ if (!shots[i].active)
+ continue;
+ for (int index = 0; index < 2; ++index) {
+ int position[3];
+ getLevel1TieShotPosition(shots[i], index, position);
+ RA2PSXMatrix transform;
+ transform.setScale(0x1000, 0x1000, shots[i].length);
+ transform.preRotateY(shots[i].yaw[index]);
+ transform.preRotateX(shots[i].pitch[index]);
+ transform.setTranslation(position[0], position[1], position[2]);
+ renderer.renderTransformedModel(bolt, transform, false);
+ }
}
- enemy.endY = (float)random.getRandomNumberRngSigned(-25, 245);
- updateLevel1Enemy(enemy);
}
-static void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
+void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
int &directionX, int &directionY, bool left, bool right, bool up, bool down) {
if (left && right)
left = right = false;
@@ -240,7 +602,7 @@ static void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
y = CLIP<int>(y + velocityY / 512, 48, 178);
}
-static void updateLevel1Ship(RA2PSXLevel1Ship &ship,
+void updateLevel1Ship(RA2PSXLevel1Ship &ship,
bool left, bool right, bool up, bool down) {
if (left == right) {
ship.velocityX = ship.velocityX * 3 / 4;
@@ -262,17 +624,22 @@ static void updateLevel1Ship(RA2PSXLevel1Ship &ship,
ship.y = CLIP<int>(ship.y + ship.velocityY * 10 / 4096, -142, 157);
}
-static void getLevel1ShipOrientation(const RA2PSXLevel1Ship &ship,
+// The nose follows both sticks, as the original's ship does: banking yaws it and
+// climbing pitches it, and the guns fire along that nose.
+void getLevel1ShipOrientation(const RA2PSXLevel1Ship &ship,
float &forwardX, float &forwardY, float &forwardZ, float &roll) {
const float bank = (ship.velocityX / 16) * 360.0f / 4096.0f;
+ const float climb = (ship.velocityY / 16) * 360.0f / 4096.0f;
const float yaw = -bank * 0.5f * 0.017453292519943295f;
- forwardX = sinf(yaw);
- forwardY = 0.0f;
- forwardZ = -cosf(yaw);
+ const float pitch = climb * 0.5f * 0.017453292519943295f;
+ const float pitchCosine = cosf(pitch);
+ forwardX = sinf(yaw) * pitchCosine;
+ forwardY = -sinf(pitch);
+ forwardZ = -cosf(yaw) * pitchCosine;
roll = bank;
}
-static void transformLevel1ShipPoint(const RA2PSXLevel1Ship &ship,
+void transformLevel1ShipPoint(const RA2PSXLevel1Ship &ship,
float localX, float localY, float localZ,
float &worldX, float &worldY, float &worldZ) {
float forwardX;
@@ -291,10 +658,17 @@ static void transformLevel1ShipPoint(const RA2PSXLevel1Ship &ship,
forwardZ * localZ;
}
-static bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
- const RA2PSXLevel1Ship *ship, int &targetScreenX, int &targetScreenY) {
+void traceLevel1Shot(RA2PSXLevel1Shot &shot) {
+ const float progress = shot.progress / 4096.0f;
+ shot.trace[0] = shot.traceStart[0] + (shot.targetX - shot.traceStart[0]) * progress;
+ shot.trace[1] = shot.traceStart[1] + (shot.targetY - shot.traceStart[1]) * progress;
+ shot.trace[2] = shot.traceStart[2] + (shot.targetZ - shot.traceStart[2]) * progress;
+}
+
+bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
+ int centerX, int centerY, int focal, const RA2PSXLevel1Ship *ship) {
int slot = -1;
- for (int i = 0; i < 8; ++i) {
+ for (int i = 0; i < kLevel1ShotCount; ++i) {
if (!shots[i].active) {
slot = i;
break;
@@ -304,17 +678,23 @@ static bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
return false;
RA2PSXLevel1Shot &shot = shots[slot];
+ shot = RA2PSXLevel1Shot();
shot.active = true;
- shot.progress = 400;
+ // The original launches from the muzzle in the cockpit and only skips ahead outside.
+ shot.progress = ship ? 400 : 0;
if (!ship) {
for (int i = 0; i < 3; ++i) {
for (int axis = 0; axis < 3; ++axis)
shot.start[i][axis] = kLevel1LaserStart[i][axis];
shot.roll[i] = kLevel1LaserRoll[i];
}
- shot.targetX = (aimX - 160) * 18000.0f / 640.0f;
- shot.targetY = (aimY - 120) * 18000.0f / 640.0f;
+ shot.targetX = (float)(aimX - centerX) * 18000.0f / focal;
+ shot.targetY = (float)(aimY - centerY) * 18000.0f / focal;
shot.targetZ = 18000.0f;
+ // Midway between the two cannon the original alternates between.
+ shot.traceStart[0] = 0.0f;
+ shot.traceStart[1] = 50.0f;
+ shot.traceStart[2] = 400.0f;
} else {
float forwardX;
float forwardY;
@@ -328,30 +708,50 @@ static bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
shot.roll[i] = kLevel1LaserRoll[i] + shipRoll;
}
transformLevel1ShipPoint(*ship, 0.0f, -70.0f, -100.0f,
- shot.targetX, shot.targetY, shot.targetZ);
+ shot.traceStart[0], shot.traceStart[1], shot.traceStart[2]);
+ shot.targetX = shot.traceStart[0];
+ shot.targetY = shot.traceStart[1];
+ shot.targetZ = shot.traceStart[2];
shot.targetX -= forwardX * 18000.0f;
shot.targetY -= forwardY * 18000.0f;
shot.targetZ -= forwardZ * 18000.0f;
}
- targetScreenX = 160 + (int)(shot.targetX * 640.0f / shot.targetZ);
- targetScreenY = 120 + (int)(shot.targetY * 640.0f / shot.targetZ);
+ traceLevel1Shot(shot);
+ for (int axis = 0; axis < 3; ++axis)
+ shot.previousTrace[axis] = shot.trace[axis];
return true;
}
-static void updateLevel1Shots(RA2PSXLevel1Shot *shots) {
- for (int i = 0; i < 8; ++i) {
+void updateLevel1Shots(RA2PSXLevel1Shot *shots) {
+ for (int i = 0; i < kLevel1ShotCount; ++i) {
if (!shots[i].active)
continue;
+ for (int axis = 0; axis < 3; ++axis)
+ shots[i].previousTrace[axis] = shots[i].trace[axis];
shots[i].progress += 200;
if (shots[i].progress > 4399)
shots[i].active = false;
+ else
+ traceLevel1Shot(shots[i]);
}
}
-static void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &laser,
+// ra2FindPlayerShotHit: once the bolt's midpoint is past the TIE it keeps testing every
+// frame, so aiming inside a TIE still scores as the bolt sweeps outwards.
+bool level1ShotHitsEnemy(const RA2PSXLevel1Shot &shot, const int *position) {
+ const float toPrevious = position[2] - shot.previousTrace[2];
+ const float toCurrent = position[2] - shot.trace[2];
+ if (toPrevious * toPrevious >= toCurrent * toCurrent)
+ return false;
+ const float dx = shot.trace[0] - position[0];
+ const float dy = shot.trace[1] - position[1];
+ return dx * dx + dy * dy <= (float)kLevel1HitRadius;
+}
+
+void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &laser,
const RA2PSXLevel1Shot *shots) {
- for (int shotIndex = 0; shotIndex < 8; ++shotIndex) {
+ for (int shotIndex = 0; shotIndex < kLevel1ShotCount; ++shotIndex) {
const RA2PSXLevel1Shot &shot = shots[shotIndex];
if (!shot.active || shot.progress >= 4000)
continue;
@@ -370,18 +770,9 @@ static void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel
}
}
-static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &ui,
- const RA2PSXLevel1Enemy *enemies, const RA2PSXLevel1Explosion *explosions,
- int laserTargetX, int laserTargetY) {
- const uint32 green = surface.format.RGBToColor(64, 255, 96);
-
- for (int i = 0; i < 3; ++i) {
- if (enemies[i].active && enemies[i].laserFrames > 0) {
- surface.drawLine((int)enemies[i].x - 2, (int)enemies[i].y,
- laserTargetX - 15, laserTargetY, green);
- surface.drawLine((int)enemies[i].x + 2, (int)enemies[i].y,
- laserTargetX + 15, laserTargetY, green);
- }
+void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &ui,
+ const RA2PSXLevel1Explosion *explosions) {
+ for (int i = 0; i < kLevel1EnemyCount; ++i) {
if (explosions[i].frames > 0)
ui.drawExplosion(surface, explosions[i].x, explosions[i].y,
10 - explosions[i].frames);
@@ -391,12 +782,14 @@ static void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &
Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const RA2PSXModel &shipModel, const RA2PSXModel &crosshair,
- const RA2PSXModel &laser, const RA2PSXLevel1UI &ui, int lives, int &score) {
+ const RA2PSXModel &laser, const RA2PSXModel &tieLaser,
+ const RA2PSXLevel1UI &ui, int lives, int &score) {
#ifndef USE_TINYGL
(void)enemyModel;
(void)shipModel;
(void)crosshair;
(void)laser;
+ (void)tieLaser;
(void)ui;
(void)lives;
(void)score;
@@ -418,12 +811,18 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
return kLevel1Error;
}
- RA2PSXLevel1Enemy enemies[3];
- RA2PSXLevel1Explosion explosions[3];
- RA2PSXLevel1Shot shots[8];
+ // The renderer's own projection, so aiming and the world agree.
+ const int centerX = _vm->_screenWidth / 2;
+ const int centerY = _vm->_screenHeight / 2;
+ const int focal = _vm->_screenWidth * 2;
+
+ RA2PSXLevel1Enemy enemies[kLevel1EnemyCount];
+ RA2PSXLevel1Explosion explosions[kLevel1EnemyCount];
+ RA2PSXLevel1Shot shots[kLevel1ShotCount];
+ RA2PSXLevel1TieShot tieShots[kLevel1ShotCount];
RA2PSXLevel1Ship ship;
RA2PSXSoundPlayer soundPlayer(_vm, _soundBank);
- RA2PSXSoundPlayer::SoundId approachSounds[3] = {};
+ RA2PSXSoundPlayer::SoundId approachSounds[kLevel1EnemyCount] = {};
// A hard left/right pair for the cockpit and a centred one for outside,
// cross-faded as the view changes.
const RA2PSXSoundPlayer::SoundId engineLeft =
@@ -444,7 +843,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
int aimVelocityY = 0;
int aimDirectionX = 0;
int aimDirectionY = 0;
- int shield = 100;
+ int shield = kLevel1ShieldFull;
int spawnDelay = 0;
int spawnRange = 80;
int spawnBase = 60;
@@ -679,7 +1078,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
// Started once, then left running, as in the original.
- if (shield <= 31 && lowShieldAlarm == RA2PSXSoundPlayer::kInvalidSoundId)
+ if (shield < kLevel1LowShield && lowShieldAlarm == RA2PSXSoundPlayer::kInvalidSoundId)
lowShieldAlarm = soundPlayer.play(kLevel1SfxLowShield, 0x50, 0x40);
if (logicFrame >= nextSpawnAdjustment) {
@@ -688,58 +1087,133 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
spawnBase = MAX(20, spawnBase - 1);
}
+ const int view = thirdPersonView ? 1 : 0;
+ const int shipPosition[3] = { ship.x, ship.y, ship.z };
+ // The cockpit aims from the camera, so the fire gate uses the origin there.
+ const int playerX = thirdPersonView ? ship.x : 0;
+ const int playerY = thirdPersonView ? ship.y : 0;
+
int activeEnemies = 0;
- for (int i = 0; i < 3; ++i)
+ for (int i = 0; i < kLevel1EnemyCount; ++i)
activeEnemies += enemies[i].active ? 1 : 0;
--spawnDelay;
- if (videoFrame < 1599 && activeEnemies < 3 && spawnDelay <= 0) {
- int spawnedEnemy = -1;
- for (int i = 0; i < 3; ++i) {
+ if (videoFrame < 1599 && activeEnemies < kLevel1EnemyCount && spawnDelay <= 0) {
+ for (int i = 0; i < kLevel1EnemyCount; ++i) {
if (!enemies[i].active) {
spawnLevel1Enemy(enemies[i], _vm->_rnd);
- spawnedEnemy = i;
break;
}
}
- if (spawnedEnemy >= 0) {
- const uint16 sfx = _vm->_rnd.getRandomNumber(999) < 800 ?
- kLevel1SfxTieApproachA : kLevel1SfxTieApproachB;
- const int pitch = 0x1c18 + _vm->_rnd.getRandomNumber(1999);
- approachSounds[spawnedEnemy] = soundPlayer.play(sfx, 0x5e, 0x40, pitch);
- }
spawnDelay = spawnBase + _vm->_rnd.getRandomNumber(spawnRange - 1);
}
updateLevel1Shots(shots);
- for (int i = 0; i < 3; ++i) {
+ for (int i = 0; i < kLevel1ShotCount; ++i) {
+ if (!tieShots[i].active)
+ continue;
+ bool hit = false;
+ if (!updateLevel1TieShot(tieShots[i], thirdPersonView, shipPosition, hit))
+ tieShots[i].active = false;
+ if (hit) {
+ shield = MAX(0, shield - kLevel1BoltDamage[view][difficulty]);
+ soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
+ }
+ }
+
+ for (int i = 0; i < kLevel1EnemyCount; ++i) {
if (explosions[i].frames > 0)
--explosions[i].frames;
if (!enemies[i].active)
continue;
- if (enemies[i].laserFrames > 0)
- --enemies[i].laserFrames;
- ++enemies[i].age;
- updateLevel1Enemy(enemies[i]);
- const int soundPan = getLevel1SoundPan(enemies[i].x);
- soundPlayer.setPan(approachSounds[i], soundPan);
- if (enemies[i].age == enemies[i].fireFrame) {
- enemies[i].laserFrames = 4;
- soundPlayer.play(kLevel1SfxTieFire, 0x4e, soundPan);
- if (_vm->_rnd.getRandomNumber(99) < 38) {
- shield = MAX(0, shield - (int)_vm->_rnd.getRandomNumberRng(6, 10));
- soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
+ RA2PSXLevel1Enemy &enemy = enemies[i];
+ advanceSplineObject(enemy.lead);
+ advanceSplineObject(enemy.path);
+ orientTieAlongSpline(enemy, playerX, playerY);
+
+ const int *position = enemy.path.position;
+ const int soundPan = getLevel1WorldPan(position);
+ bool destroyed = false;
+
+ // Close in, a TIE that has not been shot down collides with the player.
+ if (position[2] <= 1499) {
+ bool rammed;
+ if (thirdPersonView) {
+ const int dx = position[0] - ship.x;
+ const int dy = position[1] - ship.y;
+ const int dz = position[2] - ship.z;
+ rammed = (int)sqrt((double)dx * dx + (double)dy * dy +
+ (double)dz * dz) < kLevel1RamRadius[1];
+ } else {
+ rammed = (int)sqrt((double)position[0] * position[0] +
+ (double)position[1] * position[1]) < kLevel1RamRadius[0];
+ }
+ if (rammed) {
+ shield = MAX(0, shield - kLevel1RamDamage[difficulty]);
+ destroyed = true;
}
}
- if (enemies[i].age >= enemies[i].lifetime) {
+
+ if (!destroyed && position[2] < 0) {
+ soundPlayer.stop(approachSounds[i]);
+ approachSounds[i] = RA2PSXSoundPlayer::kInvalidSoundId;
+ enemy.active = false;
+ continue;
+ }
+
+ if (!destroyed) {
+ if (position[2] < 10000 &&
+ approachSounds[i] == RA2PSXSoundPlayer::kInvalidSoundId) {
+ const uint16 sfx = _vm->_rnd.getRandomNumber(999) < 800 ?
+ kLevel1SfxTieApproachA : kLevel1SfxTieApproachB;
+ const int pitch = 0x1c18 + _vm->_rnd.getRandomNumber(1999);
+ approachSounds[i] = soundPlayer.play(sfx, 0x5e, 0x40, pitch);
+ }
+ soundPlayer.setPan(approachSounds[i], soundPan);
+
+ for (int shotIndex = 0; shotIndex < kLevel1ShotCount && !destroyed; ++shotIndex) {
+ if (shots[shotIndex].active &&
+ level1ShotHitsEnemy(shots[shotIndex], position)) {
+ shots[shotIndex].active = false;
+ destroyed = true;
+ score = MIN(9999999, score + kLevel1KillScore[difficulty]);
+ if (score >= nextExtraLife) {
+ // The original also awards a life; the shared runner owns it.
+ soundPlayer.play(kLevel1SfxExtraLife, 0x7f, 0x40);
+ extraLifeStage = MIN(extraLifeStage + 1, 2);
+ nextExtraLife += kLevel1ExtraLife[difficulty][extraLifeStage];
+ }
+ }
+ }
+ }
+
+ // A TIE only shoots while it is still out at range and running square at
+ // the player; most shots are sprayed ahead rather than aimed.
+ if (!destroyed && position[2] < 16000 && --enemy.fireCountdown < 0 &&
+ enemy.facing >= kLevel1FireFacing && position[2] >= 2000) {
+ enemy.fireCountdown = (int)_vm->_rnd.getRandomNumber(44) + 8;
+ const bool aimed = (int)_vm->_rnd.getRandomNumber(99) <
+ kLevel1AimChance[view][difficulty];
+ if (spawnLevel1TieShot(tieShots, enemy, aimed, thirdPersonView,
+ shipPosition, _vm->_rnd))
+ soundPlayer.play(kLevel1SfxTieFire, 0x4e, soundPan);
+ }
+
+ if (destroyed) {
const int pitch = _vm->_rnd.getRandomNumber(0x3fff);
soundPlayer.play(kLevel1SfxTieExplode, 0x5a, soundPan, pitch);
soundPlayer.stop(approachSounds[i]);
approachSounds[i] = RA2PSXSoundPlayer::kInvalidSoundId;
- enemies[i].active = false;
- if (_vm->_rnd.getRandomNumber(99) < 18) {
- shield = MAX(0, shield - 12);
- soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
+ enemy.active = false;
+ for (int slot = 0; slot < kLevel1EnemyCount; ++slot) {
+ if (explosions[slot].frames == 0) {
+ explosions[slot].frames = 10;
+ explosions[slot].x = centerX +
+ position[0] * focal / MAX(1, position[2]);
+ explosions[slot].y = centerY +
+ position[1] * focal / MAX(1, position[2]);
+ break;
+ }
}
}
}
@@ -750,51 +1224,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
fireWasPressed = heldFire;
const bool shootRequested = fireRequested || triggerShot;
fireRequested = false;
- if (shootRequested) {
- int shotTargetX;
- int shotTargetY;
- if (!spawnLevel1Shot(shots, aimX, aimY,
- thirdPersonView ? &ship : nullptr, shotTargetX, shotTargetY))
- continue;
+ if (shootRequested && spawnLevel1Shot(shots, aimX, aimY, centerX, centerY, focal,
+ thirdPersonView ? &ship : nullptr))
soundPlayer.play(kLevel1SfxPlayerFire, 0x3f, 0x40);
- int hitEnemy = -1;
- float hitDistance = 1000000.0f;
- for (int i = 0; i < 3; ++i) {
- if (!enemies[i].active)
- continue;
- const float dx = enemies[i].x - shotTargetX;
- const float dy = enemies[i].y - shotTargetY;
- const float distance = dx * dx + dy * dy;
- const float radius = MAX(10.0f, enemies[i].size * 0.72f);
- if (distance <= radius * radius && distance < hitDistance) {
- hitEnemy = i;
- hitDistance = distance;
- }
- }
- if (hitEnemy >= 0) {
- const int soundPan = getLevel1SoundPan(enemies[hitEnemy].x);
- const int pitch = _vm->_rnd.getRandomNumber(0x3fff);
- soundPlayer.play(kLevel1SfxTieExplode, 0x5a, soundPan, pitch);
- soundPlayer.stop(approachSounds[hitEnemy]);
- approachSounds[hitEnemy] = RA2PSXSoundPlayer::kInvalidSoundId;
- enemies[hitEnemy].active = false;
- score = MIN(9999999, score + kLevel1KillScore[difficulty]);
- if (score >= nextExtraLife) {
- // The original also awards a life; the shared runner owns it.
- soundPlayer.play(kLevel1SfxExtraLife, 0x7f, 0x40);
- extraLifeStage = MIN(extraLifeStage + 1, 2);
- nextExtraLife += kLevel1ExtraLife[difficulty][extraLifeStage];
- }
- for (int i = 0; i < 3; ++i) {
- if (explosions[i].frames == 0) {
- explosions[i].frames = 10;
- explosions[i].x = (int)enemies[hitEnemy].x;
- explosions[i].y = (int)enemies[hitEnemy].y;
- break;
- }
- }
- }
- }
}
if (shield <= 0) {
@@ -804,11 +1236,28 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
if (background && redraw) {
renderer.beginFrame(*background);
- for (int i = 0; i < 3; ++i) {
- if (enemies[i].active)
- renderer.renderModel(enemyModel, enemies[i].x, enemies[i].y, enemies[i].size,
- enemies[i].pitch, enemies[i].yaw, enemies[i].roll);
+ // Everything shares one painter's pass, farthest first, as the original's
+ // ordering table does.
+ int order[kLevel1EnemyCount] = { 0, 1, 2 };
+ for (int i = 0; i < kLevel1EnemyCount; ++i) {
+ for (int j = i + 1; j < kLevel1EnemyCount; ++j) {
+ if (enemies[order[j]].path.position[2] > enemies[order[i]].path.position[2])
+ SWAP(order[i], order[j]);
+ }
+ }
+ for (int i = 0; i < kLevel1EnemyCount; ++i) {
+ const RA2PSXLevel1Enemy &enemy = enemies[order[i]];
+ if (!enemy.active)
+ continue;
+ RA2PSXMatrix transform;
+ transform.setRotationZ(enemy.rotation[2]);
+ transform.preRotateY(enemy.rotation[1]);
+ transform.preRotateX(enemy.rotation[0]);
+ transform.setTranslation(enemy.path.position[0], enemy.path.position[1],
+ enemy.path.position[2]);
+ renderer.renderTransformedModel(enemyModel, transform, false);
}
+ renderLevel1TieShots(renderer, tieLaser, tieShots);
renderLevel1Shots(renderer, laser, shots);
if (thirdPersonView) {
float forwardX;
@@ -824,12 +1273,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
Graphics::Surface output;
renderer.finishFrame(output);
- const int laserTargetX = thirdPersonView ? 160 + ship.x * 640 / ship.z : 160;
- const int laserTargetY = thirdPersonView ? 120 + ship.y * 640 / ship.z : output.h - 1;
- drawLevel1Effects(output, ui, enemies, explosions, laserTargetX, laserTargetY);
+ drawLevel1Effects(output, ui, explosions);
if (!thirdPersonView)
ui.drawCockpit(output);
- ui.drawHUD(output, score, lives, shield, logicFrame);
+ ui.drawHUD(output, score, lives, shield * 100 / kLevel1ShieldFull, logicFrame);
g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
g_system->updateScreen();
}
diff --git a/engines/scumm/insane/rebel2/psx/menu.cpp b/engines/scumm/insane/rebel2/psx/menu.cpp
index 15bc1601ffa..8f2e4608a05 100644
--- a/engines/scumm/insane/rebel2/psx/menu.cpp
+++ b/engines/scumm/insane/rebel2/psx/menu.cpp
@@ -35,7 +35,7 @@
namespace Scumm {
// The menus run one logic step per vertical blank.
-static const int kMenuFrameRate = 60;
+const int kMenuFrameRate = 60;
// Sound effect ids the menus use.
enum {
@@ -173,7 +173,7 @@ struct RA2PSXMenuEvents {
int mouseY;
};
-static void pollMenuEvents(ScummEngine_v7 *vm, RA2PSXMenuEvents &events) {
+void pollMenuEvents(ScummEngine_v7 *vm, RA2PSXMenuEvents &events) {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
switch (event.type) {
@@ -259,7 +259,7 @@ static void pollMenuEvents(ScummEngine_v7 *vm, RA2PSXMenuEvents &events) {
}
}
-static int hitTestMenu(Common::Rect (*itemRect)(int), int count, const Graphics::Surface &surface,
+int hitTestMenu(Common::Rect (*itemRect)(int), int count, const Graphics::Surface &surface,
int x, int y) {
const int xOffset = (surface.w - 320) / 2;
const int yOffset = (surface.h - 240) / 2;
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index a88d3af4bb9..20b00ecca51 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -21,14 +21,14 @@
namespace Scumm {
-static bool readU16(const Common::Array<byte> &data, uint32 offset, uint16 &value) {
+bool readU16(const Common::Array<byte> &data, uint32 offset, uint16 &value) {
if (offset + 2 > data.size())
return false;
value = READ_LE_UINT16(data.data() + offset);
return true;
}
-static bool readU32(const Common::Array<byte> &data, uint32 offset, uint32 &value) {
+bool readU32(const Common::Array<byte> &data, uint32 offset, uint32 &value) {
if (offset + 4 > data.size())
return false;
value = READ_LE_UINT32(data.data() + offset);
@@ -36,7 +36,7 @@ static bool readU32(const Common::Array<byte> &data, uint32 offset, uint32 &valu
}
// One turn is 4096 PlayStation angle units.
-static const float kRA2PSXAngleScale = 0.0015339807878856412f;
+const float kRA2PSXAngleScale = 0.0015339807878856412f;
void RA2PSXMatrix::setIdentity() {
for (uint row = 0; row < 3; ++row) {
@@ -83,7 +83,7 @@ void RA2PSXMatrix::setRotationZ(int angle) {
rotation[1][1] = cosine;
}
-static void preMultiply(RA2PSXMatrix &matrix, const RA2PSXMatrix &left) {
+void preMultiply(RA2PSXMatrix &matrix, const RA2PSXMatrix &left) {
RA2PSXMatrix result;
for (uint row = 0; row < 3; ++row) {
for (uint column = 0; column < 3; ++column) {
@@ -486,7 +486,7 @@ void RA2PSXTinyGLRenderer::setFaceState(const RA2PSXModel &model, const RA2PSXFa
}
}
-static int getRA2PSXDepthCue(float depth, float focalLength) {
+int getRA2PSXDepthCue(float depth, float focalLength) {
if (depth <= 0.0f)
return 4096;
const int quotient = MIN(0x1ffff, (int)(focalLength * 65536.0f / depth));
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index be7f11be0ff..b9375c2869d 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -49,9 +49,9 @@ public:
Level1Handler(Rebel2PSX &psx, const RA2PSXModel &enemy, const RA2PSXModel &ship,
const RA2PSXModel &crosshair, const RA2PSXModel &laser,
- const RA2PSXLevel1UI &ui, int &score) :
+ const RA2PSXModel &tieLaser, const RA2PSXLevel1UI &ui, int &score) :
_psx(psx), _enemy(enemy), _ship(ship), _crosshair(crosshair), _laser(laser),
- _ui(ui), _score(score), _error(kGameplayError) {
+ _tieLaser(tieLaser), _ui(ui), _score(score), _error(kGameplayError) {
}
bool shouldQuit() const override {
@@ -59,7 +59,8 @@ public:
}
Result playAttempt(int &lives) override {
- switch (_psx.playLevel1(_enemy, _ship, _crosshair, _laser, _ui, lives, _score)) {
+ switch (_psx.playLevel1(_enemy, _ship, _crosshair, _laser, _tieLaser, _ui,
+ lives, _score)) {
case Rebel2PSX::kLevel1Quit:
return kQuit;
case Rebel2PSX::kLevel1Complete:
@@ -106,6 +107,7 @@ private:
const RA2PSXModel &_ship;
const RA2PSXModel &_crosshair;
const RA2PSXModel &_laser;
+ const RA2PSXModel &_tieLaser;
const RA2PSXLevel1UI &_ui;
int &_score;
Error _error;
@@ -252,7 +254,8 @@ bool Rebel2PSX::loadMovieTextAssets(RA2PSXMovieText &movieText) {
}
bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
- RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui) {
+ RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXModel &tieLaser,
+ RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
@@ -266,6 +269,7 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
Common::Array<byte> shipData;
Common::Array<byte> crosshairData;
Common::Array<byte> laserData;
+ Common::Array<byte> tieLaserData;
Common::Array<byte> enemyTextureData;
Common::Array<byte> shipTextureData;
return archive.getMember("fOFS/TieFighter/main", enemyData) && enemy.load(enemyData) &&
@@ -274,6 +278,7 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
archive.getMember("tex/BWingCockp", shipTextureData) && ship.loadTextures(shipTextureData) &&
archive.getMember("fOFS/CrosshairW", crosshairData) && crosshair.load(crosshairData) &&
archive.getMember("fOFS/WingLaser", laserData) && laser.load(laserData) &&
+ archive.getMember("fOFS/TieLaser", tieLaserData) && tieLaser.load(tieLaserData) &&
ui.load(archive);
}
@@ -322,8 +327,9 @@ Common::Error Rebel2PSX::runGame() {
RA2PSXModel ship;
RA2PSXModel crosshair;
RA2PSXModel laser;
+ RA2PSXModel tieLaser;
RA2PSXLevel1UI ui;
- if (!loadLevel1Assets(enemy, ship, crosshair, laser, ui))
+ if (!loadLevel1Assets(enemy, ship, crosshair, laser, tieLaser, ui))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation Level 1 resources"));
@@ -337,7 +343,7 @@ Common::Error Rebel2PSX::runGame() {
int lives = 3;
int score = 0;
- Level1Handler handler(*this, enemy, ship, crosshair, laser, ui, score);
+ Level1Handler handler(*this, enemy, ship, crosshair, laser, tieLaser, ui, score);
if (runRebel2Level1(handler, lives) != Rebel2Level1Handler::kError)
return Common::kNoError;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index e8dd3b72a4c..fb7f4041620 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -315,10 +315,11 @@ private:
bool loadGlobalAssets(RA2PSXMainMenuUI &menu);
bool loadMovieTextAssets(RA2PSXMovieText &movieText);
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
- RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXLevel1UI &ui);
+ RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXModel &tieLaser,
+ RA2PSXLevel1UI &ui);
Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
const RA2PSXModel &crosshair, const RA2PSXModel &laser,
- const RA2PSXLevel1UI &ui, int lives, int &score);
+ const RA2PSXModel &tieLaser, const RA2PSXLevel1UI &ui, int lives, int &score);
ScummEngine_v7 *_vm;
RA2PSXSoundBank _soundBank;
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index e22b4617596..69eec1f35da 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -29,7 +29,7 @@ struct RA2PSXUIGradientStop {
RA2PSXUIColor color;
};
-static RA2PSXUIColor interpolateColor(const RA2PSXUIColor &from,
+RA2PSXUIColor interpolateColor(const RA2PSXUIColor &from,
const RA2PSXUIColor &to, int value, int maximum) {
if (maximum <= 0)
return from;
@@ -40,7 +40,7 @@ static RA2PSXUIColor interpolateColor(const RA2PSXUIColor &from,
return color;
}
-static RA2PSXUIColor shieldColor(const RA2PSXUIGradientStop *stops, uint count, int index) {
+RA2PSXUIColor shieldColor(const RA2PSXUIGradientStop *stops, uint count, int index) {
for (uint i = 1; i < count; ++i) {
if (index <= stops[i].index)
return interpolateColor(stops[i - 1].color, stops[i].color,
@@ -205,8 +205,8 @@ void drawRA2PSXMenuHints(Graphics::Surface &surface, const RA2PSXTextureSet &tex
textures.draw(surface, "SELECT", xOffset + 252, yOffset + 216, Common::Rect(0, 0, 56, 11));
}
-static const char kSmallGlyphs[] = "abcdefghijklmnopqrstuvwxyz0123456789%-:.?+/C ";
-static const byte kSmallWidths[] = {
+const char kSmallGlyphs[] = "abcdefghijklmnopqrstuvwxyz0123456789%-:.?+/C ";
+const byte kSmallWidths[] = {
6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
6, 6, 6, 6, 6, 6, 8, 6, 7, 6, 6, 4, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 2, 2, 6, 6, 6, 8, 2
@@ -214,7 +214,7 @@ static const byte kSmallWidths[] = {
static_assert(ARRAYSIZE(kSmallGlyphs) == ARRAYSIZE(kSmallWidths) + 1,
"RA2 PSX glyph widths do not match the font map");
-static int findSmallGlyph(char character) {
+int findSmallGlyph(char character) {
for (uint i = 0; i < ARRAYSIZE(kSmallWidths); ++i) {
if (character == kSmallGlyphs[i])
return i;
@@ -274,27 +274,27 @@ struct RA2PSXMovieTextRecord {
const char *text;
};
-static const byte kMovieBigAdvances[] = {
+const byte kMovieBigAdvances[] = {
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 16, 8, 8, 8, 8,
8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 16, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 16,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8
};
-static const byte kMovieBigWidths[] = {
+const byte kMovieBigWidths[] = {
6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 10, 6, 6, 6, 6,
6, 6, 6, 6, 8, 14, 6, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6,
6, 2, 4, 6, 2, 10, 6, 4, 6, 6, 6, 4, 6, 6, 6, 6, 10,
6, 6, 6, 8, 2, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 8, 4
};
-static const byte kMovieSmallWidths[] = {
+const byte kMovieSmallWidths[] = {
6, 6, 6, 6, 6, 6, 6, 6, 2, 6, 6, 6, 8, 6, 6, 6,
6, 6, 6, 6, 6, 6, 8, 6, 6, 6, 6, 4, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 2, 2, 6, 2, 6, 10, 10, 10, 10, 2
};
-static const byte kMovieTinyWidths[] = {
+const byte kMovieTinyWidths[] = {
10, 10, 10, 10, 10, 10, 12, 12, 4, 10, 10, 8, 14, 10, 12,
10, 12, 10, 10, 12, 12, 12, 14, 10, 12, 10, 8, 8, 8, 8,
8, 6, 8, 8, 4, 6, 8, 4, 12, 8, 8, 8, 8, 6, 8, 6, 8, 8,
@@ -303,21 +303,21 @@ static const byte kMovieTinyWidths[] = {
6, 6, 6, 6, 8, 6, 4, 6
};
-static const RA2PSXMovieFont kMovieBigFont = {
+const RA2PSXMovieFont kMovieBigFont = {
"fNT24b",
"ABCDEFGHIJKLMN" "\x01" "OPQRSTUVWXYZ-" "\x01"
"abcdefghijklmn," "\x01" "opqrstuvwxyz%." "\x01" "0123456789? ",
kMovieBigWidths, kMovieBigAdvances, 0, 16, 16
};
-static const RA2PSXMovieFont kMovieSmallFont = {
+const RA2PSXMovieFont kMovieSmallFont = {
"fNT24s",
"abcdefghijkl" "\x01" "mnopqrstuvwx" "\x01" "yz0123456789" "\x01"
"%-:.? /{}[]|",
kMovieSmallWidths, nullptr, 12, 10, 10
};
-static const RA2PSXMovieFont kMovieTinyFont = {
+const RA2PSXMovieFont kMovieTinyFont = {
"fNT24t",
"ABCDEFGHIJ" "\x01" "KLMNOPQRST" "\x01" "UVWXYZabcd" "\x01"
"efghijklmn" "\x01" "opqrstuvwx" "\x01" "yz01234567" "\x01"
@@ -325,7 +325,7 @@ static const RA2PSXMovieFont kMovieTinyFont = {
kMovieTinyWidths, nullptr, 16, 16, 17
};
-static const RA2PSXMovieTextRecord kOpeningText[] = {
+const RA2PSXMovieTextRecord kOpeningText[] = {
{ 720, 30, 1, 2, 40, 0, 65, "starring" },
{ 720, 30, 6, 129, 40, 70, 85, "Jamison Jones" },
{ 720, 30, 6, 129, 40, 250, 100, "Julie Eccles" },
@@ -383,12 +383,12 @@ static const RA2PSXMovieTextRecord kOpeningText[] = {
{ 1366, 30, 7, 129, 40, 0, 100, "George Lucas" }
};
-static const RA2PSXMovieTextRecord kChapter1Text[] = {
+const RA2PSXMovieTextRecord kChapter1Text[] = {
{ 30, 80, 1, 1, 0, 0, 24, "chapter 1" },
{ 40, 70, 3, 128, 0, 0, 37, "The Dreighton Triangle" }
};
-static bool findMovieGlyph(const RA2PSXMovieFont &font, char character,
+bool findMovieGlyph(const RA2PSXMovieFont &font, char character,
RA2PSXMovieGlyph &glyph) {
int metric = 0;
int row = 0;
@@ -411,7 +411,7 @@ static bool findMovieGlyph(const RA2PSXMovieFont &font, char character,
return false;
}
-static const RA2PSXMovieFont &getMovieFont(byte style) {
+const RA2PSXMovieFont &getMovieFont(byte style) {
if (style == 2 || style == 3)
return kMovieBigFont;
if (style == 6 || style == 7)
@@ -419,7 +419,7 @@ static const RA2PSXMovieFont &getMovieFont(byte style) {
return kMovieSmallFont;
}
-static int measureMovieText(const RA2PSXMovieFont &font, const char *text,
+int measureMovieText(const RA2PSXMovieFont &font, const char *text,
uint characters, int spacing) {
int width = 0;
uint drawn = 0;
@@ -449,14 +449,14 @@ void RA2PSXTextureSet::drawHeadline(Graphics::Surface &surface, const char *text
}
}
-static int scaleMovieX(int x) {
+int scaleMovieX(int x) {
const int remainder = x % 3;
if (remainder)
x += 3 - remainder;
return x * 2 / 3;
}
-static int findMovieFontArchive(const Common::Array<byte> &data) {
+int findMovieFontArchive(const Common::Array<byte> &data) {
for (uint offset = 0; offset + 40 <= data.size(); offset += 4) {
if (!memcmp(data.data() + offset, "fNT24s", 7) &&
!memcmp(data.data() + offset + 16, "fNT24b", 7) &&
@@ -593,8 +593,8 @@ Common::Rect RA2PSXMainMenuUI::itemRect(int item) {
}
// Row positions from the original widget tables.
-static const int16 kOptionRowY[] = { 70, 85, 100, 115, 130, 145, 160, 190 };
-static const int16 kSoundRowY[] = { 87, 102, 117, 132, 162 };
+const int16 kOptionRowY[] = { 70, 85, 100, 115, 130, 145, 160, 190 };
+const int16 kSoundRowY[] = { 87, 102, 117, 132, 162 };
Common::Rect RA2PSXOptionsUI::mainItemRect(int item) {
const int y = kOptionRowY[CLIP<int>(item, 0, ARRAYSIZE(kOptionRowY) - 1)];
@@ -793,7 +793,7 @@ struct RA2PSXChapterEntry {
int16 barWidth;
};
-static const RA2PSXChapterEntry kChapters[RA2PSXChapterSelectUI::kChapterCount] = {
+const RA2PSXChapterEntry kChapters[RA2PSXChapterSelectUI::kChapterCount] = {
{ "chapter 1:", 142, "the dreighton triangle", 52, 158 },
{ "chapter 2:", 110, "the corellia star", 110, 122 },
{ "chapter 3:", 140, "mining tunnels", 110, 100 },
diff --git a/engines/scumm/insane/rebel2/psx/video.cpp b/engines/scumm/insane/rebel2/psx/video.cpp
index f714b6c2f5a..826712933ea 100644
--- a/engines/scumm/insane/rebel2/psx/video.cpp
+++ b/engines/scumm/insane/rebel2/psx/video.cpp
@@ -26,11 +26,11 @@
namespace Scumm {
-static const uint32 kRawSectorSize = 2352;
-static const uint32 kFrameDataOffset = 56;
-static const byte kSectorTypeMask = 0x0e;
-static const byte kDataSectorType = 0x08;
-static const byte kVideoSectorType = 0x02;
+const uint32 kRawSectorSize = 2352;
+const uint32 kFrameDataOffset = 56;
+const byte kSectorTypeMask = 0x0e;
+const byte kDataSectorType = 0x08;
+const byte kVideoSectorType = 0x02;
class RA2PSXVideoReadStream final : public Common::SeekableReadStream {
public:
diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index d3353bd2482..660c5dd0c53 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -44,11 +44,11 @@ int getRebel2IndicatorScale(int width, int height) {
return (width >= 640 || height >= 400) ? 2 : 1;
}
-static bool isValidEmbeddedFrame(const InsaneRebel2::EmbeddedSanFrame &frame) {
+bool isValidEmbeddedFrame(const InsaneRebel2::EmbeddedSanFrame &frame) {
return frame.valid && frame.pixels && frame.width > 0 && frame.height > 0;
}
-static int countEmbeddedFramePixels(const InsaneRebel2::EmbeddedSanFrame &frame) {
+int countEmbeddedFramePixels(const InsaneRebel2::EmbeddedSanFrame &frame) {
if (!isValidEmbeddedFrame(frame))
return 0;
@@ -72,7 +72,7 @@ bool parseRebel2TextOverlayFormat(const char *&str, NutRenderer *&curFont, int &
return fontId == -2;
}
-static Common::String getRebel2VisibleTextPrefix(const char *text, int visibleChars) {
+Common::String getRebel2VisibleTextPrefix(const char *text, int visibleChars) {
Common::String out;
if (!text || visibleChars <= 0)
return out;
@@ -102,7 +102,7 @@ static Common::String getRebel2VisibleTextPrefix(const char *text, int visibleCh
return out;
}
-static const char *getRebel2LevelEndFallbackString(int id) {
+const char *getRebel2LevelEndFallbackString(int id) {
switch (id) {
case 190:
return "^f02^c244Chapter Complete\n^f01^c244Password: %s";
@@ -172,7 +172,7 @@ void drawHandler8PovOverlayText(const Rebel2FontSet &fontSet, byte *renderBitmap
drawRebel2String(fontSet, str, strlen(str), renderBitmap, clipRect, x, y, pitch, color, flags);
}
-static void blitEmbeddedFrameRegion(byte *renderBitmap, int pitch, int clipWidth, int clipHeight,
+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)
@@ -204,7 +204,7 @@ static void blitEmbeddedFrameRegion(byte *renderBitmap, int pitch, int clipWidth
}
}
-static bool readEmbeddedSanChunkHeader(Common::SeekableReadStream &stream, int64 containerEnd, const char *context,
+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)
@@ -1939,7 +1939,7 @@ void renderNutSpriteClipped(byte *dst, int pitch, int dstH,
}
}
-static void renderNutSpriteScaledClipped(byte *dst, int pitch, int width, int height,
+void renderNutSpriteScaledClipped(byte *dst, int pitch, int width, int height,
int clipLeft, int clipTop, int clipRight, int clipBottom,
int x, int y, NutRenderer *nut, int spriteIdx, bool mirror, int scale, bool transparent231) {
if (!nut || spriteIdx < 0 || spriteIdx >= nut->getNumChars() || !dst)
@@ -3196,7 +3196,7 @@ void InsaneRebel2::renderHandler7FlySprite(byte *renderBitmap, int pitch, int wi
}
}
-static Common::Point getHandler7SpriteDrawPoint(const Common::Point &base, NutRenderer *nut, int spriteIndex) {
+Common::Point getHandler7SpriteDrawPoint(const Common::Point &base, NutRenderer *nut, int spriteIndex) {
Common::Point point = base;
if (nut && spriteIndex >= 0 && spriteIndex < nut->getNumChars()) {
point.x += nut->getCharXOffset(spriteIndex);
Commit: 68f27cca0512c3f68c64efc4ae10b2cd8c3bbf6d
https://github.com/scummvm/scummvm/commit/68f27cca0512c3f68c64efc4ae10b2cd8c3bbf6d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: added correct explosions and debris among others in L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 4102c542c9f..04d15e45dea 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -83,8 +83,11 @@ enum {
kLevel1LeadFrames = 30,
kLevel1EnemyCount = 3,
kLevel1ShotCount = 8,
- kLevel1ShieldFull = 0x1000,
- kLevel1LowShield = 0x501,
+ kLevel1ShieldFull = kRA2PSXShieldFull,
+ kLevel1LowShield = kRA2PSXLowShield,
+ // The gauge chases the real value by 40 a frame, and a hit shakes the view.
+ kLevel1ShieldStep = 0x28,
+ kLevel1ShakeFrames = 6,
kLevel1FireFacing = 0xf3c,
kLevel1HitRadius = 0xc000
};
@@ -314,14 +317,218 @@ struct RA2PSXLevel1Enemy {
int fireCountdown;
};
+// The fireball billboard is scaled about eight times its 68x56 texture, drifts toward
+// the camera and runs one frame per tick; only five may burn at once.
+enum {
+ kLevel1ExplosionSlots = 5,
+ kLevel1ExplosionScale = 0x7fff,
+ kLevel1ExplosionDrift = 0x32,
+ kLevel1DebrisCount = 16,
+ kLevel1DebrisLife = 30
+};
+
struct RA2PSXLevel1Explosion {
- RA2PSXLevel1Explosion() : frames(0), x(0), y(0) {}
+ RA2PSXLevel1Explosion() : frame(-1), rotation(0) {
+ position[0] = position[1] = position[2] = 0;
+ }
- int frames;
- int x;
- int y;
+ int frame;
+ int rotation;
+ int position[3];
};
+struct RA2PSXLevel1Debris {
+ RA2PSXLevel1Debris() : life(0), model(0) {
+ for (int axis = 0; axis < 3; ++axis)
+ position[axis] = velocity[axis] = rotation[axis] = spin[axis] = 0;
+ }
+
+ int life;
+ int model;
+ int position[3];
+ int velocity[3];
+ int rotation[3];
+ int spin[3];
+};
+
+void spawnLevel1Explosion(RA2PSXLevel1Explosion *explosions, const int *position,
+ Common::RandomSource &random) {
+ int slot = -1;
+ int oldest = -1;
+ for (int i = 0; i < kLevel1ExplosionSlots; ++i) {
+ if (explosions[i].frame < 0) {
+ slot = i;
+ break;
+ }
+ if (explosions[i].frame > oldest) {
+ oldest = explosions[i].frame;
+ slot = i;
+ }
+ }
+
+ RA2PSXLevel1Explosion &explosion = explosions[slot];
+ explosion.frame = 0;
+ explosion.rotation = (int)random.getRandomNumber(15) * 0x100;
+ for (int axis = 0; axis < 3; ++axis)
+ explosion.position[axis] = position[axis];
+}
+
+void updateLevel1Explosions(RA2PSXLevel1Explosion *explosions, int frameCount) {
+ for (int i = 0; i < kLevel1ExplosionSlots; ++i) {
+ if (explosions[i].frame < 0)
+ continue;
+ if (++explosions[i].frame >= frameCount) {
+ explosions[i].frame = -1;
+ continue;
+ }
+ explosions[i].position[2] -= kLevel1ExplosionDrift;
+ }
+}
+
+void renderLevel1Explosions(RA2PSXTinyGLRenderer &renderer,
+ const Common::Array<RA2PSXTexture> &frames, const RA2PSXLevel1Explosion *explosions) {
+ for (int i = 0; i < kLevel1ExplosionSlots; ++i) {
+ const RA2PSXLevel1Explosion &explosion = explosions[i];
+ if (explosion.frame < 0 || (uint)explosion.frame >= frames.size())
+ continue;
+ const RA2PSXTexture &frame = frames[explosion.frame];
+ const float scale = kLevel1ExplosionScale / 4096.0f;
+ renderer.renderSprite(frame, (float)explosion.position[0], (float)explosion.position[1],
+ (float)explosion.position[2], frame.width * 0.5f * scale,
+ frame.height * 0.5f * scale, explosion.rotation);
+ }
+}
+
+// Each shard gets a tumble and a shove back toward the camera; one spin axis is dropped
+// so the pieces do not all rotate the same way.
+int debrisImpulse(Common::RandomSource &random) {
+ const int magnitude = (int)random.getRandomNumber(31) + 16;
+ return random.getRandomBit() ? magnitude : -magnitude;
+}
+
+void spawnLevel1Debris(RA2PSXLevel1Debris *debris, const int *position,
+ const int *rotation, int count, int modelCount, Common::RandomSource &random) {
+ for (int piece = 0; piece < count; ++piece) {
+ int slot = -1;
+ for (int i = 0; i < kLevel1DebrisCount; ++i) {
+ if (!debris[i].life) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0)
+ return;
+
+ RA2PSXLevel1Debris &shard = debris[slot];
+ shard = RA2PSXLevel1Debris();
+ shard.life = kLevel1DebrisLife;
+ shard.model = modelCount ? (int)random.getRandomNumber(modelCount - 1) : 0;
+ for (int axis = 0; axis < 3; ++axis) {
+ shard.position[axis] = position[axis];
+ shard.rotation[axis] = rotation[axis];
+ shard.spin[axis] = debrisImpulse(random);
+ }
+ shard.velocity[0] = debrisImpulse(random);
+ shard.velocity[1] = debrisImpulse(random);
+ shard.velocity[2] = -((int)random.getRandomNumber(63) + 32);
+ shard.spin[(int)random.getRandomNumber(2)] = 0;
+ }
+}
+
+void updateLevel1Debris(RA2PSXLevel1Debris *debris) {
+ for (int i = 0; i < kLevel1DebrisCount; ++i) {
+ if (!debris[i].life)
+ continue;
+ for (int axis = 0; axis < 3; ++axis) {
+ debris[i].position[axis] += debris[i].velocity[axis];
+ debris[i].rotation[axis] = (int16)(debris[i].rotation[axis] + debris[i].spin[axis]);
+ }
+ if (--debris[i].life && debris[i].position[2] < 0)
+ debris[i].life = 0;
+ }
+}
+
+void renderLevel1Debris(RA2PSXTinyGLRenderer &renderer,
+ const Common::Array<RA2PSXModel> &models, const RA2PSXLevel1Debris *debris) {
+ if (models.empty())
+ return;
+ for (int i = 0; i < kLevel1DebrisCount; ++i) {
+ if (!debris[i].life)
+ continue;
+ RA2PSXMatrix transform;
+ transform.setRotationZ(debris[i].rotation[2]);
+ transform.preRotateY(debris[i].rotation[1]);
+ transform.preRotateX(debris[i].rotation[0]);
+ transform.setTranslation(debris[i].position[0], debris[i].position[1],
+ debris[i].position[2]);
+ renderer.renderTransformedModel(models[debris[i].model % models.size()], transform, false);
+ }
+}
+
+// The gameplay frames are wider and taller than the screen; the view slides around
+// inside them and tilts, following a twenty frame average of where the player is aiming.
+enum {
+ kLevel1ViewHistory = 20,
+ kLevel1ViewCenterX = 0x34,
+ kLevel1ViewCenterY = 0x13,
+ kLevel1ViewTiltBase = 0x10
+};
+
+struct RA2PSXLevel1ViewTracker {
+ RA2PSXLevel1ViewTracker() : next(0), filled(0) {
+ for (int i = 0; i < kLevel1ViewHistory; ++i)
+ historyX[i] = historyY[i] = 0;
+ }
+
+ void push(int x, int y) {
+ historyX[next] = x;
+ historyY[next] = y;
+ next = (next + 1) % kLevel1ViewHistory;
+ filled = MIN(filled + 1, (int)kLevel1ViewHistory);
+ }
+
+ void average(int &x, int &y) const {
+ int totalX = 0;
+ int totalY = 0;
+ for (int i = 0; i < kLevel1ViewHistory; ++i) {
+ totalX += historyX[i];
+ totalY += historyY[i];
+ }
+ x = totalX / kLevel1ViewHistory;
+ y = totalY / kLevel1ViewHistory;
+ }
+
+ int historyX[kLevel1ViewHistory];
+ int historyY[kLevel1ViewHistory];
+ int next;
+ int filled;
+};
+
+// The original scales by 0x400000 then shifts back down by 11, rounding away from zero.
+int scaleViewOffset(int value, int divisor) {
+ const int scaled = (int)((int64)value * 0x400000 / divisor);
+ return (scaled + (scaled < 0 ? -0x400 : 0x400)) >> 11;
+}
+
+void getLevel1BackgroundView(const RA2PSXLevel1ViewTracker &tracker,
+ const Graphics::Surface &background, int width, int height,
+ RA2PSXBackgroundView &view) {
+ int averageX = 0;
+ int averageY = 0;
+ tracker.average(averageX, averageY);
+
+ const int tilt = scaleViewOffset(averageX, 0x4f00);
+ view.tiltLeft = kLevel1ViewTiltBase - tilt;
+ view.tiltRight = kLevel1ViewTiltBase + tilt;
+ view.panX = kLevel1ViewCenterX + scaleViewOffset(averageX, 5220);
+ // The band sits eight rows below the top of the port's cropped frame.
+ view.panY = kLevel1ViewCenterY + scaleViewOffset(averageY, 0x1ce3) - 8;
+
+ const int slack = MAX(0, (int)background.h - height) - MAX(view.tiltLeft, view.tiltRight);
+ view.panX = CLIP(view.panX, 0, MAX(0, (int)background.w - width));
+ view.panY = CLIP(view.panY, 0, MAX(0, slack));
+}
+
struct RA2PSXLevel1TieShot {
RA2PSXLevel1TieShot() : active(false), distance(0), length(0) {}
@@ -770,26 +977,20 @@ void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &laser,
}
}
-void drawLevel1Effects(Graphics::Surface &surface, const RA2PSXLevel1UI &ui,
- const RA2PSXLevel1Explosion *explosions) {
- for (int i = 0; i < kLevel1EnemyCount; ++i) {
- if (explosions[i].frames > 0)
- ui.drawExplosion(surface, explosions[i].x, explosions[i].y,
- 10 - explosions[i].frames);
- }
-}
#endif
Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const RA2PSXModel &shipModel, const RA2PSXModel &crosshair,
const RA2PSXModel &laser, const RA2PSXModel &tieLaser,
- const RA2PSXLevel1UI &ui, int lives, int &score) {
+ const Common::Array<RA2PSXModel> &debrisModels, const RA2PSXLevel1UI &ui,
+ int lives, int &score) {
#ifndef USE_TINYGL
(void)enemyModel;
(void)shipModel;
(void)crosshair;
(void)laser;
(void)tieLaser;
+ (void)debrisModels;
(void)ui;
(void)lives;
(void)score;
@@ -817,7 +1018,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const int focal = _vm->_screenWidth * 2;
RA2PSXLevel1Enemy enemies[kLevel1EnemyCount];
- RA2PSXLevel1Explosion explosions[kLevel1EnemyCount];
+ RA2PSXLevel1Explosion explosions[kLevel1ExplosionSlots];
+ RA2PSXLevel1Debris debris[kLevel1DebrisCount];
+ RA2PSXLevel1ViewTracker viewTracker;
RA2PSXLevel1Shot shots[kLevel1ShotCount];
RA2PSXLevel1TieShot tieShots[kLevel1ShotCount];
RA2PSXLevel1Ship ship;
@@ -844,6 +1047,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
int aimDirectionX = 0;
int aimDirectionY = 0;
int shield = kLevel1ShieldFull;
+ int shieldDisplayed = kLevel1ShieldFull;
+ int shakeFrames = 0;
+ int shakeX = 0;
+ int shakeY = 0;
int spawnDelay = 0;
int spawnRange = 80;
int spawnBase = 60;
@@ -1077,8 +1284,23 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
CLIP<int>(ship.x * 4096 / 0x479f + 64, 0, 127));
}
+ if (shieldDisplayed != shield) {
+ shieldDisplayed = shieldDisplayed < shield ?
+ MIN(shield, shieldDisplayed + kLevel1ShieldStep) :
+ MAX(shield, shieldDisplayed - kLevel1ShieldStep);
+ shieldDisplayed = MAX(1, shieldDisplayed);
+ }
+ if (shakeFrames > 0) {
+ --shakeFrames;
+ shakeX = (int)_vm->_rnd.getRandomNumber(3);
+ shakeY = (int)_vm->_rnd.getRandomNumber(3);
+ } else {
+ shakeX = shakeY = 0;
+ }
+
// Started once, then left running, as in the original.
- if (shield < kLevel1LowShield && lowShieldAlarm == RA2PSXSoundPlayer::kInvalidSoundId)
+ if (shieldDisplayed < kLevel1LowShield &&
+ lowShieldAlarm == RA2PSXSoundPlayer::kInvalidSoundId)
lowShieldAlarm = soundPlayer.play(kLevel1SfxLowShield, 0x50, 0x40);
if (logicFrame >= nextSpawnAdjustment) {
@@ -1108,6 +1330,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
updateLevel1Shots(shots);
+ updateLevel1Explosions(explosions, _explosionFrames.size());
+ updateLevel1Debris(debris);
+ viewTracker.push(thirdPersonView ? ship.x * focal / MAX(1, ship.z) : aimX - centerX,
+ thirdPersonView ? ship.y * focal / MAX(1, ship.z) : aimY - centerY);
for (int i = 0; i < kLevel1ShotCount; ++i) {
if (!tieShots[i].active)
continue;
@@ -1116,13 +1342,12 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
tieShots[i].active = false;
if (hit) {
shield = MAX(0, shield - kLevel1BoltDamage[view][difficulty]);
+ shakeFrames = kLevel1ShakeFrames;
soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
}
}
for (int i = 0; i < kLevel1EnemyCount; ++i) {
- if (explosions[i].frames > 0)
- --explosions[i].frames;
if (!enemies[i].active)
continue;
@@ -1150,6 +1375,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
if (rammed) {
shield = MAX(0, shield - kLevel1RamDamage[difficulty]);
+ shakeFrames = kLevel1ShakeFrames;
destroyed = true;
}
}
@@ -1205,16 +1431,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
soundPlayer.stop(approachSounds[i]);
approachSounds[i] = RA2PSXSoundPlayer::kInvalidSoundId;
enemy.active = false;
- for (int slot = 0; slot < kLevel1EnemyCount; ++slot) {
- if (explosions[slot].frames == 0) {
- explosions[slot].frames = 10;
- explosions[slot].x = centerX +
- position[0] * focal / MAX(1, position[2]);
- explosions[slot].y = centerY +
- position[1] * focal / MAX(1, position[2]);
- break;
- }
- }
+ spawnLevel1Explosion(explosions, position, _vm->_rnd);
+ spawnLevel1Debris(debris, position, enemy.rotation,
+ (int)_vm->_rnd.getRandomNumber(4) + 1, debrisModels.size(), _vm->_rnd);
}
}
@@ -1235,7 +1454,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
if (background && redraw) {
- renderer.beginFrame(*background);
+ RA2PSXBackgroundView view;
+ getLevel1BackgroundView(viewTracker, *background, _vm->_screenWidth,
+ _vm->_screenHeight, view);
+ renderer.beginFrame(*background, view);
// Everything shares one painter's pass, farthest first, as the original's
// ordering table does.
int order[kLevel1EnemyCount] = { 0, 1, 2 };
@@ -1259,6 +1481,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
renderLevel1TieShots(renderer, tieLaser, tieShots);
renderLevel1Shots(renderer, laser, shots);
+ renderLevel1Debris(renderer, debrisModels, debris);
+ renderLevel1Explosions(renderer, _explosionFrames, explosions);
if (thirdPersonView) {
float forwardX;
float forwardY;
@@ -1273,11 +1497,11 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
Graphics::Surface output;
renderer.finishFrame(output);
- drawLevel1Effects(output, ui, explosions);
if (!thirdPersonView)
ui.drawCockpit(output);
- ui.drawHUD(output, score, lives, shield * 100 / kLevel1ShieldFull, logicFrame);
- g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0, output.w, output.h);
+ ui.drawHUD(output, score, lives, shieldDisplayed, logicFrame);
+ g_system->copyRectToScreen(output.getPixels(), output.pitch, shakeX, shakeY,
+ output.w - shakeX, output.h - shakeY);
g_system->updateScreen();
}
g_system->delayMillis(5);
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index 20b00ecca51..ecc6f02cba2 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -179,6 +179,45 @@ bool loadRA2PSXTextures(const Common::Array<byte> &data,
return textures.size() > initialCount;
}
+bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeight,
+ Common::Array<RA2PSXTexture> &frames) {
+ if (data.size() < 4 + 512 || !frameHeight)
+ return false;
+ const uint16 widthField = READ_LE_UINT16(data.data());
+ const uint16 width = (widthField & 0xff) ? widthField & 0xff : 256;
+ if (!(widthField & 0x100) || !width)
+ return false;
+
+ const uint32 paletteOffset = 4;
+ const uint32 pixelsOffset = paletteOffset + 512;
+ const uint32 frameBytes = (uint32)width * frameHeight;
+ const uint32 frameCount = (data.size() - pixelsOffset) / frameBytes;
+ const uint32 height = frameHeight;
+ if (!frameCount)
+ return false;
+
+ for (uint32 frame = 0; frame < frameCount; ++frame) {
+ RA2PSXTexture texture;
+ texture.width = width;
+ texture.height = (uint16)height;
+ texture.pixels.resize(frameBytes);
+ for (uint32 i = 0; i < frameBytes; ++i) {
+ const byte paletteIndex = data[pixelsOffset + frame * frameBytes + i];
+ const uint16 value = READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2);
+ if (!value) {
+ texture.pixels[i] = 0;
+ continue;
+ }
+ const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
+ const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
+ const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
+ texture.pixels[i] = 0x01000000 | (r << 16) | (g << 8) | b;
+ }
+ frames.push_back(texture);
+ }
+ return true;
+}
+
RA2PSXModel::RA2PSXModel() : _radius(1.0f) {
}
@@ -541,6 +580,14 @@ void RA2PSXTinyGLRenderer::setFaceColor(const RA2PSXFace &face, uint vertexIndex
}
void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background) {
+ RA2PSXBackgroundView view;
+ view.panX = (background.w - MIN<int>(background.w, _width)) / 2;
+ view.panY = (background.h - MIN<int>(background.h, _height)) / 2;
+ beginFrame(background, view);
+}
+
+void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background,
+ const RA2PSXBackgroundView &view) {
if (!_context)
return;
TinyGL::setContext(_context);
@@ -550,12 +597,28 @@ void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background) {
surface.fillRect(Common::Rect(surface.w, surface.h), 0);
const int width = MIN<int>(background.w, surface.w);
const int height = MIN<int>(background.h, surface.h);
- const int sourceX = (background.w - width) / 2;
- const int sourceY = (background.h - height) / 2;
const int destX = (surface.w - width) / 2;
const int destY = (surface.h - height) / 2;
- surface.copyRectToSurface(background, destX, destY,
- Common::Rect(sourceX, sourceY, sourceX + width, sourceY + height));
+ // The source row ramps from one screen edge to the other, which rolls the view.
+ // Columns that land on the same row are copied as one run, as the original does.
+ const int span = MAX(1, width - 1);
+ const int bytesPerPixel = background.format.bytesPerPixel;
+ int column = 0;
+ while (column < width) {
+ const int tilt = view.tiltLeft + (view.tiltRight - view.tiltLeft) * column / span;
+ int end = column + 1;
+ while (end < width &&
+ view.tiltLeft + (view.tiltRight - view.tiltLeft) * end / span == tilt)
+ ++end;
+ const int sourceX = CLIP(view.panX + column, 0, background.w - 1);
+ const int run = MIN(end - column, background.w - sourceX);
+ for (int row = 0; row < height; ++row) {
+ const int sourceY = CLIP(view.panY + tilt + row, 0, background.h - 1);
+ memcpy(surface.getBasePtr(destX + column, destY + row),
+ background.getBasePtr(sourceX, sourceY), run * bytesPerPixel);
+ }
+ column = end;
+ }
tglClear(TGL_DEPTH_BUFFER_BIT);
tglMatrixMode(TGL_PROJECTION);
@@ -565,6 +628,47 @@ void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background) {
tglLoadIdentity();
}
+void RA2PSXTinyGLRenderer::renderSprite(const RA2PSXTexture &texture, float x, float y, float z,
+ float halfWidth, float halfHeight, int rotation) {
+ if (!_context || z <= 1.0f || texture.pixels.empty())
+ return;
+ TinyGL::setContext(_context);
+
+ const float focalLength = _width * 2.0f;
+ const float centerX = _width * 0.5f + x * focalLength / z;
+ const float centerY = _height * 0.5f + y * focalLength / z;
+ const float scaleX = halfWidth * focalLength / z;
+ const float scaleY = halfHeight * focalLength / z;
+ const float angle = rotation * kRA2PSXAngleScale;
+ const float cosine = cosf(angle);
+ const float sine = sinf(angle);
+ const float cornerX[4] = { -scaleX, scaleX, -scaleX, scaleX };
+ const float cornerY[4] = { -scaleY, -scaleY, scaleY, scaleY };
+ const float u[4] = { 0.0f, 1.0f, 0.0f, 1.0f };
+ const float v[4] = { 0.0f, 0.0f, 1.0f, 1.0f };
+
+ tglDisable(TGL_DEPTH_TEST);
+ tglEnable(TGL_TEXTURE_2D);
+ tglEnable(TGL_ALPHA_TEST);
+ tglEnable(TGL_BLEND);
+ tglBindTexture(TGL_TEXTURE_2D, getTextureId(texture));
+ tglColor4ub(0xff, 0xff, 0xff, 0xff);
+ tglBegin(TGL_QUAD_STRIP);
+ for (uint corner = 0; corner < 4; ++corner) {
+ tglTexCoord2f(u[corner], v[corner]);
+ tglVertex3f(centerX + cornerX[corner] * cosine - cornerY[corner] * sine,
+ centerY + cornerX[corner] * sine + cornerY[corner] * cosine, 0.0f);
+ }
+ tglEnd();
+ tglDisable(TGL_BLEND);
+ tglDisable(TGL_ALPHA_TEST);
+ tglDisable(TGL_TEXTURE_2D);
+ tglEnable(TGL_DEPTH_TEST);
+ _activeTexture = nullptr;
+ _textureEnabled = false;
+ _blendEnabled = false;
+}
+
void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float y, float size,
float pitch, float yaw, float roll, bool depthTest) {
if (!_context || model.vertices().empty())
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index b9375c2869d..081091b4c4e 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -49,9 +49,10 @@ public:
Level1Handler(Rebel2PSX &psx, const RA2PSXModel &enemy, const RA2PSXModel &ship,
const RA2PSXModel &crosshair, const RA2PSXModel &laser,
- const RA2PSXModel &tieLaser, const RA2PSXLevel1UI &ui, int &score) :
+ const RA2PSXModel &tieLaser, const Common::Array<RA2PSXModel> &debris,
+ const RA2PSXLevel1UI &ui, int &score) :
_psx(psx), _enemy(enemy), _ship(ship), _crosshair(crosshair), _laser(laser),
- _tieLaser(tieLaser), _ui(ui), _score(score), _error(kGameplayError) {
+ _tieLaser(tieLaser), _debris(debris), _ui(ui), _score(score), _error(kGameplayError) {
}
bool shouldQuit() const override {
@@ -59,8 +60,8 @@ public:
}
Result playAttempt(int &lives) override {
- switch (_psx.playLevel1(_enemy, _ship, _crosshair, _laser, _tieLaser, _ui,
- lives, _score)) {
+ switch (_psx.playLevel1(_enemy, _ship, _crosshair, _laser, _tieLaser, _debris,
+ _ui, lives, _score)) {
case Rebel2PSX::kLevel1Quit:
return kQuit;
case Rebel2PSX::kLevel1Complete:
@@ -108,11 +109,31 @@ private:
const RA2PSXModel &_crosshair;
const RA2PSXModel &_laser;
const RA2PSXModel &_tieLaser;
+ const Common::Array<RA2PSXModel> &_debris;
const RA2PSXLevel1UI &_ui;
int &_score;
Error _error;
};
+// The TIE breaks into its own body and wing pieces, five of each.
+bool loadRA2PSXDebrisModels(const RA2PSXArchive &archive,
+ const Common::Array<byte> &textureData, Common::Array<RA2PSXModel> &models) {
+ static const char *const kParts[] = { "body", "wing" };
+ for (uint part = 0; part < ARRAYSIZE(kParts); ++part) {
+ for (int index = 1; index <= 5; ++index) {
+ const Common::String path = Common::String::format("fOFS/TieFighter/%s_%d",
+ kParts[part], index);
+ Common::Array<byte> data;
+ RA2PSXModel model;
+ if (!archive.getMember(path, data) || !model.load(data) ||
+ !model.loadTextures(textureData))
+ return false;
+ models.push_back(model);
+ }
+ }
+ return true;
+}
+
Common::SeekableReadStream *Rebel2PSX::openResource(int number) {
const Common::Path path(Common::String::format("RESOURCE.%03d", number));
Common::File *file = new Common::File();
@@ -230,6 +251,7 @@ bool Rebel2PSX::loadGlobalAssets(RA2PSXMainMenuUI &menu) {
return false;
Common::Array<byte> textureData;
+ Common::Array<byte> explosionData;
Common::Array<byte> logoData;
Common::Array<byte> cloakData;
Common::Array<byte> crestData;
@@ -238,7 +260,9 @@ bool Rebel2PSX::loadGlobalAssets(RA2PSXMainMenuUI &menu) {
!_logoModel.loadTextures(textureData) ||
!archive.getMember("fOFS/Cloak", cloakData) || !_cloakModel.load(cloakData) ||
!_cloakModel.loadTextures(textureData) ||
- !archive.getMember("fOFS/LogoDbl", crestData) || !_crestModel.load(crestData))
+ !archive.getMember("fOFS/LogoDbl", crestData) || !_crestModel.load(crestData) ||
+ !archive.getMember("bigEx", explosionData) ||
+ !loadRA2PSXSpriteAnimation(explosionData, kRA2PSXExplosionHeight, _explosionFrames))
return false;
Common::Array<byte> soundData;
@@ -255,7 +279,7 @@ bool Rebel2PSX::loadMovieTextAssets(RA2PSXMovieText &movieText) {
bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXModel &tieLaser,
- RA2PSXLevel1UI &ui) {
+ Common::Array<RA2PSXModel> &debris, RA2PSXLevel1UI &ui) {
Common::SeekableReadStream *stream = openResource(1);
if (!stream)
return false;
@@ -279,6 +303,7 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
archive.getMember("fOFS/CrosshairW", crosshairData) && crosshair.load(crosshairData) &&
archive.getMember("fOFS/WingLaser", laserData) && laser.load(laserData) &&
archive.getMember("fOFS/TieLaser", tieLaserData) && tieLaser.load(tieLaserData) &&
+ loadRA2PSXDebrisModels(archive, enemyTextureData, debris) &&
ui.load(archive);
}
@@ -328,8 +353,9 @@ Common::Error Rebel2PSX::runGame() {
RA2PSXModel crosshair;
RA2PSXModel laser;
RA2PSXModel tieLaser;
+ Common::Array<RA2PSXModel> debris;
RA2PSXLevel1UI ui;
- if (!loadLevel1Assets(enemy, ship, crosshair, laser, tieLaser, ui))
+ if (!loadLevel1Assets(enemy, ship, crosshair, laser, tieLaser, debris, ui))
return Common::Error(Common::kReadingFailed,
_("Could not load the PlayStation Level 1 resources"));
@@ -343,7 +369,7 @@ Common::Error Rebel2PSX::runGame() {
int lives = 3;
int score = 0;
- Level1Handler handler(*this, enemy, ship, crosshair, laser, tieLaser, ui, score);
+ Level1Handler handler(*this, enemy, ship, crosshair, laser, tieLaser, debris, ui, score);
if (runRebel2Level1(handler, lives) != Rebel2Level1Handler::kError)
return Common::kNoError;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index fb7f4041620..361614b96fd 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -41,6 +41,11 @@ class RA2PSXMainMenuUI;
class RA2PSXMovieText;
class RA2PSXOptionsUI;
class RA2PSXChapterSelectUI;
+class RA2PSXArchive;
+class RA2PSXModel;
+
+bool loadRA2PSXDebrisModels(const RA2PSXArchive &archive,
+ const Common::Array<byte> &textureData, Common::Array<RA2PSXModel> &models);
enum RA2PSXMovieTextSequence {
kRA2PSXMovieTextNone,
@@ -202,6 +207,23 @@ struct RA2PSXTexture {
bool loadRA2PSXTextures(const Common::Array<byte> &data,
Common::Array<RA2PSXTexture> &textures);
+// The fireball frames are 68x56, the size the original's VRAM upload uses.
+enum { kRA2PSXExplosionHeight = 56 };
+
+// bigEx: a 4 byte format word, a 256 colour palette and equal sized 8 bit frames.
+bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeight,
+ Common::Array<RA2PSXTexture> &frames);
+
+// How far the level 1 background is panned and tilted inside its oversized frame.
+struct RA2PSXBackgroundView {
+ RA2PSXBackgroundView() : panX(0), panY(0), tiltLeft(0), tiltRight(0) {}
+
+ int panX;
+ int panY;
+ int tiltLeft;
+ int tiltRight;
+};
+
struct RA2PSXFace {
uint16 vertex[4];
int16 normalX[4];
@@ -246,6 +268,10 @@ public:
bool init(int width, int height);
void beginFrame(const Graphics::Surface &background);
+ void beginFrame(const Graphics::Surface &background, const RA2PSXBackgroundView &view);
+ // A camera facing quad, sized in world units like the original's explosion billboard.
+ void renderSprite(const RA2PSXTexture &texture, float x, float y, float z,
+ float halfWidth, float halfHeight, int rotation);
void renderModel(const RA2PSXModel &model, float x, float y, float size,
float pitch, float yaw, float roll, bool depthTest = true);
void renderPerspectiveModel(const RA2PSXModel &model, float x, float y, float z,
@@ -316,10 +342,11 @@ private:
bool loadMovieTextAssets(RA2PSXMovieText &movieText);
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXModel &tieLaser,
- RA2PSXLevel1UI &ui);
+ Common::Array<RA2PSXModel> &debris, RA2PSXLevel1UI &ui);
Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
const RA2PSXModel &crosshair, const RA2PSXModel &laser,
- const RA2PSXModel &tieLaser, const RA2PSXLevel1UI &ui, int lives, int &score);
+ const RA2PSXModel &tieLaser, const Common::Array<RA2PSXModel> &debris,
+ const RA2PSXLevel1UI &ui, int lives, int &score);
ScummEngine_v7 *_vm;
RA2PSXSoundBank _soundBank;
@@ -329,6 +356,8 @@ private:
RA2PSXModel _logoModel;
RA2PSXModel _cloakModel;
RA2PSXModel _crestModel;
+ // The 42 frame fireball the original streams from the bigEx resource.
+ Common::Array<RA2PSXTexture> _explosionFrames;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index 69eec1f35da..d88b4f07549 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -964,11 +964,12 @@ void RA2PSXLevel1UI::drawShield(Graphics::Surface &surface, int shield,
{ 0, { 226, 207, 0 } }, { 15, { 219, 24, 0 } }, { 31, { 139, 3, 0 } }
};
- shield = CLIP<int>(shield, 0, 100);
- const int width = shield * 68 / 100;
+ // The gauge runs on the original's 0x1000 scale: 68 pixels wide, 32 colour steps.
+ shield = CLIP<int>(shield, 0, kRA2PSXShieldFull);
+ const int width = shield * 0x44 >> 12;
if (!width)
return;
- const int colorIndex = CLIP<int>(32 - shield * 32 / 100, 0, 31);
+ const int colorIndex = CLIP<int>(0x20 - (shield >> 7), 0, 31);
const RA2PSXUIColor tl = shieldColor(topLeft, ARRAYSIZE(topLeft), colorIndex);
const RA2PSXUIColor tr = shieldColor(topRight, ARRAYSIZE(topRight), colorIndex);
const RA2PSXUIColor bl = shieldColor(bottomLeft, ARRAYSIZE(bottomLeft), colorIndex);
@@ -1027,7 +1028,7 @@ void RA2PSXLevel1UI::drawHUD(Graphics::Surface &surface, int score, int lives,
_textures.draw(surface, "FONT8X9", xOffset + 283, yOffset + 29, Common::Rect(80, 0, 88, 9));
int shieldLabelBrightness = 0x5a;
- if (shield <= 31) {
+ if (shield < kRA2PSXLowShield) {
const int phase = (MAX(frame, 0) / 2) % 14;
shieldLabelBrightness += phase < 7 ? -50 + phase * 10 : 20 - (phase - 7) * 10;
}
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 3733a144acc..f4dc6ca9aab 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -156,6 +156,12 @@ private:
const RA2PSXTextureSet &_textures;
};
+// The shield gauge runs 0 to 0x1000, and warns below 0x501.
+enum {
+ kRA2PSXShieldFull = 0x1000,
+ kRA2PSXLowShield = 0x501
+};
+
class RA2PSXLevel1UI {
public:
bool load(const RA2PSXArchive &archive);
Commit: a5985cf827c287cb6c5b1fb9f9cef0c90e073556
https://github.com/scummvm/scummvm/commit/a5985cf827c287cb6c5b1fb9f9cef0c90e073556
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: added smoke trails and other effects in L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 04d15e45dea..2722eb180f4 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -435,10 +435,80 @@ void spawnLevel1Debris(RA2PSXLevel1Debris *debris, const int *position,
}
}
-void updateLevel1Debris(RA2PSXLevel1Debris *debris) {
+// Each shard drags a smoke trail: a SMALLEX cell that spins and dims three steps a
+// frame from the grey the original picks on spawn.
+enum {
+ kLevel1PuffCount = 48,
+ kLevel1PuffSpin = 0x10,
+ kLevel1PuffFade = 3,
+ kLevel1PuffInterval = 3,
+ kLevel1PuffHalfSize = 64
+};
+
+struct RA2PSXLevel1Puff {
+ RA2PSXLevel1Puff() : brightness(0), rotation(0), spin(0) {
+ position[0] = position[1] = position[2] = 0;
+ }
+
+ int brightness;
+ int rotation;
+ int spin;
+ int position[3];
+};
+
+void spawnLevel1Puff(RA2PSXLevel1Puff *puffs, const int *position,
+ Common::RandomSource &random) {
+ int slot = -1;
+ int dimmest = 0x7fffffff;
+ for (int i = 0; i < kLevel1PuffCount; ++i) {
+ if (!puffs[i].brightness) {
+ slot = i;
+ break;
+ }
+ if (puffs[i].brightness < dimmest) {
+ dimmest = puffs[i].brightness;
+ slot = i;
+ }
+ }
+
+ RA2PSXLevel1Puff &puff = puffs[slot];
+ puff.brightness = (int)random.getRandomNumber(31) + 0x60;
+ puff.rotation = (int)random.getRandomNumber(0xfff);
+ puff.spin = 0;
+ for (int axis = 0; axis < 3; ++axis)
+ puff.position[axis] = position[axis];
+}
+
+void updateLevel1Puffs(RA2PSXLevel1Puff *puffs) {
+ for (int i = 0; i < kLevel1PuffCount; ++i) {
+ if (!puffs[i].brightness)
+ continue;
+ puffs[i].spin += kLevel1PuffSpin;
+ puffs[i].brightness = MAX(0, puffs[i].brightness - kLevel1PuffFade);
+ }
+}
+
+void renderLevel1Puffs(RA2PSXTinyGLRenderer &renderer, const RA2PSXTexture &texture,
+ const RA2PSXLevel1Puff *puffs) {
+ if (texture.pixels.empty())
+ return;
+ for (int i = 0; i < kLevel1PuffCount; ++i) {
+ const RA2PSXLevel1Puff &puff = puffs[i];
+ if (!puff.brightness)
+ continue;
+ renderer.renderSprite(texture, (float)puff.position[0], (float)puff.position[1],
+ (float)puff.position[2], kLevel1PuffHalfSize, kLevel1PuffHalfSize,
+ (int16)(puff.rotation + puff.spin), puff.brightness);
+ }
+}
+
+void updateLevel1Debris(RA2PSXLevel1Debris *debris, RA2PSXLevel1Puff *puffs,
+ Common::RandomSource &random) {
for (int i = 0; i < kLevel1DebrisCount; ++i) {
if (!debris[i].life)
continue;
+ if (debris[i].life % kLevel1PuffInterval == 0)
+ spawnLevel1Puff(puffs, debris[i].position, random);
for (int axis = 0; axis < 3; ++axis) {
debris[i].position[axis] += debris[i].velocity[axis];
debris[i].rotation[axis] = (int16)(debris[i].rotation[axis] + debris[i].spin[axis]);
@@ -1020,6 +1090,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
RA2PSXLevel1Enemy enemies[kLevel1EnemyCount];
RA2PSXLevel1Explosion explosions[kLevel1ExplosionSlots];
RA2PSXLevel1Debris debris[kLevel1DebrisCount];
+ RA2PSXLevel1Puff puffs[kLevel1PuffCount];
RA2PSXLevel1ViewTracker viewTracker;
RA2PSXLevel1Shot shots[kLevel1ShotCount];
RA2PSXLevel1TieShot tieShots[kLevel1ShotCount];
@@ -1049,6 +1120,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
int shield = kLevel1ShieldFull;
int shieldDisplayed = kLevel1ShieldFull;
int shakeFrames = 0;
+ int flashFrame = -1;
int shakeX = 0;
int shakeY = 0;
int spawnDelay = 0;
@@ -1290,6 +1362,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
MAX(shield, shieldDisplayed - kLevel1ShieldStep);
shieldDisplayed = MAX(1, shieldDisplayed);
}
+ if (flashFrame >= 0 && ++flashFrame >= kRA2PSXHitFlashFrames)
+ flashFrame = -1;
if (shakeFrames > 0) {
--shakeFrames;
shakeX = (int)_vm->_rnd.getRandomNumber(3);
@@ -1331,7 +1405,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
updateLevel1Shots(shots);
updateLevel1Explosions(explosions, _explosionFrames.size());
- updateLevel1Debris(debris);
+ updateLevel1Debris(debris, puffs, _vm->_rnd);
+ updateLevel1Puffs(puffs);
viewTracker.push(thirdPersonView ? ship.x * focal / MAX(1, ship.z) : aimX - centerX,
thirdPersonView ? ship.y * focal / MAX(1, ship.z) : aimY - centerY);
for (int i = 0; i < kLevel1ShotCount; ++i) {
@@ -1343,6 +1418,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
if (hit) {
shield = MAX(0, shield - kLevel1BoltDamage[view][difficulty]);
shakeFrames = kLevel1ShakeFrames;
+ flashFrame = 0;
soundPlayer.play(kLevel1SfxPlayerHit, 0x7f, 0x40);
}
}
@@ -1376,6 +1452,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
if (rammed) {
shield = MAX(0, shield - kLevel1RamDamage[difficulty]);
shakeFrames = kLevel1ShakeFrames;
+ flashFrame = 0;
destroyed = true;
}
}
@@ -1482,6 +1559,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
renderLevel1TieShots(renderer, tieLaser, tieShots);
renderLevel1Shots(renderer, laser, shots);
renderLevel1Debris(renderer, debrisModels, debris);
+ renderLevel1Puffs(renderer, _smokeTexture, puffs);
renderLevel1Explosions(renderer, _explosionFrames, explosions);
if (thirdPersonView) {
float forwardX;
@@ -1497,6 +1575,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
Graphics::Surface output;
renderer.finishFrame(output);
+ drawRA2PSXHitFlash(output, flashFrame);
if (!thirdPersonView)
ui.drawCockpit(output);
ui.drawHUD(output, score, lives, shieldDisplayed, logicFrame);
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index ecc6f02cba2..f05fd4508be 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -629,7 +629,7 @@ void RA2PSXTinyGLRenderer::beginFrame(const Graphics::Surface &background,
}
void RA2PSXTinyGLRenderer::renderSprite(const RA2PSXTexture &texture, float x, float y, float z,
- float halfWidth, float halfHeight, int rotation) {
+ float halfWidth, float halfHeight, int rotation, int brightness) {
if (!_context || z <= 1.0f || texture.pixels.empty())
return;
TinyGL::setContext(_context);
@@ -652,7 +652,9 @@ void RA2PSXTinyGLRenderer::renderSprite(const RA2PSXTexture &texture, float x, f
tglEnable(TGL_ALPHA_TEST);
tglEnable(TGL_BLEND);
tglBindTexture(TGL_TEXTURE_2D, getTextureId(texture));
- tglColor4ub(0xff, 0xff, 0xff, 0xff);
+ // Textured primitives treat 0x80 as neutral, so the tint doubles.
+ const byte tint = (byte)MIN(brightness * 2, 0xff);
+ tglColor4ub(tint, tint, tint, 0xff);
tglBegin(TGL_QUAD_STRIP);
for (uint corner = 0; corner < 4; ++corner) {
tglTexCoord2f(u[corner], v[corner]);
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 081091b4c4e..037111c074b 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -115,6 +115,28 @@ private:
Error _error;
};
+// The smoke trail draws the third 16x16 cell of the SMALLEX strip.
+static bool extractRA2PSXSmokeCell(const Common::Array<byte> &sheet, RA2PSXTexture &smoke) {
+ Common::Array<RA2PSXTexture> textures;
+ if (!loadRA2PSXTextures(sheet, textures))
+ return false;
+ for (uint i = 0; i < textures.size(); ++i) {
+ const RA2PSXTexture &source = textures[i];
+ if (!source.name.equalsIgnoreCase("SMALLEX") || source.width < 48 || source.height < 16)
+ continue;
+ smoke.name = "SMOKE";
+ smoke.width = 16;
+ smoke.height = 16;
+ smoke.pixels.resize(16 * 16);
+ for (int y = 0; y < 16; ++y) {
+ for (int x = 0; x < 16; ++x)
+ smoke.pixels[y * 16 + x] = source.pixels[y * source.width + 32 + x];
+ }
+ return true;
+ }
+ return false;
+}
+
// The TIE breaks into its own body and wing pieces, five of each.
bool loadRA2PSXDebrisModels(const RA2PSXArchive &archive,
const Common::Array<byte> &textureData, Common::Array<RA2PSXModel> &models) {
@@ -296,6 +318,7 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
Common::Array<byte> tieLaserData;
Common::Array<byte> enemyTextureData;
Common::Array<byte> shipTextureData;
+ Common::Array<byte> commonTextureData;
return archive.getMember("fOFS/TieFighter/main", enemyData) && enemy.load(enemyData) &&
archive.getMember("tex/Ties", enemyTextureData) && enemy.loadTextures(enemyTextureData) &&
archive.getMember("fOFS/Ship", shipData) && ship.load(shipData) &&
@@ -304,6 +327,8 @@ bool Rebel2PSX::loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
archive.getMember("fOFS/WingLaser", laserData) && laser.load(laserData) &&
archive.getMember("fOFS/TieLaser", tieLaserData) && tieLaser.load(tieLaserData) &&
loadRA2PSXDebrisModels(archive, enemyTextureData, debris) &&
+ archive.getMember("tex/Common", commonTextureData) &&
+ extractRA2PSXSmokeCell(commonTextureData, _smokeTexture) &&
ui.load(archive);
}
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 361614b96fd..3a3ef0a5d1e 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -271,7 +271,7 @@ public:
void beginFrame(const Graphics::Surface &background, const RA2PSXBackgroundView &view);
// A camera facing quad, sized in world units like the original's explosion billboard.
void renderSprite(const RA2PSXTexture &texture, float x, float y, float z,
- float halfWidth, float halfHeight, int rotation);
+ float halfWidth, float halfHeight, int rotation, int brightness = 0x80);
void renderModel(const RA2PSXModel &model, float x, float y, float size,
float pitch, float yaw, float roll, bool depthTest = true);
void renderPerspectiveModel(const RA2PSXModel &model, float x, float y, float z,
@@ -356,8 +356,10 @@ private:
RA2PSXModel _logoModel;
RA2PSXModel _cloakModel;
RA2PSXModel _crestModel;
- // The 42 frame fireball the original streams from the bigEx resource.
+ // The 42 frame fireball the original streams from the bigEx resource, and the
+ // single SMALLEX cell it trails smoke with.
Common::Array<RA2PSXTexture> _explosionFrames;
+ RA2PSXTexture _smokeTexture;
};
} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index d88b4f07549..f84577ed499 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -934,6 +934,41 @@ bool RA2PSXLevel1UI::load(const RA2PSXArchive &archive) {
return true;
}
+// The damage wash: a full screen Gouraud quad whose corners run through the five frame
+// CFlash table in the executable's own resource directory, green against orange.
+const byte kRA2PSXHitFlash[4][kRA2PSXHitFlashFrames][3] = {
+ { { 0, 39, 0 }, { 0, 91, 0 }, { 0, 69, 0 }, { 0, 48, 0 }, { 0, 27, 0 } },
+ { { 63, 0, 0 }, { 147, 23, 0 }, { 116, 19, 0 }, { 85, 15, 0 }, { 55, 11, 0 } },
+ { { 55, 11, 0 }, { 85, 15, 0 }, { 116, 19, 0 }, { 147, 23, 0 }, { 63, 0, 0 } },
+ { { 0, 27, 0 }, { 0, 48, 0 }, { 0, 69, 0 }, { 0, 91, 0 }, { 0, 39, 0 } }
+};
+
+void drawRA2PSXHitFlash(Graphics::Surface &surface, int frame) {
+ if (frame < 0 || frame >= kRA2PSXHitFlashFrames)
+ return;
+ const int lastX = MAX(1, surface.w - 1);
+ const int lastY = MAX(1, surface.h - 1);
+ for (int y = 0; y < surface.h; ++y) {
+ int left[3];
+ int right[3];
+ for (int channel = 0; channel < 3; ++channel) {
+ left[channel] = (kRA2PSXHitFlash[0][frame][channel] * (lastY - y) +
+ kRA2PSXHitFlash[2][frame][channel] * y) / lastY;
+ right[channel] = (kRA2PSXHitFlash[1][frame][channel] * (lastY - y) +
+ kRA2PSXHitFlash[3][frame][channel] * y) / lastY;
+ }
+ for (int x = 0; x < surface.w; ++x) {
+ byte r, g, b;
+ surface.format.colorToRGB(surface.getPixel(x, y), r, g, b);
+ const int addR = (left[0] * (lastX - x) + right[0] * x) / lastX;
+ const int addG = (left[1] * (lastX - x) + right[1] * x) / lastX;
+ const int addB = (left[2] * (lastX - x) + right[2] * x) / lastX;
+ surface.setPixel(x, y, surface.format.RGBToColor(
+ MIN(0xff, r + addR), MIN(0xff, g + addG), MIN(0xff, b + addB)));
+ }
+ }
+}
+
void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface) const {
const int xOffset = (surface.w - 320) / 2;
const int yOffset = (surface.h - 240) / 2 + 120;
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index f4dc6ca9aab..97b28479e5e 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -156,6 +156,11 @@ private:
const RA2PSXTextureSet &_textures;
};
+// A hit washes the screen for five frames.
+enum { kRA2PSXHitFlashFrames = 5 };
+
+void drawRA2PSXHitFlash(Graphics::Surface &surface, int frame);
+
// The shield gauge runs 0 to 0x1000, and warns below 0x501.
enum {
kRA2PSXShieldFull = 0x1000,
Commit: e182864bf7d9e0d2e9e15f0084bb9c1c7999e1c0
https://github.com/scummvm/scummvm/commit/e182864bf7d9e0d2e9e15f0084bb9c1c7999e1c0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: added missing animation and improved controls for L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 2722eb180f4..c7ad2768c27 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -102,6 +102,30 @@ const int kLevel1AimChance[2][3] = { { 5, 5, 5 }, { 15, 20, 20 } };
// A TIE that gets this close without being shot collides with the player.
const int kLevel1RamRadius[2] = { 200, 220 };
+// ra2InitializeCrosshair's box and start point, in the original's 240 line frame.
+enum {
+ kLevel1AimLeft = 30,
+ kLevel1AimWidth = 260,
+ kLevel1AimTop = 48,
+ kLevel1AimHeight = 130,
+ kLevel1AimStartX = 160,
+ kLevel1AimStartY = 113
+};
+
+// Swapping views dollies the ship out to 1000 and back to 40 at 20 a tick while the
+// cockpit shell zooms away; the original locks the controls until it lands.
+enum {
+ kLevel1ViewCockpitZ = 40,
+ kLevel1ViewOutsideZ = 1000,
+ kLevel1ViewStep = 20,
+ kLevel1ViewRecentre = 10,
+ kLevel1ViewZoomOutZ = 400,
+ kLevel1ViewZoomInZ = 300,
+ kLevel1CockpitScaleStep = 0xe0,
+ kLevel1CockpitScaleMin = 0x400,
+ kLevel1CockpitScaleFull = 0x1000
+};
+
// The original truncates its 1/4096 products toward zero.
int fixedShift12(int value) {
return (value < 0 ? value + 0xfff : value) >> 12;
@@ -830,7 +854,8 @@ void renderLevel1TieShots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &bol
}
void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
- int &directionX, int &directionY, bool left, bool right, bool up, bool down) {
+ int &directionX, int &directionY, bool left, bool right, bool up, bool down,
+ const Common::Rect &bounds) {
if (left && right)
left = right = false;
if (up && down)
@@ -855,7 +880,7 @@ void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
if ((up || down) && ABS(velocityX) < ABS(velocityY) / 2)
velocityX = ABS(velocityY) * directionX / 2;
}
- x = CLIP<int>(x + velocityX / 512, 30, 290);
+ x = CLIP<int>(x + velocityX / 512, bounds.left, bounds.right);
if (!up && !down) {
directionY = 0;
@@ -876,7 +901,7 @@ void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
if ((left || right) && ABS(velocityX) / 2 > ABS(velocityY))
velocityY = ABS(velocityX) * directionY / 2;
}
- y = CLIP<int>(y + velocityY / 512, 48, 178);
+ y = CLIP<int>(y + velocityY / 512, bounds.top, bounds.bottom);
}
void updateLevel1Ship(RA2PSXLevel1Ship &ship,
@@ -1111,8 +1136,14 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const int difficulty = CLIP(_settings.difficulty, 0, 2);
int extraLifeStage = 0;
int nextExtraLife = kLevel1ExtraLife[difficulty][0];
- int aimX = 160;
- int aimY = 113;
+ // The crosshair box lives in the original's 240 line frame; the port crops 20 rows.
+ const int viewOffsetX = (_vm->_screenWidth - 320) / 2;
+ const int viewOffsetY = (_vm->_screenHeight - 240) / 2;
+ const Common::Rect aimBounds(kLevel1AimLeft + viewOffsetX, kLevel1AimTop + viewOffsetY,
+ kLevel1AimLeft + kLevel1AimWidth + viewOffsetX,
+ kLevel1AimTop + kLevel1AimHeight + viewOffsetY);
+ int aimX = kLevel1AimStartX + viewOffsetX;
+ int aimY = kLevel1AimStartY + viewOffsetY;
int aimVelocityX = 0;
int aimVelocityY = 0;
int aimDirectionX = 0;
@@ -1145,7 +1176,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
bool actionFire = false;
bool fireRequested = false;
bool fireWasPressed = false;
- bool thirdPersonView = true;
+ bool thirdPersonView = false;
+ int viewTransition = 0;
+ int cockpitScale = kLevel1CockpitScaleFull;
+ bool showCockpit = true;
Level1Result result = kLevel1Complete;
const int joystickDeadzone = MIN<int>(Common::JOYAXIS_MAX,
MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000);
@@ -1168,20 +1202,20 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
switch (event.type) {
case Common::EVENT_MOUSEMOVE:
if (thirdPersonView) {
- ship.x = CLIP<int>((event.mouse.x - 160) * ship.z / 640, -282, 282);
- ship.y = CLIP<int>((event.mouse.y - 120) * ship.z / 640, -142, 157);
+ ship.x = CLIP<int>((event.mouse.x - centerX) * ship.z / focal, -282, 282);
+ ship.y = CLIP<int>((event.mouse.y - centerY) * ship.z / focal, -142, 157);
ship.velocityX = ship.velocityY = 0;
} else {
- aimX = CLIP<int>(event.mouse.x, 30, 290);
- aimY = CLIP<int>(event.mouse.y, 48, 178);
+ aimX = CLIP<int>(event.mouse.x, aimBounds.left, aimBounds.right);
+ aimY = CLIP<int>(event.mouse.y, aimBounds.top, aimBounds.bottom);
aimVelocityX = aimVelocityY = 0;
aimDirectionX = aimDirectionY = 0;
}
break;
case Common::EVENT_LBUTTONDOWN:
if (!thirdPersonView) {
- aimX = CLIP<int>(event.mouse.x, 30, 290);
- aimY = CLIP<int>(event.mouse.y, 48, 178);
+ aimX = CLIP<int>(event.mouse.x, aimBounds.left, aimBounds.right);
+ aimY = CLIP<int>(event.mouse.y, aimBounds.top, aimBounds.bottom);
}
mouseFire = true;
fireRequested = true;
@@ -1294,14 +1328,13 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
break;
}
}
- if (toggleViewRequested) {
- thirdPersonView = !thirdPersonView;
+ if (toggleViewRequested && !viewTransition) {
+ viewTransition = thirdPersonView ? -1 : 1;
ship.velocityX = ship.velocityY = 0;
- aimX = 160;
- aimY = 113;
aimVelocityX = aimVelocityY = 0;
aimDirectionX = aimDirectionY = 0;
- g_system->warpMouse(160, thirdPersonView ? 120 : aimY);
+ if (viewTransition > 0)
+ ship.z = kLevel1ViewCockpitZ;
redraw = true;
}
if (result == kLevel1Quit)
@@ -1324,16 +1357,53 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
while (logicFrame < targetLogicFrame && shield > 0) {
++logicFrame;
redraw = true;
- const bool left = moveLeft || actionLeft || joystickAxisX < -joystickDeadzone;
- const bool right = moveRight || actionRight || joystickAxisX > joystickDeadzone;
- const bool up = moveUp || actionUp || joystickAxisY < -joystickDeadzone;
- const bool down = moveDown || actionDown || joystickAxisY > joystickDeadzone;
- if (thirdPersonView) {
+ // Controls are dead while the camera moves, exactly as the original does.
+ const bool steering = viewTransition == 0;
+ const bool left = steering && (moveLeft || actionLeft ||
+ joystickAxisX < -joystickDeadzone);
+ const bool right = steering && (moveRight || actionRight ||
+ joystickAxisX > joystickDeadzone);
+ const bool up = steering && (moveUp || actionUp || joystickAxisY < -joystickDeadzone);
+ const bool down = steering && (moveDown || actionDown ||
+ joystickAxisY > joystickDeadzone);
+ if (viewTransition > 0) {
+ ship.z += kLevel1ViewStep;
+ // The original only keeps the shell on screen while it is still zooming.
+ showCockpit = ship.z <= kLevel1ViewZoomOutZ;
+ if (showCockpit)
+ cockpitScale = MAX<int>(kLevel1CockpitScaleMin,
+ cockpitScale - kLevel1CockpitScaleStep);
+ if (ship.z >= kLevel1ViewOutsideZ) {
+ ship.z = kLevel1ViewOutsideZ;
+ thirdPersonView = true;
+ viewTransition = 0;
+ }
+ } else if (viewTransition < 0) {
+ ship.z -= kLevel1ViewStep;
+ ship.x -= CLIP(ship.x, -kLevel1ViewRecentre, (int)kLevel1ViewRecentre);
+ ship.y -= CLIP(ship.y, -kLevel1ViewRecentre, (int)kLevel1ViewRecentre);
+ showCockpit = ship.z <= kLevel1ViewZoomInZ;
+ if (showCockpit) {
+ cockpitScale = MIN<int>(kLevel1CockpitScaleFull,
+ cockpitScale + kLevel1CockpitScaleStep);
+ if (cockpitScale == kLevel1CockpitScaleFull)
+ ship.z = kLevel1ViewCockpitZ;
+ }
+ if (ship.z <= kLevel1ViewCockpitZ) {
+ ship.z = kLevel1ViewOutsideZ;
+ cockpitScale = kLevel1CockpitScaleFull;
+ thirdPersonView = false;
+ viewTransition = 0;
+ showCockpit = true;
+ aimX = kLevel1AimStartX + viewOffsetX;
+ aimY = kLevel1AimStartY + viewOffsetY;
+ }
+ } else if (thirdPersonView) {
updateLevel1Ship(ship, left, right, up, down);
} else {
updateLevel1Aim(aimX, aimY, aimVelocityX, aimVelocityY,
aimDirectionX, aimDirectionY,
- left, right, up, down);
+ left, right, up, down, aimBounds);
}
const int cockpitTarget = thirdPersonView ? 0 : kLevel1MixMaximum;
@@ -1561,7 +1631,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
renderLevel1Debris(renderer, debrisModels, debris);
renderLevel1Puffs(renderer, _smokeTexture, puffs);
renderLevel1Explosions(renderer, _explosionFrames, explosions);
- if (thirdPersonView) {
+ if (thirdPersonView || viewTransition) {
float forwardX;
float forwardY;
float forwardZ;
@@ -1576,8 +1646,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
Graphics::Surface output;
renderer.finishFrame(output);
drawRA2PSXHitFlash(output, flashFrame);
- if (!thirdPersonView)
- ui.drawCockpit(output);
+ if (showCockpit && (!thirdPersonView || viewTransition))
+ ui.drawCockpit(output, cockpitScale);
ui.drawHUD(output, score, lives, shieldDisplayed, logicFrame);
g_system->copyRectToScreen(output.getPixels(), output.pitch, shakeX, shakeY,
output.w - shakeX, output.h - shakeY);
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index f84577ed499..69203cd0d2f 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -969,11 +969,38 @@ void drawRA2PSXHitFlash(Graphics::Surface &surface, int frame) {
}
}
-void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface) const {
+void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface, int scale) const {
const int xOffset = (surface.w - 320) / 2;
const int yOffset = (surface.h - 240) / 2 + 120;
- _textures.draw(surface, "COCKPITL", xOffset, yOffset, Common::Rect(0, 0, 224, 120));
- _textures.draw(surface, "COCKPITR", xOffset + 224, yOffset, Common::Rect(0, 0, 120, 120));
+ if (scale >= 0x1000) {
+ _textures.draw(surface, "COCKPITL", xOffset, yOffset, Common::Rect(0, 0, 224, 120));
+ _textures.draw(surface, "COCKPITR", xOffset + 224, yOffset, Common::Rect(0, 0, 120, 120));
+ return;
+ }
+
+ // Draw the shell once, then rescale it about the screen centre.
+ Graphics::Surface shell;
+ shell.create(320, 120, surface.format);
+ _textures.draw(shell, "COCKPITL", 0, 0, Common::Rect(0, 0, 224, 120));
+ _textures.draw(shell, "COCKPITR", 224, 0, Common::Rect(0, 0, 120, 120));
+
+ const int centerX = surface.w / 2;
+ const int centerY = surface.h / 2;
+ const uint32 transparent = shell.format.RGBToColor(0, 0, 0);
+ for (int y = 0; y < surface.h; ++y) {
+ const int sourceY = ((y - centerY) * 0x1000 / scale) + centerY - yOffset;
+ if (sourceY < 0 || sourceY >= shell.h)
+ continue;
+ for (int x = 0; x < surface.w; ++x) {
+ const int sourceX = ((x - centerX) * 0x1000 / scale) + centerX - xOffset;
+ if (sourceX < 0 || sourceX >= shell.w)
+ continue;
+ const uint32 pixel = shell.getPixel(sourceX, sourceY);
+ if (pixel != transparent)
+ surface.setPixel(x, y, pixel);
+ }
+ }
+ shell.free();
}
void RA2PSXLevel1UI::drawExplosion(Graphics::Surface &surface, int x, int y, int frame) const {
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 97b28479e5e..5aead6cd965 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -171,7 +171,8 @@ class RA2PSXLevel1UI {
public:
bool load(const RA2PSXArchive &archive);
- void drawCockpit(Graphics::Surface &surface) const;
+ // The shell zooms away while the camera swaps views; 0x1000 is its resting size.
+ void drawCockpit(Graphics::Surface &surface, int scale = 0x1000) const;
void drawExplosion(Graphics::Surface &surface, int x, int y, int frame) const;
void drawHUD(Graphics::Surface &surface, int score, int lives, int shield, int frame) const;
Commit: b9d3731531c98b74db18d8fd795c4e062925a5fb
https://github.com/scummvm/scummvm/commit/b9d3731531c98b74db18d8fd795c4e062925a5fb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: use the original's fire cadence and cockpit details for L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index c7ad2768c27..29832741102 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -89,9 +89,19 @@ enum {
kLevel1ShieldStep = 0x28,
kLevel1ShakeFrames = 6,
kLevel1FireFacing = 0xf3c,
- kLevel1HitRadius = 0xc000
+ kLevel1HitRadius = 0xc000,
+ // ra2FirePlayerShot alternates between two diagonal cannon pairs on every trigger.
+ kLevel1BoltCount = 2
};
+// Ticks the original insists on between single shots, and its auto fire repeat, indexed
+// by view.
+const int kLevel1FireGap[2] = { 4, 5 };
+const int kLevel1AutoFireRepeat[2] = { 12, 14 };
+// The cockpit shell drifts with the crosshair by these divisors.
+const int kLevel1CockpitDriftX = 0x628b;
+const int kLevel1CockpitDriftY = 0x3800;
+
const int kLevel1SplineDepth[kLevel1SplinePoints] = { 17000, 14000, 11000, -4000, -8000 };
// Bolt damage by view then difficulty, and the far heavier damage from a TIE that rams.
@@ -123,7 +133,8 @@ enum {
kLevel1ViewZoomInZ = 300,
kLevel1CockpitScaleStep = 0xe0,
kLevel1CockpitScaleMin = 0x400,
- kLevel1CockpitScaleFull = 0x1000
+ kLevel1CockpitScaleFull = 0x1000,
+ kLevel1ShipSwellStep = 200
};
// The original truncates its 1/4096 products toward zero.
@@ -640,8 +651,8 @@ struct RA2PSXLevel1Shot {
bool active;
int progress;
- float start[3][3];
- float roll[3];
+ float start[kLevel1BoltCount][3];
+ float roll[kLevel1BoltCount];
float targetX;
float targetY;
float targetZ;
@@ -661,18 +672,16 @@ struct RA2PSXLevel1Ship {
int velocityY;
};
-const float kLevel1LaserStart[3][3] = {
- { -350.0f, 200.0f, 400.0f },
- { 350.0f, 200.0f, 400.0f },
- { 0.0f, 500.0f, 400.0f }
+const float kLevel1LaserStart[2][kLevel1BoltCount][3] = {
+ { { -600.0f, 200.0f, 400.0f }, { 600.0f, -100.0f, 400.0f } },
+ { { -600.0f, -100.0f, 400.0f }, { 600.0f, 200.0f, 400.0f } }
};
-const float kLevel1LaserRoll[3] = { -45.0f, 45.0f, 0.0f };
+const float kLevel1LaserRoll[kLevel1BoltCount] = { -45.0f, 45.0f };
-const float kLevel1ShipLaserStart[3][3] = {
+const float kLevel1ShipLaserStart[kLevel1BoltCount][3] = {
{ -93.0f, 11.0f, -139.0f },
- { 93.0f, 11.0f, -139.0f },
- { 4.0f, 210.0f, -111.0f }
+ { 93.0f, 11.0f, -139.0f }
};
// The two cannon mounts, in the TIE's own space.
@@ -904,6 +913,58 @@ void updateLevel1Aim(int &x, int &y, int &velocityX, int &velocityY,
y = CLIP<int>(y + velocityY / 512, bounds.top, bounds.bottom);
}
+// The camera swap. ra2UpdateExternalViewInput steps this twice per frame while it runs,
+// and keeps the pad dead throughout.
+struct RA2PSXLevel1ViewSwap {
+ RA2PSXLevel1ViewSwap() : direction(0), cockpitScale(kLevel1CockpitScaleFull),
+ shipScale(kLevel1CockpitScaleFull), showCockpit(true) {}
+
+ int direction;
+ int cockpitScale;
+ int shipScale;
+ bool showCockpit;
+};
+
+// Returns true once the swap lands, with outside telling the caller which view it landed in.
+bool stepLevel1ViewSwap(RA2PSXLevel1ViewSwap &swap, RA2PSXLevel1Ship &ship, bool &outside) {
+ if (swap.direction > 0) {
+ ship.z += kLevel1ViewStep;
+ swap.showCockpit = ship.z <= kLevel1ViewZoomOutZ;
+ if (swap.showCockpit)
+ swap.cockpitScale = MAX<int>(kLevel1CockpitScaleMin,
+ swap.cockpitScale - kLevel1CockpitScaleStep);
+ if (ship.z >= kLevel1ViewOutsideZ) {
+ ship.z = kLevel1ViewOutsideZ;
+ swap.direction = 0;
+ outside = true;
+ return true;
+ }
+ return false;
+ }
+
+ swap.shipScale += kLevel1ShipSwellStep;
+ ship.z -= kLevel1ViewStep;
+ ship.x -= CLIP(ship.x, -kLevel1ViewRecentre, (int)kLevel1ViewRecentre);
+ ship.y -= CLIP(ship.y, -kLevel1ViewRecentre, (int)kLevel1ViewRecentre);
+ swap.showCockpit = ship.z <= kLevel1ViewZoomInZ;
+ if (swap.showCockpit) {
+ swap.cockpitScale = MIN<int>(kLevel1CockpitScaleFull,
+ swap.cockpitScale + kLevel1CockpitScaleStep);
+ if (swap.cockpitScale == kLevel1CockpitScaleFull)
+ ship.z = kLevel1ViewCockpitZ;
+ }
+ if (ship.z <= kLevel1ViewCockpitZ) {
+ ship.z = kLevel1ViewOutsideZ;
+ swap.cockpitScale = kLevel1CockpitScaleFull;
+ swap.shipScale = kLevel1CockpitScaleFull;
+ swap.showCockpit = true;
+ swap.direction = 0;
+ outside = false;
+ return true;
+ }
+ return false;
+}
+
void updateLevel1Ship(RA2PSXLevel1Ship &ship,
bool left, bool right, bool up, bool down) {
if (left == right) {
@@ -968,7 +1029,7 @@ void traceLevel1Shot(RA2PSXLevel1Shot &shot) {
}
bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
- int centerX, int centerY, int focal, const RA2PSXLevel1Ship *ship) {
+ int centerX, int centerY, int focal, const RA2PSXLevel1Ship *ship, int pair) {
int slot = -1;
for (int i = 0; i < kLevel1ShotCount; ++i) {
if (!shots[i].active) {
@@ -985,9 +1046,9 @@ bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
// The original launches from the muzzle in the cockpit and only skips ahead outside.
shot.progress = ship ? 400 : 0;
if (!ship) {
- for (int i = 0; i < 3; ++i) {
+ for (int i = 0; i < kLevel1BoltCount; ++i) {
for (int axis = 0; axis < 3; ++axis)
- shot.start[i][axis] = kLevel1LaserStart[i][axis];
+ shot.start[i][axis] = kLevel1LaserStart[pair][i][axis];
shot.roll[i] = kLevel1LaserRoll[i];
}
shot.targetX = (float)(aimX - centerX) * 18000.0f / focal;
@@ -1003,7 +1064,7 @@ bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
float forwardZ;
float shipRoll;
getLevel1ShipOrientation(*ship, forwardX, forwardY, forwardZ, shipRoll);
- for (int i = 0; i < 3; ++i) {
+ for (int i = 0; i < kLevel1BoltCount; ++i) {
transformLevel1ShipPoint(*ship, kLevel1ShipLaserStart[i][0],
kLevel1ShipLaserStart[i][1], kLevel1ShipLaserStart[i][2],
shot.start[i][0], shot.start[i][1], shot.start[i][2]);
@@ -1058,7 +1119,7 @@ void renderLevel1Shots(RA2PSXTinyGLRenderer &renderer, const RA2PSXModel &laser,
if (!shot.active || shot.progress >= 4000)
continue;
const float progress = shot.progress / 4096.0f;
- for (int laserIndex = 0; laserIndex < 3; ++laserIndex) {
+ for (int laserIndex = 0; laserIndex < kLevel1BoltCount; ++laserIndex) {
const float *start = shot.start[laserIndex];
const float directionX = shot.targetX - start[0];
const float directionY = shot.targetY - start[1];
@@ -1078,7 +1139,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const RA2PSXModel &shipModel, const RA2PSXModel &crosshair,
const RA2PSXModel &laser, const RA2PSXModel &tieLaser,
const Common::Array<RA2PSXModel> &debrisModels, const RA2PSXLevel1UI &ui,
- int lives, int &score) {
+ int &lives, int &score) {
#ifndef USE_TINYGL
(void)enemyModel;
(void)shipModel;
@@ -1160,7 +1221,10 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
int nextSpawnAdjustment = 0;
int logicFrame = -1;
int videoFrame = -1;
- int16 rapidFireCounter = 0;
+ int fireTick = 0;
+ int lastFireTick = -kLevel1FireGap[1];
+ int autoFireCounter = 0;
+ int cannonPair = 0;
bool moveLeft = false;
bool moveRight = false;
bool moveUp = false;
@@ -1177,9 +1241,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
bool fireRequested = false;
bool fireWasPressed = false;
bool thirdPersonView = false;
- int viewTransition = 0;
- int cockpitScale = kLevel1CockpitScaleFull;
- bool showCockpit = true;
+ RA2PSXLevel1ViewSwap viewSwap;
Level1Result result = kLevel1Complete;
const int joystickDeadzone = MIN<int>(Common::JOYAXIS_MAX,
MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000);
@@ -1328,12 +1390,12 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
break;
}
}
- if (toggleViewRequested && !viewTransition) {
- viewTransition = thirdPersonView ? -1 : 1;
+ if (toggleViewRequested && !viewSwap.direction) {
+ viewSwap.direction = thirdPersonView ? -1 : 1;
ship.velocityX = ship.velocityY = 0;
aimVelocityX = aimVelocityY = 0;
aimDirectionX = aimDirectionY = 0;
- if (viewTransition > 0)
+ if (viewSwap.direction > 0)
ship.z = kLevel1ViewCockpitZ;
redraw = true;
}
@@ -1358,7 +1420,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
++logicFrame;
redraw = true;
// Controls are dead while the camera moves, exactly as the original does.
- const bool steering = viewTransition == 0;
+ const bool steering = viewSwap.direction == 0;
const bool left = steering && (moveLeft || actionLeft ||
joystickAxisX < -joystickDeadzone);
const bool right = steering && (moveRight || actionRight ||
@@ -1366,37 +1428,13 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
const bool up = steering && (moveUp || actionUp || joystickAxisY < -joystickDeadzone);
const bool down = steering && (moveDown || actionDown ||
joystickAxisY > joystickDeadzone);
- if (viewTransition > 0) {
- ship.z += kLevel1ViewStep;
- // The original only keeps the shell on screen while it is still zooming.
- showCockpit = ship.z <= kLevel1ViewZoomOutZ;
- if (showCockpit)
- cockpitScale = MAX<int>(kLevel1CockpitScaleMin,
- cockpitScale - kLevel1CockpitScaleStep);
- if (ship.z >= kLevel1ViewOutsideZ) {
- ship.z = kLevel1ViewOutsideZ;
- thirdPersonView = true;
- viewTransition = 0;
- }
- } else if (viewTransition < 0) {
- ship.z -= kLevel1ViewStep;
- ship.x -= CLIP(ship.x, -kLevel1ViewRecentre, (int)kLevel1ViewRecentre);
- ship.y -= CLIP(ship.y, -kLevel1ViewRecentre, (int)kLevel1ViewRecentre);
- showCockpit = ship.z <= kLevel1ViewZoomInZ;
- if (showCockpit) {
- cockpitScale = MIN<int>(kLevel1CockpitScaleFull,
- cockpitScale + kLevel1CockpitScaleStep);
- if (cockpitScale == kLevel1CockpitScaleFull)
- ship.z = kLevel1ViewCockpitZ;
- }
- if (ship.z <= kLevel1ViewCockpitZ) {
- ship.z = kLevel1ViewOutsideZ;
- cockpitScale = kLevel1CockpitScaleFull;
- thirdPersonView = false;
- viewTransition = 0;
- showCockpit = true;
- aimX = kLevel1AimStartX + viewOffsetX;
- aimY = kLevel1AimStartY + viewOffsetY;
+ if (viewSwap.direction) {
+ for (int step = 0; step < 2 && viewSwap.direction; ++step) {
+ if (stepLevel1ViewSwap(viewSwap, ship, thirdPersonView) &&
+ !thirdPersonView) {
+ aimX = kLevel1AimStartX + viewOffsetX;
+ aimY = kLevel1AimStartY + viewOffsetY;
+ }
}
} else if (thirdPersonView) {
updateLevel1Ship(ship, left, right, up, down);
@@ -1551,7 +1589,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
destroyed = true;
score = MIN(9999999, score + kLevel1KillScore[difficulty]);
if (score >= nextExtraLife) {
- // The original also awards a life; the shared runner owns it.
+ ++lives;
soundPlayer.play(kLevel1SfxExtraLife, 0x7f, 0x40);
extraLifeStage = MIN(extraLifeStage + 1, 2);
nextExtraLife += kLevel1ExtraLife[difficulty][extraLifeStage];
@@ -1584,15 +1622,28 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
}
- const bool heldFire = mouseFire || keyFire || actionFire;
- const bool triggerShot = updateRebel2Fire(heldFire, fireWasPressed,
- rapidFire, false, rapidFireCounter);
+ // A press only counts once the minimum gap has passed; with auto fire the
+ // press just re-arms the repeat, which then fires on the same tick.
+ ++fireTick;
+ const bool heldFire = steering && (mouseFire || keyFire || actionFire);
+ const bool fireEdge = heldFire && (fireRequested || !fireWasPressed);
fireWasPressed = heldFire;
- const bool shootRequested = fireRequested || triggerShot;
fireRequested = false;
+ bool shootRequested = false;
+ if (fireEdge && fireTick - lastFireTick >= kLevel1FireGap[view]) {
+ lastFireTick = fireTick;
+ autoFireCounter = 0;
+ shootRequested = !rapidFire;
+ }
+ if (rapidFire && heldFire && --autoFireCounter < 0) {
+ autoFireCounter = kLevel1AutoFireRepeat[view];
+ shootRequested = true;
+ }
if (shootRequested && spawnLevel1Shot(shots, aimX, aimY, centerX, centerY, focal,
- thirdPersonView ? &ship : nullptr))
+ thirdPersonView ? &ship : nullptr, cannonPair)) {
+ cannonPair ^= 1;
soundPlayer.play(kLevel1SfxPlayerFire, 0x3f, 0x40);
+ }
}
if (shield <= 0) {
@@ -1631,14 +1682,14 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
renderLevel1Debris(renderer, debrisModels, debris);
renderLevel1Puffs(renderer, _smokeTexture, puffs);
renderLevel1Explosions(renderer, _explosionFrames, explosions);
- if (thirdPersonView || viewTransition) {
+ if (thirdPersonView || viewSwap.direction) {
float forwardX;
float forwardY;
float forwardZ;
float shipRoll;
getLevel1ShipOrientation(ship, forwardX, forwardY, forwardZ, shipRoll);
renderer.renderPerspectiveModel(shipModel, ship.x, ship.y, ship.z,
- forwardX, forwardY, forwardZ, shipRoll, false);
+ forwardX, forwardY, forwardZ, shipRoll, false, viewSwap.shipScale);
} else {
renderer.renderModel(crosshair, aimX, aimY, 31.0f,
0.0f, 0.0f, 0.0f, false);
@@ -1646,8 +1697,12 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
Graphics::Surface output;
renderer.finishFrame(output);
drawRA2PSXHitFlash(output, flashFrame);
- if (showCockpit && (!thirdPersonView || viewTransition))
- ui.drawCockpit(output, cockpitScale);
+ if (viewSwap.showCockpit && (!thirdPersonView || viewSwap.direction))
+ ui.drawCockpit(output, viewSwap.cockpitScale,
+ scaleViewOffset(aimX - (kLevel1AimStartX + viewOffsetX),
+ kLevel1CockpitDriftX),
+ scaleViewOffset(aimY - (kLevel1AimStartY + viewOffsetY),
+ kLevel1CockpitDriftY));
ui.drawHUD(output, score, lives, shieldDisplayed, logicFrame);
g_system->copyRectToScreen(output.getPixels(), output.pitch, shakeX, shakeY,
output.w - shakeX, output.h - shakeY);
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index f05fd4508be..a94ae9b74a0 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -745,7 +745,7 @@ void RA2PSXTinyGLRenderer::renderModel(const RA2PSXModel &model, float x, float
void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
float x, float y, float z, float directionX, float directionY, float directionZ,
- float roll, bool depthTest) {
+ float roll, bool depthTest, int scale) {
if (!_context || model.vertices().empty())
return;
@@ -791,6 +791,13 @@ void RA2PSXTinyGLRenderer::renderPerspectiveModel(const RA2PSXModel &model,
transform.rotation[2][0] = modelXz;
transform.rotation[2][1] = modelYz;
transform.rotation[2][2] = forwardZ;
+ if (scale != 0x1000) {
+ const float factor = scale / 4096.0f;
+ for (uint row = 0; row < 3; ++row) {
+ for (uint column = 0; column < 3; ++column)
+ transform.rotation[row][column] *= factor;
+ }
+ }
transform.translation[0] = x;
transform.translation[1] = y;
transform.translation[2] = z;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 3a3ef0a5d1e..4461a1ec4a7 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -276,7 +276,7 @@ public:
float pitch, float yaw, float roll, bool depthTest = true);
void renderPerspectiveModel(const RA2PSXModel &model, float x, float y, float z,
float directionX, float directionY, float directionZ, float roll,
- bool depthTest = true);
+ bool depthTest = true, int scale = 0x1000);
void renderTransformedModel(const RA2PSXModel &model, const RA2PSXMatrix &transform,
bool depthTest = true);
void finishFrame(Graphics::Surface &surface);
@@ -346,7 +346,7 @@ private:
Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
const RA2PSXModel &crosshair, const RA2PSXModel &laser,
const RA2PSXModel &tieLaser, const Common::Array<RA2PSXModel> &debris,
- const RA2PSXLevel1UI &ui, int lives, int &score);
+ const RA2PSXLevel1UI &ui, int &lives, int &score);
ScummEngine_v7 *_vm;
RA2PSXSoundBank _soundBank;
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index 69203cd0d2f..a3999ab76f9 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -969,9 +969,10 @@ void drawRA2PSXHitFlash(Graphics::Surface &surface, int frame) {
}
}
-void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface, int scale) const {
- const int xOffset = (surface.w - 320) / 2;
- const int yOffset = (surface.h - 240) / 2 + 120;
+void RA2PSXLevel1UI::drawCockpit(Graphics::Surface &surface, int scale,
+ int driftX, int driftY) const {
+ const int xOffset = (surface.w - 320) / 2 + driftX;
+ const int yOffset = (surface.h - 240) / 2 + 120 + driftY;
if (scale >= 0x1000) {
_textures.draw(surface, "COCKPITL", xOffset, yOffset, Common::Rect(0, 0, 224, 120));
_textures.draw(surface, "COCKPITR", xOffset + 224, yOffset, Common::Rect(0, 0, 120, 120));
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 5aead6cd965..44d45d9796a 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -171,8 +171,10 @@ class RA2PSXLevel1UI {
public:
bool load(const RA2PSXArchive &archive);
- // The shell zooms away while the camera swaps views; 0x1000 is its resting size.
- void drawCockpit(Graphics::Surface &surface, int scale = 0x1000) const;
+ // The shell zooms away while the camera swaps views (0x1000 is its resting size)
+ // and drifts a few pixels with the crosshair.
+ void drawCockpit(Graphics::Surface &surface, int scale = 0x1000,
+ int driftX = 0, int driftY = 0) const;
void drawExplosion(Graphics::Surface &surface, int x, int y, int frame) const;
void drawHUD(Graphics::Surface &surface, int score, int lives, int shield, int frame) const;
Commit: d8041fdd6b8b6186660562bf13d72a7e8a9dafbe
https://github.com/scummvm/scummvm/commit/d8041fdd6b8b6186660562bf13d72a7e8a9dafbe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: pan the 3D scene with the view in L1 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.h
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 29832741102..0997a486e58 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -625,9 +625,12 @@ void getLevel1BackgroundView(const RA2PSXLevel1ViewTracker &tracker,
const int tilt = scaleViewOffset(averageX, 0x4f00);
view.tiltLeft = kLevel1ViewTiltBase - tilt;
view.tiltRight = kLevel1ViewTiltBase + tilt;
- view.panX = kLevel1ViewCenterX + scaleViewOffset(averageX, 5220);
+ // ra2SetProjectionOffset gets the same figure negated, so the 3D scene rides the pan.
+ view.sceneX = -scaleViewOffset(averageX, 5220);
+ view.sceneY = -scaleViewOffset(averageY, 0x1ce3);
+ view.panX = kLevel1ViewCenterX - view.sceneX;
// The band sits eight rows below the top of the port's cropped frame.
- view.panY = kLevel1ViewCenterY + scaleViewOffset(averageY, 0x1ce3) - 8;
+ view.panY = kLevel1ViewCenterY - view.sceneY - 8;
const int slack = MAX(0, (int)background.h - height) - MAX(view.tiltLeft, view.tiltRight);
view.panX = CLIP(view.panX, 0, MAX(0, (int)background.w - width));
@@ -1029,7 +1032,8 @@ void traceLevel1Shot(RA2PSXLevel1Shot &shot) {
}
bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
- int centerX, int centerY, int focal, const RA2PSXLevel1Ship *ship, int pair) {
+ int centerX, int centerY, int focal, const RA2PSXLevel1Ship *ship, int pair,
+ int sceneX, int sceneY) {
int slot = -1;
for (int i = 0; i < kLevel1ShotCount; ++i) {
if (!shots[i].active) {
@@ -1051,8 +1055,8 @@ bool spawnLevel1Shot(RA2PSXLevel1Shot *shots, int aimX, int aimY,
shot.start[i][axis] = kLevel1LaserStart[pair][i][axis];
shot.roll[i] = kLevel1LaserRoll[i];
}
- shot.targetX = (float)(aimX - centerX) * 18000.0f / focal;
- shot.targetY = (float)(aimY - centerY) * 18000.0f / focal;
+ shot.targetX = (float)(aimX - centerX - sceneX) * 18000.0f / focal;
+ shot.targetY = (float)(aimY - centerY - sceneY) * 18000.0f / focal;
shot.targetZ = 18000.0f;
// Midway between the two cannon the original alternates between.
shot.traceStart[0] = 0.0f;
@@ -1242,6 +1246,7 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
bool fireWasPressed = false;
bool thirdPersonView = false;
RA2PSXLevel1ViewSwap viewSwap;
+ RA2PSXBackgroundView backgroundView;
Level1Result result = kLevel1Complete;
const int joystickDeadzone = MIN<int>(Common::JOYAXIS_MAX,
MAX(0, ConfMan.getInt("joystick_deadzone")) * 1000);
@@ -1517,6 +1522,9 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
updateLevel1Puffs(puffs);
viewTracker.push(thirdPersonView ? ship.x * focal / MAX(1, ship.z) : aimX - centerX,
thirdPersonView ? ship.y * focal / MAX(1, ship.z) : aimY - centerY);
+ if (background)
+ getLevel1BackgroundView(viewTracker, *background, _vm->_screenWidth,
+ _vm->_screenHeight, backgroundView);
for (int i = 0; i < kLevel1ShotCount; ++i) {
if (!tieShots[i].active)
continue;
@@ -1640,7 +1648,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
shootRequested = true;
}
if (shootRequested && spawnLevel1Shot(shots, aimX, aimY, centerX, centerY, focal,
- thirdPersonView ? &ship : nullptr, cannonPair)) {
+ thirdPersonView ? &ship : nullptr, cannonPair,
+ backgroundView.sceneX, backgroundView.sceneY)) {
cannonPair ^= 1;
soundPlayer.play(kLevel1SfxPlayerFire, 0x3f, 0x40);
}
@@ -1652,10 +1661,8 @@ Rebel2PSX::Level1Result Rebel2PSX::playLevel1(const RA2PSXModel &enemyModel,
}
if (background && redraw) {
- RA2PSXBackgroundView view;
- getLevel1BackgroundView(viewTracker, *background, _vm->_screenWidth,
- _vm->_screenHeight, view);
- renderer.beginFrame(*background, view);
+ renderer.setViewOffset(backgroundView.sceneX, backgroundView.sceneY);
+ renderer.beginFrame(*background, backgroundView);
// Everything shares one painter's pass, farthest first, as the original's
// ordering table does.
int order[kLevel1EnemyCount] = { 0, 1, 2 };
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index a94ae9b74a0..e29d8b55e80 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -434,7 +434,8 @@ bool RA2PSXModel::parseObject(const Common::Array<byte> &data, uint32 objectOffs
#ifdef USE_TINYGL
RA2PSXTinyGLRenderer::RA2PSXTinyGLRenderer() : _context(nullptr), _activeTexture(nullptr),
- _textureEnabled(false), _blendEnabled(false), _width(0), _height(0) {
+ _textureEnabled(false), _blendEnabled(false), _width(0), _height(0),
+ _viewOffsetX(0), _viewOffsetY(0) {
}
RA2PSXTinyGLRenderer::~RA2PSXTinyGLRenderer() {
@@ -635,8 +636,8 @@ void RA2PSXTinyGLRenderer::renderSprite(const RA2PSXTexture &texture, float x, f
TinyGL::setContext(_context);
const float focalLength = _width * 2.0f;
- const float centerX = _width * 0.5f + x * focalLength / z;
- const float centerY = _height * 0.5f + y * focalLength / z;
+ const float centerX = _width * 0.5f + _viewOffsetX + x * focalLength / z;
+ const float centerY = _height * 0.5f + _viewOffsetY + y * focalLength / z;
const float scaleX = halfWidth * focalLength / z;
const float scaleY = halfHeight * focalLength / z;
const float angle = rotation * kRA2PSXAngleScale;
@@ -817,8 +818,8 @@ void RA2PSXTinyGLRenderer::renderTransformedModel(const RA2PSXModel &model,
};
Common::Array<ProjectedVertex> projected;
projected.resize(model.vertices().size());
- const float centerX = _width * 0.5f;
- const float centerY = _height * 0.5f;
+ const float centerX = _width * 0.5f + _viewOffsetX;
+ const float centerY = _height * 0.5f + _viewOffsetY;
const float focalLength = _width * 2.0f;
for (uint i = 0; i < model.vertices().size(); ++i) {
const RA2PSXVertex &vertex = model.vertices()[i];
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 4461a1ec4a7..405fc353427 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -214,14 +214,18 @@ enum { kRA2PSXExplosionHeight = 56 };
bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeight,
Common::Array<RA2PSXTexture> &frames);
-// How far the level 1 background is panned and tilted inside its oversized frame.
+// How far the level 1 background is panned and tilted inside its oversized frame, and the
+// matching shift the 3D scene rides along with.
struct RA2PSXBackgroundView {
- RA2PSXBackgroundView() : panX(0), panY(0), tiltLeft(0), tiltRight(0) {}
+ RA2PSXBackgroundView() : panX(0), panY(0), tiltLeft(0), tiltRight(0),
+ sceneX(0), sceneY(0) {}
int panX;
int panY;
int tiltLeft;
int tiltRight;
+ int sceneX;
+ int sceneY;
};
struct RA2PSXFace {
@@ -269,6 +273,8 @@ public:
bool init(int width, int height);
void beginFrame(const Graphics::Surface &background);
void beginFrame(const Graphics::Surface &background, const RA2PSXBackgroundView &view);
+ // Shifts every world space draw, the way the original moves the GTE screen offset.
+ void setViewOffset(int x, int y) { _viewOffsetX = x; _viewOffsetY = y; }
// A camera facing quad, sized in world units like the original's explosion billboard.
void renderSprite(const RA2PSXTexture &texture, float x, float y, float z,
float halfWidth, float halfHeight, int rotation, int brightness = 0x80);
@@ -299,6 +305,8 @@ private:
bool _blendEnabled;
int _width;
int _height;
+ int _viewOffsetX;
+ int _viewOffsetY;
};
#endif
Commit: 1872ad720dbcee441b8551e0fc9c50eb39fd343c
https://github.com/scummvm/scummvm/commit/1872ad720dbcee441b8551e0fc9c50eb39fd343c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: sustain the looping sounds in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/audio.cpp
diff --git a/engines/scumm/insane/rebel2/psx/audio.cpp b/engines/scumm/insane/rebel2/psx/audio.cpp
index 39cba3ff0df..a207ca34a3b 100644
--- a/engines/scumm/insane/rebel2/psx/audio.cpp
+++ b/engines/scumm/insane/rebel2/psx/audio.cpp
@@ -107,6 +107,37 @@ struct RA2PSXPitch {
typedef Common::SharedPtr<RA2PSXPitch> RA2PSXPitchRef;
+// A sample whose ADPCM blocks ask the voice to repeat plays until something stops it.
+class RA2PSXLoopStream : public Audio::RewindableAudioStream {
+public:
+ explicit RA2PSXLoopStream(Audio::RewindableAudioStream *stream) : _stream(stream) {}
+ ~RA2PSXLoopStream() override { delete _stream; }
+
+ int readBuffer(int16 *buffer, const int numSamples) override {
+ int produced = 0;
+ while (produced < numSamples) {
+ const int read = _stream->readBuffer(buffer + produced, numSamples - produced);
+ if (read > 0) {
+ produced += read;
+ continue;
+ }
+ // An empty pass after rewinding means there is nothing to loop over.
+ if (!_stream->rewind() || _stream->readBuffer(buffer + produced, 1) <= 0)
+ break;
+ ++produced;
+ }
+ return produced;
+ }
+
+ bool isStereo() const override { return _stream->isStereo(); }
+ int getRate() const override { return _stream->getRate(); }
+ bool endOfData() const override { return false; }
+ bool rewind() override { return _stream->rewind(); }
+
+private:
+ Audio::RewindableAudioStream *_stream;
+};
+
class RA2PSXPitchStream : public Audio::RewindableAudioStream {
public:
RA2PSXPitchStream(Audio::RewindableAudioStream *stream, const RA2PSXPitchRef &pitch)
@@ -365,9 +396,23 @@ Audio::RewindableAudioStream *RA2PSXSoundBank::makeStream(uint16 id, uint32 macr
return nullptr;
memcpy(copy, _data.data() + sample->offset, size);
+ // Every ADPCM block carries flags: bit 2 opens a loop, bit 1 asks the voice to repeat
+ // at the end. Nearly every sample points that at its own final block, which is just a
+ // sustain tail; only a genuine loop - the engine rumbles - opens earlier than that.
+ uint32 loopStart = size;
+ for (uint32 block = 0; block + 16 <= size; block += 16) {
+ if (copy[block + 1] & 0x04) {
+ loopStart = block;
+ break;
+ }
+ }
+ const bool loops = size >= 32 && (copy[size - 15] & 0x02) && loopStart + 16 < size;
+
Common::SeekableReadStream *source =
new Common::MemoryReadStream(copy, size, DisposeAfterUse::YES);
Audio::RewindableAudioStream *stream = Audio::makeXAStream(source, rate);
+ if (loops)
+ stream = new RA2PSXLoopStream(stream);
const ADSR *adsr = findADSR(adsrId);
if (adsr)
stream = new RA2PSXADSRStream(stream, adsr->attack, adsr->decay, adsr->sustain);
@@ -384,7 +429,7 @@ struct RA2PSXSoundPlayer::Impl {
struct Voice {
Voice() : active(false), waiting(false), waitForSampleEnd(false), macroDone(false),
- root(false), sound(0), macro(0), step(0), adsr(0xffff), rate(0),
+ root(false), sound(0), macro(0), step(0), loopsLeft(-1), adsr(0xffff), rate(0),
sampleId(0), bendUp(2), bendDown(2),
pitch(RA2PSXSoundPlayer::kNeutralBend), volume(0), pan(64), priority(0),
born(0), readyTick(0),
@@ -398,6 +443,8 @@ struct RA2PSXSoundPlayer::Impl {
SoundId sound;
uint16 macro;
uint16 step;
+ // Countdown for macro op 5; negative means it has not been armed yet.
+ int loopsLeft;
uint16 adsr;
uint16 sampleId;
RA2PSXPitchRef pitchRef;
@@ -617,6 +664,18 @@ struct RA2PSXSoundPlayer::Impl {
}
break;
}
+ case 5:
+ // Jump back to a step, repeating the count in bytes 6-7; 0xffff never
+ // runs out. Only the low shield alarm uses it: 05 01 .. ff ff, so the
+ // beep-pause-beep body repeats for as long as the sound is held.
+ if (voice.loopsLeft == 0)
+ break;
+ if (voice.loopsLeft > 0)
+ --voice.loopsLeft;
+ else if (READ_LE_UINT16(command + 6) != 0xffff)
+ voice.loopsLeft = READ_LE_UINT16(command + 6);
+ voice.step = command[1];
+ break;
case 0xc:
voice.adsr = READ_LE_UINT16(command + 1);
break;
Commit: 22a782936cb79c755e80b7fccbb48493f30f1bfe
https://github.com/scummvm/scummvm/commit/22a782936cb79c755e80b7fccbb48493f30f1bfe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: emit the debris smoke trails as the original does in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level1.cpp
diff --git a/engines/scumm/insane/rebel2/psx/level1.cpp b/engines/scumm/insane/rebel2/psx/level1.cpp
index 0997a486e58..1a7cc0ade3c 100644
--- a/engines/scumm/insane/rebel2/psx/level1.cpp
+++ b/engines/scumm/insane/rebel2/psx/level1.cpp
@@ -373,13 +373,14 @@ struct RA2PSXLevel1Explosion {
};
struct RA2PSXLevel1Debris {
- RA2PSXLevel1Debris() : life(0), model(0) {
+ RA2PSXLevel1Debris() : life(0), model(0), puffCountdown(0) {
for (int axis = 0; axis < 3; ++axis)
position[axis] = velocity[axis] = rotation[axis] = spin[axis] = 0;
}
int life;
int model;
+ int puffCountdown;
int position[3];
int velocity[3];
int rotation[3];
@@ -470,25 +471,41 @@ void spawnLevel1Debris(RA2PSXLevel1Debris *debris, const int *position,
}
}
-// Each shard drags a smoke trail: a SMALLEX cell that spins and dims three steps a
-// frame from the grey the original picks on spawn.
+void rotateVector(const RA2PSXMatrix &transform, const int *source, int *result) {
+ for (int row = 0; row < 3; ++row) {
+ float value = 0.0f;
+ for (int column = 0; column < 3; ++column)
+ value += transform.rotation[row][column] * source[column];
+ result[row] = (int)value;
+ }
+}
+
+// Each shard drags a smoke trail: a SMALLEX cell that spins and dims three steps a frame
+// from the grey the original picks on spawn. The emitter counts down 5 and is stepped
+// twice a frame, so a puff leaves every two and a half frames, and each one is thrown
+// along an upward base velocity turned through up to a quarter turn on all three axes.
enum {
- kLevel1PuffCount = 48,
+ kLevel1PuffCount = 64,
kLevel1PuffSpin = 0x10,
kLevel1PuffFade = 3,
- kLevel1PuffInterval = 3,
- kLevel1PuffHalfSize = 64
+ kLevel1PuffReload = 5,
+ kLevel1PuffStepsPerFrame = 2,
+ kLevel1PuffHalfSize = 64,
+ kLevel1PuffSpread = 0x400
};
struct RA2PSXLevel1Puff {
RA2PSXLevel1Puff() : brightness(0), rotation(0), spin(0) {
- position[0] = position[1] = position[2] = 0;
+ for (int axis = 0; axis < 3; ++axis)
+ position[axis] = velocity[axis] = 0;
}
int brightness;
int rotation;
int spin;
+ // Kept in 1/4096 like the original's particles, so the drift stays smooth.
int position[3];
+ int velocity[3];
};
void spawnLevel1Puff(RA2PSXLevel1Puff *puffs, const int *position,
@@ -511,13 +528,28 @@ void spawnLevel1Puff(RA2PSXLevel1Puff *puffs, const int *position,
puff.rotation = (int)random.getRandomNumber(0xfff);
puff.spin = 0;
for (int axis = 0; axis < 3; ++axis)
- puff.position[axis] = position[axis];
+ puff.position[axis] = position[axis] * 4096;
+
+ RA2PSXMatrix spin;
+ spin.preRotateX((int)random.getRandomNumber(kLevel1PuffSpread - 1) - kLevel1PuffSpread / 2);
+ spin.preRotateY((int)random.getRandomNumber(kLevel1PuffSpread - 1) - kLevel1PuffSpread / 2);
+ spin.preRotateZ((int)random.getRandomNumber(kLevel1PuffSpread - 1) - kLevel1PuffSpread / 2);
+ const int base[3] = {
+ (int)random.getRandomNumber(499) - 250,
+ -((int)random.getRandomNumber(299) + 0xb6a),
+ 0
+ };
+ rotateVector(spin, base, puff.velocity);
+ for (int axis = 0; axis < 3; ++axis)
+ puff.velocity[axis] *= 16;
}
void updateLevel1Puffs(RA2PSXLevel1Puff *puffs) {
for (int i = 0; i < kLevel1PuffCount; ++i) {
if (!puffs[i].brightness)
continue;
+ for (int axis = 0; axis < 3; ++axis)
+ puffs[i].position[axis] += puffs[i].velocity[axis];
puffs[i].spin += kLevel1PuffSpin;
puffs[i].brightness = MAX(0, puffs[i].brightness - kLevel1PuffFade);
}
@@ -531,8 +563,8 @@ void renderLevel1Puffs(RA2PSXTinyGLRenderer &renderer, const RA2PSXTexture &text
const RA2PSXLevel1Puff &puff = puffs[i];
if (!puff.brightness)
continue;
- renderer.renderSprite(texture, (float)puff.position[0], (float)puff.position[1],
- (float)puff.position[2], kLevel1PuffHalfSize, kLevel1PuffHalfSize,
+ renderer.renderSprite(texture, puff.position[0] / 4096.0f, puff.position[1] / 4096.0f,
+ puff.position[2] / 4096.0f, kLevel1PuffHalfSize, kLevel1PuffHalfSize,
(int16)(puff.rotation + puff.spin), puff.brightness);
}
}
@@ -542,8 +574,11 @@ void updateLevel1Debris(RA2PSXLevel1Debris *debris, RA2PSXLevel1Puff *puffs,
for (int i = 0; i < kLevel1DebrisCount; ++i) {
if (!debris[i].life)
continue;
- if (debris[i].life % kLevel1PuffInterval == 0)
+ debris[i].puffCountdown -= kLevel1PuffStepsPerFrame;
+ while (debris[i].puffCountdown <= 0) {
+ debris[i].puffCountdown += kLevel1PuffReload;
spawnLevel1Puff(puffs, debris[i].position, random);
+ }
for (int axis = 0; axis < 3; ++axis) {
debris[i].position[axis] += debris[i].velocity[axis];
debris[i].rotation[axis] = (int16)(debris[i].rotation[axis] + debris[i].spin[axis]);
@@ -690,15 +725,6 @@ const float kLevel1ShipLaserStart[kLevel1BoltCount][3] = {
// The two cannon mounts, in the TIE's own space.
const int kLevel1TieMuzzle[2][3] = { { -60, 10, -50 }, { 60, 10, -50 } };
-void rotateVector(const RA2PSXMatrix &transform, const int *source, int *result) {
- for (int row = 0; row < 3; ++row) {
- float value = 0.0f;
- for (int column = 0; column < 3; ++column)
- value += transform.rotation[row][column] * source[column];
- result[row] = (int)value;
- }
-}
-
void spawnLevel1Enemy(RA2PSXLevel1Enemy &enemy, Common::RandomSource &random) {
enemy = RA2PSXLevel1Enemy();
enemy.active = true;
Commit: 233d70d6b1eb107806ef4641bab8c6111b5ff982
https://github.com/scummvm/scummvm/commit/233d70d6b1eb107806ef4641bab8c6111b5ff982
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: initial code for L2 in psx release
Changed paths:
A engines/scumm/insane/rebel2/psx/level2.cpp
engines/scumm/insane/rebel2/psx/psx.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.h
engines/scumm/insane/rebel2/rebel.cpp
engines/scumm/insane/rebel2/rebel.h
engines/scumm/insane/rebel2/runlevels.cpp
engines/scumm/insane/rebel2/shared.h
engines/scumm/module.mk
diff --git a/engines/scumm/insane/rebel2/psx/level2.cpp b/engines/scumm/insane/rebel2/psx/level2.cpp
new file mode 100644
index 00000000000..24fb957cc25
--- /dev/null
+++ b/engines/scumm/insane/rebel2/psx/level2.cpp
@@ -0,0 +1,455 @@
+/* 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/events.h"
+#include "common/system.h"
+#include "common/translation.h"
+#include "common/util.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/surface.h"
+
+#include "scumm/scumm_v7.h"
+#include "scumm/insane/rebel2/shared.h"
+#include "scumm/insane/rebel2/psx/psx.h"
+#include "scumm/insane/rebel2/psx/ui.h"
+
+namespace Scumm {
+
+// Level 2 is a cover shooter, and the original runs its three parts as separate levels:
+// 2, 0xc9 and 0xca. The same engine drives chapters 11 and 12.
+const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
+ { "l2P1bg", "r1L2P1", 0x26, 0x68, 0, 0x26, true,
+ 0x2a, 0x32, 0xcf, 0x8d,
+ { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
+ {
+ { 255, 65, 78, 116 }, { 250, 68, 76, 113 }, { 234, 68, 76, 113 },
+ { 219, 68, 70, 113 }, { 200, 72, 80, 109 }, { 169, 73, 88, 108 },
+ { 169, 73, 88, 108 }, { 167, 73, 90, 108 }, { 165, 73, 92, 108 },
+ { 165, 73, 92, 108 }, { 179, 73, 78, 108 }, { 179, 73, 78, 108 },
+ { 178, 73, 80, 108 }, { 174, 74, 84, 107 }, { 174, 74, 84, 107 },
+ { 181, 73, 78, 109 }, { 181, 73, 78, 109 }, { 181, 73, 78, 108 },
+ { 181, 73, 80, 108 }, { 181, 73, 80, 108 }, { 180, 73, 80, 108 },
+ { 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
+ { 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
+ { 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
+ } },
+ { "l2P2bg", "r1L2P2", 0x00, 0x00, -10, 0x12, false,
+ 0x1c, 0x26, 0x11e, 0xa0,
+ { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
+ {
+ { 255, 65, 78, 116 }, { 250, 68, 76, 113 }, { 234, 68, 76, 113 },
+ { 219, 68, 70, 113 }, { 200, 72, 80, 109 }, { 169, 73, 88, 108 },
+ { 169, 73, 88, 108 }, { 167, 73, 90, 108 }, { 165, 73, 92, 108 },
+ { 165, 73, 92, 108 }, { 179, 73, 78, 108 }, { 179, 73, 78, 108 },
+ { 178, 73, 80, 108 }, { 174, 74, 84, 107 }, { 174, 74, 84, 107 },
+ { 181, 73, 78, 109 }, { 181, 73, 78, 109 }, { 181, 73, 78, 108 },
+ { 181, 73, 80, 108 }, { 181, 73, 80, 108 }, { 180, 73, 80, 108 },
+ { 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
+ { 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
+ { 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
+ } },
+ { "l2P3bg", "r1L2P3", 0x00, 0x00, 0, 0x23, false,
+ 0x1a, 0x43, 0xc9, 0x92,
+ { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
+ {
+ { 255, 65, 78, 116 }, { 250, 68, 76, 113 }, { 234, 68, 76, 113 },
+ { 219, 68, 70, 113 }, { 200, 72, 80, 109 }, { 169, 73, 88, 108 },
+ { 169, 73, 88, 108 }, { 167, 73, 90, 108 }, { 165, 73, 92, 108 },
+ { 165, 73, 92, 108 }, { 179, 73, 78, 108 }, { 179, 73, 78, 108 },
+ { 178, 73, 80, 108 }, { 174, 74, 84, 107 }, { 174, 74, 84, 107 },
+ { 181, 73, 78, 109 }, { 181, 73, 78, 109 }, { 181, 73, 78, 108 },
+ { 181, 73, 80, 108 }, { 181, 73, 80, 108 }, { 180, 73, 80, 108 },
+ { 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
+ { 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
+ { 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
+ } }
+};
+
+RA2PSXLevel2Scene::RA2PSXLevel2Scene() : _part(0), _frame(0), _delay(0), _out(false),
+ _moving(false) {
+ for (int layer = 0; layer < 2; ++layer) {
+ for (int axis = 0; axis < 2; ++axis)
+ _scroll[layer][axis] = _scrollTarget[layer][axis] = _scrollStep[layer][axis] = 0;
+ }
+}
+
+const RA2PSXLevel2PartInfo &RA2PSXLevel2Scene::info() const {
+ return kRA2PSXLevel2Parts[_part];
+}
+
+bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part) {
+ if (part < 0 || part >= kRA2PSXLevel2PartCount)
+ return false;
+
+ Common::Array<byte> data;
+ _textures.clear();
+ if (!archive.getMember(kRA2PSXLevel2Parts[part].sheet, data) || !_textures.append(data))
+ return false;
+
+ static const char *const required[] = { "BACK_L", "BACK_R", "PARA_L", "PARA_R" };
+ for (uint i = 0; i < ARRAYSIZE(required); ++i) {
+ if (!_textures.has(required[i]))
+ return false;
+ }
+
+ // Every rookie pose is its own member: a format word, a palette and one image.
+ _rookie.clear();
+ for (int frame = 0; frame < kRA2PSXLevel2FrameCount; ++frame) {
+ const Common::String path = Common::String::format("%s/anim%02d",
+ kRA2PSXLevel2Parts[part].anims, frame);
+ Common::Array<byte> anim;
+ Common::Array<RA2PSXTexture> decoded;
+ if (!archive.getMember(path, anim) || anim.size() < 8)
+ return false;
+ const uint16 height = READ_LE_UINT16(anim.data() + 2) & 0xff;
+ if (!loadRA2PSXSpriteAnimation(anim, height ? height : 256, decoded) || decoded.empty())
+ return false;
+ _rookie.push_back(decoded[0]);
+ }
+
+ Common::Array<byte> hud;
+ _hud.clear();
+ if (archive.getMember("trpTex", hud))
+ _hud.append(hud);
+
+ _part = part;
+ _frame = 0;
+ _delay = 0;
+ _out = false;
+ _moving = false;
+ const RA2PSXLevel2PartInfo &part_ = kRA2PSXLevel2Parts[part];
+ _scroll[0][0] = _scrollTarget[0][0] = part_.backScroll << 16;
+ _scroll[1][0] = _scrollTarget[1][0] = part_.paraScroll << 16;
+ _scroll[0][1] = _scrollTarget[0][1] = 0;
+ _scroll[1][1] = _scrollTarget[1][1] = 0;
+ for (int layer = 0; layer < 2; ++layer) {
+ for (int axis = 0; axis < 2; ++axis)
+ _scrollStep[layer][axis] = 0;
+ }
+ return true;
+}
+
+void RA2PSXLevel2Scene::setScrollTarget(int backX, int backY, int paraX, int paraY) {
+ _scrollTarget[0][0] = backX;
+ _scrollTarget[0][1] = backY;
+ _scrollTarget[1][0] = paraX;
+ _scrollTarget[1][1] = paraY;
+ for (int layer = 0; layer < 2; ++layer) {
+ for (int axis = 0; axis < 2; ++axis)
+ _scrollStep[layer][axis] =
+ (_scrollTarget[layer][axis] - _scroll[layer][axis]) / kRA2PSXLevel2ScrollSteps;
+ }
+}
+
+void RA2PSXLevel2Scene::toggleCover() {
+ if (_moving)
+ return;
+ _moving = true;
+ if (_out) {
+ // Ducking back puts the walls where they started.
+ setScrollTarget(info().backScroll << 16, 0, info().paraScroll << 16, 0);
+ } else {
+ setScrollTarget(0, 0, 0, 0);
+ }
+}
+
+int RA2PSXLevel2Scene::aimFrame(int aimX, int aimY) const {
+ const RA2PSXLevel2PartInfo &part = info();
+ int column = 1;
+ while (column <= kRA2PSXLevel2AimColumns && part.aimColumns[column - 1] < aimX)
+ ++column;
+ int row = 0;
+ while (row < kRA2PSXLevel2AimRows && part.aimRows[row] < aimY)
+ ++row;
+ return CLIP<int>(column * kRA2PSXLevel2AimColumns + row,
+ (int)kRA2PSXLevel2CoverFrames, (int)kRA2PSXLevel2FrameCount - 1);
+}
+
+void RA2PSXLevel2Scene::update(int aimX, int aimY) {
+ for (int layer = 0; layer < 2; ++layer) {
+ for (int axis = 0; axis < 2; ++axis) {
+ int &value = _scroll[layer][axis];
+ const int target = _scrollTarget[layer][axis];
+ const int step = _scrollStep[layer][axis];
+ if (value == target || !step)
+ continue;
+ value += step;
+ if ((step < 0 && value <= target) || (step > 0 && value >= target))
+ value = target;
+ }
+ }
+
+ if (_moving) {
+ if (--_delay >= 0)
+ return;
+ _delay = kRA2PSXLevel2RookieDelay;
+ // Leaning out walks 0 to 4, ducking back walks it down again.
+ if (!_out) {
+ if (_frame < kRA2PSXLevel2CoverFrames - 1) {
+ ++_frame;
+ } else {
+ _moving = false;
+ _out = true;
+ _frame = aimFrame(aimX, aimY);
+ }
+ } else {
+ if (_frame >= kRA2PSXLevel2CoverFrames)
+ _frame = kRA2PSXLevel2CoverFrames - 1;
+ if (_frame > 0) {
+ --_frame;
+ } else {
+ _moving = false;
+ _out = false;
+ }
+ }
+ return;
+ }
+
+ if (_out)
+ _frame = aimFrame(aimX, aimY);
+}
+
+void RA2PSXLevel2Scene::drawLayer(Graphics::Surface &surface, const char *name,
+ int x, int y) const {
+ if (!_textures.has(name))
+ return;
+ _textures.draw(surface, name, x, y, Common::Rect(0, 0, 256, 256));
+}
+
+void RA2PSXLevel2Scene::drawFrame(Graphics::Surface &surface, const RA2PSXTexture &frame,
+ int x, int y) const {
+ for (int row = 0; row < frame.height; ++row) {
+ const int destY = y + row;
+ if (destY < 0 || destY >= surface.h)
+ continue;
+ for (int column = 0; column < frame.width; ++column) {
+ const int destX = x + column;
+ if (destX < 0 || destX >= surface.w)
+ continue;
+ const uint32 pixel = frame.pixels[row * frame.width + column];
+ if (!(pixel & 0x01000000))
+ continue;
+ surface.setPixel(destX, destY, surface.format.RGBToColor(
+ (pixel >> 16) & 0xff, (pixel >> 8) & 0xff, pixel & 0xff));
+ }
+ }
+}
+
+void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) const {
+ const RA2PSXLevel2PartInfo &part = info();
+ const int left = (surface.w - 320) / 2;
+ const int top = (surface.h - 240) / 2 + kRA2PSXLevel2SceneTop;
+ const int backX = _scroll[0][0] >> 16;
+ const int paraX = _scroll[1][0] >> 16;
+ surface.fillRect(Common::Rect(surface.w, surface.h), 0);
+
+ drawLayer(surface, "BACK_L", left - backX, top);
+ drawLayer(surface, "BACK_R", left + 256 - backX, top);
+ drawLayer(surface, "PARA_L", left - paraX, top);
+ if (part.hasMiddleStrip)
+ drawLayer(surface, "PARA_M", left + 0x36 - paraX, top);
+ drawLayer(surface, "PARA_R", left + 0x36 - paraX, top);
+
+ if ((uint)_frame < _rookie.size()) {
+ const RA2PSXLevel2Pose &pose = part.poses[_frame];
+ drawFrame(surface, _rookie[_frame], left + pose.x + part.rookieOffsetX,
+ (surface.h - 240) / 2 + pose.y + part.rookieOffsetY);
+ }
+
+ if (outOfCover() && _hud.has("CROSS"))
+ _hud.draw(surface, "CROSS", left + aimX - 4,
+ (surface.h - 240) / 2 + aimY - 3, Common::Rect(0, 0, 8, 7));
+}
+
+// The PSX release composites its waves from sprite sheets instead of playing a SMUSH
+// segment, but the level's shape is the same, so it rides runRebel2Level2 as well.
+class Rebel2PSX::Level2Handler : public Rebel2Level2Handler {
+public:
+ Level2Handler(Rebel2PSX &psx, const RA2PSXArchive &archive) :
+ _psx(psx), _archive(archive), _phaseState(0) {
+ }
+
+ bool shouldQuit() const override { return _psx._vm->shouldQuit(); }
+
+ bool playOpening() override {
+ return _psx.playVideo("S1/L02_INTR.STR", 1, false);
+ }
+
+ void beginAttempt() override {}
+
+ void beginPhase(int phase, bool) override {
+ _phaseState = 0;
+ if (!_scene.load(_archive, phase - 1))
+ warning("Rebel Assault II: could not load level 2 part %d", phase);
+ }
+
+ int16 waveBudget(int phase) override {
+ // The per difficulty budget table is not read out of the PSX build yet.
+ return (int16)(4 + phase);
+ }
+
+ bool playBackgroundWave(int phase) override { return showScene(phase); }
+ bool playWave(int phase, uint16) override { return showScene(phase); }
+
+ WaveCredit creditWave(int16, int16 *, int16) override {
+ // Nothing decodes r1L2P* or play* yet, so every phase reports itself finished
+ // rather than looping on waves that cannot be built.
+ WaveCredit credit;
+ credit.stop = true;
+ _phaseState = 0x0e;
+ return credit;
+ }
+
+ bool playPhaseEnd(int phase) override {
+ static const char *const cutscenes[] = {
+ "S1/L02_CUT1.STR", "S1/L02_CUT2.STR"
+ };
+ return _psx.playVideo(cutscenes[phase - 1], 1, false);
+ }
+
+ uint16 phaseState() const override { return _phaseState; }
+ bool playerDead() const override { return false; }
+ void accumulateKills() override {}
+ void accumulateMisses() override {}
+
+ bool handleDeath(int, Result &result) override {
+ result = kQuit;
+ return false;
+ }
+
+ void playComplete(int) override {
+ _psx.playVideo("S1/L02_CUT3.STR", 1, false);
+ _psx.playVideo("S1/L02_EXTR.STR", 1, false);
+ }
+
+private:
+ // Runs the part. The rookie leans out and ducks back, the crosshair picks one of the
+ // twenty five aim poses, and the walls slide between the two cover positions.
+ bool showScene(int phase) {
+ const RA2PSXLevel2PartInfo &part = kRA2PSXLevel2Parts[phase - 1];
+ const bool cursorWasVisible = CursorMan.isVisible();
+ CursorMan.showMouse(false);
+
+ int aimX = (part.aimLeft + part.aimRight) / 2;
+ int aimY = (part.aimTop + part.aimBottom) / 2;
+ bool left = false, right = false, up = false, down = false;
+ bool running = true;
+ const int viewX = ((int)_psx._vm->_screenWidth - 320) / 2;
+ const int viewY = ((int)_psx._vm->_screenHeight - 240) / 2;
+
+ while (running && !shouldQuit()) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ const bool pressed = event.type == Common::EVENT_KEYDOWN;
+ if (pressed || event.type == Common::EVENT_KEYUP) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ if (pressed)
+ running = false;
+ break;
+ case Common::KEYCODE_LEFT:
+ case Common::KEYCODE_a:
+ left = pressed;
+ break;
+ case Common::KEYCODE_RIGHT:
+ case Common::KEYCODE_d:
+ right = pressed;
+ break;
+ case Common::KEYCODE_UP:
+ case Common::KEYCODE_w:
+ up = pressed;
+ break;
+ case Common::KEYCODE_DOWN:
+ case Common::KEYCODE_s:
+ down = pressed;
+ break;
+ case Common::KEYCODE_SPACE:
+ case Common::KEYCODE_RETURN:
+ if (pressed && !event.kbdRepeat)
+ _scene.toggleCover();
+ break;
+ default:
+ break;
+ }
+ } else if (event.type == Common::EVENT_MOUSEMOVE) {
+ aimX = event.mouse.x - viewX;
+ aimY = event.mouse.y - viewY;
+ } else if (event.type == Common::EVENT_RBUTTONDOWN) {
+ _scene.toggleCover();
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ if (!_scene.outOfCover())
+ _scene.toggleCover();
+ } else if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _psx._vm->quitGame();
+ }
+ }
+
+ // Aiming only moves while the rookie is actually out of cover.
+ if (_scene.outOfCover()) {
+ aimX += (right ? 3 : 0) - (left ? 3 : 0);
+ aimY += (down ? 3 : 0) - (up ? 3 : 0);
+ }
+ aimX = CLIP<int>(aimX, part.aimLeft, part.aimRight);
+ aimY = CLIP<int>(aimY, part.aimTop, part.aimBottom);
+ _scene.update(aimX, aimY);
+
+ Graphics::Surface output;
+ output.create(_psx._vm->_screenWidth, _psx._vm->_screenHeight,
+ g_system->getScreenFormat());
+ _scene.draw(output, aimX, aimY);
+ g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0,
+ output.w, output.h);
+ output.free();
+ g_system->updateScreen();
+ g_system->delayMillis(20);
+ }
+
+ CursorMan.showMouse(cursorWasVisible);
+ return !shouldQuit();
+ }
+
+ Rebel2PSX &_psx;
+ const RA2PSXArchive &_archive;
+ RA2PSXLevel2Scene _scene;
+ uint16 _phaseState;
+};
+
+Common::Error Rebel2PSX::runLevel2() {
+ Common::SeekableReadStream *stream = openResource(2);
+ if (!stream)
+ return Common::Error(Common::kReadingFailed,
+ _("Could not open the PlayStation Level 2 resources"));
+ RA2PSXArchive archive;
+ const bool loaded = archive.load(*stream);
+ delete stream;
+ if (!loaded)
+ return Common::Error(Common::kReadingFailed,
+ _("Could not read the PlayStation Level 2 resources"));
+
+ Level2Handler handler(*this, archive);
+ if (runRebel2Level2(handler, _vm->_rnd) == Rebel2Level2Handler::kError)
+ return Common::Error(Common::kReadingFailed,
+ _("Could not play the PlayStation Level 2 videos"));
+ return Common::kNoError;
+}
+
+} // End of namespace Scumm
diff --git a/engines/scumm/insane/rebel2/psx/psx.cpp b/engines/scumm/insane/rebel2/psx/psx.cpp
index 037111c074b..95ff564b32a 100644
--- a/engines/scumm/insane/rebel2/psx/psx.cpp
+++ b/engines/scumm/insane/rebel2/psx/psx.cpp
@@ -362,9 +362,13 @@ Common::Error Rebel2PSX::runGame() {
const int picked = runChapterSelect(chapters);
if (!picked)
backedOut = true;
- else if (picked > 1)
- // Only chapter 1 is ported; stay on the list rather than
- // pretending to start the rest.
+ else if (picked == 2) {
+ const Common::Error error = runLevel2();
+ if (error.getCode() != Common::kNoError)
+ return error;
+ } else if (picked > 2)
+ // Chapters 3 and up are not ported; stay on the list rather than
+ // pretending to start them.
warning("Rebel Assault II: chapter %d is not implemented yet", picked);
else
chapter = picked;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 405fc353427..48c6224f886 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -37,6 +37,7 @@ namespace Scumm {
class ScummEngine_v7;
class RA2PSXLevel1UI;
+class RA2PSXLevel2Scene;
class RA2PSXMainMenuUI;
class RA2PSXMovieText;
class RA2PSXOptionsUI;
@@ -214,6 +215,46 @@ enum { kRA2PSXExplosionHeight = 56 };
bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeight,
Common::Array<RA2PSXTexture> &frames);
+// Level 2's three parts, each a sheet of backdrop halves, parallax walls and actors.
+enum {
+ kRA2PSXLevel2PartCount = 3,
+ kRA2PSXLevel2SceneTop = 28,
+ // Frames 0 to 4 are the rookie leaning out of cover; 5 to 29 are a five by five
+ // grid of aim poses picked from where the crosshair sits.
+ kRA2PSXLevel2CoverFrames = 5,
+ kRA2PSXLevel2FrameCount = 30,
+ kRA2PSXLevel2RookieDelay = 3,
+ kRA2PSXLevel2AimColumns = 5,
+ kRA2PSXLevel2AimRows = 5,
+ // The scroll eases to its target over this many ticks.
+ kRA2PSXLevel2ScrollSteps = 20
+};
+
+// Where each rookie frame is drawn, straight out of the original's per level table.
+struct RA2PSXLevel2Pose {
+ int16 x;
+ int16 y;
+ int16 width;
+ int16 height;
+};
+
+struct RA2PSXLevel2PartInfo {
+ const char *sheet;
+ const char *anims;
+ int backScroll;
+ int paraScroll;
+ int rookieOffsetX;
+ int rookieOffsetY;
+ bool hasMiddleStrip;
+ // Crosshair box, then the thresholds that split it into the aim grid.
+ int16 aimLeft, aimTop, aimRight, aimBottom;
+ int16 aimColumns[kRA2PSXLevel2AimColumns];
+ int16 aimRows[kRA2PSXLevel2AimRows];
+ RA2PSXLevel2Pose poses[kRA2PSXLevel2FrameCount];
+};
+
+extern const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount];
+
// How far the level 1 background is panned and tilted inside its oversized frame, and the
// matching shift the 3D scene rides along with.
struct RA2PSXBackgroundView {
@@ -319,6 +360,7 @@ public:
private:
class Level1Handler;
+ class Level2Handler;
enum MenuResult {
kMenuStart,
@@ -351,6 +393,7 @@ private:
bool loadLevel1Assets(RA2PSXModel &enemy, RA2PSXModel &ship,
RA2PSXModel &crosshair, RA2PSXModel &laser, RA2PSXModel &tieLaser,
Common::Array<RA2PSXModel> &debris, RA2PSXLevel1UI &ui);
+ Common::Error runLevel2();
Level1Result playLevel1(const RA2PSXModel &enemy, const RA2PSXModel &ship,
const RA2PSXModel &crosshair, const RA2PSXModel &laser,
const RA2PSXModel &tieLaser, const Common::Array<RA2PSXModel> &debris,
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 44d45d9796a..1d85fa398d2 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -52,6 +52,42 @@ private:
Common::Array<RA2PSXTexture> _textures;
};
+// One part of level 2: the backdrop halves and parallax walls scroll behind the rookie,
+// who stands centred and ducks in and out of cover.
+class RA2PSXLevel2Scene {
+public:
+ RA2PSXLevel2Scene();
+
+ bool load(const RA2PSXArchive &archive, int part);
+ // Toggles between cover and the open; ignored while a move is already running.
+ void toggleCover();
+ void update(int aimX, int aimY);
+ bool outOfCover() const { return _out && !_moving; }
+ bool busy() const { return _moving; }
+ void draw(Graphics::Surface &surface, int aimX, int aimY) const;
+ const RA2PSXLevel2PartInfo &info() const;
+
+private:
+ void drawLayer(Graphics::Surface &surface, const char *name, int x, int y) const;
+ void drawFrame(Graphics::Surface &surface, const RA2PSXTexture &frame,
+ int x, int y) const;
+ void setScrollTarget(int backX, int backY, int paraX, int paraY);
+ int aimFrame(int aimX, int aimY) const;
+
+ RA2PSXTextureSet _textures;
+ RA2PSXTextureSet _hud;
+ Common::Array<RA2PSXTexture> _rookie;
+ int _part;
+ int _frame;
+ int _delay;
+ bool _out;
+ bool _moving;
+ // Background and parallax scroll, held in 16.16 with a target to ease toward.
+ int _scroll[2][2];
+ int _scrollTarget[2][2];
+ int _scrollStep[2][2];
+};
+
// The GPU's semi-transparency mode 2: background minus source.
void subtractRA2PSXRect(Graphics::Surface &surface, const Common::Rect &rect,
int r, int g, int b);
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index 882da9df8fc..03ccda4446e 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -231,6 +231,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
_rebelAutoPlay = false;
_rebelWaveState = 0;
_rebelPhaseState = 0;
+ _totalKills = 0;
+ _totalMisses = 0;
_rebelAutopilot = 0;
_rebelDamageLevel = 0;
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index ed180e99ffc..a9858d8f441 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -272,6 +272,7 @@ public:
// Per-level handlers.
class Level1Handler;
int runLevel1();
+ class Level2Handler;
int runLevel2();
int runLevel3();
int runLevel4();
@@ -685,6 +686,9 @@ public:
int _rebelWaveState;
int _rebelPhaseState;
+ // Kills and misses banked across the phases of a level 2 style attempt.
+ int _totalKills;
+ int _totalMisses;
int _rebelAutopilot;
int _rebelDamageLevel;
diff --git a/engines/scumm/insane/rebel2/runlevels.cpp b/engines/scumm/insane/rebel2/runlevels.cpp
index 66c0f2889bc..ca223022b9e 100644
--- a/engines/scumm/insane/rebel2/runlevels.cpp
+++ b/engines/scumm/insane/rebel2/runlevels.cpp
@@ -455,226 +455,245 @@ void InsaneRebel2::resetHandler7FlightState() {
}
}
-int InsaneRebel2::runLevel2() {
- int bonusCount = 0;
- int totalKills = 0;
- int totalMisses = 0;
- int prevWaveState = 0;
-
- playCinematic("LEV02/02CUT.SAN");
- if (_vm->shouldQuit())
- return kLevelQuit;
-
- playLevelBegin(2);
- if (_vm->shouldQuit())
- return kLevelQuit;
+Rebel2Level2Handler::Result runRebel2Level2(Rebel2Level2Handler &handler,
+ Common::RandomSource &random) {
+ typedef Rebel2Level2Handler Handler;
- clearBit(0);
+ if (!handler.playOpening())
+ return handler.shouldQuit() ? Handler::kQuit : Handler::kError;
- while (!_vm->shouldQuit()) {
- resetLevelAttemptState(1);
- bonusCount = 0;
- totalKills = 0;
- totalMisses = 0;
-
- resetLevelPhaseState(false);
+ while (!handler.shouldQuit()) {
+ handler.beginAttempt();
+ int bonusCount = 0;
+ uint16 previousWave = 0;
+ bool died = false;
+ Handler::Result deathResult = Handler::kQuit;
+
+ for (int phase = 1; phase <= 3 && !died; ++phase) {
+ handler.beginPhase(phase, phase > 1);
+ int16 budget = handler.waveBudget(phase);
+ if (!handler.playBackgroundWave(phase))
+ return Handler::kQuit;
+
+ if (phase == 1) {
+ // Phase 1 ignores what a wave credited and simply rolls one of three.
+ handler.creditWave(0x36, &budget, 0);
+ while (!handler.playerDead() && (handler.phaseState() & 0x06) != 0x06) {
+ if (handler.shouldQuit())
+ return Handler::kQuit;
+ if (!handler.playWave(1, (uint16)random.getRandomNumber(2)))
+ return Handler::kQuit;
+ handler.creditWave(0x36, &budget, 0x14);
+ }
+ } else if (phase == 2) {
+ // Phase 2 re-credits from a threshold of zero at the top of every pass.
+ while (true) {
+ const Handler::WaveCredit credit = handler.creditWave(0x3e, &budget, 0);
+ uint16 wave = credit.bits;
+ if (credit.stop || (handler.phaseState() & 0x0e) == 0x0e)
+ break;
+ if (handler.shouldQuit())
+ return Handler::kQuit;
+ if ((wave & 0x0c) == 0)
+ wave = (uint16)random.getRandomNumber(2) + 0x10;
+ if (!handler.playWave(2, wave))
+ return Handler::kQuit;
+ }
+ } else {
+ Handler::WaveCredit credit = handler.creditWave(0x3e, &budget, 0);
+ while (!credit.stop && (handler.phaseState() & 0x0e) != 0x0e) {
+ if (handler.shouldQuit())
+ return Handler::kQuit;
+ uint16 wave = credit.bits;
+ // One pass in eight sets the low bit, but never twice running.
+ if (!(previousWave & 1) && !random.getRandomNumber(7))
+ wave |= 1;
+ previousWave = wave;
+ if (!handler.playWave(3, wave))
+ return Handler::kQuit;
+ credit = handler.creditWave(0x3e, &budget, 0x14);
+ }
+ }
- int16 budget = getWaveBudgetBase(1) + _vm->_rnd.getRandomNumber(2);
+ if (handler.phaseState() & 0x10)
+ ++bonusCount;
+ // The last phase banks its kills before the death check, the others after.
+ if (phase == 3)
+ handler.accumulateKills();
- debugC(DEBUG_INSANE, "Level 2 Phase 1 - playing 02P01_A.SAN (background) budget=%d", budget);
- if (!playLevelSegment("LEV02/P1/02P01_A.SAN", 0x28))
- return kLevelQuit;
+ if (handler.playerDead()) {
+ died = true;
+ if (!handler.handleDeath(phase, deathResult))
+ return deathResult;
+ break;
+ }
+ if (handler.shouldQuit())
+ return Handler::kQuit;
- processWaveEnd(0x36, &budget, 0, 0);
+ if (phase == 3) {
+ handler.accumulateMisses();
+ } else {
+ if (!handler.playPhaseEnd(phase))
+ return Handler::kQuit;
+ handler.accumulateKills();
+ handler.accumulateMisses();
+ }
+ }
- while (_playerDamage < 255 && (_rebelPhaseState & 0x06) != 0x06) {
- if (_vm->shouldQuit())
- return kLevelQuit;
+ if (died)
+ continue;
+ handler.playComplete(bonusCount);
+ return Handler::kComplete;
+ }
- 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"
- };
- debugC(DEBUG_INSANE, "Phase 1 wave - playing %s (state=0x%x budget=%d)", variants[variant], _rebelPhaseState, budget);
- if (!playLevelSegment(variants[variant], 0x428))
- return kLevelQuit;
+ return Handler::kQuit;
+}
- processWaveEnd(0x36, &budget, 0x14, 0);
- debugC(DEBUG_INSANE, "Phase 1 wave done - state=0x%x (need 0x06) budget=%d", _rebelPhaseState, budget);
- }
+// The DOS release plays each wave as a SMUSH segment; everything else about the level's
+// shape lives in runRebel2Level2.
+class InsaneRebel2::Level2Handler : public Rebel2Level2Handler {
+public:
+ explicit Level2Handler(InsaneRebel2 &rebel) : _rebel(rebel) {}
- if ((_rebelPhaseState & 0x10) != 0)
- bonusCount++;
+ bool shouldQuit() const override { return _rebel._vm->shouldQuit(); }
- if (_playerDamage >= 255) {
- int levelResult;
- if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
- continue;
- return levelResult;
- }
- if (_vm->shouldQuit())
- return kLevelQuit;
+ bool playOpening() override {
+ _rebel.playCinematic("LEV02/02CUT.SAN");
+ if (shouldQuit())
+ return false;
+ _rebel.playLevelBegin(2);
+ if (shouldQuit())
+ return false;
+ _rebel.clearBit(0);
+ return true;
+ }
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- if (!playLevelSegment("LEV02/02PST1.SAN", 0x28, false))
- return kLevelQuit;
+ void beginAttempt() override {
+ _rebel.resetLevelAttemptState(1);
+ _rebel._totalKills = 0;
+ _rebel._totalMisses = 0;
+ }
- totalKills += _rebelKillCounter;
- totalMisses += _rebelHitCounter;
+ void beginPhase(int phase, bool clearEnemies) override {
+ _rebel._currentPhase = phase;
+ _rebel.resetLevelPhaseState(clearEnemies);
+ }
- _currentPhase = 2;
- resetLevelPhaseState(true);
+ int16 waveBudget(int phase) override {
+ return _rebel.getWaveBudgetBase(phase) + _rebel._vm->_rnd.getRandomNumber(2);
+ }
- budget = getWaveBudgetBase(2) + _vm->_rnd.getRandomNumber(2);
+ bool playBackgroundWave(int phase) override {
+ static const char *const backgrounds[] = {
+ "LEV02/P1/02P01_A.SAN", "LEV02/P2/02P02_A.SAN", "LEV02/P3/02P03_A.SAN"
+ };
+ if (phase > 1)
+ _rebel._rebelHandler = 8;
+ return _rebel.playLevelSegment(backgrounds[phase - 1], 0x28);
+ }
- _rebelHandler = 8;
+ bool playWave(int phase, uint16 selection) override {
+ return _rebel.playLevelSegment(waveFile(phase, selection), 0x428);
+ }
- debugC(DEBUG_INSANE, "Level 2 Phase 2 - playing 02P02_A.SAN (background) budget=%d", budget);
- if (!playLevelSegment("LEV02/P2/02P02_A.SAN", 0x28))
- return kLevelQuit;
+ WaveCredit creditWave(int16 mask, int16 *budget, int16 threshold) override {
+ const WaveEndResult result = _rebel.processWaveEnd(mask, budget, threshold, 0);
+ WaveCredit credit;
+ credit.bits = result.creditedBits;
+ credit.stop = result.shouldStop();
+ return credit;
+ }
- while (true) {
- WaveEndResult waveEnd = processWaveEnd(0x3e, &budget, 0, 0);
- uint16 waveSelect = waveEnd.creditedBits;
- if (waveEnd.shouldStop() || (_rebelPhaseState & 0x0e) == 0x0e)
- break;
- if (_vm->shouldQuit())
- return kLevelQuit;
+ bool playPhaseEnd(int phase) override {
+ _rebel._rebelHandler = 0;
+ _rebel._rebelStatusBarSprite = 0;
+ return _rebel.playLevelSegment(phase == 1 ? "LEV02/02PST1.SAN" : "LEV02/02PST2.SAN",
+ 0x28, false);
+ }
+ uint16 phaseState() const override { return (uint16)_rebel._rebelPhaseState; }
+ bool playerDead() const override { return _rebel._playerDamage >= 255; }
+ void accumulateKills() override { _rebel._totalKills += _rebel._rebelKillCounter; }
+ void accumulateMisses() override { _rebel._totalMisses += _rebel._rebelHitCounter; }
+
+ bool handleDeath(int phase, Result &result) override {
+ int levelResult;
+ if (_rebel.handleLevelDeath(2, phase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN",
+ levelResult))
+ return true;
+ result = levelResult == kLevelGameOver ? kGameOver :
+ levelResult == kLevelQuit ? kQuit : kError;
+ return false;
+ }
- if ((waveSelect & 0x0c) == 0) {
- waveSelect = _vm->_rnd.getRandomNumber(2) + 0x10;
- }
+ void playComplete(int bonusCount) override {
+ const int accuracy = _rebel.calculateAccuracy(_rebel._totalKills, _rebel._totalMisses);
+ _rebel.playLevelEnd(2, accuracy, -1, bonusCount > 2);
+ _rebel._levelUnlocked[2] = true;
+ }
- const char *filename;
- switch (waveSelect) {
+private:
+ static const char *waveFile(int phase, uint16 selection) {
+ if (phase == 1) {
+ static const char *const variants[] = {
+ "LEV02/P1/02P01_B.SAN", "LEV02/P1/02P01_C.SAN", "LEV02/P1/02P01_D.SAN"
+ };
+ return variants[selection % 3];
+ }
+ if (phase == 2) {
+ switch (selection) {
case 4: case 6:
- filename = "LEV02/P2/02P02_B.SAN"; break;
+ return "LEV02/P2/02P02_B.SAN";
case 8: case 10:
- filename = "LEV02/P2/02P02_C.SAN"; break;
+ return "LEV02/P2/02P02_C.SAN";
case 0x0c: case 0x0e:
- filename = "LEV02/P2/02P02_A.SAN"; break;
+ return "LEV02/P2/02P02_A.SAN";
case 0x11:
- filename = "LEV02/P2/02P02_E.SAN"; break;
+ return "LEV02/P2/02P02_E.SAN";
case 0x12:
- filename = "LEV02/P2/02P02_F.SAN"; break;
+ return "LEV02/P2/02P02_F.SAN";
default:
- filename = "LEV02/P2/02P02_D.SAN"; break;
+ return "LEV02/P2/02P02_D.SAN";
}
-
- debugC(DEBUG_INSANE, "Phase 2 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
- if (!playLevelSegment(filename, 0x428))
- return kLevelQuit;
}
-
- if ((_rebelPhaseState & 0x10) != 0)
- bonusCount++;
-
- if (_playerDamage >= 255) {
- int levelResult;
- if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
- continue;
- return levelResult;
+ switch (selection) {
+ case 0:
+ return "LEV02/P3/02P03_H.SAN";
+ case 2:
+ return "LEV02/P3/02P03_G.SAN";
+ case 4:
+ return "LEV02/P3/02P03_F.SAN";
+ case 6:
+ return "LEV02/P3/02P03_E.SAN";
+ case 8:
+ return "LEV02/P3/02P03_D.SAN";
+ case 10:
+ return "LEV02/P3/02P03_C.SAN";
+ case 0x0c:
+ return "LEV02/P3/02P03_B.SAN";
+ case 0x0e:
+ return "LEV02/P3/02P03_A.SAN";
+ default:
+ return "LEV02/P3/02P03_I.SAN";
}
- if (_vm->shouldQuit())
- return kLevelQuit;
-
- _rebelHandler = 0;
- _rebelStatusBarSprite = 0;
- if (!playLevelSegment("LEV02/02PST2.SAN", 0x28, false))
- return kLevelQuit;
-
- totalKills += _rebelKillCounter;
- totalMisses += _rebelHitCounter;
-
- _currentPhase = 3;
- resetLevelPhaseState(true);
- prevWaveState = 0;
-
- budget = getWaveBudgetBase(3) + _vm->_rnd.getRandomNumber(2);
-
- _rebelHandler = 8;
-
- debugC(DEBUG_INSANE, "Level 2 Phase 3 - playing 02P03_A.SAN (background) budget=%d", budget);
- if (!playLevelSegment("LEV02/P3/02P03_A.SAN", 0x28))
- return kLevelQuit;
-
- {
- WaveEndResult waveEnd = processWaveEnd(0x3e, &budget, 0, 0);
-
- while (!waveEnd.shouldStop() && (_rebelPhaseState & 0x0e) != 0x0e) {
- if (_vm->shouldQuit())
- return kLevelQuit;
-
- uint16 waveSelect = waveEnd.creditedBits;
-
- if (((prevWaveState & 1) == 0) && (_vm->_rnd.getRandomNumber(7) == 0)) {
- waveSelect |= 1;
- }
- prevWaveState = waveSelect;
-
- 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;
- }
-
- debugC(DEBUG_INSANE, "Phase 3 wave - playing %s (state=0x%x sel=0x%x budget=%d)", filename, _rebelPhaseState, waveSelect, budget);
- if (!playLevelSegment(filename, 0x428))
- return kLevelQuit;
-
- waveEnd = processWaveEnd(0x3e, &budget, 0x14, 0);
- debugC(DEBUG_INSANE, "Phase 3 wave done - state=0x%x (need 0x0e) budget=%d", _rebelPhaseState, budget);
- }
- }
-
- if ((_rebelPhaseState & 0x10) != 0)
- bonusCount++;
- totalKills += _rebelKillCounter;
-
- if (_playerDamage >= 255) {
- int levelResult;
- if (handleLevelDeath(2, _currentPhase, "LEV02/02DIE.SAN", "LEV02/02RETRY.SAN", levelResult))
- continue;
- return levelResult;
- }
- if (_vm->shouldQuit())
- return kLevelQuit;
+ }
- {
- totalMisses += _rebelHitCounter;
- int accuracy = calculateAccuracy(totalKills, totalMisses);
- debugC(DEBUG_INSANE, "Level 2 completed! kills=%d misses=%d accuracy=%d%% bonus=%d",
- totalKills, totalMisses, accuracy, bonusCount);
- playLevelEnd(2, accuracy, -1, bonusCount > 2);
- }
+ InsaneRebel2 &_rebel;
+};
- _levelUnlocked[2] = true;
+int InsaneRebel2::runLevel2() {
+ Level2Handler handler(*this);
+ switch (runRebel2Level2(handler, _vm->_rnd)) {
+ case Rebel2Level2Handler::kComplete:
return kLevelNextLevel;
+ case Rebel2Level2Handler::kGameOver:
+ return kLevelGameOver;
+ default:
+ return kLevelQuit;
}
-
- return kLevelQuit;
}
+
int InsaneRebel2::runLevel3() {
int phase1Score = 0;
int phase1FlightErrors = 0;
diff --git a/engines/scumm/insane/rebel2/shared.h b/engines/scumm/insane/rebel2/shared.h
index f143d3488f1..2c2aeb6a4e3 100644
--- a/engines/scumm/insane/rebel2/shared.h
+++ b/engines/scumm/insane/rebel2/shared.h
@@ -15,6 +15,10 @@
#include "common/keyboard.h"
+namespace Common {
+class RandomSource;
+}
+
namespace Scumm {
enum Rebel2MenuCommand {
@@ -51,6 +55,52 @@ public:
Rebel2Level1Handler::Result runRebel2Level1(Rebel2Level1Handler &handler, int lives);
+// Level 2 - and the chapters built on the same cover shooter - runs three phases of waves
+// against a budget. The flow below is the level's design rather than any one release's, so
+// the handler only has to play what the runner asks for and report the state it keeps.
+class Rebel2Level2Handler {
+public:
+ enum Result {
+ kQuit,
+ kComplete,
+ kGameOver,
+ kError
+ };
+
+ // What a finished wave credited toward picking the next one, and whether the phase
+ // should stop regardless.
+ struct WaveCredit {
+ WaveCredit() : bits(0), stop(false) {}
+
+ uint16 bits;
+ bool stop;
+ };
+
+ virtual ~Rebel2Level2Handler() {}
+ virtual bool shouldQuit() const = 0;
+
+ virtual bool playOpening() = 0;
+ virtual void beginAttempt() = 0;
+ virtual void beginPhase(int phase, bool clearEnemies) = 0;
+ virtual int16 waveBudget(int phase) = 0;
+ virtual bool playBackgroundWave(int phase) = 0;
+ virtual bool playWave(int phase, uint16 selection) = 0;
+ virtual WaveCredit creditWave(int16 mask, int16 *budget, int16 threshold) = 0;
+ virtual bool playPhaseEnd(int phase) = 0;
+
+ virtual uint16 phaseState() const = 0;
+ virtual bool playerDead() const = 0;
+ virtual void accumulateKills() = 0;
+ virtual void accumulateMisses() = 0;
+
+ // Returns true when the player still has a life and the attempt should restart.
+ virtual bool handleDeath(int phase, Result &result) = 0;
+ virtual void playComplete(int bonusCount) = 0;
+};
+
+Rebel2Level2Handler::Result runRebel2Level2(Rebel2Level2Handler &handler,
+ Common::RandomSource &random);
+
inline Rebel2MenuCommand getRebel2MenuCommand(const Common::KeyState &key) {
switch (key.keycode) {
case Common::KEYCODE_UP:
diff --git a/engines/scumm/module.mk b/engines/scumm/module.mk
index 93edc2ba909..d2e5f1dc058 100644
--- a/engines/scumm/module.mk
+++ b/engines/scumm/module.mk
@@ -181,6 +181,7 @@ ifdef ENABLE_REBEL2_PSX
MODULE_OBJS += \
insane/rebel2/psx/audio.o \
insane/rebel2/psx/level1.o \
+ insane/rebel2/psx/level2.o \
insane/rebel2/psx/menu.o \
insane/rebel2/psx/model.o \
insane/rebel2/psx/psx.o \
Commit: 61864ceb266678865531f649b6df16c0bb4a573f
https://github.com/scummvm/scummvm/commit/61864ceb266678865531f649b6df16c0bb4a573f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: correctly implemented sliding effect and enemy waves in L2 in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level2.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.cpp
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level2.cpp b/engines/scumm/insane/rebel2/psx/level2.cpp
index 24fb957cc25..23e2a7010ec 100644
--- a/engines/scumm/insane/rebel2/psx/level2.cpp
+++ b/engines/scumm/insane/rebel2/psx/level2.cpp
@@ -35,8 +35,18 @@ namespace Scumm {
// Level 2 is a cover shooter, and the original runs its three parts as separate levels:
// 2, 0xc9 and 0xca. The same engine drives chapters 11 and 12.
+//
+// Every trooper slot owns four animations - stepping out, an aimed burst, a wild burst
+// and falling - at play script indices 8i to 8i+3, with its bolt at 4+8i.
const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
- { "l2P1bg", "r1L2P1", 0x26, 0x68, 0, 0x26, true,
+ { "l2P1bg", "r1L2P1",
+ { { 0x26, 0 }, { 0x68, 0 } }, { { 0, 0 }, { 0, 0 } }, 0,
+ { { "BACK_L", 0, 28, 256, 190, 0 },
+ { "BACK_R", 256, 28, 64, 190, 0 },
+ { "PARA_L", 0, 28, 54, 190, 1 },
+ { "PARA_M", 54, 28, 180, 25, 1 },
+ { "PARA_R", 234, 28, 190, 190, 1 } },
+ 0, 0x26,
0x2a, 0x32, 0xcf, 0x8d,
{ 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
{
@@ -50,8 +60,19 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
{ 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
{ 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
- } },
- { "l2P2bg", "r1L2P2", 0x00, 0x00, -10, 0x12, false,
+ },
+ 2, { "TRP_0", "TRP_1", nullptr }, { "LAS_0", "LAS_1", nullptr },
+ { { { 7, 0x1f }, { 7, 0x1a }, { 8, 0x0b } },
+ { { 7, 0x36 }, { 7, 0x28 }, { 8, 0x1a } },
+ { { 0, 0 }, { 0, 0 }, { 0, 0 } } } },
+ { "l2P2bg", "r1L2P2",
+ { { 0, 0x28 }, { 0, 0x28 } }, { { 0, 0x0a }, { 0, -20 } }, 1,
+ { { "BACK_L", 0, 28, 256, 200, 0 },
+ { "BACK_R", 256, 28, 64, 200, 0 },
+ { "PARA_L", 0, 28, 64, 230, 1 },
+ { "PARA_R", 64, 151, 256, 107, 1 },
+ { nullptr, 0, 0, 0, 0, 0 } },
+ -10, 0x12,
0x1c, 0x26, 0x11e, 0xa0,
{ 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
{
@@ -65,8 +86,19 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
{ 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
{ 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
- } },
- { "l2P3bg", "r1L2P3", 0x00, 0x00, 0, 0x23, false,
+ },
+ 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
+ { { { 0x12, 0x2b }, { 0x0e, 0x24 }, { 0x0e, 0x19 } },
+ { { 0x17, 0x3a }, { 0x15, 0x28 }, { 0x15, 0x1e } },
+ { { 0x0d, 0x3b }, { 0x0d, 0x29 }, { 0x0f, 0x1c } } } },
+ { "l2P3bg", "r1L2P3",
+ { { 0x0f, 0 }, { -114, 0 } }, { { 0x1e, 0 }, { -14, 0 } }, 0,
+ { { "BACK_L", 0, 28, 256, 190, 0 },
+ { "BACK_R", 256, 28, 94, 190, 0 },
+ { "PARA_L", 0, 28, 46, 190, 1 },
+ { "PARA_R", 46, 28, 44, 190, 1 },
+ { nullptr, 0, 0, 0, 0, 0 } },
+ 0, 0x23,
0x1a, 0x43, 0xc9, 0x92,
{ 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
{
@@ -80,14 +112,48 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
{ 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
{ 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
- } }
+ },
+ 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
+ { { { 0, 0 }, { 0, 0 }, { 0, 0 } },
+ { { 0, 0 }, { 0, 0 }, { 0, 0 } },
+ { { 0, 0 }, { 0, 0 }, { 0, 0 } } } }
};
-RA2PSXLevel2Scene::RA2PSXLevel2Scene() : _part(0), _frame(0), _delay(0), _out(false),
- _moving(false) {
+// How many troopers a part sends, as base plus a roll of range.
+const int16 kRA2PSXLevel2WaveTable[kRA2PSXLevel2PartCount][3][2] = {
+ { { 4, 0 }, { 6, 2 }, { 6, 4 } },
+ { { 5, 0 }, { 6, 3 }, { 7, 4 } },
+ { { 6, 0 }, { 8, 4 }, { 9, 5 } }
+};
+
+// { reappear base, reappear range, aim chance percent, respawn base, respawn range }
+const int16 kRA2PSXLevel2SlotTable[kRA2PSXLevel2PartCount][3][5] = {
+ { { 140, 20, 66, 120, 60 }, { 100, 20, 66, 120, 60 }, { 80, 20, 50, 90, 40 } },
+ { { 140, 60, 66, 120, 60 }, { 100, 60, 58, 120, 60 }, { 80, 20, 50, 90, 40 } },
+ { { 140, 80, 66, 120, 60 }, { 100, 80, 50, 120, 60 }, { 80, 20, 50, 90, 40 } }
+};
+
+const int16 kRA2PSXLevel2FireTable[2][3][2] = {
+ { { 9, 6 }, { 8, 6 }, { 6, 4 } },
+ { { 9, 6 }, { 10, 6 }, { 6, 4 } }
+};
+
+const int16 kRA2PSXLevel2BoltTable[kRA2PSXLevel2PartCount][3][2] = {
+ { { 30, 180 }, { 40, 256 }, { 50, 256 } },
+ { { 30, 180 }, { 40, 256 }, { 50, 256 } },
+ { { 30, 180 }, { 40, 256 }, { 50, 256 } }
+};
+
+const int16 kRA2PSXLevel2KillScore[3] = { 80, 100, 150 };
+
+RA2PSXLevel2Scene::RA2PSXLevel2Scene() : _part(0), _difficulty(0), _frame(0), _delay(0),
+ _out(false), _moving(false), _tick(0), _remaining(0), _active(0), _clearTicks(0),
+ _kills(0), _misses(0) {
for (int layer = 0; layer < 2; ++layer) {
- for (int axis = 0; axis < 2; ++axis)
- _scroll[layer][axis] = _scrollTarget[layer][axis] = _scrollStep[layer][axis] = 0;
+ for (int axis = 0; axis < 2; ++axis) {
+ _scroll[layer][axis] = _scrollTarget[layer][axis] = 0;
+ _scrollStep[layer][axis] = _scrollHold[layer][axis] = 0;
+ }
}
}
@@ -95,7 +161,8 @@ const RA2PSXLevel2PartInfo &RA2PSXLevel2Scene::info() const {
return kRA2PSXLevel2Parts[_part];
}
-bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part) {
+bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part, int difficulty,
+ Common::RandomSource &random) {
if (part < 0 || part >= kRA2PSXLevel2PartCount)
return false;
@@ -130,32 +197,197 @@ bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part) {
if (archive.getMember("trpTex", hud))
_hud.append(hud);
+ Common::Array<byte> script;
+ _play.clear();
+ if (!archive.getMember(Common::String::format("play%d", part + 1), script) ||
+ !loadRA2PSXPlayScript(script, _play))
+ return false;
+
_part = part;
+ _difficulty = CLIP(difficulty, 0, 2);
_frame = 0;
_delay = 0;
_out = false;
_moving = false;
const RA2PSXLevel2PartInfo &part_ = kRA2PSXLevel2Parts[part];
- _scroll[0][0] = _scrollTarget[0][0] = part_.backScroll << 16;
- _scroll[1][0] = _scrollTarget[1][0] = part_.paraScroll << 16;
- _scroll[0][1] = _scrollTarget[0][1] = 0;
- _scroll[1][1] = _scrollTarget[1][1] = 0;
- for (int layer = 0; layer < 2; ++layer) {
- for (int axis = 0; axis < 2; ++axis)
- _scrollStep[layer][axis] = 0;
+ for (int plane = 0; plane < 2; ++plane) {
+ for (int axis = 0; axis < 2; ++axis) {
+ _scroll[plane][axis] = _scrollTarget[plane][axis] =
+ part_.coverScroll[plane][axis] << 16;
+ _scrollStep[plane][axis] = _scrollHold[plane][axis] = 0;
+ }
+ }
+
+ const int16 *wave = kRA2PSXLevel2WaveTable[part][_difficulty];
+ _tick = 0;
+ _remaining = wave[0] + (wave[1] ? (int)random.getRandomNumber(wave[1]) : 0);
+ _active = 0;
+ _clearTicks = 0;
+ _kills = 0;
+ _misses = 0;
+ for (int i = 0; i < kRA2PSXLevel2TrooperCount; ++i) {
+ _troopers[i] = RA2PSXLevel2Actor();
+ _bolts[i] = RA2PSXLevel2Actor();
+ // Nothing steps out before the part has settled.
+ _troopers[i].slotTick = slotDelay(kRA2PSXLevel2SlotTable[part][_difficulty][0],
+ kRA2PSXLevel2SlotTable[part][_difficulty][1], random);
}
return true;
}
-void RA2PSXLevel2Scene::setScrollTarget(int backX, int backY, int paraX, int paraY) {
- _scrollTarget[0][0] = backX;
- _scrollTarget[0][1] = backY;
- _scrollTarget[1][0] = paraX;
- _scrollTarget[1][1] = paraY;
- for (int layer = 0; layer < 2; ++layer) {
- for (int axis = 0; axis < 2; ++axis)
- _scrollStep[layer][axis] =
- (_scrollTarget[layer][axis] - _scroll[layer][axis]) / kRA2PSXLevel2ScrollSteps;
+int RA2PSXLevel2Scene::slotDelay(int base, int range, Common::RandomSource &random) const {
+ return _tick + base + (range ? (int)random.getRandomNumber(range) : 0);
+}
+
+void RA2PSXLevel2Scene::startActor(RA2PSXLevel2Actor &actor, int state, int animation) {
+ actor.state = state;
+ actor.animation = animation;
+ actor.frame = 0;
+ actor.hold = kRA2PSXLevel2FrameTicks;
+ actor.hit = false;
+ actor.fireNext = 0;
+ actor.fireEnd = 0;
+}
+
+const RA2PSXPlayFrame *RA2PSXLevel2Scene::actorFrame(const RA2PSXLevel2Actor &actor) const {
+ if (actor.animation < 0 || (uint)actor.animation >= _play.size())
+ return nullptr;
+ const RA2PSXPlayAnimation &animation = _play[actor.animation];
+ if ((uint)actor.frame >= animation.size())
+ return nullptr;
+ return &animation[actor.frame];
+}
+
+void RA2PSXLevel2Scene::advanceActor(RA2PSXLevel2Actor &actor) {
+ const RA2PSXPlayFrame *frame = actorFrame(actor);
+ if (!frame || (frame->flags & kRA2PSXPlayLastFrame))
+ return;
+ ++actor.frame;
+}
+
+int RA2PSXLevel2Scene::updateEnemies(Common::RandomSource &random) {
+ ++_tick;
+ const RA2PSXLevel2PartInfo &part = info();
+ const int16 *slot = kRA2PSXLevel2SlotTable[_part][_difficulty];
+ const int16 *bolt = kRA2PSXLevel2BoltTable[_part][_difficulty];
+ int damage = 0;
+
+ for (int i = 0; i < part.trooperCount; ++i) {
+ RA2PSXLevel2Actor &trooper = _troopers[i];
+ RA2PSXLevel2Actor &shot = _bolts[i];
+
+ // The bolt goes first: its flagged frame is the moment the shot lands.
+ if (shot.state != kRA2PSXLevel2StateIdle && --shot.hold <= 0) {
+ shot.hold = kRA2PSXLevel2FrameTicks;
+ const RA2PSXPlayFrame *frame = actorFrame(shot);
+ if (frame && (frame->flags & kRA2PSXPlayHitsPlayer) && exposed() &&
+ (int)random.getRandomNumber(99) < bolt[0])
+ damage += bolt[1];
+ if (!frame || (frame->flags & kRA2PSXPlayLastFrame))
+ shot.state = kRA2PSXLevel2StateIdle;
+ else
+ ++shot.frame;
+ }
+
+ switch (trooper.state) {
+ case kRA2PSXLevel2StateIdle:
+ // A fresh trooper only steps out while the wave still owes bodies.
+ if (_tick >= trooper.slotTick && _remaining - _active > 0) {
+ startActor(trooper, kRA2PSXLevel2StateAppear, i * 8);
+ ++_active;
+ trooper.fireNext = part.fireWindows[i][0].start;
+ trooper.fireEnd = part.fireWindows[i][0].end;
+ if (!trooper.fireEnd)
+ startActor(shot, kRA2PSXLevel2StateShot, 4 + i * 8);
+ }
+ continue;
+ case kRA2PSXLevel2StateCover:
+ if (_tick >= trooper.slotTick) {
+ const bool aimed = (int)random.getRandomNumber(99) < slot[2];
+ const int state = aimed ? kRA2PSXLevel2StateAimed : kRA2PSXLevel2StateWild;
+ startActor(trooper, state, i * 8 + (aimed ? 1 : 2));
+ trooper.fireNext = part.fireWindows[i][aimed ? 1 : 2].start;
+ trooper.fireEnd = part.fireWindows[i][aimed ? 1 : 2].end;
+ if (!trooper.fireEnd)
+ startActor(shot, kRA2PSXLevel2StateShot, i * 8 + (aimed ? 5 : 6));
+ }
+ continue;
+ default:
+ break;
+ }
+
+ // Parts one and two shoot on a frame window inside the animation instead.
+ if (trooper.fireEnd && trooper.frame >= trooper.fireNext &&
+ trooper.frame <= trooper.fireEnd) {
+ const int16 *cadence = kRA2PSXLevel2FireTable[MIN(_part, 1)][_difficulty];
+ startActor(shot, kRA2PSXLevel2StateShot, 4 + i * 8);
+ trooper.fireNext = trooper.frame + cadence[0] +
+ (int)random.getRandomNumber(cadence[1]);
+ }
+
+ if (--trooper.hold > 0)
+ continue;
+ trooper.hold = kRA2PSXLevel2FrameTicks;
+ const RA2PSXPlayFrame *frame = actorFrame(trooper);
+ if (frame && !(frame->flags & kRA2PSXPlayLastFrame)) {
+ ++trooper.frame;
+ continue;
+ }
+
+ if (trooper.state == kRA2PSXLevel2StateDie) {
+ trooper.state = kRA2PSXLevel2StateIdle;
+ --_active;
+ --_remaining;
+ trooper.slotTick = slotDelay(slot[3], slot[4], random);
+ continue;
+ }
+ // It ducked back untouched, which the original scores as a miss.
+ ++_misses;
+ trooper.state = kRA2PSXLevel2StateCover;
+ trooper.slotTick = slotDelay(slot[0], slot[1], random);
+ }
+
+ if (_remaining <= 0)
+ ++_clearTicks;
+ return damage;
+}
+
+int RA2PSXLevel2Scene::shoot(int aimX, int aimY) {
+ if (!outOfCover())
+ return 0;
+
+ for (int i = 0; i < info().trooperCount; ++i) {
+ RA2PSXLevel2Actor &trooper = _troopers[i];
+ if (trooper.state == kRA2PSXLevel2StateIdle ||
+ trooper.state == kRA2PSXLevel2StateCover ||
+ trooper.state == kRA2PSXLevel2StateDie)
+ continue;
+ const RA2PSXPlayFrame *frame = actorFrame(trooper);
+ if (!frame || !(frame->flags & kRA2PSXPlayTargetable))
+ continue;
+ if (aimX < frame->boxLeft || aimX > frame->boxRight ||
+ aimY < frame->boxTop || aimY > frame->boxBottom)
+ continue;
+
+ startActor(trooper, kRA2PSXLevel2StateDie, i * 8 + 3);
+ _bolts[i].state = kRA2PSXLevel2StateIdle;
+ ++_kills;
+ return kRA2PSXLevel2KillScore[_difficulty];
+ }
+ return 0;
+}
+
+void RA2PSXLevel2Scene::setScrollTarget(const int16 target[2][2]) {
+ const RA2PSXLevel2PartInfo &part = info();
+ for (int plane = 0; plane < 2; ++plane) {
+ for (int axis = 0; axis < 2; ++axis) {
+ _scrollTarget[plane][axis] = target[plane][axis] << 16;
+ _scrollStep[plane][axis] =
+ (_scrollTarget[plane][axis] - _scroll[plane][axis]) / kRA2PSXLevel2ScrollSteps;
+ // Only the axis the part actually slides along eases out.
+ _scrollHold[plane][axis] =
+ axis == part.scrollAxis ? kRA2PSXLevel2ScrollHold : 1000;
+ }
}
}
@@ -163,12 +395,7 @@ void RA2PSXLevel2Scene::toggleCover() {
if (_moving)
return;
_moving = true;
- if (_out) {
- // Ducking back puts the walls where they started.
- setScrollTarget(info().backScroll << 16, 0, info().paraScroll << 16, 0);
- } else {
- setScrollTarget(0, 0, 0, 0);
- }
+ setScrollTarget(_out ? info().coverScroll : info().openScroll);
}
int RA2PSXLevel2Scene::aimFrame(int aimX, int aimY) const {
@@ -187,10 +414,14 @@ void RA2PSXLevel2Scene::update(int aimX, int aimY) {
for (int layer = 0; layer < 2; ++layer) {
for (int axis = 0; axis < 2; ++axis) {
int &value = _scroll[layer][axis];
+ int &step = _scrollStep[layer][axis];
const int target = _scrollTarget[layer][axis];
- const int step = _scrollStep[layer][axis];
if (value == target || !step)
continue;
+ if (_scrollHold[layer][axis] > 0)
+ --_scrollHold[layer][axis];
+ else
+ step = (step >> 8) * (kRA2PSXLevel2ScrollDamping >> 8);
value += step;
if ((step < 0 && value <= target) || (step > 0 && value >= target))
value = target;
@@ -227,11 +458,14 @@ void RA2PSXLevel2Scene::update(int aimX, int aimY) {
_frame = aimFrame(aimX, aimY);
}
-void RA2PSXLevel2Scene::drawLayer(Graphics::Surface &surface, const char *name,
- int x, int y) const {
- if (!_textures.has(name))
+void RA2PSXLevel2Scene::drawLayer(Graphics::Surface &surface,
+ const RA2PSXLevel2Layer &layer, int left, int top) const {
+ if (!layer.name || !_textures.has(layer.name))
return;
- _textures.draw(surface, name, x, y, Common::Rect(0, 0, 256, 256));
+ _textures.draw(surface, layer.name,
+ left + layer.x - (_scroll[layer.plane][0] >> 16),
+ top + layer.y - (_scroll[layer.plane][1] >> 16),
+ Common::Rect(0, 0, layer.width, layer.height));
}
void RA2PSXLevel2Scene::drawFrame(Graphics::Surface &surface, const RA2PSXTexture &frame,
@@ -253,38 +487,76 @@ void RA2PSXLevel2Scene::drawFrame(Graphics::Surface &surface, const RA2PSXTextur
}
}
+void RA2PSXLevel2Scene::drawPlayFrame(Graphics::Surface &surface,
+ const RA2PSXPlayFrame &frame, const Common::Array<uint32> &palette,
+ int left, int top) const {
+ for (int row = 0; row < frame.height; ++row) {
+ const int destY = top + frame.y + row;
+ if (destY < 0 || destY >= surface.h)
+ continue;
+ for (int column = 0; column < frame.width; ++column) {
+ const int destX = left + frame.x + column;
+ if (destX < 0 || destX >= surface.w)
+ continue;
+ const byte index = frame.pixels[row * frame.width + column];
+ const uint32 pixel = index < palette.size() ? palette[index] : 0;
+ if (!(pixel & 0x01000000))
+ continue;
+ surface.setPixel(destX, destY, surface.format.RGBToColor(
+ (pixel >> 16) & 0xff, (pixel >> 8) & 0xff, pixel & 0xff));
+ }
+ }
+}
+
void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) const {
const RA2PSXLevel2PartInfo &part = info();
const int left = (surface.w - 320) / 2;
- const int top = (surface.h - 240) / 2 + kRA2PSXLevel2SceneTop;
- const int backX = _scroll[0][0] >> 16;
- const int paraX = _scroll[1][0] >> 16;
+ const int top = (surface.h - 240) / 2;
surface.fillRect(Common::Rect(surface.w, surface.h), 0);
- drawLayer(surface, "BACK_L", left - backX, top);
- drawLayer(surface, "BACK_R", left + 256 - backX, top);
- drawLayer(surface, "PARA_L", left - paraX, top);
- if (part.hasMiddleStrip)
- drawLayer(surface, "PARA_M", left + 0x36 - paraX, top);
- drawLayer(surface, "PARA_R", left + 0x36 - paraX, top);
+ for (int i = 0; i < kRA2PSXLevel2LayerCount; ++i)
+ drawLayer(surface, part.layers[i], left, top);
if ((uint)_frame < _rookie.size()) {
const RA2PSXLevel2Pose &pose = part.poses[_frame];
drawFrame(surface, _rookie[_frame], left + pose.x + part.rookieOffsetX,
- (surface.h - 240) / 2 + pose.y + part.rookieOffsetY);
+ top + pose.y + part.rookieOffsetY);
+ }
+
+ // Troopers ride the backdrop, so ducking slides them behind the near wall.
+ const int actorX = left - (_scroll[0][0] >> 16);
+ const int actorY = top + kRA2PSXLevel2SceneTop - (_scroll[0][1] >> 16);
+ for (int i = 0; i < part.trooperCount; ++i) {
+ const Common::Array<uint32> *palette = _textures.palette(part.trooperPalettes[i]);
+ if (!palette)
+ continue;
+ if (_troopers[i].state != kRA2PSXLevel2StateIdle &&
+ _troopers[i].state != kRA2PSXLevel2StateCover) {
+ const RA2PSXPlayFrame *frame = actorFrame(_troopers[i]);
+ if (frame)
+ drawPlayFrame(surface, *frame, *palette, actorX, actorY);
+ }
+ const Common::Array<uint32> *bolt = _textures.palette(part.boltPalettes[i]);
+ if (bolt && _bolts[i].state != kRA2PSXLevel2StateIdle) {
+ const RA2PSXPlayFrame *frame = actorFrame(_bolts[i]);
+ if (frame)
+ drawPlayFrame(surface, *frame, *bolt, actorX, actorY);
+ }
}
if (outOfCover() && _hud.has("CROSS"))
- _hud.draw(surface, "CROSS", left + aimX - 4,
- (surface.h - 240) / 2 + aimY - 3, Common::Rect(0, 0, 8, 7));
+ _hud.draw(surface, "CROSS", left + aimX - 4, top + aimY - 3,
+ Common::Rect(0, 0, 8, 7));
}
// The PSX release composites its waves from sprite sheets instead of playing a SMUSH
-// segment, but the level's shape is the same, so it rides runRebel2Level2 as well.
+// segment, but the level's shape is the same, so it rides runRebel2Level2 as well. Each
+// wave is one pass at the part's troopers; the part is over once its budget is spent.
class Rebel2PSX::Level2Handler : public Rebel2Level2Handler {
public:
Level2Handler(Rebel2PSX &psx, const RA2PSXArchive &archive) :
- _psx(psx), _archive(archive), _phaseState(0) {
+ _psx(psx), _archive(archive), _phaseState(0), _shield(kRA2PSXShieldFull),
+ _score(0), _kills(0), _misses(0), _lives(3), _skipped(false) {
}
bool shouldQuit() const override { return _psx._vm->shouldQuit(); }
@@ -293,28 +565,44 @@ public:
return _psx.playVideo("S1/L02_INTR.STR", 1, false);
}
- void beginAttempt() override {}
+ void beginAttempt() override {
+ _shield = kRA2PSXShieldFull;
+ _kills = 0;
+ _misses = 0;
+ }
void beginPhase(int phase, bool) override {
_phaseState = 0;
- if (!_scene.load(_archive, phase - 1))
+ _skipped = false;
+ if (!_scene.load(_archive, phase - 1, _psx._settings.difficulty, _psx._vm->_rnd))
warning("Rebel Assault II: could not load level 2 part %d", phase);
}
+ // One wave per trooper the part scripts, so the shared loop runs the whole part.
int16 waveBudget(int phase) override {
- // The per difficulty budget table is not read out of the PSX build yet.
- return (int16)(4 + phase);
+ const int16 *wave = kRA2PSXLevel2WaveTable[CLIP(phase - 1, 0, 2)]
+ [CLIP(_psx._settings.difficulty, 0, 2)];
+ return (int16)(wave[0] + wave[1]);
}
bool playBackgroundWave(int phase) override { return showScene(phase); }
bool playWave(int phase, uint16) override { return showScene(phase); }
- WaveCredit creditWave(int16, int16 *, int16) override {
- // Nothing decodes r1L2P* or play* yet, so every phase reports itself finished
- // rather than looping on waves that cannot be built.
+ // The PSX part is one continuous scene rather than a chain of movie waves, so a pass
+ // normally settles the phase outright. The budget only matters if one is cut short.
+ WaveCredit creditWave(int16 mask, int16 *budget, int16) override {
WaveCredit credit;
- credit.stop = true;
- _phaseState = 0x0e;
+ if (_scene.cleared() || playerDead() || _skipped) {
+ _phaseState = (uint16)mask;
+ credit.stop = true;
+ return credit;
+ }
+ if (budget && *budget > 0) {
+ --*budget;
+ credit.bits = 2;
+ } else {
+ credit.stop = true;
+ }
return credit;
}
@@ -326,12 +614,20 @@ public:
}
uint16 phaseState() const override { return _phaseState; }
- bool playerDead() const override { return false; }
- void accumulateKills() override {}
- void accumulateMisses() override {}
+ bool playerDead() const override { return _shield <= 0; }
+ void accumulateKills() override { _kills += _scene.kills(); }
+ void accumulateMisses() override { _misses += _scene.misses(); }
+ // A death costs a life and restarts the level from its first part.
bool handleDeath(int, Result &result) override {
- result = kQuit;
+ if (!_psx.playVideo("S1/L02_DIE.STR", 1, false)) {
+ result = shouldQuit() ? kQuit : kError;
+ return false;
+ }
+ if (--_lives > 0)
+ return true;
+ _psx.playVideo("S1/L02_OVER.STR", 1, false);
+ result = kGameOver;
return false;
}
@@ -341,8 +637,8 @@ public:
}
private:
- // Runs the part. The rookie leans out and ducks back, the crosshair picks one of the
- // twenty five aim poses, and the walls slide between the two cover positions.
+ // Runs one pass at the part: the rookie leans out and ducks, the scripted troopers
+ // step out and trade fire, and the pass ends when the wave or the shield runs out.
bool showScene(int phase) {
const RA2PSXLevel2PartInfo &part = kRA2PSXLevel2Parts[phase - 1];
const bool cursorWasVisible = CursorMan.isVisible();
@@ -352,18 +648,25 @@ private:
int aimY = (part.aimTop + part.aimBottom) / 2;
bool left = false, right = false, up = false, down = false;
bool running = true;
+ int flashFrame = kRA2PSXHitFlashFrames;
+ int tick = -1;
+ const uint32 startTime = g_system->getMillis();
const int viewX = ((int)_psx._vm->_screenWidth - 320) / 2;
const int viewY = ((int)_psx._vm->_screenHeight - 240) / 2;
while (running && !shouldQuit()) {
+ bool shoot = false;
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
const bool pressed = event.type == Common::EVENT_KEYDOWN;
if (pressed || event.type == Common::EVENT_KEYUP) {
switch (event.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- if (pressed)
+ // Stands in for the pause menu, which is not ported yet.
+ if (pressed) {
+ _skipped = true;
running = false;
+ }
break;
case Common::KEYCODE_LEFT:
case Common::KEYCODE_a:
@@ -381,11 +684,14 @@ private:
case Common::KEYCODE_s:
down = pressed;
break;
- case Common::KEYCODE_SPACE:
case Common::KEYCODE_RETURN:
if (pressed && !event.kbdRepeat)
_scene.toggleCover();
break;
+ case Common::KEYCODE_SPACE:
+ if (pressed && !event.kbdRepeat)
+ shoot = true;
+ break;
default:
break;
}
@@ -395,32 +701,54 @@ private:
} else if (event.type == Common::EVENT_RBUTTONDOWN) {
_scene.toggleCover();
} else if (event.type == Common::EVENT_LBUTTONDOWN) {
- if (!_scene.outOfCover())
- _scene.toggleCover();
+ shoot = true;
} else if (event.type == Common::EVENT_QUIT ||
event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
_psx._vm->quitGame();
}
}
- // Aiming only moves while the rookie is actually out of cover.
- if (_scene.outOfCover()) {
- aimX += (right ? 3 : 0) - (left ? 3 : 0);
- aimY += (down ? 3 : 0) - (up ? 3 : 0);
+ const uint32 elapsed = g_system->getMillis() - startTime;
+ const int target = (int)((uint64)elapsed * kRA2PSXLevel2TickRate / 1000);
+ bool redraw = false;
+ while (tick < target && running) {
+ ++tick;
+ redraw = true;
+ if (_scene.outOfCover()) {
+ aimX += (right ? 2 : 0) - (left ? 2 : 0);
+ aimY += (down ? 2 : 0) - (up ? 2 : 0);
+ }
+ aimX = CLIP<int>(aimX, part.aimLeft, part.aimRight);
+ aimY = CLIP<int>(aimY, part.aimTop, part.aimBottom);
+ _scene.update(aimX, aimY);
+
+ const int damage = _scene.updateEnemies(_psx._vm->_rnd);
+ if (damage) {
+ _shield = MAX(0, _shield - damage);
+ flashFrame = 0;
+ }
+ if (shoot) {
+ _score += _scene.shoot(aimX, aimY);
+ shoot = false;
+ }
+ if (flashFrame < kRA2PSXHitFlashFrames)
+ ++flashFrame;
+ if (_shield <= 0 || _scene.cleared())
+ running = false;
+ }
+
+ if (redraw) {
+ Graphics::Surface output;
+ output.create(_psx._vm->_screenWidth, _psx._vm->_screenHeight,
+ g_system->getScreenFormat());
+ _scene.draw(output, aimX, aimY);
+ drawRA2PSXHitFlash(output, flashFrame);
+ g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0,
+ output.w, output.h);
+ output.free();
+ g_system->updateScreen();
}
- aimX = CLIP<int>(aimX, part.aimLeft, part.aimRight);
- aimY = CLIP<int>(aimY, part.aimTop, part.aimBottom);
- _scene.update(aimX, aimY);
-
- Graphics::Surface output;
- output.create(_psx._vm->_screenWidth, _psx._vm->_screenHeight,
- g_system->getScreenFormat());
- _scene.draw(output, aimX, aimY);
- g_system->copyRectToScreen(output.getPixels(), output.pitch, 0, 0,
- output.w, output.h);
- output.free();
- g_system->updateScreen();
- g_system->delayMillis(20);
+ g_system->delayMillis(5);
}
CursorMan.showMouse(cursorWasVisible);
@@ -431,6 +759,12 @@ private:
const RA2PSXArchive &_archive;
RA2PSXLevel2Scene _scene;
uint16 _phaseState;
+ int _shield;
+ int _score;
+ int _kills;
+ int _misses;
+ int _lives;
+ bool _skipped;
};
Common::Error Rebel2PSX::runLevel2() {
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index e29d8b55e80..b8ebb562bea 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -124,6 +124,16 @@ void RA2PSXMatrix::setTranslation(int x, int y, int z) {
translation[2] = (float)z;
}
+// A 16 bit GPU colour, unpacked to opaque bit, semi transparency bit and 8 bit channels.
+uint32 decodeRA2PSXColor(uint16 value) {
+ if (!value)
+ return 0;
+ const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
+ const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
+ const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
+ return 0x01000000 | ((value & 0x8000) ? 0x02000000 : 0) | (r << 16) | (g << 8) | b;
+}
+
bool loadRA2PSXTextures(const Common::Array<byte> &data,
Common::Array<RA2PSXTexture> &textures) {
const uint initialCount = textures.size();
@@ -157,21 +167,15 @@ bool loadRA2PSXTextures(const Common::Array<byte> &data,
texture.name = name;
texture.width = width;
texture.height = height;
+ texture.palette.resize(paletteColors);
+ for (uint32 i = 0; i < paletteColors; ++i)
+ texture.palette[i] = decodeRA2PSXColor(
+ READ_LE_UINT16(data.data() + paletteOffset + i * 2));
texture.pixels.resize(pixelCount);
for (uint32 i = 0; i < pixelCount; ++i) {
const byte packed = data[pixelsOffset + (eightBit ? i : i / 2)];
const byte paletteIndex = eightBit ? packed : ((i & 1) ? packed >> 4 : packed & 0xf);
- const uint16 value = READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2);
- if (!value) {
- texture.pixels[i] = 0;
- continue;
- }
-
- const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
- const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
- const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
- texture.pixels[i] = 0x01000000 | ((value & 0x8000) ? 0x02000000 : 0) |
- (r << 16) | (g << 8) | b;
+ texture.pixels[i] = texture.palette[paletteIndex];
}
textures.push_back(texture);
offset += recordSize;
@@ -218,6 +222,80 @@ bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeig
return true;
}
+// The playN scripts stream their frames through one byte oriented RLE: a zero ends the
+// stream, a count with the top bit set repeats the next byte, and any other count copies
+// that many literals.
+bool decodeRA2PSXPlayRun(const Common::Array<byte> &data, uint32 &offset,
+ uint32 expected, Common::Array<byte> &pixels) {
+ pixels.clear();
+ pixels.reserve(expected);
+ while (offset < data.size()) {
+ const byte count = data[offset++];
+ if (!count)
+ return pixels.size() == expected;
+ if (count & 0x80) {
+ if (offset >= data.size())
+ return false;
+ const byte value = data[offset++];
+ for (int i = (count & 0x7f) + 1; i > 0; --i)
+ pixels.push_back(value);
+ continue;
+ }
+ if (offset + count > data.size())
+ return false;
+ for (int i = 0; i < count; ++i)
+ pixels.push_back(data[offset++]);
+ }
+ return false;
+}
+
+bool loadRA2PSXPlayScript(const Common::Array<byte> &data,
+ Common::Array<RA2PSXPlayAnimation> &animations) {
+ animations.clear();
+ if (data.size() < 4)
+ return false;
+
+ // The file opens with a table of animation offsets that runs up to the first one.
+ const uint32 first = READ_LE_UINT32(data.data());
+ if (first < 4 || first > data.size() || (first & 3))
+ return false;
+
+ animations.resize(first / 4);
+ for (uint index = 0; index < animations.size(); ++index) {
+ uint32 offset = READ_LE_UINT32(data.data() + index * 4);
+ while (offset && offset + 16 <= data.size()) {
+ RA2PSXPlayFrame frame;
+ const uint16 recordSize = READ_LE_UINT16(data.data() + offset);
+ frame.x = READ_LE_INT16(data.data() + offset + 2);
+ frame.y = READ_LE_INT16(data.data() + offset + 4);
+ frame.width = data[offset + 6];
+ frame.height = data[offset + 7];
+ frame.boxLeft = frame.x + (int8)data[offset + 8];
+ frame.boxTop = frame.y + (int8)data[offset + 9];
+ frame.boxRight = frame.boxLeft + (int8)data[offset + 10];
+ frame.boxBottom = frame.boxTop + (int8)data[offset + 11];
+ frame.flags = READ_LE_UINT16(data.data() + offset + 12);
+
+ const uint32 pixelCount = (uint32)frame.width * frame.height;
+ uint32 cursor = offset + 16;
+ if (frame.flags & kRA2PSXPlayCompressed) {
+ if (!decodeRA2PSXPlayRun(data, cursor, pixelCount, frame.pixels))
+ return false;
+ } else {
+ if (cursor + pixelCount > data.size())
+ return false;
+ frame.pixels.resize(pixelCount);
+ memcpy(frame.pixels.data(), data.data() + cursor, pixelCount);
+ }
+ animations[index].push_back(frame);
+ if ((frame.flags & kRA2PSXPlayLastFrame) || !recordSize)
+ break;
+ offset += recordSize;
+ }
+ }
+ return true;
+}
+
RA2PSXModel::RA2PSXModel() : _radius(1.0f) {
}
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 48c6224f886..2cb07b77859 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -203,11 +203,47 @@ struct RA2PSXTexture {
uint16 width;
uint16 height;
Common::Array<uint32> pixels;
+ // The resolved CLUT, kept so sprites streamed into this texture's VRAM slot can
+ // borrow its colours the way the original's fixed page assignment does.
+ Common::Array<uint32> palette;
};
+uint32 decodeRA2PSXColor(uint16 value);
bool loadRA2PSXTextures(const Common::Array<byte> &data,
Common::Array<RA2PSXTexture> &textures);
+// One frame of a playN script: where it lands on screen, its 8 bit pixels, and the flags
+// that say whether it can be shot, whether it hurts the player, and when it makes noise.
+struct RA2PSXPlayFrame {
+ RA2PSXPlayFrame() : x(0), y(0), width(0), height(0), boxLeft(0), boxTop(0),
+ boxRight(0), boxBottom(0), flags(0) {}
+
+ int16 x;
+ int16 y;
+ int16 width;
+ int16 height;
+ int16 boxLeft;
+ int16 boxTop;
+ int16 boxRight;
+ int16 boxBottom;
+ uint16 flags;
+ Common::Array<byte> pixels;
+};
+
+enum {
+ kRA2PSXPlayLastFrame = 0x0001,
+ kRA2PSXPlayCompressed = 0x0002,
+ kRA2PSXPlayTargetable = 0x0004,
+ kRA2PSXPlayHitsPlayer = 0x0008,
+ kRA2PSXPlaySound = 0x0010
+};
+
+typedef Common::Array<RA2PSXPlayFrame> RA2PSXPlayAnimation;
+
+// playN: a table of animation offsets, each opening a chain of frame records.
+bool loadRA2PSXPlayScript(const Common::Array<byte> &data,
+ Common::Array<RA2PSXPlayAnimation> &animations);
+
// The fireball frames are 68x56, the size the original's VRAM upload uses.
enum { kRA2PSXExplosionHeight = 56 };
@@ -218,6 +254,7 @@ bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeig
// Level 2's three parts, each a sheet of backdrop halves, parallax walls and actors.
enum {
kRA2PSXLevel2PartCount = 3,
+ kRA2PSXLevel2LayerCount = 5,
kRA2PSXLevel2SceneTop = 28,
// Frames 0 to 4 are the rookie leaning out of cover; 5 to 29 are a five by five
// grid of aim poses picked from where the crosshair sits.
@@ -226,8 +263,54 @@ enum {
kRA2PSXLevel2RookieDelay = 3,
kRA2PSXLevel2AimColumns = 5,
kRA2PSXLevel2AimRows = 5,
- // The scroll eases to its target over this many ticks.
- kRA2PSXLevel2ScrollSteps = 20
+ // The slide runs at a constant speed of distance/20 for 14 ticks, then decays by
+ // 0xe000/0x10000 a tick until it lands - ra2 eases out rather than lerping.
+ kRA2PSXLevel2ScrollSteps = 20,
+ kRA2PSXLevel2ScrollHold = 14,
+ kRA2PSXLevel2ScrollDamping = 0xe000
+};
+
+enum {
+ kRA2PSXLevel2TrooperCount = 3,
+ // The gameplay clock is the 60Hz vblank, and a play script steps every fourth tick.
+ kRA2PSXLevel2TickRate = 60,
+ kRA2PSXLevel2FrameTicks = 4,
+ // A part ends a second after its last trooper falls.
+ kRA2PSXLevel2ClearTicks = 60,
+ // A trooper hides behind its own sprite until its slot has counted down.
+ kRA2PSXLevel2SlotNever = 0x7fffffff
+};
+
+// The states a play script actor walks, named after the values the original stores.
+enum {
+ kRA2PSXLevel2StateIdle = 0,
+ kRA2PSXLevel2StateAppear = 1,
+ kRA2PSXLevel2StateAimed = 2,
+ kRA2PSXLevel2StateWild = 3,
+ kRA2PSXLevel2StateDie = 4,
+ kRA2PSXLevel2StateShot = 5,
+ kRA2PSXLevel2StateCover = 0x80
+};
+
+// One play script actor: a trooper, or the bolt it fires.
+struct RA2PSXLevel2Actor {
+ RA2PSXLevel2Actor() : state(kRA2PSXLevel2StateIdle), animation(-1), frame(0), hold(0),
+ hit(false), slotTick(0), fireNext(0), fireEnd(0) {}
+
+ int state;
+ int animation;
+ int frame;
+ int hold;
+ bool hit;
+ int slotTick;
+ int fireNext;
+ int fireEnd;
+};
+
+// The frame window a trooper shoots inside, per part, per trooper, per animation.
+struct RA2PSXLevel2FireWindow {
+ int16 start;
+ int16 end;
};
// Where each rookie frame is drawn, straight out of the original's per level table.
@@ -238,21 +321,54 @@ struct RA2PSXLevel2Pose {
int16 height;
};
+// One backdrop sprite, as the original's display list holds it: a fixed screen position
+// the scroll is subtracted from, and the size the SPRT draws rather than the sheet's.
+struct RA2PSXLevel2Layer {
+ const char *name;
+ int16 x;
+ int16 y;
+ int16 width;
+ int16 height;
+ // 0 rides the backdrop scroll, 1 the nearer parallax one.
+ byte plane;
+};
+
struct RA2PSXLevel2PartInfo {
const char *sheet;
const char *anims;
- int backScroll;
- int paraScroll;
+ // Where each plane sits in cover and out in the open, as 16.16 pixels.
+ int16 coverScroll[2][2];
+ int16 openScroll[2][2];
+ // Which axis eases out; the other one holds its speed all the way.
+ byte scrollAxis;
+ RA2PSXLevel2Layer layers[kRA2PSXLevel2LayerCount];
int rookieOffsetX;
int rookieOffsetY;
- bool hasMiddleStrip;
// Crosshair box, then the thresholds that split it into the aim grid.
int16 aimLeft, aimTop, aimRight, aimBottom;
int16 aimColumns[kRA2PSXLevel2AimColumns];
int16 aimRows[kRA2PSXLevel2AimRows];
RA2PSXLevel2Pose poses[kRA2PSXLevel2FrameCount];
+ // How many trooper slots the part scripts, and the sheet sprites whose palettes the
+ // streamed trooper and bolt frames borrow.
+ int trooperCount;
+ const char *trooperPalettes[kRA2PSXLevel2TrooperCount];
+ const char *boltPalettes[kRA2PSXLevel2TrooperCount];
+ // Empty windows mean the part fires as it changes state instead of on a frame count.
+ RA2PSXLevel2FireWindow fireWindows[kRA2PSXLevel2TrooperCount][3];
};
+// Waves per part by difficulty, and the timings and odds that drive a trooper's slot.
+extern const int16 kRA2PSXLevel2WaveTable[kRA2PSXLevel2PartCount][3][2];
+// Per slot: how long until it reappears, the odds it aims rather than sprays, and how
+// long a killed trooper's spot stays empty.
+extern const int16 kRA2PSXLevel2SlotTable[kRA2PSXLevel2PartCount][3][5];
+// How many animation frames pass between two shots. Part 3 fires on state changes only.
+extern const int16 kRA2PSXLevel2FireTable[2][3][2];
+// The odds an enemy bolt connects, and what it takes off the shield when it does.
+extern const int16 kRA2PSXLevel2BoltTable[kRA2PSXLevel2PartCount][3][2];
+extern const int16 kRA2PSXLevel2KillScore[3];
+
extern const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount];
// How far the level 1 background is panned and tilted inside its oversized frame, and the
diff --git a/engines/scumm/insane/rebel2/psx/ui.cpp b/engines/scumm/insane/rebel2/psx/ui.cpp
index a3999ab76f9..d2a696856c7 100644
--- a/engines/scumm/insane/rebel2/psx/ui.cpp
+++ b/engines/scumm/insane/rebel2/psx/ui.cpp
@@ -76,6 +76,13 @@ bool RA2PSXTextureSet::appendRaw24(const char *name, const Common::Array<byte> &
return true;
}
+const Common::Array<uint32> *RA2PSXTextureSet::palette(const char *name) const {
+ if (!name)
+ return nullptr;
+ const RA2PSXTexture *texture = find(name);
+ return texture && !texture->palette.empty() ? &texture->palette : nullptr;
+}
+
const RA2PSXTexture *RA2PSXTextureSet::find(const char *name) const {
for (uint i = 0; i < _textures.size(); ++i) {
if (_textures[i].name.equalsIgnoreCase(name))
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 1d85fa398d2..3ae06f51375 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -13,6 +13,7 @@
#ifndef SCUMM_INSANE_REBEL2_PSX_UI_H
#define SCUMM_INSANE_REBEL2_PSX_UI_H
+#include "common/random.h"
#include "common/rect.h"
#include "graphics/surface.h"
@@ -35,6 +36,8 @@ public:
bool appendRaw24(const char *name, const Common::Array<byte> &data,
uint16 width, uint16 height);
bool has(const char *name) const { return find(name) != nullptr; }
+ // Sprites streamed into a texture's VRAM slot are drawn through its CLUT.
+ const Common::Array<uint32> *palette(const char *name) const;
void draw(Graphics::Surface &surface, const char *name, int x, int y,
const Common::Rect &source, int brightness = 0x80,
BlendMode blend = kBlendOpaque) const;
@@ -53,31 +56,51 @@ private:
};
// One part of level 2: the backdrop halves and parallax walls scroll behind the rookie,
-// who stands centred and ducks in and out of cover.
+// who stands centred and ducks in and out of cover while scripted troopers trade fire.
class RA2PSXLevel2Scene {
public:
RA2PSXLevel2Scene();
- bool load(const RA2PSXArchive &archive, int part);
+ bool load(const RA2PSXArchive &archive, int part, int difficulty,
+ Common::RandomSource &random);
// Toggles between cover and the open; ignored while a move is already running.
void toggleCover();
void update(int aimX, int aimY);
+ // One 60Hz tick of the trooper slots. Returns the shield damage it cost the player.
+ int updateEnemies(Common::RandomSource &random);
+ // Kills whatever the crosshair covers; returns the score it earned, or zero.
+ int shoot(int aimX, int aimY);
bool outOfCover() const { return _out && !_moving; }
+ // Anything but fully ducked; the original only shields a rookie who has finished.
+ bool exposed() const { return _out || _moving; }
bool busy() const { return _moving; }
+ bool cleared() const { return _clearTicks >= kRA2PSXLevel2ClearTicks; }
+ int kills() const { return _kills; }
+ int misses() const { return _misses; }
void draw(Graphics::Surface &surface, int aimX, int aimY) const;
const RA2PSXLevel2PartInfo &info() const;
private:
- void drawLayer(Graphics::Surface &surface, const char *name, int x, int y) const;
+ void drawLayer(Graphics::Surface &surface, const RA2PSXLevel2Layer &layer,
+ int left, int top) const;
void drawFrame(Graphics::Surface &surface, const RA2PSXTexture &frame,
int x, int y) const;
- void setScrollTarget(int backX, int backY, int paraX, int paraY);
+ void drawPlayFrame(Graphics::Surface &surface, const RA2PSXPlayFrame &frame,
+ const Common::Array<uint32> &palette, int left, int top) const;
+ void setScrollTarget(const int16 target[2][2]);
int aimFrame(int aimX, int aimY) const;
+ // Restarts an actor on the given animation, the way the original's loader does.
+ void startActor(RA2PSXLevel2Actor &actor, int state, int animation);
+ const RA2PSXPlayFrame *actorFrame(const RA2PSXLevel2Actor &actor) const;
+ void advanceActor(RA2PSXLevel2Actor &actor);
+ int slotDelay(int base, int range, Common::RandomSource &random) const;
RA2PSXTextureSet _textures;
RA2PSXTextureSet _hud;
Common::Array<RA2PSXTexture> _rookie;
+ Common::Array<RA2PSXPlayAnimation> _play;
int _part;
+ int _difficulty;
int _frame;
int _delay;
bool _out;
@@ -86,6 +109,16 @@ private:
int _scroll[2][2];
int _scrollTarget[2][2];
int _scrollStep[2][2];
+ int _scrollHold[2][2];
+ // The trooper slots, the bolt each one has in the air, and the wave they draw from.
+ RA2PSXLevel2Actor _troopers[kRA2PSXLevel2TrooperCount];
+ RA2PSXLevel2Actor _bolts[kRA2PSXLevel2TrooperCount];
+ int _tick;
+ int _remaining;
+ int _active;
+ int _clearTicks;
+ int _kills;
+ int _misses;
};
// The GPU's semi-transparency mode 2: background minus source.
Commit: 53a736900d2c8de823b59f91bf4b6c7f06d0e8bd
https://github.com/scummvm/scummvm/commit/53a736900d2c8de823b59f91bf4b6c7f06d0e8bd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: fix L2 aiming, occlusion and per-part rookie tables in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level2.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level2.cpp b/engines/scumm/insane/rebel2/psx/level2.cpp
index 23e2a7010ec..9515582b77e 100644
--- a/engines/scumm/insane/rebel2/psx/level2.cpp
+++ b/engines/scumm/insane/rebel2/psx/level2.cpp
@@ -48,7 +48,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ "PARA_R", 234, 28, 190, 190, 1 } },
0, 0x26,
0x2a, 0x32, 0xcf, 0x8d,
- { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
+ { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 }, 0,
{
{ 255, 65, 78, 116 }, { 250, 68, 76, 113 }, { 234, 68, 76, 113 },
{ 219, 68, 70, 113 }, { 200, 72, 80, 109 }, { 169, 73, 88, 108 },
@@ -74,18 +74,18 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ nullptr, 0, 0, 0, 0, 0 } },
-10, 0x12,
0x1c, 0x26, 0x11e, 0xa0,
- { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
+ { 59, 125, 190, 257, 320 }, { 60, 90, 120, 138, 240 }, 0,
{
- { 255, 65, 78, 116 }, { 250, 68, 76, 113 }, { 234, 68, 76, 113 },
- { 219, 68, 70, 113 }, { 200, 72, 80, 109 }, { 169, 73, 88, 108 },
- { 169, 73, 88, 108 }, { 167, 73, 90, 108 }, { 165, 73, 92, 108 },
- { 165, 73, 92, 108 }, { 179, 73, 78, 108 }, { 179, 73, 78, 108 },
- { 178, 73, 80, 108 }, { 174, 74, 84, 107 }, { 174, 74, 84, 107 },
- { 181, 73, 78, 109 }, { 181, 73, 78, 109 }, { 181, 73, 78, 108 },
- { 181, 73, 80, 108 }, { 181, 73, 80, 108 }, { 180, 73, 80, 108 },
- { 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
- { 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
- { 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
+ { 146, 150, 68, 50 }, { 140, 141, 74, 59 }, { 147, 112, 70, 88 },
+ { 156, 106, 70, 94 }, { 164, 106, 70, 94 }, { 139, 106, 90, 94 },
+ { 134, 113, 94, 87 }, { 132, 114, 96, 86 }, { 131, 115, 96, 85 },
+ { 124, 118, 102, 82 }, { 157, 103, 76, 97 }, { 155, 113, 76, 87 },
+ { 155, 114, 76, 86 }, { 154, 115, 76, 85 }, { 144, 117, 86, 83 },
+ { 160, 103, 72, 97 }, { 159, 109, 74, 91 }, { 161, 111, 72, 89 },
+ { 160, 112, 72, 88 }, { 161, 113, 72, 87 }, { 157, 106, 72, 94 },
+ { 157, 108, 74, 92 }, { 158, 110, 72, 90 }, { 157, 111, 72, 89 },
+ { 156, 112, 72, 88 }, { 159, 106, 72, 94 }, { 158, 112, 76, 88 },
+ { 160, 112, 78, 88 }, { 159, 113, 80, 87 }, { 160, 115, 80, 85 }
},
3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
{ { { 0x12, 0x2b }, { 0x0e, 0x24 }, { 0x0e, 0x19 } },
@@ -100,18 +100,19 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ nullptr, 0, 0, 0, 0, 0 } },
0, 0x23,
0x1a, 0x43, 0xc9, 0x92,
- { 63, 104, 145, 186, 320 }, { 73, 88, 98, 118, 240 },
+ { 48, 91, 134, 177, 320 }, { 77, 87, 106, 126, 240 }, 3,
{
- { 255, 65, 78, 116 }, { 250, 68, 76, 113 }, { 234, 68, 76, 113 },
- { 219, 68, 70, 113 }, { 200, 72, 80, 109 }, { 169, 73, 88, 108 },
- { 169, 73, 88, 108 }, { 167, 73, 90, 108 }, { 165, 73, 92, 108 },
- { 165, 73, 92, 108 }, { 179, 73, 78, 108 }, { 179, 73, 78, 108 },
- { 178, 73, 80, 108 }, { 174, 74, 84, 107 }, { 174, 74, 84, 107 },
- { 181, 73, 78, 109 }, { 181, 73, 78, 109 }, { 181, 73, 78, 108 },
- { 181, 73, 80, 108 }, { 181, 73, 80, 108 }, { 180, 73, 80, 108 },
- { 180, 73, 80, 108 }, { 181, 73, 82, 108 }, { 182, 73, 80, 108 },
- { 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
- { 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
+ { 147, 103, 90, 81 }, { 156, 100, 78, 84 }, { 150, 99, 64, 85 },
+ { 127, 98, 74, 86 }, { 121, 97, 74, 87 }, { 107, 101, 82, 83 },
+ { 74, 101, 96, 83 }, { 81, 92, 86, 92 }, { 91, 95, 74, 89 },
+ { 90, 99, 74, 85 }, { 92, 103, 74, 81 }, { 91, 103, 74, 81 },
+ { 89, 102, 74, 82 }, { 91, 94, 76, 90 }, { 90, 99, 74, 85 },
+ { 88, 103, 74, 81 }, { 89, 103, 74, 81 }, { 87, 102, 74, 82 },
+ { 90, 95, 72, 89 }, { 92, 99, 70, 85 }, { 91, 102, 72, 82 },
+ { 91, 102, 72, 82 }, { 90, 102, 72, 82 }, { 92, 95, 72, 89 },
+ { 92, 100, 76, 84 }, { 89, 103, 78, 81 }, { 91, 103, 78, 81 },
+ { 90, 102, 76, 82 }, { 93, 95, 90, 89 }, { 95, 101, 90, 83 },
+ { 95, 103, 90, 81 }, { 95, 103, 92, 81 }, { 93, 103, 90, 81 }
},
3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
{ { { 0, 0 }, { 0, 0 }, { 0, 0 } },
@@ -179,7 +180,8 @@ bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part, int difficu
// Every rookie pose is its own member: a format word, a palette and one image.
_rookie.clear();
- for (int frame = 0; frame < kRA2PSXLevel2FrameCount; ++frame) {
+ const int poseCount = kRA2PSXLevel2Parts[part].aimBase + 30;
+ for (int frame = 0; frame < poseCount; ++frame) {
const Common::String path = Common::String::format("%s/anim%02d",
kRA2PSXLevel2Parts[part].anims, frame);
Common::Array<byte> anim;
@@ -212,8 +214,9 @@ bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part, int difficu
const RA2PSXLevel2PartInfo &part_ = kRA2PSXLevel2Parts[part];
for (int plane = 0; plane < 2; ++plane) {
for (int axis = 0; axis < 2; ++axis) {
+ // Multiply rather than shift: parts two and three scroll to negatives.
_scroll[plane][axis] = _scrollTarget[plane][axis] =
- part_.coverScroll[plane][axis] << 16;
+ part_.coverScroll[plane][axis] * 0x10000;
_scrollStep[plane][axis] = _scrollHold[plane][axis] = 0;
}
}
@@ -356,6 +359,9 @@ int RA2PSXLevel2Scene::shoot(int aimX, int aimY) {
if (!outOfCover())
return 0;
+ // The crosshair is in screen space; a frame's box is where the backdrop puts it.
+ const int shotX = aimX + (_scroll[0][0] >> 16);
+ const int shotY = aimY - kRA2PSXLevel2SceneTop + (_scroll[0][1] >> 16);
for (int i = 0; i < info().trooperCount; ++i) {
RA2PSXLevel2Actor &trooper = _troopers[i];
if (trooper.state == kRA2PSXLevel2StateIdle ||
@@ -365,8 +371,8 @@ int RA2PSXLevel2Scene::shoot(int aimX, int aimY) {
const RA2PSXPlayFrame *frame = actorFrame(trooper);
if (!frame || !(frame->flags & kRA2PSXPlayTargetable))
continue;
- if (aimX < frame->boxLeft || aimX > frame->boxRight ||
- aimY < frame->boxTop || aimY > frame->boxBottom)
+ if (shotX < frame->boxLeft || shotX > frame->boxRight ||
+ shotY < frame->boxTop || shotY > frame->boxBottom)
continue;
startActor(trooper, kRA2PSXLevel2StateDie, i * 8 + 3);
@@ -381,7 +387,7 @@ void RA2PSXLevel2Scene::setScrollTarget(const int16 target[2][2]) {
const RA2PSXLevel2PartInfo &part = info();
for (int plane = 0; plane < 2; ++plane) {
for (int axis = 0; axis < 2; ++axis) {
- _scrollTarget[plane][axis] = target[plane][axis] << 16;
+ _scrollTarget[plane][axis] = target[plane][axis] * 0x10000;
_scrollStep[plane][axis] =
(_scrollTarget[plane][axis] - _scroll[plane][axis]) / kRA2PSXLevel2ScrollSteps;
// Only the axis the part actually slides along eases out.
@@ -406,8 +412,12 @@ int RA2PSXLevel2Scene::aimFrame(int aimX, int aimY) const {
int row = 0;
while (row < kRA2PSXLevel2AimRows && part.aimRows[row] < aimY)
++row;
- return CLIP<int>(column * kRA2PSXLevel2AimColumns + row,
- (int)kRA2PSXLevel2CoverFrames, (int)kRA2PSXLevel2FrameCount - 1);
+ return CLIP<int>(column * kRA2PSXLevel2AimColumns + row + part.aimBase,
+ coverFrames(), part.aimBase + 29);
+}
+
+int RA2PSXLevel2Scene::coverFrames() const {
+ return info().aimBase + kRA2PSXLevel2CoverFrames;
}
void RA2PSXLevel2Scene::update(int aimX, int aimY) {
@@ -434,7 +444,7 @@ void RA2PSXLevel2Scene::update(int aimX, int aimY) {
_delay = kRA2PSXLevel2RookieDelay;
// Leaning out walks 0 to 4, ducking back walks it down again.
if (!_out) {
- if (_frame < kRA2PSXLevel2CoverFrames - 1) {
+ if (_frame < coverFrames() - 1) {
++_frame;
} else {
_moving = false;
@@ -442,8 +452,8 @@ void RA2PSXLevel2Scene::update(int aimX, int aimY) {
_frame = aimFrame(aimX, aimY);
}
} else {
- if (_frame >= kRA2PSXLevel2CoverFrames)
- _frame = kRA2PSXLevel2CoverFrames - 1;
+ if (_frame >= coverFrames())
+ _frame = coverFrames() - 1;
if (_frame > 0) {
--_frame;
} else {
@@ -514,16 +524,13 @@ void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) con
const int top = (surface.h - 240) / 2;
surface.fillRect(Common::Rect(surface.w, surface.h), 0);
- for (int i = 0; i < kRA2PSXLevel2LayerCount; ++i)
- drawLayer(surface, part.layers[i], left, top);
-
- if ((uint)_frame < _rookie.size()) {
- const RA2PSXLevel2Pose &pose = part.poses[_frame];
- drawFrame(surface, _rookie[_frame], left + pose.x + part.rookieOffsetX,
- top + pose.y + part.rookieOffsetY);
+ for (int i = 0; i < kRA2PSXLevel2LayerCount; ++i) {
+ if (!part.layers[i].plane)
+ drawLayer(surface, part.layers[i], left, top);
}
- // Troopers ride the backdrop, so ducking slides them behind the near wall.
+ // Troopers ride the backdrop, so ducking both slides them along and puts the near
+ // wall in front of them.
const int actorX = left - (_scroll[0][0] >> 16);
const int actorY = top + kRA2PSXLevel2SceneTop - (_scroll[0][1] >> 16);
for (int i = 0; i < part.trooperCount; ++i) {
@@ -544,6 +551,18 @@ void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) con
}
}
+ for (int i = 0; i < kRA2PSXLevel2LayerCount; ++i) {
+ if (part.layers[i].plane)
+ drawLayer(surface, part.layers[i], left, top);
+ }
+
+ // The rookie stands nearest of all, in front of his own cover.
+ if ((uint)_frame < _rookie.size()) {
+ const RA2PSXLevel2Pose &pose = part.poses[_frame];
+ drawFrame(surface, _rookie[_frame], left + pose.x + part.rookieOffsetX,
+ top + pose.y + part.rookieOffsetY);
+ }
+
if (outOfCover() && _hud.has("CROSS"))
_hud.draw(surface, "CROSS", left + aimX - 4, top + aimY - 3,
Common::Rect(0, 0, 8, 7));
@@ -648,6 +667,11 @@ private:
int aimY = (part.aimTop + part.aimBottom) / 2;
bool left = false, right = false, up = false, down = false;
bool running = true;
+ // The fire press has to outlive the poll that saw it: ticks run at 60Hz while
+ // events are drained several times as often, so a per pass flag loses presses.
+ bool firePressed = false;
+ bool fireEdge = false;
+ bool fireWasPressed = false;
int flashFrame = kRA2PSXHitFlashFrames;
int tick = -1;
const uint32 startTime = g_system->getMillis();
@@ -655,7 +679,6 @@ private:
const int viewY = ((int)_psx._vm->_screenHeight - 240) / 2;
while (running && !shouldQuit()) {
- bool shoot = false;
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
const bool pressed = event.type == Common::EVENT_KEYDOWN;
@@ -684,24 +707,43 @@ private:
case Common::KEYCODE_s:
down = pressed;
break;
+ case Common::KEYCODE_SPACE:
case Common::KEYCODE_RETURN:
- if (pressed && !event.kbdRepeat)
- _scene.toggleCover();
+ case Common::KEYCODE_KP_ENTER:
+ if (pressed && !firePressed && !event.kbdRepeat)
+ fireEdge = true;
+ firePressed = pressed;
break;
- case Common::KEYCODE_SPACE:
+ case Common::KEYCODE_TAB:
+ case Common::KEYCODE_LCTRL:
+ case Common::KEYCODE_RCTRL:
if (pressed && !event.kbdRepeat)
- shoot = true;
+ _scene.toggleCover();
break;
default:
break;
}
+ } else 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 (event.customType == kScummActionInsaneAttack) {
+ if (pressed && !firePressed)
+ fireEdge = true;
+ firePressed = pressed;
+ } else if (pressed && event.customType == kScummActionInsaneSwitch) {
+ _scene.toggleCover();
+ }
} else if (event.type == Common::EVENT_MOUSEMOVE) {
aimX = event.mouse.x - viewX;
aimY = event.mouse.y - viewY;
} else if (event.type == Common::EVENT_RBUTTONDOWN) {
_scene.toggleCover();
} else if (event.type == Common::EVENT_LBUTTONDOWN) {
- shoot = true;
+ if (!firePressed)
+ fireEdge = true;
+ firePressed = true;
+ } else if (event.type == Common::EVENT_LBUTTONUP) {
+ firePressed = false;
} else if (event.type == Common::EVENT_QUIT ||
event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
_psx._vm->quitGame();
@@ -727,10 +769,13 @@ private:
_shield = MAX(0, _shield - damage);
flashFrame = 0;
}
- if (shoot) {
+ // Holding fire keeps shooting, the way the original polls its pad.
+ const bool shoot = fireEdge || (firePressed && fireWasPressed &&
+ !(tick % kRA2PSXLevel2FireRepeat));
+ fireWasPressed = firePressed;
+ fireEdge = false;
+ if (shoot)
_score += _scene.shoot(aimX, aimY);
- shoot = false;
- }
if (flashFrame < kRA2PSXHitFlashFrames)
++flashFrame;
if (_shield <= 0 || _scene.cleared())
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index b8ebb562bea..fe5aac18b15 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -159,9 +159,14 @@ bool loadRA2PSXTextures(const Common::Array<byte> &data,
const uint32 pixelBytes = eightBit ? pixelCount : (pixelCount + 1) / 2;
const uint32 paletteOffset = offset + 20;
const uint32 pixelsOffset = paletteOffset + paletteColors * 2;
- if (recordSize < 20 || offset + recordSize > data.size() ||
- pixelsOffset + pixelBytes > offset + recordSize)
+ if (recordSize < 20 || offset + recordSize > data.size())
break;
+ // Some sheets open with a short header record that carries no image; skip it
+ // rather than giving up on the rest of the sheet.
+ if (pixelsOffset + pixelBytes > offset + recordSize) {
+ offset += recordSize;
+ continue;
+ }
RA2PSXTexture texture;
texture.name = name;
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 2cb07b77859..cd0ee202f95 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -259,7 +259,8 @@ enum {
// Frames 0 to 4 are the rookie leaning out of cover; 5 to 29 are a five by five
// grid of aim poses picked from where the crosshair sits.
kRA2PSXLevel2CoverFrames = 5,
- kRA2PSXLevel2FrameCount = 30,
+ // Part three's rookie owns three more poses, and shifts its aim grid past them.
+ kRA2PSXLevel2FrameCount = 33,
kRA2PSXLevel2RookieDelay = 3,
kRA2PSXLevel2AimColumns = 5,
kRA2PSXLevel2AimRows = 5,
@@ -278,7 +279,9 @@ enum {
// A part ends a second after its last trooper falls.
kRA2PSXLevel2ClearTicks = 60,
// A trooper hides behind its own sprite until its slot has counted down.
- kRA2PSXLevel2SlotNever = 0x7fffffff
+ kRA2PSXLevel2SlotNever = 0x7fffffff,
+ // How often a held fire button repeats, in ticks.
+ kRA2PSXLevel2FireRepeat = 12
};
// The states a play script actor walks, named after the values the original stores.
@@ -348,6 +351,8 @@ struct RA2PSXLevel2PartInfo {
int16 aimLeft, aimTop, aimRight, aimBottom;
int16 aimColumns[kRA2PSXLevel2AimColumns];
int16 aimRows[kRA2PSXLevel2AimRows];
+ // Where the aim grid starts; the cover run and the pose count shift with it.
+ int16 aimBase;
RA2PSXLevel2Pose poses[kRA2PSXLevel2FrameCount];
// How many trooper slots the part scripts, and the sheet sprites whose palettes the
// streamed trooper and bolt frames borrow.
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 3ae06f51375..77b54391359 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -89,6 +89,8 @@ private:
const Common::Array<uint32> &palette, int left, int top) const;
void setScrollTarget(const int16 target[2][2]);
int aimFrame(int aimX, int aimY) const;
+ // How many poses the rookie walks leaning out; the aim grid starts right after.
+ int coverFrames() const;
// Restarts an actor on the given animation, the way the original's loader does.
void startActor(RA2PSXLevel2Actor &actor, int state, int animation);
const RA2PSXPlayFrame *actorFrame(const RA2PSXLevel2Actor &actor) const;
Commit: 2a8dc23c1a92386dc5490a7820e91209013b8836
https://github.com/scummvm/scummvm/commit/2a8dc23c1a92386dc5490a7820e91209013b8836
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: fix L2 shot origin, cover occlusion and cutscene order in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level2.cpp
engines/scumm/insane/rebel2/psx/psx.h
engines/scumm/insane/rebel2/psx/ui.h
diff --git a/engines/scumm/insane/rebel2/psx/level2.cpp b/engines/scumm/insane/rebel2/psx/level2.cpp
index 9515582b77e..701dc52bf16 100644
--- a/engines/scumm/insane/rebel2/psx/level2.cpp
+++ b/engines/scumm/insane/rebel2/psx/level2.cpp
@@ -61,7 +61,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
{ 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
},
- 2, { "TRP_0", "TRP_1", nullptr }, { "LAS_0", "LAS_1", nullptr },
+ 2, { "TRP_0", "TRP_1", nullptr }, { "LAS_0", "LAS_1", nullptr }, 1, 0x3e,
{ { { 7, 0x1f }, { 7, 0x1a }, { 8, 0x0b } },
{ { 7, 0x36 }, { 7, 0x28 }, { 8, 0x1a } },
{ { 0, 0 }, { 0, 0 }, { 0, 0 } } } },
@@ -87,7 +87,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 156, 112, 72, 88 }, { 159, 106, 72, 94 }, { 158, 112, 76, 88 },
{ 160, 112, 78, 88 }, { 159, 113, 80, 87 }, { 160, 115, 80, 85 }
},
- 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
+ 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" }, 0, 0x44,
{ { { 0x12, 0x2b }, { 0x0e, 0x24 }, { 0x0e, 0x19 } },
{ { 0x17, 0x3a }, { 0x15, 0x28 }, { 0x15, 0x1e } },
{ { 0x0d, 0x3b }, { 0x0d, 0x29 }, { 0x0f, 0x1c } } } },
@@ -114,7 +114,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 90, 102, 76, 82 }, { 93, 95, 90, 89 }, { 95, 101, 90, 83 },
{ 95, 103, 90, 81 }, { 95, 103, 92, 81 }, { 93, 103, 90, 81 }
},
- 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
+ 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" }, 0, 0x44,
{ { { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, 0 }, { 0, 0 }, { 0, 0 } } } }
@@ -147,6 +147,26 @@ const int16 kRA2PSXLevel2BoltTable[kRA2PSXLevel2PartCount][3][2] = {
const int16 kRA2PSXLevel2KillScore[3] = { 80, 100, 150 };
+// Where the gun points for each aim pose. The five cover poses never fire.
+const int16 kRA2PSXLevel2Muzzles[2][30][2] = {
+ {
+ { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 },
+ { 49, 86 }, { 49, 86 }, { 62, 100 }, { 65, 132 }, { 65, 132 },
+ { 72, 80 }, { 80, 90 }, { 63, 100 }, { 65, 120 }, { 58, 125 },
+ { 93, 81 }, { 93, 90 }, { 101, 99 }, { 89, 109 }, { 89, 129 },
+ { 108, 82 }, { 108, 90 }, { 111, 100 }, { 116, 115 }, { 116, 130 },
+ { 119, 83 }, { 119, 90 }, { 124, 100 }, { 127, 112 }, { 127, 130 }
+ },
+ {
+ { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 },
+ { 63, 71 }, { 63, 80 }, { 67, 90 }, { 76, 123 }, { 76, 123 },
+ { 78, 67 }, { 78, 70 }, { 81, 95 }, { 76, 122 }, { 76, 122 },
+ { 87, 64 }, { 87, 72 }, { 96, 88 }, { 100, 102 }, { 100, 116 },
+ { 101, 61 }, { 104, 73 }, { 105, 84 }, { 111, 99 }, { 111, 102 },
+ { 109, 60 }, { 114, 76 }, { 122, 82 }, { 122, 95 }, { 122, 100 }
+ }
+};
+
RA2PSXLevel2Scene::RA2PSXLevel2Scene() : _part(0), _difficulty(0), _frame(0), _delay(0),
_out(false), _moving(false), _tick(0), _remaining(0), _active(0), _clearTicks(0),
_kills(0), _misses(0) {
@@ -350,19 +370,57 @@ int RA2PSXLevel2Scene::updateEnemies(Common::RandomSource &random) {
trooper.slotTick = slotDelay(slot[0], slot[1], random);
}
+ for (int i = 0; i < kRA2PSXLevel2ShotCount; ++i) {
+ if (!_shots[i].step)
+ continue;
+ _shots[i].step += kRA2PSXLevel2ShotStep;
+ if (_shots[i].step > kRA2PSXLevel2ShotEnd)
+ _shots[i].step = 0;
+ }
+
if (_remaining <= 0)
++_clearTicks;
return damage;
}
+void RA2PSXLevel2Scene::projectShot(const RA2PSXLevel2Shot &shot, int step,
+ int &x, int &y) const {
+ step = CLIP<int>(step, 0, 4096);
+ // The bolt is a straight 3D line from the gun at z 1000 to the crosshair at z 18000.
+ // Projecting that line back through its own endpoints cancels the focal length, so
+ // the screen position is a perspective weighted blend of the two screen points.
+ const int nearZ = kRA2PSXLevel2ShotNearZ;
+ const int farZ = kRA2PSXLevel2ShotFarZ;
+ const int z = nearZ + (farZ - nearZ) * step / 4096;
+ const int deltaX = shot.targetX * farZ - shot.muzzleX * nearZ;
+ const int deltaY = shot.targetY * farZ - shot.muzzleY * nearZ;
+ x = (int)((shot.muzzleX * nearZ + (int)(((int64)deltaX * step) >> 12)) / z);
+ y = (int)((shot.muzzleY * nearZ + (int)(((int64)deltaY * step) >> 12)) / z);
+}
+
int RA2PSXLevel2Scene::shoot(int aimX, int aimY) {
if (!outOfCover())
return 0;
+ const RA2PSXLevel2PartInfo &part = info();
+ // The bolt leaves the gun this pose points, exactly as the DOS build's per sprite
+ // origin table does; the table is biased against the projection's own centre.
+ const int16 *muzzle = kRA2PSXLevel2Muzzles[part.muzzleTable][MIN(_frame, 29)];
+ for (int i = 0; i < kRA2PSXLevel2ShotCount; ++i) {
+ if (_shots[i].step)
+ continue;
+ _shots[i].step = kRA2PSXLevel2ShotStep;
+ _shots[i].muzzleX = muzzle[0] - 4;
+ _shots[i].muzzleY = muzzle[1] + 116 - part.muzzleBias;
+ _shots[i].targetX = aimX;
+ _shots[i].targetY = aimY;
+ break;
+ }
+
// The crosshair is in screen space; a frame's box is where the backdrop puts it.
const int shotX = aimX + (_scroll[0][0] >> 16);
const int shotY = aimY - kRA2PSXLevel2SceneTop + (_scroll[0][1] >> 16);
- for (int i = 0; i < info().trooperCount; ++i) {
+ for (int i = 0; i < part.trooperCount; ++i) {
RA2PSXLevel2Actor &trooper = _troopers[i];
if (trooper.state == kRA2PSXLevel2StateIdle ||
trooper.state == kRA2PSXLevel2StateCover ||
@@ -543,12 +601,6 @@ void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) con
if (frame)
drawPlayFrame(surface, *frame, *palette, actorX, actorY);
}
- const Common::Array<uint32> *bolt = _textures.palette(part.boltPalettes[i]);
- if (bolt && _bolts[i].state != kRA2PSXLevel2StateIdle) {
- const RA2PSXPlayFrame *frame = actorFrame(_bolts[i]);
- if (frame)
- drawPlayFrame(surface, *frame, *bolt, actorX, actorY);
- }
}
for (int i = 0; i < kRA2PSXLevel2LayerCount; ++i) {
@@ -556,6 +608,20 @@ void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) con
drawLayer(surface, part.layers[i], left, top);
}
+ // A bolt flies past the near wall on its way to the camera, so it sits in front of
+ // it - but only while the rookie is out; cover stops the shot, as the DOS handler
+ // does by refusing to draw a beam at all while he is ducked.
+ if (exposed()) {
+ for (int i = 0; i < part.trooperCount; ++i) {
+ const Common::Array<uint32> *bolt = _textures.palette(part.boltPalettes[i]);
+ if (!bolt || _bolts[i].state == kRA2PSXLevel2StateIdle)
+ continue;
+ const RA2PSXPlayFrame *frame = actorFrame(_bolts[i]);
+ if (frame)
+ drawPlayFrame(surface, *frame, *bolt, actorX, actorY);
+ }
+ }
+
// The rookie stands nearest of all, in front of his own cover.
if ((uint)_frame < _rookie.size()) {
const RA2PSXLevel2Pose &pose = part.poses[_frame];
@@ -563,6 +629,21 @@ void RA2PSXLevel2Scene::draw(Graphics::Surface &surface, int aimX, int aimY) con
top + pose.y + part.rookieOffsetY);
}
+ // The player's own bolts are nearest of all, and stop with him behind cover.
+ static const byte kShotHead[3] = { 0xff, 0xf0, 0xc0 };
+ static const byte kShotTail[3] = { 0xc0, 0x30, 0x00 };
+ for (int i = 0; exposed() && i < kRA2PSXLevel2ShotCount; ++i) {
+ if (!_shots[i].step || _shots[i].step >= kRA2PSXLevel2ShotDraw)
+ continue;
+ int headX, headY, tailX, tailY;
+ projectShot(_shots[i], _shots[i].step, headX, headY);
+ projectShot(_shots[i], _shots[i].step - kRA2PSXLevel2ShotStep, tailX, tailY);
+ for (int thickness = 0; thickness < 2; ++thickness) {
+ drawRA2PSXGouraudLine(surface, left + tailX, top + tailY + thickness,
+ left + headX, top + headY + thickness, kShotTail, kShotHead);
+ }
+ }
+
if (outOfCover() && _hud.has("CROSS"))
_hud.draw(surface, "CROSS", left + aimX - 4, top + aimY - 3,
Common::Rect(0, 0, 8, 7));
@@ -580,8 +661,10 @@ public:
bool shouldQuit() const override { return _psx._vm->shouldQuit(); }
+ // The briefing, then the long cinematic that drops the rookie into the corridor.
bool playOpening() override {
- return _psx.playVideo("S1/L02_INTR.STR", 1, false);
+ return _psx.playVideo("S1/L02_INTR.STR", 1, false) &&
+ _psx.playVideo("S1/L02_CUT1.STR", 1, false);
}
void beginAttempt() override {
@@ -625,9 +708,10 @@ public:
return credit;
}
+ // The two short links between the parts; CUT1 belongs to the opening.
bool playPhaseEnd(int phase) override {
static const char *const cutscenes[] = {
- "S1/L02_CUT1.STR", "S1/L02_CUT2.STR"
+ "S1/L02_CUT2.STR", "S1/L02_CUT3.STR"
};
return _psx.playVideo(cutscenes[phase - 1], 1, false);
}
@@ -651,7 +735,6 @@ public:
}
void playComplete(int) override {
- _psx.playVideo("S1/L02_CUT3.STR", 1, false);
_psx.playVideo("S1/L02_EXTR.STR", 1, false);
}
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index cd0ee202f95..6c02160ff9c 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -281,7 +281,17 @@ enum {
// A trooper hides behind its own sprite until its slot has counted down.
kRA2PSXLevel2SlotNever = 0x7fffffff,
// How often a held fire button repeats, in ticks.
- kRA2PSXLevel2FireRepeat = 12
+ kRA2PSXLevel2FireRepeat = 12,
+ // A player bolt runs a 3D line from the gun to the crosshair: the parameter steps
+ // by 300 of 4096 a tick, the bolt stops drawing at 4000 and is dropped past 0x112f.
+ // Two slots, the same as the DOS build's cover handler.
+ kRA2PSXLevel2ShotCount = 2,
+ kRA2PSXLevel2ShotStep = 300,
+ kRA2PSXLevel2ShotDraw = 4000,
+ kRA2PSXLevel2ShotEnd = 0x112f,
+ // The bolt leaves the muzzle at z 1000 and reaches the crosshair at z 18000.
+ kRA2PSXLevel2ShotNearZ = 1000,
+ kRA2PSXLevel2ShotFarZ = 18000
};
// The states a play script actor walks, named after the values the original stores.
@@ -295,6 +305,22 @@ enum {
kRA2PSXLevel2StateCover = 0x80
};
+// One of the player's own bolts, travelling from the gun toward the crosshair.
+struct RA2PSXLevel2Shot {
+ RA2PSXLevel2Shot() : step(0), muzzleX(0), muzzleY(0), targetX(0), targetY(0) {}
+
+ // Zero when the slot is free; otherwise how far along the line the bolt is.
+ int step;
+ // Both ends in screen space: the gun this pose fires from, and the crosshair.
+ int muzzleX;
+ int muzzleY;
+ int targetX;
+ int targetY;
+};
+
+// Where the gun sits for each rookie pose, as the original's two tables hold it.
+extern const int16 kRA2PSXLevel2Muzzles[2][30][2];
+
// One play script actor: a trooper, or the bolt it fires.
struct RA2PSXLevel2Actor {
RA2PSXLevel2Actor() : state(kRA2PSXLevel2StateIdle), animation(-1), frame(0), hold(0),
@@ -359,6 +385,9 @@ struct RA2PSXLevel2PartInfo {
int trooperCount;
const char *trooperPalettes[kRA2PSXLevel2TrooperCount];
const char *boltPalettes[kRA2PSXLevel2TrooperCount];
+ // Which muzzle table the part's poses index, and the bias its bolts start from.
+ byte muzzleTable;
+ int16 muzzleBias;
// Empty windows mean the part fires as it changes state instead of on a frame count.
RA2PSXLevel2FireWindow fireWindows[kRA2PSXLevel2TrooperCount][3];
};
diff --git a/engines/scumm/insane/rebel2/psx/ui.h b/engines/scumm/insane/rebel2/psx/ui.h
index 77b54391359..3800a03e5f1 100644
--- a/engines/scumm/insane/rebel2/psx/ui.h
+++ b/engines/scumm/insane/rebel2/psx/ui.h
@@ -68,7 +68,7 @@ public:
void update(int aimX, int aimY);
// One 60Hz tick of the trooper slots. Returns the shield damage it cost the player.
int updateEnemies(Common::RandomSource &random);
- // Kills whatever the crosshair covers; returns the score it earned, or zero.
+ // Fires a bolt at the crosshair and kills whatever it covers; returns the score.
int shoot(int aimX, int aimY);
bool outOfCover() const { return _out && !_moving; }
// Anything but fully ducked; the original only shields a rookie who has finished.
@@ -96,6 +96,8 @@ private:
const RA2PSXPlayFrame *actorFrame(const RA2PSXLevel2Actor &actor) const;
void advanceActor(RA2PSXLevel2Actor &actor);
int slotDelay(int base, int range, Common::RandomSource &random) const;
+ // Projects a bolt's 3D line onto the screen at the given point along it.
+ void projectShot(const RA2PSXLevel2Shot &shot, int step, int &x, int &y) const;
RA2PSXTextureSet _textures;
RA2PSXTextureSet _hud;
@@ -115,6 +117,7 @@ private:
// The trooper slots, the bolt each one has in the air, and the wave they draw from.
RA2PSXLevel2Actor _troopers[kRA2PSXLevel2TrooperCount];
RA2PSXLevel2Actor _bolts[kRA2PSXLevel2TrooperCount];
+ RA2PSXLevel2Shot _shots[kRA2PSXLevel2ShotCount];
int _tick;
int _remaining;
int _active;
Commit: 79b6a1e895bbd53201fdc575f572400ae63ff633
https://github.com/scummvm/scummvm/commit/79b6a1e895bbd53201fdc575f572400ae63ff633
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:13:28+02:00
Commit Message:
SCUMM: RA2: draw the L2 rookie with its slot CLUT and fire from the pistol in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level2.cpp
engines/scumm/insane/rebel2/psx/model.cpp
engines/scumm/insane/rebel2/psx/psx.h
diff --git a/engines/scumm/insane/rebel2/psx/level2.cpp b/engines/scumm/insane/rebel2/psx/level2.cpp
index 701dc52bf16..f1c00a61336 100644
--- a/engines/scumm/insane/rebel2/psx/level2.cpp
+++ b/engines/scumm/insane/rebel2/psx/level2.cpp
@@ -61,7 +61,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 182, 73, 80, 108 }, { 181, 73, 78, 109 }, { 181, 73, 78, 109 },
{ 182, 73, 78, 108 }, { 183, 73, 80, 108 }, { 183, 73, 80, 108 }
},
- 2, { "TRP_0", "TRP_1", nullptr }, { "LAS_0", "LAS_1", nullptr }, 1, 0x3e,
+ 2, { "TRP_0", "TRP_1", nullptr }, { "LAS_0", "LAS_1", nullptr },
{ { { 7, 0x1f }, { 7, 0x1a }, { 8, 0x0b } },
{ { 7, 0x36 }, { 7, 0x28 }, { 8, 0x1a } },
{ { 0, 0 }, { 0, 0 }, { 0, 0 } } } },
@@ -87,7 +87,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 156, 112, 72, 88 }, { 159, 106, 72, 94 }, { 158, 112, 76, 88 },
{ 160, 112, 78, 88 }, { 159, 113, 80, 87 }, { 160, 115, 80, 85 }
},
- 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" }, 0, 0x44,
+ 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
{ { { 0x12, 0x2b }, { 0x0e, 0x24 }, { 0x0e, 0x19 } },
{ { 0x17, 0x3a }, { 0x15, 0x28 }, { 0x15, 0x1e } },
{ { 0x0d, 0x3b }, { 0x0d, 0x29 }, { 0x0f, 0x1c } } } },
@@ -114,7 +114,7 @@ const RA2PSXLevel2PartInfo kRA2PSXLevel2Parts[kRA2PSXLevel2PartCount] = {
{ 90, 102, 76, 82 }, { 93, 95, 90, 89 }, { 95, 101, 90, 83 },
{ 95, 103, 90, 81 }, { 95, 103, 92, 81 }, { 93, 103, 90, 81 }
},
- 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" }, 0, 0x44,
+ 3, { "TRP_0", "TRP_1", "TRP_2" }, { "LAS_0", "LAS_1", "LAS_2" },
{ { { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, 0 }, { 0, 0 }, { 0, 0 } } } }
@@ -147,26 +147,6 @@ const int16 kRA2PSXLevel2BoltTable[kRA2PSXLevel2PartCount][3][2] = {
const int16 kRA2PSXLevel2KillScore[3] = { 80, 100, 150 };
-// Where the gun points for each aim pose. The five cover poses never fire.
-const int16 kRA2PSXLevel2Muzzles[2][30][2] = {
- {
- { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 },
- { 49, 86 }, { 49, 86 }, { 62, 100 }, { 65, 132 }, { 65, 132 },
- { 72, 80 }, { 80, 90 }, { 63, 100 }, { 65, 120 }, { 58, 125 },
- { 93, 81 }, { 93, 90 }, { 101, 99 }, { 89, 109 }, { 89, 129 },
- { 108, 82 }, { 108, 90 }, { 111, 100 }, { 116, 115 }, { 116, 130 },
- { 119, 83 }, { 119, 90 }, { 124, 100 }, { 127, 112 }, { 127, 130 }
- },
- {
- { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 },
- { 63, 71 }, { 63, 80 }, { 67, 90 }, { 76, 123 }, { 76, 123 },
- { 78, 67 }, { 78, 70 }, { 81, 95 }, { 76, 122 }, { 76, 122 },
- { 87, 64 }, { 87, 72 }, { 96, 88 }, { 100, 102 }, { 100, 116 },
- { 101, 61 }, { 104, 73 }, { 105, 84 }, { 111, 99 }, { 111, 102 },
- { 109, 60 }, { 114, 76 }, { 122, 82 }, { 122, 95 }, { 122, 100 }
- }
-};
-
RA2PSXLevel2Scene::RA2PSXLevel2Scene() : _part(0), _difficulty(0), _frame(0), _delay(0),
_out(false), _moving(false), _tick(0), _remaining(0), _active(0), _clearTicks(0),
_kills(0), _misses(0) {
@@ -198,7 +178,10 @@ bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part, int difficu
return false;
}
- // Every rookie pose is its own member: a format word, a palette and one image.
+ // Every rookie pose is its own member: a format word, a palette and one image. The
+ // poses stream into ROOKIE_A's VRAM slot, so they take that texture's CLUT - their
+ // own often leaves the backdrop index opaque.
+ const Common::Array<uint32> *rookieClut = _textures.palette("ROOKIE_A");
_rookie.clear();
const int poseCount = kRA2PSXLevel2Parts[part].aimBase + 30;
for (int frame = 0; frame < poseCount; ++frame) {
@@ -209,7 +192,8 @@ bool RA2PSXLevel2Scene::load(const RA2PSXArchive &archive, int part, int difficu
if (!archive.getMember(path, anim) || anim.size() < 8)
return false;
const uint16 height = READ_LE_UINT16(anim.data() + 2) & 0xff;
- if (!loadRA2PSXSpriteAnimation(anim, height ? height : 256, decoded) || decoded.empty())
+ if (!loadRA2PSXSpriteAnimation(anim, height ? height : 256, decoded, rookieClut) ||
+ decoded.empty())
return false;
_rookie.push_back(decoded[0]);
}
@@ -403,15 +387,15 @@ int RA2PSXLevel2Scene::shoot(int aimX, int aimY) {
return 0;
const RA2PSXLevel2PartInfo &part = info();
- // The bolt leaves the gun this pose points, exactly as the DOS build's per sprite
- // origin table does; the table is biased against the projection's own centre.
- const int16 *muzzle = kRA2PSXLevel2Muzzles[part.muzzleTable][MIN(_frame, 29)];
+ // The bolt leaves the pistol the rookie is holding, which the pose box locates: he
+ // aims across the frame, so the gun sits at the leading edge of his sprite.
+ const RA2PSXLevel2Pose &pose = part.poses[CLIP<int>(_frame, 0, kRA2PSXLevel2FrameCount - 1)];
for (int i = 0; i < kRA2PSXLevel2ShotCount; ++i) {
if (_shots[i].step)
continue;
_shots[i].step = kRA2PSXLevel2ShotStep;
- _shots[i].muzzleX = muzzle[0] - 4;
- _shots[i].muzzleY = muzzle[1] + 116 - part.muzzleBias;
+ _shots[i].muzzleX = pose.x + part.rookieOffsetX + pose.width / 8;
+ _shots[i].muzzleY = pose.y + part.rookieOffsetY + pose.height / 6;
_shots[i].targetX = aimX;
_shots[i].targetY = aimY;
break;
diff --git a/engines/scumm/insane/rebel2/psx/model.cpp b/engines/scumm/insane/rebel2/psx/model.cpp
index fe5aac18b15..bde1ba86ecc 100644
--- a/engines/scumm/insane/rebel2/psx/model.cpp
+++ b/engines/scumm/insane/rebel2/psx/model.cpp
@@ -189,7 +189,7 @@ bool loadRA2PSXTextures(const Common::Array<byte> &data,
}
bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeight,
- Common::Array<RA2PSXTexture> &frames) {
+ Common::Array<RA2PSXTexture> &frames, const Common::Array<uint32> *clut) {
if (data.size() < 4 + 512 || !frameHeight)
return false;
const uint16 widthField = READ_LE_UINT16(data.data());
@@ -212,15 +212,14 @@ bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeig
texture.pixels.resize(frameBytes);
for (uint32 i = 0; i < frameBytes; ++i) {
const byte paletteIndex = data[pixelsOffset + frame * frameBytes + i];
- const uint16 value = READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2);
- if (!value) {
- texture.pixels[i] = 0;
+ // Frames streamed into another texture's VRAM slot are drawn through that
+ // slot's CLUT, not the one the member happens to carry.
+ if (clut) {
+ texture.pixels[i] = paletteIndex < clut->size() ? (*clut)[paletteIndex] : 0;
continue;
}
- const uint32 r = ((value & 0x1f) << 3) | ((value & 0x1f) >> 2);
- const uint32 g = (((value >> 5) & 0x1f) << 3) | (((value >> 5) & 0x1f) >> 2);
- const uint32 b = (((value >> 10) & 0x1f) << 3) | (((value >> 10) & 0x1f) >> 2);
- texture.pixels[i] = 0x01000000 | (r << 16) | (g << 8) | b;
+ texture.pixels[i] = decodeRA2PSXColor(
+ READ_LE_UINT16(data.data() + paletteOffset + paletteIndex * 2));
}
frames.push_back(texture);
}
diff --git a/engines/scumm/insane/rebel2/psx/psx.h b/engines/scumm/insane/rebel2/psx/psx.h
index 6c02160ff9c..99b292697ba 100644
--- a/engines/scumm/insane/rebel2/psx/psx.h
+++ b/engines/scumm/insane/rebel2/psx/psx.h
@@ -249,7 +249,7 @@ enum { kRA2PSXExplosionHeight = 56 };
// bigEx: a 4 byte format word, a 256 colour palette and equal sized 8 bit frames.
bool loadRA2PSXSpriteAnimation(const Common::Array<byte> &data, uint16 frameHeight,
- Common::Array<RA2PSXTexture> &frames);
+ Common::Array<RA2PSXTexture> &frames, const Common::Array<uint32> *clut = nullptr);
// Level 2's three parts, each a sheet of backdrop halves, parallax walls and actors.
enum {
@@ -318,9 +318,6 @@ struct RA2PSXLevel2Shot {
int targetY;
};
-// Where the gun sits for each rookie pose, as the original's two tables hold it.
-extern const int16 kRA2PSXLevel2Muzzles[2][30][2];
-
// One play script actor: a trooper, or the bolt it fires.
struct RA2PSXLevel2Actor {
RA2PSXLevel2Actor() : state(kRA2PSXLevel2StateIdle), animation(-1), frame(0), hold(0),
@@ -385,9 +382,6 @@ struct RA2PSXLevel2PartInfo {
int trooperCount;
const char *trooperPalettes[kRA2PSXLevel2TrooperCount];
const char *boltPalettes[kRA2PSXLevel2TrooperCount];
- // Which muzzle table the part's poses index, and the bias its bolts start from.
- byte muzzleTable;
- int16 muzzleBias;
// Empty windows mean the part fires as it changes state instead of on a frame count.
RA2PSXLevel2FireWindow fireWindows[kRA2PSXLevel2TrooperCount][3];
};
Commit: 64d051dc08be08b230344d4d55de734304c9d5d4
https://github.com/scummvm/scummvm/commit/64d051dc08be08b230344d4d55de734304c9d5d4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-26T13:16:23+02:00
Commit Message:
SCUMM: RA2: fixed variable shadow warning in psx release
Changed paths:
engines/scumm/insane/rebel2/psx/level2.cpp
diff --git a/engines/scumm/insane/rebel2/psx/level2.cpp b/engines/scumm/insane/rebel2/psx/level2.cpp
index f1c00a61336..afb8738b5f3 100644
--- a/engines/scumm/insane/rebel2/psx/level2.cpp
+++ b/engines/scumm/insane/rebel2/psx/level2.cpp
@@ -792,12 +792,12 @@ private:
}
} else 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;
+ const bool started = event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START;
if (event.customType == kScummActionInsaneAttack) {
- if (pressed && !firePressed)
+ if (started && !firePressed)
fireEdge = true;
- firePressed = pressed;
- } else if (pressed && event.customType == kScummActionInsaneSwitch) {
+ firePressed = started;
+ } else if (started && event.customType == kScummActionInsaneSwitch) {
_scene.toggleCover();
}
} else if (event.type == Common::EVENT_MOUSEMOVE) {
More information about the Scummvm-git-logs
mailing list