[Scummvm-git-logs] scummvm master -> 0cca97f0eae66bc1399580194045d3901af920b8
bluegr
noreply at scummvm.org
Tue Mar 17 20:51:13 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
0cca97f0ea NANCY: Implement arcadepuzzle - Barnacle Blast (Arkanoid) in Nancy 8
Commit: 0cca97f0eae66bc1399580194045d3901af920b8
https://github.com/scummvm/scummvm/commit/0cca97f0eae66bc1399580194045d3901af920b8
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-17T22:50:12+02:00
Commit Message:
NANCY: Implement arcadepuzzle - Barnacle Blast (Arkanoid) in Nancy 8
Changed paths:
engines/nancy/action/puzzle/arcadepuzzle.cpp
engines/nancy/action/puzzle/arcadepuzzle.h
diff --git a/engines/nancy/action/puzzle/arcadepuzzle.cpp b/engines/nancy/action/puzzle/arcadepuzzle.cpp
index 1322d949242..86d342de7d0 100644
--- a/engines/nancy/action/puzzle/arcadepuzzle.cpp
+++ b/engines/nancy/action/puzzle/arcadepuzzle.cpp
@@ -29,36 +29,1382 @@
#include "engines/nancy/state/scene.h"
#include "engines/nancy/action/puzzle/arcadepuzzle.h"
+#include "common/system.h"
+#include "common/random.h"
+
namespace Nancy {
namespace Action {
+// Wall normals for collision types 0..16.
+// Each entry is {dx, dy, spin} matching original offsets at this+0x188 + type*0xc.
+// Type 0 = paddle center (handled specially by FUN_0044d03d â normal not used here).
+// Positive dy = up in "math" coords, i.e. y -= dy*speed on screen.
+const float ArcadePuzzle::_wallNormals[17][3] = {
+ { 0.0f, 1.0f, 0.0f }, // 0 paddle center (placeholder â handled separately)
+ { -1.0f, 0.0f, 0.0f }, // 1 paddle left edge
+ { 1.0f, 0.0f, 0.0f }, // 2 paddle right edge
+ { 1.0f, 0.0f, 0.0f }, // 3 left wall
+ { 0.0f, -1.0f, 0.0f }, // 4 top wall
+ { -1.0f, 0.0f, 0.0f }, // 5 right wall
+ { 0.0f, 1.0f, 0.0f }, // 6 bottom wall (wallBounceMode)
+ { -1.0f, 0.0f, 0.0f }, // 7 brick left face
+ { 0.0f, 1.0f, 0.0f }, // 8 brick top face
+ { 1.0f, 0.0f, 0.0f }, // 9 brick right face
+ { 0.0f, -1.0f, 0.0f }, // 10 brick bottom face
+ { 0.7071068f, -0.7071068f, 0.0f }, // 0xb top-left corner
+ { -0.7071068f, -0.7071068f, 0.0f }, // 0xc top-right corner
+ { 0.7071068f, 0.7071068f, 0.0f }, // 0xd bottom-left corner
+ { -0.7071068f, 0.7071068f, 0.0f }, // 0xe bottom-right corner
+ { 0.0f, 1.0f, 0.0f }, // 0xf stuck/bounce-back
+ { 0.0f, 0.0f, 0.0f }, // 0x10 out-of-bounds catch-all (negate)
+};
+
+ArcadePuzzle::~ArcadePuzzle() {
+ _explosionList.clear();
+}
+
+// ---- readData ---------------------------------------------------------------
+
+void ArcadePuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName); // 33 bytes, +0x00
+
+ _numLevelsToWin = stream.readUint32LE(); // 4 bytes, +0x21
+
+ // Per-level grid dimensions: [cols0,rows0, cols1,rows1, ...] (48 bytes, 0x25)
+ for (int i = 0; i < 6; ++i) {
+ _levelCols[i] = stream.readUint32LE();
+ _levelRows[i] = stream.readUint32LE();
+ }
+
+ // Per-level offsets within field (48 bytes, +0x55)
+ for (int i = 0; i < 6; ++i) {
+ _levelXOff[i] = stream.readSint32LE();
+ _levelYOff[i] = stream.readSint32LE();
+ }
+
+ readRect(stream, _ballSrc); // 16 bytes, +0x85
+ readRect(stream, _paddleSrc); // 16 bytes, +0x95
+ for (int i = 0; i < 8; ++i) // 128 bytes, +0xa5
+ readRect(stream, _brickTypeSrc[i]);
+
+ // Score digit sprites (10 Ã 16 = 160 bytes, +0x125)
+ for (int i = 0; i < 10; ++i)
+ readRect(stream, _scoreDigitSrc[i]);
+
+ // Timer digit sprites (10 Ã 16 = 160 bytes, +0x1c5)
+ for (int i = 0; i < 10; ++i)
+ readRect(stream, _timerDigitSrc[i]);
+
+ readRect(stream, _timerDisplayDest); // 16 bytes, +0x265
+ readRect(stream, _lifeSrc[0]); // 16 bytes, +0x275
+ readRect(stream, _lifeSrc[1]); // 16 bytes, +0x285
+ readRect(stream, _lifeSrc[2]); // 16 bytes, +0x295
+
+ _stateDelayMs = stream.readUint32LE(); // 4 bytes, +0x2a5
+
+ stream.skip(4 * 4); // viewport rect, 4x4 bytes - we get it from NancySceneState
+
+ _fieldOffX = stream.readSint32LE(); // +0x2b9
+ _fieldOffY = stream.readSint32LE(); // +0x2bd
+
+ _scoreDisplayX = stream.readSint32LE(); // +0x2c1
+ _scoreDisplayY = stream.readSint32LE(); // +0x2c5
+ _timerDisplayX = stream.readSint32LE(); // +0x2c9
+ _timerDisplayY = stream.readSint32LE(); // +0x2cd
+
+ stream.skip(4); // +0x2d1 (unused render field)
+ stream.skip(4); // +0x2d5 (unused render field)
+ _deathYDist = stream.readSint32LE(); // +0x2d9
+
+ stream.skip(4); // +0x2dd left VK key code (unused)
+ stream.skip(4); // +0x2e1 right VK key code (unused)
+ stream.skip(4); // +0x2e5 launch VK key code (unused)
+
+ _paddleSteps = stream.readUint32LE(); // +0x2e9 (integer)
+ _ballSteps = stream.readUint32LE(); // +0x2ed (integer)
+ _paddlePixPerStep = stream.readFloatLE(); // +0x2f1 (IEEE 754 float)
+ _ballPixPerStep = stream.readFloatLE(); // +0x2f5 (IEEE 754 float)
+ _angleTableStart = stream.readSint32LE(); // +0x2f9
+ _angleTableEnd = stream.readSint32LE(); // +0x2fd
+
+ _randomBallStart = stream.readByte() != 0; // +0x301
+ _wallBounceMode = stream.readByte() != 0; // +0x302
+ _cumulativeScore = stream.readByte() != 0; // +0x303
+ stream.skip(1); // +0x304 (random seed flag, unused)
+
+ _scoreStepSize = stream.readSint32LE(); // +0x305
+ _timeBonusMax = stream.readSint32LE(); // +0x309
+ _timeLimitSec = stream.readSint32LE(); // +0x30d
+ _scoreParam4 = stream.readSint32LE(); // +0x311
+
+ for (int i = 0; i < 6; ++i) // 6Ã49 = 294 bytes, +0x315..+0x43a
+ _sounds[i].readNormal(stream);
+
+ _levelClearSound.readNormal(stream); // 49 bytes, +0x43b..+0x46b
+ _winScene.readData(stream); // 25 bytes, +0x46c..+0x484
+ stream.skip(1); // 1-byte gap, +0x485
+
+ _gameOverSound.readNormal(stream); // 49 bytes, +0x486..+0x4b6
+ _lifeLostSound.readNormal(stream); // 49 bytes, +0x4b7..+0x4e7
+}
+
+// ---- init -------------------------------------------------------------------
+
void ArcadePuzzle::init() {
- // TODO
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ _drawSurface.create(vpBounds.width(), vpBounds.height(),
+ g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ // Determine game field bounds within viewport
+ int vpDataWidth = (vpBounds.right - vpBounds.left);
+ int vpDataHeight = (vpBounds.bottom - vpBounds.top);
+
+ // Field may be offset within the viewport
+ _fieldLeft = _fieldOffX;
+ _fieldTop = _fieldOffY;
+ _fieldRight = _fieldOffX + vpDataWidth - 1;
+ _fieldBottom = _fieldOffY + vpDataHeight - 1;
+ _fieldWidth = vpDataWidth;
+ _fieldHeight = vpDataHeight;
+
+ // Death line: ball dies when it passes this y coord
+ _deathY = _fieldBottom - _deathYDist;
+
+ // Paddle dimensions from paddle src rect
+ _paddleWidth = _paddleSrc.width();
+ _paddleHeight = _paddleSrc.height();
+ _paddleHalfW = _paddleWidth / 2;
+
+ // Ball dimensions from ball src rect
+ _ballWidth = _ballSrc.width();
+ _ballHeight = _ballSrc.height();
+ _ballHalfW = _ballWidth / 2;
+ _ballHalfH = _ballHeight / 2;
+
+ // Brick dimensions from first brick type src
+ _brickWidth = _brickTypeSrc[0].width();
+ _brickHeight = _brickTypeSrc[0].height();
+
+ // Derived speeds (pixels per ms) â pixPerStep is float from data, steps is integer
+ _paddleSpeedPerMs = (_paddleSteps > 0)
+ ? _paddlePixPerStep / (float)_paddleSteps : 1.0f;
+ _ballSpeedPerMs = (_ballSteps > 0)
+ ? _ballPixPerStep / (float)_ballSteps : 1.0f;
+
+ // Build angle table
+ buildAngleTable();
+
+ // Score/timer display positions adjusted for field offset
+ // (stored coords are relative to field, so add _fieldLeft/_fieldTop)
+ // (stored as-is; drawing uses them with +field offset)
+
+ // Initialize sublevel (finds first unbeaten level, sets up bricks, paddle, ball)
+ initSublevel();
+}
+
+// ---- buildAngleTable --------------------------------------------------------
+// Populates _angleTable with (dx,dy,spin) pairs.
+// Each paddle position has 2 sub-entries: direct (random_bit=0) and mirrored (random_bit=1).
+// Layout: _angleTable[pos*6 + sub*3 + {0,1,2}] = {dx, dy, spin}.
+
+void ArcadePuzzle::buildAngleTable() {
+ _angleTable.resize(_paddleWidth * 6, 0.0f);
+
+ if (_paddleWidth == 0)
+ return;
+
+ int halfW = _paddleWidth / 2;
+ float angleStep = (_paddleWidth > 1)
+ ? (float)(_angleTableEnd - _angleTableStart) / (float)halfW : 0.0f;
+ float curAngleDeg = (float)_angleTableStart;
+
+ // Mirror normal: (0, 1) â reflects across y-axis
+ float mirrorNx = 0.0f, mirrorNy = 1.0f;
+
+ int left = 0;
+ int right = _paddleWidth - 1;
+
+ while (left <= halfW) {
+ // Rotate unit vector (1,0) by curAngleDeg
+ float angleRad = curAngleDeg * (float)M_PI / 180.0f;
+ float dx = (float)cos(angleRad);
+ float dy = (float)sin(angleRad);
+ // Normalize (already unit length from cos/sin, but in case of precision)
+ float len = (float)sqrt(dx * dx + dy * dy);
+ if (len > 0.0f) { dx /= len; dy /= len; }
+
+ // sub-entry 0: direct
+ _angleTable[left * 6 + 0] = dx;
+ _angleTable[left * 6 + 1] = dy;
+ _angleTable[left * 6 + 2] = 0.0f; // spin always 0
+
+ // Copy to right position (symmetric)
+ _angleTable[right * 6 + 0] = dx;
+ _angleTable[right * 6 + 1] = dy;
+ _angleTable[right * 6 + 2] = 0.0f;
+
+ // sub-entry 1: reflect across y-axis: (-dx, dy)
+ // FUN_00447eb9 with no-negate: result = 2*(v·n)*n - v
+ // n=(0,1), v=(dx,dy): result = (-dx, dy)
+ float dot = dx * mirrorNx + dy * mirrorNy; // = dy
+ float rdx = 2.0f * dot * mirrorNx - dx; // = -dx
+ float rdy = 2.0f * dot * mirrorNy - dy; // = dy
+ // Normalize
+ float rlen = (float)sqrt(rdx * rdx + rdy * rdy);
+ if (rlen > 0.0f) { rdx /= rlen; rdy /= rlen; }
+
+ _angleTable[left * 6 + 3] = rdx;
+ _angleTable[left * 6 + 4] = rdy;
+ _angleTable[left * 6 + 5] = 0.0f;
+
+ _angleTable[right * 6 + 3] = rdx;
+ _angleTable[right * 6 + 4] = rdy;
+ _angleTable[right * 6 + 5] = 0.0f;
+
+ curAngleDeg += angleStep;
+
+ if (left + 1 == right || right <= left)
+ break;
+ ++left;
+ --right;
+ }
+}
+
+// ---- resetRound ------------------------------------------------------------
+// Clears bricks, resets flags and timers. Called at start of each sublevel.
+
+void ArcadePuzzle::resetRound() {
+ // Free bricks array
+ _bricks.clear();
+
+ // Free explosion list
+ _explosionList.clear();
+
+ // Reset state flags
+ _levelClear = false;
+ _lifeLost = false;
+ _lifeLostBall = false;
+ _gameHalted = false;
+ _score = 0;
+ _launchBall = false;
+
+ // Reset timers to now
+ _paddleLastMs = _ballLastMs = g_system->getMillis();
+}
+
+// ---- getNextLevel ----------------------------------------------------------
+// Returns index of first unbeaten level, or -1 if enough levels are won.
+
+int ArcadePuzzle::getNextLevel() const {
+ int won = 0;
+ for (int i = 0; i < 6; ++i)
+ if (_winFlags[i]) ++won;
+
+ if (won < (int)_numLevelsToWin) {
+ for (int i = 0; i < 6; ++i)
+ if (!_winFlags[i])
+ return i;
+ }
+ return -1;
}
+// ---- generateBricks --------------------------------------------------------
+// Randomly assigns brick types, retrying until >=50% are non-empty.
+
+void ArcadePuzzle::generateBricks() {
+ bool ok = false;
+ while (!ok) {
+ int filled = 0;
+ for (int i = 0; i < _totalBricks; ++i) {
+ int r = g_nancy->_randomSource->getRandomNumber(4); // 0..4
+ if (r == 4) {
+ _bricks[i].type = -1; // empty
+ } else {
+ _bricks[i].type = r; // type 0-3
+ ++filled;
+ }
+ }
+ if (filled * 2 >= _totalBricks) // >= 50%
+ ok = true;
+ }
+}
+
+// ---- initSublevel ----------------------------------------------------------
+
+void ArcadePuzzle::initSublevel() {
+ _currentLevel = getNextLevel();
+ if (_currentLevel < 0)
+ return; // shouldn't happen
+
+ resetRound();
+
+ // Grid dimensions for this level
+ _brickCols = (int)_levelCols[_currentLevel];
+ _brickRows = (int)_levelRows[_currentLevel];
+ _totalBricks = _brickCols * _brickRows;
+
+ // Brick area position (viewport-relative)
+ _brickAreaLeft = _fieldLeft + _levelXOff[_currentLevel];
+ _brickAreaTop = _fieldTop + _levelYOff[_currentLevel];
+ _brickAreaRight = _brickAreaLeft + _brickCols * _brickWidth - 1;
+ _brickAreaBottom = _brickAreaTop + _brickRows * _brickHeight - 1;
+
+ // Paddle initial position: centered in field, bottom at deathY
+ _paddleLeft = (_fieldLeft + _fieldWidth / 2) - _paddleHalfW;
+ _paddleRight = _paddleLeft + _paddleWidth - 1;
+ _paddleBottom = _deathY;
+ _paddleTop = _paddleBottom - _paddleHeight + 1;
+ _paddlePrevLeft = _paddleLeft; _paddlePrevTop = _paddleTop;
+ _paddlePrevRight = _paddleRight; _paddlePrevBottom= _paddleBottom;
+ _paddleX = (float)_paddleLeft;
+ _paddleSrcCur= _paddleSrc;
+
+ // Ball initial position: centered above paddle
+ if (!_randomBallStart) {
+ _ballInitOffset = _paddleHalfW - _ballHalfW;
+ } else {
+ _ballInitOffset = g_nancy->_randomSource->getRandomNumber(
+ MAX(1, _paddleWidth - _ballWidth));
+ }
+ _ballLeft = _paddleLeft + _ballInitOffset;
+ _ballRight = _ballLeft + _ballWidth - 1;
+ _ballBottom = _paddleTop;
+ _ballTop = _paddleTop - _ballHeight + 1;
+ _ballCenterX = _ballLeft + _ballHalfW;
+ _ballCenterY = _ballTop + _ballHalfH;
+ _ballX = (float)_ballLeft;
+ _ballY = (float)_ballTop;
+ _ballPrevLeft = _ballLeft; _ballPrevTop = _ballTop;
+ _ballPrevRight= _ballRight;_ballPrevBottom=_ballBottom;
+ _ballState = kOnPaddle;
+ _ballNeedsRedraw = false;
+ _collisionType = -2;
+
+ // Default ball direction (straight up)
+ _ballDX = 0.0f;
+ _ballDY = 1.0f;
+ _ballSpin = 0.0f;
+
+ // Allocate bricks
+ _bricks.resize(_totalBricks);
+ generateBricks();
+
+ // Set up brick rects
+ int py = _brickAreaTop;
+ for (int row = 0; row < _brickRows; ++row) {
+ int px = _brickAreaLeft;
+ for (int col = 0; col < _brickCols; ++col) {
+ int idx = row * _brickCols + col;
+ Brick &b = _bricks[idx];
+
+ // Source rect in image (from brick type sprite)
+ if (b.type >= 0 && b.type < 8) {
+ b.srcRect = _brickTypeSrc[b.type];
+ } else {
+ b.srcRect = Common::Rect();
+ }
+
+ // Viewport-relative rect for this brick cell
+ b.vpRect = Common::Rect(px, py, px + _brickWidth, py + _brickHeight);
+
+ b.alive = (b.type >= 0);
+
+ b.neighborLeft = -1;
+ b.neighborUp = -1;
+ b.neighborRight = -1;
+ b.neighborDown = -1;
+
+ b.explosionTimer = 0;
+
+ px += _brickWidth;
+ }
+ py += _brickHeight;
+ }
+
+ // Link neighbors (only live bricks)
+ for (int row = 0; row < _brickRows; ++row) {
+ for (int col = 0; col < _brickCols; ++col) {
+ int idx = row * _brickCols + col;
+ if (!_bricks[idx].alive) continue;
+
+ if (row > 0 && _bricks[idx - _brickCols].alive)
+ _bricks[idx].neighborUp = idx - _brickCols;
+ if (row < _brickRows - 1 && _bricks[idx + _brickCols].alive)
+ _bricks[idx].neighborDown = idx + _brickCols;
+ if (col > 0 && _bricks[idx - 1].alive)
+ _bricks[idx].neighborLeft = idx - 1;
+ if (col < _brickCols - 1 && _bricks[idx + 1].alive)
+ _bricks[idx].neighborRight = idx + 1;
+ }
+ }
+
+ // Reset timer and score for this level
+ _timerStartMs = g_system->getMillis();
+ _timerElapsedMs = 0;
+ _timerMins = 0; _timerSecs = 0;
+ _levelScore[_currentLevel] = 0;
+ _prevScore = -1;
+
+ if (!_cumulativeScore)
+ _score = 0;
+ else {
+ // Cumulative: sum of all level scores
+ _score = _totalLevelScore;
+ }
+
+ // Draw initial state
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ for (int i = 0; i < _totalBricks; ++i)
+ if (_bricks[i].alive)
+ drawBrick(i);
+
+ drawPaddle();
+ drawBall();
+ drawScore();
+ drawTimer();
+
+ _needsRedraw = true;
+}
+
+// ---- execute ----------------------------------------------------------------
+
void ArcadePuzzle::execute() {
- if (_state == kBegin) {
+ switch (_state) {
+ case kBegin:
init();
registerGraphics();
+
+ // Load sounds
+ for (int i = 0; i < 6; ++i)
+ g_nancy->_sound->loadSound(_sounds[i]);
+ // Dynamic sounds are loaded when needed
+
+ _livesLeft = 3;
_state = kRun;
+ break;
+
+ case kRun: {
+ switch (_gameSubState) {
+ case kPlaying:
+ paddleMovement();
+ ballAndCollision();
+ processExplosions();
+ updateTimer();
+ updateScore();
+
+ if (_levelClear) {
+ // Mark current level as won
+ _winFlags[_currentLevel] = 1;
+
+ // Recalculate total score
+ _totalLevelScore = 0;
+ for (int i = 0; i < 6; ++i)
+ _totalLevelScore += _levelScore[i];
+
+ if (getNextLevel() == -1) {
+ // All required levels beaten
+ _winGame = true;
+ _levelClear = false;
+ _gameSubState = kGameOverWin;
+ } else {
+ _gameSubState = kLevelClear;
+ }
+ }
+
+ if (_lifeLost) {
+ // Reset current level flags and score
+ _winFlags[_currentLevel] = 0;
+ _levelScore[_currentLevel] = 0;
+ _gameSubState = kLifeLost;
+ }
+ break;
+
+ case kLevelClear:
+ if (_levelClearSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_levelClearSound);
+ g_nancy->_sound->playSound(_levelClearSound);
+ }
+ _stateWaitUntil = g_system->getMillis() + _stateDelayMs;
+ _gameSubState = kWaitTimer;
+ break;
+
+ case kLifeLost:
+ --_livesLeft;
+ if (_livesLeft <= 0) {
+ // No lives left: game over lose
+ _lifeLost = false;
+ _winGame = false;
+ if (_gameOverSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_gameOverSound);
+ g_nancy->_sound->playSound(_gameOverSound);
+ }
+ _stateWaitUntil = g_system->getMillis() + _stateDelayMs;
+ _gameSubState = kGameOverLose;
+ break;
+ }
+ if (_lifeLostSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_lifeLostSound);
+ g_nancy->_sound->playSound(_lifeLostSound);
+ }
+ _stateWaitUntil = g_system->getMillis() + _stateDelayMs;
+ _gameSubState = kWaitTimer;
+ break;
+
+ case kResetBoard:
+ initSublevel();
+ drawScore();
+ drawTimer();
+ _gameSubState = kPlaying;
+ break;
+
+ case kGameOverWin:
+ if (_gameOverSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_gameOverSound);
+ g_nancy->_sound->playSound(_gameOverSound);
+ }
+ _stateWaitUntil = g_system->getMillis() + _stateDelayMs;
+ _gameSubState = kWaitTimer;
+ break;
+
+ case kFinish:
+ _state = kActionTrigger;
+ break;
+
+ case kWaitTimer:
+ if (g_system->getMillis() > _stateWaitUntil) {
+ if (_levelClear || _lifeLost) {
+ _gameSubState = kResetBoard;
+ } else if (_winGame) {
+ _gameSubState = kFinish;
+ }
+ // else nothing (shouldn't happen)
+ }
+ break;
+
+ case kGameOverLose:
+ if (g_system->getMillis() > _stateWaitUntil)
+ _gameSubState = kFinish;
+ break;
+
+ default:
+ break;
+ }
+ break;
}
- // TODO
- // Stub - return to the winning screen
- warning("STUB - Nancy 8 Barnacle Blast game");
- SceneChangeDescription scene;
- scene.sceneID = 4445;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
-}
+ case kActionTrigger:
+ // Stop all sounds
+ for (int i = 0; i < 6; ++i)
+ g_nancy->_sound->stopSound(_sounds[i]);
+ g_nancy->_sound->stopSound(_levelClearSound);
+ g_nancy->_sound->stopSound(_gameOverSound);
+ g_nancy->_sound->stopSound(_lifeLostSound);
-void ArcadePuzzle::readData(Common::SeekableReadStream &stream) {
- // TODO
- stream.skip(stream.size() - stream.pos());
+ if (_livesLeft > 0) {
+ _winScene.execute();
+ } else {
+ // Change to the same destination scene as winning, but without setting
+ // the win flag. The game script checks that flag to determine win/lose
+ // and will show the "you lost" message when it is absent.
+ NancySceneState.changeScene(_winScene._sceneChange);
+ }
+
+ finishExecution();
+ break;
+ }
}
+// ---- handleInput ------------------------------------------------------------
+
void ArcadePuzzle::handleInput(NancyInput &input) {
- // TODO
+ if (_state != kRun)
+ return;
+
+ // Update movement flags (these are "held" flags)
+ _moveLeft = (input.input & NancyInput::kMoveLeft) != 0;
+ _moveRight = (input.input & NancyInput::kMoveRight) != 0;
+
+ // Launch ball: check for space or up-arrow
+ if (_ballState == kOnPaddle) {
+ for (const Common::KeyState &ks : input.otherKbdInput) {
+ if (ks.keycode == Common::KEYCODE_SPACE ||
+ ks.keycode == Common::KEYCODE_RETURN ||
+ ks.keycode == Common::KEYCODE_UP) {
+ _launchBall = true;
+ break;
+ }
+ }
+ if (input.input & NancyInput::kMoveUp)
+ _launchBall = true;
+ }
+}
+
+// ---- paddleMovement ---------------------------------------------------------
+
+void ArcadePuzzle::paddleMovement() {
+ // Paddle stops when game is halted or ball is dying
+ if (_gameHalted || _lifeLostBall)
+ return;
+
+ uint32 now = g_system->getMillis();
+ uint32 dt = now - _paddleLastMs;
+ _paddleLastMs = now;
+
+ if (!_moveLeft && !_moveRight)
+ return;
+
+ // Save previous position
+ _paddlePrevLeft = _paddleLeft;
+ _paddlePrevTop = _paddleTop;
+ _paddlePrevRight = _paddleRight;
+ _paddlePrevBottom= _paddleBottom;
+
+ if (_moveLeft) {
+ _paddleX -= (float)dt * _paddleSpeedPerMs;
+ if (_paddleX < (float)_fieldLeft) {
+ _paddleX = (float)_fieldLeft;
+ }
+ _paddleLeft = (int)_paddleX;
+ _paddleRight = _paddleLeft + _paddleWidth - 1;
+ } else { // _moveRight
+ _paddleX += (float)dt * _paddleSpeedPerMs;
+ if (_paddleX + (float)_paddleWidth - 1.0f > (float)_fieldRight) {
+ _paddleX = (float)(_fieldRight - _paddleWidth + 1);
+ }
+ _paddleLeft = (int)_paddleX;
+ _paddleRight = _paddleLeft + _paddleWidth - 1;
+ }
+
+ if (_paddleLeft != _paddlePrevLeft || _paddleRight != _paddlePrevRight ||
+ _paddleTop != _paddlePrevTop || _paddleBottom!= _paddlePrevBottom) {
+ _paddleNeedsRedraw = true;
+ }
+}
+
+// ---- ballAndCollision -------------------------------------------------------
+
+void ArcadePuzzle::ballAndCollision() {
+ uint32 now = g_system->getMillis();
+
+ // Update float position while in flight or dying
+ if (_ballState == kInFlight || _ballState == kDying) {
+ uint32 dt = now - _ballLastMs;
+ _ballLastMs = now;
+
+ _ballX += _ballDX * _ballSpeedPerMs * (float)dt;
+ _ballY -= _ballDY * _ballSpeedPerMs * (float)dt;
+ }
+
+ if (_ballState == kOnPaddle) {
+ // Ball sitting on paddle: follow paddle
+ _ballPrevLeft = _ballLeft;
+ _ballPrevTop = _ballTop;
+ _ballPrevRight = _ballRight;
+ _ballPrevBottom= _ballBottom;
+
+ _ballLeft = _paddleLeft + _ballInitOffset;
+ _ballRight = _ballLeft + _ballWidth - 1;
+ _ballBottom = _paddleTop;
+ _ballTop = _paddleTop - _ballHeight + 1;
+ _ballCenterX = _ballLeft + _ballHalfW;
+ _ballCenterY = _ballTop + _ballHalfH;
+ _ballX = (float)_ballLeft;
+ _ballY = (float)_ballTop;
+
+ if (_ballLeft != _ballPrevLeft || _ballRight != _ballPrevRight ||
+ _ballTop != _ballPrevTop || _ballBottom!= _ballPrevBottom)
+ _ballNeedsRedraw = true;
+
+ // Launch?
+ if (_launchBall) {
+ _launchBall = false;
+ // Pick sub-entry based on random bit
+ int hitPos = _ballInitOffset + _ballHalfW; // offset of ball center from paddle left
+ hitPos = CLIP(hitPos, 0, _paddleWidth - 1);
+ uint32 rbit = g_nancy->_randomSource->getRandomBit();
+ int tableIdx = hitPos * 6 + rbit * 3;
+ _ballDX = _angleTable[tableIdx + 0];
+ _ballDY = _angleTable[tableIdx + 1];
+ _ballSpin = _angleTable[tableIdx + 2];
+ _ballState = kInFlight;
+ _ballLastMs = g_system->getMillis();
+ g_nancy->_sound->playSound(_sounds[1]); // launch sound
+ }
+ } else if (_ballState == kInFlight) {
+ // Ball in flight: save previous, compute new integer position
+ _ballPrevLeft = _ballLeft;
+ _ballPrevTop = _ballTop;
+ _ballPrevRight = _ballRight;
+ _ballPrevBottom= _ballBottom;
+
+ int ballLeft = (int)_ballX;
+ int ballTop = (int)_ballY;
+ int ballRight = ballLeft + _ballWidth - 1;
+ int ballBottom = ballTop + _ballHeight - 1;
+ int ballCenterX = ballLeft + _ballHalfW;
+ int ballCenterY = ballTop + _ballHalfH;
+
+ // Detect and resolve collision
+ wallAndPaddleCollision(ballLeft, ballTop, ballRight, ballBottom, ballCenterX, ballCenterY);
+
+ // Snap float position for certain collision types
+ switch (_collisionType) {
+ case 0: case 4: case 6: case 8: case 10:
+ _ballY = (float)ballTop; break;
+ case 1: case 2: case 3: case 5: case 7: case 9:
+ _ballX = (float)ballLeft; break;
+ case 0xb: case 0xc: case 0xd: case 0xe:
+ _ballX = (float)ballLeft;
+ _ballY = (float)ballTop;
+ break;
+ case 0x10:
+ _ballX = (float)_ballLeft;
+ _ballY = (float)_ballTop;
+ break;
+ default:
+ break;
+ }
+
+ // Apply position (unless no-move types)
+ if (_collisionType != 0xf && _collisionType != -1 && _collisionType != 0x10) {
+ _ballLeft = ballLeft;
+ _ballTop = ballTop;
+ _ballRight = ballRight;
+ _ballBottom = ballBottom;
+ _ballCenterX = ballCenterX;
+ _ballCenterY = ballCenterY;
+ }
+
+ // Apply velocity change
+ applyCollision();
+
+ if (_ballLeft != _ballPrevLeft || _ballRight != _ballPrevRight ||
+ _ballTop != _ballPrevTop || _ballBottom!= _ballPrevBottom)
+ _ballNeedsRedraw = true;
+
+ if (_collisionType == -1) {
+ // Ball died
+ _ballState = kDying;
+ _lifeLostBall = true;
+ _deadBallLeft = _ballLeft; _deadBallTop = _ballTop;
+ _deadBallRight = _ballRight; _deadBallBottom = _ballBottom;
+ _deadBallSrcLeft = _ballSrc.left; _deadBallSrcTop = _ballSrc.top;
+ _deadBallSrcRight = _ballSrc.right; _deadBallSrcBottom = _ballSrc.bottom;
+ _ballX = (float)_ballLeft;
+ _ballY = (float)_ballTop;
+ }
+ } else if (_ballState == kDying) {
+ // Dying ball: keep moving until it exits field
+ _ballPrevLeft = _deadBallLeft;
+ _ballPrevTop = _deadBallTop;
+ _ballPrevRight = _deadBallRight;
+ _ballPrevBottom= _deadBallBottom;
+
+ int ballLeft = (int)_ballX;
+ int ballTop = (int)_ballY;
+ int ballRight = ballLeft + _ballWidth - 1;
+ int ballBottom = ballTop + _ballHeight - 1;
+
+ if (!ballExited(ballLeft, ballTop, ballRight, ballBottom, ballLeft, ballTop)) {
+ // Still visible â update dead ball display rect
+ _ballX = (float)_deadBallLeft;
+ _ballY = (float)_deadBallTop;
+ } else {
+ // Fully exited
+ _lifeLost = true;
+ _gameHalted = true;
+ }
+
+ if (_deadBallLeft != _ballPrevLeft || _deadBallRight != _ballPrevRight ||
+ _deadBallTop != _ballPrevTop || _deadBallBottom!= _ballPrevBottom)
+ _ballNeedsRedraw = true;
+ }
+
+ // Redraw if needed
+ if (_paddleNeedsRedraw) {
+ erasePaddle();
+ drawPaddle();
+ _paddleNeedsRedraw = false;
+ }
+ if (_ballNeedsRedraw) {
+ Common::Rect prevBallRect(_ballPrevLeft, _ballPrevTop, _ballPrevRight + 1, _ballPrevBottom + 1);
+ eraseBall();
+ // Restore any brick pixels the ball erase may have cleared
+ for (int i = 0; i < _totalBricks; ++i) {
+ if (_bricks[i].alive && _bricks[i].vpRect.intersects(prevBallRect))
+ drawBrick(i);
+ }
+ drawBall();
+ // Restore HUD in case ball erase clipped into the score/timer area
+ drawScore();
+ drawTimer();
+ _ballNeedsRedraw = false;
+ }
+}
+
+// ---- wallAndPaddleCollision -------------------------------------------------
+// Detects wall and paddle collisions, adjusts ball rect, sets _collisionType.
+
+void ArcadePuzzle::wallAndPaddleCollision(int &ballLeft, int &ballTop, int &ballRight, int &ballBottom,
+ int &ballCenterX, int &ballCenterY) {
+ _collisionType = -2; // "not yet determined"
+
+ // --- Paddle collision (ball coming from above the paddle top) ---
+ if (_paddleLeft < ballLeft && ballRight < _paddleRight && _paddleTop < ballBottom) {
+ // Ball rect crosses paddle top
+ ballBottom = _paddleTop;
+ ballTop = (ballBottom - _ballHeight) + 1;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = 0; // paddle center (direction-based)
+ }
+ // Left edge of paddle
+ else if (ballLeft < _paddleLeft && _fieldBottom < ballBottom) {
+ ballLeft = _fieldLeft;
+ ballRight = ballLeft + _ballWidth - 1;
+ ballBottom = _fieldBottom;
+ ballTop = (ballBottom - _ballHeight) + 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = _wallBounceMode ? 0xd : -1;
+ }
+ // Right edge of paddle
+ else if (_paddleRight < ballRight && _fieldBottom < ballBottom) {
+ ballRight = _fieldRight;
+ ballLeft = (ballRight - _ballWidth) + 1;
+ ballBottom = _fieldBottom;
+ ballTop = (ballBottom - _ballHeight) + 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = _wallBounceMode ? 0xe : -1;
+ }
+ // Top-left corner
+ else if (ballLeft < _fieldLeft && ballTop < _fieldTop) {
+ ballLeft = _fieldLeft;
+ ballRight = ballLeft + _ballWidth - 1;
+ ballTop = _fieldTop;
+ ballBottom = ballTop + _ballHeight - 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = 0xb;
+ }
+ // Top-right corner
+ else if (_fieldRight < ballRight && ballTop < _fieldTop) {
+ ballRight = _fieldRight;
+ ballLeft = (ballRight - _ballWidth) + 1;
+ ballTop = _fieldTop;
+ ballBottom = ballTop + _ballHeight - 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = 0xc;
+ }
+ // Bottom wall (past deathY or field bottom)
+ else if (_fieldBottom < ballBottom) {
+ ballBottom = _fieldBottom;
+ ballTop = (ballBottom - _ballHeight) + 1;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = _wallBounceMode ? 6 : -1;
+ }
+ // Paddle left-edge collision
+ else if (ballLeft < _paddleLeft && _paddleLeft < ballRight && _paddleTop < ballTop) {
+ ballRight = _paddleLeft;
+ ballLeft = (ballRight - _ballWidth) + 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ _collisionType = 1;
+ }
+ // Paddle right-edge collision
+ else if (ballLeft < _paddleRight && _paddleRight < ballRight && _paddleTop < ballTop) {
+ ballLeft = _paddleRight;
+ ballRight = ballLeft + _ballWidth - 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ _collisionType = 2;
+ }
+ // Left wall
+ else if (ballLeft < _fieldLeft) {
+ ballLeft = _fieldLeft;
+ ballRight = ballLeft + _ballWidth - 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ _collisionType = 3;
+ }
+ // Right wall
+ else if (_fieldRight < ballRight) {
+ ballRight = _fieldRight;
+ ballLeft = (ballRight - _ballWidth) + 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ _collisionType = 5;
+ }
+ // Top wall
+ else if (ballTop < _fieldTop) {
+ ballTop = _fieldTop;
+ ballBottom = ballTop + _ballHeight - 1;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = 4;
+ }
+ // Brick area
+ else if (_brickAreaLeft <= ballCenterX && ballCenterX <= _brickAreaRight &&
+ _brickAreaTop <= ballCenterY && ballCenterY <= _brickAreaBottom) {
+ brickCollision(ballLeft, ballTop, ballRight, ballBottom, ballCenterX, ballCenterY);
+ }
+
+ // Sanity: if ball is still outside field after all corrections â stuck
+ if (ballTop < _fieldTop || _fieldBottom < ballBottom || ballLeft < _fieldLeft || _fieldRight < ballRight) {
+ _collisionType = 0x10;
+ }
+}
+
+// ---- segmentsCross (FUN_00450120 / FUN_0045022b) ----------------------------
+// Returns true if segment P1->P2 crosses segment P3->P4 (including touching).
+// Uses the standard 2D cross-product orientation test.
+
+static int crossSign(int ax, int ay, int bx, int by, int px, int py) {
+ return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
+}
+
+static bool segmentsCross(int p1x, int p1y, int p2x, int p2y,
+ int p3x, int p3y, int p4x, int p4y) {
+ int d1 = crossSign(p3x, p3y, p4x, p4y, p1x, p1y);
+ int d2 = crossSign(p3x, p3y, p4x, p4y, p2x, p2y);
+ int d3 = crossSign(p1x, p1y, p2x, p2y, p3x, p3y);
+ int d4 = crossSign(p1x, p1y, p2x, p2y, p4x, p4y);
+ if (d1 == 0 || d2 == 0 || d3 == 0 || d4 == 0)
+ return true;
+ if ((d1 > 0) == (d2 > 0) || (d3 > 0) == (d4 > 0))
+ return false;
+ return true;
+}
+
+// ---- brickCollision (FUN_0044cad5) ------------------------------------------
+// Ball center is inside brick area. Find which brick is hit and set collision type.
+// Direct grid-index lookup (O(1)), neighbor-based face detection with line-segment
+// intersection test (FUN_00450120).
+
+bool ArcadePuzzle::brickCollision(int &ballLeft, int &ballTop, int &ballRight, int &ballBottom,
+ int &ballCenterX, int &ballCenterY) {
+ // Direct index calculation
+ int row = (ballCenterY - _brickAreaTop) / _brickHeight;
+ int col = (ballCenterX - _brickAreaLeft) / _brickWidth;
+ int i = row * _brickCols + col;
+
+ // Out of bounds or dead/pending â ball passes through freely (leave collisionType = -2)
+ if (i < 0 || i >= _totalBricks)
+ return false;
+ Brick &b = _bricks[i];
+ if (!b.alive || b.pendingExplosion)
+ return false;
+
+ // Previous ball center (this+0x148 in original = previous center point)
+ int prevCX = _ballPrevLeft + _ballHalfW;
+ int prevCY = _ballPrevTop + _ballHalfH;
+
+ // Check each exposed face using trajectory line-segment intersection.
+ // Priority order matches original: top(8) â bottom(10) â left(7) â right(9).
+ if (b.neighborUp == -1 &&
+ segmentsCross(prevCX, prevCY, ballCenterX, ballCenterY,
+ b.vpRect.left, b.vpRect.top, b.vpRect.right, b.vpRect.top)) {
+ ballBottom = b.vpRect.top;
+ ballTop = (ballBottom - _ballHeight) + 1;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = 8;
+ } else if (b.neighborDown == -1 &&
+ segmentsCross(prevCX, prevCY, ballCenterX, ballCenterY,
+ b.vpRect.left, b.vpRect.bottom, b.vpRect.right, b.vpRect.bottom)) {
+ ballTop = b.vpRect.bottom;
+ ballBottom = ballTop + _ballHeight - 1;
+ ballCenterY = ballTop + _ballHalfH;
+ _collisionType = 10;
+ } else if (b.neighborLeft == -1 &&
+ segmentsCross(prevCX, prevCY, ballCenterX, ballCenterY,
+ b.vpRect.left, b.vpRect.top, b.vpRect.left, b.vpRect.bottom)) {
+ ballRight = b.vpRect.left;
+ ballLeft = (ballRight - _ballWidth) + 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ _collisionType = 7;
+ } else if (b.neighborRight == -1 &&
+ segmentsCross(prevCX, prevCY, ballCenterX, ballCenterY,
+ b.vpRect.right, b.vpRect.top, b.vpRect.right, b.vpRect.bottom)) {
+ ballLeft = b.vpRect.right;
+ ballRight = ballLeft + _ballWidth - 1;
+ ballCenterX = ballLeft + _ballHalfW;
+ _collisionType = 9;
+ } else {
+ // Alive brick but all faces blocked by neighbors (or trajectory didn't cross any face)
+ _collisionType = 0xf;
+ }
+
+ // Queue brick for removal and accumulate score
+ addToExplosionList(i, 125);
+
+ _levelScore[_currentLevel] += (int32)_timeBonusMultiplier;
+ if (_cumulativeScore)
+ _score = _totalLevelScore + _levelScore[_currentLevel];
+ else
+ _score = _levelScore[_currentLevel];
+
+ _needsRedraw = true;
+ return true;
+}
+
+// ---- applyCollision ---------------------------------------------------------
+// Changes ball velocity based on current _collisionType.
+
+void ArcadePuzzle::applyCollision() {
+ switch (_collisionType) {
+ case 0: {
+ // Paddle center: look up angle table based on where ball hit paddle
+ int hitPos = _ballCenterX - _paddleLeft;
+ hitPos = CLIP(hitPos, 0, _paddleWidth - 1);
+ uint32 rbit = g_nancy->_randomSource->getRandomBit();
+ int idx = hitPos * 6 + rbit * 3;
+ _ballDX = _angleTable[idx + 0];
+ _ballDY = _angleTable[idx + 1];
+ _ballSpin = _angleTable[idx + 2];
+ g_nancy->_sound->playSound(_sounds[0]); // bounce sound
+ break;
+ }
+
+ case 1: case 2:
+ // Paddle edge: negate both components
+ _ballDX = -_ballDX;
+ _ballDY = -_ballDY;
+ break;
+
+ case 3: case 4: case 5: case 6: {
+ // Wall: reflect with negation, then play wall sound
+ float dx = -_ballDX, dy = -_ballDY;
+ const float *n = _wallNormals[_collisionType];
+ float dot = dx * n[0] + dy * n[1];
+ _ballDX = 2.0f * dot * n[0] - dx;
+ _ballDY = 2.0f * dot * n[1] - dy;
+ float len = (float)sqrt(_ballDX * _ballDX + _ballDY * _ballDY);
+ if (len > 0.0f) { _ballDX /= len; _ballDY /= len; }
+ g_nancy->_sound->playSound(_sounds[0]); // wall bounce sound (same as paddle)
+ break;
+ }
+
+ case 7: case 8: case 9: case 10: {
+ // Brick face: reflect with negation, then play brick hit sound
+ float dx = -_ballDX, dy = -_ballDY;
+ const float *n = _wallNormals[_collisionType];
+ float dot = dx * n[0] + dy * n[1];
+ _ballDX = 2.0f * dot * n[0] - dx;
+ _ballDY = 2.0f * dot * n[1] - dy;
+ float len = (float)sqrt(_ballDX * _ballDX + _ballDY * _ballDY);
+ if (len > 0.0f) { _ballDX /= len; _ballDY /= len; }
+ playBrickHitSound();
+ break;
+ }
+
+ case 0xb: case 0xc: case 0xd: case 0xe:
+ // Corner: set velocity directly from precomputed normal
+ _ballDX = _wallNormals[_collisionType][0];
+ _ballDY = _wallNormals[_collisionType][1];
+ break;
+
+ case 0xf:
+ // Stuck in brick area: negate and play brick hit sound
+ _ballDX = -_ballDX;
+ _ballDY = -_ballDY;
+ playBrickHitSound();
+ break;
+
+ case 0x10:
+ // Catch-all out-of-bounds: negate
+ _ballDX = -_ballDX;
+ _ballDY = -_ballDY;
+ break;
+
+ case -1:
+ // Ball died â handled in ballAndCollision
+ break;
+
+ default:
+ break;
+ }
+}
+
+// ---- ballExited -------------------------------------------------------------
+// Returns true if the dead ball has completely exited the field.
+// Updates _deadBall* rects to the visible clipped portion while still on screen.
+
+bool ArcadePuzzle::ballExited(int ballLeft, int ballTop, int ballRight, int ballBottom, int &, int &) {
+ // Check if any part of ball is still within field
+ if (ballLeft < _fieldRight && ballRight > _fieldLeft &&
+ ballTop < _fieldBottom && ballBottom > _fieldTop) {
+ // Clamp visible portion
+ _deadBallTop = ballTop;
+ _deadBallSrcTop = _ballSrc.top;
+
+ if (ballLeft < _fieldLeft) {
+ _deadBallLeft = _fieldLeft;
+ _deadBallSrcLeft = _ballSrc.left + (_fieldLeft - ballLeft);
+ } else {
+ _deadBallLeft = ballLeft;
+ _deadBallSrcLeft = _ballSrc.left;
+ }
+ if (_fieldBottom < ballBottom) {
+ _deadBallBottom = _fieldBottom;
+ _deadBallSrcBottom = _ballSrc.bottom - (ballBottom - _fieldBottom);
+ } else {
+ _deadBallBottom = ballBottom;
+ _deadBallSrcBottom = _ballSrc.bottom;
+ }
+ if (_fieldRight < ballRight) {
+ _deadBallRight = _fieldRight;
+ _deadBallSrcRight = _ballSrc.right - (ballRight - _fieldRight);
+ } else {
+ _deadBallRight = ballRight;
+ _deadBallSrcRight = _ballSrc.right;
+ }
+ return false; // still visible
+ }
+
+ // Completely outside
+ _deadBallLeft = _deadBallTop = _deadBallRight = _deadBallBottom = 0;
+ _deadBallSrcLeft = _deadBallSrcTop = _deadBallSrcRight = _deadBallSrcBottom = 0;
+ return true;
+}
+
+void ArcadePuzzle::processExplosions() {
+ if (_explosionList.empty())
+ return;
+
+ uint32 now = g_system->getMillis();
+ for (auto it = _explosionList.begin(); it != _explosionList.end(); ++it) {
+ int i = *it;
+ if (now >= _bricks[i].explosionTimer) {
+ // Erase brick from draw surface
+ eraseBrick(i);
+
+ // If ball was overlapping this brick, force a ball redraw so it isn't erased
+ {
+ Common::Rect ballRect(_ballLeft, _ballTop, _ballRight + 1, _ballBottom + 1);
+ if (_bricks[i].vpRect.intersects(ballRect))
+ _ballNeedsRedraw = true;
+ }
+
+ // Mark dead
+ _bricks[i].alive = false;
+ _bricks[i].pendingExplosion = false;
+
+ // Unlink from neighbors
+ int uIdx = _bricks[i].neighborUp;
+ int dIdx = _bricks[i].neighborDown;
+ int lIdx = _bricks[i].neighborLeft;
+ int rIdx = _bricks[i].neighborRight;
+
+ if (uIdx != -1)
+ _bricks[uIdx].neighborDown = -1;
+ if (dIdx != -1)
+ _bricks[dIdx].neighborUp = -1;
+ if (lIdx != -1)
+ _bricks[lIdx].neighborRight = -1;
+ if (rIdx != -1)
+ _bricks[rIdx].neighborLeft = -1;
+
+ _needsRedraw = true;
+ it = _explosionList.erase(it);
+ }
+ }
+
+ // If explosion list is now empty, check whether all bricks are gone
+ if (_explosionList.empty()) {
+ bool allGone = true;
+ for (int i = 0; i < _totalBricks; ++i) {
+ if (_bricks[i].alive) {
+ allGone = false;
+ break;
+ }
+ }
+ if (allGone)
+ _levelClear = true;
+ }
+}
+
+void ArcadePuzzle::addToExplosionList(int brickIdx, uint32 delay) {
+ // Don't add the same brick twice
+ for (int i : _explosionList)
+ if (i == brickIdx)
+ return;
+
+ _explosionList.push_back(brickIdx);
+
+ _bricks[brickIdx].explosionTimer = g_system->getMillis() + delay;
+ _bricks[brickIdx].pendingExplosion = true;
+}
+
+void ArcadePuzzle::playBrickHitSound() {
+ // Rotates through sounds[2], sounds[3], sounds[4] (brick hit A/B/C)
+ int slot = _brickSoundRotator + 2; // maps 0â2, 1â3, 2â4
+ g_nancy->_sound->playSound(_sounds[slot]);
+ _brickSoundRotator = (_brickSoundRotator + 1) % 3;
+}
+
+void ArcadePuzzle::updateTimer() {
+ uint32 now = g_system->getMillis();
+ _timerElapsedMs = now - _timerStartMs;
+
+ uint32 totalSecs = _timerElapsedMs / 1000;
+ uint32 mins = totalSecs / 60;
+ uint32 secs = totalSecs % 60;
+ if (mins > 59) mins = 59;
+ if (secs > 59) secs = 59;
+
+ if (mins != _timerMins || secs != _timerSecs) {
+ _timerMins = mins;
+ _timerSecs = secs;
+ drawTimer();
+ }
+}
+
+void ArcadePuzzle::updateScore() {
+ if (_score == _prevScore)
+ return;
+
+ // Cap at 12,000,000
+ if (_score > 12000000)
+ _score = 12000000;
+
+ // Update time bonus multiplier
+ uint32 elapsedSecs = _timerElapsedMs / 1000;
+ if ((int32)elapsedSecs < _timeLimitSec) {
+ _timeBonusMultiplier =
+ ((float)(_timeLimitSec - (int32)elapsedSecs)) *
+ ((float)_timeBonusMax / (float)_timeLimitSec);
+ } else {
+ _timeBonusMultiplier = 1.0f;
+ }
+
+ drawScore();
+ _prevScore = _score;
+}
+
+// ---- Drawing helpers --------------------------------------------------------
+
+void ArcadePuzzle::drawBrick(int idx) {
+ const Brick &b = _bricks[idx];
+ if (!b.alive || b.type < 0) return;
+ _drawSurface.blitFrom(_image, b.srcRect, Common::Point(b.vpRect.left, b.vpRect.top));
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::eraseBrick(int idx) {
+ _drawSurface.fillRect(_bricks[idx].vpRect, _drawSurface.getTransparentColor());
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::drawPaddle() {
+ _drawSurface.blitFrom(_image, _paddleSrcCur,
+ Common::Point(_paddleLeft, _paddleTop));
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::erasePaddle() {
+ Common::Rect r(_paddlePrevLeft, _paddlePrevTop,
+ _paddlePrevRight + 1, _paddlePrevBottom + 1);
+ _drawSurface.fillRect(r, _drawSurface.getTransparentColor());
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::drawBall() {
+ if (_ballState == kDying) {
+ // Draw dead ball clipped rect
+ if (_deadBallRight > _deadBallLeft && _deadBallBottom > _deadBallTop) {
+ Common::Rect src(_deadBallSrcLeft, _deadBallSrcTop,
+ _deadBallSrcRight, _deadBallSrcBottom);
+ _drawSurface.blitFrom(_image, src,
+ Common::Point(_deadBallLeft, _deadBallTop));
+ _needsRedraw = true;
+ }
+ } else {
+ _drawSurface.blitFrom(_image, _ballSrc,
+ Common::Point(_ballLeft, _ballTop));
+ _needsRedraw = true;
+ }
+}
+
+void ArcadePuzzle::eraseBall() {
+ Common::Rect r(_ballPrevLeft, _ballPrevTop,
+ _ballPrevRight + 1, _ballPrevBottom + 1);
+ if (!r.isEmpty())
+ _drawSurface.fillRect(r, _drawSurface.getTransparentColor());
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::drawDigit(const Common::Rect &destRect, int digit,
+ bool useTimerRects) {
+ if (digit < 0 || digit > 9)
+ return;
+ const Common::Rect &src = useTimerRects ? _timerDigitSrc[digit]
+ : _scoreDigitSrc[digit];
+ _drawSurface.blitFrom(_image, src, Common::Point(destRect.left, destRect.top));
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::drawScore() {
+ if (_scoreDigitSrc[0].isEmpty())
+ return;
+
+ int digitW = _scoreDigitSrc[0].width();
+ int digitH = _scoreDigitSrc[0].height();
+ int baseX = _scoreDisplayX + _fieldLeft;
+ int baseY = _scoreDisplayY + _fieldTop;
+
+ // Collect digits least-significant first
+ int digits[7];
+ int nDigits = 0;
+ int val = ABS(_score);
+ do {
+ digits[nDigits++] = val % 10;
+ val /= 10;
+ } while (val != 0 && nDigits < 7);
+
+ // Erase old digits
+ Common::Rect eraseR(baseX, baseY, baseX + 7 * digitW, baseY + digitH);
+ _drawSurface.fillRect(eraseR, _drawSurface.getTransparentColor());
+
+ // Draw digits left-to-right (most-significant first)
+ for (int i = nDigits - 1; i >= 0; --i) {
+ int col = (nDigits - 1 - i);
+ Common::Rect dest(baseX + col * digitW, baseY,
+ baseX + col * digitW + digitW, baseY + digitH);
+ drawDigit(dest, digits[i], false);
+ }
+ _needsRedraw = true;
+}
+
+void ArcadePuzzle::drawTimer() {
+ if (_timerDigitSrc[0].isEmpty())
+ return;
+
+ int digitW = _timerDigitSrc[0].width();
+ int digitH = _timerDigitSrc[0].height();
+ int baseX = _timerDisplayX + _fieldLeft;
+ int baseY = _timerDisplayY + _fieldTop;
+
+ // Erase old digits
+ Common::Rect eraseR(baseX, baseY, baseX + 4 * digitW, baseY + digitH);
+ _drawSurface.fillRect(eraseR, _drawSurface.getTransparentColor());
+
+ // Draw MM:SS left-to-right: [mins_tens][mins_ones][secs_tens][secs_ones]
+ uint32 mins = _timerMins;
+ uint32 secs = _timerSecs;
+
+ Common::Rect d3(baseX, baseY, baseX + digitW, baseY + digitH);
+ drawDigit(d3, mins / 10, true);
+ Common::Rect d2(baseX + digitW, baseY, baseX + 2 * digitW, baseY + digitH);
+ drawDigit(d2, mins % 10, true);
+ Common::Rect d1(baseX + 2 * digitW, baseY, baseX + 3 * digitW, baseY + digitH);
+ drawDigit(d1, secs / 10, true);
+ Common::Rect d0(baseX + 3 * digitW, baseY, baseX + 4 * digitW, baseY + digitH);
+ drawDigit(d0, secs % 10, true);
+ _needsRedraw = true;
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/arcadepuzzle.h b/engines/nancy/action/puzzle/arcadepuzzle.h
index 1b20bf35275..79baa3303f7 100644
--- a/engines/nancy/action/puzzle/arcadepuzzle.h
+++ b/engines/nancy/action/puzzle/arcadepuzzle.h
@@ -23,16 +23,23 @@
#define NANCY_ACTION_ARCADEPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+
+#include "graphics/managed_surface.h"
+#include "common/path.h"
+#include "common/rect.h"
namespace Nancy {
namespace Action {
-// Barnacle Blast (Arcanoid) mini-game in Nancy 8
-
+// Barnacle Blast (Arkanoid clone) mini-game in Nancy 8.
+// The player controls a paddle at the bottom of the screen, bouncing a ball
+// to destroy bricks. The player needs to beat 2 levels to get the full prize.
+// Data file is 0x4e8 (1256) bytes.
class ArcadePuzzle : public RenderActionRecord {
public:
ArcadePuzzle() : RenderActionRecord(7) {}
- virtual ~ArcadePuzzle() {}
+ virtual ~ArcadePuzzle();
void init() override;
@@ -43,6 +50,234 @@ public:
protected:
Common::String getRecordTypeName() const override { return "ArcadePuzzle"; }
bool isViewportRelative() const override { return true; }
+
+ // ---------- Inner types ----------
+
+ struct Brick {
+ Common::Rect srcRect; // source in _image (brick type sprite)
+ Common::Rect vpRect; // viewport-relative position (for collision and rendering)
+ int type = -1; // -1=empty, 0-3=brick types
+ int neighborLeft = -1; // indices of live neighbors (-1=none)
+ int neighborUp = -1;
+ int neighborRight = -1;
+ int neighborDown = -1;
+ uint32 explosionTimer = 0; // absolute ms when to remove sprite
+ bool alive = false; // false after explosion completes
+ bool pendingExplosion = false; // true while queued in explosion list (not yet erased)
+ };
+
+ // ---------- Subfunction translations ----------
+ void paddleMovement(); // FUN_0044b99b
+ void ballAndCollision(); // FUN_0044bca3
+ void processExplosions(); // FUN_0044f9dd
+ void updateTimer(); // FUN_00450276
+ void updateScore(); // FUN_00450354
+ int getNextLevel() const; // FUN_0044e1a8
+ void initSublevel(); // FUN_0044d3c7
+ void resetRound(); // FUN_0044b78e
+ void wallAndPaddleCollision(int &ballLeft, int &ballTop, int &ballRight, int &ballBottom,
+ int &ballCenterX, int &ballCenterY); // FUN_0044c378
+ bool brickCollision(int &ballLeft, int &ballTop, int &ballRight, int &ballBottom,
+ int &ballCenterX, int &ballCenterY); // FUN_0044cad5
+ void applyCollision(); // FUN_0044ced2
+ bool ballExited(int ballLeft, int ballTop, int ballRight, int ballBottom,
+ int &ballCenterX, int &ballCenterY); // FUN_0044d20e
+ void playBrickHitSound(); // FUN_0044fff5
+ void addToExplosionList(int brickIdx, uint32 delay);
+ void generateBricks(); // FUN_0044e0b5
+ void buildAngleTable(); // FUN_0044e1a8
+ void drawBrick(int idx);
+ void eraseBrick(int idx);
+ void drawPaddle();
+ void erasePaddle();
+ void drawBall();
+ void eraseBall();
+ void drawDigit(const Common::Rect &destRect, int digit, bool useTimerRects);
+ void drawTimer();
+ void drawScore();
+
+ // ---------- Data (read from stream) ----------
+ Common::Path _imageName;
+
+ uint32 _numLevelsToWin = 0;
+
+ // Per-level grid dimensions (0x25): [cols0, rows0, cols1, rows1, ...]
+ uint32 _levelCols[6] = {};
+ uint32 _levelRows[6] = {};
+
+ // Per-level grid offsets within viewport (0x55): [xOff0, yOff0, ...]
+ int32 _levelXOff[6] = {};
+ int32 _levelYOff[6] = {};
+
+ Common::Rect _ballSrc; // source rect in image for ball
+ Common::Rect _paddleSrc; // source rect in image for paddle
+ Common::Rect _brickTypeSrc[8]; // source rects for brick types 0..7 (only 0-3 used)
+
+ Common::Rect _scoreDigitSrc[10]; // 10-frame sprite sheet for score digits (0x125)
+ Common::Rect _timerDigitSrc[10]; // 10-frame sprite sheet for timer digits (0x1c5)
+
+ Common::Rect _timerDisplayDest; // destination on screen for timer (0x265)
+
+ Common::Rect _lifeSrc[3]; // life indicator source rects (0x275, 0x285, 0x295)
+
+ uint32 _stateDelayMs = 0; // wait-state duration in ms (0x2a5)
+
+ // Viewport rect as written in data (0x2a9..0x2b8)
+ // We validate against actual viewport but don't store all fields
+
+ // Display positions
+ int32 _timerDisplayX = 0, _timerDisplayY = 0; // 0x2c9, 0x2cd (+ viewport offset)
+ int32 _scoreDisplayX = 0, _scoreDisplayY = 0; // 0x2c1, 0x2c5 (+ viewport offset)
+
+ int32 _deathYDist = 0; // pixels from playfield bottom to death line (0x2d9)
+
+ // Speed parameters: steps = ms interval (int); pixPerStep = pixels per interval (float in data)
+ uint32 _paddleSteps = 1; // 0x2e9 (integer)
+ uint32 _ballSteps = 1; // 0x2ed (integer)
+ float _paddlePixPerStep = 1.0f; // 0x2f1 (IEEE 754 float stored in binary)
+ float _ballPixPerStep = 1.0f; // 0x2f5 (IEEE 754 float stored in binary)
+
+ int32 _angleTableStart = 45; // starting angle in degrees (0x2f9)
+ int32 _angleTableEnd = 90; // ending angle (0x2fd)
+
+ bool _randomBallStart = false; // random ball starting offset (0x301)
+ bool _wallBounceMode = false; // true=ball bounces off bottom, false=dies (0x302)
+ bool _cumulativeScore = false; // true=score accumulates across levels (0x303)
+
+ int32 _scoreStepSize = 1; // score per brick tier (0x305)
+ int32 _timeBonusMax = 100; // max time bonus (0x309)
+ int32 _timeLimitSec = 60; // time limit in seconds (0x30d)
+ int32 _scoreParam4 = 0; // extra score param (0x311)
+
+ // Sounds: 6 permanent + 3 dynamic
+ SoundDescription _sounds[6]; // 0x315..0x43a (bounce, launch, brick hit A/B/C, wall)
+ SoundDescription _levelClearSound; // 0x43b
+ SoundDescription _gameOverSound; // 0x486
+ SoundDescription _lifeLostSound; // 0x4b7
+
+ // Win scene (0x46c, 25 bytes)
+ SceneChangeWithFlag _winScene;
+
+ // ---------- Runtime state ----------
+
+ enum GameSubState {
+ kPlaying = 0, // normal gameplay loop
+ kLevelClear = 1, // play level-clear sound then wait
+ kLifeLost = 2, // decrement lives, play sound, then wait
+ kResetBoard = 3, // init new sublevel
+ kGameOverWin = 4, // play game-over sound then wait
+ kFinish = 5, // trigger win/lose scene
+ kWaitTimer = 6, // wait for _stateWaitUntil
+ kGameOverLose = 7 // wait then finish (no lives left)
+ };
+
+ enum BallState {
+ kOnPaddle = 0, // resting on paddle before launch
+ kInFlight = 1, // moving freely
+ kDying = 2 // ball has exited the playfield
+ };
+
+ GameSubState _gameSubState = kPlaying;
+
+ // Flags set by gameplay events
+ bool _winGame = false; // 0x18
+ bool _levelClear = false; // 0x1c
+ bool _lifeLost = false; // 0x20
+ bool _lifeLostBall = false; // 0x24 (set by ball exit, used to stop paddle)
+ bool _gameHalted = false; // 0x28
+
+ int _livesLeft = 3; // remaining lives (starts at 3)
+
+ uint32 _stateWaitUntil = 0; // absolute ms for wait-state expiry
+
+ // Level tracking
+ int _currentLevel = 0; // 0xb0
+ uint32 _winFlags[6] = {}; // 0x270..0x284
+ int32 _levelScore[6] = {}; // 0x254..0x268 (accumulated score per level)
+ int32 _totalLevelScore = 0; // 0x26c
+ int32 _score = 0; // 0xb4 (displayed score)
+ int32 _prevScore = -1; // 0x298 (to detect changes)
+
+ // Brick grid for current sublevel
+ int _brickCols = 0; // 0x2ac[level]
+ int _brickRows = 0; // 0x2b0[level]
+ int _totalBricks = 0; // 0xac
+
+ // Brick area screen coords (playfield-relative = viewport-relative)
+ int _brickAreaLeft = 0; // 0x31c
+ int _brickAreaTop = 0; // 0x320
+ int _brickAreaRight = 0; // 0x324
+ int _brickAreaBottom = 0; // 0x328
+ int _brickWidth = 0; // 0xa4
+ int _brickHeight = 0; // 0xa8
+
+ Common::Array<Brick> _bricks;
+
+ // Explosion list (0x6a7)
+ Common::List<int> _explosionList;
+ int _brickSoundRotator = 0; // 0x6af: cycles 0,1,2 for brick hit sounds A/B/C
+
+ // Playfield screen rect (where the game is drawn)
+ int _fieldLeft = 0, _fieldTop = 0, _fieldRight = 0, _fieldBottom = 0; // 0x3c..0x48
+ int _fieldWidth = 0, _fieldHeight = 0; // 0x35c, 0x360
+ int _fieldOffX = 0, _fieldOffY = 0; // 0x4c, 0x50
+
+ // Death line: ball dies if it passes _deathY (y coord on viewport)
+ int _deathY = 0; // 0xa0
+
+ // Paddle state
+ Common::Rect _paddleSrcCur; // current paddle source (may differ for damaged paddle)
+ int _paddleLeft = 0, _paddleTop = 0, _paddleRight = 0, _paddleBottom = 0; // 0xc8..0xd4
+ int _paddlePrevLeft = 0, _paddlePrevTop = 0, _paddlePrevRight = 0, _paddlePrevBottom = 0;
+ int _paddleWidth = 0, _paddleHeight = 0, _paddleHalfW = 0; // 0x94, 0x98, 0x9c
+ float _paddleX = 0.0f; // 0x50c (floating-point left edge)
+ float _paddleSpeedPerMs = 1.0f; // pixels/ms (derived from _paddleSteps/_paddlePixPerStep)
+ uint32 _paddleLastMs = 0; // 0x4e8
+ bool _paddleNeedsRedraw = false; // 0xc0
+
+ // Ball state
+ BallState _ballState = kOnPaddle; // 0x180
+ float _ballX = 0.0f, _ballY = 0.0f; // floating-point position; 0x100, 0x104
+ float _ballDX = 0.0f, _ballDY = 1.0f; // velocity direction; 0xe8, 0xec
+ float _ballSpin = 0.0f; // 0xf0
+ int _ballLeft = 0, _ballTop = 0, _ballRight = 0, _ballBottom = 0; // 0x118..0x124
+ int _ballPrevLeft = 0, _ballPrevTop = 0, _ballPrevRight = 0, _ballPrevBottom = 0; // 0x150..0x15c
+ int _ballCenterX = 0, _ballCenterY = 0; // 0x148, 0x14c
+ int _ballInitOffset = 0; // offset from paddle left for initial position; 0x174
+ int _ballWidth = 0, _ballHeight = 0, _ballHalfW = 0, _ballHalfH = 0; // 0x164..0x170
+ float _ballSpeedPerMs = 1.0f; // pixels/ms
+ uint32 _ballLastMs = 0; // 0x178
+ bool _ballNeedsRedraw = false; // 0x160
+ int _collisionType = -2; // 0x184 (-2 = no collision yet)
+
+ // Dead-ball state (when ball is dying, this tracks the ball's last valid rect)
+ int _deadBallSrcLeft = 0, _deadBallSrcTop = 0, _deadBallSrcRight = 0, _deadBallSrcBottom = 0; // 0x128..0x134
+ int _deadBallLeft = 0, _deadBallTop = 0, _deadBallRight = 0, _deadBallBottom = 0; // 0x138..0x144
+
+ // Wall normals (0x188 + type*12, 3 floats each)
+ // Only dx, dy used (index 0 and 1 per entry)
+ static const float _wallNormals[17][3]; // for collision types 0..16 (0xf = 15, 0x10 = 16)
+
+ // Angle table (dynamically allocated, size = _paddleWidth * 2 * 3 floats)
+ // table[hitPos * 2 + random_bit] = {dx, dy, spin}
+ Common::Array<float> _angleTable; // 0x51c
+
+ // Timer tracking
+ uint32 _timerStartMs = 0; // 0x344
+ uint32 _timerElapsedMs = 0; // 0x348
+ uint32 _timerMins = 0, _timerSecs = 0; // 0x33c, 0x340 (for display)
+
+ // Time bonus
+ float _timeBonusMultiplier = 1.0f; // 0x29c
+
+ // Input state (updated each handleInput call, consumed in execute/paddleMovement)
+ bool _moveLeft = false;
+ bool _moveRight = false;
+ bool _launchBall = false;
+
+ // Surfaces
+ Graphics::ManagedSurface _image;
+ Graphics::ManagedSurface _backgroundCache; // copy of initial draw surface for erasing
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list