[Scummvm-git-logs] scummvm master -> 3407d66ddf1e1135b56dab07667cd376d39ab2b3
bluegr
noreply at scummvm.org
Sat Sep 19 21:42:16 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:
3407d66ddf NANCY: NANCY12: Match the original MirrorLightPuzzle behavior
Commit: 3407d66ddf1e1135b56dab07667cd376d39ab2b3
https://github.com/scummvm/scummvm/commit/3407d66ddf1e1135b56dab07667cd376d39ab2b3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-20T00:41:56+03:00
Commit Message:
NANCY: NANCY12: Match the original MirrorLightPuzzle behavior
- Mirrors are now turned by holding a mouse button
- The beam now uses the color and opacity stored in the puzzle data. It
is drawn as a band that fades from its center to its edges.
- The beam now stops at the boundary zone inside the bulb, instead of
at the edge of the larger scene-change zone around it
- Solving the puzzle runs the zone's special effect and changes the
scene right away, instead of waiting a fixed time.
- The puzzle state is now saved
Changed paths:
engines/nancy/action/actionzone.cpp
engines/nancy/action/actionzone.h
engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
engines/nancy/action/puzzle/mirrorlightpuzzle.h
engines/nancy/puzzledata.cpp
engines/nancy/puzzledata.h
engines/nancy/ui/viewport.h
diff --git a/engines/nancy/action/actionzone.cpp b/engines/nancy/action/actionzone.cpp
index 41447b17280..909422d7045 100644
--- a/engines/nancy/action/actionzone.cpp
+++ b/engines/nancy/action/actionzone.cpp
@@ -171,7 +171,7 @@ void ActionZone::readOverlayZone(Common::SeekableReadStream &stream, bool isNanc
stream.skip(4); // extra int32 vs Nancy12
}
stream.skip(4); // int32
- stream.skip(1); // byte (loop/play mode)
+ overlayPlayMode = stream.readByte();
overlayLayer = stream.readSint32LE();
}
diff --git a/engines/nancy/action/actionzone.h b/engines/nancy/action/actionzone.h
index 4d3045a09aa..9d43b0f0cbb 100644
--- a/engines/nancy/action/actionzone.h
+++ b/engines/nancy/action/actionzone.h
@@ -56,7 +56,7 @@ enum ActionZoneType : byte {
kZoneUnknown12 = 0x12, // another special-effect variant
kZoneUnknown13 = 0x13, // another special-effect variant
kZoneBoundary = 0x14, // play-area wall
- kZoneUnknown15 = 0x15, // special effect + int32; a damage range in Nancy13
+ kZoneUnknown15 = 0x15, // Nancy12: on entry, sound + special effect + change to scene specialEffectId; a damage range in Nancy13
kZoneBumper = 0x16, // Nancy12: an overlay variant; Nancy13: a pachinko hole
kZoneFlatTire = 0x17 // pothole: damages the car driving over it
};
@@ -121,6 +121,7 @@ struct ActionZone {
Common::String overlayName;
Common::Array<Common::Rect> overlaySrcRects;
Common::Rect overlayDestRect;
+ byte overlayPlayMode = 0; // 1 = drawn opaque; anything else = color-keyed
int32 overlayLayer = 0; // draw pass: 0 renders under the car, 1 over it
// The Nancy13 pinball layout (AR 175) differs from the Nancy12 one: the base carries an
diff --git a/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp b/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
index 06b96697afe..1aa5e462cfb 100644
--- a/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
+++ b/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
@@ -20,12 +20,15 @@
*/
+#include "common/random.h"
+
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/input.h"
#include "engines/nancy/util.h"
+#include "engines/nancy/puzzledata.h"
#include "engines/nancy/state/scene.h"
#include "engines/nancy/action/puzzle/mirrorlightpuzzle.h"
@@ -33,24 +36,25 @@
namespace Nancy {
namespace Action {
-// TODO - open items for this puzzle (mostly cosmetic):
-// - Win presentation: the original lights the bulb via a per-zone movie
-// animation (loaded by the movie loader at init); we draw a single static
-// overlay frame with no sound instead.
-// - The player's quit/cancel path (leaving an unsolved puzzle) is unknown.
-// - Mirror rotation step (2 deg/click) approximates the original's exact
-// per-click amount.
+static const double kTwoPi = 2.0 * M_PI;
+static const double kRotateStep = kTwoPi * 0.1 * 0.02; // 0.72 degrees per tick
+static const uint32 kRotateDelay = 200; // ms from pressing a button to the first turn
+static const uint32 kRotateTickTime = 20; // ms between turns while the button is held
+static const uint32 kOverlayFrameTime = 66; // ms per overlay animation frame
+static const double kUnsetMirrorAngle = -1.0;
void MirrorLightPuzzle::readData(Common::SeekableReadStream &stream) {
- // 65-byte base header.
readFilename(stream, _imageName);
_beamAngle = stream.readSint16LE();
_beamOriginX = stream.readSint32LE();
_beamOriginY = stream.readSint32LE();
- stream.skip(4);
- _glowRadius = stream.readSint16LE();
- stream.skip(8); // step size (double)
- stream.skip(8);
+ stream.skip(1); // designer debug flag (shows the hovered mirror's angle)
+ _beamColor[2] = stream.readByte(); // stored as b, g, r
+ _beamColor[1] = stream.readByte();
+ _beamColor[0] = stream.readByte();
+ _beamHalfWidth = stream.readSint16LE();
+ _beamCenterOpacity = stream.readDoubleLE();
+ _beamEdgeOpacity = stream.readDoubleLE();
// Mirror sprite frames - one per evenly-spaced angle around a full turn.
uint16 numFrames = stream.readUint16LE();
@@ -66,9 +70,14 @@ void MirrorLightPuzzle::readData(Common::SeekableReadStream &stream) {
Mirror &m = _mirrors[i];
readRect(stream, m.destRect);
m.angle = (double)stream.readSint16LE() * (M_PI / 180.0);
- stream.skip(16); // secondary rect (unused; empty in the data)
+ stream.skip(16); // bounds for dragging the mirror around (unused; empty in the data)
m.minAngle = (double)stream.readSint16LE() * (M_PI / 180.0);
m.maxAngle = (double)stream.readSint16LE() * (M_PI / 180.0);
+ m.step = kRotateStep;
+
+ if (m.isRotatable() && !isAngleWithinLimits(m, m.angle)) {
+ m.angle = m.minAngle;
+ }
}
if (_mirrors.size() > kMaxMirrors) {
_mirrors.resize(kMaxMirrors);
@@ -77,18 +86,32 @@ void MirrorLightPuzzle::readData(Common::SeekableReadStream &stream) {
readActionZoneArray(stream, _zones);
}
+bool MirrorLightPuzzle::isAngleWithinLimits(const Mirror &m, double angle) const {
+ if (m.minAngle <= m.maxAngle) {
+ return angle >= m.minAngle && angle <= m.maxAngle;
+ }
+
+ // The range wraps past 0 (min > max)
+ if (angle > m.minAngle && angle < kTwoPi) {
+ return true;
+ }
+ if (angle >= m.maxAngle) {
+ return false;
+ }
+ return angle > 0.0;
+}
+
uint MirrorLightPuzzle::frameForAngle(double angle) const {
if (_frameSrcRects.empty()) {
return 0;
}
- const double twoPi = 2.0 * M_PI;
- double a = fmod(angle, twoPi);
+ double a = fmod(angle, kTwoPi);
if (a < 0.0) {
- a += twoPi;
+ a += kTwoPi;
}
- uint frame = (uint)((a / twoPi) * _frameSrcRects.size() + 0.5);
+ uint frame = (uint)((a / kTwoPi) * _frameSrcRects.size() + 0.5);
return frame % _frameSrcRects.size();
}
@@ -104,7 +127,6 @@ void MirrorLightPuzzle::drawMirror(uint index) {
void MirrorLightPuzzle::traceBeam() {
_beamPath.clear();
- _solved = false;
// The original marches the beam a fixed step at a time, reflecting off each
// mirror it enters. It works in a y-up frame (it negates dy when measuring
@@ -152,7 +174,7 @@ void MirrorLightPuzzle::traceBeam() {
}
if (hit != -1) {
- // Bounce at the mirror's centre (not the edge where the beam entered),
+ // Bounce at the mirror's center (not the edge where the beam entered),
// so the beam visually meets each mirror at its middle.
const Common::Rect &mr = _mirrors[hit].destRect;
px = (mr.left + mr.right) / 2.0;
@@ -169,152 +191,194 @@ void MirrorLightPuzzle::traceBeam() {
continue;
}
- // Reaching the detector zone (the SpecialEffect zone at the bulb) solves
- // the puzzle.
- if (!_detectorRect.isEmpty() && _detectorRect.contains(p)) {
- _solved = true;
- _beamPath.push_back(p);
- return;
+ // The beam ends on hitting a boundary zone (the one inside the bulb). The
+ // scene-change zone around it only reacts to where the beam ends.
+ for (uint i = 0; i < _zones.size(); ++i) {
+ if (_zones[i].type == kZoneBoundary && _zones[i].rect.contains(p)) {
+ _beamPath.push_back(p);
+ return;
+ }
}
}
_beamPath.push_back(Common::Point((int16)(px + 0.5), (int16)(py + 0.5)));
}
-void MirrorLightPuzzle::drawBeamGlow() {
- if (_beamPath.size() < 2) {
- return;
- }
+void MirrorLightPuzzle::updateZones() {
+ // Zones react to the beam's end point. Entering a zone plays its sound; the
+ // scene-change zone solves the puzzle, and an overlay zone animates while the
+ // beam stays in it.
+ const Common::Point &end = _beamPath.back();
+ for (uint i = 0; i < _zones.size(); ++i) {
+ const ActionZone &z = _zones[i];
+ Common::Rect r = z.rect;
+ if (z.type == kZoneOverlay) {
+ if (r.isEmpty()) {
+ r = z.overlayDestRect;
+ }
+ } else if (z.type != kZoneUnknown15) {
+ continue;
+ }
- const int w = _drawSurface.w;
- const int h = _drawSurface.h;
- // A soft Gaussian profile reads as a fuzzy, translucent light ray. peak is the
- // additive intensity at the beam centre (well under 255 so the room shows
- // through); sigma controls how quickly it fades; R clips the faint tail.
- const int R = MAX<int>(4, _glowRadius + _glowRadius / 2);
- const int R2 = R * R;
- const double sigma = MAX<double>(1.5, _glowRadius / 2.0);
- const double twoSigma2 = 2.0 * sigma * sigma;
- const int peak = 110;
-
- // Accumulate the max glow intensity per pixel (so overlapping stamps don't
- // over-brighten), then composite once.
- Common::Array<byte> intensity(w * h, 0);
- for (uint s = 1; s < _beamPath.size(); ++s) {
- const Common::Point &p0 = _beamPath[s - 1];
- const Common::Point &p1 = _beamPath[s];
- int dx = p1.x - p0.x;
- int dy = p1.y - p0.y;
- int len = MAX<int>(1, (int)sqrt((double)(dx * dx + dy * dy)));
- for (int i = 0; i <= len; ++i) {
- int cx = p0.x + dx * i / len;
- int cy = p0.y + dy * i / len;
- for (int oy = -R; oy <= R; ++oy) {
- for (int ox = -R; ox <= R; ++ox) {
- int px = cx + ox;
- int py = cy + oy;
- if (px < 0 || py < 0 || px >= w || py >= h) {
- continue;
- }
- int d2 = ox * ox + oy * oy;
- if (d2 > R2) {
- continue;
- }
- int val = (int)(peak * exp(-(double)d2 / twoSigma2));
- if (val <= 0) {
- continue;
- }
- byte &acc = intensity[py * w + px];
- if (val > acc) {
- acc = (byte)val;
- }
- }
+ bool inside = r.contains(end);
+ if (inside && !_zoneOccupied[i]) {
+ playSoundBlock(z._sound);
+ if (z.type == kZoneUnknown15) {
+ _winZone = (int)i;
+ _solved = true;
}
}
+ _zoneOccupied[i] = inside;
+ }
+}
+
+void MirrorLightPuzzle::blendBeamPixel(int x, int y, double opacity) {
+ if (x < 0 || y < 0 || x >= _drawSurface.w || y >= _drawSurface.h) {
+ return;
}
- const Graphics::ManagedSurface &bg = NancySceneState.getViewport().getBackground();
+ // Blend over whatever is already drawn here (a mirror or an earlier beam line),
+ // or over the scene background where the surface is still transparent.
const Graphics::PixelFormat &fmt = _drawSurface.format;
- uint32 transColor = _drawSurface.getTransparentColor();
+ uint32 pixel = _drawSurface.getPixel(x, y);
+ byte r, g, b;
+ if (pixel == _drawSurface.getTransparentColor()) {
+ const Graphics::ManagedSurface &bg = NancySceneState.getViewport().getBackground();
+ if (x < bg.w && y < bg.h) {
+ bg.format.colorToRGB(bg.getPixel(x, y), r, g, b);
+ } else {
+ r = g = b = 0;
+ }
+ } else {
+ fmt.colorToRGB(pixel, r, g, b);
+ }
- for (int y = 0; y < h; ++y) {
- for (int x = 0; x < w; ++x) {
- int inten = intensity[y * w + x];
- if (inten == 0) {
- continue;
- }
+ r = (byte)(r * (1.0 - opacity) + _beamColor[0] * opacity);
+ g = (byte)(g * (1.0 - opacity) + _beamColor[1] * opacity);
+ b = (byte)(b * (1.0 - opacity) + _beamColor[2] * opacity);
+ _drawSurface.setPixel(x, y, fmt.RGBToColor(r, g, b));
+}
- // Skip actual mirror-sprite pixels (the mirror faces stay clean), but
- // still glow over the transparent parts of a mirror's bounding box.
- // Mirrors are the only thing drawn before the glow, so any opaque
- // pixel here is a mirror pixel.
- uint32 under = _drawSurface.getPixel(x, y);
- if (under != transColor) {
- continue;
- }
+void MirrorLightPuzzle::drawBeamLine(Common::Point p0, Common::Point p1, double opacity) {
+ int dx = ABS(p1.x - p0.x);
+ int dy = -ABS(p1.y - p0.y);
+ int sx = p0.x < p1.x ? 1 : -1;
+ int sy = p0.y < p1.y ? 1 : -1;
+ int err = dx + dy;
+ int x = p0.x;
+ int y = p0.y;
+
+ while (true) {
+ blendBeamPixel(x, y, opacity);
+ if (x == p1.x && y == p1.y) {
+ break;
+ }
- // Glow over the scene background behind the beam.
- byte r, g, b;
- if (x < bg.w && y < bg.h) {
- bg.format.colorToRGB(bg.getPixel(x, y), r, g, b);
- } else {
- r = g = b = 0;
- }
+ int e2 = 2 * err;
+ if (e2 >= dy) {
+ err += dy;
+ x += sx;
+ }
+ if (e2 <= dx) {
+ err += dx;
+ y += sy;
+ }
+ }
+}
- r = (byte)MIN<int>(255, r + inten);
- g = (byte)MIN<int>(255, g + inten);
- b = (byte)MIN<int>(255, b + inten);
- _drawSurface.setPixel(x, y, fmt.RGBToColor(r, g, b));
+void MirrorLightPuzzle::drawBeam() {
+ // Each segment is drawn as a band of parallel lines, offset vertically for a
+ // mostly horizontal segment and horizontally otherwise. The opacity falls off
+ // linearly from the center line to the edges of the band.
+ const int halfWidth = MAX<int>(0, _beamHalfWidth);
+ for (uint s = 1; s < _beamPath.size(); ++s) {
+ const Common::Point &p0 = _beamPath[s - 1];
+ const Common::Point &p1 = _beamPath[s];
+ bool horizontal = ABS(p1.y - p0.y) < ABS(p1.x - p0.x);
+
+ for (int o = -halfWidth; o <= halfWidth; ++o) {
+ double t = halfWidth ? (double)(halfWidth - ABS(o)) / halfWidth : 1.0;
+ double opacity = _beamEdgeOpacity + (_beamCenterOpacity - _beamEdgeOpacity) * t;
+ Common::Point offset = horizontal ? Common::Point(0, o) : Common::Point(o, 0);
+ drawBeamLine(p0 + offset, p1 + offset, opacity);
+ }
+ }
+}
+
+void MirrorLightPuzzle::drawOverlays() {
+ for (const ZoneOverlay &overlay : _overlays) {
+ if (!_zoneOccupied[overlay.zoneIndex]) {
+ continue;
}
+
+ const ActionZone &z = _zones[overlay.zoneIndex];
+ _drawSurface.blitFrom(overlay.image, z.overlaySrcRects[overlay.frame],
+ Common::Point(z.overlayDestRect.left, z.overlayDestRect.top));
}
}
void MirrorLightPuzzle::redraw() {
_drawSurface.clear(g_nancy->_graphics->getTransColor());
- // Mirror sprites first, then the beam on top of them.
+ // Mirror sprites first, then the beam over them, then the lit target.
for (uint i = 0; i < _mirrors.size(); ++i) {
drawMirror(i);
}
- // The beam, composited additively over the mirrors and scene background so it
- // reads as glowing light rather than flat lines.
- drawBeamGlow();
-
- // Once solved, light up the detector overlay (the bulb graphic).
- if (_solved) {
- for (uint i = 0; i < _zones.size(); ++i) {
- const ActionZone &z = _zones[i];
- if (!z.overlaySrcRects.empty() && !z.overlayDestRect.isEmpty()) {
- _drawSurface.blitFrom(_image, z.overlaySrcRects[0],
- Common::Point(z.overlayDestRect.left, z.overlayDestRect.top));
- }
- }
- }
+ drawBeam();
+ drawOverlays();
_needsRedraw = true;
}
-void MirrorLightPuzzle::rotateMirror(uint index, bool clockwise) {
+void MirrorLightPuzzle::rotateMirror(uint index, int dir) {
Mirror &m = _mirrors[index];
- if (m.minAngle == m.maxAngle) {
- return; // fixed mirror
- }
- // Fine step so the beam can be aimed precisely; clamped to the mirror's
- // rotation range. The exact per-click amount in the original is unconfirmed.
- const double step = 2.0 * (M_PI / 180.0);
- m.angle += clockwise ? step : -step;
- if (m.angle < m.minAngle) {
- m.angle = m.minAngle;
- } else if (m.angle > m.maxAngle) {
- m.angle = m.maxAngle;
+ // A turn that would cross a limit reverses the mirror, so it swings back.
+ if (!isAngleWithinLimits(m, m.angle + dir * m.step)) {
+ m.step = -m.step;
}
+ m.angle += dir * m.step;
+ saveMirrorAngles();
traceBeam();
+ updateZones();
redraw();
}
+void MirrorLightPuzzle::saveMirrorAngles() {
+ MirrorLightData *data = (MirrorLightData *)NancySceneState.getPuzzleData(MirrorLightData::getTag());
+ if (!data) {
+ return;
+ }
+
+ data->angles.resize(_mirrors.size());
+ for (uint i = 0; i < _mirrors.size(); ++i) {
+ data->angles[i] = _mirrors[i].angle;
+ }
+}
+
+void MirrorLightPuzzle::playSoundBlock(const RandomSoundBlock &block) {
+ if (block.names.empty()) {
+ return;
+ }
+
+ uint idx = block.names.size() == 1 ? 0 : g_nancy->_randomSource->getRandomNumber(block.names.size() - 1);
+ const Common::String &name = block.names[idx];
+ if (name.empty() || name == "NO SOUND") {
+ return;
+ }
+
+ SoundDescription desc;
+ desc.name = name;
+ desc.channelID = block.channel;
+ desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+ desc.volume = block.volume;
+
+ g_nancy->_sound->loadSound(desc);
+ g_nancy->_sound->playSound(desc);
+}
+
void MirrorLightPuzzle::init() {
Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
_drawSurface.create(vpBounds.width(), vpBounds.height(),
@@ -327,17 +391,48 @@ void MirrorLightPuzzle::init() {
g_nancy->_resource->loadImage(_imageName, _image);
_image.setTransparentColor(_drawSurface.getTransparentColor());
- // The detector zone carries a SpecialEffect whose id is the win scene (same
- // semantics as MinigolfPuzzle's sink zone). Its rect is the beam target.
+ // Mirrors keep the angle the player last left them at.
+ MirrorLightData *data = (MirrorLightData *)NancySceneState.getPuzzleData(MirrorLightData::getTag());
+ if (data) {
+ for (uint i = 0; i < _mirrors.size() && i < data->angles.size(); ++i) {
+ Mirror &m = _mirrors[i];
+ double saved = data->angles[i];
+ if (saved != kUnsetMirrorAngle && (!m.isRotatable() || isAngleWithinLimits(m, saved))) {
+ m.angle = saved;
+ }
+ }
+ }
+
+ // Each overlay zone draws from its own image.
+ uint numOverlays = 0;
+ for (const ActionZone &z : _zones) {
+ if (z.type == kZoneOverlay && !z.overlayName.empty() && !z.overlaySrcRects.empty()) {
+ ++numOverlays;
+ }
+ }
+
+ _overlays.resize(numOverlays);
+ uint32 now = g_nancy->getTotalPlayTime();
+ uint overlayIndex = 0;
for (uint i = 0; i < _zones.size(); ++i) {
- if (_zones[i].specialEffectId >= 1000) {
- _detectorRect = _zones[i].rect;
- _winScene.sceneID = _zones[i].specialEffectId;
- break;
+ const ActionZone &z = _zones[i];
+ if (z.type != kZoneOverlay || z.overlayName.empty() || z.overlaySrcRects.empty()) {
+ continue;
+ }
+
+ ZoneOverlay &overlay = _overlays[overlayIndex++];
+ overlay.zoneIndex = i;
+ overlay.nextFrameTime = now + kOverlayFrameTime;
+ g_nancy->_resource->loadImage(Common::Path(z.overlayName), overlay.image);
+ if (z.overlayPlayMode != 1) {
+ overlay.image.setTransparentColor(_drawSurface.getTransparentColor());
}
}
+ _zoneOccupied.resize(_zones.size(), false);
+
traceBeam();
+ updateZones();
redraw();
}
@@ -348,24 +443,48 @@ void MirrorLightPuzzle::execute() {
registerGraphics();
_state = kRun;
// fall through
- case kRun:
+ case kRun: {
+ // Solving shows one frame with the target lit, then triggers.
if (_solved) {
- // Hold on the lit-bulb win state briefly before releasing.
- if (_solvedTime == 0) {
- _solvedTime = g_nancy->getTotalPlayTime();
- } else if (g_nancy->getTotalPlayTime() - _solvedTime > 1500) {
- _state = kActionTrigger;
+ _state = kActionTrigger;
+ break;
+ }
+
+ uint32 now = g_nancy->getTotalPlayTime();
+ if (_rotateDir != 0 && _hoveredMirror != -1 && now >= _nextRotateTime) {
+ rotateMirror(_hoveredMirror, _rotateDir);
+ _nextRotateTime = now + kRotateTickTime;
+ }
+
+ bool advanced = false;
+ for (ZoneOverlay &overlay : _overlays) {
+ if (_zoneOccupied[overlay.zoneIndex] && now >= overlay.nextFrameTime) {
+ overlay.frame = (overlay.frame + 1) % _zones[overlay.zoneIndex].overlaySrcRects.size();
+ overlay.nextFrameTime = now + kOverlayFrameTime;
+ advanced = true;
}
}
+ if (advanced) {
+ redraw();
+ }
break;
- case kActionTrigger:
- // The detector zone's SpecialEffect id is the win scene.
- if (_solved && _winScene.sceneID >= 1000 && _winScene.sceneID != kNoScene) {
- NancySceneState.changeScene(_winScene);
+ }
+ case kActionTrigger: {
+ // The scene-change zone carries the target scene as its special effect id,
+ // plus the special effect that covers the change.
+ const ActionZone &z = _zones[_winZone];
+ if (z.specialEffectId != kNoScene) {
+ if (z.hasSpecialEffect) {
+ NancySceneState.specialEffect(z.seType, z.seTotalTime, z.seFadeToBlackTime, z.seRect);
+ }
+ SceneChangeDescription sceneChange;
+ sceneChange.sceneID = z.specialEffectId;
+ NancySceneState.changeScene(sceneChange);
}
finishExecution();
break;
}
+ }
}
void MirrorLightPuzzle::handleInput(NancyInput &input) {
@@ -373,25 +492,45 @@ void MirrorLightPuzzle::handleInput(NancyInput &input) {
return;
}
- for (uint i = 0; i < _mirrors.size(); ++i) {
- Mirror &m = _mirrors[i];
- if (m.minAngle == m.maxAngle) {
- continue; // fixed mirror
+ // A held button keeps turning its mirror, even once the pointer leaves it.
+ if (_rotateDir != 0) {
+ uint16 held = _rotateDir > 0 ? NancyInput::kLeftMouseButtonHeld : NancyInput::kRightMouseButtonHeld;
+ if (input.input & held) {
+ g_nancy->_cursor->setCursorType(CursorManager::kRotateRight);
+ return;
}
+ _rotateDir = 0;
+ }
- Common::Rect screenRect = NancySceneState.getViewport().convertViewportToScreen(m.destRect);
- if (!screenRect.contains(input.mousePos)) {
+ _hoveredMirror = -1;
+ for (uint i = 0; i < _mirrors.size(); ++i) {
+ if (!_mirrors[i].isRotatable()) {
continue;
}
- // Clicking the right half rotates one way, the left half the other.
- bool clockwise = input.mousePos.x >= (screenRect.left + screenRect.right) / 2;
- g_nancy->_cursor->setCursorType(clockwise ? CursorManager::kRotateRight : CursorManager::kRotateLeft);
- if (input.input & NancyInput::kLeftMouseButtonUp) {
- rotateMirror(i, clockwise);
+ Common::Rect screenRect = NancySceneState.getViewport().convertViewportToScreen(_mirrors[i].destRect);
+ if (screenRect.contains(input.mousePos)) {
+ _hoveredMirror = (int16)i;
+ break;
}
+ }
+
+ if (_hoveredMirror == -1) {
+ return;
+ }
+
+ g_nancy->_cursor->setCursorType(CursorManager::kRotateRight);
+
+ // Holding the left button turns the mirror one way, the right button the other.
+ // The first turn comes after a short delay, so a quick click does nothing.
+ if (input.input & NancyInput::kLeftMouseButtonDown) {
+ _rotateDir = 1;
+ } else if (input.input & NancyInput::kRightMouseButtonDown) {
+ _rotateDir = -1;
+ } else {
return;
}
+ _nextRotateTime = g_nancy->getTotalPlayTime() + kRotateDelay;
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/mirrorlightpuzzle.h b/engines/nancy/action/puzzle/mirrorlightpuzzle.h
index 1814cbc4815..b2ad35d7c34 100644
--- a/engines/nancy/action/puzzle/mirrorlightpuzzle.h
+++ b/engines/nancy/action/puzzle/mirrorlightpuzzle.h
@@ -29,9 +29,9 @@
namespace Nancy {
namespace Action {
-// Light-reflection puzzle introduced in Nancy12 (AR 163). The player rotates a
-// set of mirrors within their angle limits to route a beam of light from a
-// source to a target.
+// Light-reflection puzzle introduced in Nancy12 (AR 163). The player turns a set
+// of mirrors within their angle limits to route a beam of light from a source to
+// a target zone.
class MirrorLightPuzzle : public RenderActionRecord {
public:
MirrorLightPuzzle() : RenderActionRecord(7) {}
@@ -55,6 +55,17 @@ protected:
double angle = 0.0; // current angle (radians)
double minAngle = 0.0; // rotation limits (min == max == fixed mirror)
double maxAngle = 0.0;
+ double step = 0.0; // signed turn per tick; flips when a turn would cross a limit
+
+ bool isRotatable() const { return minAngle != maxAngle; }
+ };
+
+ // An overlay zone's looping animation, drawn while the beam ends inside the zone.
+ struct ZoneOverlay {
+ uint zoneIndex = 0;
+ Graphics::ManagedSurface image;
+ uint frame = 0;
+ uint32 nextFrameTime = 0;
};
// File data
@@ -63,7 +74,10 @@ protected:
int16 _beamAngle = 0; // initial beam angle (degrees)
int32 _beamOriginX = 0;
int32 _beamOriginY = 0;
- int16 _glowRadius = 0; // beam-glow half-width in pixels
+ byte _beamColor[3] = {}; // r, g, b
+ int16 _beamHalfWidth = 0; // the beam is drawn this many pixels to each side of its center line
+ double _beamCenterOpacity = 0.0;
+ double _beamEdgeOpacity = 0.0;
// Mirror sprite frames - the mirror appearance at each of kNumFrames angles
// (full turn split evenly), indexed by angle.
@@ -73,25 +87,31 @@ protected:
Common::Array<ActionZone> _zones;
- // Derived from the detector zone (the SpecialEffect zone at the bulb): its
- // rect is the target the beam must reach, its specialEffectId is the win scene.
- Common::Rect _detectorRect;
- SceneChangeDescription _winScene;
-
// Runtime state
- int16 _pickedUpMirror = -1;
+ int16 _hoveredMirror = -1;
+ int _rotateDir = 0; // +1 while the left button turns a mirror, -1 for the right one
+ uint32 _nextRotateTime = 0;
+ Common::Array<bool> _zoneOccupied; // per zone: the beam currently ends inside it
+ int _winZone = -1; // the scene-change zone the beam reached
bool _solved = false;
- uint32 _solvedTime = 0; // ms timestamp when solved, for the win hold
Common::Array<Common::Point> _beamPath; // traced beam polyline, in viewport coords
+ Common::Array<ZoneOverlay> _overlays;
Graphics::ManagedSurface _image;
+ bool isAngleWithinLimits(const Mirror &m, double angle) const;
uint frameForAngle(double angle) const;
void drawMirror(uint index);
- void rotateMirror(uint index, bool clockwise);
+ void rotateMirror(uint index, int dir);
+ void saveMirrorAngles();
void traceBeam();
- void drawBeamGlow();
+ void updateZones();
+ void blendBeamPixel(int x, int y, double opacity);
+ void drawBeamLine(Common::Point p0, Common::Point p1, double opacity);
+ void drawBeam();
+ void drawOverlays();
void redraw();
+ void playSoundBlock(const RandomSoundBlock &block);
};
} // End of namespace Action
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index a0697870b99..918e3d656ab 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -716,6 +716,15 @@ void DrivingData::synchronize(Common::Serializer &ser) {
ser.syncAsByte(infiniteFuel, 8);
}
+void MirrorLightData::synchronize(Common::Serializer &ser) {
+ uint16 num = (uint16)angles.size();
+ ser.syncAsUint16LE(num);
+ if (ser.isLoading())
+ angles.resize(num);
+ for (uint i = 0; i < num; ++i)
+ ser.syncAsDoubleLE(angles[i]);
+}
+
void BuildPuzzleData::synchronize(Common::Serializer &ser) {
ser.syncAsUint16LE(sceneID);
ser.syncAsSint16LE(placedCount);
@@ -729,6 +738,8 @@ PuzzleData *makePuzzleData(const uint32 tag) {
switch(tag) {
case BuildPuzzleData::getTag():
return new BuildPuzzleData();
+ case MirrorLightData::getTag():
+ return new MirrorLightData();
case DrivingData::getTag():
return new DrivingData();
case WordFindPuzzleData::getTag():
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index f8406b5d61f..dd739c748e5 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -501,6 +501,19 @@ struct DrivingData : public PuzzleData {
bool infiniteFuel = false; // cheat toggle, kept across building visits
};
+// Nancy12 MirrorLightPuzzle (AR 163). The angle of each mirror, so a mirror stays
+// where the player turned it when the puzzle scene is left and re-entered.
+// An angle of -1 marks a mirror that was never saved, which keeps its initial angle.
+struct MirrorLightData : public PuzzleData {
+ MirrorLightData() {}
+ virtual ~MirrorLightData() {}
+
+ static constexpr uint32 getTag() { return MKTAG('M', 'I', 'R', 'L'); }
+ virtual void synchronize(Common::Serializer &ser);
+
+ Common::Array<double> angles; // radians, indexed by mirror
+};
+
// Nancy14 BuildPuzzle (AR 166). The board as it was after the last drop. A puzzle
// scene that re-runs picks it back up, as long as it is still the last build
// puzzle entered and its resume flag is set; otherwise the puzzle starts over.
diff --git a/engines/nancy/ui/viewport.h b/engines/nancy/ui/viewport.h
index 319b6bf0d59..ee9d5524b27 100644
--- a/engines/nancy/ui/viewport.h
+++ b/engines/nancy/ui/viewport.h
@@ -72,7 +72,7 @@ public:
uint16 getMaxScroll() const;
// The currently-visible scene background, in viewport-local coords. Used by
- // puzzles that composite additively over the background (e.g. MirrorLight).
+ // puzzles that blend over the background (e.g. MirrorLight).
const Graphics::ManagedSurface &getBackground() const { return _drawSurface; }
Common::Rect convertViewportToScreen(const Common::Rect &viewportRect) const;
More information about the Scummvm-git-logs
mailing list