[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