[Scummvm-git-logs] scummvm master -> 6793425f01a0acb000ea336f2a96ed9d433e8aba
alexbevi
noreply at scummvm.org
Wed Oct 7 10:22:55 UTC 2026
This automated email contains information about 12 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
a0b6c9baaf HARVESTER: Preserve numbered dialogue responses
7e6bb821ee HARVESTER: Fix room exit when facing opposite direction
2f270f8577 HARVESTER: Preserve keyboard walk cycles after key release
258a1c2668 HARVESTER: Fix palette fade timing
107798fbad HARVESTER: Fix depth-scaled walking
2771b9beb7 HARVESTER: Preserve player walking through timer actions
4e05e6800f HARVESTER: Improve animation synchronization
4e6af2292f HARVESTER: Preserve player sprite bases across animation frames
78d34939f7 HARVESTER: Adjust target depth during sideways walking
05e5e68b42 HARVESTER: adjust player/npc animation frame logging
af5340ebc0 HARVESTER: adjust palette fade logging verbosity
6793425f01 HARVESTER: fix menu options overlap on game over
Commit: a0b6c9baafd7cc759b606ff3da5be66ee4b8d426
https://github.com/scummvm/scummvm/commit/a0b6c9baafd7cc759b606ff3da5be66ee4b8d426
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:18-04:00
Commit Message:
HARVESTER: Preserve numbered dialogue responses
Fixes #17292
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/dialogue.cpp
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 0ce30c30ea9..5267bbea0f5 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -457,6 +457,7 @@ public:
struct DialogueResponseOptionLayout {
Common::String text;
Common::Array<Common::String> wrappedLines;
+ char hotkey = 0;
int rowStart = 0;
int rowCount = 0;
};
@@ -842,7 +843,7 @@ public:
if (clickedTopic >= 0) {
selectedTopic = topics[(uint)clickedTopic];
selection.fromGenericBye = clickedTopic == (int)topics.size() - 1;
- debugC(1, kDebugDialogue, "Harvester: keyword menu selected topic[%d]='%s'",
+ debugC(2, kDebugDialogue, "Harvester: keyword menu selected topic[%d]='%s' via click",
clickedTopic + 1, selectedTopic.c_str());
return Common::kNoError;
}
@@ -1158,6 +1159,9 @@ private:
DialogueResponseOptionLayout option;
option.text = optionText;
+ if (optionText.size() > 1 && optionText[0] >= '1' && optionText[0] <= '9' &&
+ optionText[1] == '.')
+ option.hotkey = optionText[0];
option.rowStart = (int)totalRows;
for (const Common::String &explicitLine : explicitLines) {
Common::Array<Common::String> wrappedExplicitLines;
@@ -1181,20 +1185,12 @@ private:
if (segmentText.empty())
return;
- uint digitEnd = 0;
- while (digitEnd < segmentText.size() && segmentText[digitEnd] >= '0' &&
- segmentText[digitEnd] <= '9') {
- ++digitEnd;
- }
- const bool startsNewOption = digitEnd > 0 && digitEnd < segmentText.size() &&
- segmentText[digitEnd] == '.';
+ const bool startsNewOption = segmentText.size() > 1 && segmentText[0] >= '1' &&
+ segmentText[0] <= '9' && segmentText[1] == '.';
if (startsNewOption) {
flushOption();
-
- Common::String optionLine = segmentText.substr(digitEnd + 1);
- optionLine.trim();
- optionText = optionLine;
- explicitLines.push_back(Common::move(optionLine));
+ optionText = segmentText;
+ explicitLines.push_back(Common::move(segmentText));
return;
}
@@ -1310,8 +1306,11 @@ private:
}
Common::Array<Common::String> optionTexts;
- for (const DialogueResponseOptionLayout &option : options)
+ for (const DialogueResponseOptionLayout &option : options) {
optionTexts.push_back(option.text);
+ debugC(3, kDebugDialogue, "Harvester: response menu option[%u] hotkey=%c rowStart=%d rowCount=%d",
+ optionTexts.size(), option.hotkey ? option.hotkey : '-', option.rowStart, option.rowCount);
+ }
logDialogueMenuItems("Response menu", responseLineIndex, responseLine, optionTexts);
const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(resolveDialogueResponseTextboxIndex(totalRows));
@@ -1331,43 +1330,44 @@ private:
return result;
if (event.type == Common::EVENT_KEYDOWN) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) cancelled",
- responseLineIndex, responseLineIndex + 1);
- } else {
- debugC(1, kDebugDialogue, "Harvester: response menu text cancelled");
- }
- return Common::kNoError;
- }
- if (event.kbd.ascii >= '1' && event.kbd.ascii <= '9') {
- const int menuIndex = event.kbd.ascii - '0';
- if (menuIndex >= 1 && menuIndex <= (int)options.size()) {
- selectedIndex = menuIndex;
+ debugC(3, kDebugDialogue, "Harvester: response menu keycode=%d ascii=%u",
+ (int)event.kbd.keycode, event.kbd.ascii);
+ if (event.kbd.keycode >= Common::KEYCODE_1 && event.kbd.keycode <= Common::KEYCODE_9) {
+ const char hotkey = '1' + event.kbd.keycode - Common::KEYCODE_1;
+ for (int optionIndex = (int)options.size() - 1; optionIndex >= 0; --optionIndex) {
+ const DialogueResponseOptionLayout &option = options[(uint)optionIndex];
+ if (option.hotkey != hotkey)
+ continue;
+ selectedIndex = optionIndex + 1;
if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue,
- "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via hotkey",
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via hotkey=%c result=%d",
responseLineIndex, responseLineIndex + 1,
- selectedIndex, options[(uint)(selectedIndex - 1)].text.c_str());
+ optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
} else {
- debugC(1, kDebugDialogue,
- "Harvester: response menu text selected option[%d]='%s' via hotkey",
- selectedIndex, options[(uint)(selectedIndex - 1)].text.c_str());
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu text selected option[%d]='%s' via hotkey=%c result=%d",
+ optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
}
return waitForPointerRelease();
}
}
- } else if (event.type == Common::EVENT_LBUTTONDOWN && hoveredOptionIndex >= 0) {
- selectedIndex = hoveredOptionIndex + 1;
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ const int clickedOptionIndex = getResponseMenuItemAt(options, totalRows, event.mouse);
+ if (clickedOptionIndex < 0)
+ continue;
+ selectedIndex = clickedOptionIndex + 1;
if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue,
- "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via click",
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via click x=%d y=%d result=%d",
responseLineIndex, responseLineIndex + 1,
- selectedIndex, options[(uint)hoveredOptionIndex].text.c_str());
+ selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
+ event.mouse.x, event.mouse.y, selectedIndex);
} else {
- debugC(1, kDebugDialogue,
- "Harvester: response menu text selected option[%d]='%s' via click",
- selectedIndex, options[(uint)hoveredOptionIndex].text.c_str());
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu text selected option[%d]='%s' via click x=%d y=%d result=%d",
+ selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
+ event.mouse.x, event.mouse.y, selectedIndex);
}
return waitForPointerRelease();
}
Commit: 7e6bb821eebe3d3d98faf00af4a74bb7ae377f9a
https://github.com/scummvm/scummvm/commit/7e6bb821eebe3d3d98faf00af4a74bb7ae377f9a
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:18-04:00
Commit Message:
HARVESTER: Fix room exit when facing opposite direction
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
engines/harvester/player.h
engines/harvester/room.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 0c435957189..afe9bd37776 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -613,22 +613,22 @@ static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
}
static int resolveMoveTargetHorizontalDirection(const RoomSetupState &state,
- const RoomPlayerState &playerState) {
+ const RoomPlayerState &playerState, int regionFacing) {
const float slack = Player::computeDepthScale(state, playerState.z) *
kRoomPlayerHorizontalTargetSlackBase;
const float frameLeftX = (float)computePlayerFrameLeftX(playerState);
- if (frameLeftX < (float)playerState.targetX - slack)
+ if (frameLeftX < (float)playerState.targetX - slack || regionFacing == 2)
return 1;
- if ((float)playerState.targetX + slack < frameLeftX)
+ if ((float)playerState.targetX + slack < frameLeftX || regionFacing == 1)
return -1;
return 0;
}
-static int resolveMoveTargetDepthDirection(const RoomPlayerState &playerState) {
- if (playerState.targetZ < playerState.z - kRoomPlayerDepthTargetSlack)
+static int resolveMoveTargetDepthDirection(const RoomPlayerState &playerState, int regionFacing) {
+ if (playerState.targetZ < playerState.z - kRoomPlayerDepthTargetSlack || regionFacing == 0)
return 1;
- if (playerState.z + kRoomPlayerDepthTargetSlack < playerState.targetZ)
+ if (playerState.z + kRoomPlayerDepthTargetSlack < playerState.targetZ || regionFacing == 3)
return -1;
return 0;
@@ -639,13 +639,13 @@ static bool hasPlayerReachedMoveTarget(const RoomPlayerState &playerState) {
}
static void updatePlayerMoveTargetProgress(const RoomSetupState &state,
- RoomPlayerState &playerState) {
+ RoomPlayerState &playerState, int regionFacing) {
if (!playerState.moveTargetXReached &&
- resolveMoveTargetHorizontalDirection(state, playerState) == 0) {
+ resolveMoveTargetHorizontalDirection(state, playerState, regionFacing) == 0) {
playerState.moveTargetXReached = true;
}
if (!playerState.moveTargetZReached &&
- resolveMoveTargetDepthDirection(playerState) == 0) {
+ resolveMoveTargetDepthDirection(playerState, regionFacing) == 0) {
playerState.moveTargetZReached = true;
}
}
@@ -1633,12 +1633,16 @@ bool Player::startTurnAnimation(RoomPlayerState &playerState, int targetFacing)
playerState.facing < 0 || playerState.facing == targetFacing)
return false;
+ // update_actor_runtime_state (0x4d750) routes desired states 1/2 from the
+ // opposite horizontal facing through front-facing state 0.
+ const int nextFacing = (playerState.facing == 1 && targetFacing == 2) ||
+ (playerState.facing == 2 && targetFacing == 1) ? 0 : targetFacing;
PlayerTurnAnimationRange range;
- if (!resolvePlayerTurnAnimationRange(playerState.facing, targetFacing, range))
+ if (!resolvePlayerTurnAnimationRange(playerState.facing, nextFacing, range))
return false;
playerState.turnActive = true;
- playerState.turnTargetFacing = targetFacing;
+ playerState.turnTargetFacing = nextFacing;
playerState.turnFirstFrame = range.firstFrame;
playerState.turnLastFrame = range.lastFrame;
playerState.turnEndFrame = range.playBackwards ? range.firstFrame : range.lastFrame;
@@ -1649,9 +1653,9 @@ bool Player::startTurnAnimation(RoomPlayerState &playerState, int targetFacing)
playerState.entity->setAnimationRate(kRoomPlayerWalkAnimationRate);
playerState.entity->setCurrentFrame(range.playBackwards ? range.lastFrame : range.firstFrame);
debugC(1, kDebugPlayer,
- "Harvester: player turn animation from=%d to=%d frames=%d..%d backwards=%d rate=%d",
- playerState.facing, targetFacing, range.firstFrame, range.lastFrame,
- range.playBackwards, kRoomPlayerWalkAnimationRate);
+ "Harvester: player turn animation from=%d to=%d frames=%d..%d backwards=%d rate=%d requested_facing=%d",
+ playerState.facing, nextFacing, range.firstFrame, range.lastFrame,
+ range.playBackwards, kRoomPlayerWalkAnimationRate, targetFacing);
return true;
}
@@ -1678,13 +1682,16 @@ bool Player::updateTurnAnimationState(RoomPlayerState &playerState) {
bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
const Common::Array<ObjectRecord> &sceneObjects,
const Common::Array<AnimRecord> &sceneAnimations,
- RoomPlayerState &playerState) {
+ RoomPlayerState &playerState, int regionFacing) {
if (!playerState.entity || !playerState.hasMoveTarget || playerState.turnActive || playerState.hitActive)
return false;
refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, playerState);
- updatePlayerMoveTargetProgress(state, playerState);
+ // run_harvester_main_loop (0x6dc70) latches the clicked region facing at
+ // 0xc3f0c. update_actor_runtime_state (0x4d750) uses it to keep the matching
+ // target component active inside the usual X/Z stopping tolerance.
+ updatePlayerMoveTargetProgress(state, playerState, regionFacing);
if (hasPlayerReachedMoveTarget(playerState)) {
playerState.hasMoveTarget = false;
return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
@@ -1694,7 +1701,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
const int previousBottomY = playerState.bottomY;
int horizontalDirection = playerState.moveTargetXReached
? 0
- : resolveMoveTargetHorizontalDirection(state, playerState);
+ : resolveMoveTargetHorizontalDirection(state, playerState, regionFacing);
if (horizontalDirection != 0 && isHorizontalDirectionBlockedByHistory(playerState, horizontalDirection)) {
if (engine.isPathfindingDebugEnabled()) {
debugC(1, kDebugPathfinding,
@@ -1708,7 +1715,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
horizontalDirection = 0;
}
int depthDirection = horizontalDirection == 0 && !playerState.moveTargetZReached
- ? resolveMoveTargetDepthDirection(playerState)
+ ? resolveMoveTargetDepthDirection(playerState, regionFacing)
: 0;
if (depthDirection != 0 && isDepthDirectionBlockedByHistory(playerState, depthDirection)) {
if (engine.isPathfindingDebugEnabled()) {
@@ -1726,6 +1733,16 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
playerState.hasMoveTarget = false;
return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
}
+ const int desiredFacing = resolveFacingFromMoveDirection(
+ horizontalDirection, depthDirection, playerState.facing);
+ if (regionFacing >= 0 && desiredFacing != playerState.facing &&
+ startTurnAnimation(playerState, desiredFacing)) {
+ debugC(2, kDebugPlayer,
+ "Harvester: player region target turn room='%s' required_facing=%d movement_facing=%d target=(%d,z=%.2f)",
+ state.roomName.c_str(), regionFacing, desiredFacing,
+ playerState.targetX, (double)playerState.targetZ);
+ return true;
+ }
if (!consumePlayerMovementTick(playerState))
return false;
@@ -1739,9 +1756,16 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
} else if (depthDirection != 0) {
const int verticalStep = computeRoomPlayerVerticalScreenStep();
const float depthStep = computeRoomPlayerDepthStep(state);
- candidateBottomY = clampRoomMovementY(state,
- playerState.bottomY + depthDirection * verticalStep);
- candidateZ = stepTowardsFloat(playerState.z, playerState.targetZ, depthStep);
+ const bool depthBoundaryReached = depthDirection < 0
+ ? playerState.z >= (float)state.roomMaxZ
+ : playerState.z <= (float)state.roomMinZ;
+ if (!depthBoundaryReached) {
+ candidateBottomY = clampRoomMovementY(state,
+ playerState.bottomY + depthDirection * verticalStep);
+ candidateZ = regionFacing == 0 || regionFacing == 3
+ ? clampRoomDepth(state, playerState.z - depthDirection * depthStep)
+ : stepTowardsFloat(playerState.z, playerState.targetZ, depthStep);
+ }
}
if (engine.isPathfindingDebugEnabled()) {
@@ -1800,7 +1824,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
playerState.centerX, playerState.bottomY, (double)playerState.z,
actualFacing);
}
- updatePlayerMoveTargetProgress(state, playerState);
+ updatePlayerMoveTargetProgress(state, playerState, regionFacing);
if (hasPlayerReachedMoveTarget(playerState)) {
playerState.hasMoveTarget = false;
(void)setIdleAnimation(playerState, actualFacing);
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index 867fdff5438..f3d0f102f0a 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -75,7 +75,7 @@ public:
static bool stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
const Common::Array<ObjectRecord> &sceneObjects,
const Common::Array<AnimRecord> &sceneAnimations,
- RoomPlayerState &playerState);
+ RoomPlayerState &playerState, int regionFacing = -1);
static bool stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
const Common::Array<ObjectRecord> &sceneObjects,
const Common::Array<AnimRecord> &sceneAnimations,
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index cf99d81703c..554801617ee 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -3424,7 +3424,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
playerOverlapsRegion, region.actionTag.c_str());
}
- if (playerOverlapsRegion)
+ if (playerOverlapsRegion && doesPlayerFacingMatchRegion(playerState.facing, region))
return;
Player::setRegionMoveTarget(scene.state, playerState,
@@ -4427,13 +4427,16 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (playerCanAct &&
!playerState.attackActive && !playerState.hitActive &&
!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
+ const RegionRecord *pendingRegion = findSceneRegionByName(scene.sceneRegions, pendingRegionName);
+ const int regionFacing = pendingRegion && pendingRegion->startEnabled
+ ? pendingRegion->desiredFacing : -1;
if (Player::stepKeyboardMovement(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
playerState, moveLeft, moveRight, moveUp, moveDown)) {
notePlayerActivity();
needsRedraw = true;
} else if (Player::stepMoveTarget(
_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
- playerState)) {
+ playerState, regionFacing)) {
notePlayerActivity();
needsRedraw = true;
} else if (!moveLeft && !moveRight && !moveUp && !moveDown && !playerState.hasMoveTarget &&
Commit: 2f270f8577ee0a6213128f6769f16f38162b81fb
https://github.com/scummvm/scummvm/commit/2f270f8577ee0a6213128f6769f16f38162b81fb
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:18-04:00
Commit Message:
HARVESTER: Preserve keyboard walk cycles after key release
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
engines/harvester/room.cpp
engines/harvester/room_support.h
engines/harvester/runtime_entity.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index afe9bd37776..45723423e50 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -1380,6 +1380,7 @@ bool Player::setIdleAnimation(RoomPlayerState &playerState, int facing) {
playerState.entity->getAnimationRate() != 0;
playerState.facing = facing;
playerState.nextMovementTick = 0;
+ playerState.keyboardWalkRequest = -1;
playerState.entity->setAnimationRate(0);
playerState.entity->setAnimationFrameRange(range.idleFrame, range.idleFrame, false);
playerState.entity->setCurrentFrame(range.idleFrame);
@@ -1844,14 +1845,35 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
RoomPlayerState &playerState, bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
if (!playerState.entity || !supportsMovementBand(state))
return false;
-
- const int horizontalInput = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
- const int verticalInput = (moveDown ? 1 : 0) - (moveUp ? 1 : 0);
- if (horizontalInput == 0 && verticalInput == 0)
- return false;
if (playerState.turnActive || playerState.hitActive)
return false;
+ int horizontalInput = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
+ int verticalInput = (moveDown ? 1 : 0) - (moveUp ? 1 : 0);
+ const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
+ const int currentFrame = playerState.entity->getCurrentFrame();
+ const bool keyboardWalking = !playerState.hasMoveTarget &&
+ playerState.entity->getAnimationRate() == kRoomPlayerWalkAnimationRate &&
+ currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
+ if (keyboardWalking) {
+ if (horizontalInput != 0 || verticalInput != 0)
+ playerState.keyboardWalkRequest = resolveFacingFromMoveDirection(
+ horizontalInput, verticalInput, playerState.facing);
+ if (!playerState.entity->didAnimationAdvanceLastTick())
+ return false;
+ const int stepFacing = currentFrame < range.walkLastFrame
+ ? playerState.facing : playerState.keyboardWalkRequest;
+ horizontalInput = stepFacing == 1 ? -1 : stepFacing == 2 ? 1 : 0;
+ verticalInput = stepFacing == 3 ? -1 : stepFacing == 0 ? 1 : 0;
+ } else {
+ playerState.keyboardWalkRequest = -1;
+ }
+ if (horizontalInput == 0 && verticalInput == 0) {
+ if (playerState.hasMoveTarget || playerState.facing < 0)
+ return false;
+ return setIdleAnimation(playerState, playerState.facing);
+ }
+
const int previousCenterX = playerState.centerX;
const int previousBottomY = playerState.bottomY;
const float previousZ = playerState.z;
@@ -1892,13 +1914,15 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
}
return stopped;
}
- return false;
+ return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
}
const int desiredFacing = resolveFacingFromRoomMovement(
playerState.centerX, playerState.bottomY, candidateCenterX, candidateBottomY);
- if (desiredFacing != playerState.facing && startTurnAnimation(playerState, desiredFacing))
+ if (desiredFacing != playerState.facing && startTurnAnimation(playerState, desiredFacing)) {
+ playerState.keyboardWalkRequest = -1;
return true;
+ }
if (!consumePlayerMovementTick(playerState))
return false;
@@ -1908,14 +1932,22 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
playerState, previousCenterX, previousBottomY, previousZ,
candidateCenterX, candidateBottomY, candidateZ);
if (!moved)
- return false;
+ return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
const int actualFacing = resolveFacingFromRoomMovement(
previousCenterX, previousBottomY, playerState.centerX, playerState.bottomY);
(void)setPlayerWalkAnimation(playerState, actualFacing);
+ if (keyboardWalking && currentFrame == range.walkLastFrame) {
+ playerState.keyboardWalkRequest = -1;
+ playerState.entity->setCurrentFrame(resolvePlayerAnimationRange(actualFacing).walkFirstFrame);
+ debugC(2, kDebugPlayer,
+ "Harvester: player keyboard walk bank restart facing=%d frame=%d",
+ actualFacing, playerState.entity->getCurrentFrame());
+ }
debugC(2, kDebugPlayer,
- "Harvester: player keyboard move room='%s' input=(%d,%d) pos=(%d,%d,z=%.2f) facing=%d frame=%d moved=%d",
- state.roomName.c_str(), horizontalInput, verticalInput,
+ "Harvester: player keyboard move room='%s' input=(%d,%d) step=(%d,%d) pos=(%d,%d,z=%.2f) facing=%d frame=%d moved=%d",
+ state.roomName.c_str(), (moveRight ? 1 : 0) - (moveLeft ? 1 : 0),
+ (moveDown ? 1 : 0) - (moveUp ? 1 : 0), horizontalInput, verticalInput,
playerState.centerX, playerState.bottomY, (double)playerState.z,
playerState.facing, playerState.entity->getCurrentFrame(), moved);
return true;
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 554801617ee..ec3c206a63e 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -4439,11 +4439,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState, regionFacing)) {
notePlayerActivity();
needsRedraw = true;
- } else if (!moveLeft && !moveRight && !moveUp && !moveDown && !playerState.hasMoveTarget &&
- !playerState.turnActive && !playerState.hitActive &&
- playerState.entity && playerState.facing >= 0 &&
- Player::setIdleAnimation(playerState, playerState.facing)) {
- needsRedraw = true;
}
if (!showingInspectText && !keyboardAttackRequested &&
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 72793c1a92f..2c546f64153 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -61,6 +61,8 @@ struct RoomPlayerState {
// Mirrors native RenderEntityRuntime.player_combat_loadout_id (+0x11bc).
int combatLoadout = 0;
uint32 nextMovementTick = 0;
+ // Held input queues the next walk bank; release preserves that request.
+ int keyboardWalkRequest = -1;
bool hasMoveTarget = false;
int targetX = 0;
int targetBottomY = 0;
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index 5aec9b654e0..fc88a545ce2 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -446,6 +446,13 @@ bool Entity::tickVisualState(uint32 now) {
_nextAnimationTick = now + _animationTickInterval;
_animationAdvancedLastTick = true;
+ if (_classId == kRuntimeEntityClassPlayer) {
+ debugC(3, kDebugPlayer,
+ "Harvester: player animation advance frame=%d->%d range=%d..%d rate=%d tick=%u interval=%u next_tick=%u",
+ previousFrameIndex, _currentFrame, _firstFrame, _lastFrame,
+ _animationRate, now, _animationTickInterval, _nextAnimationTick);
+ }
+
if (_classId == kRuntimeEntityClassNpc) {
const AbmFrame ¤tFrame = _frames[(uint)_currentFrame];
const Common::Point drawOrigin = getDrawOrigin();
Commit: 258a1c26687ce1cbc7181ba1fb980093dce1b5c7
https://github.com/scummvm/scummvm/commit/258a1c26687ce1cbc7181ba1fb980093dce1b5c7
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:18-04:00
Commit Message:
HARVESTER: Fix palette fade timing
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/flow.cpp
engines/harvester/flow.h
engines/harvester/runtime_entity.cpp
engines/harvester/runtime_entity.h
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 00bf6597208..a7d0c987abd 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -80,8 +80,8 @@ static const int kIdentTextboxTextInsetX = 10;
static const int kIdentTextboxTextInsetY = 5;
static const int kNativeIdentTextLineSpacing = 3;
static const char *const kPlayerActorEntityName = "PLAYER";
-static const uint32 kPaletteFadeTickMs = 4;
-static const float kPaletteFadeStep = 0.1f;
+// ramp_palette_brightness (0x23a30) waits four DOS centiseconds per step.
+static const uint32 kPaletteFadeIntervalTicks = 4;
static const float kPaletteBrightnessBlack = 0.0f;
static const float kPaletteBrightnessFull = 1.0f;
static const uint32 kDemoEndingScreenDurationMs = 10000;
@@ -2403,19 +2403,16 @@ Common::Error Flow::beginRoomSetupTransition() {
}
if (hasVisiblePalette) {
- for (float brightness = 1.0f - kPaletteFadeStep;
- brightness > kPaletteBrightnessBlack; brightness -= kPaletteFadeStep) {
+ for (double brightness = 1.0;
+ brightness > kPaletteBrightnessBlack; brightness -= 0.1) {
+ const uint32 startTick = getRuntimeClockTicks();
setScaledDisplayPalette(*screen, displayPalette, brightness);
screen->makeAllDirty();
screen->update();
- const uint32 nextTick = g_system->getMillis() + kPaletteFadeTickMs;
- while ((int32)(nextTick - g_system->getMillis()) > 0) {
- Common::Error result = Common::kNoError;
- if (pumpTransitionEvents(result))
- return result;
- g_system->delayMillis(1);
- }
+ Common::Error fadeError = Common::kNoError;
+ if (waitForPaletteFadeTick(startTick, fadeError))
+ return fadeError;
}
setScaledDisplayPalette(*screen, displayPalette, kPaletteBrightnessBlack);
@@ -2460,18 +2457,15 @@ Common::Error Flow::fadeInRoomScene(const byte *palette, float targetBrightness)
if (!screen || !palette)
return Common::kNoError;
- for (float brightness = kPaletteFadeStep; brightness < targetBrightness; brightness += kPaletteFadeStep) {
+ for (double brightness = 0.0; brightness < targetBrightness; brightness += 0.1) {
+ const uint32 startTick = getRuntimeClockTicks();
setScaledPalette(*screen, palette, brightness);
screen->makeAllDirty();
screen->update();
- const uint32 nextTick = g_system->getMillis() + kPaletteFadeTickMs;
- while ((int32)(nextTick - g_system->getMillis()) > 0) {
- Common::Error result = Common::kNoError;
- if (pumpTransitionEvents(result))
- return result;
- g_system->delayMillis(1);
- }
+ Common::Error fadeError = Common::kNoError;
+ if (waitForPaletteFadeTick(startTick, fadeError))
+ return fadeError;
}
setScaledPalette(*screen, palette, targetBrightness);
@@ -2486,21 +2480,18 @@ Common::Error Flow::fadePalette(const byte *palette, float fromBrightness,
if (!screen || !palette)
return Common::kNoError;
- const float step = fromBrightness < toBrightness ? kPaletteFadeStep : -kPaletteFadeStep;
- for (float brightness = fromBrightness;
+ const double step = fromBrightness < toBrightness ? 0.1 : -0.1;
+ for (double brightness = fromBrightness;
step > 0.0f ? brightness < toBrightness : brightness > toBrightness;
brightness += step) {
+ const uint32 startTick = getRuntimeClockTicks();
setScaledPalette(*screen, palette, brightness);
screen->makeAllDirty();
screen->update();
- const uint32 nextTick = g_system->getMillis() + kPaletteFadeTickMs;
- while ((int32)(nextTick - g_system->getMillis()) > 0) {
- Common::Error eventError = Common::kNoError;
- if (pumpTransitionEvents(eventError))
- return eventError;
- g_system->delayMillis(1);
- }
+ Common::Error fadeError = Common::kNoError;
+ if (waitForPaletteFadeTick(startTick, fadeError))
+ return fadeError;
}
setScaledPalette(*screen, palette, toBrightness);
@@ -2509,6 +2500,19 @@ Common::Error Flow::fadePalette(const byte *palette, float fromBrightness,
return Common::kNoError;
}
+bool Flow::waitForPaletteFadeTick(uint32 startTick, Common::Error &result) {
+ const uint32 deadline = startTick + kPaletteFadeIntervalTicks;
+ while ((int32)(deadline - getRuntimeClockTicks()) > 0) {
+ if (pumpTransitionEvents(result))
+ return true;
+ g_system->delayMillis(1);
+ }
+ debugC(3, kDebugScene,
+ "Harvester: palette fade step tick=%u->%u interval=%u",
+ startTick, getRuntimeClockTicks(), kPaletteFadeIntervalTicks);
+ return false;
+}
+
bool Flow::pumpTransitionEvents(Common::Error &result) {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index bbf255d7328..8c42144c327 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -83,6 +83,7 @@ private:
Common::Error waitForRoomSetupTransitionHold();
Common::Error fadeInRoomScene(const byte *palette, float targetBrightness);
Common::Error fadePalette(const byte *palette, float fromBrightness, float toBrightness);
+ bool waitForPaletteFadeTick(uint32 startTick, Common::Error &result);
bool pumpTransitionEvents(Common::Error &result);
void executeStartupAudioCommands(const Common::Array<AudioCommand> &commands);
void queueDialogueInteraction(const InteractionResult &interaction);
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index fc88a545ce2..c6291aacdf8 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -69,18 +69,6 @@ static void scaleIndexedBitmapNearest(const IndexedBitmap &source, IndexedBitmap
}
}
-static uint32 getAnimationClockTicks() {
- if (!g_system)
- return 0;
-
- // Native obtains time through DOS int 21h/AH=2Ch. Its hundredths field
- // advances on the 18.2 Hz BIOS timer, so preserve that quantization instead
- // of deriving idealized centiseconds directly from wall-clock milliseconds.
- const uint64 pitTicks = ((uint64)g_system->getMillis() * kDosPitInputFrequency) /
- ((uint64)kDosPitTimerDivisor * 1000U);
- return (uint32)((pitTicks * kDosPitTimerDivisor * 100U) / kDosPitInputFrequency);
-}
-
static void blitAnimationFrame(Graphics::Screen &screen, const Common::Array<AbmFrame> &frames, uint frameIndex,
int x, int y) {
if (frameIndex >= frames.size() || !frames[frameIndex].isValid())
@@ -152,6 +140,18 @@ static bool decodeAnimationFrame(const byte *source, uint32 sourceSize, bool com
} // End of anonymous namespace
+uint32 getRuntimeClockTicks() {
+ if (!g_system)
+ return 0;
+
+ // Original game obtains time through DOS int 21h/AH=2Ch. Its hundredths field
+ // advances on the 18.2 Hz BIOS timer, so preserve that quantization instead
+ // of deriving idealized centiseconds directly from wall-clock milliseconds.
+ const uint64 pitTicks = ((uint64)g_system->getMillis() * kDosPitInputFrequency) /
+ ((uint64)kDosPitTimerDivisor * 1000U);
+ return (uint32)((pitTicks * kDosPitTimerDivisor * 100U) / kDosPitInputFrequency);
+}
+
static bool isRectangleOnlyHitClass(int classId) {
return classId == kRuntimeEntityClassRectHotspot ||
classId == kRuntimeEntityClassRectHotspot18 ||
@@ -517,7 +517,7 @@ void Entity::setTimerEnabled(bool enabled) {
return;
}
- const uint32 now = getAnimationClockTicks();
+ const uint32 now = getRuntimeClockTicks();
const int remainingValue = _timerCurrentValue > 0 ? _timerCurrentValue : _timerInitialValue;
_timerEnabled = true;
_timerPaused = false;
@@ -991,7 +991,7 @@ void EntityManager::configureSceneTimerEntity(Entity &entity, int initialValue,
bool enabled, bool looping, bool global) {
entity.configureTimerCountdown(initialValue, currentValue, enabled, looping, global);
if (_timerPauseDepth > 0)
- entity.pauseTimerCountdown(getAnimationClockTicks());
+ entity.pauseTimerCountdown(getRuntimeClockTicks());
}
void EntityManager::hideCursor() {
@@ -1008,7 +1008,7 @@ void EntityManager::pauseTimerCountdowns() {
if (_timerPauseDepth++ > 0)
return;
- const uint32 now = getAnimationClockTicks();
+ const uint32 now = getRuntimeClockTicks();
for (Entity *entity : _sceneEntities)
entity->pauseTimerCountdown(now);
}
@@ -1019,7 +1019,7 @@ void EntityManager::resumeTimerCountdowns() {
if (--_timerPauseDepth > 0)
return;
- const uint32 now = getAnimationClockTicks();
+ const uint32 now = getRuntimeClockTicks();
for (Entity *entity : _sceneEntities)
entity->resumeTimerCountdown(now);
}
@@ -1034,7 +1034,7 @@ bool EntityManager::takeExpiredTimerNames(Common::Array<Common::String> &expired
}
bool EntityManager::tickSceneEntities() {
- const uint32 now = getAnimationClockTicks();
+ const uint32 now = getRuntimeClockTicks();
bool changed = false;
for (Entity *entity : _sceneEntities) {
@@ -1056,7 +1056,7 @@ bool EntityManager::syncCursorEntityPosition(const Common::Point &position) {
const bool moved = _cursorEntity->getX() != position.x || _cursorEntity->getY() != position.y;
_cursorEntity->setPosition(position.x, position.y, kCursorEntityZ);
- return _cursorEntity->tickVisualState(getAnimationClockTicks()) || moved;
+ return _cursorEntity->tickVisualState(getRuntimeClockTicks()) || moved;
}
void EntityManager::drawCursor(Graphics::Screen &screen) const {
diff --git a/engines/harvester/runtime_entity.h b/engines/harvester/runtime_entity.h
index 3b1f0a1125b..e8fa15adbc2 100644
--- a/engines/harvester/runtime_entity.h
+++ b/engines/harvester/runtime_entity.h
@@ -35,6 +35,8 @@ namespace Harvester {
class ResourceManager;
+uint32 getRuntimeClockTicks();
+
enum RuntimeEntityClass {
kRuntimeEntityClassObject = 0,
kRuntimeEntityClassAnimation = 1,
Commit: 107798fbad816045c3559a50cbe624f68bd4ee50
https://github.com/scummvm/scummvm/commit/107798fbad816045c3559a50cbe624f68bd4ee50
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:18-04:00
Commit Message:
HARVESTER: Fix depth-scaled walking
Related to #17297 (doesn't fix it entirely)
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
engines/harvester/runtime_entity.cpp
engines/harvester/runtime_entity.h
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 45723423e50..3f058e74409 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -55,7 +55,6 @@ static const float kRoomPlayerAttackUpperYOffset = 144.44f;
static const float kRoomPlayerAttackMidYOffset = 75.36f;
static const char *const kPlayerIdleAnimationEntityName = "IDLE_ANIM";
static const char *const kPlayerIdleAnimationResourcePath = "1:/GRAPHIC/ROOMANIM/PCLOUN02.ABM";
-static const uint32 kRuntimeClockDivisorMs = 10;
static const uint32 kRoomPlayerIdleDelayTicks = 3000;
static const int kPlayerHitKnockbackDistance = 18;
static const int kPlayerHitKnockbackDecayStep = 3;
@@ -1193,7 +1192,7 @@ bool Player::supportsMovementBand(const RoomSetupState &state) {
}
uint32 Player::getRuntimeClockTicks() {
- return g_system ? (g_system->getMillis() / kRuntimeClockDivisorMs) : 0;
+ return Harvester::getRuntimeClockTicks();
}
bool Player::isIdleAnimationExcludedRoom(const Common::String &roomName) {
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index c6291aacdf8..b6607d08c1e 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -180,6 +180,8 @@ bool Entity::loadBitmapResource(ResourceManager &resources, const Common::String
_frames.clear();
_frames.push_back(Common::move(frame));
_baseFrames = _frames;
+ _frameDepthScales.clear();
+ _frameDepthScales.resize(_frames.size(), 1.0f);
_resourcePath = path;
_currentFrame = 0;
_firstFrame = 0;
@@ -233,6 +235,8 @@ bool Entity::loadAbmResource(ResourceManager &resources, const Common::String &p
_resourcePath = path;
_baseFrames = _frames;
+ _frameDepthScales.clear();
+ _frameDepthScales.resize(_frames.size(), 1.0f);
_currentFrame = _frames.empty() ? -1 : 0;
_firstFrame = _currentFrame;
_lastFrame = _frames.empty() ? -1 : (int)_frames.size() - 1;
@@ -369,6 +373,7 @@ void Entity::setAnimationSequence(int sequence) {
void Entity::configureHotspotBounds(int width, int height) {
_frames.clear();
_baseFrames.clear();
+ _frameDepthScales.clear();
_currentFrame = -1;
_firstFrame = -1;
_lastFrame = -1;
@@ -424,11 +429,15 @@ void Entity::setDepthScale(float scale) {
if (fabsf(_depthScale - newScale) < 0.0001f)
return;
- if (_baseFrames.empty())
+ if (_baseFrames.empty()) {
_baseFrames = _frames;
+ _frameDepthScales.clear();
+ _frameDepthScales.resize(_frames.size(), 1.0f);
+ }
_depthScale = newScale;
- rebuildScaledFrames();
+ updateBoundsFromCurrentFrame();
+ updateScreenBaseFromCurrentFrame();
}
bool Entity::tickVisualState(uint32 now) {
@@ -764,6 +773,7 @@ done:
}
void Entity::updateBoundsFromCurrentFrame() {
+ scaleCurrentFrame();
if (_frames.empty()) {
_boundsWidth = 0;
_boundsHeight = 0;
@@ -788,36 +798,31 @@ void Entity::updateScreenBaseFromCurrentFrame() {
_screenBaseY = baseY;
}
-void Entity::rebuildScaledFrames() {
- if (_baseFrames.empty()) {
- _frames.clear();
- updateBoundsFromCurrentFrame();
- updateScreenBaseFromCurrentFrame();
+void Entity::scaleCurrentFrame() {
+ if (_baseFrames.empty() || _currentFrame < 0 ||
+ (uint)_currentFrame >= _baseFrames.size())
return;
- }
- if (fabsf(_depthScale - 1.0f) < 0.0001f) {
- _frames = _baseFrames;
- updateBoundsFromCurrentFrame();
- updateScreenBaseFromCurrentFrame();
+ const uint frameIndex = (uint)_currentFrame;
+ if (_frameDepthScales[frameIndex] == _depthScale)
return;
- }
- _frames.resize(_baseFrames.size());
- for (uint i = 0; i < _baseFrames.size(); ++i) {
- const AbmFrame &source = _baseFrames[i];
- AbmFrame &scaled = _frames[i];
- const int scaledWidth = scaleDimension(source.width, _depthScale);
- const int scaledHeight = scaleDimension(source.height, _depthScale);
-
- scaleIndexedBitmapNearest(source, scaled, scaledWidth, scaledHeight);
+ const AbmFrame &source = _baseFrames[frameIndex];
+ AbmFrame &scaled = _frames[frameIndex];
+ if (fabsf(_depthScale - 1.0f) < 0.0001f) {
+ scaled = source;
+ } else {
+ scaleIndexedBitmapNearest(source, scaled,
+ scaleDimension(source.width, _depthScale),
+ scaleDimension(source.height, _depthScale));
// Native depth scaling preserves the authored horizontal frame offset.
scaled.xOffset = source.xOffset;
scaled.yOffset = roundToInt((float)source.yOffset * _depthScale);
}
-
- updateBoundsFromCurrentFrame();
- updateScreenBaseFromCurrentFrame();
+ _frameDepthScales[frameIndex] = _depthScale;
+ debugC(3, kDebugPlayer,
+ "Harvester: entity depth scale name='%s' frame=%u scale=%.4f size=%ux%u",
+ _name.c_str(), frameIndex, (double)_depthScale, scaled.width, scaled.height);
}
EntityManager::EntityManager(ResourceManager &resources) : _resources(resources) {
diff --git a/engines/harvester/runtime_entity.h b/engines/harvester/runtime_entity.h
index e8fa15adbc2..61fdd0f0b39 100644
--- a/engines/harvester/runtime_entity.h
+++ b/engines/harvester/runtime_entity.h
@@ -136,12 +136,13 @@ private:
void advanceAnimationFrame(int directive);
void updateBoundsFromCurrentFrame();
void updateScreenBaseFromCurrentFrame();
- void rebuildScaledFrames();
+ void scaleCurrentFrame();
Common::String _name;
Common::String _resourcePath;
Common::Array<AbmFrame> _frames;
Common::Array<AbmFrame> _baseFrames;
+ Common::Array<float> _frameDepthScales;
int _classId = 0;
int _x = 0;
int _y = 0;
Commit: 2771b9beb714d49fa4f621320ea7e176597e2aa8
https://github.com/scummvm/scummvm/commit/2771b9beb714d49fa4f621320ea7e176597e2aa8
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:18-04:00
Commit Message:
HARVESTER: Preserve player walking through timer actions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_interaction.cpp
engines/harvester/room_interaction.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index ec3c206a63e..a4ed59b22cc 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -4489,7 +4489,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
bool didTransition = false;
Common::Error interactionError =
interactionProcessor.handleInteractionResult(
- timerInteraction, didTransition, Common::String());
+ timerInteraction, didTransition, Common::String(), false);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
if (flow.hasPendingMainMenuReturn())
diff --git a/engines/harvester/room_interaction.cpp b/engines/harvester/room_interaction.cpp
index b41f6c9f52e..464759091bf 100644
--- a/engines/harvester/room_interaction.cpp
+++ b/engines/harvester/room_interaction.cpp
@@ -51,13 +51,21 @@ RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow
}
Common::Error RoomInteractionProcessor::handleInteractionResult(const InteractionResult &interaction,
- bool &didTransition, const Common::String &usedItemName) {
+ bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement) {
didTransition = false;
- _playerState.hasMoveTarget = false;
- _playerState.turnActive = false;
- _playerState.turnTargetFacing = -1;
- _pendingRegionName.clear();
+ if (interruptPlayerMovement) {
+ _playerState.hasMoveTarget = false;
+ _playerState.turnActive = false;
+ _playerState.turnTargetFacing = -1;
+ _pendingRegionName.clear();
+ } else if (_playerState.hasMoveTarget || _playerState.turnActive) {
+ debugC(2, kDebugPlayer,
+ "Harvester: background action preserving player movement room='%s' pos=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f) region='%s' turning=%d",
+ _scene.state.roomName.c_str(), _playerState.centerX, _playerState.bottomY,
+ _playerState.z, _playerState.targetX, _playerState.targetBottomY,
+ _playerState.targetZ, _pendingRegionName.c_str(), _playerState.turnActive);
+ }
if (interaction.requestDemoEnding) {
Common::Error endingError = _flow.runDemoEnding();
if (endingError.getCode() != Common::kNoError)
@@ -295,7 +303,7 @@ Common::Error RoomInteractionProcessor::handleInteractionResult(const Interactio
interaction.continuationTag, continuationInteraction, true,
_scene.state.roomName)) {
Common::Error interactionError =
- handleInteractionResult(continuationInteraction, didTransition, usedItemName);
+ handleInteractionResult(continuationInteraction, didTransition, usedItemName, interruptPlayerMovement);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
}
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/room_interaction.h
index 28184482c34..0fcdb578441 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/room_interaction.h
@@ -62,7 +62,7 @@ public:
bool canExitCloseupToParent, RoomInteractionCallbacks &callbacks);
Common::Error handleInteractionResult(const InteractionResult &interaction,
- bool &didTransition, const Common::String &usedItemName);
+ bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement = true);
Common::Error runScriptedDialogue(const Common::String &npcName, const Common::String &usedItemName,
const Common::String &continuationTag, bool &didTransition);
Commit: 4e05e6800f9fc37a8ab40d92fcee98ab1ebc2823
https://github.com/scummvm/scummvm/commit/4e05e6800f9fc37a8ab40d92fcee98ab1ebc2823
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:19-04:00
Commit Message:
HARVESTER: Improve animation synchronization
Fixes #17297
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/runtime_entity.cpp
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index a4ed59b22cc..5d07e06f069 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -4372,6 +4372,12 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
}
+ // At 60 Hz, consume animation advances before the next screen update so
+ // a walk frame and its movement step are presented together.
+ if (flow.tickRuntimeEntities())
+ needsRedraw = true;
+ syncAnimatedRoomActorPlacement();
+
Common::Error combatError = Common::kNoError;
if (!playerControlPaused) {
combatError = resolvePlayerAttackContact();
@@ -4463,9 +4469,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return Common::kReadingFailed;
break;
}
- if (flow.tickRuntimeEntities())
- needsRedraw = true;
- syncAnimatedRoomActorPlacement();
if (Player::updateDeathAnimationState(playerState)) {
requestPlayerGameOver("combat_player_death_complete", Common::String());
needsRedraw = true;
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index b6607d08c1e..6b8b08f46ee 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -591,6 +591,11 @@ void Entity::draw(Graphics::Screen &screen) const {
return;
const Common::Point drawOrigin = getDrawOrigin();
+ if (_classId == kRuntimeEntityClassPlayer) {
+ debugC(4, kDebugPlayer,
+ "Harvester: player animation draw frame=%d pos=(%d,%d,z=%.2f) origin=(%d,%d) tick=%u",
+ _currentFrame, _x, _y, _z, drawOrigin.x, drawOrigin.y, getRuntimeClockTicks());
+ }
blitAnimationFrame(screen, _frames, _currentFrame, drawOrigin.x, drawOrigin.y);
}
Commit: 4e6af2292f8570fe3c8012834b4b2fc637b620e1
https://github.com/scummvm/scummvm/commit/4e6af2292f8570fe3c8012834b4b2fc637b620e1
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:19-04:00
Commit Message:
HARVESTER: Preserve player sprite bases across animation frames
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
engines/harvester/player.h
engines/harvester/room.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 3f058e74409..720dbcb0ab2 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -593,17 +593,7 @@ static int computeRoomPlayerVerticalScreenStep() {
}
static int computePlayerFrameLeftX(const RoomPlayerState &playerState) {
- if (!playerState.entity)
- return playerState.centerX;
-
- int width = 0;
- int height = 0;
- int xOffset = 0;
- int yOffset = 0;
- if (!playerState.entity->getCurrentFrameMetrics(width, height, xOffset, yOffset))
- return playerState.centerX;
-
- return playerState.centerX - width / 2;
+ return playerState.entity ? playerState.entity->getScreenRect().left : playerState.centerX;
}
static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
@@ -779,6 +769,17 @@ static void reinsertPlayerSceneEntity(HarvesterEngine &engine, const RoomPlayerS
}
}
+static void applyPlayerMovementPlacement(const RoomSetupState &state, RoomPlayerState &playerState,
+ int candidateCenterX, int candidateBottomY, float candidateZ) {
+ Entity &entity = *playerState.entity;
+ // advance_entity_animation_frame (0x4c8a0) moves the pre-offset screen base.
+ // rescale_entity_sprite_for_depth (0x4b440) changes bounds without recentering it.
+ const float renderZ = candidateZ - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
+ entity.setPosition(entity.getX() + candidateCenterX - playerState.centerX,
+ entity.getY() + candidateBottomY - playerState.bottomY, renderZ);
+ entity.setDepthScale(Player::computeDepthScale(state, renderZ));
+}
+
static bool isPlayerMovementBlocked(HarvesterEngine &engine, const RoomSetupState &state,
const Common::Array<ObjectRecord> &sceneObjects,
const Common::Array<AnimRecord> &sceneAnimations,
@@ -790,19 +791,17 @@ static bool isPlayerMovementBlocked(HarvesterEngine &engine, const RoomSetupStat
if (blockerOut)
*blockerOut = nullptr;
- const int oldCenterX = playerState.centerX;
- const int oldBottomY = playerState.bottomY;
- const float oldZ = playerState.z;
- candidateBottomY = clampRoomMovementY(state, candidateBottomY);
- if (!applyRoomActorPlacement(state, *playerState.entity,
- candidateCenterX, candidateBottomY, candidateZ)) {
- (void)applyRoomActorPlacement(state, *playerState.entity, oldCenterX, oldBottomY, oldZ);
- return true;
- }
+ Entity &entity = *playerState.entity;
+ const int oldX = entity.getX();
+ const int oldY = entity.getY();
+ const float oldZ = entity.getZ();
+ const float oldScale = entity.getDepthScale();
+ applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
const Entity *blocker = findPlayerRoomBlocker(engine, sceneObjects, sceneAnimations,
- *playerState.entity, ignoredBlockerName);
- (void)applyRoomActorPlacement(state, *playerState.entity, oldCenterX, oldBottomY, oldZ);
+ entity, ignoredBlockerName);
+ entity.setPosition(oldX, oldY, oldZ);
+ entity.setDepthScale(oldScale);
if (blockerOut)
*blockerOut = blocker;
return blocker != nullptr;
@@ -820,7 +819,6 @@ static bool tryApplyPlayerMovement(HarvesterEngine &engine, const RoomSetupState
*blockerOut = nullptr;
candidateCenterX = clampPlayerCenterXToNativeBounds(playerState, candidateCenterX);
- candidateBottomY = clampRoomMovementY(state, candidateBottomY);
candidateZ = clampRoomDepth(state, candidateZ);
if (candidateCenterX == playerState.centerX &&
candidateBottomY == playerState.bottomY &&
@@ -833,13 +831,9 @@ static bool tryApplyPlayerMovement(HarvesterEngine &engine, const RoomSetupState
return false;
}
- playerState.centerX = candidateCenterX;
- playerState.bottomY = candidateBottomY;
+ applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
playerState.z = candidateZ;
- if (!applyRoomActorPlacement(state, *playerState.entity,
- playerState.centerX, playerState.bottomY, playerState.z)) {
- return false;
- }
+ Player::syncAnimationPlacement(playerState);
reinsertPlayerSceneEntity(engine, playerState);
return true;
@@ -1120,6 +1114,22 @@ static bool finishPlayerIdleAnimation(const RoomSetupState &state, RoomPlayerSta
} // End of anonymous namespace
+void Player::syncAnimationPlacement(RoomPlayerState &playerState) {
+ if (!playerState.entity)
+ return;
+
+ const Common::Rect rect = playerState.entity->getScreenRect();
+ const int centerX = rect.left + rect.width() / 2;
+ if (playerState.centerX != centerX || playerState.bottomY != rect.bottom) {
+ debugC(3, kDebugPlayer,
+ "Harvester: player frame bounds frame=%d base=(%d,%d) rect=(%d,%d)-(%d,%d) z=%.2f",
+ playerState.entity->getCurrentFrame(), playerState.entity->getX(), playerState.entity->getY(),
+ rect.left, rect.top, rect.right, rect.bottom, (double)playerState.z);
+ }
+ playerState.centerX = centerX;
+ playerState.bottomY = rect.bottom;
+}
+
float Player::computeDepthScale(const RoomSetupState &state, float z) {
float scale = 1.0f;
if (state.roomPerspectiveScale != 0.0f) {
@@ -1760,8 +1770,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
? playerState.z >= (float)state.roomMaxZ
: playerState.z <= (float)state.roomMinZ;
if (!depthBoundaryReached) {
- candidateBottomY = clampRoomMovementY(state,
- playerState.bottomY + depthDirection * verticalStep);
+ candidateBottomY = playerState.bottomY + depthDirection * verticalStep;
candidateZ = regionFacing == 0 || regionFacing == 3
? clampRoomDepth(state, playerState.z - depthDirection * depthStep)
: stepTowardsFloat(playerState.z, playerState.targetZ, depthStep);
@@ -1813,9 +1822,9 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
return setIdleAnimation(playerState, idleFacing);
}
- const int actualFacing = resolveFacingFromRoomMovement(
- previousCenterX, previousBottomY, playerState.centerX, playerState.bottomY);
+ const int actualFacing = desiredFacing;
(void)setPlayerWalkAnimation(playerState, actualFacing);
+ syncAnimationPlacement(playerState);
refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, playerState);
if (engine.isPathfindingDebugEnabled()) {
debugC(1, kDebugPathfinding,
@@ -1873,6 +1882,8 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
return setIdleAnimation(playerState, playerState.facing);
}
+ const int previousBaseX = playerState.entity->getX();
+ const int previousBaseY = playerState.entity->getY();
const int previousCenterX = playerState.centerX;
const int previousBottomY = playerState.bottomY;
const float previousZ = playerState.z;
@@ -1885,15 +1896,12 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
float candidateZ = playerState.z;
bool depthBoundaryReached = false;
if (verticalInput != 0) {
- // update_actor_runtime_state (0x4d750) only emits the paired screen-Y and Z
- // steps for state 0x04 while min_z < z and state 0x0b while z < max_z.
- // At either depth limit it zeros both steps and returns to the facing idle state.
+
depthBoundaryReached = verticalInput < 0
? playerState.z >= (float)state.roomMaxZ
: playerState.z <= (float)state.roomMinZ;
if (!depthBoundaryReached) {
- candidateBottomY = clampRoomMovementY(state,
- playerState.bottomY + verticalInput * verticalStep);
+ candidateBottomY = playerState.bottomY + verticalInput * verticalStep;
candidateZ = clampRoomDepth(state, playerState.z - verticalInput * depthStep);
}
}
@@ -1934,7 +1942,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
const int actualFacing = resolveFacingFromRoomMovement(
- previousCenterX, previousBottomY, playerState.centerX, playerState.bottomY);
+ previousBaseX, previousBaseY, playerState.entity->getX(), playerState.entity->getY());
(void)setPlayerWalkAnimation(playerState, actualFacing);
if (keyboardWalking && currentFrame == range.walkLastFrame) {
playerState.keyboardWalkRequest = -1;
@@ -1943,6 +1951,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
"Harvester: player keyboard walk bank restart facing=%d frame=%d",
actualFacing, playerState.entity->getCurrentFrame());
}
+ syncAnimationPlacement(playerState);
debugC(2, kDebugPlayer,
"Harvester: player keyboard move room='%s' input=(%d,%d) step=(%d,%d) pos=(%d,%d,z=%.2f) facing=%d frame=%d moved=%d",
state.roomName.c_str(), (moveRight ? 1 : 0) - (moveLeft ? 1 : 0),
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index f3d0f102f0a..bdf7d89649a 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -28,6 +28,7 @@ namespace Harvester {
class Player {
public:
+ static void syncAnimationPlacement(RoomPlayerState &playerState);
static float computeDepthScale(const RoomSetupState &state, float z);
static int resolveFacingFrame(int facing);
static const char *describeCombatLoadout(int loadout);
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 5d07e06f069..c8cbdd621e2 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -630,6 +630,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
};
auto captureCurrentSaveState = [&]() {
+ Player::syncAnimationPlacement(playerState);
syncCurrentRoomRuntimeState();
const int facing = playerState.facing >= 0 ? playerState.facing : scene.state.playerFacing;
_engine.captureCurrentSaveRoomState(scene.state.entranceName, scene.state.roomName,
@@ -1382,6 +1383,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.turnTargetFacing = -1;
if (playerState.entity && playerState.facing >= 0)
(void)Player::setIdleAnimation(playerState, playerState.facing);
+ Player::syncAnimationPlacement(playerState);
};
auto refreshCurrentScene = [&](bool preservePlayerPlacement) {
Common::Array<AudioCommand> entryAudioCommands = scene.state.audioCommands;
@@ -1451,10 +1453,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return _inventory.refresh();
};
auto syncAnimatedRoomActorPlacement = [&]() {
- if (playerState.entity) {
- (void)applyRoomActorPlacement(scene.state, *playerState.entity,
- playerState.centerX, playerState.bottomY, playerState.z);
- }
+ Player::syncAnimationPlacement(playerState);
if (!entityManager)
return;
Commit: 78d34939f7fb9eb0e0224309a5276369f2d48f5d
https://github.com/scummvm/scummvm/commit/78d34939f7fb9eb0e0224309a5276369f2d48f5d
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:19-04:00
Commit Message:
HARVESTER: Adjust target depth during sideways walking
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 720dbcb0ab2..53b7a8bbbdc 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -1724,7 +1724,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
}
horizontalDirection = 0;
}
- int depthDirection = horizontalDirection == 0 && !playerState.moveTargetZReached
+ int depthDirection = !playerState.moveTargetZReached
? resolveMoveTargetDepthDirection(playerState, regionFacing)
: 0;
if (depthDirection != 0 && isDepthDirectionBlockedByHistory(playerState, depthDirection)) {
@@ -1763,6 +1763,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
const int horizontalStep = computeRoomPlayerHorizontalStep(state, playerState.z);
candidateCenterX = clampPlayerCenterXToNativeBounds(
playerState, playerState.centerX + horizontalDirection * horizontalStep);
+ candidateZ = clampRoomDepth(state, playerState.z - depthDirection);
} else if (depthDirection != 0) {
const int verticalStep = computeRoomPlayerVerticalScreenStep();
const float depthStep = computeRoomPlayerDepthStep(state);
Commit: 05e5e68b42bc0abcbdb0829efa655e72c296a4d5
https://github.com/scummvm/scummvm/commit/05e5e68b42bc0abcbdb0829efa655e72c296a4d5
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:19-04:00
Commit Message:
HARVESTER: adjust player/npc animation frame logging
Changed paths:
engines/harvester/runtime_entity.cpp
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index 6b8b08f46ee..7f5086a0754 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -455,8 +455,8 @@ bool Entity::tickVisualState(uint32 now) {
_nextAnimationTick = now + _animationTickInterval;
_animationAdvancedLastTick = true;
- if (_classId == kRuntimeEntityClassPlayer) {
- debugC(3, kDebugPlayer,
+ if (_classId == kRuntimeEntityClassPlayer && previousFrameIndex != _currentFrame) {
+ debugC(4, kDebugPlayer,
"Harvester: player animation advance frame=%d->%d range=%d..%d rate=%d tick=%u interval=%u next_tick=%u",
previousFrameIndex, _currentFrame, _firstFrame, _lastFrame,
_animationRate, now, _animationTickInterval, _nextAnimationTick);
@@ -468,7 +468,7 @@ bool Entity::tickVisualState(uint32 now) {
const bool loopReset = _looping && !_pingPong &&
((!wasPlayingBackwards && previousFrameIndex == _lastFrame && _currentFrame == _firstFrame) ||
(wasPlayingBackwards && previousFrameIndex == _firstFrame && _currentFrame == _lastFrame));
- debugC(3, kDebugPlayer,
+ debugC(4, kDebugPlayer,
"Harvester: npc animation advance npc='%s' frame=%d->%d range=%d..%d loop_reset=%d backwards=%d->%d rate=%d interval=%u entity=(%d,%d,z=%.2f) previous=(size=%ux%u offset=%d,%d draw=%d,%d) current=(size=%ux%u offset=%d,%d draw=%d,%d)",
_name.c_str(),
previousFrameIndex, _currentFrame, _firstFrame, _lastFrame,
Commit: af5340ebc0a315a462969277d242cb9ded5e2154
https://github.com/scummvm/scummvm/commit/af5340ebc0a315a462969277d242cb9ded5e2154
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:19-04:00
Commit Message:
HARVESTER: adjust palette fade logging verbosity
Changed paths:
engines/harvester/flow.cpp
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index a7d0c987abd..955890d4e88 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -2507,7 +2507,7 @@ bool Flow::waitForPaletteFadeTick(uint32 startTick, Common::Error &result) {
return true;
g_system->delayMillis(1);
}
- debugC(3, kDebugScene,
+ debugC(4, kDebugScene,
"Harvester: palette fade step tick=%u->%u interval=%u",
startTick, getRuntimeClockTicks(), kPaletteFadeIntervalTicks);
return false;
Commit: 6793425f01a0acb000ea336f2a96ed9d433e8aba
https://github.com/scummvm/scummvm/commit/6793425f01a0acb000ea336f2a96ed9d433e8aba
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-07T06:22:19-04:00
Commit Message:
HARVESTER: fix menu options overlap on game over
Fixes #17290
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/menu.cpp
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 1fd4fc5c7a1..a788bad1412 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -977,8 +977,11 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
if (selectedItem == kMainMenuItemOptions) {
IndexedBitmap menuBackdrop;
- if (!captureMenuBackdrop(menuBackdrop))
+ if (_hasMainMenuBackdrop) {
+ menuBackdrop = _mainMenuBackdrop;
+ } else if (!captureMenuBackdrop(menuBackdrop)) {
return Common::kReadingFailed;
+ }
Common::Error optionsError = runOptionsMenu(menuBackdrop, menuPalette, 1.0f, flow);
needsRedraw = true;
return optionsError;
More information about the Scummvm-git-logs
mailing list