[Scummvm-git-logs] scummvm master -> c9df87accbb3c8da4b647a521f45123c63543d7b
bluegr
noreply at scummvm.org
Sun Aug 2 23:09:22 UTC 2026
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
b0d53868bc NANCY: NANCY12: Document and clean up ActionZone
f15f03ac26 NANCY: NANCY12: Implement the rest of the DrivingPuzzle functionality
c9df87accb NANCY: Filter out more invalid file name chars in actionrecord_export
Commit: b0d53868bcc714479e400fd82fd3ce93202e7606
https://github.com/scummvm/scummvm/commit/b0d53868bcc714479e400fd82fd3ce93202e7606
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-03T02:09:11+03:00
Commit Message:
NANCY: NANCY12: Document and clean up ActionZone
Changed paths:
engines/nancy/action/actionzone.cpp
engines/nancy/action/actionzone.h
engines/nancy/action/puzzle/minigolfpuzzle.cpp
engines/nancy/action/puzzle/pachinkopuzzle.cpp
engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
diff --git a/engines/nancy/action/actionzone.cpp b/engines/nancy/action/actionzone.cpp
index 1002b7f9aad..bf16c6bf8bd 100644
--- a/engines/nancy/action/actionzone.cpp
+++ b/engines/nancy/action/actionzone.cpp
@@ -20,6 +20,7 @@
*/
#include "common/stream.h"
+#include "common/util.h"
#include "engines/nancy/util.h"
#include "engines/nancy/action/actionzone.h"
@@ -27,10 +28,13 @@
namespace Nancy {
namespace Action {
+// Terminator in place of a special effect's type byte, meaning "no effect follows"
+static const byte kNoSpecialEffect = 0xff;
+
void ActionZone::readData(Common::SeekableReadStream &stream, bool isNancy13) {
// Base ActionZone fields, shared by every subtype.
typeField = stream.readSint32LE();
- type = typeField & 0xFF;
+ type = (ActionZoneType)(typeField & 0xFF);
readRect(stream, rect);
readFilename(stream, ovlName);
@@ -55,53 +59,54 @@ void ActionZone::readData(Common::SeekableReadStream &stream, bool isNancy13) {
// Subtype-specific trailing data. Fields not yet needed are skipped to keep the stream
// aligned. The Nancy12 and Nancy13 layouts are identical apart from three subtypes
-// (0x0d / 0x15 / 0x16), which branch on isNancy13.
+// (overlay, unknown 0x15 and bumper), which branch on isNancy13.
void ActionZone::readSubtype(Common::SeekableReadStream &stream, bool isNancy13) {
switch (type) {
- case 1: // base only
- case 5:
- case 0x10:
- case 0x14: // Boundary
- break;
- case 0: // Special Effect (and movement variants) - peeked terminator
- case 0x11:
- case 0x12:
- case 0x13:
+ case kZoneSpecialEffect:
+ case kZoneDestination:
+ case kZoneUnknown12:
+ case kZoneUnknown13:
readSpecialEffect(stream);
break;
- case 0x0b: // collision zone: event-flag id + on/off
- tailId = stream.readSint16LE();
- tailFlag = stream.readByte();
+ case kZoneBaseOnly:
+ case kZoneUnknown05:
+ case kZoneUnknown10:
+ case kZoneBoundary:
+ // No trailing data.
break;
- case 0x0e:
- stream.skip(2); // int16
- break;
- case 0x0f:
- stream.skip(4); // int16 + int16
+ case kZoneTeleport:
+ readRect(stream, exitRect);
+ teleportDelay = stream.readSint32LE();
+ exitAngle = stream.readSint16LE();
+ exitSpeed = stream.readSint16LE();
break;
- case 3: // terrain (sand): extra deceleration while the ball is inside
+ case kZoneTerrain:
terrainDecel = stream.readDoubleLE();
break;
- case 0x17: // Flat Tire (min/max) - Nancy12 only
- stream.skip(8); // int32 + int32
- break;
- case 4: // slope: a velocity kick of slopeForce along slopeAngle
+ case kZoneSlope:
slopeForce = stream.readDoubleLE();
slopeAngle = stream.readSint16LE();
break;
- case 2: // teleport / pipe: exit rect + hold time + exit angle/speed
- readRect(stream, exitRect);
- teleportDelay = stream.readSint32LE();
- exitAngle = stream.readSint16LE();
- exitSpeed = stream.readSint16LE();
+ case kZoneEventFlag:
+ tailId = stream.readSint16LE();
+ tailFlag = stream.readByte();
break;
- case 0x0c: // trigger zone: special effect + target scene id + flag
+ case kZoneSceneChange:
readSpecialEffect(stream);
tailId = stream.readSint16LE();
tailFlag = stream.readByte();
break;
- case 0x15:
- // Nancy12: special effect + a trailing int32. Nancy13: the flat-tire zone
+ case kZoneOverlay:
+ readOverlayZone(stream, isNancy13);
+ break;
+ case kZoneUnknown0E:
+ stream.skip(2); // int16
+ break;
+ case kZoneUnknown0F:
+ stream.skip(4); // int16 + int16
+ break;
+ case kZoneUnknown15:
+ // Nancy12: special effect + a trailing int32. Nancy13: a damage range
// (min/max int32), with no special effect.
if (isNancy13) {
stream.skip(8);
@@ -110,11 +115,8 @@ void ActionZone::readSubtype(Common::SeekableReadStream &stream, bool isNancy13)
stream.skip(4);
}
break;
- case 0x0d: // OverlayZone (Nancy13 carries one extra int32)
- readOverlayZone(stream, isNancy13);
- break;
- case 0x16:
- // Nancy12: OverlayZone + int32. Nancy13: a short bumper record (two bytes + int16).
+ case kZoneBumper:
+ // Nancy12: an OverlayZone + int32. Nancy13: a short record (two bytes + int16).
if (isNancy13) {
stream.skip(4);
} else {
@@ -122,6 +124,13 @@ void ActionZone::readSubtype(Common::SeekableReadStream &stream, bool isNancy13)
stream.skip(4);
}
break;
+ case kZoneFlatTire: // Nancy12 only
+ flatTireMin = stream.readSint32LE();
+ flatTireMax = stream.readSint32LE();
+ if (flatTireMax < flatTireMin) {
+ SWAP(flatTireMin, flatTireMax);
+ }
+ break;
default:
warning("ActionZone: unknown type %d - chunk may desync", type);
break;
@@ -129,13 +138,13 @@ void ActionZone::readSubtype(Common::SeekableReadStream &stream, bool isNancy13)
}
// Special Effect block: an int16 id (a target scene on transition zones), then the
-// effect type byte. If the type is the 0xff terminator the effect is absent;
-// otherwise a 21-byte SpecialEffect record follows (type + totalTime +
-// fadeToBlackTime + Rect), matching the standalone SpecialEffect action record.
+// effect type byte. If the type is the terminator the effect is absent; otherwise a
+// 21-byte SpecialEffect record follows (type + totalTime + fadeToBlackTime + Rect),
+// matching the standalone SpecialEffect action record.
void ActionZone::readSpecialEffect(Common::SeekableReadStream &stream) {
specialEffectId = stream.readUint16LE();
seType = stream.readByte();
- if (seType == 0xff) {
+ if (seType == kNoSpecialEffect) {
seType = 0;
return;
}
diff --git a/engines/nancy/action/actionzone.h b/engines/nancy/action/actionzone.h
index ae3edc77cf4..96d255e6a85 100644
--- a/engines/nancy/action/actionzone.h
+++ b/engines/nancy/action/actionzone.h
@@ -35,13 +35,38 @@ class SeekableReadStream;
namespace Nancy {
namespace Action {
+// The zone's subtype, stored in the low byte of its leading int32. It selects both
+// what the zone does at runtime and how much trailing data follows the shared base.
+// Subtypes whose meaning has not been worked out yet are named after their value;
+// those still read (or skip) the right number of bytes.
+enum ActionZoneType : byte {
+ kZoneSpecialEffect = 0x00, // carries a scene id plus an optional fade
+ kZoneBaseOnly = 0x01, // base fields only, no behavior of its own
+ kZoneTeleport = 0x02, // pipe: swallows the ball and re-emits it elsewhere
+ kZoneTerrain = 0x03, // sand trap / mud puddle: slows whatever is inside
+ kZoneSlope = 0x04, // hill: a velocity kick while crossing the zone
+ kZoneUnknown05 = 0x05, // base fields only
+ kZoneEventFlag = 0x0b, // checkpoint / sewing seam: sets an event flag
+ kZoneSceneChange = 0x0c, // finish line / golf cup: fade + target scene + flag
+ kZoneOverlay = 0x0d, // cosmetic overlay sprite
+ kZoneUnknown0E = 0x0e,
+ kZoneUnknown0F = 0x0f,
+ kZoneUnknown10 = 0x10, // base fields only
+ kZoneDestination = 0x11, // driving: a location entrance the car can park in
+ 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
+ kZoneBumper = 0x16, // Nancy12: an overlay variant; Nancy13: a pachinko hole
+ kZoneFlatTire = 0x17 // pothole: damages the car driving over it
+};
+
// A polymorphic "ActionZone" record, embedded as a count-prefixed array inside
// every Nancy12 PuzzleBase puzzle (Driving/Minigolf/MirrorLight/BoardGame/Mind/
-// Chase). The zone's type is the low byte of its leading int32; each subtype
-// reads a different amount of trailing data. readData() consumes exactly the
-// right number of bytes for the type so the surrounding chunk stays in sync.
+// Chase). Each subtype reads a different amount of trailing data; readData()
+// consumes exactly the right number of bytes so the surrounding chunk stays in sync.
struct ActionZone {
- byte type = 0;
+ ActionZoneType type = kZoneSpecialEffect;
int32 typeField = 0; // full leading int32; low byte == type
Common::Rect rect;
@@ -57,7 +82,7 @@ struct ActionZone {
RandomSoundBlock _sound;
// Special Effect (an embedded 21-byte SpecialEffect record, present on the
- // special-effect subtypes when the effect byte is not the 0xff terminator).
+ // special-effect subtypes when the effect byte is not the terminator).
uint16 specialEffectId = 0;
bool hasSpecialEffect = false;
byte seType = 0; // 1 = blackout, 2 = cross-dissolve, 3 = through-black
@@ -65,37 +90,41 @@ struct ActionZone {
uint16 seFadeToBlackTime = 0;
Common::Rect seRect;
- // int16 + byte trailer carried by the collision (0x0b) and trigger (0x0c)
- // subtypes. For 0x0b it is an event-flag id + on/off; for 0x0c it is a target
- // scene id + a flag. Left at their defaults for the other subtypes.
+ // int16 + byte trailer carried by kZoneEventFlag and kZoneSceneChange. For the
+ // former it is an event-flag id + on/off; for the latter a target scene id + a
+ // flag. Left at their defaults for the other subtypes.
int16 tailId = 0;
byte tailFlag = 0;
- // Teleport / pipe subtype (0x02): the ball entering this zone is held for
- // teleportDelay ms, then re-emerges at exitRect travelling at exitSpeed along
- // exitAngle (degrees).
+ // kZoneTeleport: the ball entering this zone is held for teleportDelay ms, then
+ // re-emerges at exitRect travelling at exitSpeed along exitAngle (degrees).
Common::Rect exitRect;
int32 teleportDelay = 0;
int16 exitAngle = 0;
int16 exitSpeed = 0;
- // Terrain subtype (0x03, e.g. a sand trap): extra deceleration added to the
- // ball's friction while it is inside this zone.
+ // kZoneTerrain: extra deceleration added to the moving object's friction while
+ // it is inside this zone.
double terrainDecel = 0.0;
- // Slope subtype (0x04, e.g. a sloped green): a velocity kick of slopeForce along
- // slopeAngle (degrees) applied on entering the zone and removed on leaving.
+ // kZoneFlatTire: entering the zone adds a random amount of damage in
+ // [flatTireMin, flatTireMax].
+ int32 flatTireMin = 0;
+ int32 flatTireMax = 0;
+
+ // kZoneSlope: a velocity kick of slopeForce along slopeAngle (degrees), applied
+ // on entering the zone and removed on leaving.
double slopeForce = 0.0;
int16 slopeAngle = 0;
- // OverlayZone subtypes (0x0d / 0x16)
+ // kZoneOverlay (and kZoneBumper in Nancy12)
Common::String overlayName;
Common::Array<Common::Rect> overlaySrcRects;
Common::Rect overlayDestRect;
// The Nancy13 pinball layout (AR 175) differs from the Nancy12 one: the base carries an
- // extra int32 before the sound block, and subtypes 0x0d/0x15/0x16 have different trailers.
- // Pass isNancy13 = true to parse it; the default keeps the Nancy12 behaviour.
+ // extra int32 before the sound block, and the overlay/unknown-0x15/bumper subtypes have
+ // different trailers. Pass isNancy13 = true to parse it; the default keeps Nancy12.
void readData(Common::SeekableReadStream &stream, bool isNancy13 = false);
private:
diff --git a/engines/nancy/action/puzzle/minigolfpuzzle.cpp b/engines/nancy/action/puzzle/minigolfpuzzle.cpp
index b514b832a56..8eac66e4088 100644
--- a/engines/nancy/action/puzzle/minigolfpuzzle.cpp
+++ b/engines/nancy/action/puzzle/minigolfpuzzle.cpp
@@ -53,10 +53,6 @@ static const double kRestSpeed = 0.5; // stop the ball below this per-step spee
static const double kSinkSpeed = 50.0; // ball sinks only if it reaches the cup at or below this speed; faster rolls over
static const double kRestitution = 0.8; // wall-bounce energy retained
static const double kDefaultAimDrag = 24.0; // default aim-cursor distance from the ball (mask px)
-static const byte kZoneTeleport = 2; // ActionZone subtype for a pipe (enter one hole, exit another)
-static const byte kZoneTerrain = 3; // ActionZone subtype for terrain (a sand trap that adds friction)
-static const byte kZoneSlope = 4; // ActionZone subtype for a slope (a velocity kick toward its angle)
-static const byte kZoneOverlay = 0xd; // ActionZone subtype for a cosmetic overlay sprite
// Isometric projection (mask space -> screen): rotate 45 degrees, foreshorten Y by
// half. cos45 == sin45; the Y component is additionally scaled by kIsoYScale.
@@ -627,7 +623,7 @@ void MinigolfPuzzle::updateBall() {
playSoundBlock(_sinkSound);
_winScene.sceneID = cup.specialEffectId;
- if (cup.type == 0x0c && cup.tailId != -1) {
+ if (cup.type == kZoneSceneChange && cup.tailId != -1) {
NancySceneState.setEventFlag(cup.tailId, cup.tailFlag ? g_nancy->_true : g_nancy->_false);
}
if (cup.hasSpecialEffect) {
diff --git a/engines/nancy/action/puzzle/pachinkopuzzle.cpp b/engines/nancy/action/puzzle/pachinkopuzzle.cpp
index 09e0ffb1042..c6ec92b8616 100644
--- a/engines/nancy/action/puzzle/pachinkopuzzle.cpp
+++ b/engines/nancy/action/puzzle/pachinkopuzzle.cpp
@@ -180,13 +180,13 @@ void PachinkoPuzzle::loadMachineImage(Machine &m) {
}
}
-// The four holes are the bumper zones (type 0x16) that carry a bell sound. The sound name
-// maps each hole to its climber: Miner/Explosion feed the Gold Digger (win), Yeti/Ouch feed
-// the Yeti (lose).
+// The four holes are the bumper zones that carry a bell sound. The sound name maps each
+// hole to its climber: Miner/Explosion feed the Gold Digger (win), Yeti/Ouch feed the
+// Yeti (lose).
void PachinkoPuzzle::buildHoles() {
_holes.clear();
for (const ActionZone &z : _zones) {
- if (z.type != 0x16 || z._sound.names.empty()) {
+ if (z.type != kZoneBumper || z._sound.names.empty()) {
continue;
}
@@ -212,10 +212,10 @@ void PachinkoPuzzle::buildHoles() {
_holes.push_back(hole);
}
- // Attach each hole's "lit" sprite from the matching overlay (0x0d) zone, which shares
- // the hole's rect and names the lit board overlay.
+ // Attach each hole's "lit" sprite from the matching overlay zone, which shares the
+ // hole's rect and names the lit board overlay.
for (const ActionZone &z : _zones) {
- if (z.type != 0x0d || z.overlaySrcRects.empty()) {
+ if (z.type != kZoneOverlay || z.overlaySrcRects.empty()) {
continue;
}
for (Hole &hole : _holes) {
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
index 03a0d54e298..82b0e26f389 100644
--- a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
@@ -66,13 +66,13 @@ void SewingMachinePuzzle::classifyZones() {
for (uint i = 0; i < _zones.size(); ++i) {
const ActionZone &z = _zones[i];
switch (z.type) {
- case 0x0b: // seam mask + mistake lines
+ case kZoneEventFlag: // seam mask + mistake lines
_collisionZone = i;
break;
- case 0x0c: // bottom completion trigger
+ case kZoneSceneChange: // bottom completion trigger
_triggerZones.push_back(i);
break;
- case 0x14: // play-area boundary
+ case kZoneBoundary: // play-area boundary
_boundaryZone = i;
break;
default:
Commit: f15f03ac26f24a98eb47b28981c991bfa539af25
https://github.com/scummvm/scummvm/commit/f15f03ac26f24a98eb47b28981c991bfa539af25
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-03T02:09:12+03:00
Commit Message:
NANCY: NANCY12: Implement the rest of the DrivingPuzzle functionality
The driving and car chase scenes are playable now
Key changes:
- Persist driving state
- Add handling for pressing space to enter buildings
- Fix physics (car movement and speed)
- Use the scene collision mask
- Fix car controls
- Fix fuel handling
- Wire up flat tire incidents
- Add scene conditional decorations
- Implement mud puddles and potholes
- Update gas/tire gauge overlays correctly
Changed paths:
engines/nancy/action/overlay.cpp
engines/nancy/action/puzzle/drivingpuzzle.cpp
engines/nancy/action/puzzle/drivingpuzzle.h
engines/nancy/puzzledata.cpp
engines/nancy/puzzledata.h
diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index 41828e3db1e..ddb0ff76a28 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -76,6 +76,14 @@ void Overlay::handleInput(NancyInput &input) {
}
void Overlay::updateGraphics() {
+ // A static overlay gated by a dynamic dependency - e.g. a Nancy12 gas/tire gauge,
+ // which is one of a set of sprites each shown for a different resource range - must be
+ // visible only while its dependency currently holds. (With the usual set-once event
+ // flags _isActive never drops back to false, so this is a no-op there.)
+ if (g_nancy->getGameType() >= kGameTypeNancy12 && _state == kRun && _overlayType == kPlayOverlayStatic) {
+ setVisible(_isActive);
+ }
+
// Update inactive animated overlays
if (!_isActive && _state == kRun && !_blitDescriptions.empty() && _overlayType == kPlayOverlayAnimated) {
uint16 newFrame = NancySceneState.getSceneInfo().frameID;
diff --git a/engines/nancy/action/puzzle/drivingpuzzle.cpp b/engines/nancy/action/puzzle/drivingpuzzle.cpp
index 7d99c38a672..7a1b0ce963e 100644
--- a/engines/nancy/action/puzzle/drivingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/drivingpuzzle.cpp
@@ -28,6 +28,7 @@
#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/drivingpuzzle.h"
@@ -78,7 +79,7 @@ void DrivingPuzzle::readBlob(Common::SeekableReadStream &stream) {
stream.skip(2);
_distanceDivisor = stream.readSint32LE(); // 0x7b
_retainState = stream.readByte() != 0; // 0x7f
- stream.skip(2);
+ _finishScene = stream.readUint16LE(); // 0x80: the scene to enter when a tire goes flat
}
void DrivingPuzzle::readData(Common::SeekableReadStream &stream) {
@@ -115,16 +116,17 @@ void DrivingPuzzle::readData(Common::SeekableReadStream &stream) {
}
void DrivingPuzzle::classifyZones(const Common::Array<ActionZone> &zones) {
- // The car often spawns already sitting inside its starting location's zone, so
- // zones are edge-triggered: a zone only fires once the car has left it and driven
- // back in. Seed each zone's "inside" state from the spawn point.
- Common::Point spawn(_startX, _startY);
+ // The car often starts already sitting inside a location's zone (its start position,
+ // or a restored parking spot), so zones are edge-triggered: a zone only fires once the
+ // car has left it and driven back in. Seed each zone's "inside" state from where the
+ // car currently is (init() has already restored any saved position).
+ Common::Point spawn((int)(_carX + 0.5), (int)(_carY + 0.5));
for (uint i = 0; i < zones.size(); ++i) {
const ActionZone &z = zones[i];
switch (z.type) {
- case 0x11: // location entrance
- case 0x0c: { // chase finish line
+ case kZoneDestination: // location entrance
+ case kZoneSceneChange: { // drive-in scene trigger (chase)
DestinationZone dest;
dest.rect = z.rect;
dest.scene.sceneID = z.specialEffectId;
@@ -135,15 +137,16 @@ void DrivingPuzzle::classifyZones(const Common::Array<ActionZone> &zones) {
dest.fadeToBlackTime = z.seFadeToBlackTime;
dest.fadeRect = z.seRect;
}
- if (z.type == 0x0c) {
+ if (z.type == kZoneSceneChange) {
dest.eventFlag = z.tailId;
dest.eventFlagValue = z.tailFlag;
+ dest.autoTrigger = true; // the chase finish line fires on drive-in, no spacebar
}
dest.carInside = dest.rect.contains(spawn);
_destinations.push_back(dest);
break;
}
- case 0x0b: { // checkpoint: sets an event flag once driven over
+ case kZoneEventFlag: { // checkpoint: sets an event flag once driven over
Checkpoint cp;
cp.rect = z.rect;
cp.flagId = z.tailId;
@@ -152,12 +155,39 @@ void DrivingPuzzle::classifyZones(const Common::Array<ActionZone> &zones) {
_checkpoints.push_back(cp);
break;
}
- case 0x14: // play-area boundary
- _boundaries.push_back(z.rect);
+ case kZoneTerrain: { // mud puddle: slows the car while inside
+ MudZone mud;
+ mud.rect = z.rect;
+ mud.decel = z.terrainDecel;
+ _mudZones.push_back(mud);
+ break;
+ }
+ case kZoneFlatTire: { // pothole: damages the tires on entry
+ Pothole hole;
+ hole.rect = z.rect;
+ hole.minDamage = z.flatTireMin;
+ hole.maxDamage = z.flatTireMax;
+ hole.carInside = hole.rect.contains(spawn);
+ _potholes.push_back(hole);
+ break;
+ }
+ case kZoneOverlay: { // cosmetic map decoration (buildings, cars, potholes, animations)
+ if (z.overlayName.empty() || z.overlaySrcRects.empty() || z.overlayDestRect.isEmpty()) {
+ break;
+ }
+ Overlay ov;
+ ov.imageIndex = overlayImageIndex(z.overlayName);
+ ov.srcRects = z.overlaySrcRects;
+ ov.destRect = z.overlayDestRect;
+ ov.condFlag = z.val49;
+ ov.condValue = z.val4b;
+ if (ov.imageIndex >= 0) {
+ _overlays.push_back(ov);
+ }
break;
+ }
default:
- // The remaining subtypes (overlays, driving hazards, terrain markers) are
- // not simulated yet.
+ // The remaining subtypes (terrain markers) are not simulated yet.
break;
}
}
@@ -184,6 +214,27 @@ void DrivingPuzzle::playSoundBlock(const RandomSoundBlock &block) {
g_nancy->_sound->playSound(desc);
}
+void DrivingPuzzle::armExit(const DestinationZone &dest) {
+ _exitScene = dest.scene;
+ _exitHasFade = dest.hasFade;
+ _exitFadeType = dest.fadeType;
+ _exitFadeTotalTime = dest.fadeTotalTime;
+ _exitFadeToBlackTime = dest.fadeToBlackTime;
+ _exitFadeRect = dest.fadeRect;
+ _exitFlag = dest.eventFlag;
+ _exitFlagValue = dest.eventFlagValue;
+ _state = kActionTrigger;
+}
+
+void DrivingPuzzle::armExitScene(uint16 sceneID, int16 flag, byte flagValue) {
+ _exitScene = SceneChangeDescription();
+ _exitScene.sceneID = sceneID;
+ _exitHasFade = false;
+ _exitFlag = flag;
+ _exitFlagValue = flagValue;
+ _state = kActionTrigger;
+}
+
void DrivingPuzzle::init() {
Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
_drawSurface.create(vpBounds.width(), vpBounds.height(),
@@ -203,12 +254,29 @@ void DrivingPuzzle::init() {
_chaseCarImage.setTransparentColor(_drawSurface.getTransparentColor());
}
+ // The collision mask marks the drivable road in white; everything else is off-road.
+ g_nancy->_resource->loadImage(_collisionName, _collisionMask);
+
// Seed the car from the physics parameters decoded from the header blob.
_carX = _startX;
_carY = _startY;
_carHeading = (double)_startAngle * (M_PI / 180.0);
_carVelocity = 0.0;
_speedCap = _forwardSpeed;
+ _lastPhysicsMs = 0;
+
+ // When the map keeps its state (retainState), resume from where the car was left the
+ // last time this map was driven (i.e. before entering a building), and keep the tire
+ // damage. The first ever visit starts from the header position.
+ if (_retainState) {
+ DrivingData *data = (DrivingData *)NancySceneState.getPuzzleData(DrivingData::getTag());
+ if (data && data->valid) {
+ _carX = data->carX;
+ _carY = data->carY;
+ _carHeading = data->heading;
+ _tireDamage = data->tireDamage;
+ }
+ }
// Seed the chaser at the start of its recorded path.
if (!_chaserPathA.empty()) {
@@ -242,6 +310,82 @@ Common::Point DrivingPuzzle::cameraOffset() const {
return Common::Point(camX, camY);
}
+int DrivingPuzzle::overlayImageIndex(const Common::String &name) {
+ for (uint i = 0; i < _overlayImageNames.size(); ++i) {
+ if (_overlayImageNames[i] == name) {
+ return (int)i;
+ }
+ }
+
+ Graphics::ManagedSurface surf;
+ g_nancy->_resource->loadImage(Common::Path(name), surf);
+ if (surf.empty()) {
+ return -1;
+ }
+ surf.setTransparentColor(_drawSurface.getTransparentColor());
+
+ _overlayImages.push_back(surf);
+ _overlayImageNames.push_back(name);
+ return (int)_overlayImages.size() - 1;
+}
+
+void DrivingPuzzle::drawOverlays(const Common::Point &cam) {
+ if (_overlays.empty()) {
+ return;
+ }
+
+ const int frameMs = 66; // decoration animation speed (0x42 ms/frame)
+ uint32 nowMs = g_system->getMillis();
+ Common::Rect view(cam.x, cam.y, cam.x + _drawSurface.w, cam.y + _drawSurface.h);
+
+ for (uint i = 0; i < _overlays.size(); ++i) {
+ const Overlay &ov = _overlays[i];
+ if (ov.imageIndex < 0 || !view.intersects(ov.destRect)) {
+ continue;
+ }
+
+ // Only draw the decoration while its event-flag condition holds (a car appears
+ // once its story flag is set, etc.).
+ if (ov.condFlag != -1 && !NancySceneState.getEventFlag(ov.condFlag, ov.condValue)) {
+ continue;
+ }
+
+ uint frame = ov.srcRects.size() == 1 ? 0 : (nowMs / frameMs) % ov.srcRects.size();
+ _drawSurface.blitFrom(_overlayImages[ov.imageIndex], ov.srcRects[frame],
+ Common::Point(ov.destRect.left - cam.x, ov.destRect.top - cam.y));
+ }
+}
+
+void DrivingPuzzle::saveState() const {
+ if (!_retainState) {
+ return;
+ }
+
+ DrivingData *data = (DrivingData *)NancySceneState.getPuzzleData(DrivingData::getTag());
+ if (data) {
+ data->valid = true;
+ data->carX = (int32)(_carX + 0.5);
+ data->carY = (int32)(_carY + 0.5);
+ data->heading = _carHeading;
+ data->tireDamage = _tireDamage;
+ }
+}
+
+bool DrivingPuzzle::isWall(int px, int py) const {
+ if (px < 0 || py < 0 || px >= _collisionMask.w || py >= _collisionMask.h) {
+ return true; // off the map
+ }
+
+ // The road is white; anything darker is off-road.
+ byte r, g, b;
+ _collisionMask.format.colorToRGB(_collisionMask.getPixel(px, py), r, g, b);
+ return r < 128 || g < 128 || b < 128;
+}
+
+bool DrivingPuzzle::isBlocked(const Common::Point &p) const {
+ return isWall(p.x, p.y);
+}
+
void DrivingPuzzle::drawScene() {
Common::Point cam = cameraOffset();
int camX = cam.x;
@@ -249,6 +393,10 @@ void DrivingPuzzle::drawScene() {
_drawSurface.blitFrom(_image, Common::Rect(camX, camY, camX + _drawSurface.w, camY + _drawSurface.h), Common::Point(0, 0));
+ // Map decorations (buildings, cars, potholes, animated cows/flags) sit on the map,
+ // under the cars.
+ drawOverlays(cam);
+
// The chaser car (kChase), drawn under the player car.
if (_variant == kChase && !_frameRects2.empty() && _chaseCarImage.w > 0) {
const Common::Rect &src = _frameRects2[frameIndexForHeading(_chaserHeading, _frameRects2.size())];
@@ -269,7 +417,8 @@ void DrivingPuzzle::drawScene() {
}
void DrivingPuzzle::updateChaser() {
- if (_chaserPathA.empty()) {
+ const Common::Array<Waypoint> &path = _chaserOnPathB ? _chaserPathB : _chaserPathA;
+ if (path.empty()) {
return;
}
@@ -282,11 +431,11 @@ void DrivingPuzzle::updateChaser() {
// Play the recorded path back in real time: advance to the last waypoint whose
// timestamp the elapsed chase time has passed.
uint32 elapsed = g_system->getMillis() - _chaseStartTime;
- while (_chaserWaypoint + 1 < _chaserPathA.size() && _chaserPathA[_chaserWaypoint].timeMs < elapsed) {
+ while (_chaserWaypoint + 1 < path.size() && path[_chaserWaypoint].timeMs < elapsed) {
++_chaserWaypoint;
}
- const Waypoint &wp = _chaserPathA[_chaserWaypoint];
+ const Waypoint &wp = path[_chaserWaypoint];
_chaserX = wp.x;
_chaserY = wp.y;
_chaserHeading = wp.heading;
@@ -300,50 +449,170 @@ void DrivingPuzzle::updateChaser() {
const double slowSlope = 5.0;
_speedCap = dist >= slowRadius ? (double)_forwardSpeed : (double)_forwardSpeed - (slowRadius - dist) * slowSlope;
- // TODO: switch onto the second path (_chaserPathB), the event-flag outcomes
- // (_chaseParams) and the "chaser left the viewport" loss branch are not simulated.
+ // While Nancy is still pursuing Jane (states 0 and 1, before Jane is caught and the
+ // crash sequence on the second path begins), letting the chaser leave the visible map
+ // means the player has lost her. The original only tests this in state 0 because its
+ // state stays there through the whole pursuit; here state advances to 1 as soon as its
+ // gate flag is clear, so the check has to cover both.
+ if (_chaseState < 2) {
+ Common::Point cam = cameraOffset();
+ Common::Rect viewport(0, 0, _drawSurface.w, _drawSurface.h);
+ Common::Rect chaserRect(1, 1);
+ if (!_frameRects2.empty()) {
+ chaserRect = _frameRects2[0];
+ }
+ chaserRect.moveTo((int)(_chaserX + 0.5) - cam.x - chaserRect.width() / 2,
+ (int)(_chaserY + 0.5) - cam.y - chaserRect.height() / 2);
+ if (!chaserRect.intersects(viewport)) {
+ armExitScene(_chaseParams[kChaseOffViewScene], _chaseParams[kChaseOffViewFlag], 1);
+ return;
+ }
+ }
+
+ // Chase state machine (mirrors the original): flags gating it are set by the chase's
+ // own checkpoints (zones2 type 0x0b) and by the scene scripts.
+ switch (_chaseState) {
+ case 0:
+ if (_chaseParams[kChaseGate01Flag] != -1 &&
+ NancySceneState.getEventFlag(_chaseParams[kChaseGate01Flag], g_nancy->_false)) {
+ _chaseState = 1;
+ }
+ break;
+ case 1:
+ // Switch onto the second path once Jane is caught.
+ if (_chaseParams[kChaseGate12Flag] != -1 &&
+ NancySceneState.getEventFlag(_chaseParams[kChaseGate12Flag], g_nancy->_true)) {
+ _chaserOnPathB = true;
+ _chaserWaypoint = 0;
+ _chaseStarted = false;
+ _chaseState = 2;
+ }
+ break;
+ case 2:
+ // The chaser has completed its route (Jane crashes) - the win.
+ if (_chaserWaypoint + 1 >= path.size()) {
+ armExitScene(_chaseParams[kChasePathEndScene], -1, 0);
+ return;
+ }
+ break;
+ default:
+ break;
+ }
}
// Per-frame car physics. Throttle is +1 (forward), -1 (reverse) or 0 (coast); the car
-// already faces the cursor (steering happens in handleInput). The acceleration divisors
-// (1.0/0.4) and 0.02 timestep are exact; the velocity decay is a playability stand-in.
-void DrivingPuzzle::updatePhysics(int throttle) {
- const double timeStep = 0.02;
- const double decay = 0.98;
- const double forwardCap = MAX(0.0, _speedCap);
+// already faces the cursor (steering happens in handleInput). cursorDist is how far the
+// cursor is from the car, which sets the forward speed. Velocity is in pixels per second
+// and integrated against the real elapsed time, so the car covers the same distance per
+// second at any frame rate - the chaser plays back in real time, so a frame-rate-dependent
+// car speed made the chase unwinnable.
+void DrivingPuzzle::updatePhysics(int throttle, double cursorDist) {
+ const double refStep = 0.02; // the original's nominal per-frame step
+ const double decay = 0.98; // coast decay per refStep
+ const int kTireFlatThreshold = 100; // accumulated pothole damage that blows a tire
+
+ // Cursor distance at which the car reaches top speed. The chase needs Nancy to sustain
+ // full speed to keep pace with Jane's recorded run, so full throttle comes at a much
+ // shorter cursor distance there than in the fuel-economy-minded free-driving map.
+ const double kFullSpeedDist = (_variant == kChase) ? 120.0 : 250.0;
+
+ uint32 nowMs = g_system->getMillis();
+ double dt = (_lastPhysicsMs == 0) ? refStep : CLIP<double>((nowMs - _lastPhysicsMs) / 1000.0, 0.0, 0.1);
+ _lastPhysicsMs = nowMs;
+
+ // The further the cursor, the faster the car; the chase slowdown (_speedCap) caps it.
+ double distanceCap = CLIP<double>(cursorDist / kFullSpeedDist * _forwardSpeed, 0.0, (double)_forwardSpeed);
+ double forwardCap = MIN(distanceCap, MAX(0.0, _speedCap));
+
+ // Mud slows the car (it does not stop it): while sitting in a puddle its top speed is
+ // cut. Jane's car is a recorded playback that ignores the terrain, so the penalty is
+ // kept mild - enough to matter in the free-driving map without making the chase, where
+ // she never slows, unwinnable.
+ Common::Point cur((int)(_carX + 0.5), (int)(_carY + 0.5));
+ for (uint i = 0; i < _mudZones.size(); ++i) {
+ if (_mudZones[i].decel > 0.0 && _mudZones[i].rect.contains(cur)) {
+ forwardCap = MIN(forwardCap, (double)_forwardSpeed * 0.7);
+ break;
+ }
+ }
if (throttle > 0) {
- _carVelocity += (forwardCap / 1.0) * timeStep;
+ _carVelocity += forwardCap * dt;
} else if (throttle < 0) {
- _carVelocity -= ((double)_forwardSpeed / 0.4) * timeStep;
+ _carVelocity -= ((double)_forwardSpeed / 0.4) * dt;
} else {
- _carVelocity *= decay;
+ _carVelocity *= pow(decay, dt / refStep);
}
_carVelocity = CLIP<double>(_carVelocity, -(double)_reverseSpeed, forwardCap);
- double newX = _carX + cos(_carHeading) * _carVelocity * timeStep;
- double newY = _carY - sin(_carHeading) * _carVelocity * timeStep;
-
- // Keep the car on the map and out of the boundary zones (coarse rect test, no mask).
- newX = CLIP<double>(newX, 0.0, MAX(0, _image.w - 1));
- newY = CLIP<double>(newY, 0.0, MAX(0, _image.h - 1));
-
- Common::Point next((int)(newX + 0.5), (int)(newY + 0.5));
- for (uint i = 0; i < _boundaries.size(); ++i) {
- if (_boundaries[i].contains(next)) {
+ // Move against the collision mask, sub-stepped ~1px at a time so a fast car can't
+ // tunnel through a thin road edge. On hitting off-road, slide along it (keep whichever
+ // single axis stays on the road) instead of stopping dead. If the car somehow starts
+ // the frame off-road, let it move out freely so it can never get wedged.
+ double preX = _carX;
+ double preY = _carY;
+ double moveX = cos(_carHeading) * _carVelocity * dt;
+ double moveY = -sin(_carHeading) * _carVelocity * dt;
+ int steps = MAX(1, (int)(MAX(ABS(moveX), ABS(moveY)) + 0.5));
+ double stepX = moveX / steps;
+ double stepY = moveY / steps;
+ bool escaping = isBlocked(cur);
+
+ for (int s = 0; s < steps; ++s) {
+ double tryX = CLIP<double>(_carX + stepX, 0.0, MAX(0, _image.w - 1));
+ double tryY = CLIP<double>(_carY + stepY, 0.0, MAX(0, _image.h - 1));
+
+ if (escaping || !isBlocked(Common::Point((int)(tryX + 0.5), (int)(tryY + 0.5)))) {
+ _carX = tryX;
+ _carY = tryY;
+ } else if (!isBlocked(Common::Point((int)(tryX + 0.5), (int)(_carY + 0.5)))) {
+ _carX = tryX;
+ } else if (!isBlocked(Common::Point((int)(_carX + 0.5), (int)(tryY + 0.5)))) {
+ _carY = tryY;
+ } else {
_carVelocity = 0.0;
- return;
+ break;
}
}
- _carX = newX;
- _carY = newY;
+ Common::Point next((int)(_carX + 0.5), (int)(_carY + 0.5));
+
+ // The gas tank empties by the distance the car actually travels this frame divided by
+ // the header's distance divisor. The DT_RESOURCE scene dependency reads the same
+ // resource to warn Nancy when it runs low.
+ if (_distanceDivisor > 0) {
+ double moved = sqrt((_carX - preX) * (_carX - preX) + (_carY - preY) * (_carY - preY));
+ _fuelBurnAccum += moved / (double)_distanceDivisor;
+ if (_fuelBurnAccum >= 1.0) {
+ int burn = (int)_fuelBurnAccum;
+ _fuelBurnAccum -= burn;
+ int fuel = NancySceneState.getUIResource(_frictionIndex);
+ NancySceneState.setUIResource(_frictionIndex, MAX(0, fuel - burn));
+ }
+ }
- // TODO: burn fuel here - decrement the gas-gauge UI resource (index _frictionIndex)
- // by distance/_distanceDivisor and stop the car at 0. Deferred: the resource is an
- // integer, so the rate needs runtime tuning to avoid corrupting the saved value.
+ // Driving into a pothole (on entry) damages the tires by a random amount; at 100 the
+ // tire blows: the damage resets (a fresh spare goes on) and the car leaves for the
+ // flat-tire scene once the blowout sound has played.
+ for (uint i = 0; i < _potholes.size(); ++i) {
+ Pothole &hole = _potholes[i];
+ bool nowInside = hole.rect.contains(next);
+ if (nowInside && !hole.carInside && !_flatTirePending) {
+ int dmg = hole.minDamage;
+ if (hole.maxDamage > hole.minDamage) {
+ dmg += g_nancy->_randomSource->getRandomNumber(hole.maxDamage - hole.minDamage);
+ }
+ _tireDamage += dmg;
+ if (_tireDamage >= kTireFlatThreshold) {
+ _tireDamage = 0;
+ _flatTirePending = true;
+ playSoundBlock(_soundBlocks[0]); // tire blowout
+ }
+ }
+ hole.carInside = nowInside;
+ }
- // Driving into a checkpoint (on entry) sets its event flag once.
+ // Driving over a checkpoint (on entry) sets its event flag once.
for (uint i = 0; i < _checkpoints.size(); ++i) {
Checkpoint &cp = _checkpoints[i];
bool nowInside = cp.rect.contains(next);
@@ -354,17 +623,24 @@ void DrivingPuzzle::updatePhysics(int throttle) {
cp.carInside = nowInside;
}
- // Driving into a location entrance / finish line (on entry) transitions there. The
- // zone the car spawned inside does not fire until the car leaves and re-enters.
+ // A drive-in destination (the chase finish line) fires on entry; a parking
+ // destination (a location) is only noted here and entered with space in handleInput.
+ _parkedDest = -1;
for (uint i = 0; i < _destinations.size(); ++i) {
DestinationZone &dest = _destinations[i];
- bool nowInside = dest.rect.contains(next);
- if (nowInside && !dest.carInside && _triggeredDest < 0 && dest.scene.sceneID != kNoScene) {
- _triggeredDest = (int)i;
- _state = kActionTrigger;
+ bool nowInside = dest.scene.sceneID != kNoScene && dest.rect.contains(next);
+ if (dest.autoTrigger) {
+ if (nowInside && !dest.carInside) {
+ armExit(dest);
+ }
+ dest.carInside = nowInside;
+ } else if (nowInside && _parkedDest < 0) {
+ _parkedDest = (int)i;
}
- dest.carInside = nowInside;
}
+
+ // Remember where the car is so it resumes here after a building visit or a save/load.
+ saveState();
}
void DrivingPuzzle::execute() {
@@ -384,15 +660,14 @@ void DrivingPuzzle::execute() {
break;
case kActionTrigger:
g_nancy->_sound->stopSound(_soundBlocks[2].channel); // stop the engine ambience
- if (_triggeredDest >= 0 && _triggeredDest < (int)_destinations.size()) {
- const DestinationZone &dest = _destinations[_triggeredDest];
- if (dest.eventFlag != -1) {
- NancySceneState.setEventFlag(dest.eventFlag, dest.eventFlagValue ? g_nancy->_true : g_nancy->_false);
- }
- if (dest.hasFade) {
- NancySceneState.specialEffect(dest.fadeType, dest.fadeTotalTime, dest.fadeToBlackTime, dest.fadeRect);
- }
- NancySceneState.changeScene(dest.scene);
+ if (_exitFlag != -1) {
+ NancySceneState.setEventFlag(_exitFlag, _exitFlagValue ? g_nancy->_true : g_nancy->_false);
+ }
+ if (_exitHasFade) {
+ NancySceneState.specialEffect(_exitFadeType, _exitFadeTotalTime, _exitFadeToBlackTime, _exitFadeRect);
+ }
+ if (_exitScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_exitScene);
}
finishExecution();
break;
@@ -404,6 +679,31 @@ void DrivingPuzzle::handleInput(NancyInput &input) {
return;
}
+ // A tire has blown: hold the car still until the blowout sound finishes, then leave
+ // for the flat-tire scene (where Nancy fits the spare).
+ if (_flatTirePending) {
+ if (!g_nancy->_sound->isSoundPlaying(_soundBlocks[0].channel)) {
+ saveState();
+ if (_finishScene != kNoScene) {
+ SceneChangeDescription scene;
+ scene.sceneID = _finishScene;
+ NancySceneState.changeScene(scene);
+ }
+ finishExecution();
+ }
+ return;
+ }
+
+ // Parked in a location: pressing space gets Nancy out of the car and into it.
+ if (_parkedDest >= 0 && _parkedDest < (int)_destinations.size()) {
+ for (uint i = 0; i < input.otherKbdInput.size(); ++i) {
+ if (input.otherKbdInput[i].keycode == Common::KEYCODE_SPACE) {
+ armExit(_destinations[_parkedDest]);
+ return;
+ }
+ }
+ }
+
// Throttle with the mouse buttons: left drives forward, right reverses.
int throttle = 0;
if (input.input & NancyInput::kLeftMouseButtonHeld) {
@@ -412,13 +712,15 @@ void DrivingPuzzle::handleInput(NancyInput &input) {
throttle = -1;
}
- // Steer the car to face the cursor while driving (its distance is irrelevant).
+ // Steer the car to face the cursor; its distance sets the driving speed.
+ double cursorDist = 0.0;
if (throttle != 0) {
Common::Point cam = cameraOffset();
Common::Rect mouseVp = NancySceneState.getViewport().convertScreenToViewport(
Common::Rect(input.mousePos.x, input.mousePos.y, input.mousePos.x + 1, input.mousePos.y + 1));
double dx = (double)mouseVp.left - (_carX - cam.x);
double dy = (double)mouseVp.top - (_carY - cam.y);
+ cursorDist = sqrt(dx * dx + dy * dy);
if (dx != 0.0 || dy != 0.0) {
_carHeading = atan2(-dy, dx);
if (_carHeading < 0.0) {
@@ -430,9 +732,12 @@ void DrivingPuzzle::handleInput(NancyInput &input) {
// Drive continuously so momentum and the chaser animate every frame.
if (_variant == kChase) {
updateChaser();
+ if (_state != kRun) {
+ return; // a chase outcome fired
+ }
}
- updatePhysics(throttle);
+ updatePhysics(throttle, cursorDist);
if (_state == kRun) {
drawScene();
diff --git a/engines/nancy/action/puzzle/drivingpuzzle.h b/engines/nancy/action/puzzle/drivingpuzzle.h
index 7571d82592d..5e9e8fb9bd1 100644
--- a/engines/nancy/action/puzzle/drivingpuzzle.h
+++ b/engines/nancy/action/puzzle/drivingpuzzle.h
@@ -34,27 +34,36 @@ namespace Action {
// action records share the same engine:
// 160 - kDriving (drive Nancy's car around the Titusville town map, entering
// locations by driving into them)
-// 167 - kChase (kDriving plus a second, chaser car, a second zone array and
-// two path-point arrays that steer the chaser)
+// 167 - kChase (kDriving plus a chaser car - Jane - that plays back a recorded path
+// in real time; Nancy has to keep her in view. An event-flag-gated
+// state machine drives the outcome: the win is the chaser completing
+// its second path once Jane is caught; letting her drive off-view, or
+// driving into a trigger zone, ends it otherwise)
//
// The map scrolls under a car-centered camera; the car is drawn as a rotation-atlas
// sprite whose frame is chosen from its heading. The map is populated with an
// ActionZone array: type 0x11 zones are location entrances (each carries the
-// destination scene id and the transition effect), type 0x14 zones are boundaries,
-// and the remaining subtypes are decorations and driving hazards.
+// destination scene id and the transition effect), type 0x0d zones are cosmetic
+// decorations (buildings, parked cars, potholes and animated cows/flags/fountains),
+// and the rest are the driving hazards.
//
-// Controls: the car steers to face the cursor (distance is irrelevant); the left mouse
-// button drives forward (accelerating while held) and the right button reverses.
+// Controls: the car steers to face the cursor; the left mouse button drives forward
+// (the further from the car the cursor is, the faster) and the right button reverses.
+// Fuel is a UI resource (index _frictionIndex) drained with the distance driven; potholes
+// damage the tires; at 100 damage a tire blows and the car leaves for the flat-tire scene
+// (blob+0x80). Mud slows the car; a location is entered by parking in its zone and pressing
+// space. The car position, heading and accumulated tire damage persist across visits
+// (DrivingData), like the original's retainState. The dashboard gas/tire gauges are not
+// part of this record: they are the scene's own OverlayStaticTerse records gated by
+// DT_RESOURCE dependencies on the fuel and tire UI resources.
//
-// TODO (need runtime tuning):
-// - Fuel: burn the gas-gauge UI resource (index _frictionIndex) while driving; it
-// currently stays at its seeded value. The DT_RESOURCE dependency that reads it is
-// handled in ActionManager::processDependency.
-// - Hazards: potholes (type 0x17) should damage the car (more the faster it is hit,
-// building to a flat tire) and mud puddles should be penalised; both are ignored.
-// - Collision: only the type 0x14 boundary rects block the car (no per-pixel mask).
-// - kChase: no second-path switch or "chaser left the viewport" loss branch.
-// - The decorative overlay zone subtypes (0x0d and friends) are ignored.
+// Collision uses the "...Collision" mask, whose white streets are drivable and dark
+// areas are off-road; the type 0x14 boundary rects are only a fallback if it fails to load.
+//
+// TODO:
+// - kChase: the "caught Jane" transition (state 1 -> 2, the win) is gated on an event
+// flag the chase scene is expected to set (nothing in this record sets it); confirm
+// what triggers it so a missed catch can't still win.
class DrivingPuzzle : public RenderActionRecord {
public:
enum Variant { kDriving = 0, kChase };
@@ -75,9 +84,11 @@ protected:
return _variant == kChase ? "ChasePuzzle" : "DrivingPuzzle";
}
- // A destination the car can drive into: a location entrance (type 0x11) or the
- // chase's finish line (type 0x0c). Entering its map-space rect optionally sets an
- // event flag and transitions to the destination scene through a fade.
+ // A destination the car can drive into: a location entrance (type 0x11) or a drive-in
+ // scene trigger (type 0x0c, used in the chase). Entering its map-space rect optionally
+ // sets an event flag and transitions to the scene through a fade. Location entrances
+ // need a spacebar press to enter (you park first); the drive-in trigger (autoTrigger)
+ // fires the moment the car drives into it.
struct DestinationZone {
Common::Rect rect;
SceneChangeDescription scene;
@@ -88,9 +99,20 @@ protected:
Common::Rect fadeRect;
int16 eventFlag = -1;
byte eventFlagValue = 0;
+ bool autoTrigger = false; // true for the chase finish (drive-in), false for parking
bool carInside = false; // the car was inside this zone last frame
};
+ // Index into _chaseParams (167). The five values are the chase's outcome scenes and
+ // the event flags that gate its state machine.
+ enum ChaseParam {
+ kChaseGate01Flag = 0, // state 0 -> 1 once this flag is clear
+ kChaseOffViewScene = 1, // scene entered when the chaser leaves the viewport
+ kChaseOffViewFlag = 2, // flag set (to 1) on the off-viewport outcome
+ kChaseGate12Flag = 3, // state 1 -> 2 (switch to the second path) once this flag is set
+ kChasePathEndScene = 4 // scene entered when the chaser finishes its route
+ };
+
// A checkpoint (type 0x0b): driving over it sets an event flag once.
struct Checkpoint {
Common::Rect rect;
@@ -100,6 +122,32 @@ protected:
bool carInside = false; // the car was inside this zone last frame
};
+ // A mud puddle (type 0x03): slows the car (adds to its velocity decay) while inside.
+ struct MudZone {
+ Common::Rect rect;
+ double decel = 0.0;
+ };
+
+ // A pothole (type 0x17): driving into it damages the tires by a random amount in
+ // [minDamage, maxDamage].
+ struct Pothole {
+ Common::Rect rect;
+ int32 minDamage = 0;
+ int32 maxDamage = 0;
+ bool carInside = false; // the car was inside this zone last frame
+ };
+
+ // A cosmetic map decoration (type 0x0d): a sprite drawn onto the map at destRect.
+ // A single source rect is static; several are animation frames cycled over time.
+ // It is only visible while its event-flag condition holds (condFlag == -1 = always).
+ struct Overlay {
+ int imageIndex = -1;
+ Common::Array<Common::Rect> srcRects;
+ Common::Rect destRect; // map space
+ int16 condFlag = -1; // base zone val49: event flag gating visibility
+ byte condValue = 0; // base zone val4b: the flag value that shows it
+ };
+
// A recorded chaser-path waypoint (kChase): the pursuer plays these back in real
// time, jumping to the entry whose timestamp the elapsed chase time has passed.
struct Waypoint {
@@ -126,13 +174,30 @@ protected:
// Plays one (randomly chosen) entry of a random-sound block.
void playSoundBlock(const RandomSoundBlock &block);
+ // Arms a pending exit (applied in kActionTrigger): from a destination zone (keeping
+ // its fade), or from a raw scene id plus an optional event flag to set.
+ void armExit(const DestinationZone &dest);
+ void armExitScene(uint16 sceneID, int16 flag, byte flagValue);
+
// Advances the car's velocity/position for one frame. Throttle is +1 forward, -1
- // reverse, 0 coast; the heading is set separately (steering toward the cursor).
- void updatePhysics(int throttle);
+ // reverse, 0 coast; cursorDist (how far the cursor is from the car) sets the forward
+ // speed. The heading is set separately (steering toward the cursor).
+ void updatePhysics(int throttle, double cursorDist);
// Top-left of the car-centered camera window into the map, clamped to its bounds.
Common::Point cameraOffset() const;
+ // Persists the car's position/heading and tire state so it survives leaving the map
+ // (and saving). Only does anything when the header's retainState flag is set.
+ void saveState() const;
+
+ // Whether a map-space point is off the road: off the map, or a non-white (dark)
+ // pixel in the collision mask (its white marks the drivable streets).
+ bool isWall(int px, int py) const;
+
+ // Whether the car may not drive at this map-space point (off the road).
+ bool isBlocked(const Common::Point &p) const;
+
// Advances the chaser along its recorded path (kChase) and slows the player's
// speed cap the closer the chaser gets.
void updateChaser();
@@ -140,6 +205,14 @@ protected:
// Chooses a rotation-atlas frame from a heading.
uint frameIndexForHeading(double heading, uint frameCount) const;
+ // Loads (and caches) a decoration sprite by name, returning its index into
+ // _overlayImages, or -1 on failure.
+ int overlayImageIndex(const Common::String &name);
+
+ // Draws the map's cosmetic decorations (animated frames cycled over time), offset by
+ // the camera and clipped to the visible window.
+ void drawOverlays(const Common::Point &cam);
+
// Redraws the scrolling map (car-centered camera) and the car sprite(s) on top.
void drawScene();
@@ -159,6 +232,7 @@ protected:
int16 _frictionIndex = 0; // blob+0x77: index into the shared friction table
int32 _distanceDivisor = 0; // blob+0x7b
bool _retainState = false; // blob+0x7f: resume from the saved position
+ uint16 _finishScene = kNoScene; // blob+0x80: the scene entered when a tire goes flat
// Three random-sound blocks (tire blowout, horn, engine) and a rotation-frame
// rect table precede the ActionZone array.
@@ -177,17 +251,46 @@ protected:
Common::Array<Waypoint> _chaserPathB;
// ActionZone gameplay roles.
- Common::Array<DestinationZone> _destinations; // types 0x11 / 0x0c
+ Common::Array<DestinationZone> _destinations; // types 0x11 / 0x0c (parking spaces)
Common::Array<Checkpoint> _checkpoints; // type 0x0b
- Common::Array<Common::Rect> _boundaries; // type 0x14
+ Common::Array<MudZone> _mudZones; // type 0x03
+ Common::Array<Pothole> _potholes; // type 0x17
+ Common::Array<Overlay> _overlays; // type 0x0d (map decorations)
+ Common::Array<Graphics::ManagedSurface> _overlayImages;
+ Common::Array<Common::String> _overlayImageNames;
// Runtime state
double _carX = 0.0; // current car position (map space)
double _carY = 0.0;
double _carHeading = 0.0; // radians
- double _carVelocity = 0.0;
+ double _carVelocity = 0.0; // pixels per second
double _speedCap = 0.0; // current forward speed cap (lowered as the chaser closes in)
- int _triggeredDest = -1; // destination zone the car has entered (-1 == none)
+ uint32 _lastPhysicsMs = 0; // real time of the last physics step (frame-rate independence)
+ int _parkedDest = -1; // destination zone the car is currently parked in (-1 == none)
+
+ // A pending exit to another scene (a location, the chase finish, or a chase outcome).
+ // Armed via armExit()/armExitScene(); applied and finished in the kActionTrigger state.
+ SceneChangeDescription _exitScene;
+ bool _exitHasFade = false;
+ byte _exitFadeType = 0;
+ uint16 _exitFadeTotalTime = 0;
+ uint16 _exitFadeToBlackTime = 0;
+ Common::Rect _exitFadeRect;
+ int16 _exitFlag = -1;
+ byte _exitFlagValue = 0;
+
+ // Chase (167) state machine: 0 = following the first path, 1 = waiting to switch,
+ // 2 = following the second path.
+ int _chaseState = 0;
+ bool _chaserOnPathB = false;
+
+ // Fuel + tire hazards. Fuel is the gas-gauge UI resource (index _frictionIndex),
+ // drained as the car drives; the fractional part is accumulated here. Tire damage
+ // builds up from potholes; once it blows a tire the car leaves for the flat-tire
+ // scene while _flatTirePending waits for the blowout sound to finish.
+ double _fuelBurnAccum = 0.0;
+ int _tireDamage = 0;
+ bool _flatTirePending = false;
// Chaser (kChase) runtime state.
bool _chaseStarted = false;
@@ -200,6 +303,7 @@ protected:
Graphics::ManagedSurface _image; // the town map
Graphics::ManagedSurface _carImage; // the player car rotation atlas
Graphics::ManagedSurface _chaseCarImage; // the chaser car rotation atlas
+ Graphics::ManagedSurface _collisionMask; // road/off-road mask ("MAP_TitusvilleCollision")
};
} // End of namespace Action
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index 0f0613fe9ac..d6e966c039e 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -478,8 +478,19 @@ void WordFindPuzzleData::synchronize(Common::Serializer &ser) {
ser.syncAsSint16LE(currentWord);
}
+void DrivingData::synchronize(Common::Serializer &ser) {
+ ser.syncAsByte(valid);
+ ser.syncAsSint32LE(carX);
+ ser.syncAsSint32LE(carY);
+ ser.syncAsDoubleLE(heading);
+ ser.syncAsSint32LE(tireDamage);
+ ser.syncAsByte(flatTire);
+}
+
PuzzleData *makePuzzleData(const uint32 tag) {
switch(tag) {
+ case DrivingData::getTag():
+ return new DrivingData();
case WordFindPuzzleData::getTag():
return new WordFindPuzzleData();
case SliderPuzzleData::getTag():
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index 91f34c81ca0..bd0b4e3b755 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -379,6 +379,26 @@ struct WordFindPuzzleData : public PuzzleData {
int16 currentWord = 0;
};
+// Nancy12 DrivingPuzzle (AR 160). The car's position, heading and tire state persist
+// across visits to the driving map (driving into a location, then coming back), matching
+// the original's retainState mechanism, which saves the car to globals every frame and
+// restores it on setup. `valid` is false until the car has been driven, so the first
+// visit starts from the header's start position.
+struct DrivingData : public PuzzleData {
+ DrivingData() {}
+ virtual ~DrivingData() {}
+
+ static constexpr uint32 getTag() { return MKTAG('D', 'R', 'V', 'G'); }
+ virtual void synchronize(Common::Serializer &ser);
+
+ bool valid = false;
+ int32 carX = 0;
+ int32 carY = 0;
+ double heading = 0.0;
+ int32 tireDamage = 0;
+ bool flatTire = false;
+};
+
PuzzleData *makePuzzleData(const uint32 tag);
} // End of namespace Nancy
Commit: c9df87accbb3c8da4b647a521f45123c63543d7b
https://github.com/scummvm/scummvm/commit/c9df87accbb3c8da4b647a521f45123c63543d7b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-03T02:09:13+03:00
Commit Message:
NANCY: Filter out more invalid file name chars in actionrecord_export
Changed paths:
engines/nancy/console.cpp
diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index a082a464be2..656d533a3b7 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -712,6 +712,8 @@ bool NancyConsole::Cmd_actionRecordExport(int argc, const char **argv) {
Common::String desc(descBuf);
desc.replace('/', '-');
desc.replace('\\', '-');
+ desc.replace('>', '_');
+ desc.replace('<', '_');
byte ARType = chunk->readByte();
chunk->skip(1); // execType
More information about the Scummvm-git-logs
mailing list