[Scummvm-git-logs] scummvm master -> c2412793813a3aacc1023ee63b25cdb44ba028cd
alexbevi
noreply at scummvm.org
Mon Oct 19 23:03:33 UTC 2026
This automated email contains information about 30 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
33dbffedb3 HARVESTER: Add remappable movement and combat controls
a1d6761463 HARVESTER: Add remappable attack, duck and jump actions
255088e37d HARVESTER: Separate town script record parsing
1f1f13ca21 HARVESTER: Split command execution into opcode handlers
4dd258fce8 HARVESTER: Separate room state materialization
3b56649b1a HARVESTER: Separate script save and reload handling
29bc2c2283 HARVESTER: Separate script runtime and actor state handling
32b820a747 HARVESTER: Show IDENT text before non-operatable object actions
a8a8ddc7c2 HARVESTER: Separate menu presentation and text configuration
eb8f943def HARVESTER: Separate options menu actions and event handling
981f404499 HARVESTER: reset hidden-to-visible ANIMs
f73b790460 HARVESTER: Retry missed melee contact until the attack ends
e1d9798441 HARVESTER: Correct player depth walking and room limits
cfeac713a6 HARVESTER: Separate main and room menu navigation
78e23bd564 HARVESTER: Separate save menus and modal prompts
7dbf808e6e HARVESTER: Separate combat targeting and reach geometry
2110408162 HARVESTER: Separate combat hit and death transitions
ab8709bc89 HARVESTER: Separate monster pursuit and attack progression
edf006a381 HARVESTER: Separate player attack contact handling
00f6720ad4 HARVESTER: Separate combat effects and HUD text
a56f1982e4 HARVESTER: Separate player movement and collision helpers
6be6653618 HARVESTER: Separate player target walking
3d8586a32d HARVESTER: Separate player keyboard walking
19a46f3107 HARVESTER: Separate player combat animation
58a5bca82e HARVESTER: Separate player lifecycle and idle animation
b8478f4522 HARVESTER: Separate flow startup diagnostics
bef83f3ebf HARVESTER: Separate town map selection and event handling
998e763b93 HARVESTER: Separate startup overlays and transitions
1e5757bcf2 HARVESTER: Separate room population and actor placement
c241279381 HARVESTER: Organize subsystem sources into directories
Commit: 33dbffedb31d929a180ed6573bad73f430784162
https://github.com/scummvm/scummvm/commit/33dbffedb31d929a180ed6573bad73f430784162
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Add remappable movement and combat controls
Fixes #17295
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/input.cpp
engines/harvester/input.h
engines/harvester/metaengine.cpp
engines/harvester/metaengine.h
engines/harvester/player.cpp
engines/harvester/player.h
engines/harvester/scene.cpp
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 4a2122367f9..7d13ef95605 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -371,10 +371,7 @@ bool Input::openInventoryOverlay() {
Player &player = _session.getPlayer();
InputState &inputState = _session.getInputState();
- inputState.moveLeft = false;
- inputState.moveRight = false;
- inputState.moveUp = false;
- inputState.moveDown = false;
+ inputState.clearHeldActions();
_session.clearPendingRegionInteraction();
(void)_session.cancelPlayerAttackAnimation();
player.cancelMovement();
@@ -675,6 +672,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
if (_roomItems.hasSelectedItem()) {
const Common::String selectedItemName = _roomItems.getSelectedItemName();
if (clickHoverState.npc) {
+ inputState.clearHeldActions();
_roomItems.clearSelectedItem();
bool didTransition = false;
Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
@@ -717,10 +715,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
}
if (hoverState.npc) {
_session.clearPendingRegionInteraction();
- inputState.moveLeft = false;
- inputState.moveRight = false;
- inputState.moveUp = false;
- inputState.moveDown = false;
+ inputState.clearHeldActions();
player.cancelMovement();
bool didTransition = false;
Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
@@ -847,10 +842,28 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
return InputResult();
}
-InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw) {
+InputResult Input::handleGameAction(GameAction action, bool pressed, bool &needsRedraw) {
+ static const Common::KeyCode keys[] = {
+ Common::KEYCODE_LEFT, Common::KEYCODE_RIGHT, Common::KEYCODE_UP, Common::KEYCODE_DOWN,
+ Common::KEYCODE_LCTRL, Common::KEYCODE_RSHIFT, Common::KEYCODE_LALT,
+ Common::KEYCODE_i, Common::KEYCODE_ESCAPE
+ };
+ if ((uint)action >= ARRAYSIZE(keys))
+ return InputResult();
+ const Common::KeyState key(keys[action]);
+ if (pressed)
+ return handleKeyDown(key, needsRedraw, true);
+ handleKeyUp(key, true);
+ return InputResult();
+}
+
+InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw, bool gameAction) {
Player &player = _session.getPlayer();
const RoomIdleAnimationState &idleState = player.getIdleState();
InputState &inputState = _session.getInputState();
+ // GOG update_actor_runtime_state (0x4d750) tests the last scan code,
+ // not the held Alt slot. Any other key press or release clears fast speed.
+ inputState.speedUpActive = gameAction && key.keycode == Common::KEYCODE_LALT;
if (_presentationState.showingInspectText) {
if (key.keycode == Common::KEYCODE_ESCAPE) {
@@ -863,8 +876,6 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
}
_session.notePlayerActivity();
- if (key.hasFlags(Common::KBD_CTRL))
- inputState.attackModifierHeld = true;
if (idleState.active || idleState.exiting) {
if (player.requestIdleAnimationExit(_scene.state))
needsRedraw = true;
@@ -931,39 +942,38 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
return InputResult();
}
}
+ if (gameAction && key.keycode == Common::KEYCODE_LCTRL) {
+ inputState.attackModifierHeld = true;
+ } else if (gameAction && key.keycode == Common::KEYCODE_RSHIFT) {
+ inputState.evasiveModifierHeld = true;
+ } else if (gameAction && key.keycode == Common::KEYCODE_LEFT)
+ inputState.moveLeft = true;
+ else if (gameAction && key.keycode == Common::KEYCODE_RIGHT)
+ inputState.moveRight = true;
+ else if (gameAction && key.keycode == Common::KEYCODE_UP)
+ inputState.moveUp = true;
+ else if (gameAction && key.keycode == Common::KEYCODE_DOWN)
+ inputState.moveDown = true;
+
if (_session.isPlayerCombatLocked())
return InputResult();
- if (key.keycode == Common::KEYCODE_LCTRL ||
- key.keycode == Common::KEYCODE_RCTRL) {
- inputState.attackModifierHeld = true;
- } else if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
+ if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
Common::Error debugKillError = _session.killDebugActiveMonster(_flow.getMousePos(), needsRedraw);
if (debugKillError.getCode() != Common::kNoError)
return InputResult::leaveRoom(debugKillError);
return InputResult();
- } else if (key.keycode == Common::KEYCODE_LEFT)
- inputState.moveLeft = true;
- else if (key.keycode == Common::KEYCODE_RIGHT)
- inputState.moveRight = true;
- else if (key.keycode == Common::KEYCODE_UP)
- inputState.moveUp = true;
- else if (key.keycode == Common::KEYCODE_DOWN)
- inputState.moveDown = true;
- else if (key.keycode == Common::KEYCODE_i) {
+ } else if (gameAction && key.keycode == Common::KEYCODE_i) {
if (!openInventoryOverlay())
return InputResult::leaveRoom(Common::kReadingFailed);
needsRedraw = true;
return InputResult();
- } else if (key.keycode == Common::KEYCODE_ESCAPE) {
+ } else if (gameAction && key.keycode == Common::KEYCODE_ESCAPE) {
if (_session.cancelPlayerAttackAnimation())
needsRedraw = true;
if (!_roomItems.stowCarriedRoomItemToInventory())
return InputResult::leaveRoom(Common::kReadingFailed);
- inputState.moveLeft = false;
- inputState.moveRight = false;
- inputState.moveUp = false;
- inputState.moveDown = false;
+ inputState.clearHeldActions();
_session.clearPendingRegionInteraction();
player.cancelMovement();
_session.captureCurrentSaveState();
@@ -997,12 +1007,17 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
return InputResult();
}
-void Input::handleKeyUp(const Common::KeyState &key) {
+void Input::handleKeyUp(const Common::KeyState &key, bool gameAction) {
InputState &inputState = _session.getInputState();
+ inputState.speedUpActive = false;
+ if (!gameAction)
+ return;
if (key.keycode == Common::KEYCODE_LCTRL ||
key.keycode == Common::KEYCODE_RCTRL)
inputState.attackModifierHeld = false;
+ else if (key.keycode == Common::KEYCODE_RSHIFT)
+ inputState.evasiveModifierHeld = false;
else if (key.keycode == Common::KEYCODE_LEFT)
inputState.moveLeft = false;
else if (key.keycode == Common::KEYCODE_RIGHT)
@@ -1019,6 +1034,10 @@ InputResult Input::handleEvent(const Common::Event &event, bool &needsRedraw) {
return InputResult::leaveRoom(result);
switch (event.type) {
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
+ case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
+ return handleGameAction((GameAction)event.customType,
+ event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_START, needsRedraw);
case Common::EVENT_MOUSEMOVE:
handleMouseMove(needsRedraw);
break;
diff --git a/engines/harvester/input.h b/engines/harvester/input.h
index 224722a90d6..ac340b018e2 100644
--- a/engines/harvester/input.h
+++ b/engines/harvester/input.h
@@ -48,14 +48,33 @@ struct RoomPresentationState;
struct RoomSetupState;
struct SceneState;
+enum GameAction {
+ kActionMoveLeft,
+ kActionMoveRight,
+ kActionMoveUp,
+ kActionMoveDown,
+ kActionAttackModifier,
+ kActionEvasiveModifier,
+ kActionSpeedUp,
+ kActionInventory,
+ kActionMenu
+};
+
struct InputState {
bool moveLeft = false;
bool moveRight = false;
bool moveUp = false;
bool moveDown = false;
bool attackModifierHeld = false;
+ bool evasiveModifierHeld = false;
+ bool speedUpActive = false;
uint32 lastLeftButtonReleaseTick = 0;
uint32 nextKeyboardAttackAllowedTick = 0;
+
+ void clearHeldActions() {
+ moveLeft = moveRight = moveUp = moveDown = false;
+ attackModifierHeld = evasiveModifierHeld = speedUpActive = false;
+ }
};
// Enter and closeup exits return from runRoomLoop even when the error is kNoError.
@@ -95,8 +114,9 @@ private:
InputResult handleRightButtonDown(bool &needsRedraw);
void handleLeftButtonUp();
InputResult handleLeftButtonDown(bool &needsRedraw);
- InputResult handleKeyDown(const Common::KeyState &key, bool &needsRedraw);
- void handleKeyUp(const Common::KeyState &key);
+ InputResult handleKeyDown(const Common::KeyState &key, bool &needsRedraw, bool gameAction = false);
+ void handleKeyUp(const Common::KeyState &key, bool gameAction = false);
+ InputResult handleGameAction(GameAction action, bool pressed, bool &needsRedraw);
void beginInspectText(const ObjectRecord &object, const ResolvedText &text);
void queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw);
bool openInventoryOverlay();
diff --git a/engines/harvester/metaengine.cpp b/engines/harvester/metaengine.cpp
index 58f4a4b2053..a91f4f8dbd7 100644
--- a/engines/harvester/metaengine.cpp
+++ b/engines/harvester/metaengine.cpp
@@ -20,11 +20,15 @@
*/
#include "base/plugins.h"
+#include "backends/keymapper/action.h"
+#include "backends/keymapper/keymap.h"
+#include "backends/keymapper/standard-actions.h"
#include "common/translation.h"
#include "harvester/detection.h"
#include "harvester/metaengine.h"
#include "harvester/harvester.h"
+#include "harvester/input.h"
static const ADExtraGuiOptionsMap gameGuiOptions[] = {
{
@@ -57,6 +61,54 @@ const char *HarvesterMetaEngine::getName() const {
return "harvester";
}
+Common::KeymapArray HarvesterMetaEngine::initKeymaps(const char *target) const {
+ using namespace Common;
+ using namespace Harvester;
+
+ KeymapArray keymaps = MetaEngine::initKeymaps(target);
+ Keymap *gameplay = new Keymap(Keymap::kKeymapTypeGame, "harvester-gameplay", _("Gameplay"));
+ Action *click = new Action(kStandardActionLeftClick, _("Left click"));
+ click->setLeftClickEvent();
+ click->addDefaultInputMapping("MOUSE_LEFT");
+ click->addDefaultInputMapping("JOY_A");
+ gameplay->addAction(click);
+ click = new Action(kStandardActionRightClick, _("Right click"));
+ click->setRightClickEvent();
+ click->addDefaultInputMapping("MOUSE_RIGHT");
+ click->addDefaultInputMapping("JOY_B");
+ gameplay->addAction(click);
+ const struct {
+ const char *id;
+ const char *description;
+ GameAction action;
+ const char *mapping;
+ const char *alternateMapping;
+ } bindings[] = {
+ { "LEFT", _s("Move left"), kActionMoveLeft, "LEFT", "JOY_LEFT" },
+ { "RIGHT", _s("Move right"), kActionMoveRight, "RIGHT", "JOY_RIGHT" },
+ { "UP", _s("Move up"), kActionMoveUp, "UP", "JOY_UP" },
+ { "DOWN", _s("Move down"), kActionMoveDown, "DOWN", "JOY_DOWN" },
+ { "ATTACK", _s("Attack modifier"), kActionAttackModifier, "LCTRL", "RCTRL" },
+ { "EVADE", _s("Evasive movement modifier"), kActionEvasiveModifier, "RSHIFT", nullptr },
+ { "SPEEDUP", _s("Speed up"), kActionSpeedUp, "LALT", "RALT" },
+ { "INVENTORY", _s("Inventory"), kActionInventory, "i", nullptr },
+ { "MENU", _s("Game menu"), kActionMenu, "ESCAPE", "JOY_Y" }
+ };
+ for (const auto &binding : bindings) {
+ Action *action = new Action(binding.id, _(binding.description));
+ action->setCustomEngineActionEvent(binding.action);
+ if (binding.action <= kActionSpeedUp)
+ action->allowKbdRepeats();
+ action->addDefaultInputMapping(binding.mapping);
+ if (binding.alternateMapping)
+ action->addDefaultInputMapping(binding.alternateMapping);
+ gameplay->addAction(action);
+ }
+ gameplay->setEnabled(false);
+ keymaps.push_back(gameplay);
+ return keymaps;
+}
+
const ADExtraGuiOptionsMap *HarvesterMetaEngine::getAdvancedExtraGuiOptions() const {
return gameGuiOptions;
}
diff --git a/engines/harvester/metaengine.h b/engines/harvester/metaengine.h
index 57644f2b606..9d9b95b9090 100644
--- a/engines/harvester/metaengine.h
+++ b/engines/harvester/metaengine.h
@@ -28,6 +28,7 @@ class HarvesterMetaEngine : public AdvancedMetaEngine<ADGameDescription> {
public:
const char *getName() const override;
const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override;
+ Common::Array<Common::Keymap *> initKeymaps(const char *target) const override;
Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
int getMaximumSaveSlot() const override;
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 5829792cb05..cbec6ab7dc2 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -45,13 +45,9 @@ static const int kRoomRegionTargetXBias = 10;
static const float kRoomDepthCompareEpsilon = 0.01f;
static const int kRoomPlayerWalkAnimationRate = 17;
static const int kRoomPlayerVerticalScreenStep = 1;
-static const int kRoomPlayerAttackAnimationRate = kRoomPlayerWalkAnimationRate;
+static const int kRoomPlayerFastAnimationRate = 30;
static const int kRoomPlayerHitAnimationRate = 4;
-// update_actor_runtime_state (0x4d750) seeds a zero-HP transition at 0x4e576
-// with rate 14, then death states 0x28..0x33 consume the normal rate 17 seeded
-// at 0x4d889 after the first death frame advances.
static const int kNativePlayerDeathEntryAnimationRate = 14;
-static const int kNativePlayerDeathAnimationRate = kRoomPlayerWalkAnimationRate;
static const int kRoomPlayerMinOpaqueLeftX = 4;
static const int kRoomPlayerMaxOpaqueRightX = 0x27c;
static const float kRoomPlayerAttackUpperYOffset = 144.44f;
@@ -61,7 +57,6 @@ static const char *const kPlayerIdleAnimationResourcePath = "1:/GRAPHIC/ROOMANIM
static const uint32 kRoomPlayerIdleDelayTicks = 3000;
static const int kPlayerHitKnockbackDistance = 18;
static const int kPlayerHitKnockbackDecayStep = 3;
-// Native run_harvester_main_loop sets EBX=0x0e before spawning PCLOUN02.ABM.
static const int kRoomPlayerIdleAnimationRate = 14;
static const int kRoomPlayerIdleLoopStartFrame = 0x0f;
static const int kRoomPlayerIdleLoopLastFrame = 0xb2;
@@ -130,10 +125,6 @@ struct PlayerCombatLoadoutTuning {
int contactFrameOffset;
};
-// The live player avatar seeds these from RenderEntityRuntime.player_combat_loadout_id (+0x11bc)
-// into attack_damage_amount (+0x1180) and attack_contact_frame_offset (+0x113c).
-// Recovered from spawn_player_combat_avatar and update_actor_runtime_state:
-// the native room-combat path commits player attack audio on first_frame + 1.
static const int kPlayerAttackSoundTriggerFrameOffset = 1;
static const PlayerAttackSoundSet kPlayerAttackSoundSets[] = {
{ 3, { "2:/sound/effects/swoosh1.wav", "2:/sound/effects/swoosh2.wav", "2:/sound/effects/swoosh3.wav" } },
@@ -319,8 +310,6 @@ static bool resolvePlayerDeathAnimationRange(const RoomPlayerState &playerState,
if (!playerState.entity)
return false;
- // Native update_actor_runtime_state selects the player death bank from the
- // live combat state family before the terminal 0x38 game-over handoff.
enum PlayerDeathBankFamily {
kPlayerDeathBankHorizontalRight,
kPlayerDeathBankHorizontalLeft,
@@ -421,9 +410,6 @@ static bool resolvePlayerAttackAnimationRange(const RoomSetupState &state,
static bool resolveKeyboardAttackAnimationRange(const RoomPlayerState &playerState,
bool attackLeft, bool attackRight, bool attackUp, bool attackDown,
PlayerAttackAnimationRange &range) {
- // Native update_player_combat_avatar_state uses scan-code 0x1d as the attack modifier:
- // Left/Right pick the side mid attacks directly, while Up/Down only choose upper/lower
- // attacks when the current actor state is already in the matching horizontal family.
if (attackRight) {
range = PlayerAttackAnimationRange(0x78, 0x81, 2);
return true;
@@ -497,6 +483,8 @@ static bool startResolvedAttackAnimation(RoomPlayerState &playerState,
return false;
playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
playerState.turnFirstFrame = -1;
@@ -516,7 +504,7 @@ static bool startResolvedAttackAnimation(RoomPlayerState &playerState,
playerState.attackContactResolved = false;
playerState.nextMovementTick = 0;
playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- playerState.entity->setAnimationRate(kRoomPlayerAttackAnimationRate);
+ playerState.entity->setAnimationRate(playerState.animationRate);
playerState.entity->setCurrentFrame(range.firstFrame);
return true;
}
@@ -589,9 +577,6 @@ static float computeRoomPlayerDepthStep(const RoomSetupState &state) {
}
static int computeRoomPlayerVerticalScreenStep() {
- // Native spawn_player_combat_avatar seeds player runtime +0x1140 with 1,
- // and update_actor_runtime_state copies abs(+0x1140) into frame_y_step for
- // the vertical walk states instead of remapping screen Y from depth.
return kRoomPlayerVerticalScreenStep;
}
@@ -676,7 +661,7 @@ static bool consumePlayerMovementTick(RoomPlayerState &playerState) {
return playerState.entity->didAnimationAdvanceLastTick();
}
- const uint32 interval = computeAnimationTickInterval(kRoomPlayerWalkAnimationRate);
+ const uint32 interval = computeAnimationTickInterval(playerState.animationRate);
if (interval == 0) {
playerState.nextMovementTick = 0;
return true;
@@ -772,8 +757,6 @@ 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);
@@ -969,7 +952,7 @@ static void playPlayerBlockedWalkSounds(HarvesterEngine &engine, const RoomSetup
const RoomPlayerState &playerState, const Entity &blocker) {
const Entity &entity = *playerState.entity;
const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
- if (entity.getAnimationRate() != kRoomPlayerWalkAnimationRate ||
+ if (entity.getAnimationRate() != playerState.animationRate ||
entity.getCurrentFrame() < range.walkFirstFrame ||
entity.getCurrentFrame() > range.walkLastFrame)
return;
@@ -1010,8 +993,6 @@ static bool tryApplyPlayerWalkMovement(HarvesterEngine &engine, const RoomSetupS
fabsf(candidateZ - playerState.z) <= kRoomDepthCompareEpsilon)
return false;
- // tick_entity_visual_state (0x4cae0) records overlap after placement.
- // Region activation gets the resulting frame before the next blocker response.
applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
playerState.z = candidateZ;
syncPlayerAnimationPlacement(playerState);
@@ -1053,17 +1034,17 @@ static bool setPlayerWalkAnimation(RoomPlayerState &playerState, int facing) {
const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
const int currentFrame = playerState.entity->getCurrentFrame();
const bool alreadyWalking = playerState.facing == facing &&
- playerState.entity->getAnimationRate() == kRoomPlayerWalkAnimationRate &&
+ playerState.entity->getAnimationRate() == playerState.animationRate &&
currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
if (alreadyWalking)
return false;
playerState.facing = facing;
playerState.entity->setAnimationFrameRange(range.walkFirstFrame, range.walkLastFrame, true);
- playerState.entity->setAnimationRate(kRoomPlayerWalkAnimationRate);
+ playerState.entity->setAnimationRate(playerState.animationRate);
debugC(1, kDebugPlayer,
"Harvester: player walk animation facing=%d frames=%d..%d rate=%d",
- facing, range.walkFirstFrame, range.walkLastFrame, kRoomPlayerWalkAnimationRate);
+ facing, range.walkFirstFrame, range.walkLastFrame, playerState.animationRate);
return true;
}
@@ -1161,6 +1142,8 @@ static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing) {
playerState.entity->getCurrentFrame() != range.idleFrame ||
playerState.entity->getAnimationRate() != 0;
playerState.facing = facing;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.nextMovementTick = 0;
playerState.keyboardWalkRequest = -1;
playerState.walkHorizontalDirection = 0;
@@ -1186,16 +1169,20 @@ void Player::initialize(const RoomSetupState &state, Entity *entity, int combatL
playerState.z = (float)state.playerSpawnZ;
playerState.facing = state.playerFacing;
playerState.combatLoadout = combatLoadout;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
}
bool Player::isCombatLocked() const {
- return _state.attackActive || _state.hitActive || _state.deathActive;
+ return _state.attackActive || _state.hitActive || _state.deathActive || _state.evasiveState >= 0;
}
void Player::cancelMovement() {
_state.hasMoveTarget = false;
+ _state.evasiveState = -1;
+ _state.evasiveXStep = 0;
_state.turnActive = false;
_state.turnTargetFacing = -1;
}
@@ -1277,6 +1264,8 @@ void Player::cancelIdleAnimation() {
void Player::applyGotoXZ(const RoomSetupState &state, EntityManager *entityManager, int x, int z) {
RoomPlayerState &playerState = _state;
playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
playerState.centerX = x;
@@ -1323,6 +1312,8 @@ void Player::rebindAfterSceneRefresh(HarvesterEngine &engine, const RoomSetupSta
// Death state survives a scene refresh.
playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
playerState.attackActive = false;
@@ -1600,6 +1591,107 @@ bool Player::setIdleAnimation(int facing) {
return setPlayerIdleAnimation(_state, facing);
}
+bool Player::updateAnimationRate(bool speedUp) {
+ RoomPlayerState &playerState = _state;
+ const int rate = speedUp ? kRoomPlayerFastAnimationRate : kRoomPlayerWalkAnimationRate;
+ if (playerState.animationRate == rate)
+ return false;
+ playerState.animationRate = rate;
+ if (playerState.entity && !playerState.hitActive && !playerState.deathActive &&
+ playerState.entity->getAnimationRate() != 0)
+ playerState.entity->setAnimationRate(rate);
+ debugC(2, kDebugPlayer, "Harvester: player actor rate=%d", rate);
+ return true;
+}
+
+bool Player::startEvasiveAnimation(bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.attackActive || playerState.hitActive || playerState.deathActive ||
+ playerState.evasiveState >= 0)
+ return false;
+ playerState.hasMoveTarget = false;
+ playerState.keyboardWalkRequest = -1;
+ if (playerState.turnActive)
+ return false;
+ const PlayerAnimationRange walk = resolvePlayerAnimationRange(playerState.facing);
+ const int frame = playerState.entity->getCurrentFrame();
+ if (playerState.entity->getAnimationRate() != 0 &&
+ frame >= walk.walkFirstFrame && frame < walk.walkLastFrame)
+ return false;
+
+ // Right Shift branch:
+ // vertical requests select duck/leap banks; horizontal requests can override them.
+ int actorState = -1;
+ if (playerState.facing == 1 || playerState.facing == 2) {
+ if (moveUp)
+ actorState = playerState.facing == 2 ? 0x12 : 0x13;
+ else if (moveDown)
+ actorState = playerState.facing == 2 ? 0x14 : 0x15;
+ if (moveLeft) {
+ if (playerState.facing == 2)
+ actorState = 0x08;
+ } else if (moveRight && playerState.facing == 1) {
+ actorState = 0x0f;
+ }
+ }
+ if (actorState < 0)
+ return false;
+
+ const bool backwards = actorState == 0x08 || actorState == 0x0f;
+ const int firstFrame = actorState == 0x08 ? 0x2d : actorState == 0x0f ? 0x0f :
+ 0x3c + (actorState - 0x12) * 5;
+ const int lastFrame = firstFrame + (backwards ? 9 : 4);
+ if (!playerEntityHasFrameRange(*playerState.entity, firstFrame, lastFrame))
+ return false;
+ playerState.evasiveState = actorState;
+ playerState.evasiveFirstFrame = firstFrame;
+ playerState.evasiveLastFrame = lastFrame;
+ playerState.evasiveXStep = backwards
+ ? (int)(playerState.walkHorizontalDirection * playerState.entity->getDepthScale() * 8.0f) : 0;
+ playerState.walkHorizontalDirection = 0;
+ playerState.walkDepthDirection = 0;
+ playerState.nextMovementTick = 0;
+ playerState.entity->setAnimationFrameRange(firstFrame, lastFrame, false);
+ playerState.entity->setPlayBackwards(backwards);
+ playerState.entity->setAnimationRate(playerState.animationRate);
+ playerState.entity->setCurrentFrame(backwards ? lastFrame : firstFrame);
+ debugC(2, kDebugPlayer, "Harvester: player evasive state=0x%x facing=%d frames=%d..%d backwards=%d",
+ actorState, playerState.facing, firstFrame, lastFrame, backwards);
+ return true;
+}
+
+bool Player::updateEvasiveAnimation(HarvesterEngine &engine) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.evasiveState < 0 ||
+ !playerState.entity->didAnimationAdvanceLastTick())
+ return false;
+
+ Entity &entity = *playerState.entity;
+ const int actorState = playerState.evasiveState;
+ if (playerState.evasiveXStep != 0) {
+ entity.setPosition(entity.getX() + playerState.evasiveXStep, entity.getY(), entity.getZ());
+ syncAnimationPlacement();
+ reinsertPlayerSceneEntity(engine, playerState);
+ debugC(3, kDebugPlayer, "Harvester: player evasive step state=0x%x frame=%d dx=%d",
+ actorState, entity.getCurrentFrame(), playerState.evasiveXStep);
+ }
+ const bool backwards = actorState == 0x08 || actorState == 0x0f;
+ if (entity.getCurrentFrame() == (backwards ? playerState.evasiveFirstFrame : playerState.evasiveLastFrame)) {
+ debugC(2, kDebugPlayer, "Harvester: player evasive complete state=0x%x facing=%d",
+ actorState, playerState.facing);
+ return setIdleAnimation(playerState.facing);
+ }
+
+ const int frameLeft = computePlayerFrameLeftX(playerState);
+ if ((actorState == 0x08 && frameLeft > 20) || (actorState == 0x0f && frameLeft < 620)) {
+ playerState.evasiveXStep = (int)(entity.getDepthScale() * (actorState == 0x08 ? -8.0f : 8.0f));
+ } else if (entity.getCurrentFrame() > playerState.evasiveFirstFrame + 2 &&
+ ((actorState == 0x12 && frameLeft > 36) || (actorState == 0x13 && frameLeft < 616))) {
+ playerState.evasiveXStep = (int)(entity.getDepthScale() * (actorState == 0x12 ? -16.0f : 16.0f));
+ }
+ return true;
+}
+
bool Player::startAttackAnimation(const RoomSetupState &state, const Common::Point &mousePos) {
RoomPlayerState &playerState = _state;
PlayerAttackAnimationRange range;
@@ -1687,6 +1779,8 @@ bool Player::startHitAnimation(HarvesterEngine &engine, int monsterAttackFirstFr
return false;
playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
playerState.turnFirstFrame = -1;
@@ -1771,6 +1865,8 @@ bool Player::startDeathAnimation(int damageType, bool goreEnabled) {
return false;
playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
playerState.turnFirstFrame = -1;
@@ -1809,7 +1905,7 @@ bool Player::startDeathAnimation(int damageType, bool goreEnabled) {
"Harvester: player death animation timing frames=%d..%d entry_rate=%d entry_interval_ticks=%d running_rate=%d running_interval_ticks=%d",
range.firstFrame, range.lastFrame,
kNativePlayerDeathEntryAnimationRate, 100 / kNativePlayerDeathEntryAnimationRate,
- kNativePlayerDeathAnimationRate, 100 / kNativePlayerDeathAnimationRate);
+ playerState.animationRate, 100 / playerState.animationRate);
return true;
}
@@ -1821,12 +1917,12 @@ bool Player::updateDeathAnimationState() {
const int currentFrame = playerState.entity->getCurrentFrame();
if (currentFrame > playerState.deathFirstFrame &&
currentFrame < playerState.deathLastFrame &&
- playerState.entity->getAnimationRate() != kNativePlayerDeathAnimationRate) {
- playerState.entity->setAnimationRate(kNativePlayerDeathAnimationRate);
+ playerState.entity->getAnimationRate() != playerState.animationRate) {
+ playerState.entity->setAnimationRate(playerState.animationRate);
debugC(3, kDebugCombat,
"Harvester: player death animation running frame=%d rate=%d interval_ticks=%d",
- currentFrame, kNativePlayerDeathAnimationRate,
- 100 / kNativePlayerDeathAnimationRate);
+ currentFrame, playerState.animationRate,
+ 100 / playerState.animationRate);
}
if (currentFrame < playerState.deathLastFrame)
@@ -1841,7 +1937,7 @@ bool Player::updateDeathAnimationState() {
bool Player::startTurnAnimation(int targetFacing) {
RoomPlayerState &playerState = _state;
- if (!playerState.entity || playerState.hitActive ||
+ if (!playerState.entity || playerState.hitActive || playerState.evasiveState >= 0 ||
playerState.facing < 0 || playerState.facing == targetFacing)
return false;
@@ -1868,12 +1964,12 @@ bool Player::startTurnAnimation(int targetFacing) {
playerState.nextMovementTick = 0;
playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
playerState.entity->setPlayBackwards(range.playBackwards);
- playerState.entity->setAnimationRate(kRoomPlayerWalkAnimationRate);
+ playerState.entity->setAnimationRate(playerState.animationRate);
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 requested_facing=%d",
playerState.facing, nextFacing, range.firstFrame, range.lastFrame,
- range.playBackwards, kRoomPlayerWalkAnimationRate, targetFacing);
+ range.playBackwards, playerState.animationRate, targetFacing);
return true;
}
@@ -1902,20 +1998,16 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
int regionFacing) {
RoomPlayerState &playerState = _state;
- if (!playerState.entity || !playerState.hasMoveTarget || playerState.turnActive || playerState.hitActive)
+ if (!playerState.entity || !playerState.hasMoveTarget || playerState.turnActive ||
+ playerState.hitActive || playerState.evasiveState >= 0)
return false;
refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, 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.
const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
const int currentFrame = playerState.entity->getCurrentFrame();
- const bool walking = playerState.entity->getAnimationRate() == kRoomPlayerWalkAnimationRate &&
+ const bool walking = playerState.entity->getAnimationRate() == playerState.animationRate &&
currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
- // update_actor_runtime_state (0x4d750) evaluates waypoints only at a bank
- // endpoint. Between endpoints the current walk state retains frame_z_step.
const bool updateTarget = !walking || currentFrame == range.walkLastFrame;
if (updateTarget)
updatePlayerMoveTargetProgress(state, playerState, regionFacing);
@@ -2070,7 +2162,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
RoomPlayerState &playerState = _state;
if (!playerState.entity || !supportsMovementBand(state))
return false;
- if (playerState.turnActive || playerState.hitActive)
+ if (playerState.turnActive || playerState.hitActive || playerState.evasiveState >= 0)
return false;
int horizontalInput = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
@@ -2078,7 +2170,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
const int currentFrame = playerState.entity->getCurrentFrame();
const bool keyboardWalking = !playerState.hasMoveTarget &&
- playerState.entity->getAnimationRate() == kRoomPlayerWalkAnimationRate &&
+ playerState.entity->getAnimationRate() == playerState.animationRate &&
currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
if (keyboardWalking) {
if (horizontalInput != 0 || verticalInput != 0)
@@ -2221,6 +2313,8 @@ bool Player::startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &s
idleEntity->setCurrentFrame(0);
idleEntity->setAnimationRate(kRoomPlayerIdleAnimationRate);
playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
playerState.entity->setVisible(false);
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index 25cacd7a423..d9d81b80abd 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -35,6 +35,11 @@ struct RoomPlayerState {
float z = 0.0f;
int facing = -1;
int combatLoadout = 0;
+ int animationRate = 17;
+ int evasiveState = -1;
+ int evasiveFirstFrame = -1;
+ int evasiveLastFrame = -1;
+ int evasiveXStep = 0;
uint32 nextMovementTick = 0;
int keyboardWalkRequest = -1;
int walkHorizontalDirection = 0;
@@ -138,6 +143,9 @@ public:
void syncCombatLoadout(HarvesterEngine &engine, const RoomSetupState &state);
bool syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state, int loadout);
bool setIdleAnimation(int facing);
+ bool updateAnimationRate(bool speedUp);
+ bool startEvasiveAnimation(bool moveLeft, bool moveRight, bool moveUp, bool moveDown);
+ bool updateEvasiveAnimation(HarvesterEngine &engine);
bool startAttackAnimation(const RoomSetupState &state, const Common::Point &mousePos);
bool startKeyboardAttackAnimation(const RoomSetupState &state,
bool attackLeft, bool attackRight, bool attackUp, bool attackDown);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index baaac7fd858..9b6a5f988df 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -21,6 +21,7 @@
#include "harvester/scene.h"
+#include "backends/keymapper/keymapper.h"
#include "common/algorithm.h"
#include "common/events.h"
#include "common/system.h"
@@ -531,8 +532,24 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
}
InputResult Scene::pollRoomEvents(Input &input, bool &needsRedraw) {
+ Common::Keymap *gameplay = g_system->getEventManager()->getKeymapper()->getKeymap("harvester-gameplay");
+ Common::Keymap *defaults = g_system->getEventManager()->getKeymapper()->getKeymap("engine-default");
+ const bool defaultsEnabled = defaults && defaults->isEnabled();
Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
+ while (true) {
+ // Nested menus and dialogue retain their keyboard input while the room
+ // handler runs. Only the room event poll uses the gameplay actions.
+ if (gameplay)
+ gameplay->setEnabled(true);
+ if (gameplay && defaults)
+ defaults->setEnabled(false);
+ const bool hasEvent = g_system->getEventManager()->pollEvent(event);
+ if (gameplay)
+ gameplay->setEnabled(false);
+ if (gameplay && defaults)
+ defaults->setEnabled(defaultsEnabled);
+ if (!hasEvent)
+ break;
const InputResult inputResult = input.handleEvent(event, needsRedraw);
if (inputResult.returnFromRoom)
return inputResult;
@@ -546,6 +563,7 @@ Scene::PlayerControlState Scene::preparePlayerControlState() {
const bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
const bool playerControlPaused = script && script->isPlayerControlPaused();
const bool playerCanAct = playerAlive && !playerControlPaused;
+ (void)_player.updateAnimationRate(!playerControlPaused && _inputState.speedUpActive);
if (!playerAlive && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown ||
playerState.hasMoveTarget || playerState.turnActive ||
playerState.attackActive || playerState.hitActive)) {
@@ -572,6 +590,8 @@ void Scene::updatePlayerCombatAnimations(bool &needsRedraw) {
if (_player.updateHitAnimationState(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations)) {
needsRedraw = true;
}
+ if (_player.updateEvasiveAnimation(_engine))
+ needsRedraw = true;
}
void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsRedraw) {
@@ -580,7 +600,7 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
const bool keyboardAttackRequested =
_inputState.attackModifierHeld && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown);
if (control.canAct &&
- !playerState.attackActive && !playerState.hitActive && keyboardAttackRequested &&
+ !playerState.attackActive && !playerState.hitActive && playerState.evasiveState < 0 && keyboardAttackRequested &&
!idleState.active && !idleState.exiting) {
const uint32 now = Player::getRuntimeClockTicks();
if (_inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - _inputState.nextKeyboardAttackAllowedTick) >= 0) {
@@ -611,10 +631,18 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
}
if (control.canAct &&
- !playerState.attackActive && !playerState.hitActive &&
+ !playerState.attackActive && !playerState.hitActive && playerState.evasiveState < 0 &&
!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
const int regionFacing = _interactionProcessor.getPendingRegionFacing();
- if (_player.stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
+ if (_inputState.evasiveModifierHeld) {
+ if (_player.startEvasiveAnimation(_inputState.moveLeft, _inputState.moveRight,
+ _inputState.moveUp, _inputState.moveDown) ||
+ _player.stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
+ false, false, false, false)) {
+ notePlayerActivity();
+ needsRedraw = true;
+ }
+ } else if (_player.stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
_inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
notePlayerActivity();
needsRedraw = true;
@@ -625,7 +653,7 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
if (!_presentationState.showingInspectText && !keyboardAttackRequested &&
!_inputState.moveLeft && !_inputState.moveRight && !_inputState.moveUp && !_inputState.moveDown &&
- !playerState.hasMoveTarget && !playerState.turnActive && !playerState.hitActive &&
+ !playerState.hasMoveTarget && !playerState.turnActive && !playerState.hitActive && playerState.evasiveState < 0 &&
playerState.entity && playerState.facing >= 0 &&
!Player::isIdleAnimationExcludedRoom(_scene.state.roomName) &&
Player::getRuntimeClockTicks() > idleState.triggerTick &&
Commit: a1d676146395f00978253e915e58a83eeb6cfc1d
https://github.com/scummvm/scummvm/commit/a1d676146395f00978253e915e58a83eeb6cfc1d
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Add remappable attack, duck and jump actions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/input.cpp
engines/harvester/input.h
engines/harvester/metaengine.cpp
engines/harvester/scene.cpp
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index 7d13ef95605..f296ff6175c 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -843,6 +843,46 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
}
InputResult Input::handleGameAction(GameAction action, bool pressed, bool &needsRedraw) {
+ InputState &inputState = _session.getInputState();
+ bool *heldAction = nullptr;
+ switch (action) {
+ case kActionHighAttack:
+ heldAction = &inputState.highAttackHeld;
+ break;
+ case kActionMediumAttack:
+ heldAction = &inputState.mediumAttackHeld;
+ break;
+ case kActionLowAttack:
+ heldAction = &inputState.lowAttackHeld;
+ break;
+ case kActionDuck:
+ heldAction = &inputState.duckHeld;
+ break;
+ case kActionJump:
+ heldAction = &inputState.jumpHeld;
+ break;
+ default:
+ break;
+ }
+ if (heldAction) {
+ inputState.speedUpActive = false;
+ if (!pressed) {
+ *heldAction = false;
+ return InputResult();
+ }
+ _session.notePlayerActivity();
+ if (_presentationState.showingInspectText || _inventory.isOpen())
+ return InputResult();
+ Player &player = _session.getPlayer();
+ const RoomIdleAnimationState &idleState = player.getIdleState();
+ if (idleState.active || idleState.exiting) {
+ if (player.requestIdleAnimationExit(_scene.state))
+ needsRedraw = true;
+ return InputResult();
+ }
+ *heldAction = true;
+ return InputResult();
+ }
static const Common::KeyCode keys[] = {
Common::KEYCODE_LEFT, Common::KEYCODE_RIGHT, Common::KEYCODE_UP, Common::KEYCODE_DOWN,
Common::KEYCODE_LCTRL, Common::KEYCODE_RSHIFT, Common::KEYCODE_LALT,
diff --git a/engines/harvester/input.h b/engines/harvester/input.h
index ac340b018e2..9865e564217 100644
--- a/engines/harvester/input.h
+++ b/engines/harvester/input.h
@@ -57,7 +57,12 @@ enum GameAction {
kActionEvasiveModifier,
kActionSpeedUp,
kActionInventory,
- kActionMenu
+ kActionMenu,
+ kActionHighAttack,
+ kActionMediumAttack,
+ kActionLowAttack,
+ kActionDuck,
+ kActionJump
};
struct InputState {
@@ -68,12 +73,18 @@ struct InputState {
bool attackModifierHeld = false;
bool evasiveModifierHeld = false;
bool speedUpActive = false;
+ bool highAttackHeld = false;
+ bool mediumAttackHeld = false;
+ bool lowAttackHeld = false;
+ bool duckHeld = false;
+ bool jumpHeld = false;
uint32 lastLeftButtonReleaseTick = 0;
uint32 nextKeyboardAttackAllowedTick = 0;
void clearHeldActions() {
moveLeft = moveRight = moveUp = moveDown = false;
attackModifierHeld = evasiveModifierHeld = speedUpActive = false;
+ highAttackHeld = mediumAttackHeld = lowAttackHeld = duckHeld = jumpHeld = false;
}
};
diff --git a/engines/harvester/metaengine.cpp b/engines/harvester/metaengine.cpp
index a91f4f8dbd7..a51f6ff6be3 100644
--- a/engines/harvester/metaengine.cpp
+++ b/engines/harvester/metaengine.cpp
@@ -92,14 +92,20 @@ Common::KeymapArray HarvesterMetaEngine::initKeymaps(const char *target) const {
{ "EVADE", _s("Evasive movement modifier"), kActionEvasiveModifier, "RSHIFT", nullptr },
{ "SPEEDUP", _s("Speed up"), kActionSpeedUp, "LALT", "RALT" },
{ "INVENTORY", _s("Inventory"), kActionInventory, "i", nullptr },
- { "MENU", _s("Game menu"), kActionMenu, "ESCAPE", "JOY_Y" }
+ { "MENU", _s("Game menu"), kActionMenu, "ESCAPE", "JOY_Y" },
+ { "HIGHATTACK", _s("High attack"), kActionHighAttack, nullptr, nullptr },
+ { "MEDIUMATTACK", _s("Medium attack"), kActionMediumAttack, nullptr, nullptr },
+ { "LOWATTACK", _s("Low attack"), kActionLowAttack, nullptr, nullptr },
+ { "DUCK", _s("Duck"), kActionDuck, nullptr, nullptr },
+ { "JUMP", _s("Jump"), kActionJump, nullptr, nullptr }
};
for (const auto &binding : bindings) {
Action *action = new Action(binding.id, _(binding.description));
action->setCustomEngineActionEvent(binding.action);
- if (binding.action <= kActionSpeedUp)
+ if (binding.action <= kActionSpeedUp || binding.action >= kActionHighAttack)
action->allowKbdRepeats();
- action->addDefaultInputMapping(binding.mapping);
+ if (binding.mapping)
+ action->addDefaultInputMapping(binding.mapping);
if (binding.alternateMapping)
action->addDefaultInputMapping(binding.alternateMapping);
gameplay->addAction(action);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 9b6a5f988df..3f61575dbae 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -597,17 +597,24 @@ void Scene::updatePlayerCombatAnimations(bool &needsRedraw) {
void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsRedraw) {
const RoomPlayerState &playerState = _player.getState();
const RoomIdleAnimationState &idleState = _player.getIdleState();
+ const bool attackLeft = (_inputState.attackModifierHeld && _inputState.moveLeft) ||
+ (_inputState.mediumAttackHeld && playerState.facing == 1);
+ const bool attackRight = (_inputState.attackModifierHeld && _inputState.moveRight) ||
+ (_inputState.mediumAttackHeld && playerState.facing == 2);
+ const bool attackUp = _inputState.highAttackHeld || (_inputState.attackModifierHeld && _inputState.moveUp);
+ const bool attackDown = _inputState.lowAttackHeld || (_inputState.attackModifierHeld && _inputState.moveDown);
const bool keyboardAttackRequested =
- _inputState.attackModifierHeld && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown);
+ attackLeft || attackRight || attackUp || attackDown || _inputState.mediumAttackHeld;
+ const bool directEvasiveRequested = _inputState.duckHeld || _inputState.jumpHeld;
if (control.canAct &&
!playerState.attackActive && !playerState.hitActive && playerState.evasiveState < 0 && keyboardAttackRequested &&
!idleState.active && !idleState.exiting) {
const uint32 now = Player::getRuntimeClockTicks();
if (_inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - _inputState.nextKeyboardAttackAllowedTick) >= 0) {
_combat.capturePlayerAttackTargetForKeyboard(
- _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown);
+ attackLeft, attackRight, attackUp, attackDown);
if (_player.startKeyboardAttackAnimation(_scene.state,
- _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
+ attackLeft, attackRight, attackUp, attackDown)) {
_inputState.nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
notePlayerActivity();
debugC(1, kDebugCombat,
@@ -618,7 +625,7 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
describeCombatTargetClass(playerState.attackTargetClassId),
playerState.attackTargetName.c_str(),
- _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown, playerState.facing);
+ attackLeft, attackRight, attackUp, attackDown, playerState.facing);
needsRedraw = true;
} else {
_player.clearAttackTarget();
@@ -634,9 +641,12 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
!playerState.attackActive && !playerState.hitActive && playerState.evasiveState < 0 &&
!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
const int regionFacing = _interactionProcessor.getPendingRegionFacing();
- if (_inputState.evasiveModifierHeld) {
- if (_player.startEvasiveAnimation(_inputState.moveLeft, _inputState.moveRight,
- _inputState.moveUp, _inputState.moveDown) ||
+ if (_inputState.evasiveModifierHeld || directEvasiveRequested) {
+ if (_player.startEvasiveAnimation(
+ !directEvasiveRequested && _inputState.moveLeft,
+ !directEvasiveRequested && _inputState.moveRight,
+ _inputState.jumpHeld || (!directEvasiveRequested && _inputState.moveUp),
+ _inputState.duckHeld || (!directEvasiveRequested && _inputState.moveDown)) ||
_player.stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
false, false, false, false)) {
notePlayerActivity();
@@ -651,7 +661,7 @@ void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsR
needsRedraw = true;
}
- if (!_presentationState.showingInspectText && !keyboardAttackRequested &&
+ if (!_presentationState.showingInspectText && !keyboardAttackRequested && !directEvasiveRequested &&
!_inputState.moveLeft && !_inputState.moveRight && !_inputState.moveUp && !_inputState.moveDown &&
!playerState.hasMoveTarget && !playerState.turnActive && !playerState.hitActive && playerState.evasiveState < 0 &&
playerState.entity && playerState.facing >= 0 &&
Commit: 255088e37d6530d859155b1964e3b81778fa62f2
https://github.com/scummvm/scummvm/commit/255088e37d6530d859155b1964e3b81778fa62f2
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Separate town script record parsing
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/script_records.cpp
engines/harvester/module.mk
engines/harvester/script.cpp
engines/harvester/script.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index ddcde655bc6..4ccd5daa022 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -26,6 +26,7 @@ MODULE_OBJS = \
entity.o \
saveload.o \
script.o \
+ script_records.o \
text.o \
xfile_archive.o \
npc/authority_dialogue.o \
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index bd69c58ef5b..a865c23c32e 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -42,7 +42,6 @@ static const char *const kConfigSectionName = "harvester";
static const char *const kDefaultTownScript = "HARVEST.SCR";
static const char *const kInventoryOwnerName = "INVENTORY";
static const char *const kDefaultVoicePath = "SOUND/VOICE/";
-static const byte kTownScriptXorKey = 0xaa;
static const float kDefaultPaletteBrightness = 1.0f;
static const float kDimmedPaletteBrightness = 0.6f;
static const int kDefaultPlayerCombatLoadout = 0;
@@ -388,18 +387,6 @@ static const char *resolveInventoryStatusObjectName(int hitPoints) {
return "INV_STAT1";
}
-static Common::String trimAsciiLine(const Common::String &value) {
- uint start = 0;
- uint end = value.size();
-
- while (start < end && (value[start] == ' ' || value[start] == '\t'))
- ++start;
- while (end > start && (value[end - 1] == ' ' || value[end - 1] == '\t' || value[end - 1] == '\r'))
- --end;
-
- return value.substr(start, end - start);
-}
-
static Common::String normalizeInteractionLabel(const Common::String &value) {
Common::String label = value;
for (uint i = 0; i < label.size(); ++i) {
@@ -432,30 +419,6 @@ static Common::String dialogueTextModeToConfigValue(StartupDialogueTextMode mode
}
}
-static int parseEntranceFacing(const Common::String &direction) {
- if (direction.equalsIgnoreCase("FRONT"))
- return 0;
- if (direction.equalsIgnoreCase("LEFT"))
- return 1;
- if (direction.equalsIgnoreCase("RIGHT"))
- return 2;
- if (direction.equalsIgnoreCase("BACK"))
- return 3;
-
- return -1;
-}
-
-static int parseDeathDamageType(const Common::String &value) {
- if (value.equalsIgnoreCase("BLUDGE"))
- return 1;
- if (value.equalsIgnoreCase("SLASH"))
- return 2;
- if (value.equalsIgnoreCase("PROJ"))
- return 4;
-
- return 0;
-}
-
static bool appendStartupAudioCommand(const CommandRecord &command, Common::Array<AudioCommand> &commands) {
if (command.opcodeName.equalsIgnoreCase("START_WAV")) {
AudioCommand audioCommand;
@@ -536,47 +499,6 @@ static bool isFullscreenRoomBackgroundObject(const ObjectRecord &candidate,
return width == 640 && height == 480;
}
-static void tokenizeTownScriptLine(const Common::String &line, Common::Array<Common::String> &tokens) {
- tokens.clear();
-
- for (uint i = 0; i < line.size();) {
- while (i < line.size() && (line[i] == ' ' || line[i] == '\t'))
- ++i;
- if (i >= line.size())
- break;
-
- if (line[i] == '"') {
- ++i;
- Common::String token;
- bool hasEmbeddedQuote = false;
- while (i < line.size()) {
- const bool isClosingQuote = line[i] == '"' &&
- (i + 1 == line.size() || line[i + 1] == ' ' || line[i + 1] == '\t');
- if (isClosingQuote)
- break;
-
- // French TEXT records contain quotes inside underscore-separated values.
- // The native word reader keeps them and strips only the outer quote pair.
- if (line[i] == '"')
- hasEmbeddedQuote = true;
- token += line[i++];
- }
- if (i < line.size() && line[i] == '"')
- ++i;
- if (hasEmbeddedQuote)
- debugC(3, kDebugGeneral, "Harvester: TownScript token contains embedded quote value='%s'",
- token.c_str());
- tokens.push_back(Common::move(token));
- continue;
- }
-
- Common::String token;
- while (i < line.size() && line[i] != ' ' && line[i] != '\t')
- token += line[i++];
- tokens.push_back(Common::move(token));
- }
-}
-
bool Script::load(ResourceManager &resources) {
_path = kDefaultTownScript;
_data.clear();
@@ -907,467 +829,6 @@ void Script::setGammaLevel(int level) {
_gammaLevel = clampStartupOptionLevel(level);
}
-void Script::decode() {
- for (byte &value : _data) {
- if (value == '\r' || value == '\n')
- continue;
-
- value ^= kTownScriptXorKey;
- }
-}
-
-void Script::parseTownRecords(ResourceManager &resources) {
- _entrances.clear();
- _mapEntrances.clear();
- _mapLocations.clear();
- _rooms.clear();
- _objects.clear();
- _animations.clear();
- _npcs.clear();
- _monsters.clear();
- _timers.clear();
- _regions.clear();
- _flags.clear();
- _commands.clear();
- _execLists.clear();
- _texts.clear();
- _heads.clear();
- _useItems.clear();
-
- uint32 anonymousObjectId = 0;
- auto parseLine = [&](const Common::String &rawLine) {
- const Common::String line = trimAsciiLine(rawLine);
- if (line.empty() || line[0] == '{')
- return;
-
- Common::Array<Common::String> tokens;
- tokenizeTownScriptLine(line, tokens);
- if (tokens.empty())
- return;
-
- uint tagIndex = tokens.size();
- for (uint i = 0; i < tokens.size(); ++i) {
- if (tokens[i] == "ANIM" || tokens[i] == "ENTRANCE" || tokens[i] == "MAP_ENTRANCE" ||
- tokens[i] == "MAP_LOCATION" || tokens[i] == "ROOM" || tokens[i] == "OBJECT" ||
- tokens[i] == "NPC" || tokens[i] == "MONSTER" || tokens[i] == "REGION" ||
- tokens[i] == "HEAD" || tokens[i] == "FLAG" || tokens[i] == "COMMAND" ||
- tokens[i] == "EXEC_LIST" || tokens[i] == "TEXT" || tokens[i] == "TIMER" ||
- tokens[i] == "USEITEM") {
- tagIndex = i;
- break;
- }
- }
- if (tagIndex == tokens.size())
- return;
-
- const Common::String &tag = tokens[tagIndex];
- if (tag == "FLAG") {
- if (tokens.size() < tagIndex + 3)
- return;
-
- FlagRecord flag;
- flag.name = tokens[tagIndex + 1];
- flag.value = tokens[tagIndex + 2].equalsIgnoreCase("T");
- if (!flag.name.empty())
- _flags.push_back(Common::move(flag));
- return;
- }
-
- if (tag == "COMMAND") {
- if (tokens.size() < tagIndex + 6)
- return;
-
- CommandRecord command;
- command.triggerTag = tokens[tagIndex + 1];
- command.opcodeName = tokens[tagIndex + 2];
- command.arg1 = resources.normalizeResourcePath(tokens[tagIndex + 3]);
- command.arg2 = tokens[tagIndex + 4];
- command.arg3 = tokens[tagIndex + 5];
- command.arg4 = tokens.size() > tagIndex + 6 ? tokens[tagIndex + 6] : Common::String();
-
- // Only path-like operands should be normalized.
- if (!command.opcodeName.equalsIgnoreCase("SPOOL_MUSIC") &&
- !command.opcodeName.equalsIgnoreCase("GOFLIC") &&
- !command.opcodeName.equalsIgnoreCase("START_WAV") &&
- !command.opcodeName.equalsIgnoreCase("LOAD_WAV") &&
- !command.opcodeName.equalsIgnoreCase("START_SINGLE_WAV")) {
- command.arg1 = tokens[tagIndex + 3];
- }
-
- if (!command.triggerTag.empty() && !command.opcodeName.empty())
- _commands.push_back(Common::move(command));
- return;
- }
-
- if (tag == "EXEC_LIST") {
- if (tokens.size() < tagIndex + 2)
- return;
-
- ExecListRecord execList;
- execList.listName = tokens[tagIndex + 1];
- for (uint i = tagIndex + 2; i < tokens.size(); ++i) {
- if (tokens[i].empty())
- break;
- execList.entries.push_back(tokens[i]);
- }
- if (!execList.listName.empty())
- _execLists.push_back(Common::move(execList));
- return;
- }
-
- if (tag == "TEXT") {
- if (tokens.size() < tagIndex + 4)
- return;
-
- TextRecord textRecord;
- textRecord.key = tokens[tagIndex + 1];
- textRecord.boxName = tokens[tagIndex + 2];
- textRecord.value = tokens[tagIndex + 3];
- for (uint i = 0; i < textRecord.value.size(); ++i) {
- if (textRecord.value[i] == '_')
- textRecord.value.setChar(' ', i);
- }
- if (!textRecord.key.empty())
- _texts.push_back(Common::move(textRecord));
- return;
- }
-
- if (tag == "HEAD") {
- if (tokens.size() < tagIndex + 3)
- return;
-
- HeadRecord head;
- head.headId = tokens[tagIndex + 1];
- head.portraitPath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
- if (!head.headId.empty() && !head.portraitPath.empty())
- _heads.push_back(Common::move(head));
- return;
- }
-
- if (tag == "USEITEM") {
- if (tokens.size() < tagIndex + 5)
- return;
-
- UseItemRecord useItem;
- useItem.itemName = tokens[tagIndex + 1];
- useItem.ownerOrRoom = tokens[tagIndex + 2];
- useItem.targetName = tokens[tagIndex + 3];
- useItem.actionTag = tokens[tagIndex + 4];
- if (!useItem.itemName.empty() && !useItem.targetName.empty())
- _useItems.push_back(Common::move(useItem));
- return;
- }
-
- if (tag == "ENTRANCE") {
- if (tokens.size() < tagIndex + 4)
- return;
-
- EntranceRecord entrance;
- if (tagIndex >= 3) {
- entrance.posX = parseAsciiIntOrZero(tokens[0]);
- entrance.posY = parseAsciiIntOrZero(tokens[1]);
- entrance.posZ = parseAsciiIntOrZero(tokens[2]);
- }
- entrance.direction = tokens[tagIndex + 1];
- entrance.facing = parseEntranceFacing(entrance.direction);
- entrance.roomName = tokens[tagIndex + 2];
- entrance.entranceName = tokens[tagIndex + 3];
- if (!entrance.roomName.empty() && !entrance.entranceName.empty())
- _entrances.push_back(Common::move(entrance));
- return;
- }
-
- if (tag == "MAP_ENTRANCE") {
- if (tokens.size() < tagIndex + 2)
- return;
-
- MapEntranceRecord mapEntrance;
- if (tagIndex >= 3) {
- mapEntrance.mapX = parseAsciiIntOrZero(tokens[0]);
- mapEntrance.mapY = parseAsciiIntOrZero(tokens[1]);
- mapEntrance.initialPanelIndex = parseAsciiIntOrZero(tokens[2]);
- }
- mapEntrance.entryName = tokens[tagIndex + 1];
- if (!mapEntrance.entryName.empty())
- _mapEntrances.push_back(Common::move(mapEntrance));
- return;
- }
-
- if (tag == "MAP_LOCATION") {
- if (tokens.size() < tagIndex + 3)
- return;
-
- MapLocationRecord mapLocation;
- if (tagIndex >= 7) {
- mapLocation.minX = parseAsciiIntOrZero(tokens[0]);
- mapLocation.minY = parseAsciiIntOrZero(tokens[1]);
- mapLocation.maxX = parseAsciiIntOrZero(tokens[2]);
- mapLocation.maxY = parseAsciiIntOrZero(tokens[3]);
- mapLocation.panelIndex = parseAsciiIntOrZero(tokens[4]);
- mapLocation.labelX = parseAsciiIntOrZero(tokens[5]);
- mapLocation.labelY = parseAsciiIntOrZero(tokens[6]);
- }
- mapLocation.labelText = tokens[tagIndex + 1];
- for (uint i = 0; i < mapLocation.labelText.size(); ++i) {
- if (mapLocation.labelText[i] == '_')
- mapLocation.labelText.setChar(' ', i);
- }
- mapLocation.destinationEntranceName = tokens[tagIndex + 2];
- if (!mapLocation.destinationEntranceName.empty())
- _mapLocations.push_back(Common::move(mapLocation));
- return;
- }
-
- if (tag == "ROOM") {
- if (tokens.size() < tagIndex + 10)
- return;
-
- RoomRecord room;
- if (tagIndex >= 6) {
- room.minZ = parseAsciiIntOrZero(tokens[0]);
- room.maxZ = parseAsciiIntOrZero(tokens[1]);
- room.maxZScreenY = parseAsciiIntOrZero(tokens[2]);
- room.minZScreenY = parseAsciiIntOrZero(tokens[3]);
- room.fullScaleZ = parseAsciiIntOrZero(tokens[4]);
- room.maxZScalePercent = parseAsciiIntOrZero(tokens[5]);
- if (room.maxZ != room.fullScaleZ) {
- room.perspectiveScale = ((100.0f - (float)room.maxZScalePercent) /
- (float)(room.maxZ - room.fullScaleZ)) * 0.01f;
- }
- }
- room.roomName = tokens[tagIndex + 1];
- room.musicPath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
- room.reservedString38 = tokens[tagIndex + 3];
- room.reservedString3c = tokens[tagIndex + 4];
- room.reservedString40 = tokens[tagIndex + 5];
- room.palettePath = resources.normalizeResourcePath(tokens[tagIndex + 6]);
- room.dimmable = tokens[tagIndex + 7].equalsIgnoreCase("T");
- room.onEnterCommand = tokens[tagIndex + 8];
- room.onExitCommand = tokens[tagIndex + 9];
- if (!room.roomName.empty())
- _rooms.push_back(Common::move(room));
- return;
- }
-
- if (tag == "NPC") {
- if (tokens.size() < tagIndex + 10)
- return;
-
- NpcRecord npc;
- if (tagIndex >= 4) {
- npc.posX = parseAsciiIntOrZero(tokens[0]);
- npc.posY = parseAsciiIntOrZero(tokens[1]);
- npc.posZ = parseAsciiIntOrZero(tokens[2]);
- npc.frameDelay = parseAsciiIntOrZero(tokens[3]);
- }
- npc.roomName = tokens[tagIndex + 1];
- npc.modelPath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
- npc.npcName = tokens[tagIndex + 3];
- npc.monsterfyTargetName = tokens[tagIndex + 4];
- npc.active = tokens[tagIndex + 5].equalsIgnoreCase("T");
- npc.visible = tokens[tagIndex + 6].equalsIgnoreCase("T");
- if (npc.active)
- npc.visible = true;
- npc.savedVisible = npc.visible;
- npc.onDeathActionTag = tokens[tagIndex + 7];
- npc.audioPath = resources.normalizeResourcePath(tokens[tagIndex + 8]);
- npc.entityInitArg = tokens[tagIndex + 9];
- if (!npc.roomName.empty() && !npc.modelPath.empty() && !npc.npcName.empty())
- _npcs.push_back(Common::move(npc));
- return;
- }
-
- if (tag == "MONSTER") {
- if (tokens.size() < tagIndex + 24)
- return;
-
- MonsterRecord monster;
- if (tagIndex >= 6) {
- monster.posX = parseAsciiIntOrZero(tokens[0]);
- monster.posY = parseAsciiIntOrZero(tokens[1]);
- monster.posZ = parseAsciiIntOrZero(tokens[2]);
- // Native MonsterRecord numeric order is engage distance, initial HP, then damage.
- monster.engageDistance = parseAsciiIntOrZero(tokens[3]);
- monster.initialHitPoints = parseAsciiIntOrZero(tokens[4]);
- monster.damageAmount = parseAsciiIntOrZero(tokens[5]);
- }
- if (tagIndex >= 10) {
- monster.attackSoundTriggerFrame = parseAsciiIntOrZero(tokens[6]);
- monster.hitSoundTriggerFrame = parseAsciiIntOrZero(tokens[7]);
- monster.footstepSoundTriggerFrame = parseAsciiIntOrZero(tokens[8]);
- monster.deathSoundTriggerFrame = parseAsciiIntOrZero(tokens[9]);
- }
- monster.roomName = tokens[tagIndex + 1];
- monster.monsterName = tokens[tagIndex + 2];
- monster.modelPath = resources.normalizeResourcePath(tokens[tagIndex + 3]);
- monster.reservedString44 = tokens[tagIndex + 4];
- monster.reservedString48 = tokens[tagIndex + 5];
- monster.reservedString38 = tokens[tagIndex + 6];
- monster.reservedString3c = tokens[tagIndex + 7];
- monster.facing = parseEntranceFacing(tokens[tagIndex + 8]);
- monster.damageType = parseDeathDamageType(tokens[tagIndex + 9]);
- monster.attackSound1 = resources.normalizeResourcePath(tokens[tagIndex + 10]);
- monster.attackSound2 = resources.normalizeResourcePath(tokens[tagIndex + 11]);
- monster.attackSound3 = resources.normalizeResourcePath(tokens[tagIndex + 12]);
- monster.hitSound1 = resources.normalizeResourcePath(tokens[tagIndex + 13]);
- monster.hitSound2 = resources.normalizeResourcePath(tokens[tagIndex + 14]);
- monster.hitSound3 = resources.normalizeResourcePath(tokens[tagIndex + 15]);
- monster.footstepSoundLeft = resources.normalizeResourcePath(tokens[tagIndex + 16]);
- monster.footstepSoundRight = resources.normalizeResourcePath(tokens[tagIndex + 17]);
- monster.deathSound = resources.normalizeResourcePath(tokens[tagIndex + 18]);
- monster.active = tokens[tagIndex + 19].equalsIgnoreCase("T");
- monster.visible = tokens[tagIndex + 20].equalsIgnoreCase("T");
- monster.onDeathActionTag = tokens[tagIndex + 21];
- if (tokens.size() > tagIndex + 22 && !tokens[tagIndex + 22].empty())
- monster.screenMinXBound = parseAsciiIntOrZero(tokens[tagIndex + 22]);
- if (tokens.size() > tagIndex + 23 && !tokens[tagIndex + 23].empty())
- monster.screenMaxXBound = parseAsciiIntOrZero(tokens[tagIndex + 23]);
- monster.currentHitPoints = monster.initialHitPoints;
- monster.savedVisible = monster.visible;
- if (monster.active)
- monster.visible = true;
- if (!monster.roomName.empty() && !monster.monsterName.empty() && !monster.modelPath.empty()) {
- monster.recordIndex = (int)_monsters.size();
- _monsters.push_back(Common::move(monster));
- }
- return;
- }
-
- if (tag == "TIMER") {
- if (tokens.size() < tagIndex + 7)
- return;
-
- TimerRecord timer;
- if (tagIndex >= 1) {
- timer.initialValue = parseAsciiIntOrZero(tokens[0]);
- timer.currentValue = timer.initialValue;
- }
- timer.timerName = tokens[tagIndex + 1];
- timer.arg1 = tokens[tagIndex + 2];
- timer.arg2 = tokens[tagIndex + 3];
- timer.enabled = tokens[tagIndex + 4].equalsIgnoreCase("T");
- timer.looping = tokens[tagIndex + 5].equalsIgnoreCase("T");
- timer.global = tokens[tagIndex + 6].equalsIgnoreCase("T");
- if (!timer.timerName.empty())
- _timers.push_back(Common::move(timer));
- return;
- }
-
- if (tag == "REGION") {
- if (tokens.size() < tagIndex + 7)
- return;
-
- RegionRecord region;
- if (tagIndex >= 6) {
- region.left = parseAsciiIntOrZero(tokens[0]);
- region.top = parseAsciiIntOrZero(tokens[1]);
- region.right = parseAsciiIntOrZero(tokens[2]);
- region.bottom = parseAsciiIntOrZero(tokens[3]);
- region.minZ = parseAsciiIntOrZero(tokens[4]);
- region.maxZ = parseAsciiIntOrZero(tokens[5]);
- }
- region.regionName = tokens[tagIndex + 1];
- region.direction = tokens[tagIndex + 2];
- region.desiredFacing = parseEntranceFacing(region.direction);
- region.roomName = tokens[tagIndex + 3];
- region.actionTag = tokens[tagIndex + 4];
- region.startEnabled = tokens[tagIndex + 5].equalsIgnoreCase("T");
- region.cursorEnabled = tokens[tagIndex + 6].equalsIgnoreCase("T");
- if (!region.roomName.empty() && !region.regionName.empty())
- _regions.push_back(Common::move(region));
- return;
- }
-
- if (tag == "ANIM") {
- if (tokens.size() < tagIndex + 10)
- return;
-
- AnimRecord anim;
- if (tagIndex >= 4) {
- anim.x = parseAsciiIntOrZero(tokens[0]);
- anim.y = parseAsciiIntOrZero(tokens[1]);
- anim.z = parseAsciiIntOrZero(tokens[2]);
- anim.frameDelay = parseAsciiIntOrZero(tokens[3]);
- }
- anim.roomName = tokens[tagIndex + 1];
- anim.resourcePath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
- anim.animName = tokens[tagIndex + 3];
- anim.active = tokens[tagIndex + 4].equalsIgnoreCase("T");
- anim.visible = tokens[tagIndex + 5].equalsIgnoreCase("T");
- anim.looping = tokens[tagIndex + 6].equalsIgnoreCase("T");
- anim.backward = tokens[tagIndex + 7].equalsIgnoreCase("T");
- anim.pingPong = tokens[tagIndex + 8].equalsIgnoreCase("T");
- anim.remove = tokens[tagIndex + 9].equalsIgnoreCase("T");
- anim.runtimeActive = anim.active;
- anim.runtimeVisible = anim.visible;
- if (!anim.roomName.empty() && !anim.resourcePath.empty() && !anim.animName.empty())
- _animations.push_back(Common::move(anim));
- return;
- }
-
- if (tokens.size() < tagIndex + 13)
- return;
-
- ObjectRecord object;
- if (tagIndex >= 6) {
- object.initialX = parseAsciiIntOrZero(tokens[0]);
- object.initialY = parseAsciiIntOrZero(tokens[1]);
- object.boundsX2 = parseAsciiIntOrZero(tokens[2]);
- object.boundsY2 = parseAsciiIntOrZero(tokens[3]);
- object.initialZ = parseAsciiIntOrZero(tokens[4]);
- object.zExtent = parseAsciiIntOrZero(tokens[5]);
- }
- object.currentX = object.initialX;
- object.currentY = object.initialY;
- object.currentZ = object.initialZ;
- object.initialOwnerOrRoom = tokens[tagIndex + 1];
- object.objectName = tokens[tagIndex + 2];
- object.spritePath = resources.normalizeResourcePath(tokens[tagIndex + 3]);
- object.altSpritePath = resources.normalizeResourcePath(tokens[tagIndex + 4]);
- object.reservedString40 = tokens[tagIndex + 5];
- object.inventoryTextKey = tokens[tagIndex + 6];
- object.reservedXFlag = tokens[tagIndex + 7];
- object.identTextKey = tokens[tagIndex + 8];
- object.operatable = tokens[tagIndex + 9].equalsIgnoreCase("T");
- object.visible = tokens[tagIndex + 10].equalsIgnoreCase("T");
- object.actionTag = tokens[tagIndex + 11];
- object.interactionLabel = tokens[tagIndex + 12];
- object.currentOwnerOrRoom = object.initialOwnerOrRoom;
- object.runtimeVisible = object.visible;
- object.identShown = object.identTextKey.empty();
- if (object.objectName.empty()) {
- // Native room scripts use unnamed OBJECT entries as blocker rectangles.
- object.objectName = Common::String::format("__ANON_OBJECT_%u", anonymousObjectId++);
- }
- if (!object.initialOwnerOrRoom.empty())
- _objects.push_back(Common::move(object));
- };
-
- Common::String line;
- for (uint i = 0; i < _data.size(); ++i) {
- const char ch = (char)_data[i];
- if (ch == '\r')
- continue;
- if (ch == '\n') {
- parseLine(line);
- line.clear();
- continue;
- }
-
- line += ch;
- }
-
- parseLine(line);
-
- debug(1, "Harvester: parsed %u entrances, %u map entrances, %u map locations, %u rooms, %u objects, %u anims, %u npcs, %u regions, %u flags, %u commands, %u exec_lists, %u texts, %u heads, %u useitems from '%s'",
- (uint)_entrances.size(), (uint)_mapEntrances.size(), (uint)_mapLocations.size(),
- (uint)_rooms.size(), (uint)_objects.size(), (uint)_animations.size(),
- (uint)_npcs.size(), (uint)_regions.size(),
- (uint)_flags.size(), (uint)_commands.size(), (uint)_execLists.size(),
- (uint)_texts.size(), (uint)_heads.size(),
- (uint)_useItems.size(), _path.c_str());
-}
-
bool Script::resolveRoomSetupState(const Common::String &entranceName, RoomSetupState &state,
ResourceManager &resources) {
state = RoomSetupState();
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index 70cc7ff6f93..f045cbfc545 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -473,6 +473,32 @@ private:
bool loadConfig(ResourceManager &resources);
void decode();
void parseTownRecords(ResourceManager &resources);
+ void parseTownRecordLine(ResourceManager &resources,
+ const Common::String &rawLine, uint32 &anonymousObjectId);
+ void parseFlagRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseCommandRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseExecListRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseTextRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseHeadRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseUseItemRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseEntranceRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseMapEntranceRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseMapLocationRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseRoomRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseNpcRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseMonsterRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseTimerRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseRegionRecord(const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseAnimRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex);
+ void parseObjectRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex, uint32 &anonymousObjectId);
+ static int parseDeathDamageType(const Common::String &value);
const RoomRecord *findRoomRecord(const Common::String &roomName) const;
const CommandRecord *findCommandRecord(const Common::String &tag) const;
const ExecListRecord *findExecListRecord(const Common::String &name) const;
diff --git a/engines/harvester/script_records.cpp b/engines/harvester/script_records.cpp
new file mode 100644
index 00000000000..28550c473d4
--- /dev/null
+++ b/engines/harvester/script_records.cpp
@@ -0,0 +1,601 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/script.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "harvester/detection.h"
+#include "harvester/parse_utils.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+static const byte kTownScriptXorKey = 0xaa;
+
+static Common::String trimAsciiLine(const Common::String &value) {
+ uint start = 0;
+ uint end = value.size();
+
+ while (start < end && (value[start] == ' ' || value[start] == '\t'))
+ ++start;
+ while (end > start && (value[end - 1] == ' ' || value[end - 1] == '\t' || value[end - 1] == '\r'))
+ --end;
+
+ return value.substr(start, end - start);
+}
+
+static int parseEntranceFacing(const Common::String &direction) {
+ if (direction.equalsIgnoreCase("FRONT"))
+ return 0;
+ if (direction.equalsIgnoreCase("LEFT"))
+ return 1;
+ if (direction.equalsIgnoreCase("RIGHT"))
+ return 2;
+ if (direction.equalsIgnoreCase("BACK"))
+ return 3;
+
+ return -1;
+}
+
+int Script::parseDeathDamageType(const Common::String &value) {
+ if (value.equalsIgnoreCase("BLUDGE"))
+ return 1;
+ if (value.equalsIgnoreCase("SLASH"))
+ return 2;
+ if (value.equalsIgnoreCase("PROJ"))
+ return 4;
+
+ return 0;
+}
+
+static void tokenizeTownScriptLine(const Common::String &line, Common::Array<Common::String> &tokens) {
+ tokens.clear();
+
+ for (uint i = 0; i < line.size();) {
+ while (i < line.size() && (line[i] == ' ' || line[i] == '\t'))
+ ++i;
+ if (i >= line.size())
+ break;
+
+ if (line[i] == '"') {
+ ++i;
+ Common::String token;
+ bool hasEmbeddedQuote = false;
+ while (i < line.size()) {
+ const bool isClosingQuote = line[i] == '"' &&
+ (i + 1 == line.size() || line[i + 1] == ' ' || line[i + 1] == '\t');
+ if (isClosingQuote)
+ break;
+
+ // French TEXT records contain quotes inside underscore-separated values.
+ // The native word reader keeps them and strips only the outer quote pair.
+ if (line[i] == '"')
+ hasEmbeddedQuote = true;
+ token += line[i++];
+ }
+ if (i < line.size() && line[i] == '"')
+ ++i;
+ if (hasEmbeddedQuote)
+ debugC(3, kDebugGeneral, "Harvester: TownScript token contains embedded quote value='%s'",
+ token.c_str());
+ tokens.push_back(Common::move(token));
+ continue;
+ }
+
+ Common::String token;
+ while (i < line.size() && line[i] != ' ' && line[i] != '\t')
+ token += line[i++];
+ tokens.push_back(Common::move(token));
+ }
+}
+
+void Script::decode() {
+ for (byte &value : _data) {
+ if (value == '\r' || value == '\n')
+ continue;
+
+ value ^= kTownScriptXorKey;
+ }
+}
+
+void Script::parseFlagRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 3)
+ return;
+
+ FlagRecord flag;
+ flag.name = tokens[tagIndex + 1];
+ flag.value = tokens[tagIndex + 2].equalsIgnoreCase("T");
+ if (!flag.name.empty())
+ _flags.push_back(Common::move(flag));
+}
+
+void Script::parseCommandRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 6)
+ return;
+
+ CommandRecord command;
+ command.triggerTag = tokens[tagIndex + 1];
+ command.opcodeName = tokens[tagIndex + 2];
+ command.arg1 = resources.normalizeResourcePath(tokens[tagIndex + 3]);
+ command.arg2 = tokens[tagIndex + 4];
+ command.arg3 = tokens[tagIndex + 5];
+ command.arg4 = tokens.size() > tagIndex + 6 ? tokens[tagIndex + 6] : Common::String();
+
+ // Only path-like operands should be normalized.
+ if (!command.opcodeName.equalsIgnoreCase("SPOOL_MUSIC") &&
+ !command.opcodeName.equalsIgnoreCase("GOFLIC") &&
+ !command.opcodeName.equalsIgnoreCase("START_WAV") &&
+ !command.opcodeName.equalsIgnoreCase("LOAD_WAV") &&
+ !command.opcodeName.equalsIgnoreCase("START_SINGLE_WAV")) {
+ command.arg1 = tokens[tagIndex + 3];
+ }
+
+ if (!command.triggerTag.empty() && !command.opcodeName.empty())
+ _commands.push_back(Common::move(command));
+}
+
+void Script::parseExecListRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 2)
+ return;
+
+ ExecListRecord execList;
+ execList.listName = tokens[tagIndex + 1];
+ for (uint i = tagIndex + 2; i < tokens.size(); ++i) {
+ if (tokens[i].empty())
+ break;
+ execList.entries.push_back(tokens[i]);
+ }
+ if (!execList.listName.empty())
+ _execLists.push_back(Common::move(execList));
+}
+
+void Script::parseTextRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 4)
+ return;
+
+ TextRecord textRecord;
+ textRecord.key = tokens[tagIndex + 1];
+ textRecord.boxName = tokens[tagIndex + 2];
+ textRecord.value = tokens[tagIndex + 3];
+ for (uint i = 0; i < textRecord.value.size(); ++i) {
+ if (textRecord.value[i] == '_')
+ textRecord.value.setChar(' ', i);
+ }
+ if (!textRecord.key.empty())
+ _texts.push_back(Common::move(textRecord));
+}
+
+void Script::parseHeadRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 3)
+ return;
+
+ HeadRecord head;
+ head.headId = tokens[tagIndex + 1];
+ head.portraitPath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
+ if (!head.headId.empty() && !head.portraitPath.empty())
+ _heads.push_back(Common::move(head));
+}
+
+void Script::parseUseItemRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 5)
+ return;
+
+ UseItemRecord useItem;
+ useItem.itemName = tokens[tagIndex + 1];
+ useItem.ownerOrRoom = tokens[tagIndex + 2];
+ useItem.targetName = tokens[tagIndex + 3];
+ useItem.actionTag = tokens[tagIndex + 4];
+ if (!useItem.itemName.empty() && !useItem.targetName.empty())
+ _useItems.push_back(Common::move(useItem));
+}
+
+void Script::parseEntranceRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 4)
+ return;
+
+ EntranceRecord entrance;
+ if (tagIndex >= 3) {
+ entrance.posX = parseAsciiIntOrZero(tokens[0]);
+ entrance.posY = parseAsciiIntOrZero(tokens[1]);
+ entrance.posZ = parseAsciiIntOrZero(tokens[2]);
+ }
+ entrance.direction = tokens[tagIndex + 1];
+ entrance.facing = parseEntranceFacing(entrance.direction);
+ entrance.roomName = tokens[tagIndex + 2];
+ entrance.entranceName = tokens[tagIndex + 3];
+ if (!entrance.roomName.empty() && !entrance.entranceName.empty())
+ _entrances.push_back(Common::move(entrance));
+}
+
+void Script::parseMapEntranceRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 2)
+ return;
+
+ MapEntranceRecord mapEntrance;
+ if (tagIndex >= 3) {
+ mapEntrance.mapX = parseAsciiIntOrZero(tokens[0]);
+ mapEntrance.mapY = parseAsciiIntOrZero(tokens[1]);
+ mapEntrance.initialPanelIndex = parseAsciiIntOrZero(tokens[2]);
+ }
+ mapEntrance.entryName = tokens[tagIndex + 1];
+ if (!mapEntrance.entryName.empty())
+ _mapEntrances.push_back(Common::move(mapEntrance));
+}
+
+void Script::parseMapLocationRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 3)
+ return;
+
+ MapLocationRecord mapLocation;
+ if (tagIndex >= 7) {
+ mapLocation.minX = parseAsciiIntOrZero(tokens[0]);
+ mapLocation.minY = parseAsciiIntOrZero(tokens[1]);
+ mapLocation.maxX = parseAsciiIntOrZero(tokens[2]);
+ mapLocation.maxY = parseAsciiIntOrZero(tokens[3]);
+ mapLocation.panelIndex = parseAsciiIntOrZero(tokens[4]);
+ mapLocation.labelX = parseAsciiIntOrZero(tokens[5]);
+ mapLocation.labelY = parseAsciiIntOrZero(tokens[6]);
+ }
+ mapLocation.labelText = tokens[tagIndex + 1];
+ for (uint i = 0; i < mapLocation.labelText.size(); ++i) {
+ if (mapLocation.labelText[i] == '_')
+ mapLocation.labelText.setChar(' ', i);
+ }
+ mapLocation.destinationEntranceName = tokens[tagIndex + 2];
+ if (!mapLocation.destinationEntranceName.empty())
+ _mapLocations.push_back(Common::move(mapLocation));
+}
+
+void Script::parseRoomRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 10)
+ return;
+
+ RoomRecord room;
+ if (tagIndex >= 6) {
+ room.minZ = parseAsciiIntOrZero(tokens[0]);
+ room.maxZ = parseAsciiIntOrZero(tokens[1]);
+ room.maxZScreenY = parseAsciiIntOrZero(tokens[2]);
+ room.minZScreenY = parseAsciiIntOrZero(tokens[3]);
+ room.fullScaleZ = parseAsciiIntOrZero(tokens[4]);
+ room.maxZScalePercent = parseAsciiIntOrZero(tokens[5]);
+ if (room.maxZ != room.fullScaleZ) {
+ room.perspectiveScale = ((100.0f - static_cast<float>(room.maxZScalePercent)) /
+ static_cast<float>(room.maxZ - room.fullScaleZ)) * 0.01f;
+ }
+ }
+ room.roomName = tokens[tagIndex + 1];
+ room.musicPath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
+ room.reservedString38 = tokens[tagIndex + 3];
+ room.reservedString3c = tokens[tagIndex + 4];
+ room.reservedString40 = tokens[tagIndex + 5];
+ room.palettePath = resources.normalizeResourcePath(tokens[tagIndex + 6]);
+ room.dimmable = tokens[tagIndex + 7].equalsIgnoreCase("T");
+ room.onEnterCommand = tokens[tagIndex + 8];
+ room.onExitCommand = tokens[tagIndex + 9];
+ if (!room.roomName.empty())
+ _rooms.push_back(Common::move(room));
+}
+
+void Script::parseNpcRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 10)
+ return;
+
+ NpcRecord npc;
+ if (tagIndex >= 4) {
+ npc.posX = parseAsciiIntOrZero(tokens[0]);
+ npc.posY = parseAsciiIntOrZero(tokens[1]);
+ npc.posZ = parseAsciiIntOrZero(tokens[2]);
+ npc.frameDelay = parseAsciiIntOrZero(tokens[3]);
+ }
+ npc.roomName = tokens[tagIndex + 1];
+ npc.modelPath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
+ npc.npcName = tokens[tagIndex + 3];
+ npc.monsterfyTargetName = tokens[tagIndex + 4];
+ npc.active = tokens[tagIndex + 5].equalsIgnoreCase("T");
+ npc.visible = tokens[tagIndex + 6].equalsIgnoreCase("T");
+ if (npc.active)
+ npc.visible = true;
+ npc.savedVisible = npc.visible;
+ npc.onDeathActionTag = tokens[tagIndex + 7];
+ npc.audioPath = resources.normalizeResourcePath(tokens[tagIndex + 8]);
+ npc.entityInitArg = tokens[tagIndex + 9];
+ if (!npc.roomName.empty() && !npc.modelPath.empty() && !npc.npcName.empty())
+ _npcs.push_back(Common::move(npc));
+}
+
+void Script::parseMonsterRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 24)
+ return;
+
+ MonsterRecord monster;
+ if (tagIndex >= 6) {
+ monster.posX = parseAsciiIntOrZero(tokens[0]);
+ monster.posY = parseAsciiIntOrZero(tokens[1]);
+ monster.posZ = parseAsciiIntOrZero(tokens[2]);
+ // Native MonsterRecord numeric order is engage distance, initial HP, then damage.
+ monster.engageDistance = parseAsciiIntOrZero(tokens[3]);
+ monster.initialHitPoints = parseAsciiIntOrZero(tokens[4]);
+ monster.damageAmount = parseAsciiIntOrZero(tokens[5]);
+ }
+ if (tagIndex >= 10) {
+ monster.attackSoundTriggerFrame = parseAsciiIntOrZero(tokens[6]);
+ monster.hitSoundTriggerFrame = parseAsciiIntOrZero(tokens[7]);
+ monster.footstepSoundTriggerFrame = parseAsciiIntOrZero(tokens[8]);
+ monster.deathSoundTriggerFrame = parseAsciiIntOrZero(tokens[9]);
+ }
+ monster.roomName = tokens[tagIndex + 1];
+ monster.monsterName = tokens[tagIndex + 2];
+ monster.modelPath = resources.normalizeResourcePath(tokens[tagIndex + 3]);
+ monster.reservedString44 = tokens[tagIndex + 4];
+ monster.reservedString48 = tokens[tagIndex + 5];
+ monster.reservedString38 = tokens[tagIndex + 6];
+ monster.reservedString3c = tokens[tagIndex + 7];
+ monster.facing = parseEntranceFacing(tokens[tagIndex + 8]);
+ monster.damageType = parseDeathDamageType(tokens[tagIndex + 9]);
+ monster.attackSound1 = resources.normalizeResourcePath(tokens[tagIndex + 10]);
+ monster.attackSound2 = resources.normalizeResourcePath(tokens[tagIndex + 11]);
+ monster.attackSound3 = resources.normalizeResourcePath(tokens[tagIndex + 12]);
+ monster.hitSound1 = resources.normalizeResourcePath(tokens[tagIndex + 13]);
+ monster.hitSound2 = resources.normalizeResourcePath(tokens[tagIndex + 14]);
+ monster.hitSound3 = resources.normalizeResourcePath(tokens[tagIndex + 15]);
+ monster.footstepSoundLeft = resources.normalizeResourcePath(tokens[tagIndex + 16]);
+ monster.footstepSoundRight = resources.normalizeResourcePath(tokens[tagIndex + 17]);
+ monster.deathSound = resources.normalizeResourcePath(tokens[tagIndex + 18]);
+ monster.active = tokens[tagIndex + 19].equalsIgnoreCase("T");
+ monster.visible = tokens[tagIndex + 20].equalsIgnoreCase("T");
+ monster.onDeathActionTag = tokens[tagIndex + 21];
+ if (tokens.size() > tagIndex + 22 && !tokens[tagIndex + 22].empty())
+ monster.screenMinXBound = parseAsciiIntOrZero(tokens[tagIndex + 22]);
+ if (tokens.size() > tagIndex + 23 && !tokens[tagIndex + 23].empty())
+ monster.screenMaxXBound = parseAsciiIntOrZero(tokens[tagIndex + 23]);
+ monster.currentHitPoints = monster.initialHitPoints;
+ monster.savedVisible = monster.visible;
+ if (monster.active)
+ monster.visible = true;
+ if (!monster.roomName.empty() && !monster.monsterName.empty() && !monster.modelPath.empty()) {
+ monster.recordIndex = static_cast<int>(_monsters.size());
+ _monsters.push_back(Common::move(monster));
+ }
+}
+
+void Script::parseTimerRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 7)
+ return;
+
+ TimerRecord timer;
+ if (tagIndex >= 1) {
+ timer.initialValue = parseAsciiIntOrZero(tokens[0]);
+ timer.currentValue = timer.initialValue;
+ }
+ timer.timerName = tokens[tagIndex + 1];
+ timer.arg1 = tokens[tagIndex + 2];
+ timer.arg2 = tokens[tagIndex + 3];
+ timer.enabled = tokens[tagIndex + 4].equalsIgnoreCase("T");
+ timer.looping = tokens[tagIndex + 5].equalsIgnoreCase("T");
+ timer.global = tokens[tagIndex + 6].equalsIgnoreCase("T");
+ if (!timer.timerName.empty())
+ _timers.push_back(Common::move(timer));
+}
+
+void Script::parseRegionRecord(const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 7)
+ return;
+
+ RegionRecord region;
+ if (tagIndex >= 6) {
+ region.left = parseAsciiIntOrZero(tokens[0]);
+ region.top = parseAsciiIntOrZero(tokens[1]);
+ region.right = parseAsciiIntOrZero(tokens[2]);
+ region.bottom = parseAsciiIntOrZero(tokens[3]);
+ region.minZ = parseAsciiIntOrZero(tokens[4]);
+ region.maxZ = parseAsciiIntOrZero(tokens[5]);
+ }
+ region.regionName = tokens[tagIndex + 1];
+ region.direction = tokens[tagIndex + 2];
+ region.desiredFacing = parseEntranceFacing(region.direction);
+ region.roomName = tokens[tagIndex + 3];
+ region.actionTag = tokens[tagIndex + 4];
+ region.startEnabled = tokens[tagIndex + 5].equalsIgnoreCase("T");
+ region.cursorEnabled = tokens[tagIndex + 6].equalsIgnoreCase("T");
+ if (!region.roomName.empty() && !region.regionName.empty())
+ _regions.push_back(Common::move(region));
+}
+
+void Script::parseAnimRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex) {
+ if (tokens.size() < tagIndex + 10)
+ return;
+
+ AnimRecord anim;
+ if (tagIndex >= 4) {
+ anim.x = parseAsciiIntOrZero(tokens[0]);
+ anim.y = parseAsciiIntOrZero(tokens[1]);
+ anim.z = parseAsciiIntOrZero(tokens[2]);
+ anim.frameDelay = parseAsciiIntOrZero(tokens[3]);
+ }
+ anim.roomName = tokens[tagIndex + 1];
+ anim.resourcePath = resources.normalizeResourcePath(tokens[tagIndex + 2]);
+ anim.animName = tokens[tagIndex + 3];
+ anim.active = tokens[tagIndex + 4].equalsIgnoreCase("T");
+ anim.visible = tokens[tagIndex + 5].equalsIgnoreCase("T");
+ anim.looping = tokens[tagIndex + 6].equalsIgnoreCase("T");
+ anim.backward = tokens[tagIndex + 7].equalsIgnoreCase("T");
+ anim.pingPong = tokens[tagIndex + 8].equalsIgnoreCase("T");
+ anim.remove = tokens[tagIndex + 9].equalsIgnoreCase("T");
+ anim.runtimeActive = anim.active;
+ anim.runtimeVisible = anim.visible;
+ if (!anim.roomName.empty() && !anim.resourcePath.empty() && !anim.animName.empty())
+ _animations.push_back(Common::move(anim));
+}
+
+void Script::parseObjectRecord(ResourceManager &resources,
+ const Common::Array<Common::String> &tokens, uint tagIndex, uint32 &anonymousObjectId) {
+ if (tokens.size() < tagIndex + 13)
+ return;
+
+ ObjectRecord object;
+ if (tagIndex >= 6) {
+ object.initialX = parseAsciiIntOrZero(tokens[0]);
+ object.initialY = parseAsciiIntOrZero(tokens[1]);
+ object.boundsX2 = parseAsciiIntOrZero(tokens[2]);
+ object.boundsY2 = parseAsciiIntOrZero(tokens[3]);
+ object.initialZ = parseAsciiIntOrZero(tokens[4]);
+ object.zExtent = parseAsciiIntOrZero(tokens[5]);
+ }
+ object.currentX = object.initialX;
+ object.currentY = object.initialY;
+ object.currentZ = object.initialZ;
+ object.initialOwnerOrRoom = tokens[tagIndex + 1];
+ object.objectName = tokens[tagIndex + 2];
+ object.spritePath = resources.normalizeResourcePath(tokens[tagIndex + 3]);
+ object.altSpritePath = resources.normalizeResourcePath(tokens[tagIndex + 4]);
+ object.reservedString40 = tokens[tagIndex + 5];
+ object.inventoryTextKey = tokens[tagIndex + 6];
+ object.reservedXFlag = tokens[tagIndex + 7];
+ object.identTextKey = tokens[tagIndex + 8];
+ object.operatable = tokens[tagIndex + 9].equalsIgnoreCase("T");
+ object.visible = tokens[tagIndex + 10].equalsIgnoreCase("T");
+ object.actionTag = tokens[tagIndex + 11];
+ object.interactionLabel = tokens[tagIndex + 12];
+ object.currentOwnerOrRoom = object.initialOwnerOrRoom;
+ object.runtimeVisible = object.visible;
+ object.identShown = object.identTextKey.empty();
+ if (object.objectName.empty()) {
+ // Native room scripts use unnamed OBJECT entries as blocker rectangles.
+ object.objectName = Common::String::format("__ANON_OBJECT_%u", anonymousObjectId++);
+ }
+ if (!object.initialOwnerOrRoom.empty())
+ _objects.push_back(Common::move(object));
+}
+
+void Script::parseTownRecordLine(ResourceManager &resources,
+ const Common::String &rawLine, uint32 &anonymousObjectId) {
+ const Common::String line = trimAsciiLine(rawLine);
+ if (line.empty() || line[0] == '{')
+ return;
+
+ Common::Array<Common::String> tokens;
+ tokenizeTownScriptLine(line, tokens);
+ if (tokens.empty())
+ return;
+
+ uint tagIndex = tokens.size();
+ for (uint i = 0; i < tokens.size(); ++i) {
+ if (tokens[i] == "ANIM" || tokens[i] == "ENTRANCE" || tokens[i] == "MAP_ENTRANCE" ||
+ tokens[i] == "MAP_LOCATION" || tokens[i] == "ROOM" || tokens[i] == "OBJECT" ||
+ tokens[i] == "NPC" || tokens[i] == "MONSTER" || tokens[i] == "REGION" ||
+ tokens[i] == "HEAD" || tokens[i] == "FLAG" || tokens[i] == "COMMAND" ||
+ tokens[i] == "EXEC_LIST" || tokens[i] == "TEXT" || tokens[i] == "TIMER" ||
+ tokens[i] == "USEITEM") {
+ tagIndex = i;
+ break;
+ }
+ }
+ if (tagIndex == tokens.size())
+ return;
+
+ const Common::String &tag = tokens[tagIndex];
+ if (tag == "FLAG")
+ parseFlagRecord(tokens, tagIndex);
+ else if (tag == "COMMAND")
+ parseCommandRecord(resources, tokens, tagIndex);
+ else if (tag == "EXEC_LIST")
+ parseExecListRecord(tokens, tagIndex);
+ else if (tag == "TEXT")
+ parseTextRecord(tokens, tagIndex);
+ else if (tag == "HEAD")
+ parseHeadRecord(resources, tokens, tagIndex);
+ else if (tag == "USEITEM")
+ parseUseItemRecord(tokens, tagIndex);
+ else if (tag == "ENTRANCE")
+ parseEntranceRecord(tokens, tagIndex);
+ else if (tag == "MAP_ENTRANCE")
+ parseMapEntranceRecord(tokens, tagIndex);
+ else if (tag == "MAP_LOCATION")
+ parseMapLocationRecord(tokens, tagIndex);
+ else if (tag == "ROOM")
+ parseRoomRecord(resources, tokens, tagIndex);
+ else if (tag == "NPC")
+ parseNpcRecord(resources, tokens, tagIndex);
+ else if (tag == "MONSTER")
+ parseMonsterRecord(resources, tokens, tagIndex);
+ else if (tag == "TIMER")
+ parseTimerRecord(tokens, tagIndex);
+ else if (tag == "REGION")
+ parseRegionRecord(tokens, tagIndex);
+ else if (tag == "ANIM")
+ parseAnimRecord(resources, tokens, tagIndex);
+ else
+ parseObjectRecord(resources, tokens, tagIndex, anonymousObjectId);
+}
+
+void Script::parseTownRecords(ResourceManager &resources) {
+ _entrances.clear();
+ _mapEntrances.clear();
+ _mapLocations.clear();
+ _rooms.clear();
+ _objects.clear();
+ _animations.clear();
+ _npcs.clear();
+ _monsters.clear();
+ _timers.clear();
+ _regions.clear();
+ _flags.clear();
+ _commands.clear();
+ _execLists.clear();
+ _texts.clear();
+ _heads.clear();
+ _useItems.clear();
+
+ uint32 anonymousObjectId = 0;
+
+ Common::String line;
+ for (uint i = 0; i < _data.size(); ++i) {
+ const char ch = static_cast<char>(_data[i]);
+ if (ch == '\r')
+ continue;
+ if (ch == '\n') {
+ parseTownRecordLine(resources, line, anonymousObjectId);
+ line.clear();
+ continue;
+ }
+
+ line += ch;
+ }
+
+ parseTownRecordLine(resources, line, anonymousObjectId);
+
+ debug(1, "Harvester: parsed %u entrances, %u map entrances, %u map locations, %u rooms, %u objects, %u anims, %u npcs, %u regions, %u flags, %u commands, %u exec_lists, %u texts, %u heads, %u useitems from '%s'",
+ (uint)_entrances.size(), (uint)_mapEntrances.size(), (uint)_mapLocations.size(),
+ (uint)_rooms.size(), (uint)_objects.size(), (uint)_animations.size(),
+ (uint)_npcs.size(), (uint)_regions.size(),
+ (uint)_flags.size(), (uint)_commands.size(), (uint)_execLists.size(),
+ (uint)_texts.size(), (uint)_heads.size(),
+ (uint)_useItems.size(), _path.c_str());
+}
+
+} // End of namespace Harvester
Commit: 1f1f13ca21fe72cb835cbfbb1452b2c65bdd6fe3
https://github.com/scummvm/scummvm/commit/1f1f13ca21fe72cb835cbfbb1452b2c65bdd6fe3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Split command execution into opcode handlers
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/script_commands.cpp
engines/harvester/module.mk
engines/harvester/script.cpp
engines/harvester/script.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 4ccd5daa022..a7f91293f6d 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -26,6 +26,7 @@ MODULE_OBJS = \
entity.o \
saveload.o \
script.o \
+ script_commands.o \
script_records.o \
text.o \
xfile_archive.o \
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index a865c23c32e..e70d2bc8d7c 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -30,7 +30,6 @@
#include "common/memstream.h"
#include "common/serializer.h"
#include "harvester/detection.h"
-#include "harvester/harvester.h"
#include "harvester/parse_utils.h"
#include "harvester/player.h"
#include "harvester/resources.h"
@@ -313,7 +312,7 @@ static int clampCombatResourceCount(int loadout, int count) {
return CLIP<int>(count, 0, maxCount);
}
-static int resolveCombatResourcePickupMax(int loadout) {
+int Script::resolveCombatResourcePickupMax(int loadout) {
switch (loadout) {
case 2:
case 3:
@@ -328,22 +327,6 @@ static int resolveCombatResourcePickupMax(int loadout) {
}
}
-static int resolveCombatResourceLoadoutForOpcode(const Common::String &opcodeName) {
- // Confirmed from dispatch_room_event_actions and HARVEST.SCR.
- if (opcodeName.equalsIgnoreCase("ADD_NAILS"))
- return 2;
- if (opcodeName.equalsIgnoreCase("ADD_SHOTGUN_SHELLS"))
- return 3;
- if (opcodeName.equalsIgnoreCase("ADD_GASCANS"))
- return 14;
-
- return 0;
-}
-
-static bool isCombatResourceOpcode(const Common::String &opcodeName) {
- return resolveCombatResourceLoadoutForOpcode(opcodeName) != 0;
-}
-
static const char *resolveCombatLoadoutInventoryObjectName(int loadout) {
for (const InventoryCombatLoadoutEntry &entry : kInventoryCombatLoadoutMap) {
if (entry.loadoutId == loadout)
@@ -419,51 +402,6 @@ static Common::String dialogueTextModeToConfigValue(StartupDialogueTextMode mode
}
}
-static bool appendStartupAudioCommand(const CommandRecord &command, Common::Array<AudioCommand> &commands) {
- if (command.opcodeName.equalsIgnoreCase("START_WAV")) {
- AudioCommand audioCommand;
- audioCommand.type = kStartupAudioCommandStartWav;
- audioCommand.path = command.arg1;
- commands.push_back(Common::move(audioCommand));
- return true;
- }
-
- if (command.opcodeName.equalsIgnoreCase("START_SINGLE_WAV")) {
- AudioCommand audioCommand;
- audioCommand.type = kStartupAudioCommandStartSingleWav;
- audioCommand.path = command.arg1;
- commands.push_back(Common::move(audioCommand));
- return true;
- }
-
- if (command.opcodeName.equalsIgnoreCase("LOAD_WAV")) {
- AudioCommand audioCommand;
- audioCommand.type = kStartupAudioCommandLoadWav;
- audioCommand.path = command.arg1;
- audioCommand.slot = command.arg2.empty() ? -1 : parseAsciiIntOrZero(command.arg2);
- commands.push_back(Common::move(audioCommand));
- return true;
- }
-
- if (command.opcodeName.equalsIgnoreCase("PLAY_WAV")) {
- AudioCommand audioCommand;
- audioCommand.type = kStartupAudioCommandPlayWav;
- audioCommand.slot = command.arg1.empty() ? -1 : parseAsciiIntOrZero(command.arg1);
- commands.push_back(Common::move(audioCommand));
- return true;
- }
-
- if (command.opcodeName.equalsIgnoreCase("DELETE_WAV")) {
- AudioCommand audioCommand;
- audioCommand.type = kStartupAudioCommandDeleteWav;
- audioCommand.slot = command.arg1.empty() ? -1 : parseAsciiIntOrZero(command.arg1);
- commands.push_back(Common::move(audioCommand));
- return true;
- }
-
- return false;
-}
-
static bool loadBitmapDimensions(ResourceManager &resources, const Common::String &path,
uint32 &width, uint32 &height) {
Common::Array<byte> data;
@@ -2195,650 +2133,6 @@ bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord
return true;
}
-void Script::executeCommandChain(const Common::String &initialTag, const CommandExecutionContext &context,
- CommandOutputPolicy outputPolicy, InteractionResult &result) {
- debugC(3, kDebugScene, "Harvester: command chain tag='%s' context='%s' source='%s' room='%s' transitions=%d outputs='%s'",
- initialTag.c_str(), context.label, context.name.c_str(), context.roomName.c_str(), context.allowTransitions,
- outputPolicy == kCommandOutputsRoomExit ? "room exit" : "interaction");
-
- auto isTruthy = [](const Common::String &value) {
- return value.equalsIgnoreCase("T") || value.equalsIgnoreCase("ON") || value.equalsIgnoreCase("TRUE");
- };
- auto noteMutation = [&](bool changed) {
- if (changed)
- result.mutatedRuntimeState = true;
- };
- auto noteCurrentRoomVisualMutation = [&](bool changed, bool affectsCurrentRoom) {
- noteMutation(changed);
- if (changed && affectsCurrentRoom)
- result.visualRuntimeStateChanged = true;
- };
- auto noteTimerMutation = [&](const TimerRecord *previousTimer, bool changed) {
- if (!changed || !previousTimer || previousTimer->timerName.empty())
- return;
-
- for (const TimerRecord &record : result.previousTimerRecords) {
- if (record.timerName.equalsIgnoreCase(previousTimer->timerName))
- return;
- }
-
- result.previousTimerRecords.push_back(*previousTimer);
- };
- auto belongsToContextRoom = [&](const Common::String &roomName) {
- return !context.roomName.empty() && roomName.equalsIgnoreCase(context.roomName);
- };
- auto hasDeferredInteractionOutputs = [&]() {
- return (outputPolicy == kCommandOutputsInteraction &&
- (!result.nextRoomName.empty() || !result.deathFlicPath.empty() || result.requestMainMenu)) ||
- !result.cutscenePath.empty() || result.requestDemoEnding ||
- !result.dialogueNpcName.empty() || !result.continuationTag.empty() ||
- !result.modalText.value.empty() || result.lightingCommand != kStartupLightingCommandNone ||
- !result.moveEntityToPlayerZName.empty() || result.requestCloseupExit;
- };
-
- Common::String currentTag = initialTag;
- for (uint step = 0; step < 128 && !currentTag.empty(); ++step) {
- const CommandRecord *command = findCommandRecord(currentTag);
- if (!command) {
- debug(1, "Harvester: unresolved %s tag '%s' for '%s'",
- context.label, currentTag.c_str(), context.name.c_str());
- break;
- }
-
- debugC(3, kDebugScene,
- "Harvester: %s '%s' step=%u tag='%s' opcode='%s' args=['%s','%s','%s','%s']",
- context.label, context.name.c_str(), step, currentTag.c_str(), command->opcodeName.c_str(),
- command->arg1.c_str(), command->arg2.c_str(), command->arg3.c_str(), command->arg4.c_str());
-
- if (command->opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
- const FlagRecord *flag = findRuntimeFlag(command->arg1);
- const bool flagValue = flag && flag->value;
- debugC(3, kDebugScene, "Harvester: %s '%s' flag '%s' -> %d",
- context.label, context.name.c_str(), command->arg1.c_str(), flagValue);
- currentTag = flagValue ? command->arg2 : command->arg3;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHECK_PERC")) {
- const int threshold = CLIP<int>(parseAsciiIntOrZero(command->arg1), 0, 100);
- const int roll = g_engine ? (int)g_engine->getRandomNumber(99) : 0;
- currentTag = roll < threshold ? command->arg2 : command->arg3;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_FLAG")) {
- const bool flagValue = isTruthy(command->arg2);
- FlagRecord *flag = findRuntimeFlag(command->arg1);
- bool changed = false;
- bool oldValue = false;
- const bool existed = flag != nullptr;
- if (flag) {
- oldValue = flag->value;
- changed = flag->value != flagValue;
- flag->value = flagValue;
- } else {
- FlagRecord newFlag;
- newFlag.name = command->arg1;
- newFlag.value = flagValue;
- _currentFlags.push_back(Common::move(newFlag));
- changed = true;
- }
- debugC(1, kDebugGeneral,
- "Harvester: %s '%s' SET_FLAG '%s' %d -> %d existed=%d changed=%d",
- context.label, context.name.c_str(), command->arg1.c_str(), oldValue, flagValue,
- existed, changed);
- noteMutation(changed);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SPOOL_MUSIC")) {
- result.musicPath = command->arg1;
- currentTag = command->arg4;
- continue;
- }
-
- if (appendStartupAudioCommand(*command, result.audioCommands)) {
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("ADD") ||
- command->opcodeName.equalsIgnoreCase("DELETE")) {
- ObjectRecord *currentObject = findRuntimeObject(command->arg1, command->arg2);
- if (!currentObject) {
- debug(1, "Harvester: unresolved object for %s '%s' owner='%s' object='%s'",
- context.label, context.name.c_str(), command->arg1.c_str(), command->arg2.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- const bool visible = command->opcodeName.equalsIgnoreCase("ADD");
- const bool affectsCurrentRoom = belongsToContextRoom(currentObject->currentOwnerOrRoom);
- const bool changed = currentObject->visible != visible ||
- currentObject->runtimeVisible != visible;
- currentObject->visible = visible;
- currentObject->runtimeVisible = visible;
- if (visible && currentObject->currentOwnerOrRoom.equalsIgnoreCase("INVENTORY"))
- currentObject->identShown = true;
- noteCurrentRoomVisualMutation(changed, affectsCurrentRoom);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("ADD2INV")) {
- const ObjectRecord *currentObject = findRuntimeObject(Common::String(), command->arg1);
- const bool affectsCurrentRoom = currentObject &&
- belongsToContextRoom(currentObject->currentOwnerOrRoom);
- noteCurrentRoomVisualMutation(
- addRuntimeObjectToInventory(command->arg1), affectsCurrentRoom);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_ANIM")) {
- AnimRecord *currentAnimation = findRuntimeAnim(command->arg1);
- if (!currentAnimation) {
- debug(1, "Harvester: unresolved anim for %s '%s' anim='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- const bool wasVisible = currentAnimation->visible;
- const bool active = isTruthy(command->arg2);
- const bool visible = isTruthy(command->arg3);
- int runtimeState = currentAnimation->runtimeState;
- if (active && visible &&
- (!wasVisible || !currentAnimation->runtimeActive || runtimeState < 0)) {
- runtimeState = 0;
- }
- const bool changed = currentAnimation->active != active ||
- currentAnimation->visible != visible ||
- currentAnimation->runtimeActive != active ||
- currentAnimation->runtimeVisible != visible ||
- currentAnimation->runtimeState != runtimeState;
- currentAnimation->active = active;
- currentAnimation->visible = visible;
- currentAnimation->runtimeActive = active;
- currentAnimation->runtimeVisible = visible;
- currentAnimation->runtimeState = runtimeState;
- noteCurrentRoomVisualMutation(changed,
- belongsToContextRoom(currentAnimation->roomName));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_REGION")) {
- RegionRecord *currentRegion = findRuntimeRegion(command->arg1);
- if (!currentRegion) {
- debug(1, "Harvester: unresolved region for %s '%s' region='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- const bool enabled = !command->arg2.equalsIgnoreCase("F");
- const bool changed = currentRegion->startEnabled != enabled;
- currentRegion->startEnabled = enabled;
- noteCurrentRoomVisualMutation(changed,
- belongsToContextRoom(currentRegion->roomName));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_NPC")) {
- NpcRecord *currentNpc = findRuntimeNpc(command->arg1);
- if (!currentNpc) {
- debug(1, "Harvester: unresolved npc for %s '%s' npc='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- const bool active = isTruthy(command->arg2);
- const bool visible = isTruthy(command->arg3);
- const bool changed = currentNpc->active != active || currentNpc->visible != visible;
- currentNpc->active = active;
- currentNpc->visible = visible;
- noteCurrentRoomVisualMutation(changed,
- belongsToContextRoom(currentNpc->roomName));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_MONSTER")) {
- MonsterRecord monsterLookup;
- monsterLookup.roomName = context.roomName;
- monsterLookup.monsterName = command->arg1;
- MonsterRecord *currentMonster = findRuntimeMonster(monsterLookup);
- if (!currentMonster) {
- debug(1, "Harvester: unresolved monster for %s '%s' monster='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- const bool active = isTruthy(command->arg2);
- const bool visible = isTruthy(command->arg3);
- const bool changed = currentMonster->active != active || currentMonster->visible != visible;
- currentMonster->active = active;
- currentMonster->visible = active ? true : visible;
- if (active) {
- currentMonster->runtimeSpawned = false;
- currentMonster->runtimeState = -1;
- }
- if (!currentMonster->visible) {
- currentMonster->runtimeSpawned = false;
- currentMonster->runtimeState = -1;
- }
- if (active && currentMonster->currentHitPoints <= 0)
- currentMonster->currentHitPoints = currentMonster->initialHitPoints;
- noteCurrentRoomVisualMutation(changed,
- belongsToContextRoom(currentMonster->roomName));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_TIMER")) {
- const TimerRecord *currentTimer = findRuntimeTimer(command->arg1);
- TimerRecord previousTimer;
- if (currentTimer)
- previousTimer = *currentTimer;
- const bool changed =
- setRuntimeTimerEnabled(command->arg1, command->arg2.equalsIgnoreCase("ON"));
- noteCurrentRoomVisualMutation(changed,
- currentTimer && belongsToContextRoom(currentTimer->arg1));
- noteTimerMutation(currentTimer ? &previousTimer : nullptr, changed);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("KILL_TIMER")) {
- const TimerRecord *currentTimer = findRuntimeTimer(command->arg1);
- TimerRecord previousTimer;
- if (currentTimer)
- previousTimer = *currentTimer;
- const bool changed = setRuntimeTimerEnabled(command->arg1, false);
- noteCurrentRoomVisualMutation(changed,
- currentTimer && belongsToContextRoom(currentTimer->arg1));
- noteTimerMutation(currentTimer ? &previousTimer : nullptr, changed);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("EXEC_LIST")) {
- const ExecListRecord *execList = findExecListRecord(command->arg1);
- if (!execList) {
- debug(1, "Harvester: unresolved exec list for %s '%s' list='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- for (const Common::String &entry : execList->entries) {
- const CommandExecutionContext entryContext = {
- "exec list entry", execList->listName, context.roomName, context.allowTransitions
- };
- executeCommandChain(entry, entryContext, outputPolicy, result);
- if (hasDeferredInteractionOutputs())
- return;
- }
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("START_DIALOG")) {
- if (command->arg1.empty()) {
- currentTag = command->arg4;
- continue;
- }
-
- result.dialogueNpcName = command->arg1;
- result.dialogueContinuationTag = command->arg4;
- return;
- }
-
- if (command->opcodeName.equalsIgnoreCase("GOFLIC")) {
- result.cutscenePath = command->arg1;
- result.continuationTag = command->arg4;
- return;
- }
-
- if (command->opcodeName.equalsIgnoreCase("GODEATHFLIC")) {
- if (outputPolicy == kCommandOutputsRoomExit) {
- debug(1, "Harvester: unsupported startup command '%s' for %s '%s' without menu-exit context",
- command->opcodeName.c_str(), context.label, context.name.c_str());
- return;
- }
-
- if (context.allowTransitions) {
- result.deathFlicPath = command->arg1;
- result.requestMainMenu = true;
- } else {
- debugC(1, kDebugScene, "Harvester: skipped transition opcode '%s' while processing %s '%s'",
- command->opcodeName.c_str(), context.label, context.name.c_str());
- }
- return;
- }
-
- if (command->opcodeName.equalsIgnoreCase("END_DEMO")) {
- result.requestDemoEnding = true;
- return;
- }
-
- if (command->opcodeName.equalsIgnoreCase("KILL_NPC") ||
- command->opcodeName.equalsIgnoreCase("MONSTERFY")) {
- NpcRecord *currentNpc = findRuntimeNpc(command->arg1);
- if (!currentNpc) {
- debug(1, "Harvester: unresolved npc for %s '%s' npc='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- const int deathDamageType = parseDeathDamageType(command->arg2);
- const bool deferLiveNpcDeathTransition =
- _deferredLiveNpcDeathTransitionDepth > 0 &&
- !context.roomName.empty() &&
- currentNpc->roomName.equalsIgnoreCase(context.roomName) &&
- currentNpc->active &&
- currentNpc->visible &&
- !currentNpc->deathOrMonsterfyFlag &&
- currentNpc->runtimeState < 0;
- noteCurrentRoomVisualMutation(deferLiveNpcDeathTransition
- ? queueRuntimeNpcDeathOrMonsterfy(command->arg1, deathDamageType)
- : finalizeRuntimeNpcDeathOrMonsterfy(command->arg1, deathDamageType),
- belongsToContextRoom(currentNpc->roomName));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SHOW_TEXT")) {
- if (!resolveTextRecord(command->arg1, result.modalText)) {
- debug(1, "Harvester: unresolved SHOW_TEXT record for %s '%s' key='%s'",
- context.label, context.name.c_str(), command->arg1.c_str());
- currentTag = command->arg4;
- continue;
- }
- result.continuationTag = command->arg4;
- return;
- }
-
- if (command->opcodeName.equalsIgnoreCase("HEAL_PC") ||
- command->opcodeName.equalsIgnoreCase("ADJ_HP")) {
- noteMutation(setPlayerCurrentHitPoints(
- _playerCurrentHitPoints + parseAsciiIntOrZero(command->arg1)));
- currentTag = command->arg4;
- continue;
- }
-
- const int combatResourceLoadout = resolveCombatResourceLoadoutForOpcode(command->opcodeName);
- if (combatResourceLoadout != 0) {
- noteMutation(adjustPlayerCombatResourceCount(combatResourceLoadout,
- parseAsciiIntOrZero(command->arg1),
- resolveCombatResourcePickupMax(combatResourceLoadout),
- command->opcodeName));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("PC_CHANGE_WEAPON")) {
- noteCurrentRoomVisualMutation(
- setPlayerCombatLoadout(parseAsciiIntOrZero(command->arg1)), true);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("MOVE_BM2PCZ")) {
- result.moveEntityToPlayerZName = command->arg1;
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("KILL_PC")) {
- result.requestPlayerDeath = true;
- result.playerDeathDamageType = parseDeathDamageType(command->arg1);
- noteMutation(setPlayerCurrentHitPoints(0));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("PAUSE_PC")) {
- noteMutation(setPlayerControlPaused(true));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("RESUME_PC")) {
- noteMutation(setPlayerControlPaused(false));
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("PC_GOTO_XZ")) {
- result.requestPlayerGotoXZ = true;
- result.playerGotoX = parseAsciiIntOrZero(command->arg1);
- result.playerGotoZ = parseAsciiIntOrZero(command->arg2);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("PC_GOTO_Z")) {
- result.requestPlayerGotoZ = true;
- result.playerGotoZ = parseAsciiIntOrZero(command->arg1);
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHANGE_LIGHTING")) {
- if (command->arg1.equalsIgnoreCase("DIM"))
- result.lightingCommand = kStartupLightingCommandDim;
- else if (command->arg1.equalsIgnoreCase("NORMAL"))
- result.lightingCommand = kStartupLightingCommandNormal;
- else if (command->arg1.equalsIgnoreCase("NONE"))
- result.lightingCommand = kStartupLightingCommandBlack;
- else if (command->arg1.equalsIgnoreCase("FADE_IN"))
- result.lightingCommand = kStartupLightingCommandFadeIn;
- else
- debug(1, "Harvester: unsupported CHANGE_LIGHTING mode '%s' for %s '%s', preserving control flow",
- command->arg1.c_str(), context.label, context.name.c_str());
- result.continuationTag = command->arg4;
- return;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHANGE_CD")) {
- const int requestedDisc = parseAsciiIntOrZero(command->arg1);
- if (requestedDisc > 0)
- result.cdChangeDisc = requestedDisc;
- else
- debug(1, "Harvester: invalid CHANGE_CD target '%s' for %s '%s'",
- command->arg1.c_str(), context.label, context.name.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CLOSEUP") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")) {
- if (context.allowTransitions && outputPolicy == kCommandOutputsInteraction) {
- result.nextRoomName = command->arg1;
- result.roomTransition = command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")
- ? kStartupRoomTransitionChangeRoom
- : kStartupRoomTransitionCloseup;
- } else {
- debugC(1, kDebugScene, "Harvester: skipped transition opcode '%s' while processing %s '%s'",
- command->opcodeName.c_str(), context.label, context.name.c_str());
- }
- currentTag = command->arg4;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("EXIT_CLOSEUP")) {
- result.requestCloseupExit = true;
- currentTag = command->arg4;
- continue;
- }
-
- debug(1, "Harvester: unsupported startup command '%s' for %s '%s', continuing",
- command->opcodeName.c_str(), context.label, context.name.c_str());
- currentTag = command->arg4;
- }
-}
-
-bool Script::probePickupBlockingCommandChain(const Common::String &initialTag,
- const Common::String &contextName, InteractionResult &result, uint recursionDepth) const {
- if (initialTag.empty() || recursionDepth > 8)
- return false;
-
- Common::String currentTag = initialTag;
- for (uint step = 0; step < 128 && !currentTag.empty(); ++step) {
- const CommandRecord *command = findCommandRecord(currentTag);
- if (!command) {
- debug(1, "Harvester: unresolved pickup preflight tag '%s' for '%s'",
- currentTag.c_str(), contextName.c_str());
- return false;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
- const FlagRecord *flag = findRuntimeFlag(command->arg1);
- currentTag = (flag && flag->value) ? command->arg2 : command->arg3;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHECK_PERC"))
- return false;
-
- if (command->opcodeName.equalsIgnoreCase("SHOW_TEXT")) {
- if (!resolveTextRecord(command->arg1, result.modalText))
- return false;
- result.continuationTag = command->arg4;
- return !result.modalText.value.empty();
- }
-
- if (command->opcodeName.equalsIgnoreCase("EXEC_LIST")) {
- const ExecListRecord *execList = findExecListRecord(command->arg1);
- if (!execList)
- return false;
-
- for (const Common::String &entry : execList->entries) {
- if (probePickupBlockingCommandChain(entry, contextName, result, recursionDepth + 1))
- return true;
- }
- return false;
- }
-
- Common::Array<AudioCommand> audioCommands;
- if (appendStartupAudioCommand(*command, audioCommands))
- return false;
-
- if (command->opcodeName.equalsIgnoreCase("SET_FLAG") ||
- command->opcodeName.equalsIgnoreCase("SPOOL_MUSIC") ||
- command->opcodeName.equalsIgnoreCase("ADD") ||
- command->opcodeName.equalsIgnoreCase("DELETE") ||
- command->opcodeName.equalsIgnoreCase("ADD2INV") ||
- isCombatResourceOpcode(command->opcodeName) ||
- command->opcodeName.equalsIgnoreCase("SET_ANIM") ||
- command->opcodeName.equalsIgnoreCase("SET_REGION") ||
- command->opcodeName.equalsIgnoreCase("SET_NPC") ||
- command->opcodeName.equalsIgnoreCase("SET_MONSTER") ||
- command->opcodeName.equalsIgnoreCase("SET_TIMER") ||
- command->opcodeName.equalsIgnoreCase("KILL_TIMER") ||
- command->opcodeName.equalsIgnoreCase("START_DIALOG") ||
- command->opcodeName.equalsIgnoreCase("GOFLIC") ||
- command->opcodeName.equalsIgnoreCase("GODEATHFLIC") ||
- command->opcodeName.equalsIgnoreCase("END_DEMO") ||
- command->opcodeName.equalsIgnoreCase("KILL_NPC") ||
- command->opcodeName.equalsIgnoreCase("MONSTERFY") ||
- command->opcodeName.equalsIgnoreCase("HEAL_PC") ||
- command->opcodeName.equalsIgnoreCase("ADJ_HP") ||
- command->opcodeName.equalsIgnoreCase("KILL_PC") ||
- command->opcodeName.equalsIgnoreCase("PAUSE_PC") ||
- command->opcodeName.equalsIgnoreCase("RESUME_PC") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_CD") ||
- command->opcodeName.equalsIgnoreCase("MOVE_BM2PCZ") ||
- command->opcodeName.equalsIgnoreCase("PC_CHANGE_WEAPON") ||
- command->opcodeName.equalsIgnoreCase("PC_GOTO_XZ") ||
- command->opcodeName.equalsIgnoreCase("PC_GOTO_Z") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_LIGHTING") ||
- command->opcodeName.equalsIgnoreCase("EXIT_CLOSEUP") ||
- command->opcodeName.equalsIgnoreCase("CLOSEUP") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")) {
- return false;
- }
-
- currentTag = command->arg4;
- }
-
- return false;
-}
-
-bool Script::hasActionableCommandChain(const Common::String &initialTag) const {
- Common::String currentTag = initialTag;
- for (uint step = 0; step < 128 && !currentTag.empty(); ++step) {
- const CommandRecord *command = findCommandRecord(currentTag);
- if (!command) {
- debug(1, "Harvester: unresolved interaction probe tag '%s'", currentTag.c_str());
- break;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
- const FlagRecord *flag = findRuntimeFlag(command->arg1);
- currentTag = (flag && flag->value) ? command->arg2 : command->arg3;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("CHECK_PERC")) {
- currentTag = command->arg2.empty() ? command->arg3 : command->arg2;
- continue;
- }
-
- if (command->opcodeName.equalsIgnoreCase("SET_FLAG") ||
- command->opcodeName.equalsIgnoreCase("SPOOL_MUSIC") ||
- command->opcodeName.equalsIgnoreCase("ADD") ||
- command->opcodeName.equalsIgnoreCase("DELETE") ||
- command->opcodeName.equalsIgnoreCase("ADD2INV") ||
- isCombatResourceOpcode(command->opcodeName) ||
- command->opcodeName.equalsIgnoreCase("SET_ANIM") ||
- command->opcodeName.equalsIgnoreCase("SET_REGION") ||
- command->opcodeName.equalsIgnoreCase("SET_NPC") ||
- command->opcodeName.equalsIgnoreCase("SET_MONSTER") ||
- command->opcodeName.equalsIgnoreCase("SET_TIMER") ||
- command->opcodeName.equalsIgnoreCase("KILL_TIMER") ||
- command->opcodeName.equalsIgnoreCase("EXEC_LIST") ||
- command->opcodeName.equalsIgnoreCase("START_DIALOG") ||
- command->opcodeName.equalsIgnoreCase("GOFLIC") ||
- command->opcodeName.equalsIgnoreCase("GODEATHFLIC") ||
- command->opcodeName.equalsIgnoreCase("END_DEMO") ||
- command->opcodeName.equalsIgnoreCase("KILL_NPC") ||
- command->opcodeName.equalsIgnoreCase("MONSTERFY") ||
- command->opcodeName.equalsIgnoreCase("SHOW_TEXT") ||
- command->opcodeName.equalsIgnoreCase("HEAL_PC") ||
- command->opcodeName.equalsIgnoreCase("ADJ_HP") ||
- command->opcodeName.equalsIgnoreCase("KILL_PC") ||
- command->opcodeName.equalsIgnoreCase("PAUSE_PC") ||
- command->opcodeName.equalsIgnoreCase("RESUME_PC") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_CD") ||
- command->opcodeName.equalsIgnoreCase("MOVE_BM2PCZ") ||
- command->opcodeName.equalsIgnoreCase("PC_CHANGE_WEAPON") ||
- command->opcodeName.equalsIgnoreCase("PC_GOTO_XZ") ||
- command->opcodeName.equalsIgnoreCase("PC_GOTO_Z") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_LIGHTING") ||
- command->opcodeName.equalsIgnoreCase("EXIT_CLOSEUP") ||
- command->opcodeName.equalsIgnoreCase("CLOSEUP") ||
- command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")) {
- return true;
- }
-
- Common::Array<AudioCommand> audioCommands;
- if (appendStartupAudioCommand(*command, audioCommands))
- return true;
-
- currentTag = command->arg4;
- }
-
- return false;
-}
-
const TextRecord *Script::findTextRecord(const Common::String &key) const {
if (key.empty())
return nullptr;
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index f045cbfc545..4e654f2df1c 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -470,6 +470,12 @@ private:
kCommandOutputsRoomExit
};
+ enum CommandStepResult {
+ kCommandUnhandled,
+ kCommandContinue,
+ kCommandStop
+ };
+
bool loadConfig(ResourceManager &resources);
void decode();
void parseTownRecords(ResourceManager &resources);
@@ -523,6 +529,37 @@ private:
ResourceManager &resources, RoomSetupState &state) const;
void executeCommandChain(const Common::String &initialTag, const CommandExecutionContext &context,
CommandOutputPolicy outputPolicy, InteractionResult &result);
+ CommandStepResult executeCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, CommandOutputPolicy outputPolicy,
+ InteractionResult &result, Common::String &nextTag);
+ CommandStepResult executeBranchCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result, Common::String &nextTag);
+ CommandStepResult executeAudioCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeObjectCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeAnimationCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeRegionCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeNpcCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeMonsterCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeTimerCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executeExecListCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, CommandOutputPolicy outputPolicy, InteractionResult &result);
+ CommandStepResult executeModalCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result);
+ CommandStepResult executePlayerCommand(const CommandRecord &command,
+ InteractionResult &result);
+ CommandStepResult executeMovementCommand(const CommandRecord &command,
+ InteractionResult &result);
+ CommandStepResult executeTransitionCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, CommandOutputPolicy outputPolicy, InteractionResult &result);
+ static bool hasDeferredInteractionOutputs(const InteractionResult &result, CommandOutputPolicy outputPolicy);
+ static int resolveCombatResourcePickupMax(int loadout);
bool probePickupBlockingCommandChain(const Common::String &initialTag,
const Common::String &contextName, InteractionResult &result, uint recursionDepth) const;
bool hasActionableCommandChain(const Common::String &initialTag) const;
diff --git a/engines/harvester/script_commands.cpp b/engines/harvester/script_commands.cpp
new file mode 100644
index 00000000000..ded7902ee1c
--- /dev/null
+++ b/engines/harvester/script_commands.cpp
@@ -0,0 +1,822 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/script.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "harvester/detection.h"
+#include "harvester/harvester.h"
+#include "harvester/parse_utils.h"
+
+namespace Harvester {
+
+static int resolveCombatResourceLoadoutForOpcode(const Common::String &opcodeName) {
+ // Confirmed from dispatch_room_event_actions and HARVEST.SCR.
+ if (opcodeName.equalsIgnoreCase("ADD_NAILS"))
+ return 2;
+ if (opcodeName.equalsIgnoreCase("ADD_SHOTGUN_SHELLS"))
+ return 3;
+ if (opcodeName.equalsIgnoreCase("ADD_GASCANS"))
+ return 14;
+
+ return 0;
+}
+
+static bool isCombatResourceOpcode(const Common::String &opcodeName) {
+ return resolveCombatResourceLoadoutForOpcode(opcodeName) != 0;
+}
+
+static bool appendStartupAudioCommand(const CommandRecord &command, Common::Array<AudioCommand> &commands) {
+ if (command.opcodeName.equalsIgnoreCase("START_WAV")) {
+ AudioCommand audioCommand;
+ audioCommand.type = kStartupAudioCommandStartWav;
+ audioCommand.path = command.arg1;
+ commands.push_back(Common::move(audioCommand));
+ return true;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("START_SINGLE_WAV")) {
+ AudioCommand audioCommand;
+ audioCommand.type = kStartupAudioCommandStartSingleWav;
+ audioCommand.path = command.arg1;
+ commands.push_back(Common::move(audioCommand));
+ return true;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("LOAD_WAV")) {
+ AudioCommand audioCommand;
+ audioCommand.type = kStartupAudioCommandLoadWav;
+ audioCommand.path = command.arg1;
+ audioCommand.slot = command.arg2.empty() ? -1 : parseAsciiIntOrZero(command.arg2);
+ commands.push_back(Common::move(audioCommand));
+ return true;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("PLAY_WAV")) {
+ AudioCommand audioCommand;
+ audioCommand.type = kStartupAudioCommandPlayWav;
+ audioCommand.slot = command.arg1.empty() ? -1 : parseAsciiIntOrZero(command.arg1);
+ commands.push_back(Common::move(audioCommand));
+ return true;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("DELETE_WAV")) {
+ AudioCommand audioCommand;
+ audioCommand.type = kStartupAudioCommandDeleteWav;
+ audioCommand.slot = command.arg1.empty() ? -1 : parseAsciiIntOrZero(command.arg1);
+ commands.push_back(Common::move(audioCommand));
+ return true;
+ }
+
+ return false;
+}
+
+static bool isTruthy(const Common::String &value) {
+ return value.equalsIgnoreCase("T") || value.equalsIgnoreCase("ON") || value.equalsIgnoreCase("TRUE");
+}
+
+static void noteMutation(InteractionResult &result, bool changed) {
+ if (changed)
+ result.mutatedRuntimeState = true;
+}
+
+static void noteCurrentRoomVisualMutation(InteractionResult &result, bool changed, bool affectsCurrentRoom) {
+ noteMutation(result, changed);
+ if (changed && affectsCurrentRoom)
+ result.visualRuntimeStateChanged = true;
+}
+
+static void noteTimerMutation(InteractionResult &result, const TimerRecord *previousTimer, bool changed) {
+ if (!changed || !previousTimer || previousTimer->timerName.empty())
+ return;
+
+ for (const TimerRecord &record : result.previousTimerRecords) {
+ if (record.timerName.equalsIgnoreCase(previousTimer->timerName))
+ return;
+ }
+
+ result.previousTimerRecords.push_back(*previousTimer);
+}
+
+static bool belongsToContextRoom(const Common::String &contextRoom, const Common::String &roomName) {
+ return !contextRoom.empty() && roomName.equalsIgnoreCase(contextRoom);
+}
+
+bool Script::hasDeferredInteractionOutputs(const InteractionResult &result, CommandOutputPolicy outputPolicy) {
+ return (outputPolicy == kCommandOutputsInteraction &&
+ (!result.nextRoomName.empty() || !result.deathFlicPath.empty() || result.requestMainMenu)) ||
+ !result.cutscenePath.empty() || result.requestDemoEnding ||
+ !result.dialogueNpcName.empty() || !result.continuationTag.empty() ||
+ !result.modalText.value.empty() || result.lightingCommand != kStartupLightingCommandNone ||
+ !result.moveEntityToPlayerZName.empty() || result.requestCloseupExit;
+}
+
+Script::CommandStepResult Script::executeBranchCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result, Common::String &nextTag) {
+ if (command.opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
+ const FlagRecord *flag = findRuntimeFlag(command.arg1);
+ const bool flagValue = flag && flag->value;
+ debugC(3, kDebugScene, "Harvester: %s '%s' flag '%s' -> %d",
+ context.label, context.name.c_str(), command.arg1.c_str(), flagValue);
+ nextTag = flagValue ? command.arg2 : command.arg3;
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("CHECK_PERC")) {
+ const int threshold = CLIP<int>(parseAsciiIntOrZero(command.arg1), 0, 100);
+ const int roll = g_engine ? static_cast<int>(g_engine->getRandomNumber(99)) : 0;
+ nextTag = roll < threshold ? command.arg2 : command.arg3;
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("SET_FLAG")) {
+ const bool flagValue = isTruthy(command.arg2);
+ FlagRecord *flag = findRuntimeFlag(command.arg1);
+ bool changed = false;
+ bool oldValue = false;
+ const bool existed = flag != nullptr;
+ if (flag) {
+ oldValue = flag->value;
+ changed = flag->value != flagValue;
+ flag->value = flagValue;
+ } else {
+ FlagRecord newFlag;
+ newFlag.name = command.arg1;
+ newFlag.value = flagValue;
+ _currentFlags.push_back(Common::move(newFlag));
+ changed = true;
+ }
+ debugC(1, kDebugGeneral,
+ "Harvester: %s '%s' SET_FLAG '%s' %d -> %d existed=%d changed=%d",
+ context.label, context.name.c_str(), command.arg1.c_str(), oldValue, flagValue,
+ existed, changed);
+ noteMutation(result, changed);
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeAudioCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("SPOOL_MUSIC")) {
+ result.musicPath = command.arg1;
+ return kCommandContinue;
+ }
+
+ if (appendStartupAudioCommand(command, result.audioCommands)) {
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("CHANGE_CD")) {
+ const int requestedDisc = parseAsciiIntOrZero(command.arg1);
+ if (requestedDisc > 0)
+ result.cdChangeDisc = requestedDisc;
+ else
+ debug(1, "Harvester: invalid CHANGE_CD target '%s' for %s '%s'",
+ command.arg1.c_str(), context.label, context.name.c_str());
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeObjectCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("ADD") ||
+ command.opcodeName.equalsIgnoreCase("DELETE")) {
+ ObjectRecord *currentObject = findRuntimeObject(command.arg1, command.arg2);
+ if (!currentObject) {
+ debug(1, "Harvester: unresolved object for %s '%s' owner='%s' object='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str(), command.arg2.c_str());
+ return kCommandContinue;
+ }
+
+ const bool visible = command.opcodeName.equalsIgnoreCase("ADD");
+ const bool affectsCurrentRoom = belongsToContextRoom(context.roomName, currentObject->currentOwnerOrRoom);
+ const bool changed = currentObject->visible != visible ||
+ currentObject->runtimeVisible != visible;
+ currentObject->visible = visible;
+ currentObject->runtimeVisible = visible;
+ if (visible && currentObject->currentOwnerOrRoom.equalsIgnoreCase("INVENTORY"))
+ currentObject->identShown = true;
+ noteCurrentRoomVisualMutation(result, changed, affectsCurrentRoom);
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("ADD2INV")) {
+ const ObjectRecord *currentObject = findRuntimeObject(Common::String(), command.arg1);
+ const bool affectsCurrentRoom = currentObject &&
+ belongsToContextRoom(context.roomName, currentObject->currentOwnerOrRoom);
+ noteCurrentRoomVisualMutation(result,
+ addRuntimeObjectToInventory(command.arg1), affectsCurrentRoom);
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeAnimationCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("SET_ANIM")) {
+ AnimRecord *currentAnimation = findRuntimeAnim(command.arg1);
+ if (!currentAnimation) {
+ debug(1, "Harvester: unresolved anim for %s '%s' anim='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+
+ const bool wasVisible = currentAnimation->visible;
+ const bool active = isTruthy(command.arg2);
+ const bool visible = isTruthy(command.arg3);
+ int runtimeState = currentAnimation->runtimeState;
+ if (active && visible &&
+ (!wasVisible || !currentAnimation->runtimeActive || runtimeState < 0)) {
+ runtimeState = 0;
+ }
+ const bool changed = currentAnimation->active != active ||
+ currentAnimation->visible != visible ||
+ currentAnimation->runtimeActive != active ||
+ currentAnimation->runtimeVisible != visible ||
+ currentAnimation->runtimeState != runtimeState;
+ currentAnimation->active = active;
+ currentAnimation->visible = visible;
+ currentAnimation->runtimeActive = active;
+ currentAnimation->runtimeVisible = visible;
+ currentAnimation->runtimeState = runtimeState;
+ noteCurrentRoomVisualMutation(result, changed,
+ belongsToContextRoom(context.roomName, currentAnimation->roomName));
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeRegionCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("SET_REGION")) {
+ RegionRecord *currentRegion = findRuntimeRegion(command.arg1);
+ if (!currentRegion) {
+ debug(1, "Harvester: unresolved region for %s '%s' region='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+
+ const bool enabled = !command.arg2.equalsIgnoreCase("F");
+ const bool changed = currentRegion->startEnabled != enabled;
+ currentRegion->startEnabled = enabled;
+ noteCurrentRoomVisualMutation(result, changed,
+ belongsToContextRoom(context.roomName, currentRegion->roomName));
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeNpcCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("SET_NPC")) {
+ NpcRecord *currentNpc = findRuntimeNpc(command.arg1);
+ if (!currentNpc) {
+ debug(1, "Harvester: unresolved npc for %s '%s' npc='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+
+ const bool active = isTruthy(command.arg2);
+ const bool visible = isTruthy(command.arg3);
+ const bool changed = currentNpc->active != active || currentNpc->visible != visible;
+ currentNpc->active = active;
+ currentNpc->visible = visible;
+ noteCurrentRoomVisualMutation(result, changed,
+ belongsToContextRoom(context.roomName, currentNpc->roomName));
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("KILL_NPC") ||
+ command.opcodeName.equalsIgnoreCase("MONSTERFY")) {
+ NpcRecord *currentNpc = findRuntimeNpc(command.arg1);
+ if (!currentNpc) {
+ debug(1, "Harvester: unresolved npc for %s '%s' npc='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+
+ const int deathDamageType = parseDeathDamageType(command.arg2);
+ const bool deferLiveNpcDeathTransition =
+ _deferredLiveNpcDeathTransitionDepth > 0 &&
+ !context.roomName.empty() &&
+ currentNpc->roomName.equalsIgnoreCase(context.roomName) &&
+ currentNpc->active &&
+ currentNpc->visible &&
+ !currentNpc->deathOrMonsterfyFlag &&
+ currentNpc->runtimeState < 0;
+ noteCurrentRoomVisualMutation(result, deferLiveNpcDeathTransition
+ ? queueRuntimeNpcDeathOrMonsterfy(command.arg1, deathDamageType)
+ : finalizeRuntimeNpcDeathOrMonsterfy(command.arg1, deathDamageType),
+ belongsToContextRoom(context.roomName, currentNpc->roomName));
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeMonsterCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("SET_MONSTER")) {
+ MonsterRecord monsterLookup;
+ monsterLookup.roomName = context.roomName;
+ monsterLookup.monsterName = command.arg1;
+ MonsterRecord *currentMonster = findRuntimeMonster(monsterLookup);
+ if (!currentMonster) {
+ debug(1, "Harvester: unresolved monster for %s '%s' monster='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+
+ const bool active = isTruthy(command.arg2);
+ const bool visible = isTruthy(command.arg3);
+ const bool changed = currentMonster->active != active || currentMonster->visible != visible;
+ currentMonster->active = active;
+ currentMonster->visible = active ? true : visible;
+ if (active) {
+ currentMonster->runtimeSpawned = false;
+ currentMonster->runtimeState = -1;
+ }
+ if (!currentMonster->visible) {
+ currentMonster->runtimeSpawned = false;
+ currentMonster->runtimeState = -1;
+ }
+ if (active && currentMonster->currentHitPoints <= 0)
+ currentMonster->currentHitPoints = currentMonster->initialHitPoints;
+ noteCurrentRoomVisualMutation(result, changed,
+ belongsToContextRoom(context.roomName, currentMonster->roomName));
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeTimerCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("SET_TIMER")) {
+ const TimerRecord *currentTimer = findRuntimeTimer(command.arg1);
+ TimerRecord previousTimer;
+ if (currentTimer)
+ previousTimer = *currentTimer;
+ const bool changed =
+ setRuntimeTimerEnabled(command.arg1, command.arg2.equalsIgnoreCase("ON"));
+ noteCurrentRoomVisualMutation(result, changed,
+ currentTimer && belongsToContextRoom(context.roomName, currentTimer->arg1));
+ noteTimerMutation(result, currentTimer ? &previousTimer : nullptr, changed);
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("KILL_TIMER")) {
+ const TimerRecord *currentTimer = findRuntimeTimer(command.arg1);
+ TimerRecord previousTimer;
+ if (currentTimer)
+ previousTimer = *currentTimer;
+ const bool changed = setRuntimeTimerEnabled(command.arg1, false);
+ noteCurrentRoomVisualMutation(result, changed,
+ currentTimer && belongsToContextRoom(context.roomName, currentTimer->arg1));
+ noteTimerMutation(result, currentTimer ? &previousTimer : nullptr, changed);
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeExecListCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, CommandOutputPolicy outputPolicy, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("EXEC_LIST")) {
+ const ExecListRecord *execList = findExecListRecord(command.arg1);
+ if (!execList) {
+ debug(1, "Harvester: unresolved exec list for %s '%s' list='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+
+ for (const Common::String &entry : execList->entries) {
+ const CommandExecutionContext entryContext = {
+ "exec list entry", execList->listName, context.roomName, context.allowTransitions
+ };
+ executeCommandChain(entry, entryContext, outputPolicy, result);
+ if (hasDeferredInteractionOutputs(result, outputPolicy))
+ return kCommandStop;
+ }
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeModalCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("START_DIALOG")) {
+ if (command.arg1.empty()) {
+ return kCommandContinue;
+ }
+
+ result.dialogueNpcName = command.arg1;
+ result.dialogueContinuationTag = command.arg4;
+ return kCommandStop;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("GOFLIC")) {
+ result.cutscenePath = command.arg1;
+ result.continuationTag = command.arg4;
+ return kCommandStop;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("SHOW_TEXT")) {
+ if (!resolveTextRecord(command.arg1, result.modalText)) {
+ debug(1, "Harvester: unresolved SHOW_TEXT record for %s '%s' key='%s'",
+ context.label, context.name.c_str(), command.arg1.c_str());
+ return kCommandContinue;
+ }
+ result.continuationTag = command.arg4;
+ return kCommandStop;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("CHANGE_LIGHTING")) {
+ if (command.arg1.equalsIgnoreCase("DIM"))
+ result.lightingCommand = kStartupLightingCommandDim;
+ else if (command.arg1.equalsIgnoreCase("NORMAL"))
+ result.lightingCommand = kStartupLightingCommandNormal;
+ else if (command.arg1.equalsIgnoreCase("NONE"))
+ result.lightingCommand = kStartupLightingCommandBlack;
+ else if (command.arg1.equalsIgnoreCase("FADE_IN"))
+ result.lightingCommand = kStartupLightingCommandFadeIn;
+ else
+ debug(1, "Harvester: unsupported CHANGE_LIGHTING mode '%s' for %s '%s', preserving control flow",
+ command.arg1.c_str(), context.label, context.name.c_str());
+ result.continuationTag = command.arg4;
+ return kCommandStop;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executePlayerCommand(const CommandRecord &command,
+ InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("HEAL_PC") ||
+ command.opcodeName.equalsIgnoreCase("ADJ_HP")) {
+ noteMutation(result, setPlayerCurrentHitPoints(
+ _playerCurrentHitPoints + parseAsciiIntOrZero(command.arg1)));
+ return kCommandContinue;
+ }
+
+ const int combatResourceLoadout = resolveCombatResourceLoadoutForOpcode(command.opcodeName);
+ if (combatResourceLoadout != 0) {
+ noteMutation(result, adjustPlayerCombatResourceCount(combatResourceLoadout,
+ parseAsciiIntOrZero(command.arg1),
+ resolveCombatResourcePickupMax(combatResourceLoadout),
+ command.opcodeName));
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("PC_CHANGE_WEAPON")) {
+ noteCurrentRoomVisualMutation(result,
+ setPlayerCombatLoadout(parseAsciiIntOrZero(command.arg1)), true);
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("KILL_PC")) {
+ result.requestPlayerDeath = true;
+ result.playerDeathDamageType = parseDeathDamageType(command.arg1);
+ noteMutation(result, setPlayerCurrentHitPoints(0));
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("PAUSE_PC")) {
+ noteMutation(result, setPlayerControlPaused(true));
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("RESUME_PC")) {
+ noteMutation(result, setPlayerControlPaused(false));
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeMovementCommand(const CommandRecord &command,
+ InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("MOVE_BM2PCZ")) {
+ result.moveEntityToPlayerZName = command.arg1;
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("PC_GOTO_XZ")) {
+ result.requestPlayerGotoXZ = true;
+ result.playerGotoX = parseAsciiIntOrZero(command.arg1);
+ result.playerGotoZ = parseAsciiIntOrZero(command.arg2);
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("PC_GOTO_Z")) {
+ result.requestPlayerGotoZ = true;
+ result.playerGotoZ = parseAsciiIntOrZero(command.arg1);
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeTransitionCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, CommandOutputPolicy outputPolicy, InteractionResult &result) {
+ if (command.opcodeName.equalsIgnoreCase("GODEATHFLIC")) {
+ if (outputPolicy == kCommandOutputsRoomExit) {
+ debug(1, "Harvester: unsupported startup command '%s' for %s '%s' without menu-exit context",
+ command.opcodeName.c_str(), context.label, context.name.c_str());
+ return kCommandStop;
+ }
+
+ if (context.allowTransitions) {
+ result.deathFlicPath = command.arg1;
+ result.requestMainMenu = true;
+ } else {
+ debugC(1, kDebugScene, "Harvester: skipped transition opcode '%s' while processing %s '%s'",
+ command.opcodeName.c_str(), context.label, context.name.c_str());
+ }
+ return kCommandStop;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("END_DEMO")) {
+ result.requestDemoEnding = true;
+ return kCommandStop;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("CLOSEUP") ||
+ command.opcodeName.equalsIgnoreCase("CHANGE_ROOM")) {
+ if (context.allowTransitions && outputPolicy == kCommandOutputsInteraction) {
+ result.nextRoomName = command.arg1;
+ result.roomTransition = command.opcodeName.equalsIgnoreCase("CHANGE_ROOM")
+ ? kStartupRoomTransitionChangeRoom
+ : kStartupRoomTransitionCloseup;
+ } else {
+ debugC(1, kDebugScene, "Harvester: skipped transition opcode '%s' while processing %s '%s'",
+ command.opcodeName.c_str(), context.label, context.name.c_str());
+ }
+ return kCommandContinue;
+ }
+
+ if (command.opcodeName.equalsIgnoreCase("EXIT_CLOSEUP")) {
+ result.requestCloseupExit = true;
+ return kCommandContinue;
+ }
+
+ return kCommandUnhandled;
+}
+
+Script::CommandStepResult Script::executeCommand(const CommandRecord &command,
+ const CommandExecutionContext &context, CommandOutputPolicy outputPolicy,
+ InteractionResult &result, Common::String &nextTag) {
+ CommandStepResult step = executeBranchCommand(command, context, result, nextTag);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeAudioCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeObjectCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeAnimationCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeRegionCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeNpcCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeMonsterCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeTimerCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeExecListCommand(command, context, outputPolicy, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeModalCommand(command, context, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executePlayerCommand(command, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeMovementCommand(command, result);
+ if (step != kCommandUnhandled)
+ return step;
+ step = executeTransitionCommand(command, context, outputPolicy, result);
+ if (step != kCommandUnhandled)
+ return step;
+
+ debug(1, "Harvester: unsupported startup command '%s' for %s '%s', continuing",
+ command.opcodeName.c_str(), context.label, context.name.c_str());
+ return kCommandContinue;
+}
+
+void Script::executeCommandChain(const Common::String &initialTag, const CommandExecutionContext &context,
+ CommandOutputPolicy outputPolicy, InteractionResult &result) {
+ debugC(3, kDebugScene, "Harvester: command chain tag='%s' context='%s' source='%s' room='%s' transitions=%d outputs='%s'",
+ initialTag.c_str(), context.label, context.name.c_str(), context.roomName.c_str(), context.allowTransitions,
+ outputPolicy == kCommandOutputsRoomExit ? "room exit" : "interaction");
+
+ Common::String currentTag = initialTag;
+ for (uint step = 0; step < 128 && !currentTag.empty(); ++step) {
+ const CommandRecord *command = findCommandRecord(currentTag);
+ if (!command) {
+ debug(1, "Harvester: unresolved %s tag '%s' for '%s'",
+ context.label, currentTag.c_str(), context.name.c_str());
+ break;
+ }
+
+ debugC(3, kDebugScene,
+ "Harvester: %s '%s' step=%u tag='%s' opcode='%s' args=['%s','%s','%s','%s']",
+ context.label, context.name.c_str(), step, currentTag.c_str(), command->opcodeName.c_str(),
+ command->arg1.c_str(), command->arg2.c_str(), command->arg3.c_str(), command->arg4.c_str());
+
+ Common::String nextTag = command->arg4;
+ if (executeCommand(*command, context, outputPolicy, result, nextTag) == kCommandStop)
+ return;
+ currentTag = nextTag;
+ }
+}
+
+bool Script::probePickupBlockingCommandChain(const Common::String &initialTag,
+ const Common::String &contextName, InteractionResult &result, uint recursionDepth) const {
+ if (initialTag.empty() || recursionDepth > 8)
+ return false;
+
+ Common::String currentTag = initialTag;
+ for (uint step = 0; step < 128 && !currentTag.empty(); ++step) {
+ const CommandRecord *command = findCommandRecord(currentTag);
+ if (!command) {
+ debug(1, "Harvester: unresolved pickup preflight tag '%s' for '%s'",
+ currentTag.c_str(), contextName.c_str());
+ return false;
+ }
+
+ if (command->opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
+ const FlagRecord *flag = findRuntimeFlag(command->arg1);
+ currentTag = (flag && flag->value) ? command->arg2 : command->arg3;
+ continue;
+ }
+
+ if (command->opcodeName.equalsIgnoreCase("CHECK_PERC"))
+ return false;
+
+ if (command->opcodeName.equalsIgnoreCase("SHOW_TEXT")) {
+ if (!resolveTextRecord(command->arg1, result.modalText))
+ return false;
+ result.continuationTag = command->arg4;
+ return !result.modalText.value.empty();
+ }
+
+ if (command->opcodeName.equalsIgnoreCase("EXEC_LIST")) {
+ const ExecListRecord *execList = findExecListRecord(command->arg1);
+ if (!execList)
+ return false;
+
+ for (const Common::String &entry : execList->entries) {
+ if (probePickupBlockingCommandChain(entry, contextName, result, recursionDepth + 1))
+ return true;
+ }
+ return false;
+ }
+
+ Common::Array<AudioCommand> audioCommands;
+ if (appendStartupAudioCommand(*command, audioCommands))
+ return false;
+
+ if (command->opcodeName.equalsIgnoreCase("SET_FLAG") ||
+ command->opcodeName.equalsIgnoreCase("SPOOL_MUSIC") ||
+ command->opcodeName.equalsIgnoreCase("ADD") ||
+ command->opcodeName.equalsIgnoreCase("DELETE") ||
+ command->opcodeName.equalsIgnoreCase("ADD2INV") ||
+ isCombatResourceOpcode(command->opcodeName) ||
+ command->opcodeName.equalsIgnoreCase("SET_ANIM") ||
+ command->opcodeName.equalsIgnoreCase("SET_REGION") ||
+ command->opcodeName.equalsIgnoreCase("SET_NPC") ||
+ command->opcodeName.equalsIgnoreCase("SET_MONSTER") ||
+ command->opcodeName.equalsIgnoreCase("SET_TIMER") ||
+ command->opcodeName.equalsIgnoreCase("KILL_TIMER") ||
+ command->opcodeName.equalsIgnoreCase("START_DIALOG") ||
+ command->opcodeName.equalsIgnoreCase("GOFLIC") ||
+ command->opcodeName.equalsIgnoreCase("GODEATHFLIC") ||
+ command->opcodeName.equalsIgnoreCase("END_DEMO") ||
+ command->opcodeName.equalsIgnoreCase("KILL_NPC") ||
+ command->opcodeName.equalsIgnoreCase("MONSTERFY") ||
+ command->opcodeName.equalsIgnoreCase("HEAL_PC") ||
+ command->opcodeName.equalsIgnoreCase("ADJ_HP") ||
+ command->opcodeName.equalsIgnoreCase("KILL_PC") ||
+ command->opcodeName.equalsIgnoreCase("PAUSE_PC") ||
+ command->opcodeName.equalsIgnoreCase("RESUME_PC") ||
+ command->opcodeName.equalsIgnoreCase("CHANGE_CD") ||
+ command->opcodeName.equalsIgnoreCase("MOVE_BM2PCZ") ||
+ command->opcodeName.equalsIgnoreCase("PC_CHANGE_WEAPON") ||
+ command->opcodeName.equalsIgnoreCase("PC_GOTO_XZ") ||
+ command->opcodeName.equalsIgnoreCase("PC_GOTO_Z") ||
+ command->opcodeName.equalsIgnoreCase("CHANGE_LIGHTING") ||
+ command->opcodeName.equalsIgnoreCase("EXIT_CLOSEUP") ||
+ command->opcodeName.equalsIgnoreCase("CLOSEUP") ||
+ command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")) {
+ return false;
+ }
+
+ currentTag = command->arg4;
+ }
+
+ return false;
+}
+
+bool Script::hasActionableCommandChain(const Common::String &initialTag) const {
+ Common::String currentTag = initialTag;
+ for (uint step = 0; step < 128 && !currentTag.empty(); ++step) {
+ const CommandRecord *command = findCommandRecord(currentTag);
+ if (!command) {
+ debug(1, "Harvester: unresolved interaction probe tag '%s'", currentTag.c_str());
+ break;
+ }
+
+ if (command->opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
+ const FlagRecord *flag = findRuntimeFlag(command->arg1);
+ currentTag = (flag && flag->value) ? command->arg2 : command->arg3;
+ continue;
+ }
+
+ if (command->opcodeName.equalsIgnoreCase("CHECK_PERC")) {
+ currentTag = command->arg2.empty() ? command->arg3 : command->arg2;
+ continue;
+ }
+
+ if (command->opcodeName.equalsIgnoreCase("SET_FLAG") ||
+ command->opcodeName.equalsIgnoreCase("SPOOL_MUSIC") ||
+ command->opcodeName.equalsIgnoreCase("ADD") ||
+ command->opcodeName.equalsIgnoreCase("DELETE") ||
+ command->opcodeName.equalsIgnoreCase("ADD2INV") ||
+ isCombatResourceOpcode(command->opcodeName) ||
+ command->opcodeName.equalsIgnoreCase("SET_ANIM") ||
+ command->opcodeName.equalsIgnoreCase("SET_REGION") ||
+ command->opcodeName.equalsIgnoreCase("SET_NPC") ||
+ command->opcodeName.equalsIgnoreCase("SET_MONSTER") ||
+ command->opcodeName.equalsIgnoreCase("SET_TIMER") ||
+ command->opcodeName.equalsIgnoreCase("KILL_TIMER") ||
+ command->opcodeName.equalsIgnoreCase("EXEC_LIST") ||
+ command->opcodeName.equalsIgnoreCase("START_DIALOG") ||
+ command->opcodeName.equalsIgnoreCase("GOFLIC") ||
+ command->opcodeName.equalsIgnoreCase("GODEATHFLIC") ||
+ command->opcodeName.equalsIgnoreCase("END_DEMO") ||
+ command->opcodeName.equalsIgnoreCase("KILL_NPC") ||
+ command->opcodeName.equalsIgnoreCase("MONSTERFY") ||
+ command->opcodeName.equalsIgnoreCase("SHOW_TEXT") ||
+ command->opcodeName.equalsIgnoreCase("HEAL_PC") ||
+ command->opcodeName.equalsIgnoreCase("ADJ_HP") ||
+ command->opcodeName.equalsIgnoreCase("KILL_PC") ||
+ command->opcodeName.equalsIgnoreCase("PAUSE_PC") ||
+ command->opcodeName.equalsIgnoreCase("RESUME_PC") ||
+ command->opcodeName.equalsIgnoreCase("CHANGE_CD") ||
+ command->opcodeName.equalsIgnoreCase("MOVE_BM2PCZ") ||
+ command->opcodeName.equalsIgnoreCase("PC_CHANGE_WEAPON") ||
+ command->opcodeName.equalsIgnoreCase("PC_GOTO_XZ") ||
+ command->opcodeName.equalsIgnoreCase("PC_GOTO_Z") ||
+ command->opcodeName.equalsIgnoreCase("CHANGE_LIGHTING") ||
+ command->opcodeName.equalsIgnoreCase("EXIT_CLOSEUP") ||
+ command->opcodeName.equalsIgnoreCase("CLOSEUP") ||
+ command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")) {
+ return true;
+ }
+
+ Common::Array<AudioCommand> audioCommands;
+ if (appendStartupAudioCommand(*command, audioCommands))
+ return true;
+
+ currentTag = command->arg4;
+ }
+
+ return false;
+}
+
+} // End of namespace Harvester
Commit: 4dd258fce8bf82042a70c17da9e0e36ff0100a52
https://github.com/scummvm/scummvm/commit/4dd258fce8bf82042a70c17da9e0e36ff0100a52
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Separate room state materialization
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/script_room.cpp
engines/harvester/module.mk
engines/harvester/script.cpp
engines/harvester/script.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index a7f91293f6d..39b47e54559 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -28,6 +28,7 @@ MODULE_OBJS = \
script.o \
script_commands.o \
script_records.o \
+ script_room.o \
text.o \
xfile_archive.o \
npc/authority_dialogue.o \
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index e70d2bc8d7c..84d0be50f2f 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -24,7 +24,6 @@
#include "common/algorithm.h"
#include "common/config-manager.h"
#include "common/debug.h"
-#include "common/endian.h"
#include "common/file.h"
#include "common/formats/ini-file.h"
#include "common/memstream.h"
@@ -41,8 +40,6 @@ static const char *const kConfigSectionName = "harvester";
static const char *const kDefaultTownScript = "HARVEST.SCR";
static const char *const kInventoryOwnerName = "INVENTORY";
static const char *const kDefaultVoicePath = "SOUND/VOICE/";
-static const float kDefaultPaletteBrightness = 1.0f;
-static const float kDimmedPaletteBrightness = 0.6f;
static const int kDefaultPlayerCombatLoadout = 0;
static const int kMaxPlayerCombatLoadout = 0x14;
static const int kMaxStartupOptionLevel = 9;
@@ -402,41 +399,6 @@ static Common::String dialogueTextModeToConfigValue(StartupDialogueTextMode mode
}
}
-static bool loadBitmapDimensions(ResourceManager &resources, const Common::String &path,
- uint32 &width, uint32 &height) {
- Common::Array<byte> data;
- if (!resources.loadFile(path, data) || data.size() < 8)
- return false;
-
- width = READ_LE_UINT32(data.data());
- height = READ_LE_UINT32(data.data() + 4);
- return true;
-}
-
-static bool isRoomBackgroundSpriteCandidate(const ObjectRecord &candidate,
- const RoomRecord &room) {
- if (!candidate.currentOwnerOrRoom.equalsIgnoreCase(room.roomName) ||
- candidate.spritePath.empty() ||
- candidate.initialX != 0 || candidate.initialY != 0) {
- return false;
- }
-
- return true;
-}
-
-static bool isFullscreenRoomBackgroundObject(const ObjectRecord &candidate,
- ResourceManager &resources) {
- if (candidate.spritePath.empty())
- return false;
-
- uint32 width = 0;
- uint32 height = 0;
- if (!loadBitmapDimensions(resources, candidate.spritePath, width, height))
- return false;
-
- return width == 640 && height == 480;
-}
-
bool Script::load(ResourceManager &resources) {
_path = kDefaultTownScript;
_data.clear();
@@ -767,70 +729,6 @@ void Script::setGammaLevel(int level) {
_gammaLevel = clampStartupOptionLevel(level);
}
-bool Script::resolveRoomSetupState(const Common::String &entranceName, RoomSetupState &state,
- ResourceManager &resources) {
- state = RoomSetupState();
-
- const EntranceRecord *entrance = findEntranceRecord(entranceName);
- const Common::String resolvedRoomName = entrance ? entrance->roomName : entranceName;
- const RoomRecord *room = findRoomRecord(resolvedRoomName);
- if (!room) {
- warning("Harvester: unresolved room setup target '%s' (resolved room '%s')",
- entranceName.c_str(), resolvedRoomName.c_str());
- return false;
- }
-
- if (!buildRuntimeRoomState(*room, entrance, resources, state))
- return false;
-
- debugC(3, kDebugRoom,
- "Harvester: resolveRoomSetupState('%s') -> room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d palette='%s' background='%s' music='%s' brightness=%.2f roomObjects=%u activeObjects=%u roomAnims=%u roomNpcs=%u roomMonsters=%u roomTimers=%u roomRegions=%u",
- entranceName.c_str(), state.roomName.c_str(), state.entranceName.c_str(),
- state.playerSpawnX, state.playerSpawnY, state.playerSpawnZ, state.playerFacing,
- state.palettePath.c_str(), state.backgroundPath.c_str(), state.musicPath.c_str(),
- (double)state.paletteBrightness, (uint)state.roomObjects.size(),
- (uint)state.activeObjects.size(), (uint)state.roomAnimations.size(), (uint)state.roomNpcs.size(),
- (uint)state.roomMonsters.size(), (uint)state.roomTimers.size(),
- (uint)state.roomRegions.size());
-
- return true;
-}
-
-bool Script::resolveRoomSetupStateByRoomName(const Common::String &roomName, RoomSetupState &state,
- ResourceManager &resources) {
- state = RoomSetupState();
-
- const RoomRecord *room = findRoomRecord(roomName);
- if (!room) {
- warning("Harvester: unresolved room setup room '%s'", roomName.c_str());
- return false;
- }
-
- const EntranceRecord *entrance = nullptr;
- for (const EntranceRecord &candidate : _entrances) {
- if (!candidate.roomName.equalsIgnoreCase(room->roomName))
- continue;
-
- entrance = &candidate;
- break;
- }
-
- if (!buildRuntimeRoomState(*room, entrance, resources, state))
- return false;
-
- debugC(3, kDebugRoom,
- "Harvester: resolveRoomSetupStateByRoomName('%s') -> room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d palette='%s' background='%s' music='%s' brightness=%.2f roomObjects=%u activeObjects=%u roomAnims=%u roomNpcs=%u roomMonsters=%u roomTimers=%u roomRegions=%u",
- roomName.c_str(), state.roomName.c_str(), state.entranceName.c_str(),
- state.playerSpawnX, state.playerSpawnY, state.playerSpawnZ, state.playerFacing,
- state.palettePath.c_str(), state.backgroundPath.c_str(), state.musicPath.c_str(),
- (double)state.paletteBrightness, (uint)state.roomObjects.size(),
- (uint)state.activeObjects.size(), (uint)state.roomAnimations.size(), (uint)state.roomNpcs.size(),
- (uint)state.roomMonsters.size(), (uint)state.roomTimers.size(),
- (uint)state.roomRegions.size());
-
- return true;
-}
-
void Script::resetRuntimeState() {
_currentFlags = _flags;
_currentObjects = _objects;
@@ -1070,39 +968,6 @@ void Script::syncRuntimeSaveState(Common::Serializer &s) {
}
}
-bool Script::materializeRoomState(const Common::String &entranceName, const Common::String &roomName,
- RoomSetupState &state, ResourceManager &resources) const {
- state = RoomSetupState();
-
- const EntranceRecord *entrance = findEntranceRecord(entranceName);
- const Common::String resolvedRoomName = !roomName.empty()
- ? roomName
- : (entrance ? entrance->roomName : Common::String());
- if (resolvedRoomName.empty())
- return false;
-
- const RoomRecord *room = findRoomRecord(resolvedRoomName);
- if (!room) {
- warning("Harvester: unresolved materialized room target entrance='%s' room='%s'",
- entranceName.c_str(), resolvedRoomName.c_str());
- return false;
- }
-
- return buildRuntimeRoomState(*room, entrance, resources, state);
-}
-
-bool Script::hasRoomSetupTarget(const Common::String &entranceName,
- const Common::String &roomName) const {
- const EntranceRecord *entrance = findEntranceRecord(entranceName);
- const Common::String resolvedRoomName = !roomName.empty()
- ? roomName
- : (entrance ? entrance->roomName : Common::String());
- if (resolvedRoomName.empty())
- return false;
-
- return findRoomRecord(resolvedRoomName) != nullptr;
-}
-
bool Script::executeRoomEnterCommands(const Common::String &roomName,
InteractionResult &result) {
result = InteractionResult();
@@ -1980,159 +1845,6 @@ void Script::popDeferredLiveNpcDeathTransitions() {
--_deferredLiveNpcDeathTransitionDepth;
}
-bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord *entrance,
- ResourceManager &resources, RoomSetupState &state) const {
- state = RoomSetupState();
-
- const ObjectRecord *background = nullptr;
- const ObjectRecord *backgroundFallback = nullptr;
- bool usedBackgroundFallback = false;
- for (const ObjectRecord &candidate : _currentObjects) {
- if (!isRoomBackgroundSpriteCandidate(candidate, room))
- continue;
-
- if (!backgroundFallback)
- backgroundFallback = &candidate;
- if (isFullscreenRoomBackgroundObject(candidate, resources)) {
- background = &candidate;
- break;
- }
- }
- if (!background && backgroundFallback) {
- background = backgroundFallback;
- usedBackgroundFallback = true;
- }
- if (!background) {
- warning("Harvester: no room background object found for room '%s'", room.roomName.c_str());
- return false;
- }
- if (usedBackgroundFallback) {
- debugC(1, kDebugRoom,
- "Harvester: using origin room sprite as background room='%s' object='%s' sprite='%s'",
- room.roomName.c_str(), background->objectName.c_str(), background->spritePath.c_str());
- }
-
- state.roomName = room.roomName;
- state.backgroundObjectName = background->objectName;
- state.palettePath = room.palettePath;
- state.backgroundPath = background->spritePath;
- state.discNumber = resources.getCurrentDisc();
- state.roomMinZ = room.minZ;
- state.roomMaxZ = room.maxZ;
- state.roomMaxZScreenY = room.maxZScreenY;
- state.roomMinZScreenY = room.minZScreenY;
- state.roomFullScaleZ = room.fullScaleZ;
- state.roomMaxZScalePercent = room.maxZScalePercent;
- state.roomPerspectiveScale = room.perspectiveScale;
- state.roomZVelocityStep = room.zVelocityStep;
- state.musicPath = room.musicPath;
- if (entrance && entrance->roomName.equalsIgnoreCase(room.roomName)) {
- state.entranceName = entrance->entranceName;
- state.hasEntrance = true;
- state.playerSpawnX = entrance->posX;
- state.playerSpawnY = entrance->posY;
- state.playerSpawnZ = entrance->posZ;
- state.playerFacing = entrance->facing;
- }
- for (const ObjectRecord &object : _currentObjects) {
- if (object.visible && object.currentOwnerOrRoom.equalsIgnoreCase(room.roomName))
- state.roomObjects.push_back(object);
- }
- if (!state.hasEntrance) {
- static const char *const kCloseupExitObjects[] = { "EXIT_BM", "EXIT_HS" };
- for (const char *name : kCloseupExitObjects) {
- for (const ObjectRecord &object : _currentObjects) {
- if (object.objectName.equalsIgnoreCase(name)) {
- state.roomObjects.push_back(object);
- break;
- }
- }
- }
- }
- for (const AnimRecord &anim : _currentAnimations) {
- if (anim.roomName.equalsIgnoreCase(room.roomName))
- state.roomAnimations.push_back(anim);
- }
- for (const NpcRecord &npc : _currentNpcs) {
- if (!npc.roomName.equalsIgnoreCase(room.roomName) || !npc.visible)
- continue;
- if (npc.deathOrMonsterfyFlag) {
- debugC(1, kDebugRoom,
- "Harvester: suppressed room npc room='%s' npc='%s' death_or_monsterfy=%d damage_type=%d model='%s' monsterfy_target='%s'",
- room.roomName.c_str(), npc.npcName.c_str(), npc.deathOrMonsterfyFlag, npc.deathDamageType,
- npc.modelPath.c_str(), npc.monsterfyTargetName.c_str());
- continue;
- }
-
- state.roomNpcs.push_back(npc);
- }
- for (const MonsterRecord &monster : _currentMonsters) {
- if (!monster.roomName.equalsIgnoreCase(room.roomName))
- continue;
- if (!monster.active && !monster.visible)
- continue;
-
- state.roomMonsters.push_back(monster);
- }
- for (const TimerRecord &timer : _currentTimers) {
- if (timer.arg1.equalsIgnoreCase(room.roomName))
- state.roomTimers.push_back(timer);
- }
- for (const RegionRecord ®ion : _currentRegions) {
- if (region.roomName.equalsIgnoreCase(room.roomName) && region.startEnabled)
- state.roomRegions.push_back(region);
- }
-
- const FlagRecord *dayFlag = findRuntimeFlag("DAY_FLAG");
- state.paletteBrightness = (room.dimmable && (!dayFlag || !dayFlag->value))
- ? kDimmedPaletteBrightness
- : kDefaultPaletteBrightness;
-
- for (const ObjectRecord &object : state.roomObjects) {
- debugC(3, kDebugRoom,
- "Harvester: materialized room object room='%s' object='%s' owner='%s' visible=%d runtimeVisible=%d sprite='%s' alt='%s' pos=(%d,%d,%d) bounds=(%d,%d)-(%d,%d) action='%s'",
- state.roomName.c_str(), object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
- object.visible, object.runtimeVisible, object.spritePath.c_str(), object.altSpritePath.c_str(),
- object.currentX, object.currentY, object.currentZ,
- object.currentX, object.currentY, object.boundsX2, object.boundsY2, object.actionTag.c_str());
- }
- for (const NpcRecord &npc : state.roomNpcs) {
- debugC(3, kDebugRoom,
- "Harvester: materialized room npc room='%s' npc='%s' visible=%d active=%d spawned=%d runtimeState=%d damage_type=%d pos=(%d,%d,%d) frame_delay=%d model='%s' on_death='%s' audio='%s'",
- state.roomName.c_str(), npc.npcName.c_str(), npc.visible, npc.active,
- npc.runtimeSpawned, npc.runtimeState, npc.deathDamageType,
- npc.posX, npc.posY, npc.posZ, npc.frameDelay,
- npc.modelPath.c_str(), npc.onDeathActionTag.c_str(), npc.audioPath.c_str());
- }
- for (const MonsterRecord &monster : state.roomMonsters) {
- debugC(3, kDebugRoom,
- "Harvester: materialized room monster room='%s' monster='%s' visible=%d active=%d spawned=%d runtimeState=%d pos=(%d,%d,%d) facing=%d hp=%d/%d damage=%d engage=%d damage_type='%s' model='%s' on_death='%s'",
- state.roomName.c_str(), monster.monsterName.c_str(), monster.visible, monster.active,
- monster.runtimeSpawned, monster.runtimeState,
- monster.posX, monster.posY, monster.posZ, monster.facing,
- monster.currentHitPoints, monster.initialHitPoints,
- monster.damageAmount, monster.engageDistance, Player::describeCombatDamageType(monster.damageType),
- monster.modelPath.c_str(), monster.onDeathActionTag.c_str());
- }
- for (const TimerRecord &timer : state.roomTimers) {
- debugC(3, kDebugRoom,
- "Harvester: materialized room timer room='%s' timer='%s' current=%d initial=%d enabled=%d loop=%d global=%d",
- state.roomName.c_str(), timer.timerName.c_str(), timer.currentValue, timer.initialValue,
- timer.enabled, timer.looping, timer.global);
- }
-
- debugC(3, kDebugRoom,
- "Harvester: materializeRoomState room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d palette='%s' background='%s' music='%s' brightness=%.2f roomObjects=%u roomAnims=%u roomNpcs=%u roomMonsters=%u roomTimers=%u roomRegions=%u",
- state.roomName.c_str(), state.entranceName.c_str(), state.playerSpawnX, state.playerSpawnY,
- state.playerSpawnZ, state.playerFacing, state.palettePath.c_str(), state.backgroundPath.c_str(),
- state.musicPath.c_str(), (double)state.paletteBrightness,
- (uint)state.roomObjects.size(), (uint)state.roomAnimations.size(),
- (uint)state.roomNpcs.size(), (uint)state.roomMonsters.size(),
- (uint)state.roomTimers.size(), (uint)state.roomRegions.size());
-
- return true;
-}
-
const TextRecord *Script::findTextRecord(const Common::String &key) const {
if (key.empty())
return nullptr;
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index 4e654f2df1c..565ca7b7870 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -525,6 +525,8 @@ private:
const MonsterRecord *findBaseMonster(const MonsterRecord &monster) const;
TimerRecord *findRuntimeTimer(const Common::String &timerName);
const TimerRecord *findRuntimeTimer(const Common::String &timerName) const;
+ const ObjectRecord *findRoomBackground(const RoomRecord &room, ResourceManager &resources) const;
+ void collectRuntimeRoomRecords(const RoomRecord &room, RoomSetupState &state) const;
bool buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord *entrance,
ResourceManager &resources, RoomSetupState &state) const;
void executeCommandChain(const Common::String &initialTag, const CommandExecutionContext &context,
diff --git a/engines/harvester/script_room.cpp b/engines/harvester/script_room.cpp
new file mode 100644
index 00000000000..de311cdffa3
--- /dev/null
+++ b/engines/harvester/script_room.cpp
@@ -0,0 +1,341 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/script.h"
+
+#include "common/debug.h"
+#include "common/endian.h"
+#include "harvester/detection.h"
+#include "harvester/player.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+static const float kDefaultPaletteBrightness = 1.0f;
+static const float kDimmedPaletteBrightness = 0.6f;
+
+static bool loadBitmapDimensions(ResourceManager &resources, const Common::String &path,
+ uint32 &width, uint32 &height) {
+ Common::Array<byte> data;
+ if (!resources.loadFile(path, data) || data.size() < 8)
+ return false;
+
+ width = READ_LE_UINT32(data.data());
+ height = READ_LE_UINT32(data.data() + 4);
+ return true;
+}
+
+static bool isRoomBackgroundSpriteCandidate(const ObjectRecord &candidate,
+ const RoomRecord &room) {
+ if (!candidate.currentOwnerOrRoom.equalsIgnoreCase(room.roomName) ||
+ candidate.spritePath.empty() ||
+ candidate.initialX != 0 || candidate.initialY != 0) {
+ return false;
+ }
+
+ return true;
+}
+
+static bool isFullscreenRoomBackgroundObject(const ObjectRecord &candidate,
+ ResourceManager &resources) {
+ if (candidate.spritePath.empty())
+ return false;
+
+ uint32 width = 0;
+ uint32 height = 0;
+ if (!loadBitmapDimensions(resources, candidate.spritePath, width, height))
+ return false;
+
+ return width == 640 && height == 480;
+}
+
+static void initializeRoomSetupState(const RoomRecord &room, const EntranceRecord *entrance,
+ const ObjectRecord &background, ResourceManager &resources, RoomSetupState &state) {
+ state.roomName = room.roomName;
+ state.backgroundObjectName = background.objectName;
+ state.palettePath = room.palettePath;
+ state.backgroundPath = background.spritePath;
+ state.discNumber = resources.getCurrentDisc();
+ state.roomMinZ = room.minZ;
+ state.roomMaxZ = room.maxZ;
+ state.roomMaxZScreenY = room.maxZScreenY;
+ state.roomMinZScreenY = room.minZScreenY;
+ state.roomFullScaleZ = room.fullScaleZ;
+ state.roomMaxZScalePercent = room.maxZScalePercent;
+ state.roomPerspectiveScale = room.perspectiveScale;
+ state.roomZVelocityStep = room.zVelocityStep;
+ state.musicPath = room.musicPath;
+ if (entrance && entrance->roomName.equalsIgnoreCase(room.roomName)) {
+ state.entranceName = entrance->entranceName;
+ state.hasEntrance = true;
+ state.playerSpawnX = entrance->posX;
+ state.playerSpawnY = entrance->posY;
+ state.playerSpawnZ = entrance->posZ;
+ state.playerFacing = entrance->facing;
+ }
+}
+
+static void logRuntimeRoomState(const RoomSetupState &state) {
+ for (const ObjectRecord &object : state.roomObjects) {
+ debugC(3, kDebugRoom,
+ "Harvester: materialized room object room='%s' object='%s' owner='%s' visible=%d runtimeVisible=%d sprite='%s' alt='%s' pos=(%d,%d,%d) bounds=(%d,%d)-(%d,%d) action='%s'",
+ state.roomName.c_str(), object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
+ object.visible, object.runtimeVisible, object.spritePath.c_str(), object.altSpritePath.c_str(),
+ object.currentX, object.currentY, object.currentZ,
+ object.currentX, object.currentY, object.boundsX2, object.boundsY2, object.actionTag.c_str());
+ }
+ for (const NpcRecord &npc : state.roomNpcs) {
+ debugC(3, kDebugRoom,
+ "Harvester: materialized room npc room='%s' npc='%s' visible=%d active=%d spawned=%d runtimeState=%d damage_type=%d pos=(%d,%d,%d) frame_delay=%d model='%s' on_death='%s' audio='%s'",
+ state.roomName.c_str(), npc.npcName.c_str(), npc.visible, npc.active,
+ npc.runtimeSpawned, npc.runtimeState, npc.deathDamageType,
+ npc.posX, npc.posY, npc.posZ, npc.frameDelay,
+ npc.modelPath.c_str(), npc.onDeathActionTag.c_str(), npc.audioPath.c_str());
+ }
+ for (const MonsterRecord &monster : state.roomMonsters) {
+ debugC(3, kDebugRoom,
+ "Harvester: materialized room monster room='%s' monster='%s' visible=%d active=%d spawned=%d runtimeState=%d pos=(%d,%d,%d) facing=%d hp=%d/%d damage=%d engage=%d damage_type='%s' model='%s' on_death='%s'",
+ state.roomName.c_str(), monster.monsterName.c_str(), monster.visible, monster.active,
+ monster.runtimeSpawned, monster.runtimeState,
+ monster.posX, monster.posY, monster.posZ, monster.facing,
+ monster.currentHitPoints, monster.initialHitPoints,
+ monster.damageAmount, monster.engageDistance, Player::describeCombatDamageType(monster.damageType),
+ monster.modelPath.c_str(), monster.onDeathActionTag.c_str());
+ }
+ for (const TimerRecord &timer : state.roomTimers) {
+ debugC(3, kDebugRoom,
+ "Harvester: materialized room timer room='%s' timer='%s' current=%d initial=%d enabled=%d loop=%d global=%d",
+ state.roomName.c_str(), timer.timerName.c_str(), timer.currentValue, timer.initialValue,
+ timer.enabled, timer.looping, timer.global);
+ }
+
+ debugC(3, kDebugRoom,
+ "Harvester: materializeRoomState room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d palette='%s' background='%s' music='%s' brightness=%.2f roomObjects=%u roomAnims=%u roomNpcs=%u roomMonsters=%u roomTimers=%u roomRegions=%u",
+ state.roomName.c_str(), state.entranceName.c_str(), state.playerSpawnX, state.playerSpawnY,
+ state.playerSpawnZ, state.playerFacing, state.palettePath.c_str(), state.backgroundPath.c_str(),
+ state.musicPath.c_str(), static_cast<double>(state.paletteBrightness),
+ (uint)state.roomObjects.size(), (uint)state.roomAnimations.size(),
+ (uint)state.roomNpcs.size(), (uint)state.roomMonsters.size(),
+ (uint)state.roomTimers.size(), (uint)state.roomRegions.size());
+}
+
+bool Script::resolveRoomSetupState(const Common::String &entranceName, RoomSetupState &state,
+ ResourceManager &resources) {
+ state = RoomSetupState();
+
+ const EntranceRecord *entrance = findEntranceRecord(entranceName);
+ const Common::String resolvedRoomName = entrance ? entrance->roomName : entranceName;
+ const RoomRecord *room = findRoomRecord(resolvedRoomName);
+ if (!room) {
+ warning("Harvester: unresolved room setup target '%s' (resolved room '%s')",
+ entranceName.c_str(), resolvedRoomName.c_str());
+ return false;
+ }
+
+ if (!buildRuntimeRoomState(*room, entrance, resources, state))
+ return false;
+
+ debugC(3, kDebugRoom,
+ "Harvester: resolveRoomSetupState('%s') -> room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d palette='%s' background='%s' music='%s' brightness=%.2f roomObjects=%u activeObjects=%u roomAnims=%u roomNpcs=%u roomMonsters=%u roomTimers=%u roomRegions=%u",
+ entranceName.c_str(), state.roomName.c_str(), state.entranceName.c_str(),
+ state.playerSpawnX, state.playerSpawnY, state.playerSpawnZ, state.playerFacing,
+ state.palettePath.c_str(), state.backgroundPath.c_str(), state.musicPath.c_str(),
+ static_cast<double>(state.paletteBrightness), (uint)state.roomObjects.size(),
+ (uint)state.activeObjects.size(), (uint)state.roomAnimations.size(), (uint)state.roomNpcs.size(),
+ (uint)state.roomMonsters.size(), (uint)state.roomTimers.size(),
+ (uint)state.roomRegions.size());
+
+ return true;
+}
+
+bool Script::resolveRoomSetupStateByRoomName(const Common::String &roomName, RoomSetupState &state,
+ ResourceManager &resources) {
+ state = RoomSetupState();
+
+ const RoomRecord *room = findRoomRecord(roomName);
+ if (!room) {
+ warning("Harvester: unresolved room setup room '%s'", roomName.c_str());
+ return false;
+ }
+
+ const EntranceRecord *entrance = nullptr;
+ for (const EntranceRecord &candidate : _entrances) {
+ if (!candidate.roomName.equalsIgnoreCase(room->roomName))
+ continue;
+
+ entrance = &candidate;
+ break;
+ }
+
+ if (!buildRuntimeRoomState(*room, entrance, resources, state))
+ return false;
+
+ debugC(3, kDebugRoom,
+ "Harvester: resolveRoomSetupStateByRoomName('%s') -> room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d palette='%s' background='%s' music='%s' brightness=%.2f roomObjects=%u activeObjects=%u roomAnims=%u roomNpcs=%u roomMonsters=%u roomTimers=%u roomRegions=%u",
+ roomName.c_str(), state.roomName.c_str(), state.entranceName.c_str(),
+ state.playerSpawnX, state.playerSpawnY, state.playerSpawnZ, state.playerFacing,
+ state.palettePath.c_str(), state.backgroundPath.c_str(), state.musicPath.c_str(),
+ static_cast<double>(state.paletteBrightness), (uint)state.roomObjects.size(),
+ (uint)state.activeObjects.size(), (uint)state.roomAnimations.size(), (uint)state.roomNpcs.size(),
+ (uint)state.roomMonsters.size(), (uint)state.roomTimers.size(),
+ (uint)state.roomRegions.size());
+
+ return true;
+}
+
+bool Script::materializeRoomState(const Common::String &entranceName, const Common::String &roomName,
+ RoomSetupState &state, ResourceManager &resources) const {
+ state = RoomSetupState();
+
+ const EntranceRecord *entrance = findEntranceRecord(entranceName);
+ const Common::String resolvedRoomName = !roomName.empty()
+ ? roomName
+ : (entrance ? entrance->roomName : Common::String());
+ if (resolvedRoomName.empty())
+ return false;
+
+ const RoomRecord *room = findRoomRecord(resolvedRoomName);
+ if (!room) {
+ warning("Harvester: unresolved materialized room target entrance='%s' room='%s'",
+ entranceName.c_str(), resolvedRoomName.c_str());
+ return false;
+ }
+
+ return buildRuntimeRoomState(*room, entrance, resources, state);
+}
+
+bool Script::hasRoomSetupTarget(const Common::String &entranceName,
+ const Common::String &roomName) const {
+ const EntranceRecord *entrance = findEntranceRecord(entranceName);
+ const Common::String resolvedRoomName = !roomName.empty()
+ ? roomName
+ : (entrance ? entrance->roomName : Common::String());
+ if (resolvedRoomName.empty())
+ return false;
+
+ return findRoomRecord(resolvedRoomName) != nullptr;
+}
+
+const ObjectRecord *Script::findRoomBackground(const RoomRecord &room,
+ ResourceManager &resources) const {
+ const ObjectRecord *background = nullptr;
+ const ObjectRecord *backgroundFallback = nullptr;
+ bool usedBackgroundFallback = false;
+ for (const ObjectRecord &candidate : _currentObjects) {
+ if (!isRoomBackgroundSpriteCandidate(candidate, room))
+ continue;
+
+ if (!backgroundFallback)
+ backgroundFallback = &candidate;
+ if (isFullscreenRoomBackgroundObject(candidate, resources)) {
+ background = &candidate;
+ break;
+ }
+ }
+ if (!background && backgroundFallback) {
+ background = backgroundFallback;
+ usedBackgroundFallback = true;
+ }
+ if (!background) {
+ warning("Harvester: no room background object found for room '%s'", room.roomName.c_str());
+ return nullptr;
+ }
+ if (usedBackgroundFallback) {
+ debugC(1, kDebugRoom,
+ "Harvester: using origin room sprite as background room='%s' object='%s' sprite='%s'",
+ room.roomName.c_str(), background->objectName.c_str(), background->spritePath.c_str());
+ }
+
+ return background;
+}
+
+void Script::collectRuntimeRoomRecords(const RoomRecord &room, RoomSetupState &state) const {
+ for (const ObjectRecord &object : _currentObjects) {
+ if (object.visible && object.currentOwnerOrRoom.equalsIgnoreCase(room.roomName))
+ state.roomObjects.push_back(object);
+ }
+ if (!state.hasEntrance) {
+ static const char *const kCloseupExitObjects[] = { "EXIT_BM", "EXIT_HS" };
+ for (const char *name : kCloseupExitObjects) {
+ for (const ObjectRecord &object : _currentObjects) {
+ if (object.objectName.equalsIgnoreCase(name)) {
+ state.roomObjects.push_back(object);
+ break;
+ }
+ }
+ }
+ }
+ for (const AnimRecord &anim : _currentAnimations) {
+ if (anim.roomName.equalsIgnoreCase(room.roomName))
+ state.roomAnimations.push_back(anim);
+ }
+ for (const NpcRecord &npc : _currentNpcs) {
+ if (!npc.roomName.equalsIgnoreCase(room.roomName) || !npc.visible)
+ continue;
+ if (npc.deathOrMonsterfyFlag) {
+ debugC(1, kDebugRoom,
+ "Harvester: suppressed room npc room='%s' npc='%s' death_or_monsterfy=%d damage_type=%d model='%s' monsterfy_target='%s'",
+ room.roomName.c_str(), npc.npcName.c_str(), npc.deathOrMonsterfyFlag, npc.deathDamageType,
+ npc.modelPath.c_str(), npc.monsterfyTargetName.c_str());
+ continue;
+ }
+
+ state.roomNpcs.push_back(npc);
+ }
+ for (const MonsterRecord &monster : _currentMonsters) {
+ if (!monster.roomName.equalsIgnoreCase(room.roomName))
+ continue;
+ if (!monster.active && !monster.visible)
+ continue;
+
+ state.roomMonsters.push_back(monster);
+ }
+ for (const TimerRecord &timer : _currentTimers) {
+ if (timer.arg1.equalsIgnoreCase(room.roomName))
+ state.roomTimers.push_back(timer);
+ }
+ for (const RegionRecord ®ion : _currentRegions) {
+ if (region.roomName.equalsIgnoreCase(room.roomName) && region.startEnabled)
+ state.roomRegions.push_back(region);
+ }
+}
+
+bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord *entrance,
+ ResourceManager &resources, RoomSetupState &state) const {
+ state = RoomSetupState();
+
+ const ObjectRecord *background = findRoomBackground(room, resources);
+ if (!background)
+ return false;
+
+ initializeRoomSetupState(room, entrance, *background, resources, state);
+ collectRuntimeRoomRecords(room, state);
+
+ const FlagRecord *dayFlag = findRuntimeFlag("DAY_FLAG");
+ state.paletteBrightness = (room.dimmable && (!dayFlag || !dayFlag->value))
+ ? kDimmedPaletteBrightness
+ : kDefaultPaletteBrightness;
+
+ logRuntimeRoomState(state);
+ return true;
+}
+
+} // End of namespace Harvester
Commit: 3b56649b1a50d88c72c3f90e93c115f2a358edac
https://github.com/scummvm/scummvm/commit/3b56649b1a50d88c72c3f90e93c115f2a358edac
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Separate script save and reload handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/script_save.cpp
engines/harvester/module.mk
engines/harvester/script.cpp
engines/harvester/script.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 39b47e54559..b51b1dbb459 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -29,6 +29,7 @@ MODULE_OBJS = \
script_commands.o \
script_records.o \
script_room.o \
+ script_save.o \
text.o \
xfile_archive.o \
npc/authority_dialogue.o \
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index 84d0be50f2f..073a4db2b30 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -27,7 +27,6 @@
#include "common/file.h"
#include "common/formats/ini-file.h"
#include "common/memstream.h"
-#include "common/serializer.h"
#include "harvester/detection.h"
#include "harvester/parse_utils.h"
#include "harvester/player.h"
@@ -72,135 +71,6 @@ static const InventoryCombatLoadoutEntry kInventoryCombatLoadoutMap[] = {
{ 20, "POOLSTICK" }
};
-static void syncBool(Common::Serializer &s, bool &value) {
- byte serialized = value ? 1 : 0;
- s.syncAsByte(serialized);
- if (s.isLoading())
- value = serialized != 0;
-}
-
-template <typename RecordType, typename SyncFn>
-static void syncRecordArray(Common::Serializer &s, Common::Array<RecordType> &records, SyncFn syncFn) {
- uint32 count = (uint32)records.size();
- s.syncAsUint32LE(count);
- if (s.isLoading())
- records.resize(count);
- for (uint32 i = 0; i < count; ++i)
- syncFn(s, records[i]);
-}
-
-static void syncStartupFlagRecord(Common::Serializer &s, FlagRecord &record) {
- s.syncString(record.name);
- syncBool(s, record.value);
-}
-
-static void syncStartupObjectRecord(Common::Serializer &s, ObjectRecord &record) {
- s.syncAsSint32LE(record.initialX);
- s.syncAsSint32LE(record.initialY);
- s.syncAsSint32LE(record.initialZ);
- s.syncAsSint32LE(record.currentX);
- s.syncAsSint32LE(record.currentY);
- s.syncAsSint32LE(record.currentZ);
- s.syncAsSint32LE(record.boundsX2);
- s.syncAsSint32LE(record.boundsY2);
- s.syncAsSint32LE(record.zExtent);
- s.syncString(record.actionTag);
- s.syncString(record.spritePath);
- s.syncString(record.altSpritePath);
- s.syncString(record.objectName);
- s.syncString(record.reservedXFlag);
- s.syncString(record.identTextKey);
- s.syncString(record.currentOwnerOrRoom);
- s.syncString(record.reservedString40);
- s.syncString(record.inventoryTextKey);
- s.syncString(record.initialOwnerOrRoom);
- s.syncString(record.interactionLabel);
- syncBool(s, record.operatable);
- syncBool(s, record.visible);
- syncBool(s, record.runtimeVisible);
- syncBool(s, record.identShown);
-}
-
-static void syncStartupAnimRecord(Common::Serializer &s, AnimRecord &record) {
- s.syncAsSint32LE(record.x);
- s.syncAsSint32LE(record.y);
- s.syncAsSint32LE(record.z);
- s.syncAsSint32LE(record.frameDelay);
- s.syncString(record.roomName);
- s.syncString(record.resourcePath);
- s.syncString(record.animName);
- syncBool(s, record.active);
- syncBool(s, record.visible);
- syncBool(s, record.looping);
- syncBool(s, record.backward);
- syncBool(s, record.pingPong);
- syncBool(s, record.remove);
- syncBool(s, record.runtimeActive);
- syncBool(s, record.runtimeVisible);
- s.syncAsSint32LE(record.runtimeState);
-}
-
-static void syncStartupNpcRecord(Common::Serializer &s, NpcRecord &record) {
- s.syncAsSint32LE(record.posX);
- s.syncAsSint32LE(record.posY);
- s.syncAsSint32LE(record.posZ);
- s.syncAsSint32LE(record.frameDelay);
- s.syncString(record.onDeathActionTag);
- s.syncString(record.modelPath);
- s.syncString(record.npcName);
- s.syncString(record.monsterfyTargetName);
- s.syncString(record.roomName);
- syncBool(s, record.deathOrMonsterfyFlag);
- syncBool(s, record.runtimeSpawned);
- s.syncAsSint32LE(record.runtimeState);
- syncBool(s, record.active);
- syncBool(s, record.visible);
- syncBool(s, record.savedVisible);
- s.syncAsSint32LE(record.deathDamageType);
- s.syncString(record.audioPath);
- s.syncString(record.entityInitArg);
-}
-
-static void syncStartupMonsterRecord(Common::Serializer &s, MonsterRecord &record) {
- s.syncAsSint32LE(record.posX);
- s.syncAsSint32LE(record.posY);
- s.syncAsSint32LE(record.posZ);
- s.syncAsSint32LE(record.facing);
- s.syncString(record.roomName);
- s.syncString(record.monsterName);
- s.syncString(record.modelPath);
- s.syncString(record.onDeathActionTag);
- syncBool(s, record.active);
- syncBool(s, record.visible);
- s.syncAsSint32LE(record.initialHitPoints);
- s.syncAsSint32LE(record.currentHitPoints);
- s.syncAsSint32LE(record.damageAmount);
- s.syncAsSint32LE(record.engageDistance);
- s.syncAsSint32LE(record.damageType);
- s.syncString(record.reservedString38);
- s.syncString(record.reservedString3c);
- s.syncString(record.reservedString44);
- s.syncString(record.reservedString48);
- s.syncString(record.attackSound1);
- s.syncString(record.attackSound2);
- s.syncString(record.attackSound3);
- s.syncString(record.hitSound1);
- s.syncString(record.hitSound2);
- s.syncString(record.hitSound3);
- s.syncString(record.footstepSoundLeft);
- s.syncString(record.footstepSoundRight);
- s.syncString(record.deathSound);
- syncBool(s, record.savedVisible);
- syncBool(s, record.runtimeSpawned);
- s.syncAsSint32LE(record.screenMinXBound);
- s.syncAsSint32LE(record.screenMaxXBound);
- s.syncAsSint32LE(record.attackSoundTriggerFrame);
- s.syncAsSint32LE(record.hitSoundTriggerFrame);
- s.syncAsSint32LE(record.footstepSoundTriggerFrame);
- s.syncAsSint32LE(record.deathSoundTriggerFrame);
- s.syncAsSint32LE(record.runtimeState);
-}
-
static bool matchesMonsterIdentity(const MonsterRecord &candidate, const MonsterRecord &monster) {
if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
return false;
@@ -214,56 +84,6 @@ static bool matchesMonsterIdentity(const MonsterRecord &candidate, const Monster
return true;
}
-static void restoreMonsterRecordIndices(const Common::Array<MonsterRecord> &baseMonsters,
- Common::Array<MonsterRecord> &runtimeMonsters) {
- for (uint i = 0; i < runtimeMonsters.size(); ++i) {
- if (i < baseMonsters.size() &&
- baseMonsters[i].monsterName.equalsIgnoreCase(runtimeMonsters[i].monsterName) &&
- baseMonsters[i].roomName.equalsIgnoreCase(runtimeMonsters[i].roomName)) {
- runtimeMonsters[i].recordIndex = baseMonsters[i].recordIndex;
- continue;
- }
-
- runtimeMonsters[i].recordIndex = -1;
- for (const MonsterRecord &baseMonster : baseMonsters) {
- if (!baseMonster.monsterName.equalsIgnoreCase(runtimeMonsters[i].monsterName) ||
- !baseMonster.roomName.equalsIgnoreCase(runtimeMonsters[i].roomName)) {
- continue;
- }
-
- runtimeMonsters[i].recordIndex = baseMonster.recordIndex;
- break;
- }
- }
-}
-
-static void syncStartupTimerRecord(Common::Serializer &s, TimerRecord &record) {
- s.syncAsSint32LE(record.initialValue);
- s.syncAsSint32LE(record.currentValue);
- s.syncString(record.timerName);
- s.syncString(record.arg1);
- s.syncString(record.arg2);
- syncBool(s, record.enabled);
- syncBool(s, record.looping);
- syncBool(s, record.global);
-}
-
-static void syncStartupRegionRecord(Common::Serializer &s, RegionRecord &record) {
- s.syncAsSint32LE(record.left);
- s.syncAsSint32LE(record.top);
- s.syncAsSint32LE(record.right);
- s.syncAsSint32LE(record.bottom);
- s.syncAsSint32LE(record.minZ);
- s.syncAsSint32LE(record.maxZ);
- s.syncAsSint32LE(record.desiredFacing);
- s.syncString(record.regionName);
- s.syncString(record.direction);
- s.syncString(record.roomName);
- s.syncString(record.actionTag);
- syncBool(s, record.startEnabled);
- syncBool(s, record.cursorEnabled);
-}
-
static int clampStartupOptionLevel(int level) {
if (level < 0)
return 0;
@@ -273,7 +93,7 @@ static int clampStartupOptionLevel(int level) {
return level;
}
-static int clampPlayerHitPoints(int hitPoints) {
+int Script::clampPlayerHitPoints(int hitPoints) {
if (hitPoints < 0)
return 0;
if (hitPoints > Script::kDefaultPlayerHitPoints)
@@ -282,7 +102,7 @@ static int clampPlayerHitPoints(int hitPoints) {
return hitPoints;
}
-static int clampPlayerCombatLoadout(int loadout) {
+int Script::clampPlayerCombatLoadout(int loadout) {
return CLIP<int>(loadout, 0, kMaxPlayerCombatLoadout);
}
@@ -301,7 +121,7 @@ static int resolveCombatResourceDisplayMax(int loadout) {
}
}
-static int clampCombatResourceCount(int loadout, int count) {
+int Script::clampCombatResourceCount(int loadout, int count) {
const int maxCount = resolveCombatResourceDisplayMax(loadout);
if (maxCount <= 0)
return 0;
@@ -333,7 +153,7 @@ static const char *resolveCombatLoadoutInventoryObjectName(int loadout) {
return nullptr;
}
-static int validatePlayerCombatLoadoutAgainstInventory(const Common::Array<ObjectRecord> ¤tObjects,
+int Script::validatePlayerCombatLoadoutAgainstInventory(const Common::Array<ObjectRecord> ¤tObjects,
int loadout, const char *contextLabel) {
const int clampedLoadout = clampPlayerCombatLoadout(loadout);
if (clampedLoadout == kDefaultPlayerCombatLoadout)
@@ -457,153 +277,6 @@ bool Script::load(ResourceManager &resources) {
return true;
}
-bool Script::reloadTownWorld(ResourceManager &resources) {
- Common::Array<byte> reloadedData;
- if (!resources.loadFile(_path, reloadedData)) {
- warning("Harvester: unable to reload town script '%s'", _path.c_str());
- return false;
- }
-
- const Common::Array<FlagRecord> currentFlags = _currentFlags;
- const Common::Array<ObjectRecord> currentObjects = _currentObjects;
- const Common::Array<AnimRecord> currentAnimations = _currentAnimations;
- const Common::Array<RegionRecord> currentRegions = _currentRegions;
- const Common::Array<NpcRecord> currentNpcs = _currentNpcs;
- const Common::Array<MonsterRecord> currentMonsters = _currentMonsters;
- const Common::Array<TimerRecord> currentTimers = _currentTimers;
- const int nailgunAmmoCount = _nailgunAmmoCount;
- const int shotgunShellCount = _shotgunShellCount;
- const int nineGunBulletCount = _nineGunBulletCount;
- const int thirtyEightGunBulletCount = _thirtyEightGunBulletCount;
- const int chainsawFuelCount = _chainsawFuelCount;
- const int playerCurrentHitPoints = _playerCurrentHitPoints;
- const int playerCombatLoadout = _playerCombatLoadout;
- const bool playerControlPaused = _playerControlPaused;
-
- _data = Common::move(reloadedData);
- decode();
- parseTownRecords(resources);
- resetRuntimeState();
-
- for (const FlagRecord &flag : currentFlags) {
- FlagRecord *currentFlag = findRuntimeFlag(flag.name);
- if (currentFlag) {
- currentFlag->value = flag.value;
- continue;
- }
-
- _currentFlags.push_back(flag);
- }
-
- for (const ObjectRecord &object : currentObjects) {
- ObjectRecord *currentObject = findRuntimeObject(object.initialOwnerOrRoom, object.objectName);
- if (!currentObject)
- currentObject = findRuntimeObject(Common::String(), object.objectName);
- if (!currentObject) {
- _currentObjects.push_back(object);
- continue;
- }
-
- currentObject->currentX = object.currentX;
- currentObject->currentY = object.currentY;
- currentObject->currentZ = object.currentZ;
- currentObject->currentOwnerOrRoom = object.currentOwnerOrRoom;
- currentObject->visible = object.visible;
- currentObject->runtimeVisible = object.runtimeVisible;
- currentObject->identShown = object.identShown;
- }
-
- for (const AnimRecord &anim : currentAnimations) {
- AnimRecord *currentAnimation = findRuntimeAnim(anim.animName);
- if (!currentAnimation) {
- _currentAnimations.push_back(anim);
- continue;
- }
-
- currentAnimation->active = anim.active;
- currentAnimation->visible = anim.visible;
- currentAnimation->runtimeActive = anim.runtimeActive;
- currentAnimation->runtimeVisible = anim.runtimeVisible;
- currentAnimation->runtimeState = anim.runtimeState;
- }
-
- for (const RegionRecord ®ion : currentRegions) {
- RegionRecord *currentRegion = findRuntimeRegion(region.regionName);
- if (!currentRegion) {
- _currentRegions.push_back(region);
- continue;
- }
-
- currentRegion->startEnabled = region.startEnabled;
- }
-
- for (const NpcRecord &npc : currentNpcs) {
- NpcRecord *currentNpc = findRuntimeNpc(npc.npcName);
- if (!currentNpc) {
- _currentNpcs.push_back(npc);
- continue;
- }
-
- currentNpc->active = npc.active;
- currentNpc->visible = npc.visible;
- currentNpc->savedVisible = npc.savedVisible;
- currentNpc->runtimeSpawned = npc.runtimeSpawned;
- currentNpc->runtimeState = npc.runtimeState;
- currentNpc->deathOrMonsterfyFlag = npc.deathOrMonsterfyFlag;
- currentNpc->deathDamageType = npc.deathDamageType;
- }
-
- for (const MonsterRecord &monster : currentMonsters) {
- MonsterRecord *currentMonster = findRuntimeMonster(monster);
- if (!currentMonster) {
- _currentMonsters.push_back(monster);
- continue;
- }
-
- currentMonster->posX = monster.posX;
- currentMonster->posY = monster.posY;
- currentMonster->posZ = monster.posZ;
- currentMonster->facing = monster.facing;
- currentMonster->active = monster.active;
- currentMonster->visible = monster.visible;
- currentMonster->savedVisible = monster.savedVisible;
- currentMonster->runtimeSpawned = monster.runtimeSpawned;
- currentMonster->runtimeState = monster.runtimeState;
- currentMonster->currentHitPoints = monster.currentHitPoints;
- currentMonster->screenMinXBound = monster.screenMinXBound;
- currentMonster->screenMaxXBound = monster.screenMaxXBound;
- }
-
- for (const TimerRecord &timer : currentTimers) {
- TimerRecord *currentTimer = findRuntimeTimer(timer.timerName);
- if (!currentTimer) {
- _currentTimers.push_back(timer);
- continue;
- }
-
- currentTimer->initialValue = timer.initialValue;
- currentTimer->currentValue = timer.currentValue;
- currentTimer->enabled = timer.enabled;
- currentTimer->looping = timer.looping;
- currentTimer->global = timer.global;
- }
-
- _playerCurrentHitPoints = clampPlayerHitPoints(playerCurrentHitPoints);
- _nailgunAmmoCount = clampCombatResourceCount(2, nailgunAmmoCount);
- _shotgunShellCount = clampCombatResourceCount(3, shotgunShellCount);
- _nineGunBulletCount = clampCombatResourceCount(4, nineGunBulletCount);
- _thirtyEightGunBulletCount = clampCombatResourceCount(5, thirtyEightGunBulletCount);
- _chainsawFuelCount = clampCombatResourceCount(14, chainsawFuelCount);
- _playerCombatLoadout = validatePlayerCombatLoadoutAgainstInventory(
- _currentObjects, playerCombatLoadout, "town reload");
- _playerControlPaused = playerControlPaused;
-
- debugC(1, kDebugGeneral,
- "Harvester: reloaded town script '%s' for disc %d while preserving runtime state",
- _path.c_str(), resources.getCurrentDisc());
- return true;
-}
-
bool Script::loadConfig(ResourceManager &resources) {
Common::Array<byte> configData;
if (!resources.loadFile(kStartupConfigPath, configData)) {
@@ -780,194 +453,6 @@ void Script::resetRuntimeState() {
timer.currentValue = timer.initialValue;
}
-void Script::logRuntimeSaveState(const char *operation) const {
- debugC(1, kDebugGeneral,
- "Harvester: %s runtime save state flags=%u objects=%u anims=%u regions=%u npcs=%u monsters=%u timers=%u hp=%d loadout=%d paused=%d ammo=[nails:%d shells:%d bullets9:%d bullets38:%d gas:%d]",
- operation, (uint)_currentFlags.size(), (uint)_currentObjects.size(),
- (uint)_currentAnimations.size(), (uint)_currentRegions.size(),
- (uint)_currentNpcs.size(), (uint)_currentMonsters.size(),
- (uint)_currentTimers.size(), _playerCurrentHitPoints, _playerCombatLoadout,
- _playerControlPaused, _nailgunAmmoCount, _shotgunShellCount,
- _nineGunBulletCount, _thirtyEightGunBulletCount, _chainsawFuelCount);
-
- for (const FlagRecord &flag : _currentFlags) {
- const FlagRecord *baseFlag = nullptr;
- for (const FlagRecord &candidate : _flags) {
- if (candidate.name.equalsIgnoreCase(flag.name)) {
- baseFlag = &candidate;
- break;
- }
- }
- if (!baseFlag || flag.value != baseFlag->value) {
- debugC(3, kDebugGeneral,
- "Harvester: %s runtime flag '%s' value=%d base=%d defined_in_script=%d",
- operation, flag.name.c_str(), flag.value, baseFlag ? baseFlag->value : 0,
- baseFlag != nullptr);
- }
- }
-
- for (const ObjectRecord &object : _currentObjects) {
- const ObjectRecord *baseObject = nullptr;
- for (const ObjectRecord &candidate : _objects) {
- if (candidate.objectName.equalsIgnoreCase(object.objectName)) {
- baseObject = &candidate;
- break;
- }
- }
-
- const Common::String &defaultOwner = baseObject ? baseObject->initialOwnerOrRoom : object.initialOwnerOrRoom;
- const int defaultX = baseObject ? baseObject->initialX : object.initialX;
- const int defaultY = baseObject ? baseObject->initialY : object.initialY;
- const int defaultZ = baseObject ? baseObject->initialZ : object.initialZ;
- const bool defaultRuntimeVisible = baseObject ? baseObject->visible : object.visible;
- const bool defaultVisible = baseObject ? baseObject->visible : object.visible;
- const bool defaultIdentShown = baseObject ? baseObject->identTextKey.empty() : object.identTextKey.empty();
- if (!baseObject ||
- !object.currentOwnerOrRoom.equalsIgnoreCase(defaultOwner) ||
- object.currentX != defaultX ||
- object.currentY != defaultY ||
- object.currentZ != defaultZ ||
- object.runtimeVisible != defaultRuntimeVisible ||
- object.visible != defaultVisible ||
- object.identShown != defaultIdentShown) {
- debugC(3, kDebugGeneral,
- "Harvester: %s runtime object '%s' owner='%s' pos=(%d,%d,%d) visible=%d runtimeVisible=%d identShown=%d base_owner='%s' base_pos=(%d,%d,%d) base_visible=%d base_runtimeVisible=%d base_identShown=%d",
- operation, object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
- object.currentX, object.currentY, object.currentZ,
- object.visible, object.runtimeVisible, object.identShown,
- defaultOwner.c_str(), defaultX, defaultY, defaultZ,
- defaultVisible, defaultRuntimeVisible, defaultIdentShown);
- }
- }
-
- for (const NpcRecord &npc : _currentNpcs) {
- const NpcRecord *baseNpc = nullptr;
- for (const NpcRecord &candidate : _npcs) {
- if (candidate.npcName.equalsIgnoreCase(npc.npcName)) {
- baseNpc = &candidate;
- break;
- }
- }
-
- const bool defaultActive = baseNpc ? baseNpc->active : npc.active;
- const bool defaultVisible = baseNpc ? baseNpc->visible : npc.visible;
- const bool defaultSavedVisible = baseNpc ? baseNpc->visible : npc.visible;
- const bool defaultDeathOrMonsterfy = baseNpc ? baseNpc->deathOrMonsterfyFlag : npc.deathOrMonsterfyFlag;
- const int defaultDeathDamageType = baseNpc ? baseNpc->deathDamageType : npc.deathDamageType;
- const int defaultRuntimeState = -1;
- if (!baseNpc ||
- npc.active != defaultActive ||
- npc.visible != defaultVisible ||
- npc.savedVisible != defaultSavedVisible ||
- npc.deathOrMonsterfyFlag != defaultDeathOrMonsterfy ||
- npc.deathDamageType != defaultDeathDamageType ||
- npc.runtimeSpawned ||
- npc.runtimeState != defaultRuntimeState) {
- debugC(3, kDebugGeneral,
- "Harvester: %s runtime npc '%s' active=%d visible=%d savedVisible=%d spawned=%d runtimeState=%d deathOrMonsterfy=%d damageType=%d base_active=%d base_visible=%d base_savedVisible=%d base_spawned=%d base_runtimeState=%d base_deathOrMonsterfy=%d base_damageType=%d",
- operation, npc.npcName.c_str(), npc.active, npc.visible, npc.savedVisible,
- npc.runtimeSpawned, npc.runtimeState, npc.deathOrMonsterfyFlag, npc.deathDamageType,
- defaultActive, defaultVisible, defaultSavedVisible,
- 0, defaultRuntimeState, defaultDeathOrMonsterfy, defaultDeathDamageType);
- }
- }
-
- for (const MonsterRecord &monster : _currentMonsters) {
- const MonsterRecord *baseMonster = findBaseMonster(monster);
-
- const bool defaultActive = baseMonster ? baseMonster->active : monster.active;
- const bool defaultVisible = baseMonster
- ? (baseMonster->active ? true : baseMonster->visible)
- : (monster.active ? true : monster.visible);
- const bool defaultSavedVisible = defaultVisible;
- const int defaultCurrentHitPoints = baseMonster ? baseMonster->initialHitPoints : monster.initialHitPoints;
- const int defaultScreenMinXBound = baseMonster ? baseMonster->screenMinXBound : monster.screenMinXBound;
- const int defaultScreenMaxXBound = baseMonster ? baseMonster->screenMaxXBound : monster.screenMaxXBound;
- if (!baseMonster ||
- monster.active != defaultActive ||
- monster.visible != defaultVisible ||
- monster.savedVisible != defaultSavedVisible ||
- monster.currentHitPoints != defaultCurrentHitPoints ||
- monster.runtimeSpawned ||
- monster.runtimeState != -1 ||
- monster.screenMinXBound != defaultScreenMinXBound ||
- monster.screenMaxXBound != defaultScreenMaxXBound) {
- debugC(3, kDebugGeneral,
- "Harvester: %s runtime monster '%s' active=%d visible=%d savedVisible=%d spawned=%d runtimeState=%d hp=%d/%d screen_bounds=[%d,%d] base_active=%d base_visible=%d base_savedVisible=%d base_runtimeState=%d base_hp=%d/%d base_screen_bounds=[%d,%d]",
- operation, monster.monsterName.c_str(), monster.active, monster.visible,
- monster.savedVisible, monster.runtimeSpawned, monster.runtimeState,
- monster.currentHitPoints, monster.initialHitPoints,
- monster.screenMinXBound, monster.screenMaxXBound,
- defaultActive, defaultVisible, defaultSavedVisible, -1,
- defaultCurrentHitPoints, baseMonster ? baseMonster->initialHitPoints : monster.initialHitPoints,
- defaultScreenMinXBound, defaultScreenMaxXBound);
- }
- }
-
- for (const TimerRecord &timer : _currentTimers) {
- const TimerRecord *baseTimer = nullptr;
- for (const TimerRecord &candidate : _timers) {
- if (candidate.timerName.equalsIgnoreCase(timer.timerName)) {
- baseTimer = &candidate;
- break;
- }
- }
-
- const int defaultInitialValue = baseTimer ? baseTimer->initialValue : timer.initialValue;
- const int defaultCurrentValue = defaultInitialValue;
- const bool defaultEnabled = baseTimer ? baseTimer->enabled : timer.enabled;
- const bool defaultLooping = baseTimer ? baseTimer->looping : timer.looping;
- const bool defaultGlobal = baseTimer ? baseTimer->global : timer.global;
- if (!baseTimer ||
- timer.initialValue != defaultInitialValue ||
- timer.currentValue != defaultCurrentValue ||
- timer.enabled != defaultEnabled ||
- timer.looping != defaultLooping ||
- timer.global != defaultGlobal) {
- debugC(3, kDebugGeneral,
- "Harvester: %s runtime timer '%s' current=%d initial=%d enabled=%d loop=%d global=%d base_current=%d base_initial=%d base_enabled=%d base_loop=%d base_global=%d",
- operation, timer.timerName.c_str(), timer.currentValue, timer.initialValue,
- timer.enabled, timer.looping, timer.global,
- defaultCurrentValue, defaultInitialValue, defaultEnabled, defaultLooping, defaultGlobal);
- }
- }
-}
-
-void Script::syncRuntimeSaveState(Common::Serializer &s) {
- if (s.isSaving())
- logRuntimeSaveState("saving");
-
- syncRecordArray(s, _currentFlags, syncStartupFlagRecord);
- syncRecordArray(s, _currentObjects, syncStartupObjectRecord);
- syncRecordArray(s, _currentAnimations, syncStartupAnimRecord);
- syncRecordArray(s, _currentRegions, syncStartupRegionRecord);
- syncRecordArray(s, _currentNpcs, syncStartupNpcRecord);
- syncRecordArray(s, _currentMonsters, syncStartupMonsterRecord);
- if (s.isLoading())
- restoreMonsterRecordIndices(_monsters, _currentMonsters);
- s.syncAsSint32LE(_playerCurrentHitPoints);
- syncRecordArray(s, _currentTimers, syncStartupTimerRecord);
- s.syncAsSint32LE(_playerCombatLoadout);
- syncBool(s, _playerControlPaused);
- s.syncAsSint32LE(_nailgunAmmoCount);
- s.syncAsSint32LE(_shotgunShellCount);
- s.syncAsSint32LE(_nineGunBulletCount);
- s.syncAsSint32LE(_thirtyEightGunBulletCount);
- s.syncAsSint32LE(_chainsawFuelCount);
- if (s.isLoading()) {
- _playerCurrentHitPoints = clampPlayerHitPoints(_playerCurrentHitPoints);
- _playerCombatLoadout = clampPlayerCombatLoadout(_playerCombatLoadout);
- _nailgunAmmoCount = clampCombatResourceCount(2, _nailgunAmmoCount);
- _shotgunShellCount = clampCombatResourceCount(3, _shotgunShellCount);
- _nineGunBulletCount = clampCombatResourceCount(4, _nineGunBulletCount);
- _thirtyEightGunBulletCount = clampCombatResourceCount(5, _thirtyEightGunBulletCount);
- _chainsawFuelCount = clampCombatResourceCount(14, _chainsawFuelCount);
- _playerCombatLoadout = validatePlayerCombatLoadoutAgainstInventory(
- _currentObjects, _playerCombatLoadout, "save load");
- logRuntimeSaveState("loaded");
- }
-}
-
bool Script::executeRoomEnterCommands(const Common::String &roomName,
InteractionResult &result) {
result = InteractionResult();
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index 565ca7b7870..93825f8ec66 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -569,6 +569,23 @@ private:
const int *getPlayerCombatResourceCountPtr(int loadout) const;
bool adjustPlayerCombatResourceCount(int loadout, int delta, int maxCount,
const Common::String &reason);
+ void restoreRuntimeFlags(const Common::Array<FlagRecord> ¤tFlags);
+ void restoreRuntimeObjects(const Common::Array<ObjectRecord> ¤tObjects);
+ void restoreRuntimeAnimations(const Common::Array<AnimRecord> ¤tAnimations);
+ void restoreRuntimeRegions(const Common::Array<RegionRecord> ¤tRegions);
+ void restoreRuntimeNpcs(const Common::Array<NpcRecord> ¤tNpcs);
+ void restoreRuntimeMonsters(const Common::Array<MonsterRecord> ¤tMonsters);
+ void restoreRuntimeTimers(const Common::Array<TimerRecord> ¤tTimers);
+ void logRuntimeSaveFlags(const char *operation) const;
+ void logRuntimeSaveObjects(const char *operation) const;
+ void logRuntimeSaveNpcs(const char *operation) const;
+ void logRuntimeSaveMonsters(const char *operation) const;
+ void logRuntimeSaveTimers(const char *operation) const;
+ static int clampPlayerHitPoints(int hitPoints);
+ static int clampPlayerCombatLoadout(int loadout);
+ static int clampCombatResourceCount(int loadout, int count);
+ static int validatePlayerCombatLoadoutAgainstInventory(const Common::Array<ObjectRecord> ¤tObjects,
+ int loadout, const char *contextLabel);
void logRuntimeSaveState(const char *operation) const;
Common::String _path;
diff --git a/engines/harvester/script_save.cpp b/engines/harvester/script_save.cpp
new file mode 100644
index 00000000000..0af41d760d5
--- /dev/null
+++ b/engines/harvester/script_save.cpp
@@ -0,0 +1,584 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/script.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/serializer.h"
+#include "harvester/detection.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+static void syncBool(Common::Serializer &s, bool &value) {
+ byte serialized = value ? 1 : 0;
+ s.syncAsByte(serialized);
+ if (s.isLoading())
+ value = serialized != 0;
+}
+
+template <typename RecordType, typename SyncFn>
+static void syncRecordArray(Common::Serializer &s, Common::Array<RecordType> &records, SyncFn syncFn) {
+ uint32 count = (uint32)records.size();
+ s.syncAsUint32LE(count);
+ if (s.isLoading())
+ records.resize(count);
+ for (uint32 i = 0; i < count; ++i)
+ syncFn(s, records[i]);
+}
+
+static void syncStartupFlagRecord(Common::Serializer &s, FlagRecord &record) {
+ s.syncString(record.name);
+ syncBool(s, record.value);
+}
+
+static void syncStartupObjectRecord(Common::Serializer &s, ObjectRecord &record) {
+ s.syncAsSint32LE(record.initialX);
+ s.syncAsSint32LE(record.initialY);
+ s.syncAsSint32LE(record.initialZ);
+ s.syncAsSint32LE(record.currentX);
+ s.syncAsSint32LE(record.currentY);
+ s.syncAsSint32LE(record.currentZ);
+ s.syncAsSint32LE(record.boundsX2);
+ s.syncAsSint32LE(record.boundsY2);
+ s.syncAsSint32LE(record.zExtent);
+ s.syncString(record.actionTag);
+ s.syncString(record.spritePath);
+ s.syncString(record.altSpritePath);
+ s.syncString(record.objectName);
+ s.syncString(record.reservedXFlag);
+ s.syncString(record.identTextKey);
+ s.syncString(record.currentOwnerOrRoom);
+ s.syncString(record.reservedString40);
+ s.syncString(record.inventoryTextKey);
+ s.syncString(record.initialOwnerOrRoom);
+ s.syncString(record.interactionLabel);
+ syncBool(s, record.operatable);
+ syncBool(s, record.visible);
+ syncBool(s, record.runtimeVisible);
+ syncBool(s, record.identShown);
+}
+
+static void syncStartupAnimRecord(Common::Serializer &s, AnimRecord &record) {
+ s.syncAsSint32LE(record.x);
+ s.syncAsSint32LE(record.y);
+ s.syncAsSint32LE(record.z);
+ s.syncAsSint32LE(record.frameDelay);
+ s.syncString(record.roomName);
+ s.syncString(record.resourcePath);
+ s.syncString(record.animName);
+ syncBool(s, record.active);
+ syncBool(s, record.visible);
+ syncBool(s, record.looping);
+ syncBool(s, record.backward);
+ syncBool(s, record.pingPong);
+ syncBool(s, record.remove);
+ syncBool(s, record.runtimeActive);
+ syncBool(s, record.runtimeVisible);
+ s.syncAsSint32LE(record.runtimeState);
+}
+
+static void syncStartupNpcRecord(Common::Serializer &s, NpcRecord &record) {
+ s.syncAsSint32LE(record.posX);
+ s.syncAsSint32LE(record.posY);
+ s.syncAsSint32LE(record.posZ);
+ s.syncAsSint32LE(record.frameDelay);
+ s.syncString(record.onDeathActionTag);
+ s.syncString(record.modelPath);
+ s.syncString(record.npcName);
+ s.syncString(record.monsterfyTargetName);
+ s.syncString(record.roomName);
+ syncBool(s, record.deathOrMonsterfyFlag);
+ syncBool(s, record.runtimeSpawned);
+ s.syncAsSint32LE(record.runtimeState);
+ syncBool(s, record.active);
+ syncBool(s, record.visible);
+ syncBool(s, record.savedVisible);
+ s.syncAsSint32LE(record.deathDamageType);
+ s.syncString(record.audioPath);
+ s.syncString(record.entityInitArg);
+}
+
+static void syncStartupMonsterRecord(Common::Serializer &s, MonsterRecord &record) {
+ s.syncAsSint32LE(record.posX);
+ s.syncAsSint32LE(record.posY);
+ s.syncAsSint32LE(record.posZ);
+ s.syncAsSint32LE(record.facing);
+ s.syncString(record.roomName);
+ s.syncString(record.monsterName);
+ s.syncString(record.modelPath);
+ s.syncString(record.onDeathActionTag);
+ syncBool(s, record.active);
+ syncBool(s, record.visible);
+ s.syncAsSint32LE(record.initialHitPoints);
+ s.syncAsSint32LE(record.currentHitPoints);
+ s.syncAsSint32LE(record.damageAmount);
+ s.syncAsSint32LE(record.engageDistance);
+ s.syncAsSint32LE(record.damageType);
+ s.syncString(record.reservedString38);
+ s.syncString(record.reservedString3c);
+ s.syncString(record.reservedString44);
+ s.syncString(record.reservedString48);
+ s.syncString(record.attackSound1);
+ s.syncString(record.attackSound2);
+ s.syncString(record.attackSound3);
+ s.syncString(record.hitSound1);
+ s.syncString(record.hitSound2);
+ s.syncString(record.hitSound3);
+ s.syncString(record.footstepSoundLeft);
+ s.syncString(record.footstepSoundRight);
+ s.syncString(record.deathSound);
+ syncBool(s, record.savedVisible);
+ syncBool(s, record.runtimeSpawned);
+ s.syncAsSint32LE(record.screenMinXBound);
+ s.syncAsSint32LE(record.screenMaxXBound);
+ s.syncAsSint32LE(record.attackSoundTriggerFrame);
+ s.syncAsSint32LE(record.hitSoundTriggerFrame);
+ s.syncAsSint32LE(record.footstepSoundTriggerFrame);
+ s.syncAsSint32LE(record.deathSoundTriggerFrame);
+ s.syncAsSint32LE(record.runtimeState);
+}
+
+static void restoreMonsterRecordIndices(const Common::Array<MonsterRecord> &baseMonsters,
+ Common::Array<MonsterRecord> &runtimeMonsters) {
+ for (uint i = 0; i < runtimeMonsters.size(); ++i) {
+ if (i < baseMonsters.size() &&
+ baseMonsters[i].monsterName.equalsIgnoreCase(runtimeMonsters[i].monsterName) &&
+ baseMonsters[i].roomName.equalsIgnoreCase(runtimeMonsters[i].roomName)) {
+ runtimeMonsters[i].recordIndex = baseMonsters[i].recordIndex;
+ continue;
+ }
+
+ runtimeMonsters[i].recordIndex = -1;
+ for (const MonsterRecord &baseMonster : baseMonsters) {
+ if (!baseMonster.monsterName.equalsIgnoreCase(runtimeMonsters[i].monsterName) ||
+ !baseMonster.roomName.equalsIgnoreCase(runtimeMonsters[i].roomName)) {
+ continue;
+ }
+
+ runtimeMonsters[i].recordIndex = baseMonster.recordIndex;
+ break;
+ }
+ }
+}
+
+static void syncStartupTimerRecord(Common::Serializer &s, TimerRecord &record) {
+ s.syncAsSint32LE(record.initialValue);
+ s.syncAsSint32LE(record.currentValue);
+ s.syncString(record.timerName);
+ s.syncString(record.arg1);
+ s.syncString(record.arg2);
+ syncBool(s, record.enabled);
+ syncBool(s, record.looping);
+ syncBool(s, record.global);
+}
+
+static void syncStartupRegionRecord(Common::Serializer &s, RegionRecord &record) {
+ s.syncAsSint32LE(record.left);
+ s.syncAsSint32LE(record.top);
+ s.syncAsSint32LE(record.right);
+ s.syncAsSint32LE(record.bottom);
+ s.syncAsSint32LE(record.minZ);
+ s.syncAsSint32LE(record.maxZ);
+ s.syncAsSint32LE(record.desiredFacing);
+ s.syncString(record.regionName);
+ s.syncString(record.direction);
+ s.syncString(record.roomName);
+ s.syncString(record.actionTag);
+ syncBool(s, record.startEnabled);
+ syncBool(s, record.cursorEnabled);
+}
+
+void Script::restoreRuntimeFlags(const Common::Array<FlagRecord> ¤tFlags) {
+ for (const FlagRecord &flag : currentFlags) {
+ FlagRecord *currentFlag = findRuntimeFlag(flag.name);
+ if (currentFlag) {
+ currentFlag->value = flag.value;
+ continue;
+ }
+
+ _currentFlags.push_back(flag);
+ }
+}
+
+void Script::restoreRuntimeObjects(const Common::Array<ObjectRecord> ¤tObjects) {
+ for (const ObjectRecord &object : currentObjects) {
+ ObjectRecord *currentObject = findRuntimeObject(object.initialOwnerOrRoom, object.objectName);
+ if (!currentObject)
+ currentObject = findRuntimeObject(Common::String(), object.objectName);
+ if (!currentObject) {
+ _currentObjects.push_back(object);
+ continue;
+ }
+
+ currentObject->currentX = object.currentX;
+ currentObject->currentY = object.currentY;
+ currentObject->currentZ = object.currentZ;
+ currentObject->currentOwnerOrRoom = object.currentOwnerOrRoom;
+ currentObject->visible = object.visible;
+ currentObject->runtimeVisible = object.runtimeVisible;
+ currentObject->identShown = object.identShown;
+ }
+}
+
+void Script::restoreRuntimeAnimations(const Common::Array<AnimRecord> ¤tAnimations) {
+ for (const AnimRecord &anim : currentAnimations) {
+ AnimRecord *currentAnimation = findRuntimeAnim(anim.animName);
+ if (!currentAnimation) {
+ _currentAnimations.push_back(anim);
+ continue;
+ }
+
+ currentAnimation->active = anim.active;
+ currentAnimation->visible = anim.visible;
+ currentAnimation->runtimeActive = anim.runtimeActive;
+ currentAnimation->runtimeVisible = anim.runtimeVisible;
+ currentAnimation->runtimeState = anim.runtimeState;
+ }
+}
+
+void Script::restoreRuntimeRegions(const Common::Array<RegionRecord> ¤tRegions) {
+ for (const RegionRecord ®ion : currentRegions) {
+ RegionRecord *currentRegion = findRuntimeRegion(region.regionName);
+ if (!currentRegion) {
+ _currentRegions.push_back(region);
+ continue;
+ }
+
+ currentRegion->startEnabled = region.startEnabled;
+ }
+}
+
+void Script::restoreRuntimeNpcs(const Common::Array<NpcRecord> ¤tNpcs) {
+ for (const NpcRecord &npc : currentNpcs) {
+ NpcRecord *currentNpc = findRuntimeNpc(npc.npcName);
+ if (!currentNpc) {
+ _currentNpcs.push_back(npc);
+ continue;
+ }
+
+ currentNpc->active = npc.active;
+ currentNpc->visible = npc.visible;
+ currentNpc->savedVisible = npc.savedVisible;
+ currentNpc->runtimeSpawned = npc.runtimeSpawned;
+ currentNpc->runtimeState = npc.runtimeState;
+ currentNpc->deathOrMonsterfyFlag = npc.deathOrMonsterfyFlag;
+ currentNpc->deathDamageType = npc.deathDamageType;
+ }
+}
+
+void Script::restoreRuntimeMonsters(const Common::Array<MonsterRecord> ¤tMonsters) {
+ for (const MonsterRecord &monster : currentMonsters) {
+ MonsterRecord *currentMonster = findRuntimeMonster(monster);
+ if (!currentMonster) {
+ _currentMonsters.push_back(monster);
+ continue;
+ }
+
+ currentMonster->posX = monster.posX;
+ currentMonster->posY = monster.posY;
+ currentMonster->posZ = monster.posZ;
+ currentMonster->facing = monster.facing;
+ currentMonster->active = monster.active;
+ currentMonster->visible = monster.visible;
+ currentMonster->savedVisible = monster.savedVisible;
+ currentMonster->runtimeSpawned = monster.runtimeSpawned;
+ currentMonster->runtimeState = monster.runtimeState;
+ currentMonster->currentHitPoints = monster.currentHitPoints;
+ currentMonster->screenMinXBound = monster.screenMinXBound;
+ currentMonster->screenMaxXBound = monster.screenMaxXBound;
+ }
+}
+
+void Script::restoreRuntimeTimers(const Common::Array<TimerRecord> ¤tTimers) {
+ for (const TimerRecord &timer : currentTimers) {
+ TimerRecord *currentTimer = findRuntimeTimer(timer.timerName);
+ if (!currentTimer) {
+ _currentTimers.push_back(timer);
+ continue;
+ }
+
+ currentTimer->initialValue = timer.initialValue;
+ currentTimer->currentValue = timer.currentValue;
+ currentTimer->enabled = timer.enabled;
+ currentTimer->looping = timer.looping;
+ currentTimer->global = timer.global;
+ }
+}
+
+bool Script::reloadTownWorld(ResourceManager &resources) {
+ Common::Array<byte> reloadedData;
+ if (!resources.loadFile(_path, reloadedData)) {
+ warning("Harvester: unable to reload town script '%s'", _path.c_str());
+ return false;
+ }
+
+ const Common::Array<FlagRecord> currentFlags = _currentFlags;
+ const Common::Array<ObjectRecord> currentObjects = _currentObjects;
+ const Common::Array<AnimRecord> currentAnimations = _currentAnimations;
+ const Common::Array<RegionRecord> currentRegions = _currentRegions;
+ const Common::Array<NpcRecord> currentNpcs = _currentNpcs;
+ const Common::Array<MonsterRecord> currentMonsters = _currentMonsters;
+ const Common::Array<TimerRecord> currentTimers = _currentTimers;
+ const int nailgunAmmoCount = _nailgunAmmoCount;
+ const int shotgunShellCount = _shotgunShellCount;
+ const int nineGunBulletCount = _nineGunBulletCount;
+ const int thirtyEightGunBulletCount = _thirtyEightGunBulletCount;
+ const int chainsawFuelCount = _chainsawFuelCount;
+ const int playerCurrentHitPoints = _playerCurrentHitPoints;
+ const int playerCombatLoadout = _playerCombatLoadout;
+ const bool playerControlPaused = _playerControlPaused;
+
+ _data = Common::move(reloadedData);
+ decode();
+ parseTownRecords(resources);
+ resetRuntimeState();
+
+ restoreRuntimeFlags(currentFlags);
+ restoreRuntimeObjects(currentObjects);
+ restoreRuntimeAnimations(currentAnimations);
+ restoreRuntimeRegions(currentRegions);
+ restoreRuntimeNpcs(currentNpcs);
+ restoreRuntimeMonsters(currentMonsters);
+ restoreRuntimeTimers(currentTimers);
+
+ _playerCurrentHitPoints = clampPlayerHitPoints(playerCurrentHitPoints);
+ _nailgunAmmoCount = clampCombatResourceCount(2, nailgunAmmoCount);
+ _shotgunShellCount = clampCombatResourceCount(3, shotgunShellCount);
+ _nineGunBulletCount = clampCombatResourceCount(4, nineGunBulletCount);
+ _thirtyEightGunBulletCount = clampCombatResourceCount(5, thirtyEightGunBulletCount);
+ _chainsawFuelCount = clampCombatResourceCount(14, chainsawFuelCount);
+ _playerCombatLoadout = validatePlayerCombatLoadoutAgainstInventory(
+ _currentObjects, playerCombatLoadout, "town reload");
+ _playerControlPaused = playerControlPaused;
+
+ debugC(1, kDebugGeneral,
+ "Harvester: reloaded town script '%s' for disc %d while preserving runtime state",
+ _path.c_str(), resources.getCurrentDisc());
+ return true;
+}
+
+void Script::logRuntimeSaveFlags(const char *operation) const {
+ for (const FlagRecord &flag : _currentFlags) {
+ const FlagRecord *baseFlag = nullptr;
+ for (const FlagRecord &candidate : _flags) {
+ if (candidate.name.equalsIgnoreCase(flag.name)) {
+ baseFlag = &candidate;
+ break;
+ }
+ }
+ if (!baseFlag || flag.value != baseFlag->value) {
+ debugC(3, kDebugGeneral,
+ "Harvester: %s runtime flag '%s' value=%d base=%d defined_in_script=%d",
+ operation, flag.name.c_str(), flag.value, baseFlag ? baseFlag->value : 0,
+ baseFlag != nullptr);
+ }
+ }
+}
+
+void Script::logRuntimeSaveObjects(const char *operation) const {
+ for (const ObjectRecord &object : _currentObjects) {
+ const ObjectRecord *baseObject = nullptr;
+ for (const ObjectRecord &candidate : _objects) {
+ if (candidate.objectName.equalsIgnoreCase(object.objectName)) {
+ baseObject = &candidate;
+ break;
+ }
+ }
+
+ const Common::String &defaultOwner = baseObject ? baseObject->initialOwnerOrRoom : object.initialOwnerOrRoom;
+ const int defaultX = baseObject ? baseObject->initialX : object.initialX;
+ const int defaultY = baseObject ? baseObject->initialY : object.initialY;
+ const int defaultZ = baseObject ? baseObject->initialZ : object.initialZ;
+ const bool defaultRuntimeVisible = baseObject ? baseObject->visible : object.visible;
+ const bool defaultVisible = baseObject ? baseObject->visible : object.visible;
+ const bool defaultIdentShown = baseObject ? baseObject->identTextKey.empty() : object.identTextKey.empty();
+ if (!baseObject ||
+ !object.currentOwnerOrRoom.equalsIgnoreCase(defaultOwner) ||
+ object.currentX != defaultX ||
+ object.currentY != defaultY ||
+ object.currentZ != defaultZ ||
+ object.runtimeVisible != defaultRuntimeVisible ||
+ object.visible != defaultVisible ||
+ object.identShown != defaultIdentShown) {
+ debugC(3, kDebugGeneral,
+ "Harvester: %s runtime object '%s' owner='%s' pos=(%d,%d,%d) visible=%d runtimeVisible=%d identShown=%d base_owner='%s' base_pos=(%d,%d,%d) base_visible=%d base_runtimeVisible=%d base_identShown=%d",
+ operation, object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
+ object.currentX, object.currentY, object.currentZ,
+ object.visible, object.runtimeVisible, object.identShown,
+ defaultOwner.c_str(), defaultX, defaultY, defaultZ,
+ defaultVisible, defaultRuntimeVisible, defaultIdentShown);
+ }
+ }
+}
+
+void Script::logRuntimeSaveNpcs(const char *operation) const {
+ for (const NpcRecord &npc : _currentNpcs) {
+ const NpcRecord *baseNpc = nullptr;
+ for (const NpcRecord &candidate : _npcs) {
+ if (candidate.npcName.equalsIgnoreCase(npc.npcName)) {
+ baseNpc = &candidate;
+ break;
+ }
+ }
+
+ const bool defaultActive = baseNpc ? baseNpc->active : npc.active;
+ const bool defaultVisible = baseNpc ? baseNpc->visible : npc.visible;
+ const bool defaultSavedVisible = baseNpc ? baseNpc->visible : npc.visible;
+ const bool defaultDeathOrMonsterfy = baseNpc ? baseNpc->deathOrMonsterfyFlag : npc.deathOrMonsterfyFlag;
+ const int defaultDeathDamageType = baseNpc ? baseNpc->deathDamageType : npc.deathDamageType;
+ const int defaultRuntimeState = -1;
+ if (!baseNpc ||
+ npc.active != defaultActive ||
+ npc.visible != defaultVisible ||
+ npc.savedVisible != defaultSavedVisible ||
+ npc.deathOrMonsterfyFlag != defaultDeathOrMonsterfy ||
+ npc.deathDamageType != defaultDeathDamageType ||
+ npc.runtimeSpawned ||
+ npc.runtimeState != defaultRuntimeState) {
+ debugC(3, kDebugGeneral,
+ "Harvester: %s runtime npc '%s' active=%d visible=%d savedVisible=%d spawned=%d runtimeState=%d deathOrMonsterfy=%d damageType=%d base_active=%d base_visible=%d base_savedVisible=%d base_spawned=%d base_runtimeState=%d base_deathOrMonsterfy=%d base_damageType=%d",
+ operation, npc.npcName.c_str(), npc.active, npc.visible, npc.savedVisible,
+ npc.runtimeSpawned, npc.runtimeState, npc.deathOrMonsterfyFlag, npc.deathDamageType,
+ defaultActive, defaultVisible, defaultSavedVisible,
+ 0, defaultRuntimeState, defaultDeathOrMonsterfy, defaultDeathDamageType);
+ }
+ }
+}
+
+void Script::logRuntimeSaveMonsters(const char *operation) const {
+ for (const MonsterRecord &monster : _currentMonsters) {
+ const MonsterRecord *baseMonster = findBaseMonster(monster);
+
+ const bool defaultActive = baseMonster ? baseMonster->active : monster.active;
+ const bool defaultVisible = baseMonster
+ ? (baseMonster->active ? true : baseMonster->visible)
+ : (monster.active ? true : monster.visible);
+ const bool defaultSavedVisible = defaultVisible;
+ const int defaultCurrentHitPoints = baseMonster ? baseMonster->initialHitPoints : monster.initialHitPoints;
+ const int defaultScreenMinXBound = baseMonster ? baseMonster->screenMinXBound : monster.screenMinXBound;
+ const int defaultScreenMaxXBound = baseMonster ? baseMonster->screenMaxXBound : monster.screenMaxXBound;
+ if (!baseMonster ||
+ monster.active != defaultActive ||
+ monster.visible != defaultVisible ||
+ monster.savedVisible != defaultSavedVisible ||
+ monster.currentHitPoints != defaultCurrentHitPoints ||
+ monster.runtimeSpawned ||
+ monster.runtimeState != -1 ||
+ monster.screenMinXBound != defaultScreenMinXBound ||
+ monster.screenMaxXBound != defaultScreenMaxXBound) {
+ debugC(3, kDebugGeneral,
+ "Harvester: %s runtime monster '%s' active=%d visible=%d savedVisible=%d spawned=%d runtimeState=%d hp=%d/%d screen_bounds=[%d,%d] base_active=%d base_visible=%d base_savedVisible=%d base_runtimeState=%d base_hp=%d/%d base_screen_bounds=[%d,%d]",
+ operation, monster.monsterName.c_str(), monster.active, monster.visible,
+ monster.savedVisible, monster.runtimeSpawned, monster.runtimeState,
+ monster.currentHitPoints, monster.initialHitPoints,
+ monster.screenMinXBound, monster.screenMaxXBound,
+ defaultActive, defaultVisible, defaultSavedVisible, -1,
+ defaultCurrentHitPoints, baseMonster ? baseMonster->initialHitPoints : monster.initialHitPoints,
+ defaultScreenMinXBound, defaultScreenMaxXBound);
+ }
+ }
+}
+
+void Script::logRuntimeSaveTimers(const char *operation) const {
+ for (const TimerRecord &timer : _currentTimers) {
+ const TimerRecord *baseTimer = nullptr;
+ for (const TimerRecord &candidate : _timers) {
+ if (candidate.timerName.equalsIgnoreCase(timer.timerName)) {
+ baseTimer = &candidate;
+ break;
+ }
+ }
+
+ const int defaultInitialValue = baseTimer ? baseTimer->initialValue : timer.initialValue;
+ const int defaultCurrentValue = defaultInitialValue;
+ const bool defaultEnabled = baseTimer ? baseTimer->enabled : timer.enabled;
+ const bool defaultLooping = baseTimer ? baseTimer->looping : timer.looping;
+ const bool defaultGlobal = baseTimer ? baseTimer->global : timer.global;
+ if (!baseTimer ||
+ timer.initialValue != defaultInitialValue ||
+ timer.currentValue != defaultCurrentValue ||
+ timer.enabled != defaultEnabled ||
+ timer.looping != defaultLooping ||
+ timer.global != defaultGlobal) {
+ debugC(3, kDebugGeneral,
+ "Harvester: %s runtime timer '%s' current=%d initial=%d enabled=%d loop=%d global=%d base_current=%d base_initial=%d base_enabled=%d base_loop=%d base_global=%d",
+ operation, timer.timerName.c_str(), timer.currentValue, timer.initialValue,
+ timer.enabled, timer.looping, timer.global,
+ defaultCurrentValue, defaultInitialValue, defaultEnabled, defaultLooping, defaultGlobal);
+ }
+ }
+}
+
+void Script::logRuntimeSaveState(const char *operation) const {
+ debugC(1, kDebugGeneral,
+ "Harvester: %s runtime save state flags=%u objects=%u anims=%u regions=%u npcs=%u monsters=%u timers=%u hp=%d loadout=%d paused=%d ammo=[nails:%d shells:%d bullets9:%d bullets38:%d gas:%d]",
+ operation, (uint)_currentFlags.size(), (uint)_currentObjects.size(),
+ (uint)_currentAnimations.size(), (uint)_currentRegions.size(),
+ (uint)_currentNpcs.size(), (uint)_currentMonsters.size(),
+ (uint)_currentTimers.size(), _playerCurrentHitPoints, _playerCombatLoadout,
+ _playerControlPaused, _nailgunAmmoCount, _shotgunShellCount,
+ _nineGunBulletCount, _thirtyEightGunBulletCount, _chainsawFuelCount);
+
+ logRuntimeSaveFlags(operation);
+ logRuntimeSaveObjects(operation);
+ logRuntimeSaveNpcs(operation);
+ logRuntimeSaveMonsters(operation);
+ logRuntimeSaveTimers(operation);
+}
+
+void Script::syncRuntimeSaveState(Common::Serializer &s) {
+ if (s.isSaving())
+ logRuntimeSaveState("saving");
+
+ syncRecordArray(s, _currentFlags, syncStartupFlagRecord);
+ syncRecordArray(s, _currentObjects, syncStartupObjectRecord);
+ syncRecordArray(s, _currentAnimations, syncStartupAnimRecord);
+ syncRecordArray(s, _currentRegions, syncStartupRegionRecord);
+ syncRecordArray(s, _currentNpcs, syncStartupNpcRecord);
+ syncRecordArray(s, _currentMonsters, syncStartupMonsterRecord);
+ if (s.isLoading())
+ restoreMonsterRecordIndices(_monsters, _currentMonsters);
+ s.syncAsSint32LE(_playerCurrentHitPoints);
+ syncRecordArray(s, _currentTimers, syncStartupTimerRecord);
+ s.syncAsSint32LE(_playerCombatLoadout);
+ syncBool(s, _playerControlPaused);
+ s.syncAsSint32LE(_nailgunAmmoCount);
+ s.syncAsSint32LE(_shotgunShellCount);
+ s.syncAsSint32LE(_nineGunBulletCount);
+ s.syncAsSint32LE(_thirtyEightGunBulletCount);
+ s.syncAsSint32LE(_chainsawFuelCount);
+ if (s.isLoading()) {
+ _playerCurrentHitPoints = clampPlayerHitPoints(_playerCurrentHitPoints);
+ _playerCombatLoadout = clampPlayerCombatLoadout(_playerCombatLoadout);
+ _nailgunAmmoCount = clampCombatResourceCount(2, _nailgunAmmoCount);
+ _shotgunShellCount = clampCombatResourceCount(3, _shotgunShellCount);
+ _nineGunBulletCount = clampCombatResourceCount(4, _nineGunBulletCount);
+ _thirtyEightGunBulletCount = clampCombatResourceCount(5, _thirtyEightGunBulletCount);
+ _chainsawFuelCount = clampCombatResourceCount(14, _chainsawFuelCount);
+ _playerCombatLoadout = validatePlayerCombatLoadoutAgainstInventory(
+ _currentObjects, _playerCombatLoadout, "save load");
+ logRuntimeSaveState("loaded");
+ }
+}
+
+} // End of namespace Harvester
Commit: 29bc2c22834db25bd274bf6520102acd12d429e9
https://github.com/scummvm/scummvm/commit/29bc2c22834db25bd274bf6520102acd12d429e9
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Separate script runtime and actor state handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/script_actors.cpp
A engines/harvester/script_runtime.cpp
engines/harvester/module.mk
engines/harvester/script.cpp
engines/harvester/script.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index b51b1dbb459..a48c049f33f 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -26,9 +26,11 @@ MODULE_OBJS = \
entity.o \
saveload.o \
script.o \
+ script_actors.o \
script_commands.o \
script_records.o \
script_room.o \
+ script_runtime.o \
script_save.o \
text.o \
xfile_archive.o \
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index 073a4db2b30..11523f22d04 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -21,7 +21,6 @@
#include "harvester/script.h"
-#include "common/algorithm.h"
#include "common/config-manager.h"
#include "common/debug.h"
#include "common/file.h"
@@ -29,7 +28,6 @@
#include "common/memstream.h"
#include "harvester/detection.h"
#include "harvester/parse_utils.h"
-#include "harvester/player.h"
#include "harvester/resources.h"
namespace Harvester {
@@ -37,53 +35,9 @@ namespace Harvester {
static const char *const kStartupConfigPath = "CONFIG.INI";
static const char *const kConfigSectionName = "harvester";
static const char *const kDefaultTownScript = "HARVEST.SCR";
-static const char *const kInventoryOwnerName = "INVENTORY";
static const char *const kDefaultVoicePath = "SOUND/VOICE/";
-static const int kDefaultPlayerCombatLoadout = 0;
-static const int kMaxPlayerCombatLoadout = 0x14;
static const int kMaxStartupOptionLevel = 9;
-struct InventoryCombatLoadoutEntry {
- int loadoutId;
- const char *objectName;
-};
-
-static const InventoryCombatLoadoutEntry kInventoryCombatLoadoutMap[] = {
- { 1, "CLEAVER" },
- { 2, "NAILGUN" },
- { 3, "SHOTGUN" },
- { 4, "9GUN" },
- { 5, "38GUN" },
- { 6, "TOMAHAWK" },
- { 7, "KNIFE" },
- { 8, "FLAIL" },
- { 9, "HANDAXE" },
- { 10, "WRENCH" },
- { 11, "PITCHFORK" },
- { 12, "SCYTHE" },
- { 13, "SWORD" },
- { 14, "CHAINSAW" },
- { 15, "HARVEST_BLADE" },
- { 16, "SHOVEL" },
- { 17, "FIREAXE" },
- { 18, "BAT" },
- { 19, "RAZOR" },
- { 20, "POOLSTICK" }
-};
-
-static bool matchesMonsterIdentity(const MonsterRecord &candidate, const MonsterRecord &monster) {
- if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
- return false;
-
- if (candidate.recordIndex >= 0 && monster.recordIndex >= 0)
- return candidate.recordIndex == monster.recordIndex;
-
- if (!monster.roomName.empty())
- return candidate.roomName.equalsIgnoreCase(monster.roomName);
-
- return true;
-}
-
static int clampStartupOptionLevel(int level) {
if (level < 0)
return 0;
@@ -93,110 +47,6 @@ static int clampStartupOptionLevel(int level) {
return level;
}
-int Script::clampPlayerHitPoints(int hitPoints) {
- if (hitPoints < 0)
- return 0;
- if (hitPoints > Script::kDefaultPlayerHitPoints)
- return Script::kDefaultPlayerHitPoints;
-
- return hitPoints;
-}
-
-int Script::clampPlayerCombatLoadout(int loadout) {
- return CLIP<int>(loadout, 0, kMaxPlayerCombatLoadout);
-}
-
-static int resolveCombatResourceDisplayMax(int loadout) {
- switch (loadout) {
- case 2:
- case 3:
- case 14:
- return 16;
- case 4:
- return 8;
- case 5:
- return 6;
- default:
- return 0;
- }
-}
-
-int Script::clampCombatResourceCount(int loadout, int count) {
- const int maxCount = resolveCombatResourceDisplayMax(loadout);
- if (maxCount <= 0)
- return 0;
-
- return CLIP<int>(count, 0, maxCount);
-}
-
-int Script::resolveCombatResourcePickupMax(int loadout) {
- switch (loadout) {
- case 2:
- case 3:
- case 14:
- return 15;
- case 4:
- return 7;
- case 5:
- return 5;
- default:
- return 0;
- }
-}
-
-static const char *resolveCombatLoadoutInventoryObjectName(int loadout) {
- for (const InventoryCombatLoadoutEntry &entry : kInventoryCombatLoadoutMap) {
- if (entry.loadoutId == loadout)
- return entry.objectName;
- }
-
- return nullptr;
-}
-
-int Script::validatePlayerCombatLoadoutAgainstInventory(const Common::Array<ObjectRecord> ¤tObjects,
- int loadout, const char *contextLabel) {
- const int clampedLoadout = clampPlayerCombatLoadout(loadout);
- if (clampedLoadout == kDefaultPlayerCombatLoadout)
- return clampedLoadout;
-
- const char *requiredObjectName = resolveCombatLoadoutInventoryObjectName(clampedLoadout);
- if (!requiredObjectName)
- return clampedLoadout;
-
- for (const ObjectRecord &object : currentObjects) {
- if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) &&
- object.objectName.equalsIgnoreCase(requiredObjectName))
- return clampedLoadout;
- }
-
- debugC(1, kDebugGeneral,
- "Harvester: cleared player combat loadout %d('%s') after %s because inventory object '%s' is absent",
- clampedLoadout, Player::describeCombatLoadout(clampedLoadout),
- contextLabel ? contextLabel : "runtime restore", requiredObjectName);
- return kDefaultPlayerCombatLoadout;
-}
-
-static const char *resolveInventoryStatusObjectName(int hitPoints) {
- if (hitPoints < 8)
- return "INV_STAT4";
- if (hitPoints < 15)
- return "INV_STAT3";
- if (hitPoints < 23)
- return "INV_STAT2";
-
- return "INV_STAT1";
-}
-
-static Common::String normalizeInteractionLabel(const Common::String &value) {
- Common::String label = value;
- for (uint i = 0; i < label.size(); ++i) {
- if (label[i] == '_')
- label.setChar(' ', i);
- }
-
- return label;
-}
-
static Common::String toNativeConfigPath(const Common::String &value) {
Common::String result(value);
for (uint i = 0; i < result.size(); ++i) {
@@ -402,57 +252,6 @@ void Script::setGammaLevel(int level) {
_gammaLevel = clampStartupOptionLevel(level);
}
-void Script::resetRuntimeState() {
- _currentFlags = _flags;
- _currentObjects = _objects;
- _currentAnimations = _animations;
- _currentRegions = _regions;
- _currentNpcs = _npcs;
- _currentMonsters = _monsters;
- _currentTimers = _timers;
- _nailgunAmmoCount = 0;
- _shotgunShellCount = 0;
- _nineGunBulletCount = 0;
- _thirtyEightGunBulletCount = 0;
- _chainsawFuelCount = 0;
- _playerCurrentHitPoints = Script::kDefaultPlayerHitPoints;
- _playerCombatLoadout = kDefaultPlayerCombatLoadout;
- _playerControlPaused = false;
-
- for (ObjectRecord &object : _currentObjects) {
- object.currentX = object.initialX;
- object.currentY = object.initialY;
- object.currentZ = object.initialZ;
- object.currentOwnerOrRoom = object.initialOwnerOrRoom;
- object.runtimeVisible = object.visible;
- object.identShown = object.identTextKey.empty();
- }
-
- for (AnimRecord &anim : _currentAnimations) {
- anim.runtimeActive = anim.active;
- anim.runtimeVisible = anim.visible;
- anim.runtimeState = -1;
- }
-
- for (NpcRecord &npc : _currentNpcs) {
- npc.runtimeSpawned = false;
- npc.runtimeState = -1;
- npc.savedVisible = npc.visible;
- }
-
- for (MonsterRecord &monster : _currentMonsters) {
- monster.currentHitPoints = monster.initialHitPoints;
- monster.runtimeSpawned = false;
- monster.runtimeState = -1;
- monster.savedVisible = monster.visible;
- if (monster.active)
- monster.visible = true;
- }
-
- for (TimerRecord &timer : _currentTimers)
- timer.currentValue = timer.initialValue;
-}
-
bool Script::executeRoomEnterCommands(const Common::String &roomName,
InteractionResult &result) {
result = InteractionResult();
@@ -697,812 +496,4 @@ bool Script::hasUseItemInteraction(const Common::String &itemName, const ObjectR
return findUseItemRecord(itemName, target) != nullptr;
}
-void Script::getVisibleInventoryObjects(Common::Array<ObjectRecord> &objects) const {
- objects.clear();
- const char *const statusObjectName = resolveInventoryStatusObjectName(_playerCurrentHitPoints);
- const ObjectRecord *statusObject = nullptr;
-
- for (const ObjectRecord &object : _currentObjects) {
- if (object.objectName.hasPrefixIgnoreCase("INV_STAT")) {
- if (object.objectName.equalsIgnoreCase(statusObjectName))
- statusObject = &object;
- continue;
- }
-
- if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) && object.visible)
- objects.push_back(object);
- }
-
- if (!statusObject)
- return;
-
- ObjectRecord activeStatusObject = *statusObject;
- activeStatusObject.currentOwnerOrRoom = kInventoryOwnerName;
- activeStatusObject.visible = true;
- activeStatusObject.runtimeVisible = true;
- activeStatusObject.identShown = true;
- objects.push_back(Common::move(activeStatusObject));
-}
-
-bool Script::isObjectInInventory(const Common::String &objectName) const {
- if (objectName.empty())
- return false;
-
- for (const ObjectRecord &object : _currentObjects) {
- if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) &&
- object.objectName.equalsIgnoreCase(objectName))
- return true;
- }
-
- return false;
-}
-
-void Script::markObjectIdentShown(const ObjectRecord &object) {
- if (ObjectRecord *currentObject = findRuntimeObject(object.currentOwnerOrRoom, object.objectName))
- currentObject->identShown = true;
-}
-
-const EntranceRecord *Script::findEntranceRecord(const Common::String &entranceName) const {
- if (entranceName.empty())
- return nullptr;
-
- for (const EntranceRecord &entrance : _entrances) {
- if (entrance.entranceName.equalsIgnoreCase(entranceName))
- return &entrance;
- }
-
- return nullptr;
-}
-
-const MapEntranceRecord *Script::findMapEntranceRecord(const Common::String &entryName) const {
- if (entryName.empty())
- return nullptr;
-
- for (const MapEntranceRecord &mapEntrance : _mapEntrances) {
- if (mapEntrance.entryName.equalsIgnoreCase(entryName))
- return &mapEntrance;
- }
-
- return nullptr;
-}
-
-const UseItemRecord *Script::findUseItemRecord(const Common::String &itemName,
- const ObjectRecord &target) const {
- if (itemName.empty() || target.objectName.empty())
- return nullptr;
-
- for (const UseItemRecord &useItem : _useItems) {
- if (!useItem.itemName.equalsIgnoreCase(itemName) ||
- !useItem.targetName.equalsIgnoreCase(target.objectName)) {
- continue;
- }
-
- // Native dispatch ignores the parsed owner/group string and matches only
- // the carried item name plus the overlapped target entity name.
- return &useItem;
- }
-
- return nullptr;
-}
-
-const RoomRecord *Script::findRoomRecord(const Common::String &roomName) const {
- if (roomName.empty())
- return nullptr;
-
- for (const RoomRecord &room : _rooms) {
- if (room.roomName.equalsIgnoreCase(roomName))
- return &room;
- }
-
- return nullptr;
-}
-
-const CommandRecord *Script::findCommandRecord(const Common::String &tag) const {
- if (tag.empty())
- return nullptr;
-
- for (const CommandRecord &command : _commands) {
- if (command.triggerTag.equalsIgnoreCase(tag))
- return &command;
- }
-
- return nullptr;
-}
-
-const ExecListRecord *Script::findExecListRecord(const Common::String &name) const {
- if (name.empty())
- return nullptr;
-
- for (const ExecListRecord &execList : _execLists) {
- if (execList.listName.equalsIgnoreCase(name))
- return &execList;
- }
-
- return nullptr;
-}
-
-const FlagRecord *Script::findRuntimeFlag(const Common::String &flagName) const {
- if (flagName.empty())
- return nullptr;
-
- for (const FlagRecord &flag : _currentFlags) {
- if (flag.name.equalsIgnoreCase(flagName))
- return &flag;
- }
-
- return nullptr;
-}
-
-FlagRecord *Script::findRuntimeFlag(const Common::String &flagName) {
- if (flagName.empty())
- return nullptr;
-
- for (FlagRecord &flag : _currentFlags) {
- if (flag.name.equalsIgnoreCase(flagName))
- return &flag;
- }
-
- return nullptr;
-}
-
-ObjectRecord *Script::findRuntimeObject(const Common::String &ownerOrRoom,
- const Common::String &objectName) {
- if (objectName.empty())
- return nullptr;
-
- ObjectRecord *fallback = nullptr;
- for (ObjectRecord &object : _currentObjects) {
- if (!object.objectName.equalsIgnoreCase(objectName))
- continue;
-
- if (ownerOrRoom.empty())
- return &object;
-
- if (object.currentOwnerOrRoom.equalsIgnoreCase(ownerOrRoom) ||
- object.initialOwnerOrRoom.equalsIgnoreCase(ownerOrRoom)) {
- return &object;
- }
-
- if (!fallback)
- fallback = &object;
- }
-
- return fallback;
-}
-
-AnimRecord *Script::findRuntimeAnim(const Common::String &animName) {
- if (animName.empty())
- return nullptr;
-
- for (AnimRecord &anim : _currentAnimations) {
- if (anim.animName.equalsIgnoreCase(animName))
- return &anim;
- }
-
- return nullptr;
-}
-
-RegionRecord *Script::findRuntimeRegion(const Common::String ®ionName) {
- if (regionName.empty())
- return nullptr;
-
- for (RegionRecord ®ion : _currentRegions) {
- if (region.regionName.equalsIgnoreCase(regionName))
- return ®ion;
- }
-
- return nullptr;
-}
-
-NpcRecord *Script::findRuntimeNpc(const Common::String &npcName) {
- if (npcName.empty())
- return nullptr;
-
- for (NpcRecord &npc : _currentNpcs) {
- if (npc.npcName.equalsIgnoreCase(npcName))
- return &npc;
- }
-
- return nullptr;
-}
-
-MonsterRecord *Script::findRuntimeMonster(const Common::String &monsterName) {
- if (monsterName.empty())
- return nullptr;
-
- for (MonsterRecord &monster : _currentMonsters) {
- if (monster.monsterName.equalsIgnoreCase(monsterName))
- return &monster;
- }
-
- return nullptr;
-}
-
-MonsterRecord *Script::findRuntimeMonster(const MonsterRecord &monster) {
- if (monster.monsterName.empty())
- return nullptr;
-
- MonsterRecord *fallback = nullptr;
- for (MonsterRecord &candidate : _currentMonsters) {
- if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
- continue;
- if (matchesMonsterIdentity(candidate, monster))
- return &candidate;
- if (monster.roomName.empty() && monster.recordIndex < 0 && !fallback)
- fallback = &candidate;
- }
-
- return fallback;
-}
-
-TimerRecord *Script::findRuntimeTimer(const Common::String &timerName) {
- if (timerName.empty())
- return nullptr;
-
- for (TimerRecord &timer : _currentTimers) {
- if (timer.timerName.equalsIgnoreCase(timerName))
- return &timer;
- }
-
- return nullptr;
-}
-
-const NpcRecord *Script::findRuntimeNpc(const Common::String &npcName) const {
- if (npcName.empty())
- return nullptr;
-
- for (const NpcRecord &npc : _currentNpcs) {
- if (npc.npcName.equalsIgnoreCase(npcName))
- return &npc;
- }
-
- return nullptr;
-}
-
-const MonsterRecord *Script::findRuntimeMonster(const Common::String &monsterName) const {
- if (monsterName.empty())
- return nullptr;
-
- for (const MonsterRecord &monster : _currentMonsters) {
- if (monster.monsterName.equalsIgnoreCase(monsterName))
- return &monster;
- }
-
- return nullptr;
-}
-
-const MonsterRecord *Script::findRuntimeMonster(const MonsterRecord &monster) const {
- if (monster.monsterName.empty())
- return nullptr;
-
- const MonsterRecord *fallback = nullptr;
- for (const MonsterRecord &candidate : _currentMonsters) {
- if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
- continue;
- if (matchesMonsterIdentity(candidate, monster))
- return &candidate;
- if (monster.roomName.empty() && monster.recordIndex < 0 && !fallback)
- fallback = &candidate;
- }
-
- return fallback;
-}
-
-const MonsterRecord *Script::findBaseMonster(const MonsterRecord &monster) const {
- if (monster.monsterName.empty())
- return nullptr;
-
- const MonsterRecord *fallback = nullptr;
- for (const MonsterRecord &candidate : _monsters) {
- if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
- continue;
- if (matchesMonsterIdentity(candidate, monster))
- return &candidate;
- if (monster.roomName.empty() && monster.recordIndex < 0 && !fallback)
- fallback = &candidate;
- }
-
- return fallback;
-}
-
-const TimerRecord *Script::findRuntimeTimer(const Common::String &timerName) const {
- if (timerName.empty())
- return nullptr;
-
- for (const TimerRecord &timer : _currentTimers) {
- if (timer.timerName.equalsIgnoreCase(timerName))
- return &timer;
- }
-
- return nullptr;
-}
-
-const TimerRecord *Script::findRuntimeTimerRecord(const Common::String &timerName) const {
- return findRuntimeTimer(timerName);
-}
-
-const NpcRecord *Script::findRuntimeNpcRecord(const Common::String &npcName) const {
- return findRuntimeNpc(npcName);
-}
-
-bool Script::addRuntimeObjectToInventory(const Common::String &objectName) {
- ObjectRecord *currentObject = findRuntimeObject(Common::String(), objectName);
- if (!currentObject)
- return false;
-
- const bool changed = !currentObject->currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) ||
- !currentObject->visible || !currentObject->runtimeVisible || !currentObject->identShown;
- currentObject->currentOwnerOrRoom = kInventoryOwnerName;
- currentObject->visible = true;
- currentObject->runtimeVisible = true;
- currentObject->identShown = true;
- return changed;
-}
-
-bool Script::syncRuntimeObjectRecord(const ObjectRecord &object) {
- if (object.objectName.empty())
- return false;
-
- ObjectRecord *currentObject = findRuntimeObject(Common::String(), object.objectName);
- if (!currentObject)
- return false;
-
- const bool changed = currentObject->currentX != object.currentX ||
- currentObject->currentY != object.currentY ||
- currentObject->currentZ != object.currentZ ||
- !currentObject->currentOwnerOrRoom.equalsIgnoreCase(object.currentOwnerOrRoom) ||
- currentObject->visible != object.visible ||
- currentObject->runtimeVisible != object.runtimeVisible ||
- currentObject->identShown != object.identShown;
- *currentObject = object;
- return changed;
-}
-
-bool Script::setRuntimeObjectVisible(const Common::String &ownerOrRoom,
- const Common::String &objectName, bool visible) {
- if (objectName.empty())
- return false;
-
- ObjectRecord *currentObject = findRuntimeObject(ownerOrRoom, objectName);
- if (!currentObject)
- return false;
-
- const bool changed = currentObject->visible != visible ||
- currentObject->runtimeVisible != visible;
- currentObject->visible = visible;
- currentObject->runtimeVisible = visible;
- if (visible && currentObject->currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName))
- currentObject->identShown = true;
-
- return changed;
-}
-
-bool Script::setRuntimeNpcState(const Common::String &npcName, bool active, bool visible) {
- if (npcName.empty())
- return false;
-
- NpcRecord *currentNpc = findRuntimeNpc(npcName);
- if (!currentNpc)
- return false;
-
- const bool changed = currentNpc->active != active || currentNpc->visible != visible;
- currentNpc->active = active;
- currentNpc->visible = visible;
- return changed;
-}
-
-bool Script::setPlayerCurrentHitPoints(int hitPoints) {
- const int clampedHitPoints = clampPlayerHitPoints(hitPoints);
- const bool changed = _playerCurrentHitPoints != clampedHitPoints;
- _playerCurrentHitPoints = clampedHitPoints;
- return changed;
-}
-
-bool Script::adjustPlayerCurrentHitPoints(int delta) {
- return setPlayerCurrentHitPoints(_playerCurrentHitPoints + delta);
-}
-
-int Script::getPlayerCombatResourceCount(int loadout) const {
- const int *count = getPlayerCombatResourceCountPtr(loadout);
- return count ? *count : 0;
-}
-
-bool Script::consumePlayerCombatResourceUnit(int loadout) {
- const int maxCount = resolveCombatResourcePickupMax(loadout);
- if (maxCount <= 0)
- return true;
-
- if (getPlayerCombatResourceCount(loadout) < 1)
- return false;
-
- return adjustPlayerCombatResourceCount(loadout, -1, maxCount, "PLAYER_ATTACK");
-}
-
-bool Script::setPlayerCombatResourceCount(int loadout, int count) {
- int *currentCount = getPlayerCombatResourceCountPtr(loadout);
- const int maxCount = resolveCombatResourceDisplayMax(loadout);
- if (!currentCount || maxCount <= 0)
- return false;
-
- const int clampedCount = CLIP<int>(count, 0, maxCount);
- const bool changed = *currentCount != clampedCount;
- *currentCount = clampedCount;
- return changed;
-}
-
-int *Script::getPlayerCombatResourceCountPtr(int loadout) {
- switch (loadout) {
- case 2:
- return &_nailgunAmmoCount;
- case 3:
- return &_shotgunShellCount;
- case 4:
- return &_nineGunBulletCount;
- case 5:
- return &_thirtyEightGunBulletCount;
- case 14:
- return &_chainsawFuelCount;
- default:
- return nullptr;
- }
-}
-
-const int *Script::getPlayerCombatResourceCountPtr(int loadout) const {
- switch (loadout) {
- case 2:
- return &_nailgunAmmoCount;
- case 3:
- return &_shotgunShellCount;
- case 4:
- return &_nineGunBulletCount;
- case 5:
- return &_thirtyEightGunBulletCount;
- case 14:
- return &_chainsawFuelCount;
- default:
- return nullptr;
- }
-}
-
-bool Script::adjustPlayerCombatResourceCount(int loadout, int delta, int maxCount,
- const Common::String &reason) {
- int *count = getPlayerCombatResourceCountPtr(loadout);
- if (!count || maxCount <= 0)
- return false;
-
- const int oldCount = *count;
- const int newCount = CLIP<int>(oldCount + delta, 0, maxCount);
- *count = newCount;
- debugC(1, kDebugGeneral,
- "Harvester: %s player combat resource loadout=%d('%s') delta=%d count=%d->%d max=%d changed=%d",
- reason.c_str(), loadout, Player::describeCombatLoadout(loadout),
- delta, oldCount, newCount, maxCount, oldCount != newCount);
- return oldCount != newCount;
-}
-
-bool Script::setPlayerCombatLoadout(int loadout) {
- const int clampedLoadout = clampPlayerCombatLoadout(loadout);
- const bool changed = _playerCombatLoadout != clampedLoadout;
- _playerCombatLoadout = clampedLoadout;
- return changed;
-}
-
-bool Script::setPlayerControlPaused(bool paused) {
- const bool changed = _playerControlPaused != paused;
- _playerControlPaused = paused;
- return changed;
-}
-
-bool Script::syncRuntimeAnimState(const Common::String &animName, bool active, bool visible, int currentFrame) {
- AnimRecord *currentAnimation = findRuntimeAnim(animName);
- if (!currentAnimation)
- return false;
-
- const bool changed = currentAnimation->active != active ||
- currentAnimation->visible != visible ||
- currentAnimation->runtimeActive != active ||
- currentAnimation->runtimeVisible != visible ||
- currentAnimation->runtimeState != currentFrame;
- currentAnimation->active = active;
- currentAnimation->visible = visible;
- currentAnimation->runtimeActive = active;
- currentAnimation->runtimeVisible = visible;
- currentAnimation->runtimeState = currentFrame;
- return changed;
-}
-
-bool Script::syncRuntimeMonsterRecord(const MonsterRecord &monster) {
- MonsterRecord *currentMonster = findRuntimeMonster(monster);
- if (!currentMonster)
- return false;
-
- *currentMonster = monster;
- return true;
-}
-
-bool Script::syncRuntimeTimerRecord(const TimerRecord &timer) {
- TimerRecord *currentTimer = findRuntimeTimer(timer.timerName);
- if (!currentTimer)
- return false;
-
- *currentTimer = timer;
- return true;
-}
-
-bool Script::setRuntimeRegionEnabled(const Common::String ®ionName, bool enabled) {
- RegionRecord *currentRegion = findRuntimeRegion(regionName);
- if (!currentRegion)
- return false;
-
- const bool changed = currentRegion->startEnabled != enabled;
- currentRegion->startEnabled = enabled;
- return changed;
-}
-
-bool Script::setRuntimeTimerEnabled(const Common::String &timerName, bool enabled) {
- TimerRecord *currentTimer = findRuntimeTimer(timerName);
- if (!currentTimer)
- return false;
-
- const bool wasEnabled = currentTimer->enabled;
- const bool changed = wasEnabled != enabled ||
- (enabled && !wasEnabled && currentTimer->currentValue != currentTimer->initialValue);
- currentTimer->enabled = enabled;
- if (enabled && !wasEnabled)
- currentTimer->currentValue = currentTimer->initialValue;
- return changed;
-}
-
-bool Script::queueRuntimeNpcDeathOrMonsterfy(const Common::String &npcName, int deathDamageType) {
- if (npcName.empty())
- return false;
-
- NpcRecord *currentNpc = findRuntimeNpc(npcName);
- if (!currentNpc || currentNpc->deathOrMonsterfyFlag)
- return false;
-
- // Native live MONSTERFY queues actor state 0x35 without a damage argument.
- // Use bludge as the queued animation selector when scripts leave it blank.
- const int queuedDeathDamageType = deathDamageType != 0 ? deathDamageType : 1;
- if (currentNpc->deathDamageType == queuedDeathDamageType)
- return false;
-
- currentNpc->deathDamageType = queuedDeathDamageType;
- return true;
-}
-
-bool Script::finalizeRuntimeNpcDeathOrMonsterfy(const Common::String &npcName, int deathDamageType,
- bool preserveCorpse, int corpseFrame, int monsterfyPosZ) {
- if (npcName.empty())
- return false;
-
- NpcRecord *currentNpc = findRuntimeNpc(npcName);
- if (!currentNpc)
- return false;
-
- bool monsterChanged = false;
- if (!currentNpc->monsterfyTargetName.empty()) {
- MonsterRecord monsterLookup;
- monsterLookup.roomName = currentNpc->roomName;
- monsterLookup.monsterName = currentNpc->monsterfyTargetName;
- MonsterRecord *currentMonster = findRuntimeMonster(monsterLookup);
- if (currentMonster) {
- const bool overrideMonsterPosZ = monsterfyPosZ != kNoMonsterfyPosZOverride;
- monsterChanged = !currentMonster->active || !currentMonster->visible ||
- (overrideMonsterPosZ && currentMonster->posZ != monsterfyPosZ);
- if (overrideMonsterPosZ)
- currentMonster->posZ = monsterfyPosZ;
- currentMonster->active = true;
- currentMonster->visible = true;
- currentMonster->runtimeSpawned = false;
- currentMonster->runtimeState = -1;
- if (currentMonster->currentHitPoints <= 0)
- currentMonster->currentHitPoints = currentMonster->initialHitPoints;
- } else {
- debug(1, "Harvester: unresolved monsterfy target for dialogue npc='%s' target='%s'",
- currentNpc->npcName.c_str(), currentNpc->monsterfyTargetName.c_str());
- }
- }
-
- const bool changed = !currentNpc->deathOrMonsterfyFlag ||
- currentNpc->active ||
- currentNpc->visible ||
- currentNpc->runtimeSpawned ||
- currentNpc->runtimeState != (preserveCorpse && corpseFrame >= 0 ? corpseFrame : -1) ||
- (deathDamageType != 0 && currentNpc->deathDamageType != deathDamageType);
- currentNpc->active = false;
- currentNpc->visible = preserveCorpse && corpseFrame >= 0 && currentNpc->monsterfyTargetName.empty();
- currentNpc->savedVisible = currentNpc->visible;
- currentNpc->deathOrMonsterfyFlag = !currentNpc->visible;
- currentNpc->runtimeSpawned = currentNpc->visible;
- currentNpc->runtimeState = currentNpc->visible ? corpseFrame : -1;
- if (deathDamageType != 0)
- currentNpc->deathDamageType = deathDamageType;
- return changed || monsterChanged;
-}
-
-void Script::pushDeferredLiveNpcDeathTransitions() {
- ++_deferredLiveNpcDeathTransitionDepth;
-}
-
-void Script::popDeferredLiveNpcDeathTransitions() {
- if (_deferredLiveNpcDeathTransitionDepth > 0)
- --_deferredLiveNpcDeathTransitionDepth;
-}
-
-const TextRecord *Script::findTextRecord(const Common::String &key) const {
- if (key.empty())
- return nullptr;
-
- for (const TextRecord &textRecord : _texts) {
- if (textRecord.key.equalsIgnoreCase(key))
- return &textRecord;
- }
-
- return nullptr;
-}
-
-bool Script::resolveObjectInspectText(const ObjectRecord &object, ResolvedText &text) const {
- text = ResolvedText();
-
- const TextRecord *textRecord = findTextRecord(object.identTextKey);
- if (!textRecord)
- return false;
-
- text.boxName = textRecord->boxName;
- text.value = textRecord->value;
- return !text.value.empty();
-}
-
-bool Script::resolveTextRecord(const Common::String &key, ResolvedText &text) const {
- text = ResolvedText();
-
- const TextRecord *textRecord = findTextRecord(key);
- if (!textRecord)
- return false;
-
- text.boxName = textRecord->boxName;
- text.value = textRecord->value;
- return !text.value.empty();
-}
-
-Common::String Script::resolveInventoryTooltipText(const ObjectRecord &object) const {
- const TextRecord *textRecord = findTextRecord(object.inventoryTextKey);
- if (textRecord && !textRecord->value.empty())
- return textRecord->value;
-
- return Common::String();
-}
-
-Common::String Script::resolveObjectLabel(const ObjectRecord &object) const {
- if (!object.interactionLabel.empty() && !object.interactionLabel.equalsIgnoreCase("NULL_ID"))
- return normalizeInteractionLabel(object.interactionLabel);
- if (object.interactionLabel.equalsIgnoreCase("NULL_ID"))
- return Common::String();
-
- if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName)) {
- const Common::String inventoryLabel = resolveInventoryTooltipText(object);
- if (!inventoryLabel.empty())
- return inventoryLabel;
- }
-
- const TextRecord *textRecord = findTextRecord(object.identTextKey);
- if (textRecord && !textRecord->value.empty())
- return textRecord->value;
-
- Common::String label = normalizeInteractionLabel(object.objectName);
- if (!label.empty() && !label.equalsIgnoreCase("NULL ID"))
- return label;
-
- return Common::String();
-}
-
-Common::String Script::resolveTextValue(const Common::String &key) const {
- const TextRecord *textRecord = findTextRecord(key);
- if (textRecord && !textRecord->value.empty())
- return textRecord->value;
-
- return normalizeInteractionLabel(key);
-}
-
-const HeadRecord *Script::findHeadRecord(const Common::String &headId) const {
- if (headId.empty())
- return nullptr;
-
- for (const HeadRecord &head : _heads) {
- if (head.headId.equalsIgnoreCase(headId))
- return &head;
- }
-
- return nullptr;
-}
-
-bool Script::getFlagValue(const Common::String &flagName) const {
- const FlagRecord *flag = findRuntimeFlag(flagName);
- return flag && flag->value;
-}
-
-bool Script::setRuntimeFlagValue(const Common::String &flagName, bool value) {
- if (flagName.empty())
- return false;
-
- FlagRecord *flag = findRuntimeFlag(flagName);
- if (flag) {
- const bool changed = flag->value != value;
- const bool oldValue = flag->value;
- flag->value = value;
- debugC(1, kDebugGeneral,
- "Harvester: direct runtime flag '%s' %d -> %d existed=1 changed=%d",
- flagName.c_str(), oldValue, value, changed);
- return changed;
- }
-
- FlagRecord newFlag;
- newFlag.name = flagName;
- newFlag.value = value;
- _currentFlags.push_back(Common::move(newFlag));
- debugC(1, kDebugGeneral,
- "Harvester: direct runtime flag '%s' %d -> %d existed=0 changed=1",
- flagName.c_str(), 0, value);
- return true;
-}
-
-bool Script::resetRuntimeObjectToInitialState(const Common::String &objectName) {
- if (objectName.empty())
- return false;
-
- ObjectRecord *currentObject = findRuntimeObject(Common::String(), objectName);
- if (!currentObject)
- return false;
-
- const ObjectRecord *baseObject = nullptr;
- for (const ObjectRecord &object : _objects) {
- if (object.objectName.equalsIgnoreCase(objectName)) {
- baseObject = &object;
- break;
- }
- }
- if (!baseObject)
- return false;
-
- const bool changed = currentObject->currentX != currentObject->initialX ||
- currentObject->currentY != currentObject->initialY ||
- currentObject->currentZ != currentObject->initialZ ||
- !currentObject->currentOwnerOrRoom.equalsIgnoreCase(currentObject->initialOwnerOrRoom) ||
- currentObject->visible != baseObject->visible ||
- currentObject->runtimeVisible != baseObject->visible ||
- currentObject->identShown != baseObject->identShown;
-
- currentObject->currentX = currentObject->initialX;
- currentObject->currentY = currentObject->initialY;
- currentObject->currentZ = currentObject->initialZ;
- currentObject->currentOwnerOrRoom = currentObject->initialOwnerOrRoom;
- currentObject->visible = baseObject->visible;
- currentObject->runtimeVisible = baseObject->visible;
- currentObject->identShown = baseObject->identShown;
- return changed;
-}
-
-bool Script::isNamedNpcDeathTypeClear(const Common::String &npcName) const {
- const NpcRecord *npc = findRuntimeNpc(npcName);
- return npc && npc->deathDamageType == 0;
-}
-
-int Script::getCurrentStoryDayIndex() const {
- if (getFlagValue("DAY_1"))
- return 1;
- if (getFlagValue("DAY_2") || getFlagValue("NIGHT_2"))
- return 2;
- if (getFlagValue("DAY_3") || getFlagValue("NIGHT_3"))
- return 3;
- if (getFlagValue("DAY_4") || getFlagValue("NIGHT_4"))
- return 4;
- if (getFlagValue("DAY_5") || getFlagValue("NIGHT_5"))
- return 5;
- if (getFlagValue("DAY_6"))
- return 6;
-
- return 0;
-}
-
} // End of namespace Harvester
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index 93825f8ec66..38b53cf8fe8 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -457,6 +457,9 @@ public:
bool setRuntimeTimerEnabled(const Common::String &timerName, bool enabled);
private:
+ static const char *const kInventoryOwnerName;
+ static const int kDefaultPlayerCombatLoadout;
+
struct CommandExecutionContext {
const char *label;
const Common::String &name;
diff --git a/engines/harvester/script_actors.cpp b/engines/harvester/script_actors.cpp
new file mode 100644
index 00000000000..959de57a459
--- /dev/null
+++ b/engines/harvester/script_actors.cpp
@@ -0,0 +1,351 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/script.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "harvester/detection.h"
+#include "harvester/player.h"
+
+namespace Harvester {
+
+static const int kMaxPlayerCombatLoadout = 0x14;
+
+struct InventoryCombatLoadoutEntry {
+ int loadoutId;
+ const char *objectName;
+};
+
+static const InventoryCombatLoadoutEntry kInventoryCombatLoadoutMap[] = {
+ { 1, "CLEAVER" },
+ { 2, "NAILGUN" },
+ { 3, "SHOTGUN" },
+ { 4, "9GUN" },
+ { 5, "38GUN" },
+ { 6, "TOMAHAWK" },
+ { 7, "KNIFE" },
+ { 8, "FLAIL" },
+ { 9, "HANDAXE" },
+ { 10, "WRENCH" },
+ { 11, "PITCHFORK" },
+ { 12, "SCYTHE" },
+ { 13, "SWORD" },
+ { 14, "CHAINSAW" },
+ { 15, "HARVEST_BLADE" },
+ { 16, "SHOVEL" },
+ { 17, "FIREAXE" },
+ { 18, "BAT" },
+ { 19, "RAZOR" },
+ { 20, "POOLSTICK" }
+};
+
+int Script::clampPlayerHitPoints(int hitPoints) {
+ if (hitPoints < 0)
+ return 0;
+ if (hitPoints > Script::kDefaultPlayerHitPoints)
+ return Script::kDefaultPlayerHitPoints;
+
+ return hitPoints;
+}
+
+int Script::clampPlayerCombatLoadout(int loadout) {
+ return CLIP<int>(loadout, 0, kMaxPlayerCombatLoadout);
+}
+
+static int resolveCombatResourceDisplayMax(int loadout) {
+ switch (loadout) {
+ case 2:
+ case 3:
+ case 14:
+ return 16;
+ case 4:
+ return 8;
+ case 5:
+ return 6;
+ default:
+ return 0;
+ }
+}
+
+int Script::clampCombatResourceCount(int loadout, int count) {
+ const int maxCount = resolveCombatResourceDisplayMax(loadout);
+ if (maxCount <= 0)
+ return 0;
+
+ return CLIP<int>(count, 0, maxCount);
+}
+
+int Script::resolveCombatResourcePickupMax(int loadout) {
+ switch (loadout) {
+ case 2:
+ case 3:
+ case 14:
+ return 15;
+ case 4:
+ return 7;
+ case 5:
+ return 5;
+ default:
+ return 0;
+ }
+}
+
+static const char *resolveCombatLoadoutInventoryObjectName(int loadout) {
+ for (const InventoryCombatLoadoutEntry &entry : kInventoryCombatLoadoutMap) {
+ if (entry.loadoutId == loadout)
+ return entry.objectName;
+ }
+
+ return nullptr;
+}
+
+int Script::validatePlayerCombatLoadoutAgainstInventory(const Common::Array<ObjectRecord> ¤tObjects,
+ int loadout, const char *contextLabel) {
+ const int clampedLoadout = clampPlayerCombatLoadout(loadout);
+ if (clampedLoadout == kDefaultPlayerCombatLoadout)
+ return clampedLoadout;
+
+ const char *requiredObjectName = resolveCombatLoadoutInventoryObjectName(clampedLoadout);
+ if (!requiredObjectName)
+ return clampedLoadout;
+
+ for (const ObjectRecord &object : currentObjects) {
+ if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) &&
+ object.objectName.equalsIgnoreCase(requiredObjectName))
+ return clampedLoadout;
+ }
+
+ debugC(1, kDebugGeneral,
+ "Harvester: cleared player combat loadout %d('%s') after %s because inventory object '%s' is absent",
+ clampedLoadout, Player::describeCombatLoadout(clampedLoadout),
+ contextLabel ? contextLabel : "runtime restore", requiredObjectName);
+ return kDefaultPlayerCombatLoadout;
+}
+
+bool Script::setRuntimeNpcState(const Common::String &npcName, bool active, bool visible) {
+ if (npcName.empty())
+ return false;
+
+ NpcRecord *currentNpc = findRuntimeNpc(npcName);
+ if (!currentNpc)
+ return false;
+
+ const bool changed = currentNpc->active != active || currentNpc->visible != visible;
+ currentNpc->active = active;
+ currentNpc->visible = visible;
+ return changed;
+}
+
+bool Script::setPlayerCurrentHitPoints(int hitPoints) {
+ const int clampedHitPoints = clampPlayerHitPoints(hitPoints);
+ const bool changed = _playerCurrentHitPoints != clampedHitPoints;
+ _playerCurrentHitPoints = clampedHitPoints;
+ return changed;
+}
+
+bool Script::adjustPlayerCurrentHitPoints(int delta) {
+ return setPlayerCurrentHitPoints(_playerCurrentHitPoints + delta);
+}
+
+int Script::getPlayerCombatResourceCount(int loadout) const {
+ const int *count = getPlayerCombatResourceCountPtr(loadout);
+ return count ? *count : 0;
+}
+
+bool Script::consumePlayerCombatResourceUnit(int loadout) {
+ const int maxCount = resolveCombatResourcePickupMax(loadout);
+ if (maxCount <= 0)
+ return true;
+
+ if (getPlayerCombatResourceCount(loadout) < 1)
+ return false;
+
+ return adjustPlayerCombatResourceCount(loadout, -1, maxCount, "PLAYER_ATTACK");
+}
+
+bool Script::setPlayerCombatResourceCount(int loadout, int count) {
+ int *currentCount = getPlayerCombatResourceCountPtr(loadout);
+ const int maxCount = resolveCombatResourceDisplayMax(loadout);
+ if (!currentCount || maxCount <= 0)
+ return false;
+
+ const int clampedCount = CLIP<int>(count, 0, maxCount);
+ const bool changed = *currentCount != clampedCount;
+ *currentCount = clampedCount;
+ return changed;
+}
+
+int *Script::getPlayerCombatResourceCountPtr(int loadout) {
+ switch (loadout) {
+ case 2:
+ return &_nailgunAmmoCount;
+ case 3:
+ return &_shotgunShellCount;
+ case 4:
+ return &_nineGunBulletCount;
+ case 5:
+ return &_thirtyEightGunBulletCount;
+ case 14:
+ return &_chainsawFuelCount;
+ default:
+ return nullptr;
+ }
+}
+
+const int *Script::getPlayerCombatResourceCountPtr(int loadout) const {
+ switch (loadout) {
+ case 2:
+ return &_nailgunAmmoCount;
+ case 3:
+ return &_shotgunShellCount;
+ case 4:
+ return &_nineGunBulletCount;
+ case 5:
+ return &_thirtyEightGunBulletCount;
+ case 14:
+ return &_chainsawFuelCount;
+ default:
+ return nullptr;
+ }
+}
+
+bool Script::adjustPlayerCombatResourceCount(int loadout, int delta, int maxCount,
+ const Common::String &reason) {
+ int *count = getPlayerCombatResourceCountPtr(loadout);
+ if (!count || maxCount <= 0)
+ return false;
+
+ const int oldCount = *count;
+ const int newCount = CLIP<int>(oldCount + delta, 0, maxCount);
+ *count = newCount;
+ debugC(1, kDebugGeneral,
+ "Harvester: %s player combat resource loadout=%d('%s') delta=%d count=%d->%d max=%d changed=%d",
+ reason.c_str(), loadout, Player::describeCombatLoadout(loadout),
+ delta, oldCount, newCount, maxCount, oldCount != newCount);
+ return oldCount != newCount;
+}
+
+bool Script::setPlayerCombatLoadout(int loadout) {
+ const int clampedLoadout = clampPlayerCombatLoadout(loadout);
+ const bool changed = _playerCombatLoadout != clampedLoadout;
+ _playerCombatLoadout = clampedLoadout;
+ return changed;
+}
+
+bool Script::setPlayerControlPaused(bool paused) {
+ const bool changed = _playerControlPaused != paused;
+ _playerControlPaused = paused;
+ return changed;
+}
+
+bool Script::syncRuntimeMonsterRecord(const MonsterRecord &monster) {
+ MonsterRecord *currentMonster = findRuntimeMonster(monster);
+ if (!currentMonster)
+ return false;
+
+ *currentMonster = monster;
+ return true;
+}
+
+bool Script::queueRuntimeNpcDeathOrMonsterfy(const Common::String &npcName, int deathDamageType) {
+ if (npcName.empty())
+ return false;
+
+ NpcRecord *currentNpc = findRuntimeNpc(npcName);
+ if (!currentNpc || currentNpc->deathOrMonsterfyFlag)
+ return false;
+
+ // Native live MONSTERFY queues actor state 0x35 without a damage argument.
+ // Use bludge as the queued animation selector when scripts leave it blank.
+ const int queuedDeathDamageType = deathDamageType != 0 ? deathDamageType : 1;
+ if (currentNpc->deathDamageType == queuedDeathDamageType)
+ return false;
+
+ currentNpc->deathDamageType = queuedDeathDamageType;
+ return true;
+}
+
+bool Script::finalizeRuntimeNpcDeathOrMonsterfy(const Common::String &npcName, int deathDamageType,
+ bool preserveCorpse, int corpseFrame, int monsterfyPosZ) {
+ if (npcName.empty())
+ return false;
+
+ NpcRecord *currentNpc = findRuntimeNpc(npcName);
+ if (!currentNpc)
+ return false;
+
+ bool monsterChanged = false;
+ if (!currentNpc->monsterfyTargetName.empty()) {
+ MonsterRecord monsterLookup;
+ monsterLookup.roomName = currentNpc->roomName;
+ monsterLookup.monsterName = currentNpc->monsterfyTargetName;
+ MonsterRecord *currentMonster = findRuntimeMonster(monsterLookup);
+ if (currentMonster) {
+ const bool overrideMonsterPosZ = monsterfyPosZ != kNoMonsterfyPosZOverride;
+ monsterChanged = !currentMonster->active || !currentMonster->visible ||
+ (overrideMonsterPosZ && currentMonster->posZ != monsterfyPosZ);
+ if (overrideMonsterPosZ)
+ currentMonster->posZ = monsterfyPosZ;
+ currentMonster->active = true;
+ currentMonster->visible = true;
+ currentMonster->runtimeSpawned = false;
+ currentMonster->runtimeState = -1;
+ if (currentMonster->currentHitPoints <= 0)
+ currentMonster->currentHitPoints = currentMonster->initialHitPoints;
+ } else {
+ debug(1, "Harvester: unresolved monsterfy target for dialogue npc='%s' target='%s'",
+ currentNpc->npcName.c_str(), currentNpc->monsterfyTargetName.c_str());
+ }
+ }
+
+ const bool changed = !currentNpc->deathOrMonsterfyFlag ||
+ currentNpc->active ||
+ currentNpc->visible ||
+ currentNpc->runtimeSpawned ||
+ currentNpc->runtimeState != (preserveCorpse && corpseFrame >= 0 ? corpseFrame : -1) ||
+ (deathDamageType != 0 && currentNpc->deathDamageType != deathDamageType);
+ currentNpc->active = false;
+ currentNpc->visible = preserveCorpse && corpseFrame >= 0 && currentNpc->monsterfyTargetName.empty();
+ currentNpc->savedVisible = currentNpc->visible;
+ currentNpc->deathOrMonsterfyFlag = !currentNpc->visible;
+ currentNpc->runtimeSpawned = currentNpc->visible;
+ currentNpc->runtimeState = currentNpc->visible ? corpseFrame : -1;
+ if (deathDamageType != 0)
+ currentNpc->deathDamageType = deathDamageType;
+ return changed || monsterChanged;
+}
+
+void Script::pushDeferredLiveNpcDeathTransitions() {
+ ++_deferredLiveNpcDeathTransitionDepth;
+}
+
+void Script::popDeferredLiveNpcDeathTransitions() {
+ if (_deferredLiveNpcDeathTransitionDepth > 0)
+ --_deferredLiveNpcDeathTransitionDepth;
+}
+
+bool Script::isNamedNpcDeathTypeClear(const Common::String &npcName) const {
+ const NpcRecord *npc = findRuntimeNpc(npcName);
+ return npc && npc->deathDamageType == 0;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/script_runtime.cpp b/engines/harvester/script_runtime.cpp
new file mode 100644
index 00000000000..70d529703d9
--- /dev/null
+++ b/engines/harvester/script_runtime.cpp
@@ -0,0 +1,719 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/script.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "harvester/detection.h"
+
+namespace Harvester {
+
+const char *const Script::kInventoryOwnerName = "INVENTORY";
+const int Script::kDefaultPlayerCombatLoadout = 0;
+
+static bool matchesMonsterIdentity(const MonsterRecord &candidate, const MonsterRecord &monster) {
+ if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
+ return false;
+
+ if (candidate.recordIndex >= 0 && monster.recordIndex >= 0)
+ return candidate.recordIndex == monster.recordIndex;
+
+ if (!monster.roomName.empty())
+ return candidate.roomName.equalsIgnoreCase(monster.roomName);
+
+ return true;
+}
+
+static const char *resolveInventoryStatusObjectName(int hitPoints) {
+ if (hitPoints < 8)
+ return "INV_STAT4";
+ if (hitPoints < 15)
+ return "INV_STAT3";
+ if (hitPoints < 23)
+ return "INV_STAT2";
+
+ return "INV_STAT1";
+}
+
+static Common::String normalizeInteractionLabel(const Common::String &value) {
+ Common::String label = value;
+ for (uint i = 0; i < label.size(); ++i) {
+ if (label[i] == '_')
+ label.setChar(' ', i);
+ }
+
+ return label;
+}
+
+void Script::resetRuntimeState() {
+ _currentFlags = _flags;
+ _currentObjects = _objects;
+ _currentAnimations = _animations;
+ _currentRegions = _regions;
+ _currentNpcs = _npcs;
+ _currentMonsters = _monsters;
+ _currentTimers = _timers;
+ _nailgunAmmoCount = 0;
+ _shotgunShellCount = 0;
+ _nineGunBulletCount = 0;
+ _thirtyEightGunBulletCount = 0;
+ _chainsawFuelCount = 0;
+ _playerCurrentHitPoints = Script::kDefaultPlayerHitPoints;
+ _playerCombatLoadout = kDefaultPlayerCombatLoadout;
+ _playerControlPaused = false;
+
+ for (ObjectRecord &object : _currentObjects) {
+ object.currentX = object.initialX;
+ object.currentY = object.initialY;
+ object.currentZ = object.initialZ;
+ object.currentOwnerOrRoom = object.initialOwnerOrRoom;
+ object.runtimeVisible = object.visible;
+ object.identShown = object.identTextKey.empty();
+ }
+
+ for (AnimRecord &anim : _currentAnimations) {
+ anim.runtimeActive = anim.active;
+ anim.runtimeVisible = anim.visible;
+ anim.runtimeState = -1;
+ }
+
+ for (NpcRecord &npc : _currentNpcs) {
+ npc.runtimeSpawned = false;
+ npc.runtimeState = -1;
+ npc.savedVisible = npc.visible;
+ }
+
+ for (MonsterRecord &monster : _currentMonsters) {
+ monster.currentHitPoints = monster.initialHitPoints;
+ monster.runtimeSpawned = false;
+ monster.runtimeState = -1;
+ monster.savedVisible = monster.visible;
+ if (monster.active)
+ monster.visible = true;
+ }
+
+ for (TimerRecord &timer : _currentTimers)
+ timer.currentValue = timer.initialValue;
+}
+
+void Script::getVisibleInventoryObjects(Common::Array<ObjectRecord> &objects) const {
+ objects.clear();
+ const char *const statusObjectName = resolveInventoryStatusObjectName(_playerCurrentHitPoints);
+ const ObjectRecord *statusObject = nullptr;
+
+ for (const ObjectRecord &object : _currentObjects) {
+ if (object.objectName.hasPrefixIgnoreCase("INV_STAT")) {
+ if (object.objectName.equalsIgnoreCase(statusObjectName))
+ statusObject = &object;
+ continue;
+ }
+
+ if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) && object.visible)
+ objects.push_back(object);
+ }
+
+ if (!statusObject)
+ return;
+
+ ObjectRecord activeStatusObject = *statusObject;
+ activeStatusObject.currentOwnerOrRoom = kInventoryOwnerName;
+ activeStatusObject.visible = true;
+ activeStatusObject.runtimeVisible = true;
+ activeStatusObject.identShown = true;
+ objects.push_back(Common::move(activeStatusObject));
+}
+
+bool Script::isObjectInInventory(const Common::String &objectName) const {
+ if (objectName.empty())
+ return false;
+
+ for (const ObjectRecord &object : _currentObjects) {
+ if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) &&
+ object.objectName.equalsIgnoreCase(objectName))
+ return true;
+ }
+
+ return false;
+}
+
+void Script::markObjectIdentShown(const ObjectRecord &object) {
+ if (ObjectRecord *currentObject = findRuntimeObject(object.currentOwnerOrRoom, object.objectName))
+ currentObject->identShown = true;
+}
+
+const EntranceRecord *Script::findEntranceRecord(const Common::String &entranceName) const {
+ if (entranceName.empty())
+ return nullptr;
+
+ for (const EntranceRecord &entrance : _entrances) {
+ if (entrance.entranceName.equalsIgnoreCase(entranceName))
+ return &entrance;
+ }
+
+ return nullptr;
+}
+
+const MapEntranceRecord *Script::findMapEntranceRecord(const Common::String &entryName) const {
+ if (entryName.empty())
+ return nullptr;
+
+ for (const MapEntranceRecord &mapEntrance : _mapEntrances) {
+ if (mapEntrance.entryName.equalsIgnoreCase(entryName))
+ return &mapEntrance;
+ }
+
+ return nullptr;
+}
+
+const UseItemRecord *Script::findUseItemRecord(const Common::String &itemName,
+ const ObjectRecord &target) const {
+ if (itemName.empty() || target.objectName.empty())
+ return nullptr;
+
+ for (const UseItemRecord &useItem : _useItems) {
+ if (!useItem.itemName.equalsIgnoreCase(itemName) ||
+ !useItem.targetName.equalsIgnoreCase(target.objectName)) {
+ continue;
+ }
+
+ // Native dispatch ignores the parsed owner/group string and matches only
+ // the carried item name plus the overlapped target entity name.
+ return &useItem;
+ }
+
+ return nullptr;
+}
+
+const RoomRecord *Script::findRoomRecord(const Common::String &roomName) const {
+ if (roomName.empty())
+ return nullptr;
+
+ for (const RoomRecord &room : _rooms) {
+ if (room.roomName.equalsIgnoreCase(roomName))
+ return &room;
+ }
+
+ return nullptr;
+}
+
+const CommandRecord *Script::findCommandRecord(const Common::String &tag) const {
+ if (tag.empty())
+ return nullptr;
+
+ for (const CommandRecord &command : _commands) {
+ if (command.triggerTag.equalsIgnoreCase(tag))
+ return &command;
+ }
+
+ return nullptr;
+}
+
+const ExecListRecord *Script::findExecListRecord(const Common::String &name) const {
+ if (name.empty())
+ return nullptr;
+
+ for (const ExecListRecord &execList : _execLists) {
+ if (execList.listName.equalsIgnoreCase(name))
+ return &execList;
+ }
+
+ return nullptr;
+}
+
+const FlagRecord *Script::findRuntimeFlag(const Common::String &flagName) const {
+ if (flagName.empty())
+ return nullptr;
+
+ for (const FlagRecord &flag : _currentFlags) {
+ if (flag.name.equalsIgnoreCase(flagName))
+ return &flag;
+ }
+
+ return nullptr;
+}
+
+FlagRecord *Script::findRuntimeFlag(const Common::String &flagName) {
+ if (flagName.empty())
+ return nullptr;
+
+ for (FlagRecord &flag : _currentFlags) {
+ if (flag.name.equalsIgnoreCase(flagName))
+ return &flag;
+ }
+
+ return nullptr;
+}
+
+ObjectRecord *Script::findRuntimeObject(const Common::String &ownerOrRoom,
+ const Common::String &objectName) {
+ if (objectName.empty())
+ return nullptr;
+
+ ObjectRecord *fallback = nullptr;
+ for (ObjectRecord &object : _currentObjects) {
+ if (!object.objectName.equalsIgnoreCase(objectName))
+ continue;
+
+ if (ownerOrRoom.empty())
+ return &object;
+
+ if (object.currentOwnerOrRoom.equalsIgnoreCase(ownerOrRoom) ||
+ object.initialOwnerOrRoom.equalsIgnoreCase(ownerOrRoom)) {
+ return &object;
+ }
+
+ if (!fallback)
+ fallback = &object;
+ }
+
+ return fallback;
+}
+
+AnimRecord *Script::findRuntimeAnim(const Common::String &animName) {
+ if (animName.empty())
+ return nullptr;
+
+ for (AnimRecord &anim : _currentAnimations) {
+ if (anim.animName.equalsIgnoreCase(animName))
+ return &anim;
+ }
+
+ return nullptr;
+}
+
+RegionRecord *Script::findRuntimeRegion(const Common::String ®ionName) {
+ if (regionName.empty())
+ return nullptr;
+
+ for (RegionRecord ®ion : _currentRegions) {
+ if (region.regionName.equalsIgnoreCase(regionName))
+ return ®ion;
+ }
+
+ return nullptr;
+}
+
+NpcRecord *Script::findRuntimeNpc(const Common::String &npcName) {
+ if (npcName.empty())
+ return nullptr;
+
+ for (NpcRecord &npc : _currentNpcs) {
+ if (npc.npcName.equalsIgnoreCase(npcName))
+ return &npc;
+ }
+
+ return nullptr;
+}
+
+MonsterRecord *Script::findRuntimeMonster(const Common::String &monsterName) {
+ if (monsterName.empty())
+ return nullptr;
+
+ for (MonsterRecord &monster : _currentMonsters) {
+ if (monster.monsterName.equalsIgnoreCase(monsterName))
+ return &monster;
+ }
+
+ return nullptr;
+}
+
+MonsterRecord *Script::findRuntimeMonster(const MonsterRecord &monster) {
+ if (monster.monsterName.empty())
+ return nullptr;
+
+ MonsterRecord *fallback = nullptr;
+ for (MonsterRecord &candidate : _currentMonsters) {
+ if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
+ continue;
+ if (matchesMonsterIdentity(candidate, monster))
+ return &candidate;
+ if (monster.roomName.empty() && monster.recordIndex < 0 && !fallback)
+ fallback = &candidate;
+ }
+
+ return fallback;
+}
+
+TimerRecord *Script::findRuntimeTimer(const Common::String &timerName) {
+ if (timerName.empty())
+ return nullptr;
+
+ for (TimerRecord &timer : _currentTimers) {
+ if (timer.timerName.equalsIgnoreCase(timerName))
+ return &timer;
+ }
+
+ return nullptr;
+}
+
+const NpcRecord *Script::findRuntimeNpc(const Common::String &npcName) const {
+ if (npcName.empty())
+ return nullptr;
+
+ for (const NpcRecord &npc : _currentNpcs) {
+ if (npc.npcName.equalsIgnoreCase(npcName))
+ return &npc;
+ }
+
+ return nullptr;
+}
+
+const MonsterRecord *Script::findRuntimeMonster(const Common::String &monsterName) const {
+ if (monsterName.empty())
+ return nullptr;
+
+ for (const MonsterRecord &monster : _currentMonsters) {
+ if (monster.monsterName.equalsIgnoreCase(monsterName))
+ return &monster;
+ }
+
+ return nullptr;
+}
+
+const MonsterRecord *Script::findRuntimeMonster(const MonsterRecord &monster) const {
+ if (monster.monsterName.empty())
+ return nullptr;
+
+ const MonsterRecord *fallback = nullptr;
+ for (const MonsterRecord &candidate : _currentMonsters) {
+ if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
+ continue;
+ if (matchesMonsterIdentity(candidate, monster))
+ return &candidate;
+ if (monster.roomName.empty() && monster.recordIndex < 0 && !fallback)
+ fallback = &candidate;
+ }
+
+ return fallback;
+}
+
+const MonsterRecord *Script::findBaseMonster(const MonsterRecord &monster) const {
+ if (monster.monsterName.empty())
+ return nullptr;
+
+ const MonsterRecord *fallback = nullptr;
+ for (const MonsterRecord &candidate : _monsters) {
+ if (!candidate.monsterName.equalsIgnoreCase(monster.monsterName))
+ continue;
+ if (matchesMonsterIdentity(candidate, monster))
+ return &candidate;
+ if (monster.roomName.empty() && monster.recordIndex < 0 && !fallback)
+ fallback = &candidate;
+ }
+
+ return fallback;
+}
+
+const TimerRecord *Script::findRuntimeTimer(const Common::String &timerName) const {
+ if (timerName.empty())
+ return nullptr;
+
+ for (const TimerRecord &timer : _currentTimers) {
+ if (timer.timerName.equalsIgnoreCase(timerName))
+ return &timer;
+ }
+
+ return nullptr;
+}
+
+const TimerRecord *Script::findRuntimeTimerRecord(const Common::String &timerName) const {
+ return findRuntimeTimer(timerName);
+}
+
+const NpcRecord *Script::findRuntimeNpcRecord(const Common::String &npcName) const {
+ return findRuntimeNpc(npcName);
+}
+
+bool Script::addRuntimeObjectToInventory(const Common::String &objectName) {
+ ObjectRecord *currentObject = findRuntimeObject(Common::String(), objectName);
+ if (!currentObject)
+ return false;
+
+ const bool changed = !currentObject->currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName) ||
+ !currentObject->visible || !currentObject->runtimeVisible || !currentObject->identShown;
+ currentObject->currentOwnerOrRoom = kInventoryOwnerName;
+ currentObject->visible = true;
+ currentObject->runtimeVisible = true;
+ currentObject->identShown = true;
+ return changed;
+}
+
+bool Script::syncRuntimeObjectRecord(const ObjectRecord &object) {
+ if (object.objectName.empty())
+ return false;
+
+ ObjectRecord *currentObject = findRuntimeObject(Common::String(), object.objectName);
+ if (!currentObject)
+ return false;
+
+ const bool changed = currentObject->currentX != object.currentX ||
+ currentObject->currentY != object.currentY ||
+ currentObject->currentZ != object.currentZ ||
+ !currentObject->currentOwnerOrRoom.equalsIgnoreCase(object.currentOwnerOrRoom) ||
+ currentObject->visible != object.visible ||
+ currentObject->runtimeVisible != object.runtimeVisible ||
+ currentObject->identShown != object.identShown;
+ *currentObject = object;
+ return changed;
+}
+
+bool Script::setRuntimeObjectVisible(const Common::String &ownerOrRoom,
+ const Common::String &objectName, bool visible) {
+ if (objectName.empty())
+ return false;
+
+ ObjectRecord *currentObject = findRuntimeObject(ownerOrRoom, objectName);
+ if (!currentObject)
+ return false;
+
+ const bool changed = currentObject->visible != visible ||
+ currentObject->runtimeVisible != visible;
+ currentObject->visible = visible;
+ currentObject->runtimeVisible = visible;
+ if (visible && currentObject->currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName))
+ currentObject->identShown = true;
+
+ return changed;
+}
+
+bool Script::syncRuntimeAnimState(const Common::String &animName, bool active, bool visible, int currentFrame) {
+ AnimRecord *currentAnimation = findRuntimeAnim(animName);
+ if (!currentAnimation)
+ return false;
+
+ const bool changed = currentAnimation->active != active ||
+ currentAnimation->visible != visible ||
+ currentAnimation->runtimeActive != active ||
+ currentAnimation->runtimeVisible != visible ||
+ currentAnimation->runtimeState != currentFrame;
+ currentAnimation->active = active;
+ currentAnimation->visible = visible;
+ currentAnimation->runtimeActive = active;
+ currentAnimation->runtimeVisible = visible;
+ currentAnimation->runtimeState = currentFrame;
+ return changed;
+}
+
+bool Script::syncRuntimeTimerRecord(const TimerRecord &timer) {
+ TimerRecord *currentTimer = findRuntimeTimer(timer.timerName);
+ if (!currentTimer)
+ return false;
+
+ *currentTimer = timer;
+ return true;
+}
+
+bool Script::setRuntimeRegionEnabled(const Common::String ®ionName, bool enabled) {
+ RegionRecord *currentRegion = findRuntimeRegion(regionName);
+ if (!currentRegion)
+ return false;
+
+ const bool changed = currentRegion->startEnabled != enabled;
+ currentRegion->startEnabled = enabled;
+ return changed;
+}
+
+bool Script::setRuntimeTimerEnabled(const Common::String &timerName, bool enabled) {
+ TimerRecord *currentTimer = findRuntimeTimer(timerName);
+ if (!currentTimer)
+ return false;
+
+ const bool wasEnabled = currentTimer->enabled;
+ const bool changed = wasEnabled != enabled ||
+ (enabled && !wasEnabled && currentTimer->currentValue != currentTimer->initialValue);
+ currentTimer->enabled = enabled;
+ if (enabled && !wasEnabled)
+ currentTimer->currentValue = currentTimer->initialValue;
+ return changed;
+}
+
+const TextRecord *Script::findTextRecord(const Common::String &key) const {
+ if (key.empty())
+ return nullptr;
+
+ for (const TextRecord &textRecord : _texts) {
+ if (textRecord.key.equalsIgnoreCase(key))
+ return &textRecord;
+ }
+
+ return nullptr;
+}
+
+bool Script::resolveObjectInspectText(const ObjectRecord &object, ResolvedText &text) const {
+ text = ResolvedText();
+
+ const TextRecord *textRecord = findTextRecord(object.identTextKey);
+ if (!textRecord)
+ return false;
+
+ text.boxName = textRecord->boxName;
+ text.value = textRecord->value;
+ return !text.value.empty();
+}
+
+bool Script::resolveTextRecord(const Common::String &key, ResolvedText &text) const {
+ text = ResolvedText();
+
+ const TextRecord *textRecord = findTextRecord(key);
+ if (!textRecord)
+ return false;
+
+ text.boxName = textRecord->boxName;
+ text.value = textRecord->value;
+ return !text.value.empty();
+}
+
+Common::String Script::resolveInventoryTooltipText(const ObjectRecord &object) const {
+ const TextRecord *textRecord = findTextRecord(object.inventoryTextKey);
+ if (textRecord && !textRecord->value.empty())
+ return textRecord->value;
+
+ return Common::String();
+}
+
+Common::String Script::resolveObjectLabel(const ObjectRecord &object) const {
+ if (!object.interactionLabel.empty() && !object.interactionLabel.equalsIgnoreCase("NULL_ID"))
+ return normalizeInteractionLabel(object.interactionLabel);
+ if (object.interactionLabel.equalsIgnoreCase("NULL_ID"))
+ return Common::String();
+
+ if (object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName)) {
+ const Common::String inventoryLabel = resolveInventoryTooltipText(object);
+ if (!inventoryLabel.empty())
+ return inventoryLabel;
+ }
+
+ const TextRecord *textRecord = findTextRecord(object.identTextKey);
+ if (textRecord && !textRecord->value.empty())
+ return textRecord->value;
+
+ Common::String label = normalizeInteractionLabel(object.objectName);
+ if (!label.empty() && !label.equalsIgnoreCase("NULL ID"))
+ return label;
+
+ return Common::String();
+}
+
+Common::String Script::resolveTextValue(const Common::String &key) const {
+ const TextRecord *textRecord = findTextRecord(key);
+ if (textRecord && !textRecord->value.empty())
+ return textRecord->value;
+
+ return normalizeInteractionLabel(key);
+}
+
+const HeadRecord *Script::findHeadRecord(const Common::String &headId) const {
+ if (headId.empty())
+ return nullptr;
+
+ for (const HeadRecord &head : _heads) {
+ if (head.headId.equalsIgnoreCase(headId))
+ return &head;
+ }
+
+ return nullptr;
+}
+
+bool Script::getFlagValue(const Common::String &flagName) const {
+ const FlagRecord *flag = findRuntimeFlag(flagName);
+ return flag && flag->value;
+}
+
+bool Script::setRuntimeFlagValue(const Common::String &flagName, bool value) {
+ if (flagName.empty())
+ return false;
+
+ FlagRecord *flag = findRuntimeFlag(flagName);
+ if (flag) {
+ const bool changed = flag->value != value;
+ const bool oldValue = flag->value;
+ flag->value = value;
+ debugC(1, kDebugGeneral,
+ "Harvester: direct runtime flag '%s' %d -> %d existed=1 changed=%d",
+ flagName.c_str(), oldValue, value, changed);
+ return changed;
+ }
+
+ FlagRecord newFlag;
+ newFlag.name = flagName;
+ newFlag.value = value;
+ _currentFlags.push_back(Common::move(newFlag));
+ debugC(1, kDebugGeneral,
+ "Harvester: direct runtime flag '%s' %d -> %d existed=0 changed=1",
+ flagName.c_str(), 0, value);
+ return true;
+}
+
+bool Script::resetRuntimeObjectToInitialState(const Common::String &objectName) {
+ if (objectName.empty())
+ return false;
+
+ ObjectRecord *currentObject = findRuntimeObject(Common::String(), objectName);
+ if (!currentObject)
+ return false;
+
+ const ObjectRecord *baseObject = nullptr;
+ for (const ObjectRecord &object : _objects) {
+ if (object.objectName.equalsIgnoreCase(objectName)) {
+ baseObject = &object;
+ break;
+ }
+ }
+ if (!baseObject)
+ return false;
+
+ const bool changed = currentObject->currentX != currentObject->initialX ||
+ currentObject->currentY != currentObject->initialY ||
+ currentObject->currentZ != currentObject->initialZ ||
+ !currentObject->currentOwnerOrRoom.equalsIgnoreCase(currentObject->initialOwnerOrRoom) ||
+ currentObject->visible != baseObject->visible ||
+ currentObject->runtimeVisible != baseObject->visible ||
+ currentObject->identShown != baseObject->identShown;
+
+ currentObject->currentX = currentObject->initialX;
+ currentObject->currentY = currentObject->initialY;
+ currentObject->currentZ = currentObject->initialZ;
+ currentObject->currentOwnerOrRoom = currentObject->initialOwnerOrRoom;
+ currentObject->visible = baseObject->visible;
+ currentObject->runtimeVisible = baseObject->visible;
+ currentObject->identShown = baseObject->identShown;
+ return changed;
+}
+
+int Script::getCurrentStoryDayIndex() const {
+ if (getFlagValue("DAY_1"))
+ return 1;
+ if (getFlagValue("DAY_2") || getFlagValue("NIGHT_2"))
+ return 2;
+ if (getFlagValue("DAY_3") || getFlagValue("NIGHT_3"))
+ return 3;
+ if (getFlagValue("DAY_4") || getFlagValue("NIGHT_4"))
+ return 4;
+ if (getFlagValue("DAY_5") || getFlagValue("NIGHT_5"))
+ return 5;
+ if (getFlagValue("DAY_6"))
+ return 6;
+
+ return 0;
+}
+
+} // End of namespace Harvester
Commit: 32b820a74757b39ef9f04691757996f6cd0d00e5
https://github.com/scummvm/scummvm/commit/32b820a74757b39ef9f04691757996f6cd0d00e5
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Show IDENT text before non-operatable object actions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/input.cpp
engines/harvester/input.h
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index f296ff6175c..4aa4bca1928 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -341,6 +341,8 @@ Input::Input(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &sc
}
void Input::beginInspectText(const ObjectRecord &object, const ResolvedText &text) {
+ _pendingInspectObjectName.clear();
+ _inspectDismissPressed = false;
_presentationState.inspectText = text;
_presentationState.showingInspectText = true;
_presentationState.inspectCanDismiss = false;
@@ -350,6 +352,38 @@ void Input::beginInspectText(const ObjectRecord &object, const ResolvedText &tex
_presentationState.inspectText.boxName.c_str(), _presentationState.inspectText.value.c_str());
}
+InputResult Input::finishInspectText(bool &needsRedraw) {
+ _inspectDismissPressed = false;
+ _presentationState.showingInspectText = false;
+ _presentationState.inspectCanDismiss = false;
+ _presentationState.inspectText = ResolvedText();
+ needsRedraw = true;
+ const Common::String objectName = _pendingInspectObjectName;
+ _pendingInspectObjectName.clear();
+ if (objectName.empty())
+ return InputResult();
+
+ const ObjectRecord *object = SceneEntities::findSceneObjectByName(_scene.sceneObjects, objectName);
+ InteractionResult interaction;
+ if (!object || !_engine.getScript()->resolveObjectInteraction(*object, interaction, _scene.executionContext()))
+ return InputResult();
+
+ return applyObjectInteraction(interaction, needsRedraw);
+}
+
+InputResult Input::applyObjectInteraction(InteractionResult &interaction, bool &needsRedraw) {
+ bool didTransition = false;
+ Common::Error interactionError =
+ _interactionProcessor.handleInteractionResult(
+ interaction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(interactionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return InputResult::leaveRoom(Common::kNoError);
+ needsRedraw = true;
+ return InputResult();
+}
+
void Input::queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw) {
if (message.empty()) {
const bool hadMessage = !_presentationState.combatLoadoutStatusText.value.empty();
@@ -569,12 +603,8 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
}
if (_presentationState.showingInspectText) {
- if (_presentationState.inspectCanDismiss) {
- _presentationState.showingInspectText = false;
- _presentationState.inspectCanDismiss = false;
- _presentationState.inspectText = ResolvedText();
- needsRedraw = true;
- }
+ if (_presentationState.inspectCanDismiss)
+ _inspectDismissPressed = true;
return InputResult();
}
if (_session.isPlayerCombatLocked())
@@ -763,7 +793,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
_scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
_scene.state.roomMaxZScreenY, _scene.state.roomMinZScreenY,
_scene.state.roomMinZ, _scene.state.roomMaxZ,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerState.centerX, playerState.bottomY, static_cast<double>(playerState.z),
playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
}
player.setMoveTargetFromScreenPoint(_scene.state, _flow.getMousePos().x, _flow.getMousePos().y);
@@ -771,7 +801,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
debugC(1, kDebugPathfinding,
"Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
_scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
+ playerState.targetX, playerState.targetBottomY, static_cast<double>(playerState.targetZ));
}
needsRedraw = true;
}
@@ -809,6 +839,16 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
needsRedraw = true;
return InputResult();
}
+ // An IDENT can be shown before a non-operatable object's action, without setting identShown. Dispatch after dismissal.
+ if (!clickedObject->identShown && !suppressesInitialObjectInspectGate(*clickedObject) &&
+ !unlocksAfterInitialExamine && canShowInspectText) {
+ beginInspectText(*clickedObject, resolvedInspectText);
+ if (!clickedObject->actionTag.empty())
+ _pendingInspectObjectName = clickedObject->objectName;
+ player.cancelMovement();
+ needsRedraw = true;
+ return InputResult();
+ }
if (_engine.getScript()->isPickupObject(*clickedObject)) {
Common::Error pickupError = _roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
_interactionProcessor, _session, needsRedraw);
@@ -830,16 +870,7 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
return InputResult();
}
- bool didTransition = false;
- Common::Error interactionError =
- _interactionProcessor.handleInteractionResult(
- interaction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(interactionError);
- if (_flow.hasPendingMainMenuReturn())
- return InputResult::leaveRoom(Common::kNoError);
- needsRedraw = true;
- return InputResult();
+ return applyObjectInteraction(interaction, needsRedraw);
}
InputResult Input::handleGameAction(GameAction action, bool pressed, bool &needsRedraw) {
@@ -906,12 +937,8 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw,
inputState.speedUpActive = gameAction && key.keycode == Common::KEYCODE_LALT;
if (_presentationState.showingInspectText) {
- if (key.keycode == Common::KEYCODE_ESCAPE) {
- _presentationState.showingInspectText = false;
- _presentationState.inspectCanDismiss = false;
- _presentationState.inspectText = ResolvedText();
- needsRedraw = true;
- }
+ if (key.keycode == Common::KEYCODE_ESCAPE)
+ _inspectDismissPressed = true;
return InputResult();
}
@@ -1073,6 +1100,16 @@ InputResult Input::handleEvent(const Common::Event &event, bool &needsRedraw) {
if (_flow.handleSystemEvent(event, result))
return InputResult::leaveRoom(result);
+ if (_presentationState.showingInspectText && _inspectDismissPressed) {
+ if (event.type == Common::EVENT_LBUTTONUP ||
+ (event.type == Common::EVENT_KEYUP && event.kbd.keycode == Common::KEYCODE_ESCAPE) ||
+ (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END && event.customType == kActionMenu)) {
+ if (event.type == Common::EVENT_LBUTTONUP)
+ handleLeftButtonUp();
+ return finishInspectText(needsRedraw);
+ }
+ }
+
switch (event.type) {
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
diff --git a/engines/harvester/input.h b/engines/harvester/input.h
index 9865e564217..136a47eae03 100644
--- a/engines/harvester/input.h
+++ b/engines/harvester/input.h
@@ -26,6 +26,7 @@
#include "common/error.h"
#include "common/events.h"
#include "common/rect.h"
+#include "common/str.h"
namespace Harvester {
@@ -38,6 +39,7 @@ class InventorySystem;
class Interaction;
class ItemInteractions;
class Scene;
+struct InteractionResult;
struct MenuTextConfig;
struct NpcRecord;
struct ObjectRecord;
@@ -129,6 +131,8 @@ private:
void handleKeyUp(const Common::KeyState &key, bool gameAction = false);
InputResult handleGameAction(GameAction action, bool pressed, bool &needsRedraw);
void beginInspectText(const ObjectRecord &object, const ResolvedText &text);
+ InputResult finishInspectText(bool &needsRedraw);
+ InputResult applyObjectInteraction(InteractionResult &interaction, bool &needsRedraw);
void queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw);
bool openInventoryOverlay();
void capturePlayerAttackTarget();
@@ -144,6 +148,8 @@ private:
RoomPresentationState &_presentationState;
const Art &_art;
bool _canExitCloseupToParent;
+ Common::String _pendingInspectObjectName;
+ bool _inspectDismissPressed = false;
};
} // End of namespace Harvester
Commit: a8a8ddc7c202f84f952ddfcb0a0c3aadf7181627
https://github.com/scummvm/scummvm/commit/a8a8ddc7c202f84f952ddfcb0a0c3aadf7181627
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Separate menu presentation and text configuration
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/menu_config.cpp
A engines/harvester/menu_render.cpp
A engines/harvester/menu_render.h
engines/harvester/menu.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index e2653e448c2..1b3328d345b 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -20,12 +20,11 @@
*/
#include "harvester/menu.h"
+#include "harvester/menu_render.h"
#include "common/algorithm.h"
#include "common/config-manager.h"
#include "common/events.h"
-#include "common/formats/ini-file.h"
-#include "common/memstream.h"
#include "common/system.h"
#include "engines/metaengine.h"
#include "graphics/font.h"
@@ -47,37 +46,6 @@ namespace Harvester {
namespace {
-static const int kInventoryX = 64;
-static const int kInventoryY = 48;
-static const int kLogoX = 160;
-static const int kLogoY = 0;
-static const int kMenuStartY = 100;
-static const int kMenuLineSpacing = 28;
-
-static const byte kTextColorNormal = 255;
-static const byte kTextColorHover = 251;
-static const byte kTextColorDim = 248;
-static const byte kShadowColor = 0;
-static const byte kPanelFillColor = 1;
-static const byte kQuickTipActionColor = 0xc3;
-
-static const char *const kMenuPath = "MENU.INI";
-static const char *const kMenuSectionName = "menu";
-
-struct MenuWeekdayEntry {
- const char *key;
- const char *englishFallback;
-};
-
-static const MenuWeekdayEntry kMenuWeekdays[] = {
- { "monday", "Monday" },
- { "tuesday", "Tuesday" },
- { "wednesday", "Wednesday" },
- { "thursday", "Thursday" },
- { "friday", "Friday" },
- { "saturday", "Saturday" }
-};
-
static const char *const kOptionsVolumeBitmapPath = "1:/GRAPHIC/OTHER/VOLUME.BM";
static const char *const kOptionsIndicatorBitmapPath = "1:/GRAPHIC/OTHER/INDICATR.BM";
static const char *const kOptionsPreviewSoundPath = "1:/SOUND/EFFECTS/WHIP2.WAV";
@@ -88,47 +56,9 @@ static const char *const kSaveGamePalettePath = "1:/GRAPHIC/PAL/SAVEGAME.PAL";
static const char *const kGameOverBitmapPath = "1:/GRAPHIC/OTHER/GAMEOVER.BM";
static const char *const kGameOverPalettePath = "1:/GRAPHIC/PAL/GAMEOVER.PAL";
static const char *const kGameOverMusicPath = "SOUND/MUSIC/ANXIETY.CMP";
-
-static const int kOptionsSliderMinX = 0x16f;
static const int kOptionsSliderMaxX = 0x22b;
-static const int kOptionsSliderStep = 0x13;
-static const int kOptionsVolumeBitmapX = 0x168;
-static const int kOptionsVolumeBitmapY = 0x68;
-static const int kOptionsIndicatorY = 0x6c;
static const int kOptionsSliderHeight = 0x1e;
-static const int kOptionsItemCount = 7;
static const int kStartupOptionMaxLevel = 9;
-
-// The first three strings retain the padding from the European executable's
-// fixed-width table so their centered text sits underneath the volume bars.
-static const char *const kEuropeanBuiltInOptionItems[kOptionsItemCount] = {
- " SOUND FX ",
- " MUSIC ",
- " GAMMA ",
- "TEXT",
- "GORE",
- "QUICK TIPS",
- "PASSWORD"
-};
-
-static const int kQuickTipsOverlayX = 167;
-static const int kQuickTipsOverlayY = 200;
-static const int kQuickTipsHeaderY = 202;
-static const int kQuickTipTextX = 180;
-static const int kQuickTipTextY = 228;
-static const int kEnglishQuickTipTextWidth = 280;
-static const int kLocalizedQuickTipTextWidth = 300;
-static const int kLocalizedQuickTipActionY = 308;
-static const int kQuickTipExitX = 180;
-static const int kQuickTipToggleX = 258;
-static const int kQuickTipNextRightInset = 8;
-static const int kConfirmDialogX = 167;
-static const int kConfirmDialogY = 200;
-static const int kConfirmPromptTextX = 0xea;
-static const int kConfirmPromptTextY = 0xd6;
-static const int kConfirmChoiceTextY = 0xf8;
-static const int kConfirmYesTextX = 0xdc;
-static const int kConfirmNoTextX = 0x17c;
static const int kSaveSlotCount = 25;
static const int kSaveSlotLabelX = 8;
static const int kSaveSlotNameX = 0x50;
@@ -137,15 +67,13 @@ static const int kSaveSlotStride = 0x11;
static const int kSaveSlotTextWidth = 0x27b - kSaveSlotNameX;
static const int kSaveStatusTextY = 0x1b8;
static const int kSaveSlotMaxCharacters = 0xf6;
-static const int kPasswordEntryX = 0xdc;
-static const int kPasswordEntryY = 0xdc;
-static const int kPasswordEntryWidth = 0x226;
static const int kPasswordMaxCharacters = 8;
static const uint32 kPasswordCursorBlinkMs = 480;
static const uint32 kPaletteFadeTickMs = 4;
static const float kPaletteFadeStep = 0.1f;
static const char *const kCdChangePromptPalettePath = "1:/GRAPHIC/PAL/CD1.PAL";
static const float kPaletteBrightnessBlack = 0.0f;
+static const char *const kBlankMenuSlot = " ";
class ScopedSceneTimerPause {
public:
@@ -179,9 +107,6 @@ private:
bool _paused = false;
};
-static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y);
-static int getNativeRoomMenuLineHeight(const Graphics::Font &selectedFont);
-
static int clampStartupOptionLevel(int level) {
if (level < 0)
return 0;
@@ -213,167 +138,18 @@ static Common::Rect quitConfirmNoRect() {
}
static Common::Rect optionsSliderRect(int sliderIndex, int lineHeight) {
- const int y = kOptionsIndicatorY + sliderIndex * lineHeight;
- return Common::Rect(kOptionsSliderMinX, y, kOptionsSliderMaxX + 1, y + kOptionsSliderHeight + 1);
+ const int y = MenuRendering::kOptionsIndicatorY + sliderIndex * lineHeight;
+ return Common::Rect(MenuRendering::kOptionsSliderMinX, y, kOptionsSliderMaxX + 1, y + kOptionsSliderHeight + 1);
}
static int resolveOptionsSliderIndexFromMouseX(int mouseX) {
- const int relativeX = mouseX - kOptionsSliderMinX;
- return clampStartupOptionLevel((relativeX + (kOptionsSliderStep / 2)) / kOptionsSliderStep);
-}
-
-static bool loadRawMenuValue(const Common::Array<byte> &data, const char *key, Common::String &value) {
- value.clear();
- if (!key)
- return false;
-
- Common::String line;
- for (uint i = 0; i <= data.size(); ++i) {
- const bool atEnd = i == data.size();
- const byte chr = atEnd ? '\n' : data[i];
- if (chr == '\r')
- continue;
-
- if (chr != '\n') {
- line += (char)chr;
- continue;
- }
-
- const char *separator = strchr(line.c_str(), '=');
- if (separator) {
- const Common::String currentKey(line.c_str(), separator);
- if (currentKey.equalsIgnoreCase(key)) {
- value = Common::String(separator + 1);
- return true;
- }
- }
-
- line.clear();
- }
-
- return false;
-}
-
-static void loadMenuDisplayValue(Common::INIFile &menu, const char *key, Common::String &dest) {
- Common::String value;
- if (!menu.getKey(key, kMenuSectionName, value) || value.empty())
- return;
-
- for (uint i = 0; i < value.size(); ++i) {
- if (value[i] == '_')
- value.setChar(' ', i);
- }
- dest = Common::move(value);
-}
-
-static void applyEuropeanBuiltInMenuText(MenuTextConfig &config) {
- config.yesLabel = "Yes";
- config.noLabel = "No";
- config.clickLabel = "Click";
- config.newGamePrompt = "START A NEW GAME?";
- config.quitGamePrompt = " QUIT HARVESTER?";
- for (uint i = 0; i < config.optionItems.size(); ++i)
- config.optionItems[i] = kEuropeanBuiltInOptionItems[i];
+ const int relativeX = mouseX - MenuRendering::kOptionsSliderMinX;
+ return clampStartupOptionLevel(
+ (relativeX + (MenuRendering::kOptionsSliderStep / 2)) / MenuRendering::kOptionsSliderStep);
}
} // End of anonymous namespace
-bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
- config = MenuTextConfig();
- if (engine.isDemo()) {
- config.clickLabel = "Click";
- config.newGamePrompt = "START A NEW GAME?";
- config.quitGamePrompt = " QUIT HARVESTER?";
- }
- config.optionItems.resize(kOptionsItemCount);
- config.optionItems[0] = "SOUND FX";
- config.optionItems[1] = "MUSIC";
- config.optionItems[2] = "GAMMA";
- config.optionItems[3] = "TEXT";
- config.optionItems[4] = "GORE";
- config.optionItems[5] = "QUICK TIPS";
- config.optionItems[6] = "PASSWORD";
- config.weekdayLabels.resize(ARRAYSIZE(kMenuWeekdays));
- for (uint i = 0; i < config.weekdayLabels.size(); ++i)
- config.weekdayLabels[i] = kMenuWeekdays[i].englishFallback;
-
- ResourceManager *resources = engine.getResources();
- if (!resources)
- return false;
-
- Common::Array<byte> data;
- if (!resources->loadFile(kMenuPath, data)) {
- if (!engine.isDemo())
- applyEuropeanBuiltInMenuText(config);
- debugC(2, kDebugGeneral,
- "Harvester: using built-in menu text because '%s' is unavailable", kMenuPath);
- return true;
- }
-
- Common::MemoryReadStream stream(data.data(), data.size());
- Common::INIFile menu;
- menu.setDefaultSectionName(kMenuSectionName);
- menu.requireKeyValueDelimiter();
- menu.suppressValuelessLineWarning();
- if (!menu.loadFromStream(stream))
- return false;
-
- for (uint i = 0; i < config.optionItems.size(); ++i) {
- const Common::String key = Common::String::format("options_menu_%u", i + 1);
- Common::String value;
- if (loadRawMenuValue(data, key.c_str(), value) && !value.empty())
- config.optionItems[i] = Common::move(value);
- else if (menu.getKey(key, kMenuSectionName, value) && !value.empty())
- config.optionItems[i] = Common::move(value);
- }
-
- Common::String value;
- if (menu.getKey("yes", kMenuSectionName, value) && !value.empty())
- config.yesLabel = Common::move(value);
- if (menu.getKey("no", kMenuSectionName, value) && !value.empty())
- config.noLabel = Common::move(value);
- if (menu.getKey("click", kMenuSectionName, value) && !value.empty())
- config.clickLabel = Common::move(value);
- loadMenuDisplayValue(menu, "on", config.onLabel);
- loadMenuDisplayValue(menu, "off", config.offLabel);
- loadMenuDisplayValue(menu, "enter_password", config.enterPasswordLabel);
- if (loadRawMenuValue(data, "newgame", value) && !value.empty())
- config.newGamePrompt = Common::move(value);
- if (loadRawMenuValue(data, "quitgame", value) && !value.empty())
- config.quitGamePrompt = Common::move(value);
- loadMenuDisplayValue(menu, "talk_to", config.talkToVerb);
- loadMenuDisplayValue(menu, "examine", config.examineVerb);
- loadMenuDisplayValue(menu, "examine_the", config.examineTheVerb);
- loadMenuDisplayValue(menu, "operate", config.operateVerb);
- loadMenuDisplayValue(menu, "pick_up", config.pickUpVerb);
- loadMenuDisplayValue(menu, "use", config.useVerb);
- loadMenuDisplayValue(menu, "use_on", config.useOnPreposition);
- loadMenuDisplayValue(menu, "other", config.dialogueOtherLabel);
- loadMenuDisplayValue(menu, "responses", config.dialogueResponsesLabel);
- loadMenuDisplayValue(menu, "keyword", config.dialogueKeywordLabel);
- for (uint i = 0; i < config.weekdayLabels.size(); ++i)
- loadMenuDisplayValue(menu, kMenuWeekdays[i].key, config.weekdayLabels[i]);
- loadMenuDisplayValue(menu, "Exit", config.quickTipsExitLabel);
- loadMenuDisplayValue(menu, "next", config.quickTipsNextLabel);
- loadMenuDisplayValue(menu, "show_tips_on", config.quickTipsOnLabel);
- loadMenuDisplayValue(menu, "show_tips_off", config.quickTipsOffLabel);
- loadMenuDisplayValue(menu, "quick_tips_header", config.quickTipsHeader);
-
- return true;
-}
-
-Common::String buildUseItemPrompt(const MenuTextConfig &config,
- const Common::String &itemLabel, const Common::String &targetLabel) {
- if (itemLabel.empty())
- return Common::String();
- if (targetLabel.empty())
- return Common::String::format("%s %s %s",
- config.useVerb.c_str(), itemLabel.c_str(), config.useOnPreposition.c_str());
-
- return Common::String::format("%s %s %s %s", config.useVerb.c_str(), itemLabel.c_str(),
- config.useOnPreposition.c_str(), targetLabel.c_str());
-}
-
namespace {
enum MainMenuItem {
@@ -385,8 +161,6 @@ enum MainMenuItem {
kMainMenuItemQuitGame
};
-static const char *const kBlankMenuSlot = " ";
-
static void buildDisplayMainMenuItems(const Common::Array<Common::String> &source,
bool canSaveGame, bool canLoadGame, Common::Array<Common::String> &dest) {
dest = source;
@@ -413,262 +187,20 @@ static bool loadPaletteResource(ResourceManager &resources, const Common::String
return true;
}
-static void applyMenuPalette(Graphics::Screen &screen, const HarvesterEngine &engine,
- const byte *palette, float brightness) {
- if (!palette)
- return;
-
- byte displayPalette[256 * 3];
- buildHarvesterDisplayPalette(palette, brightness * engine.getGammaBrightnessScale(), displayPalette);
- screen.setPalette(displayPalette);
-}
-
-static void renderCdChangePromptScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap,
- const byte *palette) {
- Graphics::Screen *screen = engine.getScreen();
- if (!screen)
- return;
-
- applyMenuPalette(*screen, engine, palette, 1.0f);
- screen->fillRect(screen->getBounds(), 0);
- if (bitmap.isValid())
- screen->copyRectToSurface(bitmap.pixels.data(), bitmap.width, 0, 0, bitmap.width, bitmap.height);
- if (engine.getRuntimeEntities())
- engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-static void renderHelpScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap, const byte *palette) {
- Graphics::Screen *screen = engine.getScreen();
- if (!screen)
- return;
-
- applyMenuPalette(*screen, engine, palette, 1.0f);
- screen->fillRect(screen->getBounds(), 0);
- blitBitmap(*screen, bitmap, 0, 0);
- if (engine.getRuntimeEntities())
- engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-static Common::String buildTextModeSuffix(const Script &script, const MenuTextConfig &config) {
- switch (script.getDialogueTextMode()) {
- case kStartupDialogueTextNone:
- return Common::String::format(" - %s", config.noLabel.c_str());
- case kStartupDialogueTextClick:
- return Common::String::format(" - %s", config.clickLabel.c_str());
- case kStartupDialogueTextYes:
- default:
- return Common::String::format(" - %s", config.yesLabel.c_str());
- }
-}
-
-static Common::String buildOptionsMenuItemLabel(const Script &script,
- const MenuTextConfig &config, int index) {
- if (index < 0 || index >= (int)config.optionItems.size())
- return Common::String();
-
- switch (index) {
- case 3:
- return config.optionItems[index] + buildTextModeSuffix(script, config);
- case 4:
- return Common::String::format("%s - %s", config.optionItems[index].c_str(),
- (script.isGoreEnabled() ? config.onLabel : config.offLabel).c_str());
- case 6:
- return Common::String::format("%s - %s", config.optionItems[index].c_str(),
- (script.getParentalPassword().empty() ? config.offLabel : config.onLabel).c_str());
- default:
- return config.optionItems[index];
- }
-}
-
static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
return Common::String::format("GAME %02d", slotIndex + 1);
}
-static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
- if (!bitmap.isValid())
- return;
-
- screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
-}
-
-static void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
- int x, int y, int width, byte color, Graphics::TextAlign align = Graphics::kTextAlignLeft) {
- font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
- font.drawString(&screen, text, x, y, width, color, align);
-}
-
-static void drawSinglePassString(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::String &text, int x, int y) {
- if (text.empty())
- return;
-
- font.drawString(&screen, text, x, y, font.getStringWidth(text), 0);
-}
-
-static void drawWrappedShadowedText(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::String &text, int x, int y, int width, byte color) {
- Common::Array<Common::String> lines;
- font.wordWrapText(text, width, lines);
- for (uint i = 0; i < lines.size(); ++i)
- drawShadowedString(screen, font, lines[i], x, y + (int)i * (font.getFontHeight() + 2), width, color);
-}
-
-static void splitMenuConfigLines(const Common::String &text, Common::Array<Common::String> &lines) {
- lines.clear();
- Common::String currentLine;
- for (uint i = 0; i < text.size(); ++i) {
- if (text[i] == '/') {
- lines.push_back(Common::move(currentLine));
- currentLine.clear();
- continue;
- }
- currentLine += text[i];
- }
- lines.push_back(Common::move(currentLine));
-}
-
-static void renderOptionsMenuScreen(HarvesterEngine &engine, const IndexedBitmap &backdrop,
- const byte *palette, float paletteBrightness,
- const Graphics::Font &selectedFont, const Graphics::Font &unselectedFont,
- const Art &art, const MenuTextConfig &config,
- const IndexedBitmap &volumeBar, const IndexedBitmap &indicator, int selectedItem,
- bool drawCursor = true) {
- Graphics::Screen *screen = engine.getScreen();
- Script *script = engine.getScript();
- if (!screen || !script)
- return;
-
- applyMenuPalette(*screen, engine, palette, paletteBrightness);
- blitBitmap(*screen, backdrop, 0, 0);
- blitTransparentBitmap(*screen, art.getLogoBitmap(), kLogoX, kLogoY);
-
- const int lineHeight = getNativeRoomMenuLineHeight(selectedFont);
- for (int i = 0; i < (int)config.optionItems.size(); ++i) {
- const Common::String label = buildOptionsMenuItemLabel(*script, config, i);
- const Graphics::Font &font = (i == selectedItem) ? selectedFont : unselectedFont;
- const int width = font.getStringWidth(label);
- const int x = (screen->w - width) / 2;
- const int y = kMenuStartY + i * lineHeight;
- font.drawString(screen, label, x, y, width, 0);
- }
-
- // Native run_main_menu spawns the option text entities before adding the
- // VOLUME/INDICATR bitmap entities, so the bars clip the trailing overlap on
- // the first three rows instead of the text painting over them.
- for (int i = 0; i < 3; ++i)
- blitTransparentBitmap(*screen, volumeBar, kOptionsVolumeBitmapX, kOptionsVolumeBitmapY + i * lineHeight);
-
- const int levels[3] = {
- script->getFxVolumeLevel(),
- script->getMusicVolumeLevel(),
- script->getGammaLevel()
- };
- for (int i = 0; i < 3; ++i) {
- blitTransparentBitmap(*screen, indicator, kOptionsSliderMinX + levels[i] * kOptionsSliderStep,
- kOptionsIndicatorY + i * lineHeight);
- }
-
- if (drawCursor && engine.getRuntimeEntities())
- engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-static void renderPasswordPromptScreen(HarvesterEngine &engine, const IndexedBitmap &backdrop,
- const byte *palette, float paletteBrightness, const Graphics::Font &titleFont,
- const Graphics::Font &entryFont, const Art &art, const Common::String &title,
- const Common::String &text, bool cursorVisible, bool drawLogo) {
- Graphics::Screen *screen = engine.getScreen();
- if (!screen)
- return;
-
- if (palette)
- applyMenuPalette(*screen, engine, palette, paletteBrightness);
- blitBitmap(*screen, backdrop, 0, 0);
- if (drawLogo)
- blitTransparentBitmap(*screen, art.getLogoBitmap(), kLogoX, kLogoY);
-
- const int titleWidth = titleFont.getStringWidth(title);
- const int titleX = (screen->w - titleWidth) / 2;
- titleFont.drawString(screen, title, titleX, 0xa0, titleWidth, 0);
-
- Common::String displayText = text;
- if (cursorVisible)
- displayText += '~';
- entryFont.drawString(screen, displayText, kPasswordEntryX, kPasswordEntryY,
- kPasswordEntryWidth, 0);
-
- screen->makeAllDirty();
- screen->update();
-}
-
-static void renderQuickTipsOverlay(HarvesterEngine &engine, const IndexedBitmap &backdrop,
- const byte *palette, float paletteBrightness,
- const MenuTextConfig &config, const QuickTipsLayout &layout,
- const Common::String &tipText) {
- Graphics::Screen *screen = engine.getScreen();
- const Art *art = engine.getArt();
- if (!screen || !art)
- return;
-
- applyMenuPalette(*screen, engine, palette, paletteBrightness);
- blitBitmap(*screen, backdrop, 0, 0);
- blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
- drawQuickTipsPanel(engine, config, layout, tipText);
-
- if (engine.getRuntimeEntities())
- engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-static void renderConfirmPromptScreen(HarvesterEngine &engine, const IndexedBitmap &backdrop,
- const byte *palette, float paletteBrightness, const Graphics::Font &promptFont,
- const Graphics::Font &yesFont, const Graphics::Font &noFont,
- const IndexedBitmap &textbox, const Common::String &promptText,
- const MenuTextConfig &config) {
- Graphics::Screen *screen = engine.getScreen();
- const Art *art = engine.getArt();
- if (!screen || !art)
- return;
-
- applyMenuPalette(*screen, engine, palette, paletteBrightness);
- blitBitmap(*screen, backdrop, 0, 0);
- blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
- blitTransparentBitmap(*screen, textbox, kConfirmDialogX, kConfirmDialogY);
- Common::Array<Common::String> promptLines;
- splitMenuConfigLines(promptText, promptLines);
- for (uint i = 0; i < promptLines.size(); ++i) {
- drawSinglePassString(*screen, promptFont, promptLines[i], kConfirmPromptTextX,
- kConfirmPromptTextY + (int)i * (promptFont.getFontHeight() + 2));
- }
- drawSinglePassString(*screen, yesFont, config.yesLabel, kConfirmYesTextX, kConfirmChoiceTextY);
- drawSinglePassString(*screen, noFont, config.noLabel, kConfirmNoTextX, kConfirmChoiceTextY);
-
- if (engine.getRuntimeEntities())
- engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-static int getNativeRoomMenuLineHeight(const Graphics::Font &selectedFont) {
- return selectedFont.getFontHeight() + 2;
-}
-
static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFont, uint itemCount,
const Common::Point &mousePos) {
if (itemCount == 0)
return -1;
- int selection = mousePos.y - kMenuStartY;
+ int selection = mousePos.y - MenuRendering::kMenuStartY;
if (selection < 1)
selection = 1;
- selection /= MAX<int>(1, getNativeRoomMenuLineHeight(selectedFont));
+ selection /= MAX<int>(1, MenuRendering::getNativeRoomMenuLineHeight(selectedFont));
if (selection >= (int)itemCount)
selection = (int)itemCount - 1;
@@ -677,77 +209,6 @@ static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFon
} // End of anonymous namespace
-bool resolveQuickTipsLayout(HarvesterEngine &engine, const MenuTextConfig &config,
- QuickTipsLayout &layout) {
- const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- const Art *art = engine.getArt();
- const Script *script = engine.getScript();
- if (!font || !art || !script)
- return false;
-
- if (!config.hasQuickTipsHeader()) {
- layout.exitRect = Common::Rect(180, 280, 238, 291);
- layout.nextRect = Common::Rect(420, 280, 492, 291);
- layout.toggleRect = Common::Rect(258, 280, 366, 291);
- return true;
- }
-
- const IndexedBitmap *panel = art->getQuickTipsTextboxBitmap();
- if (!panel || !panel->isValid())
- return false;
-
- const Common::String &toggleLabel = script->isQuickTipsEnabled()
- ? config.quickTipsOnLabel : config.quickTipsOffLabel;
- auto makeLabelRect = [font](const Common::String &label, int x) {
- const int width = MAX<int>(1, font->getStringWidth(label));
- const int height = MAX<int>(1, font->getFontHeight());
- return Common::Rect(x, kLocalizedQuickTipActionY,
- x + width, kLocalizedQuickTipActionY + height);
- };
-
- layout.exitRect = makeLabelRect(config.quickTipsExitLabel, kQuickTipExitX);
- layout.toggleRect = makeLabelRect(toggleLabel, kQuickTipToggleX);
- const int nextX = kQuickTipsOverlayX + (int)panel->width
- - font->getStringWidth(config.quickTipsNextLabel) - kQuickTipNextRightInset;
- layout.nextRect = makeLabelRect(config.quickTipsNextLabel, nextX);
- return true;
-}
-
-void drawQuickTipsPanel(HarvesterEngine &engine, const MenuTextConfig &config,
- const QuickTipsLayout &layout, const Common::String &tipText) {
- Graphics::Screen *screen = engine.getScreen();
- const Art *art = engine.getArt();
- const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- const Script *script = engine.getScript();
- if (!screen || !art || !font || !script)
- return;
-
- const IndexedBitmap *panel = config.hasQuickTipsHeader()
- ? art->getQuickTipsTextboxBitmap() : &art->getTipsBitmap();
- if (!panel || !panel->isValid())
- return;
-
- blitBitmap(*screen, *panel, kQuickTipsOverlayX, kQuickTipsOverlayY);
- if (config.hasQuickTipsHeader()) {
- drawShadowedString(*screen, *font, config.quickTipsHeader,
- kQuickTipsOverlayX, kQuickTipsHeaderY, panel->width,
- kQuickTipActionColor, Graphics::kTextAlignCenter);
- }
-
- const int tipTextWidth = config.hasQuickTipsHeader()
- ? kLocalizedQuickTipTextWidth : kEnglishQuickTipTextWidth;
- drawWrappedShadowedText(*screen, *font, tipText,
- kQuickTipTextX, kQuickTipTextY, tipTextWidth, kTextColorNormal);
- const Common::String &toggleLabel = script->isQuickTipsEnabled()
- ? config.quickTipsOnLabel : config.quickTipsOffLabel;
- drawShadowedString(*screen, *font, config.quickTipsExitLabel,
- layout.exitRect.left, layout.exitRect.top, layout.exitRect.width(), kQuickTipActionColor);
- drawShadowedString(*screen, *font, config.quickTipsNextLabel,
- layout.nextRect.left, layout.nextRect.top, layout.nextRect.width(), kQuickTipActionColor);
- drawShadowedString(*screen, *font, toggleLabel,
- layout.toggleRect.left, layout.toggleRect.top, layout.toggleRect.width(), kQuickTipActionColor);
-}
-
MenuSystem::MenuSystem(HarvesterEngine &engine, Common::Point &mousePos,
const Common::Array<Common::String> &menuItems)
: _engine(engine), _mousePos(mousePos), _menuItems(menuItems) {
@@ -788,7 +249,7 @@ Common::Error MenuSystem::showCdChangePrompt(int discNumber, ResourceManager &re
while (!_engine.shouldQuit() && !activateRequestedDisc) {
if (needsRedraw) {
- renderCdChangePromptScreen(_engine, promptBitmap, promptPalette);
+ MenuRendering::renderCdChangePromptScreen(_engine, promptBitmap, promptPalette);
needsRedraw = false;
}
@@ -1075,9 +536,9 @@ Common::Error MenuSystem::showGameOverBackdrop(Flow &flow) {
Graphics::FrameLimiter limiter(g_system, 60);
while (!_engine.shouldQuit()) {
if (needsRedraw) {
- applyMenuPalette(*screen, _engine, _mainMenuBackdropPalette, 1.0f);
+ MenuRendering::applyMenuPalette(*screen, _engine, _mainMenuBackdropPalette, 1.0f);
screen->fillRect(screen->getBounds(), 0);
- blitBitmap(*screen, _mainMenuBackdrop, 0, 0);
+ MenuRendering::blitBitmap(*screen, _mainMenuBackdrop, 0, 0);
if (_engine.getRuntimeEntities())
_engine.getRuntimeEntities()->drawCursor(*screen);
screen->makeAllDirty();
@@ -1350,8 +811,8 @@ void MenuSystem::renderSaveSlotMenu(const SaveSlotMenu &menu, const Common::Stri
if (!screen)
return;
- applyMenuPalette(*screen, _engine, menu.palette, 1.0f);
- blitBitmap(*screen, menu.background, 0, 0);
+ MenuRendering::applyMenuPalette(*screen, _engine, menu.palette, 1.0f);
+ MenuRendering::blitBitmap(*screen, menu.background, 0, 0);
for (int i = 0; i < kSaveSlotCount; ++i) {
const Graphics::Font &labelFont = (i == menu.activeSlot) ? menu.slotNameFont : menu.slotLabelFont;
@@ -1367,8 +828,9 @@ void MenuSystem::renderSaveSlotMenu(const SaveSlotMenu &menu, const Common::Stri
}
if (!editingText && !menu.statusMessage.empty()) {
- drawShadowedString(*screen, menu.slotNameFont, menu.statusMessage, 20, kSaveStatusTextY,
- screen->w - 40, kTextColorNormal, Graphics::kTextAlignCenter);
+ MenuRendering::drawShadowedString(*screen, menu.slotNameFont, menu.statusMessage,
+ MenuRendering::TextLayout(20, kSaveStatusTextY, screen->w - 40),
+ MenuRendering::kTextColorNormal, Graphics::kTextAlignCenter);
}
if (_engine.getRuntimeEntities())
@@ -1638,8 +1100,8 @@ Common::Error MenuSystem::restoreRoomBackdropAfterSave(const IndexedBitmap &back
}
screen->fillRect(screen->getBounds(), 0);
- blitBitmap(*screen, backdrop, 0, 0);
- applyMenuPalette(*screen, _engine, palette, kPaletteBrightnessBlack);
+ MenuRendering::blitBitmap(*screen, backdrop, 0, 0);
+ MenuRendering::applyMenuPalette(*screen, _engine, palette, kPaletteBrightnessBlack);
screen->makeAllDirty();
screen->update();
@@ -1679,7 +1141,7 @@ Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const
: static_cast<const Graphics::Font &>(choiceFont);
const Graphics::Font &noFont = hoverNo ? static_cast<const Graphics::Font &>(promptFont)
: static_cast<const Graphics::Font &>(choiceFont);
- renderConfirmPromptScreen(_engine, backdrop, palette, paletteBrightness,
+ MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderConfirmPrompt(
promptFont, yesFont, noFont, *textbox, promptText, config);
needsRedraw = false;
}
@@ -1771,9 +1233,9 @@ Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const
while (!_engine.shouldQuit()) {
if (needsRedraw) {
- renderPasswordPromptScreen(_engine, backdrop, palette, paletteBrightness,
- titleFont, entryFont, *art, flow.getMenuTextConfig().enterPasswordLabel,
- password, cursorVisible, drawLogo);
+ MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderPasswordPrompt(
+ MenuRendering::FontPair(titleFont, entryFont), *art,
+ flow.getMenuTextConfig().enterPasswordLabel, password, cursorVisible, drawLogo);
needsRedraw = false;
}
@@ -1808,7 +1270,7 @@ Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const
}
if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
needsRedraw |= appendTextEntryCharacter(password, entryFont, event.kbd.ascii,
- kPasswordMaxCharacters, kPasswordEntryWidth);
+ kPasswordMaxCharacters, MenuRendering::kPasswordEntryWidth);
}
break;
default:
@@ -1891,7 +1353,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
return Common::kReadingFailed;
}
- const int lineHeight = getNativeRoomMenuLineHeight(selectedFont);
+ const int lineHeight = MenuRendering::getNativeRoomMenuLineHeight(selectedFont);
int selectedItem = 0;
int capturedSlider = -1;
bool showingQuickTips = false;
@@ -2030,11 +1492,12 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
while (!_engine.shouldQuit()) {
if (needsRedraw) {
if (showingQuickTips) {
- renderQuickTipsOverlay(_engine, backdrop, palette, paletteBrightness,
+ MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderQuickTipsOverlay(
config, quickTipsLayout, flow.getQuickTip(quickTipIndex));
} else {
- renderOptionsMenuScreen(_engine, backdrop, palette, paletteBrightness,
- selectedFont, unselectedFont, *art, config, volumeBar, indicator, selectedItem);
+ MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderOptionsMenu(
+ MenuRendering::FontPair(selectedFont, unselectedFont), *art, config,
+ MenuRendering::SliderBitmaps(volumeBar, indicator), selectedItem);
}
needsRedraw = false;
}
@@ -2145,14 +1608,14 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
break;
case Common::KEYCODE_LEFT:
if (selectedItem < 3) {
- _mousePos.x = MAX<int>(kOptionsSliderMinX, _mousePos.x - kOptionsSliderStep);
+ _mousePos.x = MAX<int>(MenuRendering::kOptionsSliderMinX, _mousePos.x - MenuRendering::kOptionsSliderStep);
updateSlider(selectedItem);
needsRedraw = true;
}
break;
case Common::KEYCODE_RIGHT:
if (selectedItem < 3) {
- _mousePos.x = MIN<int>(kOptionsSliderMaxX, _mousePos.x + kOptionsSliderStep);
+ _mousePos.x = MIN<int>(kOptionsSliderMaxX, _mousePos.x + MenuRendering::kOptionsSliderStep);
updateSlider(selectedItem);
needsRedraw = true;
}
@@ -2225,7 +1688,7 @@ Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBright
while (!_engine.shouldQuit()) {
if (needsRedraw) {
- renderHelpScreen(_engine, page == 0 ? mouseHelp : keysHelp,
+ MenuRendering::renderHelpScreen(_engine, page == 0 ? mouseHelp : keysHelp,
page == 0 ? mouseHelpPalette : keysHelpPalette);
needsRedraw = false;
}
@@ -2242,7 +1705,7 @@ Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBright
break;
case Common::EVENT_RBUTTONDOWN:
if (Graphics::Screen *screen = _engine.getScreen()) {
- applyMenuPalette(*screen, _engine, palette, paletteBrightness);
+ MenuRendering::applyMenuPalette(*screen, _engine, palette, paletteBrightness);
screen->fillRect(screen->getBounds(), 0);
}
return Common::kNoError;
@@ -2253,7 +1716,7 @@ Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBright
case Common::EVENT_KEYDOWN:
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (Graphics::Screen *screen = _engine.getScreen()) {
- applyMenuPalette(*screen, _engine, palette, paletteBrightness);
+ MenuRendering::applyMenuPalette(*screen, _engine, palette, paletteBrightness);
screen->fillRect(screen->getBounds(), 0);
}
return Common::kNoError;
@@ -2277,85 +1740,6 @@ Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBright
return Common::kNoError;
}
-void MenuSystem::renderMainMenuScreen(int selectedItem, const Common::String &statusMessage,
- bool drawCursor, const Common::Array<Common::String> &menuItems) const {
- Graphics::Screen *screen = _engine.getScreen();
- const Art *art = _engine.getArt();
- const Graphics::Font *titleFont = FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);
- const Graphics::Font *bodyFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- if (!screen || !art || !titleFont || !bodyFont)
- return;
-
- byte displayPalette[256 * 3];
- buildHarvesterDisplayPalette(art->getWaitPalette(), 1.0f, displayPalette);
- screen->setPalette(displayPalette);
- screen->fillRect(screen->getBounds(), 0);
- blitBitmap(*screen, art->getInventoryBitmap(), kInventoryX, kInventoryY);
- blitBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
-
- const Common::Rect panel(96, 96, 544, 432);
- screen->fillRect(panel, kPanelFillColor);
-
- drawShadowedString(*screen, *titleFont, "Harvester", panel.left, 110, panel.width(),
- kTextColorNormal, Graphics::kTextAlignCenter);
-
- for (uint i = 0; i < menuItems.size(); ++i) {
- const int itemY = kMenuStartY + (int)i * kMenuLineSpacing;
- const byte color = ((int)i == selectedItem) ? kTextColorHover : kTextColorNormal;
- drawShadowedString(*screen, *bodyFont, menuItems[i], panel.left, itemY, panel.width(), color,
- Graphics::kTextAlignCenter);
- }
-
- if (!statusMessage.empty()) {
- drawWrappedShadowedText(*screen, *bodyFont, statusMessage, panel.left + 24, 352, panel.width() - 48,
- kTextColorNormal);
- }
- drawShadowedString(*screen, *bodyFont, "Use mouse or arrow keys. Enter activates. Esc returns to launcher.",
- panel.left, 404, panel.width(), kTextColorDim, Graphics::kTextAlignCenter);
-
- if (drawCursor && _engine.getRuntimeEntities())
- _engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-void MenuSystem::renderBackdropMenuScreen(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, const Common::Array<Common::String> &menuItems,
- int selectedItem) const {
- Graphics::Screen *screen = _engine.getScreen();
- const Art *art = _engine.getArt();
- const Text *text = _engine.getText();
- const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
- const CftFontResource *unselectedFontResource = text ? text->findFont("HARVFNT2") : nullptr;
- if (!screen || !art || !selectedFontResource || !unselectedFontResource || !backdrop.isValid())
- return;
-
- HarvesterCftFont selectedFont(*selectedFontResource);
- HarvesterCftFont unselectedFont(*unselectedFontResource);
- if (!selectedFont.isValid() || !unselectedFont.isValid())
- return;
-
- applyMenuPalette(*screen, _engine, palette, paletteBrightness);
- blitBitmap(*screen, backdrop, 0, 0);
- blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
-
- const int lineSpacing = getNativeRoomMenuLineHeight(selectedFont);
- for (uint i = 0; i < menuItems.size(); ++i) {
- const Graphics::Font *font = ((int)i == selectedItem)
- ? static_cast<const Graphics::Font *>(&selectedFont)
- : static_cast<const Graphics::Font *>(&unselectedFont);
- const int width = font->getStringWidth(menuItems[i]);
- const int x = (screen->w - width) / 2;
- const int y = kMenuStartY + (int)i * lineSpacing;
- font->drawString(screen, menuItems[i], x, y, width, 0);
- }
-
- if (_engine.getRuntimeEntities())
- _engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
int MenuSystem::getMenuItemAt(const Common::Point &mousePos,
const Common::Array<Common::String> &menuItems) const {
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
@@ -2365,7 +1749,7 @@ int MenuSystem::getMenuItemAt(const Common::Point &mousePos,
for (uint i = 0; i < menuItems.size(); ++i) {
const int width = font->getStringWidth(menuItems[i]);
const int x = (640 - width) / 2;
- const int y = kMenuStartY + (int)i * kMenuLineSpacing;
+ const int y = MenuRendering::kMenuStartY + (int)i * MenuRendering::kMenuLineSpacing;
const Common::Rect bounds(x - 12, y - 2, x + width + 12, y + font->getFontHeight() + 2);
if (bounds.contains(mousePos))
return (int)i;
diff --git a/engines/harvester/menu_config.cpp b/engines/harvester/menu_config.cpp
new file mode 100644
index 00000000000..11c7f086e28
--- /dev/null
+++ b/engines/harvester/menu_config.cpp
@@ -0,0 +1,214 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/menu.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/formats/ini-file.h"
+#include "common/memstream.h"
+#include "harvester/detection.h"
+#include "harvester/harvester.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+static const char *const kMenuPath = "MENU.INI";
+static const char *const kMenuSectionName = "menu";
+static const int kOptionsItemCount = 7;
+
+struct MenuWeekdayEntry {
+ const char *key;
+ const char *englishFallback;
+};
+
+static const MenuWeekdayEntry kMenuWeekdays[] = {
+ { "monday", "Monday" },
+ { "tuesday", "Tuesday" },
+ { "wednesday", "Wednesday" },
+ { "thursday", "Thursday" },
+ { "friday", "Friday" },
+ { "saturday", "Saturday" }
+};
+
+// The first three strings retain the padding from the European executable's
+// fixed-width table so their centered text sits underneath the volume bars.
+static const char *const kEuropeanBuiltInOptionItems[kOptionsItemCount] = {
+ " SOUND FX ",
+ " MUSIC ",
+ " GAMMA ",
+ "TEXT",
+ "GORE",
+ "QUICK TIPS",
+ "PASSWORD"
+};
+
+static bool loadRawMenuValue(const Common::Array<byte> &data, const char *key, Common::String &value) {
+ value.clear();
+ if (!key)
+ return false;
+
+ Common::String line;
+ for (uint i = 0; i <= data.size(); ++i) {
+ const bool atEnd = i == data.size();
+ const byte chr = atEnd ? '\n' : data[i];
+ if (chr == '\r')
+ continue;
+
+ if (chr != '\n') {
+ line += static_cast<char>(chr);
+ continue;
+ }
+
+ const char *separator = strchr(line.c_str(), '=');
+ if (separator) {
+ const Common::String currentKey(line.c_str(), separator);
+ if (currentKey.equalsIgnoreCase(key)) {
+ value = Common::String(separator + 1);
+ return true;
+ }
+ }
+
+ line.clear();
+ }
+
+ return false;
+}
+
+static void loadMenuDisplayValue(Common::INIFile &menu, const char *key, Common::String &dest) {
+ Common::String value;
+ if (!menu.getKey(key, kMenuSectionName, value) || value.empty())
+ return;
+
+ for (uint i = 0; i < value.size(); ++i) {
+ if (value[i] == '_')
+ value.setChar(' ', i);
+ }
+ dest = Common::move(value);
+}
+
+static void applyEuropeanBuiltInMenuText(MenuTextConfig &config) {
+ config.yesLabel = "Yes";
+ config.noLabel = "No";
+ config.clickLabel = "Click";
+ config.newGamePrompt = "START A NEW GAME?";
+ config.quitGamePrompt = " QUIT HARVESTER?";
+ for (uint i = 0; i < config.optionItems.size(); ++i)
+ config.optionItems[i] = kEuropeanBuiltInOptionItems[i];
+}
+
+bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
+ config = MenuTextConfig();
+ if (engine.isDemo()) {
+ config.clickLabel = "Click";
+ config.newGamePrompt = "START A NEW GAME?";
+ config.quitGamePrompt = " QUIT HARVESTER?";
+ }
+ config.optionItems.resize(kOptionsItemCount);
+ config.optionItems[0] = "SOUND FX";
+ config.optionItems[1] = "MUSIC";
+ config.optionItems[2] = "GAMMA";
+ config.optionItems[3] = "TEXT";
+ config.optionItems[4] = "GORE";
+ config.optionItems[5] = "QUICK TIPS";
+ config.optionItems[6] = "PASSWORD";
+ config.weekdayLabels.resize(ARRAYSIZE(kMenuWeekdays));
+ for (uint i = 0; i < config.weekdayLabels.size(); ++i)
+ config.weekdayLabels[i] = kMenuWeekdays[i].englishFallback;
+
+ ResourceManager *resources = engine.getResources();
+ if (!resources)
+ return false;
+
+ Common::Array<byte> data;
+ if (!resources->loadFile(kMenuPath, data)) {
+ if (!engine.isDemo())
+ applyEuropeanBuiltInMenuText(config);
+ debugC(2, kDebugGeneral,
+ "Harvester: using built-in menu text because '%s' is unavailable", kMenuPath);
+ return true;
+ }
+
+ Common::MemoryReadStream stream(data.data(), data.size());
+ Common::INIFile menu;
+ menu.setDefaultSectionName(kMenuSectionName);
+ menu.requireKeyValueDelimiter();
+ menu.suppressValuelessLineWarning();
+ if (!menu.loadFromStream(stream))
+ return false;
+
+ for (uint i = 0; i < config.optionItems.size(); ++i) {
+ const Common::String key = Common::String::format("options_menu_%u", i + 1);
+ Common::String value;
+ if (loadRawMenuValue(data, key.c_str(), value) && !value.empty())
+ config.optionItems[i] = Common::move(value);
+ else if (menu.getKey(key, kMenuSectionName, value) && !value.empty())
+ config.optionItems[i] = Common::move(value);
+ }
+
+ Common::String value;
+ if (menu.getKey("yes", kMenuSectionName, value) && !value.empty())
+ config.yesLabel = Common::move(value);
+ if (menu.getKey("no", kMenuSectionName, value) && !value.empty())
+ config.noLabel = Common::move(value);
+ if (menu.getKey("click", kMenuSectionName, value) && !value.empty())
+ config.clickLabel = Common::move(value);
+ loadMenuDisplayValue(menu, "on", config.onLabel);
+ loadMenuDisplayValue(menu, "off", config.offLabel);
+ loadMenuDisplayValue(menu, "enter_password", config.enterPasswordLabel);
+ if (loadRawMenuValue(data, "newgame", value) && !value.empty())
+ config.newGamePrompt = Common::move(value);
+ if (loadRawMenuValue(data, "quitgame", value) && !value.empty())
+ config.quitGamePrompt = Common::move(value);
+ loadMenuDisplayValue(menu, "talk_to", config.talkToVerb);
+ loadMenuDisplayValue(menu, "examine", config.examineVerb);
+ loadMenuDisplayValue(menu, "examine_the", config.examineTheVerb);
+ loadMenuDisplayValue(menu, "operate", config.operateVerb);
+ loadMenuDisplayValue(menu, "pick_up", config.pickUpVerb);
+ loadMenuDisplayValue(menu, "use", config.useVerb);
+ loadMenuDisplayValue(menu, "use_on", config.useOnPreposition);
+ loadMenuDisplayValue(menu, "other", config.dialogueOtherLabel);
+ loadMenuDisplayValue(menu, "responses", config.dialogueResponsesLabel);
+ loadMenuDisplayValue(menu, "keyword", config.dialogueKeywordLabel);
+ for (uint i = 0; i < config.weekdayLabels.size(); ++i)
+ loadMenuDisplayValue(menu, kMenuWeekdays[i].key, config.weekdayLabels[i]);
+ loadMenuDisplayValue(menu, "Exit", config.quickTipsExitLabel);
+ loadMenuDisplayValue(menu, "next", config.quickTipsNextLabel);
+ loadMenuDisplayValue(menu, "show_tips_on", config.quickTipsOnLabel);
+ loadMenuDisplayValue(menu, "show_tips_off", config.quickTipsOffLabel);
+ loadMenuDisplayValue(menu, "quick_tips_header", config.quickTipsHeader);
+
+ return true;
+}
+
+Common::String buildUseItemPrompt(const MenuTextConfig &config,
+ const Common::String &itemLabel, const Common::String &targetLabel) {
+ if (itemLabel.empty())
+ return Common::String();
+ if (targetLabel.empty())
+ return Common::String::format("%s %s %s",
+ config.useVerb.c_str(), itemLabel.c_str(), config.useOnPreposition.c_str());
+
+ return Common::String::format("%s %s %s %s", config.useVerb.c_str(), itemLabel.c_str(),
+ config.useOnPreposition.c_str(), targetLabel.c_str());
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/menu_render.cpp b/engines/harvester/menu_render.cpp
new file mode 100644
index 00000000000..4de241de250
--- /dev/null
+++ b/engines/harvester/menu_render.cpp
@@ -0,0 +1,476 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/menu_render.h"
+
+#include "common/algorithm.h"
+#include "graphics/fontman.h"
+#include "harvester/cft_font.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/palette_utils.h"
+#include "harvester/script.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+namespace MenuRendering {
+
+static const int kInventoryX = 64;
+static const int kInventoryY = 48;
+static const int kLogoX = 160;
+static const int kLogoY = 0;
+const int kMenuStartY = 100;
+const int kMenuLineSpacing = 28;
+const byte kTextColorNormal = 255;
+static const byte kTextColorHover = 251;
+static const byte kTextColorDim = 248;
+static const byte kShadowColor = 0;
+static const byte kPanelFillColor = 1;
+static const byte kQuickTipActionColor = 0xc3;
+const int kOptionsSliderMinX = 0x16f;
+const int kOptionsSliderStep = 0x13;
+static const int kOptionsVolumeBitmapX = 0x168;
+static const int kOptionsVolumeBitmapY = 0x68;
+const int kOptionsIndicatorY = 0x6c;
+static const int kQuickTipsOverlayX = 167;
+static const int kQuickTipsOverlayY = 200;
+static const int kQuickTipsHeaderY = 202;
+static const int kQuickTipTextX = 180;
+static const int kQuickTipTextY = 228;
+static const int kEnglishQuickTipTextWidth = 280;
+static const int kLocalizedQuickTipTextWidth = 300;
+static const int kLocalizedQuickTipActionY = 308;
+static const int kQuickTipExitX = 180;
+static const int kQuickTipToggleX = 258;
+static const int kQuickTipNextRightInset = 8;
+static const int kConfirmDialogX = 167;
+static const int kConfirmDialogY = 200;
+static const int kConfirmPromptTextX = 0xea;
+static const int kConfirmPromptTextY = 0xd6;
+static const int kConfirmChoiceTextY = 0xf8;
+static const int kConfirmYesTextX = 0xdc;
+static const int kConfirmNoTextX = 0x17c;
+static const int kPasswordEntryX = 0xdc;
+static const int kPasswordEntryY = 0xdc;
+const int kPasswordEntryWidth = 0x226;
+
+void applyMenuPalette(Graphics::Screen &screen, const HarvesterEngine &engine,
+ const byte *palette, float brightness) {
+ if (!palette)
+ return;
+
+ byte displayPalette[256 * 3];
+ buildHarvesterDisplayPalette(palette, brightness * engine.getGammaBrightnessScale(), displayPalette);
+ screen.setPalette(displayPalette);
+}
+
+void renderCdChangePromptScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap,
+ const byte *palette) {
+ Graphics::Screen *screen = engine.getScreen();
+ if (!screen)
+ return;
+
+ applyMenuPalette(*screen, engine, palette, 1.0f);
+ screen->fillRect(screen->getBounds(), 0);
+ if (bitmap.isValid())
+ screen->copyRectToSurface(bitmap.pixels.data(), bitmap.width, 0, 0, bitmap.width, bitmap.height);
+ if (engine.getRuntimeEntities())
+ engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+void renderHelpScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap, const byte *palette) {
+ Graphics::Screen *screen = engine.getScreen();
+ if (!screen)
+ return;
+
+ applyMenuPalette(*screen, engine, palette, 1.0f);
+ screen->fillRect(screen->getBounds(), 0);
+ blitBitmap(*screen, bitmap, 0, 0);
+ if (engine.getRuntimeEntities())
+ engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+static Common::String buildTextModeSuffix(const Script &script, const MenuTextConfig &config) {
+ switch (script.getDialogueTextMode()) {
+ case kStartupDialogueTextNone:
+ return Common::String::format(" - %s", config.noLabel.c_str());
+ case kStartupDialogueTextClick:
+ return Common::String::format(" - %s", config.clickLabel.c_str());
+ case kStartupDialogueTextYes:
+ default:
+ return Common::String::format(" - %s", config.yesLabel.c_str());
+ }
+}
+
+static Common::String buildOptionsMenuItemLabel(const Script &script,
+ const MenuTextConfig &config, int index) {
+ if (index < 0 || index >= static_cast<int>(config.optionItems.size()))
+ return Common::String();
+
+ switch (index) {
+ case 3:
+ return config.optionItems[index] + buildTextModeSuffix(script, config);
+ case 4:
+ return Common::String::format("%s - %s", config.optionItems[index].c_str(),
+ (script.isGoreEnabled() ? config.onLabel : config.offLabel).c_str());
+ case 6:
+ return Common::String::format("%s - %s", config.optionItems[index].c_str(),
+ (script.getParentalPassword().empty() ? config.offLabel : config.onLabel).c_str());
+ default:
+ return config.optionItems[index];
+ }
+}
+
+void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
+ if (!bitmap.isValid())
+ return;
+
+ screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
+}
+
+void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &text, const TextLayout &layout, byte color, Graphics::TextAlign align) {
+ const int x = layout.x;
+ const int y = layout.y;
+ const int width = layout.width;
+
+ font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
+ font.drawString(&screen, text, x, y, width, color, align);
+}
+
+static void drawSinglePassString(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &text, int x, int y) {
+ if (text.empty())
+ return;
+
+ font.drawString(&screen, text, x, y, font.getStringWidth(text), 0);
+}
+
+static void drawWrappedShadowedText(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &text, const TextLayout &layout, byte color) {
+ const int x = layout.x;
+ const int y = layout.y;
+ const int width = layout.width;
+
+ Common::Array<Common::String> lines;
+ font.wordWrapText(text, width, lines);
+ for (uint i = 0; i < lines.size(); ++i)
+ drawShadowedString(screen, font, lines[i],
+ TextLayout(x, y + static_cast<int>(i) * (font.getFontHeight() + 2), width), color);
+}
+
+static void splitMenuConfigLines(const Common::String &text, Common::Array<Common::String> &lines) {
+ lines.clear();
+ Common::String currentLine;
+ for (uint i = 0; i < text.size(); ++i) {
+ if (text[i] == '/') {
+ lines.push_back(Common::move(currentLine));
+ currentLine.clear();
+ continue;
+ }
+ currentLine += text[i];
+ }
+ lines.push_back(Common::move(currentLine));
+}
+
+void Renderer::renderOptionsMenu(const FontPair &fonts, const Art &art,
+ const MenuTextConfig &config, const SliderBitmaps &sliders, int selectedItem, bool drawCursor) const {
+ Graphics::Screen *screen = _engine.getScreen();
+ Script *script = _engine.getScript();
+ if (!screen || !script)
+ return;
+
+ applyMenuPalette(*screen, _engine, _palette, _paletteBrightness);
+ blitBitmap(*screen, _backdrop, 0, 0);
+ blitTransparentBitmap(*screen, art.getLogoBitmap(), kLogoX, kLogoY);
+
+ const int lineHeight = getNativeRoomMenuLineHeight(fonts.primary);
+ for (int i = 0; i < static_cast<int>(config.optionItems.size()); ++i) {
+ const Common::String label = buildOptionsMenuItemLabel(*script, config, i);
+ const Graphics::Font &font = (i == selectedItem) ? fonts.primary : fonts.secondary;
+ const int width = font.getStringWidth(label);
+ const int x = (screen->w - width) / 2;
+ const int y = kMenuStartY + i * lineHeight;
+ font.drawString(screen, label, x, y, width, 0);
+ }
+
+ // Native run_main_menu spawns the option text entities before adding the
+ // VOLUME/INDICATR bitmap entities, so the bars clip the trailing overlap on
+ // the first three rows instead of the text painting over them.
+ for (int i = 0; i < 3; ++i)
+ blitTransparentBitmap(*screen, sliders.bar, kOptionsVolumeBitmapX, kOptionsVolumeBitmapY + i * lineHeight);
+
+ const int levels[3] = {
+ script->getFxVolumeLevel(),
+ script->getMusicVolumeLevel(),
+ script->getGammaLevel()
+ };
+ for (int i = 0; i < 3; ++i) {
+ blitTransparentBitmap(*screen, sliders.indicator, kOptionsSliderMinX + levels[i] * kOptionsSliderStep,
+ kOptionsIndicatorY + i * lineHeight);
+ }
+
+ if (drawCursor && _engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+void Renderer::renderPasswordPrompt(const FontPair &fonts, const Art &art,
+ const Common::String &title, const Common::String &text, bool cursorVisible, bool drawLogo) const {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return;
+
+ if (_palette)
+ applyMenuPalette(*screen, _engine, _palette, _paletteBrightness);
+ blitBitmap(*screen, _backdrop, 0, 0);
+ if (drawLogo)
+ blitTransparentBitmap(*screen, art.getLogoBitmap(), kLogoX, kLogoY);
+
+ const int titleWidth = fonts.primary.getStringWidth(title);
+ const int titleX = (screen->w - titleWidth) / 2;
+ fonts.primary.drawString(screen, title, titleX, 0xa0, titleWidth, 0);
+
+ Common::String displayText = text;
+ if (cursorVisible)
+ displayText += '~';
+ fonts.secondary.drawString(screen, displayText, kPasswordEntryX, kPasswordEntryY,
+ kPasswordEntryWidth, 0);
+
+ screen->makeAllDirty();
+ screen->update();
+}
+
+void Renderer::renderQuickTipsOverlay(const MenuTextConfig &config,
+ const QuickTipsLayout &layout, const Common::String &tipText) const {
+ Graphics::Screen *screen = _engine.getScreen();
+ const Art *art = _engine.getArt();
+ if (!screen || !art)
+ return;
+
+ applyMenuPalette(*screen, _engine, _palette, _paletteBrightness);
+ blitBitmap(*screen, _backdrop, 0, 0);
+ blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
+ drawQuickTipsPanel(_engine, config, layout, tipText);
+
+ if (_engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+void Renderer::renderConfirmPrompt(const Graphics::Font &promptFont,
+ const Graphics::Font &yesFont, const Graphics::Font &noFont,
+ const IndexedBitmap &textbox, const Common::String &promptText, const MenuTextConfig &config) const {
+ Graphics::Screen *screen = _engine.getScreen();
+ const Art *art = _engine.getArt();
+ if (!screen || !art)
+ return;
+
+ applyMenuPalette(*screen, _engine, _palette, _paletteBrightness);
+ blitBitmap(*screen, _backdrop, 0, 0);
+ blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
+ blitTransparentBitmap(*screen, textbox, kConfirmDialogX, kConfirmDialogY);
+ Common::Array<Common::String> promptLines;
+ splitMenuConfigLines(promptText, promptLines);
+ for (uint i = 0; i < promptLines.size(); ++i) {
+ drawSinglePassString(*screen, promptFont, promptLines[i], kConfirmPromptTextX,
+ kConfirmPromptTextY + static_cast<int>(i) * (promptFont.getFontHeight() + 2));
+ }
+ drawSinglePassString(*screen, yesFont, config.yesLabel, kConfirmYesTextX, kConfirmChoiceTextY);
+ drawSinglePassString(*screen, noFont, config.noLabel, kConfirmNoTextX, kConfirmChoiceTextY);
+
+ if (_engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+int getNativeRoomMenuLineHeight(const Graphics::Font &selectedFont) {
+ return selectedFont.getFontHeight() + 2;
+}
+
+} // End of namespace MenuRendering
+
+bool resolveQuickTipsLayout(HarvesterEngine &engine, const MenuTextConfig &config,
+ QuickTipsLayout &layout) {
+ using namespace MenuRendering;
+
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ const Art *art = engine.getArt();
+ const Script *script = engine.getScript();
+ if (!font || !art || !script)
+ return false;
+
+ if (!config.hasQuickTipsHeader()) {
+ layout.exitRect = Common::Rect(180, 280, 238, 291);
+ layout.nextRect = Common::Rect(420, 280, 492, 291);
+ layout.toggleRect = Common::Rect(258, 280, 366, 291);
+ return true;
+ }
+
+ const IndexedBitmap *panel = art->getQuickTipsTextboxBitmap();
+ if (!panel || !panel->isValid())
+ return false;
+
+ const Common::String &toggleLabel = script->isQuickTipsEnabled()
+ ? config.quickTipsOnLabel : config.quickTipsOffLabel;
+ auto makeLabelRect = [font](const Common::String &label, int x) {
+ const int width = MAX<int>(1, font->getStringWidth(label));
+ const int height = MAX<int>(1, font->getFontHeight());
+ return Common::Rect(x, kLocalizedQuickTipActionY,
+ x + width, kLocalizedQuickTipActionY + height);
+ };
+
+ layout.exitRect = makeLabelRect(config.quickTipsExitLabel, kQuickTipExitX);
+ layout.toggleRect = makeLabelRect(toggleLabel, kQuickTipToggleX);
+ const int nextX = kQuickTipsOverlayX + static_cast<int>(panel->width)
+ - font->getStringWidth(config.quickTipsNextLabel) - kQuickTipNextRightInset;
+ layout.nextRect = makeLabelRect(config.quickTipsNextLabel, nextX);
+ return true;
+}
+
+void drawQuickTipsPanel(HarvesterEngine &engine, const MenuTextConfig &config,
+ const QuickTipsLayout &layout, const Common::String &tipText) {
+ using namespace MenuRendering;
+
+ Graphics::Screen *screen = engine.getScreen();
+ const Art *art = engine.getArt();
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ const Script *script = engine.getScript();
+ if (!screen || !art || !font || !script)
+ return;
+
+ const IndexedBitmap *panel = config.hasQuickTipsHeader()
+ ? art->getQuickTipsTextboxBitmap() : &art->getTipsBitmap();
+ if (!panel || !panel->isValid())
+ return;
+
+ blitBitmap(*screen, *panel, kQuickTipsOverlayX, kQuickTipsOverlayY);
+ if (config.hasQuickTipsHeader()) {
+ drawShadowedString(*screen, *font, config.quickTipsHeader,
+ TextLayout(kQuickTipsOverlayX, kQuickTipsHeaderY, panel->width),
+ kQuickTipActionColor, Graphics::kTextAlignCenter);
+ }
+
+ const int tipTextWidth = config.hasQuickTipsHeader()
+ ? kLocalizedQuickTipTextWidth : kEnglishQuickTipTextWidth;
+ drawWrappedShadowedText(*screen, *font, tipText,
+ TextLayout(kQuickTipTextX, kQuickTipTextY, tipTextWidth), kTextColorNormal);
+ const Common::String &toggleLabel = script->isQuickTipsEnabled()
+ ? config.quickTipsOnLabel : config.quickTipsOffLabel;
+ drawShadowedString(*screen, *font, config.quickTipsExitLabel,
+ TextLayout(layout.exitRect.left, layout.exitRect.top, layout.exitRect.width()), kQuickTipActionColor);
+ drawShadowedString(*screen, *font, config.quickTipsNextLabel,
+ TextLayout(layout.nextRect.left, layout.nextRect.top, layout.nextRect.width()), kQuickTipActionColor);
+ drawShadowedString(*screen, *font, toggleLabel,
+ TextLayout(layout.toggleRect.left, layout.toggleRect.top, layout.toggleRect.width()), kQuickTipActionColor);
+}
+
+void MenuSystem::renderMainMenuScreen(int selectedItem, const Common::String &statusMessage,
+ bool drawCursor, const Common::Array<Common::String> &menuItems) const {
+ using namespace MenuRendering;
+
+ Graphics::Screen *screen = _engine.getScreen();
+ const Art *art = _engine.getArt();
+ const Graphics::Font *titleFont = FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);
+ const Graphics::Font *bodyFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ if (!screen || !art || !titleFont || !bodyFont)
+ return;
+
+ byte displayPalette[256 * 3];
+ buildHarvesterDisplayPalette(art->getWaitPalette(), 1.0f, displayPalette);
+ screen->setPalette(displayPalette);
+ screen->fillRect(screen->getBounds(), 0);
+ blitBitmap(*screen, art->getInventoryBitmap(), kInventoryX, kInventoryY);
+ blitBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
+
+ const Common::Rect panel(96, 96, 544, 432);
+ screen->fillRect(panel, kPanelFillColor);
+
+ drawShadowedString(*screen, *titleFont, "Harvester", TextLayout(panel.left, 110, panel.width()),
+ kTextColorNormal, Graphics::kTextAlignCenter);
+
+ for (uint i = 0; i < menuItems.size(); ++i) {
+ const int itemY = kMenuStartY + static_cast<int>(i) * kMenuLineSpacing;
+ const byte color = (static_cast<int>(i) == selectedItem) ? kTextColorHover : kTextColorNormal;
+ drawShadowedString(*screen, *bodyFont, menuItems[i],
+ TextLayout(panel.left, itemY, panel.width()), color, Graphics::kTextAlignCenter);
+ }
+
+ if (!statusMessage.empty()) {
+ drawWrappedShadowedText(*screen, *bodyFont, statusMessage,
+ TextLayout(panel.left + 24, 352, panel.width() - 48), kTextColorNormal);
+ }
+ drawShadowedString(*screen, *bodyFont, "Use mouse or arrow keys. Enter activates. Esc returns to launcher.",
+ TextLayout(panel.left, 404, panel.width()), kTextColorDim, Graphics::kTextAlignCenter);
+
+ if (drawCursor && _engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+void MenuSystem::renderBackdropMenuScreen(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, const Common::Array<Common::String> &menuItems,
+ int selectedItem) const {
+ using namespace MenuRendering;
+
+ Graphics::Screen *screen = _engine.getScreen();
+ const Art *art = _engine.getArt();
+ const Text *text = _engine.getText();
+ const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
+ const CftFontResource *unselectedFontResource = text ? text->findFont("HARVFNT2") : nullptr;
+ if (!screen || !art || !selectedFontResource || !unselectedFontResource || !backdrop.isValid())
+ return;
+
+ HarvesterCftFont selectedFont(*selectedFontResource);
+ HarvesterCftFont unselectedFont(*unselectedFontResource);
+ if (!selectedFont.isValid() || !unselectedFont.isValid())
+ return;
+
+ applyMenuPalette(*screen, _engine, palette, paletteBrightness);
+ blitBitmap(*screen, backdrop, 0, 0);
+ blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
+
+ const int lineSpacing = getNativeRoomMenuLineHeight(selectedFont);
+ for (uint i = 0; i < menuItems.size(); ++i) {
+ const Graphics::Font *font = (static_cast<int>(i) == selectedItem)
+ ? static_cast<const Graphics::Font *>(&selectedFont)
+ : static_cast<const Graphics::Font *>(&unselectedFont);
+ const int width = font->getStringWidth(menuItems[i]);
+ const int x = (screen->w - width) / 2;
+ const int y = kMenuStartY + static_cast<int>(i) * lineSpacing;
+ font->drawString(screen, menuItems[i], x, y, width, 0);
+ }
+
+ if (_engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/menu_render.h b/engines/harvester/menu_render.h
new file mode 100644
index 00000000000..b64ab5280af
--- /dev/null
+++ b/engines/harvester/menu_render.h
@@ -0,0 +1,104 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef HARVESTER_MENU_RENDER_H
+#define HARVESTER_MENU_RENDER_H
+
+#include "graphics/font.h"
+#include "harvester/menu.h"
+
+namespace Graphics {
+class Screen;
+}
+
+namespace Harvester {
+namespace MenuRendering {
+
+extern const int kMenuStartY;
+extern const int kMenuLineSpacing;
+extern const byte kTextColorNormal;
+extern const int kOptionsSliderMinX;
+extern const int kOptionsSliderStep;
+extern const int kOptionsIndicatorY;
+extern const int kPasswordEntryWidth;
+
+struct TextLayout {
+ TextLayout(int x, int y, int width) : x(x), y(y), width(width) {}
+
+ int x;
+ int y;
+ int width;
+};
+
+struct FontPair {
+ FontPair(const Graphics::Font &primary, const Graphics::Font &secondary)
+ : primary(primary), secondary(secondary) {}
+
+ const Graphics::Font &primary;
+ const Graphics::Font &secondary;
+};
+
+struct SliderBitmaps {
+ SliderBitmaps(const IndexedBitmap &bar, const IndexedBitmap &indicator)
+ : bar(bar), indicator(indicator) {}
+
+ const IndexedBitmap &bar;
+ const IndexedBitmap &indicator;
+};
+
+// Drawing calls borrow their backdrop, palette, fonts, and bitmaps synchronously.
+class Renderer {
+public:
+ Renderer(HarvesterEngine &engine, const IndexedBitmap &backdrop,
+ const byte *palette, float paletteBrightness)
+ : _engine(engine), _backdrop(backdrop), _palette(palette), _paletteBrightness(paletteBrightness) {}
+
+ void renderOptionsMenu(const FontPair &fonts, const Art &art, const MenuTextConfig &config,
+ const SliderBitmaps &sliders, int selectedItem, bool drawCursor = true) const;
+ void renderPasswordPrompt(const FontPair &fonts, const Art &art, const Common::String &title,
+ const Common::String &text, bool cursorVisible, bool drawLogo) const;
+ void renderQuickTipsOverlay(const MenuTextConfig &config, const QuickTipsLayout &layout,
+ const Common::String &tipText) const;
+ void renderConfirmPrompt(const Graphics::Font &promptFont,
+ const Graphics::Font &yesFont, const Graphics::Font &noFont,
+ const IndexedBitmap &textbox, const Common::String &promptText, const MenuTextConfig &config) const;
+
+private:
+ HarvesterEngine &_engine;
+ const IndexedBitmap &_backdrop;
+ const byte *_palette;
+ float _paletteBrightness;
+};
+
+void applyMenuPalette(Graphics::Screen &screen, const HarvesterEngine &engine,
+ const byte *palette, float brightness);
+void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y);
+void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &text, const TextLayout &layout, byte color,
+ Graphics::TextAlign align = Graphics::kTextAlignLeft);
+void renderCdChangePromptScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap, const byte *palette);
+void renderHelpScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap, const byte *palette);
+int getNativeRoomMenuLineHeight(const Graphics::Font &selectedFont);
+
+} // End of namespace MenuRendering
+} // End of namespace Harvester
+
+#endif // HARVESTER_MENU_RENDER_H
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index a48c049f33f..6c630d2bb7e 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -13,6 +13,8 @@ MODULE_OBJS = \
inventory.o \
media_manager.o \
menu.o \
+ menu_config.o \
+ menu_render.o \
metaengine.o \
monster.o \
player.o \
Commit: eb8f943defd471fb1732568ee424affbe62da5dd
https://github.com/scummvm/scummvm/commit/eb8f943defd471fb1732568ee424affbe62da5dd
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:01-04:00
Commit Message:
HARVESTER: Separate options menu actions and event handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/menu_options.cpp
engines/harvester/menu.cpp
engines/harvester/menu.h
engines/harvester/module.mk
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 1b3328d345b..15de0f22985 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -46,9 +46,6 @@ namespace Harvester {
namespace {
-static const char *const kOptionsVolumeBitmapPath = "1:/GRAPHIC/OTHER/VOLUME.BM";
-static const char *const kOptionsIndicatorBitmapPath = "1:/GRAPHIC/OTHER/INDICATR.BM";
-static const char *const kOptionsPreviewSoundPath = "1:/SOUND/EFFECTS/WHIP2.WAV";
static const char *const kLoadGameBitmapPath = "1:/GRAPHIC/OTHER/LOADGAME.BM";
static const char *const kLoadGamePalettePath = "1:/GRAPHIC/PAL/LOADGAME.PAL";
static const char *const kSaveGameBitmapPath = "1:/GRAPHIC/OTHER/SAVEGAME.BM";
@@ -56,9 +53,6 @@ static const char *const kSaveGamePalettePath = "1:/GRAPHIC/PAL/SAVEGAME.PAL";
static const char *const kGameOverBitmapPath = "1:/GRAPHIC/OTHER/GAMEOVER.BM";
static const char *const kGameOverPalettePath = "1:/GRAPHIC/PAL/GAMEOVER.PAL";
static const char *const kGameOverMusicPath = "SOUND/MUSIC/ANXIETY.CMP";
-static const int kOptionsSliderMaxX = 0x22b;
-static const int kOptionsSliderHeight = 0x1e;
-static const int kStartupOptionMaxLevel = 9;
static const int kSaveSlotCount = 25;
static const int kSaveSlotLabelX = 8;
static const int kSaveSlotNameX = 0x50;
@@ -107,15 +101,6 @@ private:
bool _paused = false;
};
-static int clampStartupOptionLevel(int level) {
- if (level < 0)
- return 0;
- if (level > kStartupOptionMaxLevel)
- return kStartupOptionMaxLevel;
-
- return level;
-}
-
static Common::Rect saveSlotRect(int slotIndex) {
const int top = kSaveSlotStartY + slotIndex * kSaveSlotStride;
return Common::Rect(8, top, 0x27b + 1, top + kSaveSlotStride);
@@ -137,17 +122,6 @@ static Common::Rect quitConfirmNoRect() {
return Common::Rect(0x172, 0xee, 0x19a + 1, 0x108 + 1);
}
-static Common::Rect optionsSliderRect(int sliderIndex, int lineHeight) {
- const int y = MenuRendering::kOptionsIndicatorY + sliderIndex * lineHeight;
- return Common::Rect(MenuRendering::kOptionsSliderMinX, y, kOptionsSliderMaxX + 1, y + kOptionsSliderHeight + 1);
-}
-
-static int resolveOptionsSliderIndexFromMouseX(int mouseX) {
- const int relativeX = mouseX - MenuRendering::kOptionsSliderMinX;
- return clampStartupOptionLevel(
- (relativeX + (MenuRendering::kOptionsSliderStep / 2)) / MenuRendering::kOptionsSliderStep);
-}
-
} // End of anonymous namespace
namespace {
@@ -170,14 +144,6 @@ static void buildDisplayMainMenuItems(const Common::Array<Common::String> &sourc
dest[2] = kBlankMenuSlot;
}
-static bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
- Common::Array<byte> data;
- if (!resources.loadFile(path, data) || data.size() < 12)
- return false;
-
- return decodeBitmap(data, bitmap);
-}
-
static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
Common::Array<byte> data;
if (!resources.loadFile(path, data) || data.size() < 256 * 3)
@@ -191,7 +157,17 @@ static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
return Common::String::format("GAME %02d", slotIndex + 1);
}
-static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFont, uint itemCount,
+} // End of anonymous namespace
+
+bool MenuSystem::loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
+ Common::Array<byte> data;
+ if (!resources.loadFile(path, data) || data.size() < 12)
+ return false;
+
+ return decodeBitmap(data, bitmap);
+}
+
+int MenuSystem::getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFont, uint itemCount,
const Common::Point &mousePos) {
if (itemCount == 0)
return -1;
@@ -201,14 +177,12 @@ static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFon
selection = 1;
selection /= MAX<int>(1, MenuRendering::getNativeRoomMenuLineHeight(selectedFont));
- if (selection >= (int)itemCount)
- selection = (int)itemCount - 1;
+ if (selection >= static_cast<int>(itemCount))
+ selection = static_cast<int>(itemCount) - 1;
return selection;
}
-} // End of anonymous namespace
-
MenuSystem::MenuSystem(HarvesterEngine &engine, Common::Point &mousePos,
const Common::Array<Common::String> &menuItems)
: _engine(engine), _mousePos(mousePos), _menuItems(menuItems) {
@@ -1328,344 +1302,6 @@ Common::Error MenuSystem::validateParentalPassword(Flow &flow) {
return Common::kNoError;
}
-Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow) {
- const Art *art = _engine.getArt();
- Script *script = _engine.getScript();
- ResourceManager *resources = _engine.getResources();
- const Text *text = _engine.getText();
- const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
- const CftFontResource *unselectedFontResource = text ? text->findFont("HARVFNT2") : nullptr;
- if (!art || !script || !resources || !selectedFontResource || !unselectedFontResource)
- return Common::kReadingFailed;
-
- HarvesterCftFont selectedFont(*selectedFontResource);
- HarvesterCftFont unselectedFont(*unselectedFontResource);
- if (!selectedFont.isValid() || !unselectedFont.isValid())
- return Common::kReadingFailed;
-
- const MenuTextConfig &config = flow.getMenuTextConfig();
-
- IndexedBitmap volumeBar;
- IndexedBitmap indicator;
- if (!loadBitmapResource(*resources, kOptionsVolumeBitmapPath, volumeBar) ||
- !loadBitmapResource(*resources, kOptionsIndicatorBitmapPath, indicator)) {
- return Common::kReadingFailed;
- }
-
- const int lineHeight = MenuRendering::getNativeRoomMenuLineHeight(selectedFont);
- int selectedItem = 0;
- int capturedSlider = -1;
- bool showingQuickTips = false;
- bool needsRedraw = true;
- uint quickTipIndex = flow.getQuickTipCount() == 0 ? 0 : _engine.getRandomNumber(flow.getQuickTipCount() - 1);
- QuickTipsLayout quickTipsLayout;
-
- auto persistConfig = [&]() -> bool {
- return script->saveConfig();
- };
-
- auto updateSlider = [&](int sliderIndex) {
- const int newLevel = resolveOptionsSliderIndexFromMouseX(_mousePos.x);
- switch (sliderIndex) {
- case 0:
- if (newLevel != script->getFxVolumeLevel()) {
- _engine.setFxVolumeLevel(newLevel);
- _engine.playSingleSound(kOptionsPreviewSoundPath);
- persistConfig();
- needsRedraw = true;
- }
- break;
- case 1:
- if (newLevel != script->getMusicVolumeLevel()) {
- _engine.setMusicVolumeLevel(newLevel);
- persistConfig();
- needsRedraw = true;
- }
- break;
- case 2:
- if (newLevel != script->getGammaLevel()) {
- _engine.setGammaLevel(newLevel);
- persistConfig();
- needsRedraw = true;
- }
- break;
- default:
- break;
- }
- };
-
- auto cycleTextMode = [&]() {
- switch (script->getDialogueTextMode()) {
- case kStartupDialogueTextNone:
- script->setDialogueTextMode(kStartupDialogueTextYes);
- break;
- case kStartupDialogueTextYes:
- script->setDialogueTextMode(kStartupDialogueTextClick);
- break;
- case kStartupDialogueTextClick:
- default:
- script->setDialogueTextMode(kStartupDialogueTextNone);
- break;
- }
- persistConfig();
- needsRedraw = true;
- };
-
- auto toggleGore = [&]() {
- script->setGoreEnabled(!script->isGoreEnabled());
- ConfMan.setBool("gore", script->isGoreEnabled());
- persistConfig();
- needsRedraw = true;
- };
-
- auto togglePassword = [&]() {
- if (script->getParentalPassword().empty()) {
- debugC(2, kDebugGeneral, "Harvester: parental password set prompt opened");
- Common::String password;
- bool accepted = false;
- Common::Error promptError = runPasswordPrompt(
- backdrop, palette, paletteBrightness, flow, true, password, accepted);
- if (promptError.getCode() != Common::kNoError) {
- debugC(1, kDebugGeneral,
- "Harvester: parental password set prompt failed error=%d",
- (int)promptError.getCode());
- return;
- }
- if (accepted) {
- script->setParentalPassword(password);
- if (!persistConfig()) {
- script->setParentalPassword(Common::String());
- debugC(1, kDebugGeneral,
- "Harvester: parental password enable failed");
- } else {
- debugC(1, kDebugGeneral,
- "Harvester: parental password enabled");
- }
- needsRedraw = true;
- } else {
- debugC(2, kDebugGeneral,
- "Harvester: parental password set prompt cancelled state=disabled");
- }
- } else {
- const Common::String previousPassword = script->getParentalPassword();
- script->setParentalPassword(Common::String());
- if (!persistConfig()) {
- script->setParentalPassword(previousPassword);
- debugC(1, kDebugGeneral,
- "Harvester: parental password disable failed");
- } else {
- debugC(1, kDebugGeneral,
- "Harvester: parental password disabled");
- }
- needsRedraw = true;
- }
- };
-
- auto activateSelectedItem = [&]() -> Common::Error {
- switch (selectedItem) {
- case 3:
- cycleTextMode();
- break;
- case 4:
- toggleGore();
- break;
- case 5:
- if (flow.getQuickTipCount() != 0) {
- if (!resolveQuickTipsLayout(_engine, config, quickTipsLayout))
- return Common::kReadingFailed;
- showingQuickTips = true;
- needsRedraw = true;
- }
- break;
- case 6:
- togglePassword();
- break;
- default:
- break;
- }
-
- return Common::kNoError;
- };
-
- Graphics::FrameLimiter limiter(g_system, 60);
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- if (showingQuickTips) {
- MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderQuickTipsOverlay(
- config, quickTipsLayout, flow.getQuickTip(quickTipIndex));
- } else {
- MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderOptionsMenu(
- MenuRendering::FontPair(selectedFont, unselectedFont), *art, config,
- MenuRendering::SliderBitmaps(volumeBar, indicator), selectedItem);
- }
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- if (showingQuickTips) {
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN:
- if (quickTipsLayout.exitRect.contains(_mousePos)) {
- showingQuickTips = false;
- needsRedraw = true;
- } else if (quickTipsLayout.nextRect.contains(_mousePos)) {
- quickTipIndex = (quickTipIndex + 1) % flow.getQuickTipCount();
- needsRedraw = true;
- } else if (quickTipsLayout.toggleRect.contains(_mousePos)) {
- script->setQuickTipsEnabled(!script->isQuickTipsEnabled());
- if (!resolveQuickTipsLayout(_engine, config, quickTipsLayout))
- return Common::kReadingFailed;
- persistConfig();
- needsRedraw = true;
- }
- break;
- case Common::EVENT_RBUTTONDOWN:
- showingQuickTips = false;
- needsRedraw = true;
- break;
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- showingQuickTips = false;
- needsRedraw = true;
- }
- break;
- default:
- break;
- }
- continue;
- }
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE: {
- if (capturedSlider != -1) {
- selectedItem = capturedSlider;
- updateSlider(capturedSlider);
- needsRedraw = true;
- } else {
- const int hoveredItem = getNativeRoomMenuSelectionFromMouse(selectedFont,
- config.optionItems.size(), _mousePos);
- if (hoveredItem != -1 && hoveredItem != selectedItem) {
- selectedItem = hoveredItem;
- needsRedraw = true;
- }
- }
- break;
- }
- case Common::EVENT_LBUTTONDOWN: {
- bool handled = false;
- for (int sliderIndex = 0; sliderIndex < 3; ++sliderIndex) {
- if (optionsSliderRect(sliderIndex, lineHeight).contains(_mousePos)) {
- selectedItem = sliderIndex;
- capturedSlider = sliderIndex;
- updateSlider(sliderIndex);
- needsRedraw = true;
- handled = true;
- break;
- }
- }
- if (handled)
- break;
-
- const int clickedItem = getNativeRoomMenuSelectionFromMouse(selectedFont,
- config.optionItems.size(), _mousePos);
- if (clickedItem == -1)
- break;
-
- selectedItem = clickedItem;
- needsRedraw = true;
- if (selectedItem >= 3) {
- Common::Error activateError = activateSelectedItem();
- if (activateError.getCode() != Common::kNoError)
- return activateError;
- }
- break;
- }
- case Common::EVENT_LBUTTONUP:
- capturedSlider = -1;
- break;
- case Common::EVENT_RBUTTONDOWN:
- return Common::kNoError;
- case Common::EVENT_KEYDOWN:
- switch (event.kbd.keycode) {
- case Common::KEYCODE_ESCAPE:
- return Common::kNoError;
- case Common::KEYCODE_UP:
- selectedItem = (selectedItem + (int)config.optionItems.size() - 1) % (int)config.optionItems.size();
- needsRedraw = true;
- break;
- case Common::KEYCODE_DOWN:
- selectedItem = (selectedItem + 1) % (int)config.optionItems.size();
- needsRedraw = true;
- break;
- case Common::KEYCODE_LEFT:
- if (selectedItem < 3) {
- _mousePos.x = MAX<int>(MenuRendering::kOptionsSliderMinX, _mousePos.x - MenuRendering::kOptionsSliderStep);
- updateSlider(selectedItem);
- needsRedraw = true;
- }
- break;
- case Common::KEYCODE_RIGHT:
- if (selectedItem < 3) {
- _mousePos.x = MIN<int>(kOptionsSliderMaxX, _mousePos.x + MenuRendering::kOptionsSliderStep);
- updateSlider(selectedItem);
- needsRedraw = true;
- }
- break;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER: {
- Common::Error activateError = activateSelectedItem();
- if (activateError.getCode() != Common::kNoError)
- return activateError;
- break;
- }
- case Common::KEYCODE_t:
- selectedItem = 3;
- cycleTextMode();
- break;
- case Common::KEYCODE_g:
- selectedItem = 4;
- toggleGore();
- break;
- case Common::KEYCODE_q:
- selectedItem = 5;
- if (flow.getQuickTipCount() != 0) {
- showingQuickTips = true;
- needsRedraw = true;
- }
- break;
- case Common::KEYCODE_p:
- selectedItem = 6;
- togglePassword();
- break;
- default:
- break;
- }
- break;
- default:
- break;
- }
- }
-
- EntityManager *entityManager = _engine.getRuntimeEntities();
- if (entityManager && entityManager->syncCursorEntityPosition(_mousePos))
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBrightness, Flow &flow) {
ResourceManager *resources = _engine.getResources();
if (!resources)
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index b211ea9e6e3..20a3a401a01 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -100,6 +100,23 @@ public:
Flow &flow, bool &promptCompleted);
private:
+ static bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap);
+ static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFont, uint itemCount,
+ const Common::Point &mousePos);
+
+ struct OptionsMenu;
+ void updateOptionsSlider(OptionsMenu &menu, int sliderIndex);
+ void cycleOptionsTextMode(OptionsMenu &menu);
+ void toggleOptionsGore(OptionsMenu &menu);
+ void toggleOptionsPassword(OptionsMenu &menu);
+ Common::Error activateOptionsItem(OptionsMenu &menu);
+ Common::Error handleOptionsQuickTipsEvent(OptionsMenu &menu, const Common::Event &event);
+ void handleOptionsMouseMove(OptionsMenu &menu, const Graphics::Font &selectedFont);
+ Common::Error handleOptionsMouseDown(OptionsMenu &menu, const Graphics::Font &selectedFont);
+ Common::Error handleOptionsKey(OptionsMenu &menu, const Common::Event &event, bool &closed);
+ Common::Error handleOptionsEvent(OptionsMenu &menu, const Common::Event &event,
+ const Graphics::Font &selectedFont, bool &closed);
+
struct SaveSlotMenu;
enum SaveLoadMode { kSaveMode, kLoadMode };
enum SaveSlotCommand { kNoSlotCommand, kConfirmSlot, kCancelSlot };
diff --git a/engines/harvester/menu_options.cpp b/engines/harvester/menu_options.cpp
new file mode 100644
index 00000000000..14069aa293b
--- /dev/null
+++ b/engines/harvester/menu_options.cpp
@@ -0,0 +1,443 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/menu.h"
+#include "harvester/menu_render.h"
+
+#include "common/algorithm.h"
+#include "common/config-manager.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/framelimiter.h"
+#include "harvester/cft_font.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/script.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+static const char *const kOptionsVolumeBitmapPath = "1:/GRAPHIC/OTHER/VOLUME.BM";
+static const char *const kOptionsIndicatorBitmapPath = "1:/GRAPHIC/OTHER/INDICATR.BM";
+static const char *const kOptionsPreviewSoundPath = "1:/SOUND/EFFECTS/WHIP2.WAV";
+static const int kOptionsSliderMaxX = 0x22b;
+static const int kOptionsSliderHeight = 0x1e;
+static const int kStartupOptionMaxLevel = 9;
+
+static int clampStartupOptionLevel(int level) {
+ if (level < 0)
+ return 0;
+ if (level > kStartupOptionMaxLevel)
+ return kStartupOptionMaxLevel;
+
+ return level;
+}
+
+static Common::Rect optionsSliderRect(int sliderIndex, int lineHeight) {
+ const int y = MenuRendering::kOptionsIndicatorY + sliderIndex * lineHeight;
+ return Common::Rect(MenuRendering::kOptionsSliderMinX, y, kOptionsSliderMaxX + 1, y + kOptionsSliderHeight + 1);
+}
+
+static int resolveOptionsSliderIndexFromMouseX(int mouseX) {
+ const int relativeX = mouseX - MenuRendering::kOptionsSliderMinX;
+ return clampStartupOptionLevel(
+ (relativeX + (MenuRendering::kOptionsSliderStep / 2)) / MenuRendering::kOptionsSliderStep);
+}
+
+struct MenuSystem::OptionsMenu {
+ OptionsMenu(const IndexedBitmap &backdrop, const byte *palette, float paletteBrightness,
+ Flow &flow, Script &script, const MenuTextConfig &config)
+ : backdrop(backdrop), palette(palette), paletteBrightness(paletteBrightness),
+ flow(flow), script(script), config(config) {}
+
+ const IndexedBitmap &backdrop;
+ const byte *palette;
+ float paletteBrightness;
+ Flow &flow;
+ Script &script;
+ const MenuTextConfig &config;
+ int lineHeight = 0;
+ int selectedItem = 0;
+ int capturedSlider = -1;
+ bool showingQuickTips = false;
+ bool needsRedraw = true;
+ uint quickTipIndex = 0;
+ QuickTipsLayout quickTipsLayout;
+};
+
+void MenuSystem::updateOptionsSlider(OptionsMenu &menu, int sliderIndex) {
+ const int newLevel = resolveOptionsSliderIndexFromMouseX(_mousePos.x);
+ switch (sliderIndex) {
+ case 0:
+ if (newLevel != menu.script.getFxVolumeLevel()) {
+ _engine.setFxVolumeLevel(newLevel);
+ _engine.playSingleSound(kOptionsPreviewSoundPath);
+ menu.script.saveConfig();
+ menu.needsRedraw = true;
+ }
+ break;
+ case 1:
+ if (newLevel != menu.script.getMusicVolumeLevel()) {
+ _engine.setMusicVolumeLevel(newLevel);
+ menu.script.saveConfig();
+ menu.needsRedraw = true;
+ }
+ break;
+ case 2:
+ if (newLevel != menu.script.getGammaLevel()) {
+ _engine.setGammaLevel(newLevel);
+ menu.script.saveConfig();
+ menu.needsRedraw = true;
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void MenuSystem::cycleOptionsTextMode(OptionsMenu &menu) {
+ switch (menu.script.getDialogueTextMode()) {
+ case kStartupDialogueTextNone:
+ menu.script.setDialogueTextMode(kStartupDialogueTextYes);
+ break;
+ case kStartupDialogueTextYes:
+ menu.script.setDialogueTextMode(kStartupDialogueTextClick);
+ break;
+ case kStartupDialogueTextClick:
+ default:
+ menu.script.setDialogueTextMode(kStartupDialogueTextNone);
+ break;
+ }
+ menu.script.saveConfig();
+ menu.needsRedraw = true;
+}
+
+void MenuSystem::toggleOptionsGore(OptionsMenu &menu) {
+ menu.script.setGoreEnabled(!menu.script.isGoreEnabled());
+ ConfMan.setBool("gore", menu.script.isGoreEnabled());
+ menu.script.saveConfig();
+ menu.needsRedraw = true;
+}
+
+void MenuSystem::toggleOptionsPassword(OptionsMenu &menu) {
+ if (menu.script.getParentalPassword().empty()) {
+ debugC(2, kDebugGeneral, "Harvester: parental password set prompt opened");
+ Common::String password;
+ bool accepted = false;
+ Common::Error promptError = runPasswordPrompt(
+ menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow, true, password, accepted);
+ if (promptError.getCode() != Common::kNoError) {
+ debugC(1, kDebugGeneral,
+ "Harvester: parental password set prompt failed error=%d",
+ static_cast<int>(promptError.getCode()));
+ return;
+ }
+ if (accepted) {
+ menu.script.setParentalPassword(password);
+ if (!menu.script.saveConfig()) {
+ menu.script.setParentalPassword(Common::String());
+ debugC(1, kDebugGeneral,
+ "Harvester: parental password enable failed");
+ } else {
+ debugC(1, kDebugGeneral,
+ "Harvester: parental password enabled");
+ }
+ menu.needsRedraw = true;
+ } else {
+ debugC(2, kDebugGeneral,
+ "Harvester: parental password set prompt cancelled state=disabled");
+ }
+ } else {
+ const Common::String previousPassword = menu.script.getParentalPassword();
+ menu.script.setParentalPassword(Common::String());
+ if (!menu.script.saveConfig()) {
+ menu.script.setParentalPassword(previousPassword);
+ debugC(1, kDebugGeneral,
+ "Harvester: parental password disable failed");
+ } else {
+ debugC(1, kDebugGeneral,
+ "Harvester: parental password disabled");
+ }
+ menu.needsRedraw = true;
+ }
+}
+
+Common::Error MenuSystem::activateOptionsItem(OptionsMenu &menu) {
+ switch (menu.selectedItem) {
+ case 3:
+ cycleOptionsTextMode(menu);
+ break;
+ case 4:
+ toggleOptionsGore(menu);
+ break;
+ case 5:
+ if (menu.flow.getQuickTipCount() != 0) {
+ if (!resolveQuickTipsLayout(_engine, menu.config, menu.quickTipsLayout))
+ return Common::kReadingFailed;
+ menu.showingQuickTips = true;
+ menu.needsRedraw = true;
+ }
+ break;
+ case 6:
+ toggleOptionsPassword(menu);
+ break;
+ default:
+ break;
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::handleOptionsQuickTipsEvent(OptionsMenu &menu, const Common::Event &event) {
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ menu.needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ if (menu.quickTipsLayout.exitRect.contains(_mousePos)) {
+ menu.showingQuickTips = false;
+ menu.needsRedraw = true;
+ } else if (menu.quickTipsLayout.nextRect.contains(_mousePos)) {
+ menu.quickTipIndex = (menu.quickTipIndex + 1) % menu.flow.getQuickTipCount();
+ menu.needsRedraw = true;
+ } else if (menu.quickTipsLayout.toggleRect.contains(_mousePos)) {
+ menu.script.setQuickTipsEnabled(!menu.script.isQuickTipsEnabled());
+ if (!resolveQuickTipsLayout(_engine, menu.config, menu.quickTipsLayout))
+ return Common::kReadingFailed;
+ menu.script.saveConfig();
+ menu.needsRedraw = true;
+ }
+ break;
+ case Common::EVENT_RBUTTONDOWN:
+ menu.showingQuickTips = false;
+ menu.needsRedraw = true;
+ break;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ menu.showingQuickTips = false;
+ menu.needsRedraw = true;
+ }
+ break;
+ default:
+ break;
+ }
+ return Common::kNoError;
+}
+
+void MenuSystem::handleOptionsMouseMove(OptionsMenu &menu, const Graphics::Font &selectedFont) {
+ if (menu.capturedSlider != -1) {
+ menu.selectedItem = menu.capturedSlider;
+ updateOptionsSlider(menu, menu.capturedSlider);
+ menu.needsRedraw = true;
+ } else {
+ const int hoveredItem = getNativeRoomMenuSelectionFromMouse(selectedFont,
+ menu.config.optionItems.size(), _mousePos);
+ if (hoveredItem != -1 && hoveredItem != menu.selectedItem) {
+ menu.selectedItem = hoveredItem;
+ menu.needsRedraw = true;
+ }
+ }
+}
+
+Common::Error MenuSystem::handleOptionsMouseDown(OptionsMenu &menu, const Graphics::Font &selectedFont) {
+ bool handled = false;
+ for (int sliderIndex = 0; sliderIndex < 3; ++sliderIndex) {
+ if (optionsSliderRect(sliderIndex, menu.lineHeight).contains(_mousePos)) {
+ menu.selectedItem = sliderIndex;
+ menu.capturedSlider = sliderIndex;
+ updateOptionsSlider(menu, sliderIndex);
+ menu.needsRedraw = true;
+ handled = true;
+ break;
+ }
+ }
+ if (handled)
+ return Common::kNoError;
+
+ const int clickedItem = getNativeRoomMenuSelectionFromMouse(selectedFont,
+ menu.config.optionItems.size(), _mousePos);
+ if (clickedItem == -1)
+ return Common::kNoError;
+
+ menu.selectedItem = clickedItem;
+ menu.needsRedraw = true;
+ if (menu.selectedItem >= 3) {
+ Common::Error activateError = activateOptionsItem(menu);
+ if (activateError.getCode() != Common::kNoError)
+ return activateError;
+ }
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::handleOptionsKey(OptionsMenu &menu, const Common::Event &event, bool &closed) {
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ closed = true;
+ return Common::kNoError;
+ case Common::KEYCODE_UP:
+ menu.selectedItem = (menu.selectedItem + static_cast<int>(menu.config.optionItems.size()) - 1)
+ % static_cast<int>(menu.config.optionItems.size());
+ menu.needsRedraw = true;
+ break;
+ case Common::KEYCODE_DOWN:
+ menu.selectedItem = (menu.selectedItem + 1) % static_cast<int>(menu.config.optionItems.size());
+ menu.needsRedraw = true;
+ break;
+ case Common::KEYCODE_LEFT:
+ if (menu.selectedItem < 3) {
+ _mousePos.x = MAX<int>(MenuRendering::kOptionsSliderMinX, _mousePos.x - MenuRendering::kOptionsSliderStep);
+ updateOptionsSlider(menu, menu.selectedItem);
+ menu.needsRedraw = true;
+ }
+ break;
+ case Common::KEYCODE_RIGHT:
+ if (menu.selectedItem < 3) {
+ _mousePos.x = MIN<int>(kOptionsSliderMaxX, _mousePos.x + MenuRendering::kOptionsSliderStep);
+ updateOptionsSlider(menu, menu.selectedItem);
+ menu.needsRedraw = true;
+ }
+ break;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER: {
+ Common::Error activateError = activateOptionsItem(menu);
+ if (activateError.getCode() != Common::kNoError)
+ return activateError;
+ break;
+ }
+ case Common::KEYCODE_t:
+ menu.selectedItem = 3;
+ cycleOptionsTextMode(menu);
+ break;
+ case Common::KEYCODE_g:
+ menu.selectedItem = 4;
+ toggleOptionsGore(menu);
+ break;
+ case Common::KEYCODE_q:
+ menu.selectedItem = 5;
+ if (menu.flow.getQuickTipCount() != 0) {
+ menu.showingQuickTips = true;
+ menu.needsRedraw = true;
+ }
+ break;
+ case Common::KEYCODE_p:
+ menu.selectedItem = 6;
+ toggleOptionsPassword(menu);
+ break;
+ default:
+ break;
+ }
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::handleOptionsEvent(OptionsMenu &menu, const Common::Event &event,
+ const Graphics::Font &selectedFont, bool &closed) {
+ if (menu.showingQuickTips)
+ return handleOptionsQuickTipsEvent(menu, event);
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ handleOptionsMouseMove(menu, selectedFont);
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ return handleOptionsMouseDown(menu, selectedFont);
+ case Common::EVENT_LBUTTONUP:
+ menu.capturedSlider = -1;
+ break;
+ case Common::EVENT_RBUTTONDOWN:
+ closed = true;
+ return Common::kNoError;
+ case Common::EVENT_KEYDOWN:
+ return handleOptionsKey(menu, event, closed);
+ default:
+ break;
+ }
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, Flow &flow) {
+ const Art *art = _engine.getArt();
+ Script *script = _engine.getScript();
+ ResourceManager *resources = _engine.getResources();
+ const Text *text = _engine.getText();
+ const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
+ const CftFontResource *unselectedFontResource = text ? text->findFont("HARVFNT2") : nullptr;
+ if (!art || !script || !resources || !selectedFontResource || !unselectedFontResource)
+ return Common::kReadingFailed;
+
+ HarvesterCftFont selectedFont(*selectedFontResource);
+ HarvesterCftFont unselectedFont(*unselectedFontResource);
+ if (!selectedFont.isValid() || !unselectedFont.isValid())
+ return Common::kReadingFailed;
+
+ const MenuTextConfig &config = flow.getMenuTextConfig();
+
+ IndexedBitmap volumeBar;
+ IndexedBitmap indicator;
+ if (!loadBitmapResource(*resources, kOptionsVolumeBitmapPath, volumeBar) ||
+ !loadBitmapResource(*resources, kOptionsIndicatorBitmapPath, indicator)) {
+ return Common::kReadingFailed;
+ }
+
+ OptionsMenu menu(backdrop, palette, paletteBrightness, flow, *script, config);
+ menu.lineHeight = MenuRendering::getNativeRoomMenuLineHeight(selectedFont);
+ menu.quickTipIndex = flow.getQuickTipCount() == 0 ? 0 : _engine.getRandomNumber(flow.getQuickTipCount() - 1);
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ while (!_engine.shouldQuit()) {
+ if (menu.needsRedraw) {
+ if (menu.showingQuickTips) {
+ MenuRendering::Renderer(_engine, menu.backdrop, menu.palette, menu.paletteBrightness).renderQuickTipsOverlay(
+ menu.config, menu.quickTipsLayout, menu.flow.getQuickTip(menu.quickTipIndex));
+ } else {
+ MenuRendering::Renderer(_engine, menu.backdrop, menu.palette, menu.paletteBrightness).renderOptionsMenu(
+ MenuRendering::FontPair(selectedFont, unselectedFont), *art, menu.config,
+ MenuRendering::SliderBitmaps(volumeBar, indicator), menu.selectedItem);
+ }
+ menu.needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (menu.flow.handleSystemEvent(event, result))
+ return result;
+
+ bool closed = false;
+ Common::Error eventError = handleOptionsEvent(menu, event, selectedFont, closed);
+ if (eventError.getCode() != Common::kNoError || closed)
+ return eventError;
+ }
+
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ if (entityManager && entityManager->syncCursorEntityPosition(_mousePos))
+ menu.needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 6c630d2bb7e..bd4adfe4416 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -14,6 +14,7 @@ MODULE_OBJS = \
media_manager.o \
menu.o \
menu_config.o \
+ menu_options.o \
menu_render.o \
metaengine.o \
monster.o \
Commit: 981f40449904bc1783946a5d31e3d5e89896cc21
https://github.com/scummvm/scummvm/commit/981f40449904bc1783946a5d31e3d5e89896cc21
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: reset hidden-to-visible ANIMs
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/script_commands.cpp
diff --git a/engines/harvester/script_commands.cpp b/engines/harvester/script_commands.cpp
index ded7902ee1c..5452e162e95 100644
--- a/engines/harvester/script_commands.cpp
+++ b/engines/harvester/script_commands.cpp
@@ -249,8 +249,7 @@ Script::CommandStepResult Script::executeAnimationCommand(const CommandRecord &c
const bool active = isTruthy(command.arg2);
const bool visible = isTruthy(command.arg3);
int runtimeState = currentAnimation->runtimeState;
- if (active && visible &&
- (!wasVisible || !currentAnimation->runtimeActive || runtimeState < 0)) {
+ if (visible && (!wasVisible || (active && runtimeState < 0))) {
runtimeState = 0;
}
const bool changed = currentAnimation->active != active ||
Commit: f73b7904605c447edf44b5af257efdb087a44ec3
https://github.com/scummvm/scummvm/commit/f73b7904605c447edf44b5af257efdb087a44ec3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Retry missed melee contact until the attack ends
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/combat.cpp
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index 64451dbcc14..cb7ad589a40 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -88,7 +88,7 @@ static NpcRecord *findRoomNpcRecordByName(RoomSetupState &state, const Common::S
static int findRoomNpcIndexByName(const RoomSetupState &state, const Common::String &name) {
for (uint i = 0; i < state.roomNpcs.size(); ++i) {
if (state.roomNpcs[i].npcName.equalsIgnoreCase(name))
- return (int)i;
+ return static_cast<int>(i);
}
return -1;
@@ -97,7 +97,7 @@ static int findRoomNpcIndexByName(const RoomSetupState &state, const Common::Str
static int findRoomMonsterIndexByName(const RoomSetupState &state, const Common::String &name) {
for (uint i = 0; i < state.roomMonsters.size(); ++i) {
if (state.roomMonsters[i].monsterName.equalsIgnoreCase(name))
- return (int)i;
+ return static_cast<int>(i);
}
return -1;
@@ -166,10 +166,10 @@ static byte findNearestPaletteColor(const byte *palette, byte red, byte green, b
uint32 bestDistance = 0xffffffffu;
for (int i = 0; i < 256; ++i) {
const int paletteOffset = i * 3;
- const int dr = (int)palette[paletteOffset] - red;
- const int dg = (int)palette[paletteOffset + 1] - green;
- const int db = (int)palette[paletteOffset + 2] - blue;
- const uint32 distance = (uint32)(dr * dr + dg * dg + db * db);
+ const int dr = static_cast<int>(palette[paletteOffset]) - red;
+ const int dg = static_cast<int>(palette[paletteOffset + 1]) - green;
+ const int db = static_cast<int>(palette[paletteOffset + 2]) - blue;
+ const uint32 distance = static_cast<uint32>(dr * dr + dg * dg + db * db);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = (byte)i;
@@ -262,8 +262,8 @@ int computeRectAxisGap(int minA, int maxA, int minB, int maxB) {
float clampRoomDepthForEvent(const RoomSetupState &state, float z) {
return CLIP<float>(z,
- (float)MIN(state.roomMinZ, state.roomMaxZ),
- (float)MAX(state.roomMinZ, state.roomMaxZ));
+ static_cast<float>(MIN(state.roomMinZ, state.roomMaxZ)),
+ static_cast<float>(MAX(state.roomMinZ, state.roomMaxZ)));
}
bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc) {
@@ -354,7 +354,7 @@ bool runtimeEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFr
if (!entity.hasFrames() || firstFrame < 0 || lastFrame < firstFrame)
return false;
- return (uint)lastFrame < entity.getFrameCount();
+ return static_cast<uint>(lastFrame) < entity.getFrameCount();
}
bool runtimeEntityHasDrawableFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
@@ -561,7 +561,7 @@ bool resolveRoomMonsterHitAnimationRange(const Entity &entity,
}
int roundRoomCombatFloat(float value) {
- return value >= 0.0f ? (int)(value + 0.5f) : (int)(value - 0.5f);
+ return value >= 0.0f ? static_cast<int>(value + 0.5f) : static_cast<int>(value - 0.5f);
}
int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fallbackY) {
@@ -571,9 +571,9 @@ int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fal
return CLIP<int>(fallbackY, state.roomMaxZScreenY, state.roomMinZScreenY);
const float clampedZ = clampRoomDepthForEvent(state, z);
- const float offset = ((float)state.roomMaxZ - clampedZ) *
- (float)(state.roomMinZScreenY - state.roomMaxZScreenY) /
- (float)(state.roomMaxZ - state.roomMinZ);
+ const float offset = (static_cast<float>(state.roomMaxZ) - clampedZ) *
+ static_cast<float>(state.roomMinZScreenY - state.roomMaxZScreenY) /
+ static_cast<float>(state.roomMaxZ - state.roomMinZ);
return CLIP<int>(state.roomMaxZScreenY + roundRoomCombatFloat(offset),
state.roomMaxZScreenY, state.roomMinZScreenY);
}
@@ -643,7 +643,7 @@ MonsterRecord *Combat::findOverlappingMonsterTarget() const {
Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
- *playerState.entity, playerState.z, *entity, (float)monster.posZ, monster.engageDistance))
+ *playerState.entity, playerState.z, *entity, static_cast<float>(monster.posZ), monster.engageDistance))
return &monster;
}
@@ -1210,8 +1210,8 @@ Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool
const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
MonsterCombatState *combatState =
- monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
- ? &_monsterCombatStates[(uint)monsterIndex]
+ monsterIndex >= 0 && monsterIndex < static_cast<int>(_monsterCombatStates.size())
+ ? &_monsterCombatStates[static_cast<uint>(monsterIndex)]
: nullptr;
if (combatState)
clearRoomMonsterCombatState(*combatState);
@@ -1260,7 +1260,7 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
Script &script = *_engine.getScript();
const uint32 now = Player::getRuntimeClockTicks();
- const uint32 moveInterval = MAX<uint32>(1, 100U / (uint32)kRoomMonsterAnimationRate);
+ const uint32 moveInterval = MAX<uint32>(1, 100U / static_cast<uint32>(kRoomMonsterAnimationRate));
const int horizontalStepBase = 8;
const int depthStep = MAX<int>(1, roundRoomCombatFloat(
_scene.state.roomZVelocityStep > 0.0f ? _scene.state.roomZVelocityStep : 1.0f));
@@ -1351,7 +1351,7 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
monster.posX + knockbackStep, monster.screenMinXBound, monster.screenMaxXBound);
if (monster.posX != previousX) {
(void)applyRoomActorPlacement(
- _scene.state, *entity, monster.posX, monster.posY, (float)monster.posZ);
+ _scene.state, *entity, monster.posX, monster.posY, static_cast<float>(monster.posZ));
(void)script.syncRuntimeMonsterRecord(monster);
debugC(1, kDebugCombat,
"Harvester: combat monster hit knockback target='%s' from_x=%d to_x=%d remaining=%d",
@@ -1454,19 +1454,19 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
const int liveCenterDx = playerCenterX - monsterCenterX;
const int absLiveCenterDx = ABS(liveCenterDx);
const int playerTargetZ = roundRoomCombatFloat(playerState.z);
- const float zDelta = playerState.z - (float)monster.posZ;
+ const float zDelta = playerState.z - static_cast<float>(monster.posZ);
const float absZDelta = zDelta >= 0.0f ? zDelta : -zDelta;
const int engageDistance = MAX(0, monster.engageDistance);
const int liveWaypointTolerance = roundRoomCombatFloat(
Player::computeDepthScale(_scene.state,
- MIN<float>(playerState.z, (float)monster.posZ)) * kNativeMonsterHorizontalWaypointTolerance);
+ MIN<float>(playerState.z, static_cast<float>(monster.posZ))) * kNativeMonsterHorizontalWaypointTolerance);
bool attemptedMoveX = false;
bool attemptedMoveZ = false;
if (absZDelta > kNativeMonsterPursuitZTolerance) {
attemptedMoveZ = true;
monster.posZ = stepTowardsRoomCombatInt(monster.posZ, playerTargetZ, depthStep);
- monster.posZ = (int)clampRoomDepthForEvent(_scene.state, (float)monster.posZ);
- monster.posY = mapRoomDepthToScreenYForCombat(_scene.state, (float)monster.posZ, monster.posY);
+ monster.posZ = static_cast<int>(clampRoomDepthForEvent(_scene.state, static_cast<float>(monster.posZ)));
+ monster.posY = mapRoomDepthToScreenYForCombat(_scene.state, static_cast<float>(monster.posZ), monster.posY);
} else if (absLiveCenterDx > engageDistance) {
// Native class-6 pursuit stores a target frame-left X, not a center X.
const int desiredFrameLeftX = monsterCenterX < playerCenterX
@@ -1475,7 +1475,7 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
if (ABS(monsterRect.left - desiredFrameLeftX) > liveWaypointTolerance) {
attemptedMoveX = true;
const int step = MAX<int>(1, roundRoomCombatFloat(
- Player::computeDepthScale(_scene.state, (float)monster.posZ) * (float)horizontalStepBase));
+ Player::computeDepthScale(_scene.state, static_cast<float>(monster.posZ)) * static_cast<float>(horizontalStepBase)));
const int monsterFrameLeftOffset = monsterRect.left - monster.posX;
const int desiredLogicalCenterX = desiredFrameLeftX - monsterFrameLeftOffset;
monster.posX = stepTowardsRoomCombatInt(monster.posX, desiredLogicalCenterX, step);
@@ -1494,13 +1494,13 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
monster.facing = movedX < 0 ? 1 : 2;
else if (movedY != 0)
monster.facing = movedY < 0 ? 3 : 0;
- (void)applyRoomActorPlacement(_scene.state, *entity, monster.posX, monster.posY, (float)monster.posZ);
+ (void)applyRoomActorPlacement(_scene.state, *entity, monster.posX, monster.posY, static_cast<float>(monster.posZ));
if (monster.posZ != previousZ)
_entityManager->reinsertSceneEntity(entity);
(void)script.syncRuntimeMonsterRecord(monster);
debugC(3, kDebugCombat,
"Harvester: combat monster chase move monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d from=(%d,%d,z=%d) to=(%d,%d,z=%d) facing=%d",
- monster.monsterName.c_str(), liveCenterDx, (double)zDelta, engageDistance, liveWaypointTolerance,
+ monster.monsterName.c_str(), liveCenterDx, static_cast<double>(zDelta), engageDistance, liveWaypointTolerance,
previousX, previousY, previousZ, monster.posX, monster.posY, monster.posZ, monster.facing);
needsRedraw = true;
needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, true) || needsRedraw;
@@ -1510,13 +1510,13 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
if (stalledDirections != 0 && stallChanged) {
debugC(2, kDebugCombat,
"Harvester: combat monster chase stalled monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d pos=(%d,%d,z=%d) attempted_x=%d attempted_z=%d",
- monster.monsterName.c_str(), liveCenterDx, (double)zDelta, engageDistance, liveWaypointTolerance,
+ monster.monsterName.c_str(), liveCenterDx, static_cast<double>(zDelta), engageDistance, liveWaypointTolerance,
monster.posX, monster.posY, monster.posZ, attemptedMoveX, attemptedMoveZ);
}
needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, false) || needsRedraw;
const bool closeEnoughForAttack = isWithinNativeMonsterAttackEntryRange(
- *entity, (float)monster.posZ, *playerState.entity, playerState.z, engageDistance);
+ *entity, static_cast<float>(monster.posZ), *playerState.entity, playerState.z, engageDistance);
// Class-6 monsters store `now + random(99)` when they arm an attack, then
// apply a 0x32-tick gate when deciding whether a later strike may start.
// A fresh zeroed spawn therefore attacks immediately once it reaches engage range.
@@ -1560,7 +1560,7 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
debugC(3, kDebugCombat,
"Harvester: combat monster attack timing monster='%s' target='%s' frames=%d..%d contact=%d resume_facing=%d live_center_dx=%d z_delta=%.2f engage=%d rate=%d attack_seed_tick=%u close_bypass=%d",
monster.monsterName.c_str(), combatState.attackTargetName.c_str(), range.firstFrame, range.lastFrame,
- combatState.attackContactFrame, range.resumeFacing, liveCenterDx, (double)zDelta,
+ combatState.attackContactFrame, range.resumeFacing, liveCenterDx, static_cast<double>(zDelta),
engageDistance, kNativeMonsterAttackAnimationRate, combatState.attackCooldownSeedTick,
closeRangeBypassDistance);
needsRedraw = true;
@@ -1577,15 +1577,11 @@ Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
return Common::kNoError;
}
- _player.markAttackContactResolved();
+ const bool projectile = Player::isProjectileCombatLoadout(playerState.combatLoadout);
+ if (projectile)
+ _player.markAttackContactResolved();
const int damageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
const int damageAmount = Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout);
- debugC(1, kDebugCombat,
- "Harvester: combat player attack contact frame=%d loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s'",
- playerState.entity->getCurrentFrame(), playerState.combatLoadout,
- Player::describeCombatLoadout(playerState.combatLoadout), damageAmount,
- Player::describeCombatDamageType(damageType),
- describeCombatTargetClass(playerState.attackTargetClassId), playerState.attackTargetName.c_str());
Script *script = _engine.getScript();
if (!script)
return Common::kNoError;
@@ -1595,12 +1591,15 @@ Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
Entity *npcEntity = nullptr;
NpcRecord *npc = selectNpcAttackContactTarget(npcEntity);
if (!npc) {
- debugC(1, kDebugCombat,
- "Harvester: combat player attack missed class='npc' target='%s' reason='no reachable npc'",
- playerState.attackTargetName.c_str());
+ if (playerState.entity->didAnimationAdvanceLastTick()) {
+ debugC(3, kDebugCombat,
+ "Harvester: combat player attack missed class='npc' target='%s' frame=%d reason='no reachable npc'",
+ playerState.attackTargetName.c_str(), playerState.entity->getCurrentFrame());
+ }
return Common::kNoError;
}
+ _player.markAttackContactResolved();
const bool runtimeChanged =
script->queueRuntimeNpcDeathOrMonsterfy(npc->npcName, damageType);
const bool jimmyFlagChanged = npc->npcName.equalsIgnoreCase("JIMMY")
@@ -1608,8 +1607,8 @@ Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
: false;
const int npcIndex = findRoomNpcIndexByName(_scene.state, npc->npcName);
NpcCombatState *combatState =
- npcIndex >= 0 && npcIndex < (int)_npcCombatStates.size()
- ? &_npcCombatStates[(uint)npcIndex]
+ npcIndex >= 0 && npcIndex < static_cast<int>(_npcCombatStates.size())
+ ? &_npcCombatStates[static_cast<uint>(npcIndex)]
: nullptr;
if (runtimeChanged) {
npc->deathDamageType = damageType;
@@ -1644,20 +1643,24 @@ Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
Entity *monsterEntity = nullptr;
MonsterRecord *monster = selectMonsterAttackContactTarget(monsterEntity);
if (!monster || !monsterEntity) {
- debugC(1, kDebugCombat,
- "Harvester: combat player attack missed class='monster' target='%s' reason='no reachable monster'",
- playerState.attackTargetName.c_str());
+ if (playerState.entity->didAnimationAdvanceLastTick()) {
+ debugC(3, kDebugCombat,
+ "Harvester: combat player attack missed class='monster' target='%s' frame=%d reason='no reachable monster'",
+ playerState.attackTargetName.c_str(), playerState.entity->getCurrentFrame());
+ }
return Common::kNoError;
}
+ // update_actor_runtime_state (0x4d750) latches melee contact only after a hit.
+ _player.markAttackContactResolved();
const int hitPointsBefore = monster->currentHitPoints;
monster->currentHitPoints = MAX(0,
monster->currentHitPoints - damageAmount);
const int damageLanded = hitPointsBefore - monster->currentHitPoints;
const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
MonsterCombatState *combatState =
- monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
- ? &_monsterCombatStates[(uint)monsterIndex]
+ monsterIndex >= 0 && monsterIndex < static_cast<int>(_monsterCombatStates.size())
+ ? &_monsterCombatStates[static_cast<uint>(monsterIndex)]
: nullptr;
debugC(1, kDebugCombat,
"Harvester: combat player monster hit target='%s' loadout=%d weapon='%s' damage=%d damage_type='%s' hp=%d->%d reachable=1",
@@ -1808,7 +1811,7 @@ CombatEffects::CombatEffects(EntityManager *entityManager) : _entityManager(enti
void CombatEffects::clearHitEffects() {
debugC(2, kDebugCombat, "Harvester: resetting room blood effects records=%u next_id=%u",
- (uint)_hitEffectStates.size(), _nextCombatEffectId);
+ static_cast<uint>(_hitEffectStates.size()), _nextCombatEffectId);
_hitEffectStates.clear();
_nextCombatEffectId = 0;
}
@@ -1915,8 +1918,8 @@ void CombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool combat
const uint32 elapsed = now - popupState.startTick;
const uint32 clampedElapsed = MIN<uint32>(elapsed, kCombatDebugDamagePopupDurationMs);
- const int rise = (int)(
- clampedElapsed * (uint32)kCombatDebugDamagePopupRisePixels /
+ const int rise = static_cast<int>(
+ clampedElapsed * static_cast<uint32>(kCombatDebugDamagePopupRisePixels) /
kCombatDebugDamagePopupDurationMs);
const Common::String popupText = Common::String::format("+%d", popupState.damageAmount);
const int textWidth = font->getStringWidth(popupText);
Commit: e1d97984411ae275054d00b7406c0b24abb93a27
https://github.com/scummvm/scummvm/commit/e1d97984411ae275054d00b7406c0b24abb93a27
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Correct player depth walking and room limits
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 cbec6ab7dc2..1ec1826d225 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -44,7 +44,6 @@ static const float kRoomPlayerDepthTargetSlack = 8.0f;
static const int kRoomRegionTargetXBias = 10;
static const float kRoomDepthCompareEpsilon = 0.01f;
static const int kRoomPlayerWalkAnimationRate = 17;
-static const int kRoomPlayerVerticalScreenStep = 1;
static const int kRoomPlayerFastAnimationRate = 30;
static const int kRoomPlayerHitAnimationRate = 4;
static const int kNativePlayerDeathEntryAnimationRate = 14;
@@ -553,21 +552,6 @@ static int mapRoomDepthToScreenY(const RoomSetupState &state, float z) {
return clampRoomMovementY(state, state.roomMaxZScreenY + roundToInt(offset));
}
-static int resolveFacingFromMovementDelta(int dx, int dy) {
- if (ABS(dx) > ABS(dy))
- return dx < 0 ? 1 : 2;
- if (dy < 0)
- return 3;
- return 0;
-}
-
-static int resolveFacingFromRoomMovement(
- int fromCenterX, int fromBottomY, int toCenterX, int toBottomY) {
- return resolveFacingFromMovementDelta(
- toCenterX - fromCenterX,
- toBottomY - fromBottomY);
-}
-
static int computeRoomPlayerHorizontalStep(const RoomSetupState &state, float z) {
return MAX<int>(1, (int)(Player::computeDepthScale(state, z) * kRoomPlayerHorizontalMoveBase));
}
@@ -576,10 +560,6 @@ static float computeRoomPlayerDepthStep(const RoomSetupState &state) {
return state.roomZVelocityStep > 0.0f ? state.roomZVelocityStep : 1.0f;
}
-static int computeRoomPlayerVerticalScreenStep() {
- return kRoomPlayerVerticalScreenStep;
-}
-
static int computePlayerFrameLeftX(const RoomPlayerState &playerState) {
return playerState.entity ? playerState.entity->getScreenRect().left : playerState.centerX;
}
@@ -588,6 +568,12 @@ static float computePlayerRenderZ(const RoomPlayerState &playerState) {
return playerState.entity ? playerState.entity->getZ() : playerState.z;
}
+static float clampPlayerMovementDepth(const RoomSetupState &state,
+ const RoomPlayerState &playerState, float z) {
+ const float placementOffset = playerState.z - computePlayerRenderZ(playerState);
+ return clampRoomDepth(state, z - placementOffset) + placementOffset;
+}
+
static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
playerState.moveTargetXReached = false;
playerState.moveTargetZReached = false;
@@ -2066,22 +2052,20 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
return false;
int candidateCenterX = playerState.centerX;
- int candidateBottomY = playerState.bottomY;
+ const int candidateBottomY = playerState.bottomY;
float candidateZ = playerState.z;
if (horizontalDirection != 0) {
const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
candidateCenterX = clampPlayerCenterXToNativeBounds(
playerState, playerState.centerX + horizontalDirection * horizontalStep);
- candidateZ = clampRoomDepth(state, playerState.z - depthDirection);
+ candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - depthDirection);
} else if (depthDirection != 0) {
- const int verticalStep = computeRoomPlayerVerticalScreenStep();
const float depthStep = computeRoomPlayerDepthStep(state);
const bool depthBoundaryReached = depthDirection < 0
- ? playerState.z >= (float)state.roomMaxZ
- : playerState.z <= (float)state.roomMinZ;
+ ? computePlayerRenderZ(playerState) >= state.roomMaxZ
+ : computePlayerRenderZ(playerState) <= state.roomMinZ;
if (!depthBoundaryReached) {
- candidateBottomY = playerState.bottomY + depthDirection * verticalStep;
- candidateZ = clampRoomDepth(state, playerState.z - depthDirection * depthStep);
+ candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - depthDirection * depthStep);
}
}
@@ -2191,24 +2175,20 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
return setIdleAnimation(playerState.facing);
}
- const int previousBaseX = playerState.entity->getX();
- const int previousBaseY = playerState.entity->getY();
const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
- const int verticalStep = computeRoomPlayerVerticalScreenStep();
const float depthStep = computeRoomPlayerDepthStep(state);
int candidateCenterX = clampPlayerCenterXToNativeBounds(
playerState, playerState.centerX + horizontalInput * horizontalStep);
- int candidateBottomY = playerState.bottomY;
+ const int candidateBottomY = playerState.bottomY;
float candidateZ = playerState.z;
bool depthBoundaryReached = false;
if (verticalInput != 0) {
depthBoundaryReached = verticalInput < 0
- ? playerState.z >= (float)state.roomMaxZ
- : playerState.z <= (float)state.roomMinZ;
+ ? computePlayerRenderZ(playerState) >= state.roomMaxZ
+ : computePlayerRenderZ(playerState) <= state.roomMinZ;
if (!depthBoundaryReached) {
- candidateBottomY = playerState.bottomY + verticalInput * verticalStep;
- candidateZ = clampRoomDepth(state, playerState.z - verticalInput * depthStep);
+ candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - verticalInput * depthStep);
}
}
if (candidateCenterX == playerState.centerX &&
@@ -2229,8 +2209,8 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
}
- const int desiredFacing = resolveFacingFromRoomMovement(
- playerState.centerX, playerState.bottomY, candidateCenterX, candidateBottomY);
+ const int desiredFacing = resolveFacingFromMoveDirection(
+ horizontalInput, verticalInput, playerState.facing);
if (desiredFacing != playerState.facing && startTurnAnimation(desiredFacing)) {
playerState.keyboardWalkRequest = -1;
return true;
@@ -2246,8 +2226,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
if (!moved)
return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
- const int actualFacing = resolveFacingFromRoomMovement(
- previousBaseX, previousBaseY, playerState.entity->getX(), playerState.entity->getY());
+ const int actualFacing = desiredFacing;
playerState.walkHorizontalDirection = actualFacing == 1 ? -1 : actualFacing == 2 ? 1 : 0;
playerState.walkDepthDirection = actualFacing == 3 ? -1 : actualFacing == 0 ? 1 : 0;
(void)setPlayerWalkAnimation(playerState, actualFacing);
Commit: cfeac713a68e820676e5b07c18e9a1f4a9450a63
https://github.com/scummvm/scummvm/commit/cfeac713a68e820676e5b07c18e9a1f4a9450a63
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate main and room menu navigation
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/menu_navigation.cpp
engines/harvester/menu.cpp
engines/harvester/menu.h
engines/harvester/module.mk
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 15de0f22985..3d0e8849f63 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -28,7 +28,6 @@
#include "common/system.h"
#include "engines/metaengine.h"
#include "graphics/font.h"
-#include "graphics/fontman.h"
#include "graphics/framelimiter.h"
#include "harvester/cft_font.h"
#include "harvester/detection.h"
@@ -50,9 +49,6 @@ static const char *const kLoadGameBitmapPath = "1:/GRAPHIC/OTHER/LOADGAME.BM";
static const char *const kLoadGamePalettePath = "1:/GRAPHIC/PAL/LOADGAME.PAL";
static const char *const kSaveGameBitmapPath = "1:/GRAPHIC/OTHER/SAVEGAME.BM";
static const char *const kSaveGamePalettePath = "1:/GRAPHIC/PAL/SAVEGAME.PAL";
-static const char *const kGameOverBitmapPath = "1:/GRAPHIC/OTHER/GAMEOVER.BM";
-static const char *const kGameOverPalettePath = "1:/GRAPHIC/PAL/GAMEOVER.PAL";
-static const char *const kGameOverMusicPath = "SOUND/MUSIC/ANXIETY.CMP";
static const int kSaveSlotCount = 25;
static const int kSaveSlotLabelX = 8;
static const int kSaveSlotNameX = 0x50;
@@ -67,39 +63,6 @@ static const uint32 kPaletteFadeTickMs = 4;
static const float kPaletteFadeStep = 0.1f;
static const char *const kCdChangePromptPalettePath = "1:/GRAPHIC/PAL/CD1.PAL";
static const float kPaletteBrightnessBlack = 0.0f;
-static const char *const kBlankMenuSlot = " ";
-
-class ScopedSceneTimerPause {
-public:
- explicit ScopedSceneTimerPause(HarvesterEngine &engine) : _engine(engine) {
- pause();
- }
-
- ~ScopedSceneTimerPause() {
- resume();
- }
-
- void pause() {
- if (_paused)
- return;
- if (EntityManager *entityManager = _engine.getRuntimeEntities()) {
- entityManager->pauseTimerCountdowns();
- _paused = true;
- }
- }
-
- void resume() {
- if (!_paused)
- return;
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- entityManager->resumeTimerCountdowns();
- _paused = false;
- }
-
-private:
- HarvesterEngine &_engine;
- bool _paused = false;
-};
static Common::Rect saveSlotRect(int slotIndex) {
const int top = kSaveSlotStartY + slotIndex * kSaveSlotStride;
@@ -126,25 +89,13 @@ static Common::Rect quitConfirmNoRect() {
namespace {
-enum MainMenuItem {
- kMainMenuItemNewGame = 0,
- kMainMenuItemSaveGame,
- kMainMenuItemLoadGame,
- kMainMenuItemOptions,
- kMainMenuItemHelp,
- kMainMenuItemQuitGame
-};
-
-static void buildDisplayMainMenuItems(const Common::Array<Common::String> &source,
- bool canSaveGame, bool canLoadGame, Common::Array<Common::String> &dest) {
- dest = source;
- if (dest.size() > 1 && !canSaveGame)
- dest[1] = kBlankMenuSlot;
- if (dest.size() > 2 && !canLoadGame)
- dest[2] = kBlankMenuSlot;
+static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
+ return Common::String::format("GAME %02d", slotIndex + 1);
}
-static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
+} // End of anonymous namespace
+
+bool MenuSystem::loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
Common::Array<byte> data;
if (!resources.loadFile(path, data) || data.size() < 256 * 3)
return false;
@@ -153,12 +104,6 @@ static bool loadPaletteResource(ResourceManager &resources, const Common::String
return true;
}
-static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
- return Common::String::format("GAME %02d", slotIndex + 1);
-}
-
-} // End of anonymous namespace
-
bool MenuSystem::loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
Common::Array<byte> data;
if (!resources.loadFile(path, data) || data.size() < 12)
@@ -260,477 +205,6 @@ Common::Error MenuSystem::showCdChangePrompt(int discNumber, ResourceManager &re
return Common::kNoError;
}
-Common::Error MenuSystem::runMainMenu(Flow &flow) {
- const Art *art = _engine.getArt();
- if (!art)
- return Common::kReadingFailed;
- ScopedSceneTimerPause pausedTimers(_engine);
-
- Common::Array<Common::String> mainMenuItems;
- buildDisplayMainMenuItems(_menuItems,
- _engine.canSaveGameStateCurrently(),
- _engine.canLoadGameStateCurrently(),
- mainMenuItems);
- if (flow.takePendingGameOverReturn()) {
- Common::Error gameOverError = showGameOverBackdrop(flow);
- if (gameOverError.getCode() != Common::kNoError)
- return gameOverError;
- } else {
- clearMainMenuBackdrop();
- }
-
- Graphics::FrameLimiter limiter(g_system, 60);
- int selectedItem = mainMenuItems.empty() ? -1 : 0;
- Common::String statusMessage;
- bool needsRedraw = true;
-
- auto captureMenuBackdrop = [&](IndexedBitmap &backdrop) -> bool {
- if (_hasMainMenuBackdrop) {
- renderBackdropMenuScreen(_mainMenuBackdrop, _mainMenuBackdropPalette, 1.0f,
- mainMenuItems, selectedItem);
- } else {
- renderMainMenuScreen(selectedItem, statusMessage, false, mainMenuItems);
- }
- Graphics::Screen *screen = _engine.getScreen();
- return screen && captureScreenBackdrop(*screen, backdrop);
- };
- auto runSelectedRoomLoop = [&](const Common::String &targetName) -> Common::Error {
- pausedTimers.resume();
- Common::Error roomError = flow.runRoomLoop(targetName);
- pausedTimers.pause();
- _engine.stopMusic();
- _engine.stopSound();
- if (roomError.getCode() == Common::kReadingFailed) {
- statusMessage = Common::String::format(
- "Unable to resolve %s room setup from HARVEST.SCR.", targetName.c_str());
- needsRedraw = true;
- return Common::kNoError;
- }
- if (roomError.getCode() != Common::kNoError)
- return roomError;
-
- statusMessage.clear();
- needsRedraw = true;
- (void)flow.takePendingMainMenuReturn();
- if (flow.takePendingGameOverReturn()) {
- Common::Error gameOverError = showGameOverBackdrop(flow);
- if (gameOverError.getCode() != Common::kNoError)
- return gameOverError;
- } else {
- clearMainMenuBackdrop();
- }
- return Common::kNoError;
- };
- auto activateSelectedItem = [&]() -> Common::Error {
- if (selectedItem < 0 || selectedItem >= (int)mainMenuItems.size())
- return Common::kNoError;
-
- const Common::String &item = mainMenuItems[selectedItem];
- const byte *menuPalette = _hasMainMenuBackdrop ? _mainMenuBackdropPalette : art->getWaitPalette();
- statusMessage.clear();
- if (item.empty() || item == kBlankMenuSlot)
- return Common::kNoError;
- debugC(2, kDebugGeneral, "Harvester: main menu selected index=%d label='%s'",
- selectedItem, item.c_str());
-
- if (selectedItem == kMainMenuItemNewGame) {
- const MenuTextConfig &config = flow.getMenuTextConfig();
-
- IndexedBitmap menuBackdrop;
- if (!captureMenuBackdrop(menuBackdrop))
- return Common::kReadingFailed;
-
- bool confirmed = false;
- Common::Error confirmError = runConfirmPrompt(
- menuBackdrop, menuPalette, 1.0f, flow, config.newGamePrompt, confirmed);
- if (confirmError.getCode() != Common::kNoError)
- return confirmError;
- needsRedraw = true;
- if (!confirmed)
- return Common::kNoError;
-
- flow.prepareForNewGame();
- return runSelectedRoomLoop("START");
- }
-
- if (selectedItem == kMainMenuItemLoadGame) {
- bool loadedGame = false;
- Common::Error loadError = runLoadGameMenu(menuPalette, 1.0f, flow, loadedGame);
- if (loadError.getCode() != Common::kNoError)
- return loadError;
- needsRedraw = true;
- if (!loadedGame)
- return Common::kNoError;
-
- Common::String targetName = _engine.getPendingLoadedSaveRoomState().entranceName;
- if (targetName.empty())
- targetName = _engine.getPendingLoadedSaveRoomState().roomName;
- if (targetName.empty()) {
- statusMessage = "Loaded save is missing its room target.";
- return Common::kNoError;
- }
-
- return runSelectedRoomLoop(targetName);
- }
-
- if (selectedItem == kMainMenuItemSaveGame) {
- bool savedGame = false;
- Common::Error saveError = runSaveGameMenu(menuPalette, 1.0f, flow, savedGame);
- (void)savedGame;
- needsRedraw = true;
- return saveError;
- }
-
- if (selectedItem == kMainMenuItemOptions) {
- IndexedBitmap 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;
- }
-
- if (selectedItem == kMainMenuItemHelp) {
- Common::Error helpError = runHelpScreen(menuPalette, 1.0f, flow);
- needsRedraw = true;
- return helpError;
- }
-
- if (selectedItem == kMainMenuItemQuitGame) {
- IndexedBitmap menuBackdrop;
- if (!captureMenuBackdrop(menuBackdrop))
- return Common::kReadingFailed;
- Common::Error quitError = runQuitGameConfirm(menuBackdrop, menuPalette, 1.0f, flow);
- needsRedraw = true;
- return quitError;
- }
-
- return Common::kNoError;
- };
-
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- if (_hasMainMenuBackdrop)
- renderBackdropMenuScreen(_mainMenuBackdrop, _mainMenuBackdropPalette, 1.0f, mainMenuItems, selectedItem);
- else
- renderMainMenuScreen(selectedItem, statusMessage, true, mainMenuItems);
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE: {
- const int hoveredItem = _hasMainMenuBackdrop
- ? getBackdropMenuItemAt(_mousePos, mainMenuItems)
- : getMenuItemAt(_mousePos, mainMenuItems);
- if (hoveredItem != -1 && hoveredItem != selectedItem) {
- selectedItem = hoveredItem;
- needsRedraw = true;
- }
- break;
- }
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
- return Common::kNoError;
-
- if (mainMenuItems.empty())
- break;
-
- if (event.kbd.keycode == Common::KEYCODE_UP) {
- selectedItem = (selectedItem + mainMenuItems.size() - 1) % mainMenuItems.size();
- needsRedraw = true;
- } else if (event.kbd.keycode == Common::KEYCODE_DOWN) {
- selectedItem = (selectedItem + 1) % mainMenuItems.size();
- needsRedraw = true;
- } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- Common::Error activateError = activateSelectedItem();
- if (activateError.getCode() != Common::kNoError)
- return activateError;
- }
- break;
- case Common::EVENT_LBUTTONDOWN: {
- if (mainMenuItems.empty())
- break;
-
- selectedItem = _hasMainMenuBackdrop
- ? getBackdropMenuItemAt(_mousePos, mainMenuItems)
- : getMenuItemAt(_mousePos, mainMenuItems);
- if (selectedItem == -1)
- break;
-
- Common::Error activateError = activateSelectedItem();
- if (activateError.getCode() != Common::kNoError)
- return activateError;
- break;
- }
- default:
- break;
- }
- }
-
- if (flow.tickRuntimeEntities())
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-Common::Error MenuSystem::showGameOverBackdrop(Flow &flow) {
- ResourceManager *resources = _engine.getResources();
- Graphics::Screen *screen = _engine.getScreen();
- if (!resources || !screen)
- return Common::kReadingFailed;
-
- IndexedBitmap backdrop;
- byte palette[256 * 3];
- if (!loadBitmapResource(*resources, kGameOverBitmapPath, backdrop) ||
- !loadPaletteResource(*resources, kGameOverPalettePath, palette)) {
- return Common::kReadingFailed;
- }
-
- _mainMenuBackdrop = Common::move(backdrop);
- memcpy(_mainMenuBackdropPalette, palette, sizeof(_mainMenuBackdropPalette));
- _hasMainMenuBackdrop = true;
- flow.resetCursorAnimationSequence();
- (void)_engine.playMusic(kGameOverMusicPath);
-
- bool needsRedraw = true;
- Graphics::FrameLimiter limiter(g_system, 60);
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- MenuRendering::applyMenuPalette(*screen, _engine, _mainMenuBackdropPalette, 1.0f);
- screen->fillRect(screen->getBounds(), 0);
- MenuRendering::blitBitmap(*screen, _mainMenuBackdrop, 0, 0);
- if (_engine.getRuntimeEntities())
- _engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN:
- case Common::EVENT_RBUTTONDOWN:
- case Common::EVENT_KEYDOWN:
- return Common::kNoError;
- default:
- break;
- }
- }
-
- if (EntityManager *entityManager = _engine.getRuntimeEntities()) {
- if (entityManager->syncCursorEntityPosition(_mousePos))
- needsRedraw = true;
- }
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-void MenuSystem::clearMainMenuBackdrop() {
- _mainMenuBackdrop = IndexedBitmap();
- memset(_mainMenuBackdropPalette, 0, sizeof(_mainMenuBackdropPalette));
- _hasMainMenuBackdrop = false;
-}
-
-Common::Error MenuSystem::runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow, bool canSaveGame) {
- Graphics::FrameLimiter limiter(g_system, 60);
- Common::Array<Common::String> roomMenuItems;
- buildDisplayMainMenuItems(_menuItems,
- canSaveGame && _engine.canSaveGameStateCurrently(),
- _engine.canLoadGameStateCurrently(),
- roomMenuItems);
- int selectedItem = roomMenuItems.empty() ? -1 : 0;
- bool needsRedraw = true;
- flow.resetCursorAnimationSequence();
-
- struct RoomMenuActivationResult {
- Common::Error error;
- bool closeMenu;
-
- RoomMenuActivationResult(Common::Error e, bool c) : error(e), closeMenu(c) {}
- };
- auto activateSelectedItem = [&]() -> RoomMenuActivationResult {
- if (selectedItem < 0 || selectedItem >= (int)roomMenuItems.size())
- return RoomMenuActivationResult(Common::kNoError, false);
-
- const Common::String &item = roomMenuItems[selectedItem];
- if (item.empty() || item == kBlankMenuSlot)
- return RoomMenuActivationResult(Common::kNoError, false);
- debugC(2, kDebugGeneral, "Harvester: room menu selected index=%d label='%s'",
- selectedItem, item.c_str());
-
- if (selectedItem == kMainMenuItemNewGame) {
- const MenuTextConfig &config = flow.getMenuTextConfig();
-
- bool confirmed = false;
- Common::Error confirmError = runConfirmPrompt(
- backdrop, palette, paletteBrightness, flow, config.newGamePrompt, confirmed);
- if (confirmError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(confirmError, false);
- needsRedraw = true;
- if (confirmed) {
- flow.requestNewGameRestart();
- return RoomMenuActivationResult(Common::kNoError, true);
- }
- return RoomMenuActivationResult(Common::kNoError, false);
- }
-
- if (selectedItem == kMainMenuItemLoadGame) {
- bool loadedGame = false;
- Common::Error loadError = runLoadGameMenu(palette, paletteBrightness, flow, loadedGame);
- if (loadError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(loadError, false);
- needsRedraw = true;
- return RoomMenuActivationResult(Common::kNoError, loadedGame);
- }
-
- if (selectedItem == kMainMenuItemOptions) {
- Common::Error optionsError = runOptionsMenu(backdrop, palette, paletteBrightness, flow);
- if (optionsError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(optionsError, false);
- needsRedraw = true;
- return RoomMenuActivationResult(Common::kNoError, false);
- }
-
- if (selectedItem == kMainMenuItemHelp) {
- Common::Error helpError = runHelpScreen(palette, paletteBrightness, flow);
- if (helpError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(helpError, false);
- needsRedraw = true;
- return RoomMenuActivationResult(Common::kNoError, false);
- }
-
- if (selectedItem == kMainMenuItemSaveGame) {
- bool savedGame = false;
- Common::Error saveError = runSaveGameMenu(palette, paletteBrightness, flow, savedGame);
- if (saveError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(saveError, false);
- if (savedGame) {
- // Native exits the in-room ESC menu after a successful save and restores the room view.
- Common::Error restoreError = restoreRoomBackdropAfterSave(
- backdrop, palette, paletteBrightness, flow);
- if (restoreError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(restoreError, false);
- return RoomMenuActivationResult(Common::kNoError, true);
- }
- needsRedraw = true;
- return RoomMenuActivationResult(Common::kNoError, false);
- }
-
- if (selectedItem == kMainMenuItemQuitGame) {
- Common::Error quitError = runQuitGameConfirm(backdrop, palette, paletteBrightness, flow);
- if (quitError.getCode() != Common::kNoError)
- return RoomMenuActivationResult(quitError, false);
- needsRedraw = true;
- return RoomMenuActivationResult(Common::kNoError, false);
- }
-
- debugC(1, kDebugGeneral,
- "Harvester: room menu index=%d label='%s' selected but not implemented",
- selectedItem, item.c_str());
- return RoomMenuActivationResult(Common::kNoError, false);
- };
-
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- renderBackdropMenuScreen(backdrop, palette, paletteBrightness, roomMenuItems, selectedItem);
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE: {
- const int hoveredItem = getBackdropMenuItemAt(_mousePos, roomMenuItems);
- if (hoveredItem != -1 && hoveredItem != selectedItem) {
- selectedItem = hoveredItem;
- needsRedraw = true;
- }
- break;
- }
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
- return Common::kNoError;
-
- if (roomMenuItems.empty())
- break;
-
- if (event.kbd.keycode == Common::KEYCODE_UP) {
- selectedItem = (selectedItem + roomMenuItems.size() - 1) % roomMenuItems.size();
- needsRedraw = true;
- } else if (event.kbd.keycode == Common::KEYCODE_DOWN) {
- selectedItem = (selectedItem + 1) % roomMenuItems.size();
- needsRedraw = true;
- } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- RoomMenuActivationResult activationResult = activateSelectedItem();
- if (activationResult.error.getCode() != Common::kNoError)
- return activationResult.error;
- if (activationResult.closeMenu)
- return Common::kNoError;
- }
- break;
- case Common::EVENT_LBUTTONDOWN: {
- if (roomMenuItems.empty())
- break;
-
- selectedItem = getBackdropMenuItemAt(_mousePos, roomMenuItems);
- if (selectedItem == -1)
- break;
-
- RoomMenuActivationResult activationResult = activateSelectedItem();
- if (activationResult.error.getCode() != Common::kNoError)
- return activationResult.error;
- if (activationResult.closeMenu)
- return Common::kNoError;
- break;
- }
- default:
- break;
- }
- }
-
- EntityManager *entityManager = _engine.getRuntimeEntities();
- if (entityManager && entityManager->syncCursorEntityPosition(_mousePos))
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
struct MenuSystem::SaveSlotMenu {
SaveSlotMenu(MetaEngine &meta, const CftFontResource &nameFont, const CftFontResource &labelFont)
: metaEngine(meta), slotNameFont(nameFont), slotLabelFont(labelFont) {}
@@ -1376,36 +850,4 @@ Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBright
return Common::kNoError;
}
-int MenuSystem::getMenuItemAt(const Common::Point &mousePos,
- const Common::Array<Common::String> &menuItems) const {
- const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- if (!font)
- return -1;
-
- for (uint i = 0; i < menuItems.size(); ++i) {
- const int width = font->getStringWidth(menuItems[i]);
- const int x = (640 - width) / 2;
- const int y = MenuRendering::kMenuStartY + (int)i * MenuRendering::kMenuLineSpacing;
- const Common::Rect bounds(x - 12, y - 2, x + width + 12, y + font->getFontHeight() + 2);
- if (bounds.contains(mousePos))
- return (int)i;
- }
-
- return -1;
-}
-
-int MenuSystem::getBackdropMenuItemAt(const Common::Point &mousePos,
- const Common::Array<Common::String> &menuItems) const {
- const Text *text = _engine.getText();
- const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
- if (!selectedFontResource)
- return getMenuItemAt(mousePos, menuItems);
-
- HarvesterCftFont selectedFont(*selectedFontResource);
- if (!selectedFont.isValid())
- return getMenuItemAt(mousePos, menuItems);
-
- return getNativeRoomMenuSelectionFromMouse(selectedFont, menuItems.size(), mousePos);
-}
-
} // End of namespace Harvester
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index 20a3a401a01..71fb256e6e0 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -100,6 +100,23 @@ public:
Flow &flow, bool &promptCompleted);
private:
+ static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette);
+ struct MenuSelection;
+ struct MainMenu;
+ struct RoomMenu;
+ struct RoomMenuActivationResult;
+ enum MenuNavigationCommand { kNoMenuCommand, kActivateMenuItem, kCloseMenu };
+ MenuNavigationCommand handleMenuNavigationEvent(MenuSelection &selection,
+ const Common::Event &event, bool useBackdrop) const;
+ bool captureMainMenuBackdrop(MainMenu &menu, IndexedBitmap &backdrop);
+ Common::Error runMainMenuRoomLoop(MainMenu &menu, const Common::String &targetName);
+ Common::Error startNewGameFromMainMenu(MainMenu &menu, const byte *menuPalette);
+ Common::Error loadGameFromMainMenu(MainMenu &menu, const byte *menuPalette);
+ Common::Error activateMainMenuItem(MainMenu &menu);
+ RoomMenuActivationResult startNewGameFromRoomMenu(RoomMenu &menu);
+ RoomMenuActivationResult saveGameFromRoomMenu(RoomMenu &menu);
+ RoomMenuActivationResult activateRoomMenuItem(RoomMenu &menu);
+
static bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap);
static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFont, uint itemCount,
const Common::Point &mousePos);
diff --git a/engines/harvester/menu_navigation.cpp b/engines/harvester/menu_navigation.cpp
new file mode 100644
index 00000000000..bae825e2ea7
--- /dev/null
+++ b/engines/harvester/menu_navigation.cpp
@@ -0,0 +1,611 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/menu.h"
+#include "harvester/menu_render.h"
+
+#include "common/algorithm.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/fontman.h"
+#include "graphics/framelimiter.h"
+#include "harvester/cft_font.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/room_support.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kGameOverBitmapPath = "1:/GRAPHIC/OTHER/GAMEOVER.BM";
+static const char *const kGameOverPalettePath = "1:/GRAPHIC/PAL/GAMEOVER.PAL";
+static const char *const kGameOverMusicPath = "SOUND/MUSIC/ANXIETY.CMP";
+static const char *const kBlankMenuSlot = " ";
+
+class ScopedSceneTimerPause {
+public:
+ explicit ScopedSceneTimerPause(HarvesterEngine &engine) : _engine(engine) {
+ pause();
+ }
+
+ ~ScopedSceneTimerPause() {
+ resume();
+ }
+
+ void pause() {
+ if (_paused)
+ return;
+ if (EntityManager *entityManager = _engine.getRuntimeEntities()) {
+ entityManager->pauseTimerCountdowns();
+ _paused = true;
+ }
+ }
+
+ void resume() {
+ if (!_paused)
+ return;
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ entityManager->resumeTimerCountdowns();
+ _paused = false;
+ }
+
+private:
+ HarvesterEngine &_engine;
+ bool _paused = false;
+};
+
+enum MainMenuItem {
+ kMainMenuItemNewGame = 0,
+ kMainMenuItemSaveGame,
+ kMainMenuItemLoadGame,
+ kMainMenuItemOptions,
+ kMainMenuItemHelp,
+ kMainMenuItemQuitGame
+};
+
+static void buildDisplayMainMenuItems(const Common::Array<Common::String> &source,
+ bool canSaveGame, bool canLoadGame, Common::Array<Common::String> &dest) {
+ dest = source;
+ if (dest.size() > 1 && !canSaveGame)
+ dest[1] = kBlankMenuSlot;
+ if (dest.size() > 2 && !canLoadGame)
+ dest[2] = kBlankMenuSlot;
+}
+
+} // End of anonymous namespace
+
+struct MenuSystem::MenuSelection {
+ Common::Array<Common::String> items;
+ int selectedItem = -1;
+ bool needsRedraw = true;
+};
+
+struct MenuSystem::MainMenu {
+ MainMenu(HarvesterEngine &engine, const Art &art, Flow &flow)
+ : art(art), flow(flow), pausedTimers(engine) {}
+
+ const Art &art;
+ Flow &flow;
+ ScopedSceneTimerPause pausedTimers;
+ MenuSelection selection;
+ Common::String statusMessage;
+};
+
+struct MenuSystem::RoomMenu {
+ RoomMenu(const IndexedBitmap &backdrop, const byte *palette, float paletteBrightness, Flow &flow)
+ : backdrop(backdrop), palette(palette), paletteBrightness(paletteBrightness), flow(flow) {}
+
+ const IndexedBitmap &backdrop;
+ const byte *palette;
+ float paletteBrightness;
+ Flow &flow;
+ MenuSelection selection;
+};
+
+struct MenuSystem::RoomMenuActivationResult {
+ Common::Error error;
+ bool closeMenu;
+
+ RoomMenuActivationResult(Common::Error e, bool c) : error(e), closeMenu(c) {}
+};
+
+MenuSystem::MenuNavigationCommand MenuSystem::handleMenuNavigationEvent(MenuSelection &selection,
+ const Common::Event &event, bool useBackdrop) const {
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE: {
+ const int hoveredItem = useBackdrop
+ ? getBackdropMenuItemAt(_mousePos, selection.items)
+ : getMenuItemAt(_mousePos, selection.items);
+ if (hoveredItem != -1 && hoveredItem != selection.selectedItem) {
+ selection.selectedItem = hoveredItem;
+ selection.needsRedraw = true;
+ }
+ break;
+ }
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return kCloseMenu;
+
+ if (selection.items.empty())
+ break;
+
+ if (event.kbd.keycode == Common::KEYCODE_UP) {
+ selection.selectedItem = (selection.selectedItem + selection.items.size() - 1) % selection.items.size();
+ selection.needsRedraw = true;
+ } else if (event.kbd.keycode == Common::KEYCODE_DOWN) {
+ selection.selectedItem = (selection.selectedItem + 1) % selection.items.size();
+ selection.needsRedraw = true;
+ } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+ return kActivateMenuItem;
+ }
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ if (selection.items.empty())
+ break;
+
+ selection.selectedItem = useBackdrop
+ ? getBackdropMenuItemAt(_mousePos, selection.items)
+ : getMenuItemAt(_mousePos, selection.items);
+ if (selection.selectedItem == -1)
+ break;
+
+ return kActivateMenuItem;
+ default:
+ break;
+ }
+ return kNoMenuCommand;
+}
+
+bool MenuSystem::captureMainMenuBackdrop(MainMenu &menu, IndexedBitmap &backdrop) {
+ if (_hasMainMenuBackdrop) {
+ renderBackdropMenuScreen(_mainMenuBackdrop, _mainMenuBackdropPalette, 1.0f,
+ menu.selection.items, menu.selection.selectedItem);
+ } else {
+ renderMainMenuScreen(menu.selection.selectedItem, menu.statusMessage, false, menu.selection.items);
+ }
+ Graphics::Screen *screen = _engine.getScreen();
+ return screen && captureScreenBackdrop(*screen, backdrop);
+}
+
+Common::Error MenuSystem::runMainMenuRoomLoop(MainMenu &menu, const Common::String &targetName) {
+ menu.pausedTimers.resume();
+ Common::Error roomError = menu.flow.runRoomLoop(targetName);
+ menu.pausedTimers.pause();
+ _engine.stopMusic();
+ _engine.stopSound();
+ if (roomError.getCode() == Common::kReadingFailed) {
+ menu.statusMessage = Common::String::format(
+ "Unable to resolve %s room setup from HARVEST.SCR.", targetName.c_str());
+ menu.selection.needsRedraw = true;
+ return Common::kNoError;
+ }
+ if (roomError.getCode() != Common::kNoError)
+ return roomError;
+
+ menu.statusMessage.clear();
+ menu.selection.needsRedraw = true;
+ (void)menu.flow.takePendingMainMenuReturn();
+ if (menu.flow.takePendingGameOverReturn()) {
+ Common::Error gameOverError = showGameOverBackdrop(menu.flow);
+ if (gameOverError.getCode() != Common::kNoError)
+ return gameOverError;
+ } else {
+ clearMainMenuBackdrop();
+ }
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::startNewGameFromMainMenu(MainMenu &menu, const byte *menuPalette) {
+ const MenuTextConfig &config = menu.flow.getMenuTextConfig();
+
+ IndexedBitmap menuBackdrop;
+ if (!captureMainMenuBackdrop(menu, menuBackdrop))
+ return Common::kReadingFailed;
+
+ bool confirmed = false;
+ Common::Error confirmError = runConfirmPrompt(
+ menuBackdrop, menuPalette, 1.0f, menu.flow, config.newGamePrompt, confirmed);
+ if (confirmError.getCode() != Common::kNoError)
+ return confirmError;
+ menu.selection.needsRedraw = true;
+ if (!confirmed)
+ return Common::kNoError;
+
+ menu.flow.prepareForNewGame();
+ return runMainMenuRoomLoop(menu, "START");
+}
+
+Common::Error MenuSystem::loadGameFromMainMenu(MainMenu &menu, const byte *menuPalette) {
+ bool loadedGame = false;
+ Common::Error loadError = runLoadGameMenu(menuPalette, 1.0f, menu.flow, loadedGame);
+ if (loadError.getCode() != Common::kNoError)
+ return loadError;
+ menu.selection.needsRedraw = true;
+ if (!loadedGame)
+ return Common::kNoError;
+
+ Common::String targetName = _engine.getPendingLoadedSaveRoomState().entranceName;
+ if (targetName.empty())
+ targetName = _engine.getPendingLoadedSaveRoomState().roomName;
+ if (targetName.empty()) {
+ menu.statusMessage = "Loaded save is missing its room target.";
+ return Common::kNoError;
+ }
+
+ return runMainMenuRoomLoop(menu, targetName);
+}
+
+MenuSystem::RoomMenuActivationResult MenuSystem::startNewGameFromRoomMenu(RoomMenu &menu) {
+ const MenuTextConfig &config = menu.flow.getMenuTextConfig();
+
+ bool confirmed = false;
+ Common::Error confirmError = runConfirmPrompt(
+ menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow, config.newGamePrompt, confirmed);
+ if (confirmError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(confirmError, false);
+ menu.selection.needsRedraw = true;
+ if (confirmed) {
+ menu.flow.requestNewGameRestart();
+ return RoomMenuActivationResult(Common::kNoError, true);
+ }
+ return RoomMenuActivationResult(Common::kNoError, false);
+}
+
+MenuSystem::RoomMenuActivationResult MenuSystem::saveGameFromRoomMenu(RoomMenu &menu) {
+ bool savedGame = false;
+ Common::Error saveError = runSaveGameMenu(menu.palette, menu.paletteBrightness, menu.flow, savedGame);
+ if (saveError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(saveError, false);
+ if (savedGame) {
+ // Native exits the in-room ESC menu after a successful save and restores the room view.
+ Common::Error restoreError = restoreRoomBackdropAfterSave(
+ menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow);
+ if (restoreError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(restoreError, false);
+ return RoomMenuActivationResult(Common::kNoError, true);
+ }
+ menu.selection.needsRedraw = true;
+ return RoomMenuActivationResult(Common::kNoError, false);
+}
+
+Common::Error MenuSystem::activateMainMenuItem(MainMenu &menu) {
+ if (menu.selection.selectedItem < 0 || menu.selection.selectedItem >= (int)menu.selection.items.size())
+ return Common::kNoError;
+
+ const Common::String &item = menu.selection.items[menu.selection.selectedItem];
+ const byte *menuPalette = _hasMainMenuBackdrop ? _mainMenuBackdropPalette : menu.art.getWaitPalette();
+ menu.statusMessage.clear();
+ if (item.empty() || item == kBlankMenuSlot)
+ return Common::kNoError;
+ debugC(2, kDebugGeneral, "Harvester: main menu selected index=%d label='%s'",
+ menu.selection.selectedItem, item.c_str());
+
+ if (menu.selection.selectedItem == kMainMenuItemNewGame)
+ return startNewGameFromMainMenu(menu, menuPalette);
+
+ if (menu.selection.selectedItem == kMainMenuItemLoadGame)
+ return loadGameFromMainMenu(menu, menuPalette);
+
+ if (menu.selection.selectedItem == kMainMenuItemSaveGame) {
+ bool savedGame = false;
+ Common::Error saveError = runSaveGameMenu(menuPalette, 1.0f, menu.flow, savedGame);
+ (void)savedGame;
+ menu.selection.needsRedraw = true;
+ return saveError;
+ }
+
+ if (menu.selection.selectedItem == kMainMenuItemOptions) {
+ IndexedBitmap menuBackdrop;
+ if (_hasMainMenuBackdrop) {
+ menuBackdrop = _mainMenuBackdrop;
+ } else if (!captureMainMenuBackdrop(menu, menuBackdrop)) {
+ return Common::kReadingFailed;
+ }
+ Common::Error optionsError = runOptionsMenu(menuBackdrop, menuPalette, 1.0f, menu.flow);
+ menu.selection.needsRedraw = true;
+ return optionsError;
+ }
+
+ if (menu.selection.selectedItem == kMainMenuItemHelp) {
+ Common::Error helpError = runHelpScreen(menuPalette, 1.0f, menu.flow);
+ menu.selection.needsRedraw = true;
+ return helpError;
+ }
+
+ if (menu.selection.selectedItem == kMainMenuItemQuitGame) {
+ IndexedBitmap menuBackdrop;
+ if (!captureMainMenuBackdrop(menu, menuBackdrop))
+ return Common::kReadingFailed;
+ Common::Error quitError = runQuitGameConfirm(menuBackdrop, menuPalette, 1.0f, menu.flow);
+ menu.selection.needsRedraw = true;
+ return quitError;
+ }
+
+ return Common::kNoError;
+}
+
+MenuSystem::RoomMenuActivationResult MenuSystem::activateRoomMenuItem(RoomMenu &menu) {
+ if (menu.selection.selectedItem < 0 || menu.selection.selectedItem >= (int)menu.selection.items.size())
+ return RoomMenuActivationResult(Common::kNoError, false);
+
+ const Common::String &item = menu.selection.items[menu.selection.selectedItem];
+ if (item.empty() || item == kBlankMenuSlot)
+ return RoomMenuActivationResult(Common::kNoError, false);
+ debugC(2, kDebugGeneral, "Harvester: room menu selected index=%d label='%s'",
+ menu.selection.selectedItem, item.c_str());
+
+ if (menu.selection.selectedItem == kMainMenuItemNewGame)
+ return startNewGameFromRoomMenu(menu);
+
+ if (menu.selection.selectedItem == kMainMenuItemLoadGame) {
+ bool loadedGame = false;
+ Common::Error loadError = runLoadGameMenu(menu.palette, menu.paletteBrightness, menu.flow, loadedGame);
+ if (loadError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(loadError, false);
+ menu.selection.needsRedraw = true;
+ return RoomMenuActivationResult(Common::kNoError, loadedGame);
+ }
+
+ if (menu.selection.selectedItem == kMainMenuItemOptions) {
+ Common::Error optionsError = runOptionsMenu(menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow);
+ if (optionsError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(optionsError, false);
+ menu.selection.needsRedraw = true;
+ return RoomMenuActivationResult(Common::kNoError, false);
+ }
+
+ if (menu.selection.selectedItem == kMainMenuItemHelp) {
+ Common::Error helpError = runHelpScreen(menu.palette, menu.paletteBrightness, menu.flow);
+ if (helpError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(helpError, false);
+ menu.selection.needsRedraw = true;
+ return RoomMenuActivationResult(Common::kNoError, false);
+ }
+
+ if (menu.selection.selectedItem == kMainMenuItemSaveGame)
+ return saveGameFromRoomMenu(menu);
+
+ if (menu.selection.selectedItem == kMainMenuItemQuitGame) {
+ Common::Error quitError = runQuitGameConfirm(menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow);
+ if (quitError.getCode() != Common::kNoError)
+ return RoomMenuActivationResult(quitError, false);
+ menu.selection.needsRedraw = true;
+ return RoomMenuActivationResult(Common::kNoError, false);
+ }
+
+ debugC(1, kDebugGeneral,
+ "Harvester: room menu index=%d label='%s' selected but not implemented",
+ menu.selection.selectedItem, item.c_str());
+ return RoomMenuActivationResult(Common::kNoError, false);
+}
+
+Common::Error MenuSystem::runMainMenu(Flow &flow) {
+ const Art *art = _engine.getArt();
+ if (!art)
+ return Common::kReadingFailed;
+ MainMenu menu(_engine, *art, flow);
+
+ buildDisplayMainMenuItems(_menuItems,
+ _engine.canSaveGameStateCurrently(),
+ _engine.canLoadGameStateCurrently(),
+ menu.selection.items);
+ if (menu.flow.takePendingGameOverReturn()) {
+ Common::Error gameOverError = showGameOverBackdrop(menu.flow);
+ if (gameOverError.getCode() != Common::kNoError)
+ return gameOverError;
+ } else {
+ clearMainMenuBackdrop();
+ }
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ menu.selection.selectedItem = menu.selection.items.empty() ? -1 : 0;
+
+ while (!_engine.shouldQuit()) {
+ if (menu.selection.needsRedraw) {
+ if (_hasMainMenuBackdrop)
+ renderBackdropMenuScreen(_mainMenuBackdrop, _mainMenuBackdropPalette, 1.0f,
+ menu.selection.items, menu.selection.selectedItem);
+ else
+ renderMainMenuScreen(menu.selection.selectedItem, menu.statusMessage, true, menu.selection.items);
+ menu.selection.needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (menu.flow.handleSystemEvent(event, result))
+ return result;
+
+ const MenuNavigationCommand command = handleMenuNavigationEvent(menu.selection, event, _hasMainMenuBackdrop);
+ if (command == kCloseMenu)
+ return Common::kNoError;
+ if (command == kActivateMenuItem) {
+ Common::Error activateError = activateMainMenuItem(menu);
+ if (activateError.getCode() != Common::kNoError)
+ return activateError;
+ }
+ }
+
+ if (menu.flow.tickRuntimeEntities())
+ menu.selection.needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, Flow &flow, bool canSaveGame) {
+ Graphics::FrameLimiter limiter(g_system, 60);
+ RoomMenu menu(backdrop, palette, paletteBrightness, flow);
+ buildDisplayMainMenuItems(_menuItems,
+ canSaveGame && _engine.canSaveGameStateCurrently(),
+ _engine.canLoadGameStateCurrently(),
+ menu.selection.items);
+ menu.selection.selectedItem = menu.selection.items.empty() ? -1 : 0;
+ menu.flow.resetCursorAnimationSequence();
+
+ while (!_engine.shouldQuit()) {
+ if (menu.selection.needsRedraw) {
+ renderBackdropMenuScreen(menu.backdrop, menu.palette, menu.paletteBrightness,
+ menu.selection.items, menu.selection.selectedItem);
+ menu.selection.needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (menu.flow.handleSystemEvent(event, result))
+ return result;
+
+ const MenuNavigationCommand command = handleMenuNavigationEvent(menu.selection, event, true);
+ if (command == kCloseMenu)
+ return Common::kNoError;
+ if (command == kActivateMenuItem) {
+ RoomMenuActivationResult activationResult = activateRoomMenuItem(menu);
+ if (activationResult.error.getCode() != Common::kNoError)
+ return activationResult.error;
+ if (activationResult.closeMenu)
+ return Common::kNoError;
+ }
+ }
+
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ if (entityManager && entityManager->syncCursorEntityPosition(_mousePos))
+ menu.selection.needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::showGameOverBackdrop(Flow &flow) {
+ ResourceManager *resources = _engine.getResources();
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!resources || !screen)
+ return Common::kReadingFailed;
+
+ IndexedBitmap backdrop;
+ byte palette[256 * 3];
+ if (!loadBitmapResource(*resources, kGameOverBitmapPath, backdrop) ||
+ !loadPaletteResource(*resources, kGameOverPalettePath, palette)) {
+ return Common::kReadingFailed;
+ }
+
+ _mainMenuBackdrop = Common::move(backdrop);
+ memcpy(_mainMenuBackdropPalette, palette, sizeof(_mainMenuBackdropPalette));
+ _hasMainMenuBackdrop = true;
+ flow.resetCursorAnimationSequence();
+ (void)_engine.playMusic(kGameOverMusicPath);
+
+ bool needsRedraw = true;
+ Graphics::FrameLimiter limiter(g_system, 60);
+ while (!_engine.shouldQuit()) {
+ if (needsRedraw) {
+ MenuRendering::applyMenuPalette(*screen, _engine, _mainMenuBackdropPalette, 1.0f);
+ screen->fillRect(screen->getBounds(), 0);
+ MenuRendering::blitBitmap(*screen, _mainMenuBackdrop, 0, 0);
+ if (_engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+ needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ case Common::EVENT_RBUTTONDOWN:
+ case Common::EVENT_KEYDOWN:
+ return Common::kNoError;
+ default:
+ break;
+ }
+ }
+
+ if (EntityManager *entityManager = _engine.getRuntimeEntities()) {
+ if (entityManager->syncCursorEntityPosition(_mousePos))
+ needsRedraw = true;
+ }
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+void MenuSystem::clearMainMenuBackdrop() {
+ _mainMenuBackdrop = IndexedBitmap();
+ memset(_mainMenuBackdropPalette, 0, sizeof(_mainMenuBackdropPalette));
+ _hasMainMenuBackdrop = false;
+}
+
+int MenuSystem::getMenuItemAt(const Common::Point &mousePos,
+ const Common::Array<Common::String> &menuItems) const {
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ if (!font)
+ return -1;
+
+ for (uint i = 0; i < menuItems.size(); ++i) {
+ const int width = font->getStringWidth(menuItems[i]);
+ const int x = (640 - width) / 2;
+ const int y = MenuRendering::kMenuStartY + (int)i * MenuRendering::kMenuLineSpacing;
+ const Common::Rect bounds(x - 12, y - 2, x + width + 12, y + font->getFontHeight() + 2);
+ if (bounds.contains(mousePos))
+ return (int)i;
+ }
+
+ return -1;
+}
+
+int MenuSystem::getBackdropMenuItemAt(const Common::Point &mousePos,
+ const Common::Array<Common::String> &menuItems) const {
+ const Text *text = _engine.getText();
+ const CftFontResource *selectedFontResource = text ? text->findFont("HARVFONT") : nullptr;
+ if (!selectedFontResource)
+ return getMenuItemAt(mousePos, menuItems);
+
+ HarvesterCftFont selectedFont(*selectedFontResource);
+ if (!selectedFont.isValid())
+ return getMenuItemAt(mousePos, menuItems);
+
+ return getNativeRoomMenuSelectionFromMouse(selectedFont, menuItems.size(), mousePos);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index bd4adfe4416..237c743e3bf 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -14,6 +14,7 @@ MODULE_OBJS = \
media_manager.o \
menu.o \
menu_config.o \
+ menu_navigation.o \
menu_options.o \
menu_render.o \
metaengine.o \
Commit: 78e23bd564ea0bdceed2fb31b085fe76305c578b
https://github.com/scummvm/scummvm/commit/78e23bd564ea0bdceed2fb31b085fe76305c578b
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate save menus and modal prompts
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/menu_prompts.cpp
A engines/harvester/menu_save.cpp
engines/harvester/menu.cpp
engines/harvester/menu.h
engines/harvester/menu_options.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 3d0e8849f63..67db206a4ef 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -23,75 +23,22 @@
#include "harvester/menu_render.h"
#include "common/algorithm.h"
-#include "common/config-manager.h"
#include "common/events.h"
#include "common/system.h"
-#include "engines/metaengine.h"
#include "graphics/font.h"
#include "graphics/framelimiter.h"
-#include "harvester/cft_font.h"
#include "harvester/detection.h"
#include "harvester/harvester.h"
-#include "harvester/palette_utils.h"
#include "harvester/resources.h"
-#include "harvester/room_support.h"
#include "harvester/entity.h"
#include "harvester/art.h"
#include "harvester/flow.h"
-#include "harvester/script.h"
-#include "harvester/text.h"
namespace Harvester {
namespace {
-static const char *const kLoadGameBitmapPath = "1:/GRAPHIC/OTHER/LOADGAME.BM";
-static const char *const kLoadGamePalettePath = "1:/GRAPHIC/PAL/LOADGAME.PAL";
-static const char *const kSaveGameBitmapPath = "1:/GRAPHIC/OTHER/SAVEGAME.BM";
-static const char *const kSaveGamePalettePath = "1:/GRAPHIC/PAL/SAVEGAME.PAL";
-static const int kSaveSlotCount = 25;
-static const int kSaveSlotLabelX = 8;
-static const int kSaveSlotNameX = 0x50;
-static const int kSaveSlotStartY = 0x19;
-static const int kSaveSlotStride = 0x11;
-static const int kSaveSlotTextWidth = 0x27b - kSaveSlotNameX;
-static const int kSaveStatusTextY = 0x1b8;
-static const int kSaveSlotMaxCharacters = 0xf6;
-static const int kPasswordMaxCharacters = 8;
-static const uint32 kPasswordCursorBlinkMs = 480;
-static const uint32 kPaletteFadeTickMs = 4;
-static const float kPaletteFadeStep = 0.1f;
static const char *const kCdChangePromptPalettePath = "1:/GRAPHIC/PAL/CD1.PAL";
-static const float kPaletteBrightnessBlack = 0.0f;
-
-static Common::Rect saveSlotRect(int slotIndex) {
- const int top = kSaveSlotStartY + slotIndex * kSaveSlotStride;
- return Common::Rect(8, top, 0x27b + 1, top + kSaveSlotStride);
-}
-
-static Common::Rect saveConfirmRect() {
- return Common::Rect(3, 0x1cc, 0x13d + 1, 0x1dc + 1);
-}
-
-static Common::Rect saveCancelRect() {
- return Common::Rect(0x141, 0x1cc, 0x27b + 1, 0x1dc + 1);
-}
-
-static Common::Rect quitConfirmYesRect() {
- return Common::Rect(0xd2, 0xee, 0x104 + 1, 0x108 + 1);
-}
-
-static Common::Rect quitConfirmNoRect() {
- return Common::Rect(0x172, 0xee, 0x19a + 1, 0x108 + 1);
-}
-
-} // End of anonymous namespace
-
-namespace {
-
-static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
- return Common::String::format("GAME %02d", slotIndex + 1);
-}
} // End of anonymous namespace
@@ -205,577 +152,6 @@ Common::Error MenuSystem::showCdChangePrompt(int discNumber, ResourceManager &re
return Common::kNoError;
}
-struct MenuSystem::SaveSlotMenu {
- SaveSlotMenu(MetaEngine &meta, const CftFontResource &nameFont, const CftFontResource &labelFont)
- : metaEngine(meta), slotNameFont(nameFont), slotLabelFont(labelFont) {}
-
- MetaEngine &metaEngine;
- HarvesterCftFont slotNameFont;
- HarvesterCftFont slotLabelFont;
- IndexedBitmap background;
- byte palette[256 * 3];
- Common::Array<Common::String> slotTitles;
- int activeSlot = 0;
- Common::String statusMessage;
- bool needsRedraw = true;
-};
-
-void MenuSystem::reloadSaveSlotTitles(SaveSlotMenu &menu) const {
- menu.slotTitles.clear();
- menu.slotTitles.resize(kSaveSlotCount);
- const SaveStateList saves = menu.metaEngine.listSaves(ConfMan.getActiveDomainName().c_str());
- for (const SaveStateDescriptor &save : saves) {
- const int slot = save.getSaveSlot();
- if (slot >= 0 && slot < kSaveSlotCount)
- menu.slotTitles[slot] = save.getDescription();
- }
-}
-
-int MenuSystem::getSaveSlotAt(const Common::Point &mousePos) const {
- for (int slot = 0; slot < kSaveSlotCount; ++slot) {
- if (saveSlotRect(slot).contains(mousePos))
- return slot;
- }
- return -1;
-}
-
-MenuSystem::SaveSlotCommand MenuSystem::getSaveSlotCommand(const Common::Event &event) const {
- if (event.type == Common::EVENT_KEYDOWN) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
- return kCancelSlot;
- if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER)
- return kConfirmSlot;
- } else if (event.type == Common::EVENT_LBUTTONDOWN) {
- if (saveConfirmRect().contains(_mousePos))
- return kConfirmSlot;
- if (saveCancelRect().contains(_mousePos))
- return kCancelSlot;
- }
- return kNoSlotCommand;
-}
-
-void MenuSystem::renderSaveSlotMenu(const SaveSlotMenu &menu, const Common::String *editingText) const {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen)
- return;
-
- MenuRendering::applyMenuPalette(*screen, _engine, menu.palette, 1.0f);
- MenuRendering::blitBitmap(*screen, menu.background, 0, 0);
-
- for (int i = 0; i < kSaveSlotCount; ++i) {
- const Graphics::Font &labelFont = (i == menu.activeSlot) ? menu.slotNameFont : menu.slotLabelFont;
- const int y = kSaveSlotStartY + i * kSaveSlotStride;
- const Common::String slotLabel = buildDefaultSaveSlotLabel(i);
- labelFont.drawString(screen, slotLabel, kSaveSlotLabelX, y,
- labelFont.getStringWidth(slotLabel), 0);
-
- Common::String displayTitle = menu.slotTitles[i];
- if (i == menu.activeSlot && editingText)
- displayTitle = *editingText + "_";
- menu.slotNameFont.drawString(screen, displayTitle, kSaveSlotNameX, y, kSaveSlotTextWidth, 0);
- }
-
- if (!editingText && !menu.statusMessage.empty()) {
- MenuRendering::drawShadowedString(*screen, menu.slotNameFont, menu.statusMessage,
- MenuRendering::TextLayout(20, kSaveStatusTextY, screen->w - 40),
- MenuRendering::kTextColorNormal, Graphics::kTextAlignCenter);
- }
-
- if (_engine.getRuntimeEntities())
- _engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
-Common::Error MenuSystem::runSaveSlotEditor(SaveSlotMenu &menu, Flow &flow,
- Common::String &editedTitle, bool &confirmed) {
- confirmed = false;
- bool needsRedraw = true;
- flow.resetCursorAnimationSequence();
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- renderSaveSlotMenu(menu, &editedTitle);
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- const SaveSlotCommand command = getSaveSlotCommand(event);
- if (command != kNoSlotCommand) {
- confirmed = command == kConfirmSlot;
- return Common::kNoError;
- }
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- needsRedraw = true;
- break;
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
- needsRedraw |= deleteTextEntryCharacter(editedTitle);
- break;
- }
- if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
- needsRedraw |= appendTextEntryCharacter(editedTitle, menu.slotNameFont,
- event.kbd.ascii, kSaveSlotMaxCharacters, kSaveSlotTextWidth);
- }
- break;
- default:
- break;
- }
- }
-
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- (void)entityManager->syncCursorEntityPosition(_mousePos);
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-void MenuSystem::loadSelectedSlot(SaveSlotMenu &menu, bool &loaded) {
- if (menu.slotTitles[menu.activeSlot].empty())
- return;
-
- Common::Error loadError = _engine.loadGameState(menu.activeSlot);
- if (loadError.getCode() == Common::kNoError) {
- loaded = true;
- return;
- }
-
- menu.statusMessage = loadError.getDesc();
- menu.needsRedraw = true;
-}
-
-Common::Error MenuSystem::saveSelectedSlot(SaveSlotMenu &menu, Flow &flow, bool &saved) {
- saved = false;
- Common::String editedTitle = menu.slotTitles[menu.activeSlot];
- bool confirmed = false;
- Common::Error entryError = runSaveSlotEditor(menu, flow, editedTitle, confirmed);
- if (entryError.getCode() != Common::kNoError)
- return entryError;
- if (!confirmed)
- return Common::kNoError;
-
- Common::Error saveError = _engine.saveGameState(menu.activeSlot, editedTitle);
- if (saveError.getCode() == Common::kNoError) {
- saved = true;
- return Common::kNoError;
- }
-
- menu.statusMessage = saveError.getDesc();
- reloadSaveSlotTitles(menu);
- menu.needsRedraw = true;
- return Common::kNoError;
-}
-
-Common::Error MenuSystem::runLoadGameMenu(const byte *palette, float paletteBrightness,
- Flow &flow, bool &loadedGame) {
- return runSaveSlotMenu(flow, kLoadMode, loadedGame);
-}
-
-Common::Error MenuSystem::runSaveGameMenu(const byte *palette, float paletteBrightness,
- Flow &flow, bool &savedGame) {
- return runSaveSlotMenu(flow, kSaveMode, savedGame);
-}
-
-Common::Error MenuSystem::runSaveSlotMenu(Flow &flow, SaveLoadMode mode, bool &completed) {
- completed = false;
- ResourceManager *resources = _engine.getResources();
- MetaEngine *metaEngine = _engine.getMetaEngine();
- const Text *text = _engine.getText();
- const CftFontResource *slotNameFontResource = text ? text->findFont("MEDFONT1") : nullptr;
- const CftFontResource *slotLabelFontResource = text ? text->findFont("MEDFONT2") : nullptr;
- if (!resources || !metaEngine || !slotNameFontResource || !slotLabelFontResource)
- return Common::kReadingFailed;
- if (mode == kSaveMode ? !_engine.canSaveGameStateCurrently() : !_engine.canLoadGameStateCurrently())
- return Common::kNoError;
-
- SaveSlotMenu menu(*metaEngine, *slotNameFontResource, *slotLabelFontResource);
- if (!menu.slotNameFont.isValid() || !menu.slotLabelFont.isValid())
- return Common::kReadingFailed;
-
- if (!loadBitmapResource(*resources, mode == kSaveMode ? kSaveGameBitmapPath : kLoadGameBitmapPath, menu.background) ||
- !loadPaletteResource(*resources, mode == kSaveMode ? kSaveGamePalettePath : kLoadGamePalettePath, menu.palette)) {
- return Common::kReadingFailed;
- }
-
- reloadSaveSlotTitles(menu);
- if (mode == kLoadMode) {
- for (int slot = 0; slot < kSaveSlotCount; ++slot) {
- if (!menu.slotTitles[slot].empty()) {
- menu.activeSlot = slot;
- break;
- }
- }
- }
-
- flow.resetCursorAnimationSequence();
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit()) {
- if (menu.needsRedraw) {
- renderSaveSlotMenu(menu);
- menu.needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- bool activateSlot = false;
- bool redrawAfterActivation = false;
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE: {
- const int hoveredSlot = getSaveSlotAt(_mousePos);
- if (hoveredSlot != -1 && hoveredSlot != menu.activeSlot)
- menu.activeSlot = hoveredSlot;
- menu.needsRedraw = true;
- break;
- }
- case Common::EVENT_LBUTTONDOWN: {
- const int hoveredSlot = getSaveSlotAt(_mousePos);
- menu.statusMessage.clear();
- if (hoveredSlot != -1) {
- menu.activeSlot = hoveredSlot;
- activateSlot = true;
- // Cancelling a row-opened save editor does not force a browser redraw.
- redrawAfterActivation = mode == kLoadMode;
- } else {
- const SaveSlotCommand command = getSaveSlotCommand(event);
- if (command == kCancelSlot)
- return Common::kNoError;
- activateSlot = command == kConfirmSlot;
- redrawAfterActivation = true;
- }
- break;
- }
- case Common::EVENT_KEYDOWN:
- if (getSaveSlotCommand(event) == kCancelSlot)
- return Common::kNoError;
- if (event.kbd.keycode == Common::KEYCODE_UP || event.kbd.keycode == Common::KEYCODE_DOWN) {
- const int step = event.kbd.keycode == Common::KEYCODE_UP ? kSaveSlotCount - 1 : 1;
- menu.activeSlot = (menu.activeSlot + step) % kSaveSlotCount;
- menu.statusMessage.clear();
- menu.needsRedraw = true;
- } else if (getSaveSlotCommand(event) == kConfirmSlot) {
- menu.statusMessage.clear();
- activateSlot = true;
- redrawAfterActivation = true;
- }
- break;
- default:
- break;
- }
-
- if (activateSlot) {
- Common::Error actionError = Common::kNoError;
- if (mode == kSaveMode)
- actionError = saveSelectedSlot(menu, flow, completed);
- else
- loadSelectedSlot(menu, completed);
- if (actionError.getCode() != Common::kNoError)
- return actionError;
- if (completed)
- return Common::kNoError;
- }
- if (redrawAfterActivation)
- menu.needsRedraw = true;
- }
-
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- (void)entityManager->syncCursorEntityPosition(_mousePos);
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-Common::Error MenuSystem::restoreRoomBackdropAfterSave(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow) const {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen || !backdrop.isValid())
- return Common::kNoError;
-
- byte displayPalette[256 * 3];
- screen->getPalette(displayPalette);
-
- bool hasVisiblePalette = false;
- for (byte value : displayPalette) {
- if (value != 0) {
- hasVisiblePalette = true;
- break;
- }
- }
-
- if (hasVisiblePalette) {
- for (float brightness = 1.0f - kPaletteFadeStep;
- brightness > kPaletteBrightnessBlack; brightness -= kPaletteFadeStep) {
- byte fadedPalette[256 * 3];
- buildHarvesterDisplayPalette(displayPalette, brightness, fadedPalette);
- screen->setPalette(fadedPalette);
- screen->makeAllDirty();
- screen->update();
-
- const uint32 nextTick = g_system->getMillis() + kPaletteFadeTickMs;
- while ((int32)(nextTick - g_system->getMillis()) > 0) {
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
- }
- g_system->delayMillis(1);
- }
- }
-
- byte blackPalette[256 * 3] = { 0 };
- screen->setPalette(blackPalette);
- screen->makeAllDirty();
- screen->update();
- }
-
- screen->fillRect(screen->getBounds(), 0);
- MenuRendering::blitBitmap(*screen, backdrop, 0, 0);
- MenuRendering::applyMenuPalette(*screen, _engine, palette, kPaletteBrightnessBlack);
- screen->makeAllDirty();
- screen->update();
-
- return flow.fadeInRoomScene(palette, paletteBrightness);
-}
-
-Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow, const Common::String &promptText,
- bool &confirmed) {
- const Art *art = _engine.getArt();
- const Text *text = _engine.getText();
- const CftFontResource *promptFontResource = text ? text->findFont("MEDFONT1") : nullptr;
- const CftFontResource *choiceFontResource = text ? text->findFont("MEDFONT2") : nullptr;
- if (!art || !promptFontResource || !choiceFontResource)
- return Common::kReadingFailed;
-
- HarvesterCftFont promptFont(*promptFontResource);
- HarvesterCftFont choiceFont(*choiceFontResource);
- if (!promptFont.isValid() || !choiceFont.isValid())
- return Common::kReadingFailed;
-
- const MenuTextConfig &config = flow.getMenuTextConfig();
- const IndexedBitmap *textbox = art->getTextboxBitmap(3);
- if (!textbox || !textbox->isValid())
- return Common::kReadingFailed;
-
- confirmed = false;
- flow.resetCursorAnimationSequence();
- bool needsRedraw = true;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit()) {
- const bool hoverYes = quitConfirmYesRect().contains(_mousePos);
- const bool hoverNo = quitConfirmNoRect().contains(_mousePos);
- if (needsRedraw) {
- const Graphics::Font &yesFont = hoverYes ? static_cast<const Graphics::Font &>(promptFont)
- : static_cast<const Graphics::Font &>(choiceFont);
- const Graphics::Font &noFont = hoverNo ? static_cast<const Graphics::Font &>(promptFont)
- : static_cast<const Graphics::Font &>(choiceFont);
- MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderConfirmPrompt(
- promptFont, yesFont, noFont, *textbox, promptText, config);
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- needsRedraw = true;
- break;
- case Common::EVENT_RBUTTONDOWN:
- return Common::kNoError;
- case Common::EVENT_LBUTTONDOWN:
- if (quitConfirmYesRect().contains(_mousePos)) {
- confirmed = true;
- return Common::kNoError;
- }
- if (quitConfirmNoRect().contains(_mousePos))
- return Common::kNoError;
- break;
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
- return Common::kNoError;
- if (event.kbd.keycode == Common::KEYCODE_y || event.kbd.ascii == 'y' || event.kbd.ascii == 'Y') {
- confirmed = true;
- return Common::kNoError;
- }
- if (event.kbd.keycode == Common::KEYCODE_n || event.kbd.ascii == 'n' || event.kbd.ascii == 'N')
- return Common::kNoError;
- break;
- default:
- break;
- }
- }
-
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- (void)entityManager->syncCursorEntityPosition(_mousePos);
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-Common::Error MenuSystem::runQuitGameConfirm(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow) {
- const MenuTextConfig &config = flow.getMenuTextConfig();
-
- bool confirmed = false;
- Common::Error confirmError = runConfirmPrompt(
- backdrop, palette, paletteBrightness, flow, config.quitGamePrompt, confirmed);
- if (confirmError.getCode() != Common::kNoError)
- return confirmError;
- if (!confirmed)
- return Common::kNoError;
-
- _engine.stopMusic();
- _engine.stopSound();
- _engine.quitGame();
- return Common::kNoError;
-}
-
-Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow, bool drawLogo, Common::String &password,
- bool &accepted) const {
- const Art *art = _engine.getArt();
- const Text *text = _engine.getText();
- const CftFontResource *titleFontResource = text ? text->findFont("HARVFONT") : nullptr;
- const CftFontResource *entryFontResource = text ? text->findFont("HARVFNT2") : nullptr;
- if (!art || !titleFontResource || !entryFontResource)
- return Common::kReadingFailed;
-
- HarvesterCftFont titleFont(*titleFontResource);
- HarvesterCftFont entryFont(*entryFontResource);
- if (!titleFont.isValid() || !entryFont.isValid())
- return Common::kReadingFailed;
-
- password.clear();
- accepted = false;
- bool needsRedraw = true;
- bool cursorVisible = true;
- uint32 cursorToggleTicks = g_system->getMillis() + kPasswordCursorBlinkMs;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderPasswordPrompt(
- MenuRendering::FontPair(titleFont, entryFont), *art,
- flow.getMenuTextConfig().enterPasswordLabel, password, cursorVisible, drawLogo);
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_RBUTTONDOWN:
- return Common::kNoError;
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
- return Common::kNoError;
- if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- accepted = !password.empty();
- return Common::kNoError;
- }
- if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
- event.kbd.keycode == Common::KEYCODE_LEFT) {
- needsRedraw |= deleteTextEntryCharacter(password);
- break;
- }
- if (event.kbd.keycode == Common::KEYCODE_HOME) {
- if (!password.empty()) {
- password.clear();
- needsRedraw = true;
- }
- break;
- }
- if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
- needsRedraw |= appendTextEntryCharacter(password, entryFont, event.kbd.ascii,
- kPasswordMaxCharacters, MenuRendering::kPasswordEntryWidth);
- }
- break;
- default:
- break;
- }
- }
-
- const uint32 now = g_system->getMillis();
- if ((int32)(now - cursorToggleTicks) >= 0) {
- cursorVisible = !cursorVisible;
- cursorToggleTicks = now + kPasswordCursorBlinkMs;
- needsRedraw = true;
- }
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-Common::Error MenuSystem::validateParentalPassword(Flow &flow) {
- Script *script = _engine.getScript();
- Graphics::Screen *screen = _engine.getScreen();
- if (!script || !screen)
- return Common::kReadingFailed;
-
- const Common::String &configuredPassword = script->getParentalPassword();
- if (configuredPassword.empty()) {
- debugC(2, kDebugGeneral, "Harvester: parental password startup gate bypassed state=disabled");
- return Common::kNoError;
- }
-
- IndexedBitmap backdrop;
- if (!captureScreenBackdrop(*screen, backdrop))
- return Common::kReadingFailed;
-
- debugC(1, kDebugGeneral, "Harvester: parental password startup gate active");
- Common::String enteredPassword;
- bool accepted = false;
- Common::Error promptError = runPasswordPrompt(
- backdrop, nullptr, 1.0f, flow, false, enteredPassword, accepted);
- if (promptError.getCode() != Common::kNoError || _engine.shouldQuit())
- return promptError;
-
- if (!accepted || !configuredPassword.equalsIgnoreCase(enteredPassword)) {
- debugC(1, kDebugGeneral,
- "Harvester: parental password blocked startup reason=%s",
- accepted ? "mismatch" : "empty-or-cancelled");
- _engine.quitGame();
- return Common::kNoError;
- }
-
- debugC(1, kDebugGeneral, "Harvester: parental password accepted; startup permitted");
- return Common::kNoError;
-}
-
Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBrightness, Flow &flow) {
ResourceManager *resources = _engine.getResources();
if (!resources)
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index 71fb256e6e0..a1df12c29fb 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -135,9 +135,12 @@ private:
const Graphics::Font &selectedFont, bool &closed);
struct SaveSlotMenu;
+ struct SaveSlotEventResult;
enum SaveLoadMode { kSaveMode, kLoadMode };
enum SaveSlotCommand { kNoSlotCommand, kConfirmSlot, kCancelSlot };
+ SaveSlotEventResult handleSaveSlotEvent(SaveSlotMenu &menu, SaveLoadMode mode,
+ const Common::Event &event) const;
Common::Error runSaveSlotMenu(Flow &flow, SaveLoadMode mode, bool &completed);
void reloadSaveSlotTitles(SaveSlotMenu &menu) const;
int getSaveSlotAt(const Common::Point &mousePos) const;
@@ -162,9 +165,12 @@ private:
bool &confirmed);
Common::Error runQuitGameConfirm(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, Flow &flow);
+ struct PasswordPromptResult {
+ Common::String password;
+ bool accepted = false;
+ };
Common::Error runPasswordPrompt(const IndexedBitmap &backdrop, const byte *palette,
- float paletteBrightness, Flow &flow, bool drawLogo, Common::String &password,
- bool &accepted) const;
+ float paletteBrightness, Flow &flow, bool drawLogo, PasswordPromptResult &prompt) const;
Common::Error showGameOverBackdrop(Flow &flow);
void clearMainMenuBackdrop();
void renderMainMenuScreen(int selectedItem, const Common::String &statusMessage,
diff --git a/engines/harvester/menu_options.cpp b/engines/harvester/menu_options.cpp
index 14069aa293b..743ef42f384 100644
--- a/engines/harvester/menu_options.cpp
+++ b/engines/harvester/menu_options.cpp
@@ -142,18 +142,17 @@ void MenuSystem::toggleOptionsGore(OptionsMenu &menu) {
void MenuSystem::toggleOptionsPassword(OptionsMenu &menu) {
if (menu.script.getParentalPassword().empty()) {
debugC(2, kDebugGeneral, "Harvester: parental password set prompt opened");
- Common::String password;
- bool accepted = false;
+ PasswordPromptResult prompt;
Common::Error promptError = runPasswordPrompt(
- menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow, true, password, accepted);
+ menu.backdrop, menu.palette, menu.paletteBrightness, menu.flow, true, prompt);
if (promptError.getCode() != Common::kNoError) {
debugC(1, kDebugGeneral,
"Harvester: parental password set prompt failed error=%d",
static_cast<int>(promptError.getCode()));
return;
}
- if (accepted) {
- menu.script.setParentalPassword(password);
+ if (prompt.accepted) {
+ menu.script.setParentalPassword(prompt.password);
if (!menu.script.saveConfig()) {
menu.script.setParentalPassword(Common::String());
debugC(1, kDebugGeneral,
diff --git a/engines/harvester/menu_prompts.cpp b/engines/harvester/menu_prompts.cpp
new file mode 100644
index 00000000000..e15a4d85f7c
--- /dev/null
+++ b/engines/harvester/menu_prompts.cpp
@@ -0,0 +1,268 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/menu.h"
+#include "harvester/menu_render.h"
+
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/framelimiter.h"
+#include "harvester/cft_font.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/room_support.h"
+#include "harvester/script.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+static const int kPasswordMaxCharacters = 8;
+static const uint32 kPasswordCursorBlinkMs = 480;
+
+static Common::Rect quitConfirmYesRect() {
+ return Common::Rect(0xd2, 0xee, 0x104 + 1, 0x108 + 1);
+}
+
+static Common::Rect quitConfirmNoRect() {
+ return Common::Rect(0x172, 0xee, 0x19a + 1, 0x108 + 1);
+}
+
+Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, Flow &flow, const Common::String &promptText,
+ bool &confirmed) {
+ const Art *art = _engine.getArt();
+ const Text *text = _engine.getText();
+ const CftFontResource *promptFontResource = text ? text->findFont("MEDFONT1") : nullptr;
+ const CftFontResource *choiceFontResource = text ? text->findFont("MEDFONT2") : nullptr;
+ if (!art || !promptFontResource || !choiceFontResource)
+ return Common::kReadingFailed;
+
+ HarvesterCftFont promptFont(*promptFontResource);
+ HarvesterCftFont choiceFont(*choiceFontResource);
+ if (!promptFont.isValid() || !choiceFont.isValid())
+ return Common::kReadingFailed;
+
+ const MenuTextConfig &config = flow.getMenuTextConfig();
+ const IndexedBitmap *textbox = art->getTextboxBitmap(3);
+ if (!textbox || !textbox->isValid())
+ return Common::kReadingFailed;
+
+ confirmed = false;
+ flow.resetCursorAnimationSequence();
+ bool needsRedraw = true;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit()) {
+ const bool hoverYes = quitConfirmYesRect().contains(_mousePos);
+ const bool hoverNo = quitConfirmNoRect().contains(_mousePos);
+ if (needsRedraw) {
+ const Graphics::Font &yesFont = hoverYes ? static_cast<const Graphics::Font &>(promptFont)
+ : static_cast<const Graphics::Font &>(choiceFont);
+ const Graphics::Font &noFont = hoverNo ? static_cast<const Graphics::Font &>(promptFont)
+ : static_cast<const Graphics::Font &>(choiceFont);
+ MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderConfirmPrompt(
+ promptFont, yesFont, noFont, *textbox, promptText, config);
+ needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ needsRedraw = true;
+ break;
+ case Common::EVENT_RBUTTONDOWN:
+ return Common::kNoError;
+ case Common::EVENT_LBUTTONDOWN:
+ if (quitConfirmYesRect().contains(_mousePos)) {
+ confirmed = true;
+ return Common::kNoError;
+ }
+ if (quitConfirmNoRect().contains(_mousePos))
+ return Common::kNoError;
+ break;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return Common::kNoError;
+ if (event.kbd.keycode == Common::KEYCODE_y || event.kbd.ascii == 'y' || event.kbd.ascii == 'Y') {
+ confirmed = true;
+ return Common::kNoError;
+ }
+ if (event.kbd.keycode == Common::KEYCODE_n || event.kbd.ascii == 'n' || event.kbd.ascii == 'N')
+ return Common::kNoError;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ (void)entityManager->syncCursorEntityPosition(_mousePos);
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::runQuitGameConfirm(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, Flow &flow) {
+ const MenuTextConfig &config = flow.getMenuTextConfig();
+
+ bool confirmed = false;
+ Common::Error confirmError = runConfirmPrompt(
+ backdrop, palette, paletteBrightness, flow, config.quitGamePrompt, confirmed);
+ if (confirmError.getCode() != Common::kNoError)
+ return confirmError;
+ if (!confirmed)
+ return Common::kNoError;
+
+ _engine.stopMusic();
+ _engine.stopSound();
+ _engine.quitGame();
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, Flow &flow, bool drawLogo, PasswordPromptResult &prompt) const {
+ const Art *art = _engine.getArt();
+ const Text *text = _engine.getText();
+ const CftFontResource *titleFontResource = text ? text->findFont("HARVFONT") : nullptr;
+ const CftFontResource *entryFontResource = text ? text->findFont("HARVFNT2") : nullptr;
+ if (!art || !titleFontResource || !entryFontResource)
+ return Common::kReadingFailed;
+
+ HarvesterCftFont titleFont(*titleFontResource);
+ HarvesterCftFont entryFont(*entryFontResource);
+ if (!titleFont.isValid() || !entryFont.isValid())
+ return Common::kReadingFailed;
+
+ prompt.password.clear();
+ prompt.accepted = false;
+ bool needsRedraw = true;
+ bool cursorVisible = true;
+ uint32 cursorToggleTicks = g_system->getMillis() + kPasswordCursorBlinkMs;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit()) {
+ if (needsRedraw) {
+ MenuRendering::Renderer(_engine, backdrop, palette, paletteBrightness).renderPasswordPrompt(
+ MenuRendering::FontPair(titleFont, entryFont), *art,
+ flow.getMenuTextConfig().enterPasswordLabel, prompt.password, cursorVisible, drawLogo);
+ needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+
+ switch (event.type) {
+ case Common::EVENT_RBUTTONDOWN:
+ return Common::kNoError;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return Common::kNoError;
+ if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+ prompt.accepted = !prompt.password.empty();
+ return Common::kNoError;
+ }
+ if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
+ event.kbd.keycode == Common::KEYCODE_LEFT) {
+ needsRedraw |= deleteTextEntryCharacter(prompt.password);
+ break;
+ }
+ if (event.kbd.keycode == Common::KEYCODE_HOME) {
+ if (!prompt.password.empty()) {
+ prompt.password.clear();
+ needsRedraw = true;
+ }
+ break;
+ }
+ if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
+ needsRedraw |= appendTextEntryCharacter(prompt.password, entryFont, event.kbd.ascii,
+ kPasswordMaxCharacters, MenuRendering::kPasswordEntryWidth);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ const uint32 now = g_system->getMillis();
+ if ((int32)(now - cursorToggleTicks) >= 0) {
+ cursorVisible = !cursorVisible;
+ cursorToggleTicks = now + kPasswordCursorBlinkMs;
+ needsRedraw = true;
+ }
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::validateParentalPassword(Flow &flow) {
+ Script *script = _engine.getScript();
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!script || !screen)
+ return Common::kReadingFailed;
+
+ const Common::String &configuredPassword = script->getParentalPassword();
+ if (configuredPassword.empty()) {
+ debugC(2, kDebugGeneral, "Harvester: parental password startup gate bypassed state=disabled");
+ return Common::kNoError;
+ }
+
+ IndexedBitmap backdrop;
+ if (!captureScreenBackdrop(*screen, backdrop))
+ return Common::kReadingFailed;
+
+ debugC(1, kDebugGeneral, "Harvester: parental password startup gate active");
+ PasswordPromptResult prompt;
+ Common::Error promptError = runPasswordPrompt(
+ backdrop, nullptr, 1.0f, flow, false, prompt);
+ if (promptError.getCode() != Common::kNoError || _engine.shouldQuit())
+ return promptError;
+
+ if (!prompt.accepted || !configuredPassword.equalsIgnoreCase(prompt.password)) {
+ debugC(1, kDebugGeneral,
+ "Harvester: parental password blocked startup reason=%s",
+ prompt.accepted ? "mismatch" : "empty-or-cancelled");
+ _engine.quitGame();
+ return Common::kNoError;
+ }
+
+ debugC(1, kDebugGeneral, "Harvester: parental password accepted; startup permitted");
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/menu_save.cpp b/engines/harvester/menu_save.cpp
new file mode 100644
index 00000000000..607f52d3213
--- /dev/null
+++ b/engines/harvester/menu_save.cpp
@@ -0,0 +1,438 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/menu.h"
+#include "harvester/menu_render.h"
+
+#include "common/algorithm.h"
+#include "common/config-manager.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "engines/metaengine.h"
+#include "graphics/framelimiter.h"
+#include "harvester/cft_font.h"
+#include "harvester/entity.h"
+#include "harvester/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/palette_utils.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+static const char *const kLoadGameBitmapPath = "1:/GRAPHIC/OTHER/LOADGAME.BM";
+static const char *const kLoadGamePalettePath = "1:/GRAPHIC/PAL/LOADGAME.PAL";
+static const char *const kSaveGameBitmapPath = "1:/GRAPHIC/OTHER/SAVEGAME.BM";
+static const char *const kSaveGamePalettePath = "1:/GRAPHIC/PAL/SAVEGAME.PAL";
+static const int kSaveSlotCount = 25;
+static const int kSaveSlotLabelX = 8;
+static const int kSaveSlotNameX = 0x50;
+static const int kSaveSlotStartY = 0x19;
+static const int kSaveSlotStride = 0x11;
+static const int kSaveSlotTextWidth = 0x27b - kSaveSlotNameX;
+static const int kSaveStatusTextY = 0x1b8;
+static const int kSaveSlotMaxCharacters = 0xf6;
+static const uint32 kPaletteFadeTickMs = 4;
+static const float kPaletteFadeStep = 0.1f;
+static const float kPaletteBrightnessBlack = 0.0f;
+
+static Common::Rect saveSlotRect(int slotIndex) {
+ const int top = kSaveSlotStartY + slotIndex * kSaveSlotStride;
+ return Common::Rect(8, top, 0x27b + 1, top + kSaveSlotStride);
+}
+
+static Common::Rect saveConfirmRect() {
+ return Common::Rect(3, 0x1cc, 0x13d + 1, 0x1dc + 1);
+}
+
+static Common::Rect saveCancelRect() {
+ return Common::Rect(0x141, 0x1cc, 0x27b + 1, 0x1dc + 1);
+}
+
+static Common::String buildDefaultSaveSlotLabel(int slotIndex) {
+ return Common::String::format("GAME %02d", slotIndex + 1);
+}
+
+struct MenuSystem::SaveSlotMenu {
+ SaveSlotMenu(MetaEngine &meta, const CftFontResource &nameFont, const CftFontResource &labelFont)
+ : metaEngine(meta), slotNameFont(nameFont), slotLabelFont(labelFont) {}
+
+ MetaEngine &metaEngine;
+ HarvesterCftFont slotNameFont;
+ HarvesterCftFont slotLabelFont;
+ IndexedBitmap background;
+ byte palette[256 * 3];
+ Common::Array<Common::String> slotTitles;
+ int activeSlot = 0;
+ Common::String statusMessage;
+ bool needsRedraw = true;
+};
+
+struct MenuSystem::SaveSlotEventResult {
+ explicit SaveSlotEventResult(SaveSlotCommand command = kNoSlotCommand) : command(command) {}
+
+ SaveSlotCommand command;
+ bool redrawAfterActivation = false;
+};
+
+void MenuSystem::reloadSaveSlotTitles(SaveSlotMenu &menu) const {
+ menu.slotTitles.clear();
+ menu.slotTitles.resize(kSaveSlotCount);
+ const SaveStateList saves = menu.metaEngine.listSaves(ConfMan.getActiveDomainName().c_str());
+ for (const SaveStateDescriptor &save : saves) {
+ const int slot = save.getSaveSlot();
+ if (slot >= 0 && slot < kSaveSlotCount)
+ menu.slotTitles[slot] = save.getDescription();
+ }
+}
+
+int MenuSystem::getSaveSlotAt(const Common::Point &mousePos) const {
+ for (int slot = 0; slot < kSaveSlotCount; ++slot) {
+ if (saveSlotRect(slot).contains(mousePos))
+ return slot;
+ }
+ return -1;
+}
+
+MenuSystem::SaveSlotCommand MenuSystem::getSaveSlotCommand(const Common::Event &event) const {
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return kCancelSlot;
+ if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER)
+ return kConfirmSlot;
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ if (saveConfirmRect().contains(_mousePos))
+ return kConfirmSlot;
+ if (saveCancelRect().contains(_mousePos))
+ return kCancelSlot;
+ }
+ return kNoSlotCommand;
+}
+
+void MenuSystem::renderSaveSlotMenu(const SaveSlotMenu &menu, const Common::String *editingText) const {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return;
+
+ MenuRendering::applyMenuPalette(*screen, _engine, menu.palette, 1.0f);
+ MenuRendering::blitBitmap(*screen, menu.background, 0, 0);
+
+ for (int i = 0; i < kSaveSlotCount; ++i) {
+ const Graphics::Font &labelFont = (i == menu.activeSlot) ? menu.slotNameFont : menu.slotLabelFont;
+ const int y = kSaveSlotStartY + i * kSaveSlotStride;
+ const Common::String slotLabel = buildDefaultSaveSlotLabel(i);
+ labelFont.drawString(screen, slotLabel, kSaveSlotLabelX, y,
+ labelFont.getStringWidth(slotLabel), 0);
+
+ Common::String displayTitle = menu.slotTitles[i];
+ if (i == menu.activeSlot && editingText)
+ displayTitle = *editingText + "_";
+ menu.slotNameFont.drawString(screen, displayTitle, kSaveSlotNameX, y, kSaveSlotTextWidth, 0);
+ }
+
+ if (!editingText && !menu.statusMessage.empty()) {
+ MenuRendering::drawShadowedString(*screen, menu.slotNameFont, menu.statusMessage,
+ MenuRendering::TextLayout(20, kSaveStatusTextY, screen->w - 40),
+ MenuRendering::kTextColorNormal, Graphics::kTextAlignCenter);
+ }
+
+ if (_engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+Common::Error MenuSystem::runSaveSlotEditor(SaveSlotMenu &menu, Flow &flow,
+ Common::String &editedTitle, bool &confirmed) {
+ confirmed = false;
+ bool needsRedraw = true;
+ flow.resetCursorAnimationSequence();
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit()) {
+ if (needsRedraw) {
+ renderSaveSlotMenu(menu, &editedTitle);
+ needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+
+ const SaveSlotCommand command = getSaveSlotCommand(event);
+ if (command != kNoSlotCommand) {
+ confirmed = command == kConfirmSlot;
+ return Common::kNoError;
+ }
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ needsRedraw = true;
+ break;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
+ needsRedraw |= deleteTextEntryCharacter(editedTitle);
+ break;
+ }
+ if (event.kbd.ascii >= 32 && event.kbd.ascii <= 126 && event.kbd.ascii != '~') {
+ needsRedraw |= appendTextEntryCharacter(editedTitle, menu.slotNameFont,
+ event.kbd.ascii, kSaveSlotMaxCharacters, kSaveSlotTextWidth);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ (void)entityManager->syncCursorEntityPosition(_mousePos);
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+void MenuSystem::loadSelectedSlot(SaveSlotMenu &menu, bool &loaded) {
+ if (menu.slotTitles[menu.activeSlot].empty())
+ return;
+
+ Common::Error loadError = _engine.loadGameState(menu.activeSlot);
+ if (loadError.getCode() == Common::kNoError) {
+ loaded = true;
+ return;
+ }
+
+ menu.statusMessage = loadError.getDesc();
+ menu.needsRedraw = true;
+}
+
+Common::Error MenuSystem::saveSelectedSlot(SaveSlotMenu &menu, Flow &flow, bool &saved) {
+ saved = false;
+ Common::String editedTitle = menu.slotTitles[menu.activeSlot];
+ bool confirmed = false;
+ Common::Error entryError = runSaveSlotEditor(menu, flow, editedTitle, confirmed);
+ if (entryError.getCode() != Common::kNoError)
+ return entryError;
+ if (!confirmed)
+ return Common::kNoError;
+
+ Common::Error saveError = _engine.saveGameState(menu.activeSlot, editedTitle);
+ if (saveError.getCode() == Common::kNoError) {
+ saved = true;
+ return Common::kNoError;
+ }
+
+ menu.statusMessage = saveError.getDesc();
+ reloadSaveSlotTitles(menu);
+ menu.needsRedraw = true;
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::runLoadGameMenu(const byte *palette, float paletteBrightness,
+ Flow &flow, bool &loadedGame) {
+ return runSaveSlotMenu(flow, kLoadMode, loadedGame);
+}
+
+Common::Error MenuSystem::runSaveGameMenu(const byte *palette, float paletteBrightness,
+ Flow &flow, bool &savedGame) {
+ return runSaveSlotMenu(flow, kSaveMode, savedGame);
+}
+
+MenuSystem::SaveSlotEventResult MenuSystem::handleSaveSlotEvent(SaveSlotMenu &menu,
+ SaveLoadMode mode, const Common::Event &event) const {
+ SaveSlotEventResult result;
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE: {
+ const int hoveredSlot = getSaveSlotAt(_mousePos);
+ if (hoveredSlot != -1 && hoveredSlot != menu.activeSlot)
+ menu.activeSlot = hoveredSlot;
+ menu.needsRedraw = true;
+ break;
+ }
+ case Common::EVENT_LBUTTONDOWN: {
+ const int hoveredSlot = getSaveSlotAt(_mousePos);
+ menu.statusMessage.clear();
+ if (hoveredSlot != -1) {
+ menu.activeSlot = hoveredSlot;
+ result.command = kConfirmSlot;
+ // Cancelling a row-opened save editor does not force a browser redraw.
+ result.redrawAfterActivation = mode == kLoadMode;
+ } else {
+ const SaveSlotCommand command = getSaveSlotCommand(event);
+ if (command == kCancelSlot)
+ return SaveSlotEventResult(kCancelSlot);
+ result.command = command;
+ result.redrawAfterActivation = true;
+ }
+ break;
+ }
+ case Common::EVENT_KEYDOWN:
+ if (getSaveSlotCommand(event) == kCancelSlot)
+ return SaveSlotEventResult(kCancelSlot);
+ if (event.kbd.keycode == Common::KEYCODE_UP || event.kbd.keycode == Common::KEYCODE_DOWN) {
+ const int step = event.kbd.keycode == Common::KEYCODE_UP ? kSaveSlotCount - 1 : 1;
+ menu.activeSlot = (menu.activeSlot + step) % kSaveSlotCount;
+ menu.statusMessage.clear();
+ menu.needsRedraw = true;
+ } else if (getSaveSlotCommand(event) == kConfirmSlot) {
+ menu.statusMessage.clear();
+ result.command = kConfirmSlot;
+ result.redrawAfterActivation = true;
+ }
+ break;
+ default:
+ break;
+ }
+ return result;
+}
+
+Common::Error MenuSystem::runSaveSlotMenu(Flow &flow, SaveLoadMode mode, bool &completed) {
+ completed = false;
+ ResourceManager *resources = _engine.getResources();
+ MetaEngine *metaEngine = _engine.getMetaEngine();
+ const Text *text = _engine.getText();
+ const CftFontResource *slotNameFontResource = text ? text->findFont("MEDFONT1") : nullptr;
+ const CftFontResource *slotLabelFontResource = text ? text->findFont("MEDFONT2") : nullptr;
+ if (!resources || !metaEngine || !slotNameFontResource || !slotLabelFontResource)
+ return Common::kReadingFailed;
+ if (mode == kSaveMode ? !_engine.canSaveGameStateCurrently() : !_engine.canLoadGameStateCurrently())
+ return Common::kNoError;
+
+ SaveSlotMenu menu(*metaEngine, *slotNameFontResource, *slotLabelFontResource);
+ if (!menu.slotNameFont.isValid() || !menu.slotLabelFont.isValid())
+ return Common::kReadingFailed;
+
+ if (!loadBitmapResource(*resources, mode == kSaveMode ? kSaveGameBitmapPath : kLoadGameBitmapPath, menu.background) ||
+ !loadPaletteResource(*resources, mode == kSaveMode ? kSaveGamePalettePath : kLoadGamePalettePath, menu.palette)) {
+ return Common::kReadingFailed;
+ }
+
+ reloadSaveSlotTitles(menu);
+ if (mode == kLoadMode) {
+ for (int slot = 0; slot < kSaveSlotCount; ++slot) {
+ if (!menu.slotTitles[slot].empty()) {
+ menu.activeSlot = slot;
+ break;
+ }
+ }
+ }
+
+ flow.resetCursorAnimationSequence();
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit()) {
+ if (menu.needsRedraw) {
+ renderSaveSlotMenu(menu);
+ menu.needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+
+ const SaveSlotEventResult selection = handleSaveSlotEvent(menu, mode, event);
+ if (selection.command == kCancelSlot)
+ return Common::kNoError;
+
+ if (selection.command == kConfirmSlot) {
+ Common::Error actionError = Common::kNoError;
+ if (mode == kSaveMode)
+ actionError = saveSelectedSlot(menu, flow, completed);
+ else
+ loadSelectedSlot(menu, completed);
+ if (actionError.getCode() != Common::kNoError)
+ return actionError;
+ if (completed)
+ return Common::kNoError;
+ }
+ if (selection.redrawAfterActivation)
+ menu.needsRedraw = true;
+ }
+
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ (void)entityManager->syncCursorEntityPosition(_mousePos);
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error MenuSystem::restoreRoomBackdropAfterSave(const IndexedBitmap &backdrop, const byte *palette,
+ float paletteBrightness, Flow &flow) const {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen || !backdrop.isValid())
+ return Common::kNoError;
+
+ byte displayPalette[256 * 3];
+ screen->getPalette(displayPalette);
+
+ bool hasVisiblePalette = false;
+ for (byte value : displayPalette) {
+ if (value != 0) {
+ hasVisiblePalette = true;
+ break;
+ }
+ }
+
+ if (hasVisiblePalette) {
+ for (float brightness = 1.0f - kPaletteFadeStep;
+ brightness > kPaletteBrightnessBlack; brightness -= kPaletteFadeStep) {
+ byte fadedPalette[256 * 3];
+ buildHarvesterDisplayPalette(displayPalette, brightness, fadedPalette);
+ screen->setPalette(fadedPalette);
+ screen->makeAllDirty();
+ screen->update();
+
+ const uint32 nextTick = g_system->getMillis() + kPaletteFadeTickMs;
+ while ((int32)(nextTick - g_system->getMillis()) > 0) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+ }
+ g_system->delayMillis(1);
+ }
+ }
+
+ byte blackPalette[256 * 3] = { 0 };
+ screen->setPalette(blackPalette);
+ screen->makeAllDirty();
+ screen->update();
+ }
+
+ screen->fillRect(screen->getBounds(), 0);
+ MenuRendering::blitBitmap(*screen, backdrop, 0, 0);
+ MenuRendering::applyMenuPalette(*screen, _engine, palette, kPaletteBrightnessBlack);
+ screen->makeAllDirty();
+ screen->update();
+
+ return flow.fadeInRoomScene(palette, paletteBrightness);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 237c743e3bf..6342e00f125 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -16,7 +16,9 @@ MODULE_OBJS = \
menu_config.o \
menu_navigation.o \
menu_options.o \
+ menu_prompts.o \
menu_render.o \
+ menu_save.o \
metaengine.o \
monster.o \
player.o \
Commit: 7dbf808e6edc4dbaf614088c61962212426c7e88
https://github.com/scummvm/scummvm/commit/7dbf808e6edc4dbaf614088c61962212426c7e88
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate combat targeting and reach geometry
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/combat_geometry.cpp
A engines/harvester/combat_targeting.cpp
engines/harvester/combat.cpp
engines/harvester/combat.h
engines/harvester/module.mk
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index cb7ad589a40..a7c64505576 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -48,7 +48,6 @@ const int kMuckeyAttackContactFrameOffset = 7;
const float kNativeMonsterPursuitZTolerance = 2.0f;
const float kNativeMonsterHorizontalWaypointTolerance = 50.0f;
const uint32 kNativeKeyboardAttackRepeatTicks = 25;
-const int kNativeAttackSideWindow = 30;
const int kNativeCombatHitKnockbackDistance = 18;
const int kNativeCombatHitKnockbackDecayStep = 3;
const char *const kNativeHitEffectResourcePath = "blood.abm";
@@ -65,26 +64,6 @@ const int kNativeDefaultNpcDeathDamageType = 1;
namespace {
-static Entity *findSceneRuntimeEntity(EntityManager *entityManager, const Common::String &name) {
- return entityManager ? entityManager->findSceneEntityByName(name) : nullptr;
-}
-
-static MonsterRecord *findRoomMonsterRecordByName(RoomSetupState &state, const Common::String &name) {
- for (MonsterRecord &monster : state.roomMonsters) {
- if (monster.monsterName.equalsIgnoreCase(name))
- return &monster;
- }
- return nullptr;
-}
-
-static NpcRecord *findRoomNpcRecordByName(RoomSetupState &state, const Common::String &name) {
- for (NpcRecord &npc : state.roomNpcs) {
- if (npc.npcName.equalsIgnoreCase(name))
- return &npc;
- }
- return nullptr;
-}
-
static int findRoomNpcIndexByName(const RoomSetupState &state, const Common::String &name) {
for (uint i = 0; i < state.roomNpcs.size(); ++i) {
if (state.roomNpcs[i].npcName.equalsIgnoreCase(name))
@@ -103,61 +82,6 @@ static int findRoomMonsterIndexByName(const RoomSetupState &state, const Common:
return -1;
}
-static bool isKillableMonster(const MonsterRecord *monster) {
- return monster && monster->active && monster->visible && monster->currentHitPoints > 0;
-}
-
-static Entity *selectMonsterEntity(EntityManager *entityManager, const MonsterRecord &monster) {
- Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
- return (entity && entity->isVisible()) ? entity : nullptr;
-}
-
-enum NativeAttackSide {
- kNativeAttackSideNone,
- kNativeAttackSideLeft,
- kNativeAttackSideRight
-};
-
-enum NativeAttackBand {
- kNativeAttackBandMid,
- kNativeAttackBandUpper,
- kNativeAttackBandLower
-};
-
-static bool resolveNativePlayerAttackGeometry(int attackFirstFrame, NativeAttackSide &attackSide, NativeAttackBand &attackBand) {
- attackSide = kNativeAttackSideNone;
- attackBand = kNativeAttackBandMid;
-
- switch (attackFirstFrame) {
- case 0x50:
- attackSide = kNativeAttackSideLeft;
- attackBand = kNativeAttackBandUpper;
- return true;
- case 0x5a:
- attackSide = kNativeAttackSideLeft;
- attackBand = kNativeAttackBandMid;
- return true;
- case 0x64:
- attackSide = kNativeAttackSideLeft;
- attackBand = kNativeAttackBandLower;
- return true;
- case 0x6e:
- attackSide = kNativeAttackSideRight;
- attackBand = kNativeAttackBandUpper;
- return true;
- case 0x78:
- attackSide = kNativeAttackSideRight;
- attackBand = kNativeAttackBandMid;
- return true;
- case 0x82:
- attackSide = kNativeAttackSideRight;
- attackBand = kNativeAttackBandLower;
- return true;
- default:
- return false;
- }
-}
-
static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
if (!palette)
return 0;
@@ -250,22 +174,8 @@ RoomMonsterFacingAnimationRange resolveRoomMonsterFacingAnimationRange(int facin
}
}
-int computeRectAxisGap(int minA, int maxA, int minB, int maxB) {
- if (maxA <= minB)
- return minB - maxA;
- if (maxB <= minA)
- return minA - maxB;
- return 0;
-}
-
} // End of anonymous namespace
-float clampRoomDepthForEvent(const RoomSetupState &state, float z) {
- return CLIP<float>(z,
- static_cast<float>(MIN(state.roomMinZ, state.roomMaxZ)),
- static_cast<float>(MAX(state.roomMinZ, state.roomMaxZ)));
-}
-
bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc) {
return npc.monsterfyTargetName.empty();
}
@@ -465,49 +375,6 @@ int stepTowardsRoomCombatInt(int current, int target, int step) {
return MAX(current - step, target);
}
-int computeRuntimeEntityHorizontalGap(const Entity &left, const Entity &right) {
- const Common::Rect leftRect = left.getScreenRect();
- const Common::Rect rightRect = right.getScreenRect();
- return computeRectAxisGap(leftRect.left, leftRect.right, rightRect.left, rightRect.right);
-}
-
-bool doRuntimeEntityDepthExtentsOverlap(const Entity &first, const Entity &second) {
- const float firstZMin = first.getZ();
- const float firstZMax = firstZMin + first.getZExtent();
- const float secondZMin = second.getZ();
- const float secondZMax = secondZMin + second.getZExtent();
- return !(firstZMax < secondZMin || secondZMax < firstZMin);
-}
-
-bool areCombatantsWithinRoomCombatReach(const RoomSetupState &state,
- const Entity &attacker, float attackerZ,
- const Entity &target, float targetZ, int engageDistance) {
- if (attacker.overlapsEntity(target))
- return true;
-
- const float zDelta = attackerZ >= targetZ ? attackerZ - targetZ : targetZ - attackerZ;
- if (zDelta > kNativeMonsterPursuitZTolerance)
- return false;
-
- const float nearZ = attackerZ < targetZ ? attackerZ : targetZ;
- const int nativeTolerance = roundRoomCombatFloat(
- Player::computeDepthScale(state, nearZ) * kNativeMonsterHorizontalWaypointTolerance);
- return computeRuntimeEntityHorizontalGap(attacker, target) <= MAX(MAX(0, engageDistance), nativeTolerance);
-}
-
-bool isWithinNativeMonsterAttackEntryRange(const Entity &monster, float monsterZ,
- const Entity &player, float playerZ, int engageDistance) {
- const float zDelta = monsterZ >= playerZ ? monsterZ - playerZ : playerZ - monsterZ;
- if (zDelta > kNativeMonsterPursuitZTolerance)
- return false;
-
- const Common::Rect monsterRect = monster.getScreenRect();
- const Common::Rect playerRect = player.getScreenRect();
- const int monsterCenterX = monsterRect.left + monsterRect.width() / 2;
- const int playerCenterX = playerRect.left + playerRect.width() / 2;
- return ABS(playerCenterX - monsterCenterX) <= MAX(0, engageDistance);
-}
-
bool playRandomRoomAttackSound(HarvesterEngine &engine, const Common::String &sound1,
const Common::String &sound2, const Common::String &sound3) {
const Common::String *availableSounds[3];
@@ -560,10 +427,6 @@ bool resolveRoomMonsterHitAnimationRange(const Entity &entity,
return true;
}
-int roundRoomCombatFloat(float value) {
- return value >= 0.0f ? static_cast<int>(value + 0.5f) : static_cast<int>(value - 0.5f);
-}
-
int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fallbackY) {
if (state.roomMaxZScreenY < 0 || state.roomMinZScreenY < state.roomMaxZScreenY)
return fallbackY;
@@ -605,374 +468,6 @@ Combat::Combat(HarvesterEngine &engine, SceneState &scene, Player &player,
_callbacks(callbacks), _interactionProcessor(interactionProcessor) {
}
-MonsterRecord *Combat::findMonsterTargetAtPoint(const Common::Point &point) const {
- MonsterRecord *bestMonster = nullptr;
- int bestZ = -0x7fffffff;
- int bestTop = -0x7fffffff;
- for (MonsterRecord &monster : _scene.state.roomMonsters) {
- if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
- continue;
-
- Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
- if (!entity || !entity->isVisible())
- continue;
-
- const Common::Rect entityRect = entity->getScreenRect();
- if (!entityRect.contains(point))
- continue;
-
- if (!bestMonster || monster.posZ > bestZ ||
- (monster.posZ == bestZ && entityRect.top > bestTop)) {
- bestMonster = &monster;
- bestZ = monster.posZ;
- bestTop = entityRect.top;
- }
- }
-
- return bestMonster;
-}
-
-MonsterRecord *Combat::findOverlappingMonsterTarget() const {
- const RoomPlayerState &playerState = _player.getState();
- if (!playerState.entity)
- return nullptr;
-
- for (MonsterRecord &monster : _scene.state.roomMonsters) {
- if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
- continue;
-
- Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
- if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
- *playerState.entity, playerState.z, *entity, static_cast<float>(monster.posZ), monster.engageDistance))
- return &monster;
- }
-
- return nullptr;
-}
-
-NpcRecord *Combat::findOverlappingNpcTarget() const {
- const RoomPlayerState &playerState = _player.getState();
- if (!playerState.entity)
- return nullptr;
-
- for (NpcRecord &npc : _scene.state.roomNpcs) {
- if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
- continue;
-
- Entity *entity = findSceneRuntimeEntity(_entityManager, npc.npcName);
- if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
- *playerState.entity, playerState.z, *entity, entity->getZ(), 0))
- return &npc;
- }
-
- return nullptr;
-}
-
-bool Combat::playerAttackCanReachTarget(Entity *targetEntity, int engageDistance) const {
- const RoomPlayerState &playerState = _player.getState();
- if (!playerState.entity || !targetEntity)
- return false;
- if (Player::isProjectileCombatLoadout(playerState.combatLoadout))
- return true;
-
- return areCombatantsWithinRoomCombatReach(_scene.state,
- *playerState.entity, playerState.z, *targetEntity, targetEntity->getZ(), engageDistance);
-}
-
-MonsterRecord *Combat::findProjectileContactMonsterTarget() const {
- const RoomPlayerState &playerState = _player.getState();
- if (!playerState.entity)
- return nullptr;
-
- NativeAttackSide attackSide = kNativeAttackSideNone;
- NativeAttackBand attackBand = kNativeAttackBandMid;
- if (!resolveNativePlayerAttackGeometry(playerState.attackFirstFrame, attackSide, attackBand))
- return nullptr;
-
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- const int playerLeft = playerRect.left;
- const int playerRight = playerRect.right;
- const int playerCenterX = playerLeft + playerRect.width() / 2;
- MonsterRecord *bestMonster = nullptr;
- Entity *bestEntity = nullptr;
- int bestLeadingEdge = attackSide == kNativeAttackSideLeft ? -0x7fffffff : 0x7fffffff;
-
- for (MonsterRecord &monster : _scene.state.roomMonsters) {
- if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
- continue;
-
- Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
- if (!entity || !entity->isVisible() ||
- !doRuntimeEntityDepthExtentsOverlap(*playerState.entity, *entity)) {
- continue;
- }
-
- const Common::Rect targetRect = entity->getScreenRect();
- const int targetLeft = targetRect.left;
- const int targetCenterX = targetLeft + targetRect.width() / 2;
- bool matchesDirection = false;
- if (attackSide == kNativeAttackSideLeft) {
- if (attackBand == kNativeAttackBandMid)
- matchesDirection = targetLeft < playerCenterX;
- else
- matchesDirection =
- targetLeft < playerCenterX && targetCenterX > playerLeft - kNativeAttackSideWindow;
- if (!matchesDirection || targetLeft <= bestLeadingEdge)
- continue;
- } else {
- if (attackBand == kNativeAttackBandMid)
- matchesDirection = playerCenterX < targetLeft;
- else
- matchesDirection =
- playerCenterX < targetLeft && targetCenterX < playerRight + kNativeAttackSideWindow;
- if (!matchesDirection || targetLeft >= bestLeadingEdge)
- continue;
- }
-
- bestMonster = &monster;
- bestEntity = entity;
- bestLeadingEdge = targetLeft;
- }
-
- if (bestMonster && bestEntity) {
- debugC(1, kDebugCombat,
- "Harvester: combat player projectile contact fallback target='%s' attack_frame=%d player_rect=(%d,%d)-(%d,%d) target_rect=(%d,%d)-(%d,%d)",
- bestMonster->monsterName.c_str(), playerState.attackFirstFrame,
- playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
- bestEntity->getScreenRect().left, bestEntity->getScreenRect().top,
- bestEntity->getScreenRect().right, bestEntity->getScreenRect().bottom);
- }
-
- return bestMonster;
-}
-
-void Combat::capturePlayerAttackTargetForKeyboard(bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
- const RoomPlayerState &playerState = _player.getState();
- _player.clearAttackTarget();
- if (!playerState.entity)
- return;
-
- NativeAttackSide attackSide = kNativeAttackSideNone;
- NativeAttackBand attackBand = kNativeAttackBandMid;
- if (attackRight) {
- attackSide = kNativeAttackSideRight;
- attackBand = kNativeAttackBandMid;
- } else if (attackLeft) {
- attackSide = kNativeAttackSideLeft;
- attackBand = kNativeAttackBandMid;
- } else if (attackUp) {
- if (playerState.facing == 2) {
- attackSide = kNativeAttackSideRight;
- attackBand = kNativeAttackBandUpper;
- } else if (playerState.facing == 1) {
- attackSide = kNativeAttackSideLeft;
- attackBand = kNativeAttackBandUpper;
- }
- } else if (attackDown) {
- if (playerState.facing == 2) {
- attackSide = kNativeAttackSideRight;
- attackBand = kNativeAttackBandLower;
- } else if (playerState.facing == 1) {
- attackSide = kNativeAttackSideLeft;
- attackBand = kNativeAttackBandLower;
- }
- }
- if (attackSide == kNativeAttackSideNone) {
- debugC(1, kDebugCombat,
- "Harvester: combat player keyboard target capture class='none' input=(L=%d R=%d U=%d D=%d) facing=%d reason='no native attack family'",
- attackLeft, attackRight, attackUp, attackDown, playerState.facing);
- return;
- }
-
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- const int playerLeft = playerRect.left;
- const int playerRight = playerRect.right;
- // Keyboard selection uses the cached center; projectile fallback uses the frame rectangle.
- const int playerCenterX = playerState.centerX;
- Entity *bestEntity = nullptr;
- Common::String bestName;
- int bestClassId = -1;
- int bestLeadingEdge = attackSide == kNativeAttackSideLeft ? -0x7fffffff : 0x7fffffff;
-
- auto considerTarget = [&](Entity *targetEntity, const Common::String &targetName, int classId) {
- if (!targetEntity || !targetEntity->isVisible() ||
- !doRuntimeEntityDepthExtentsOverlap(*playerState.entity, *targetEntity)) {
- return;
- }
-
- const Common::Rect targetRect = targetEntity->getScreenRect();
- const int targetLeft = targetRect.left;
- const int targetCenterX = targetLeft + (targetRect.width() / 2);
- bool matchesDirection = false;
- if (attackSide == kNativeAttackSideLeft) {
- if (attackBand == kNativeAttackBandMid)
- matchesDirection = targetLeft < playerCenterX;
- else
- matchesDirection =
- targetLeft < playerCenterX && targetCenterX > playerLeft - kNativeAttackSideWindow;
- if (!matchesDirection || targetLeft <= bestLeadingEdge)
- return;
- } else {
- if (attackBand == kNativeAttackBandMid)
- matchesDirection = playerCenterX < targetLeft;
- else
- matchesDirection =
- playerCenterX < targetLeft && targetCenterX < playerRight + kNativeAttackSideWindow;
- if (!matchesDirection || targetLeft >= bestLeadingEdge)
- return;
- }
-
- bestEntity = targetEntity;
- bestName = targetName;
- bestClassId = classId;
- bestLeadingEdge = targetLeft;
- };
-
- for (NpcRecord &npc : _scene.state.roomNpcs) {
- if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
- continue;
- considerTarget(findSceneRuntimeEntity(_entityManager, npc.npcName), npc.npcName, kEntityClassNpc);
- }
- for (MonsterRecord &monster : _scene.state.roomMonsters) {
- if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
- continue;
- considerTarget(findSceneRuntimeEntity(_entityManager, monster.monsterName), monster.monsterName, kEntityClassMonster);
- }
-
- if (!bestEntity) {
- debugC(1, kDebugCombat,
- "Harvester: combat player keyboard target capture class='none' input=(L=%d R=%d U=%d D=%d) facing=%d reason='no directional target'",
- attackLeft, attackRight, attackUp, attackDown, playerState.facing);
- return;
- }
-
- _player.captureAttackTarget(bestName, bestClassId);
- debugC(1, kDebugCombat,
- "Harvester: combat player keyboard target capture class='%s' name='%s' input=(L=%d R=%d U=%d D=%d) facing=%d target_rect=(%d,%d)-(%d,%d)",
- describeCombatTargetClass(bestClassId), bestName.c_str(),
- attackLeft, attackRight, attackUp, attackDown, playerState.facing,
- bestEntity->getScreenRect().left, bestEntity->getScreenRect().top,
- bestEntity->getScreenRect().right, bestEntity->getScreenRect().bottom);
-}
-
-MonsterRecord *Combat::resolveKillTarget(const Common::Point &mousePos) const {
- const RoomPlayerState &playerState = _player.getState();
- if (playerState.attackTargetClassId == kEntityClassMonster) {
- MonsterRecord *monster = findRoomMonsterRecordByName(_scene.state, playerState.attackTargetName);
- if (isKillableMonster(monster) && selectMonsterEntity(_entityManager, *monster))
- return monster;
- }
-
- if (MonsterRecord *monster = findMonsterTargetAtPoint(mousePos)) {
- if (isKillableMonster(monster) && selectMonsterEntity(_entityManager, *monster))
- return monster;
- }
-
- if (playerState.entity) {
- for (uint i = 0; i < _scene.state.roomMonsters.size() && i < _monsterCombatStates.size(); ++i) {
- MonsterRecord &monster = _scene.state.roomMonsters[i];
- const MonsterCombatState &combatState = _monsterCombatStates[i];
- if (!combatState.attackActive || !isKillableMonster(&monster) || !selectMonsterEntity(_entityManager, monster))
- continue;
- if (!combatState.attackTargetName.empty() &&
- playerState.entity->getName().equalsIgnoreCase(combatState.attackTargetName)) {
- return &monster;
- }
- }
- }
-
- if (!playerState.entity)
- return nullptr;
-
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- const int playerCenterX = playerRect.left + playerRect.width() / 2;
- MonsterRecord *bestMonster = nullptr;
- int bestScore = 0x7fffffff;
- for (MonsterRecord &monster : _scene.state.roomMonsters) {
- if (!isKillableMonster(&monster))
- continue;
-
- Entity *entity = selectMonsterEntity(_entityManager, monster);
- if (!entity)
- continue;
-
- const Common::Rect monsterRect = entity->getScreenRect();
- const int monsterCenterX = monsterRect.left + monsterRect.width() / 2;
- const int score = ABS(monsterCenterX - playerCenterX) +
- ABS(monster.posZ - roundRoomCombatFloat(playerState.z)) * 8;
- if (!bestMonster || score < bestScore) {
- bestMonster = &monster;
- bestScore = score;
- }
- }
-
- return bestMonster;
-}
-
-void Combat::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
- const RoomPlayerState &playerState = _player.getState();
- if (hoverNpc) {
- _player.captureAttackTarget(hoverNpc->npcName, kEntityClassNpc);
- debugC(1, kDebugCombat,
- "Harvester: combat player target capture class='npc' name='%s' cursor=(%d,%d)",
- playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
- return;
- }
-
- if (MonsterRecord *monster = findMonsterTargetAtPoint(mousePos)) {
- _player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
- debugC(1, kDebugCombat,
- "Harvester: combat player target capture class='monster' name='%s' cursor=(%d,%d)",
- playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
- return;
- }
-
- debugC(1, kDebugCombat,
- "Harvester: combat player target capture class='none' cursor=(%d,%d)",
- mousePos.x, mousePos.y);
-}
-
-void Combat::captureOverlappingNpcAttackTarget() const {
- const RoomPlayerState &playerState = _player.getState();
- if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
- if (NpcRecord *fallbackNpc = findOverlappingNpcTarget()) {
- _player.captureAttackTarget(fallbackNpc->npcName, kEntityClassNpc);
- }
- }
-}
-
-NpcRecord *Combat::selectNpcAttackContactTarget(Entity *&npcEntity) const {
- const RoomPlayerState &playerState = _player.getState();
- NpcRecord *npc = findRoomNpcRecordByName(_scene.state, playerState.attackTargetName);
- npcEntity = npc ? findSceneRuntimeEntity(_entityManager, npc->npcName) : nullptr;
- if (!npc || !npcEntity || !playerAttackCanReachTarget(npcEntity))
- npc = Player::isProjectileCombatLoadout(playerState.combatLoadout) ? nullptr : findOverlappingNpcTarget();
- npcEntity = npc ? findSceneRuntimeEntity(_entityManager, npc->npcName) : nullptr;
- return npc;
-}
-
-MonsterRecord *Combat::selectMonsterAttackContactTarget(Entity *&monsterEntity) const {
- const RoomPlayerState &playerState = _player.getState();
- MonsterRecord *monster = nullptr;
- if (playerState.attackTargetClassId == kEntityClassMonster)
- monster = findRoomMonsterRecordByName(_scene.state, playerState.attackTargetName);
- monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
- if (!monster || !monsterEntity || !playerAttackCanReachTarget(monsterEntity, monster->engageDistance)) {
- if (!Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
- monster = findOverlappingMonsterTarget();
- } else if (playerState.attackTargetClassId < 0) {
- monster = findProjectileContactMonsterTarget();
- if (monster) {
- _player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
- }
- } else {
- monster = nullptr;
- }
- monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
- }
- return monster;
-}
-
NpcCombatUpdateResult Combat::updateRoomNpcCombat(bool &needsRedraw) {
NpcCombatUpdateResult result;
for (uint i = 0; i < _scene.state.roomNpcs.size(); ++i) {
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index 95c6008eeb5..5d49b6c6f9f 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -170,6 +170,10 @@ private:
void startMonsterHitReaction(MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
int attackerAttackFirstFrame, bool &needsRedraw) const;
+ static Entity *findSceneRuntimeEntity(EntityManager *entityManager, const Common::String &name);
+ static NpcRecord *findRoomNpcRecordByName(RoomSetupState &state, const Common::String &name);
+ static Entity *selectMonsterEntity(EntityManager *entityManager, const MonsterRecord &monster);
+
MonsterRecord *findMonsterTargetAtPoint(const Common::Point &point) const;
MonsterRecord *findOverlappingMonsterTarget() const;
NpcRecord *findOverlappingNpcTarget() const;
diff --git a/engines/harvester/combat_geometry.cpp b/engines/harvester/combat_geometry.cpp
new file mode 100644
index 00000000000..4a472c28e8d
--- /dev/null
+++ b/engines/harvester/combat_geometry.cpp
@@ -0,0 +1,95 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "common/util.h"
+#include "harvester/player.h"
+#include "harvester/entity.h"
+
+namespace Harvester {
+
+namespace {
+
+int computeRectAxisGap(int minA, int maxA, int minB, int maxB) {
+ if (maxA <= minB)
+ return minB - maxA;
+ if (maxB <= minA)
+ return minA - maxB;
+ return 0;
+}
+
+} // End of anonymous namespace
+
+float clampRoomDepthForEvent(const RoomSetupState &state, float z) {
+ return CLIP<float>(z,
+ static_cast<float>(MIN(state.roomMinZ, state.roomMaxZ)),
+ static_cast<float>(MAX(state.roomMinZ, state.roomMaxZ)));
+}
+
+int computeRuntimeEntityHorizontalGap(const Entity &left, const Entity &right) {
+ const Common::Rect leftRect = left.getScreenRect();
+ const Common::Rect rightRect = right.getScreenRect();
+ return computeRectAxisGap(leftRect.left, leftRect.right, rightRect.left, rightRect.right);
+}
+
+bool doRuntimeEntityDepthExtentsOverlap(const Entity &first, const Entity &second) {
+ const float firstZMin = first.getZ();
+ const float firstZMax = firstZMin + first.getZExtent();
+ const float secondZMin = second.getZ();
+ const float secondZMax = secondZMin + second.getZExtent();
+ return !(firstZMax < secondZMin || secondZMax < firstZMin);
+}
+
+bool areCombatantsWithinRoomCombatReach(const RoomSetupState &state,
+ const Entity &attacker, float attackerZ,
+ const Entity &target, float targetZ, int engageDistance) {
+ if (attacker.overlapsEntity(target))
+ return true;
+
+ const float zDelta = attackerZ >= targetZ ? attackerZ - targetZ : targetZ - attackerZ;
+ if (zDelta > kNativeMonsterPursuitZTolerance)
+ return false;
+
+ const float nearZ = attackerZ < targetZ ? attackerZ : targetZ;
+ const int nativeTolerance = roundRoomCombatFloat(
+ Player::computeDepthScale(state, nearZ) * kNativeMonsterHorizontalWaypointTolerance);
+ return computeRuntimeEntityHorizontalGap(attacker, target) <= MAX(MAX(0, engageDistance), nativeTolerance);
+}
+
+bool isWithinNativeMonsterAttackEntryRange(const Entity &monster, float monsterZ,
+ const Entity &player, float playerZ, int engageDistance) {
+ const float zDelta = monsterZ >= playerZ ? monsterZ - playerZ : playerZ - monsterZ;
+ if (zDelta > kNativeMonsterPursuitZTolerance)
+ return false;
+
+ const Common::Rect monsterRect = monster.getScreenRect();
+ const Common::Rect playerRect = player.getScreenRect();
+ const int monsterCenterX = monsterRect.left + monsterRect.width() / 2;
+ const int playerCenterX = playerRect.left + playerRect.width() / 2;
+ return ABS(playerCenterX - monsterCenterX) <= MAX(0, engageDistance);
+}
+
+int roundRoomCombatFloat(float value) {
+ return value >= 0.0f ? static_cast<int>(value + 0.5f) : static_cast<int>(value - 0.5f);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/combat_targeting.cpp b/engines/harvester/combat_targeting.cpp
new file mode 100644
index 00000000000..93ac3fb9423
--- /dev/null
+++ b/engines/harvester/combat_targeting.cpp
@@ -0,0 +1,485 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/player.h"
+#include "harvester/entity.h"
+
+namespace Harvester {
+
+const int kNativeAttackSideWindow = 30;
+
+namespace {
+
+static MonsterRecord *findRoomMonsterRecordByName(RoomSetupState &state, const Common::String &name) {
+ for (MonsterRecord &monster : state.roomMonsters) {
+ if (monster.monsterName.equalsIgnoreCase(name))
+ return &monster;
+ }
+ return nullptr;
+}
+
+static bool isKillableMonster(const MonsterRecord *monster) {
+ return monster && monster->active && monster->visible && monster->currentHitPoints > 0;
+}
+
+enum NativeAttackSide {
+ kNativeAttackSideNone,
+ kNativeAttackSideLeft,
+ kNativeAttackSideRight
+};
+
+enum NativeAttackBand {
+ kNativeAttackBandMid,
+ kNativeAttackBandUpper,
+ kNativeAttackBandLower
+};
+
+static bool resolveNativePlayerAttackGeometry(int attackFirstFrame, NativeAttackSide &attackSide, NativeAttackBand &attackBand) {
+ attackSide = kNativeAttackSideNone;
+ attackBand = kNativeAttackBandMid;
+
+ switch (attackFirstFrame) {
+ case 0x50:
+ attackSide = kNativeAttackSideLeft;
+ attackBand = kNativeAttackBandUpper;
+ return true;
+ case 0x5a:
+ attackSide = kNativeAttackSideLeft;
+ attackBand = kNativeAttackBandMid;
+ return true;
+ case 0x64:
+ attackSide = kNativeAttackSideLeft;
+ attackBand = kNativeAttackBandLower;
+ return true;
+ case 0x6e:
+ attackSide = kNativeAttackSideRight;
+ attackBand = kNativeAttackBandUpper;
+ return true;
+ case 0x78:
+ attackSide = kNativeAttackSideRight;
+ attackBand = kNativeAttackBandMid;
+ return true;
+ case 0x82:
+ attackSide = kNativeAttackSideRight;
+ attackBand = kNativeAttackBandLower;
+ return true;
+ default:
+ return false;
+ }
+}
+
+struct NativeAttackGeometry {
+ NativeAttackSide side = kNativeAttackSideNone;
+ NativeAttackBand band = kNativeAttackBandMid;
+};
+
+static NativeAttackGeometry resolveKeyboardAttackGeometry(int facing,
+ bool attackLeft, bool attackRight, bool attackUp, bool attackDown) {
+ NativeAttackGeometry geometry;
+ if (attackRight) {
+ geometry.side = kNativeAttackSideRight;
+ geometry.band = kNativeAttackBandMid;
+ } else if (attackLeft) {
+ geometry.side = kNativeAttackSideLeft;
+ geometry.band = kNativeAttackBandMid;
+ } else if (attackUp) {
+ if (facing == 2) {
+ geometry.side = kNativeAttackSideRight;
+ geometry.band = kNativeAttackBandUpper;
+ } else if (facing == 1) {
+ geometry.side = kNativeAttackSideLeft;
+ geometry.band = kNativeAttackBandUpper;
+ }
+ } else if (attackDown) {
+ if (facing == 2) {
+ geometry.side = kNativeAttackSideRight;
+ geometry.band = kNativeAttackBandLower;
+ } else if (facing == 1) {
+ geometry.side = kNativeAttackSideLeft;
+ geometry.band = kNativeAttackBandLower;
+ }
+ }
+ return geometry;
+}
+
+static bool isBetterDirectionalAttackTarget(const Common::Rect &playerRect, int playerCenterX,
+ const Common::Rect &targetRect, NativeAttackSide attackSide, NativeAttackBand attackBand,
+ int bestLeadingEdge) {
+ const int playerLeft = playerRect.left;
+ const int playerRight = playerRect.right;
+ const int targetLeft = targetRect.left;
+ const int targetCenterX = targetLeft + targetRect.width() / 2;
+ bool matchesDirection = false;
+ if (attackSide == kNativeAttackSideLeft) {
+ if (attackBand == kNativeAttackBandMid)
+ matchesDirection = targetLeft < playerCenterX;
+ else
+ matchesDirection =
+ targetLeft < playerCenterX && targetCenterX > playerLeft - kNativeAttackSideWindow;
+ if (!matchesDirection || targetLeft <= bestLeadingEdge)
+ return false;
+ } else {
+ if (attackBand == kNativeAttackBandMid)
+ matchesDirection = playerCenterX < targetLeft;
+ else
+ matchesDirection =
+ playerCenterX < targetLeft && targetCenterX < playerRight + kNativeAttackSideWindow;
+ if (!matchesDirection || targetLeft >= bestLeadingEdge)
+ return false;
+ }
+ return true;
+}
+
+} // End of anonymous namespace
+
+Entity *Combat::findSceneRuntimeEntity(EntityManager *entityManager, const Common::String &name) {
+ return entityManager ? entityManager->findSceneEntityByName(name) : nullptr;
+}
+
+NpcRecord *Combat::findRoomNpcRecordByName(RoomSetupState &state, const Common::String &name) {
+ for (NpcRecord &npc : state.roomNpcs) {
+ if (npc.npcName.equalsIgnoreCase(name))
+ return &npc;
+ }
+ return nullptr;
+}
+
+Entity *Combat::selectMonsterEntity(EntityManager *entityManager, const MonsterRecord &monster) {
+ Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
+ return (entity && entity->isVisible()) ? entity : nullptr;
+}
+
+MonsterRecord *Combat::findMonsterTargetAtPoint(const Common::Point &point) const {
+ MonsterRecord *bestMonster = nullptr;
+ int bestZ = -0x7fffffff;
+ int bestTop = -0x7fffffff;
+ for (MonsterRecord &monster : _scene.state.roomMonsters) {
+ if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
+ continue;
+
+ Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
+ if (!entity || !entity->isVisible())
+ continue;
+
+ const Common::Rect entityRect = entity->getScreenRect();
+ if (!entityRect.contains(point))
+ continue;
+
+ if (!bestMonster || monster.posZ > bestZ ||
+ (monster.posZ == bestZ && entityRect.top > bestTop)) {
+ bestMonster = &monster;
+ bestZ = monster.posZ;
+ bestTop = entityRect.top;
+ }
+ }
+
+ return bestMonster;
+}
+
+MonsterRecord *Combat::findOverlappingMonsterTarget() const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!playerState.entity)
+ return nullptr;
+
+ for (MonsterRecord &monster : _scene.state.roomMonsters) {
+ if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
+ continue;
+
+ Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
+ if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
+ *playerState.entity, playerState.z, *entity, static_cast<float>(monster.posZ), monster.engageDistance))
+ return &monster;
+ }
+
+ return nullptr;
+}
+
+NpcRecord *Combat::findOverlappingNpcTarget() const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!playerState.entity)
+ return nullptr;
+
+ for (NpcRecord &npc : _scene.state.roomNpcs) {
+ if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
+ continue;
+
+ Entity *entity = findSceneRuntimeEntity(_entityManager, npc.npcName);
+ if (entity && areCombatantsWithinRoomCombatReach(_scene.state,
+ *playerState.entity, playerState.z, *entity, entity->getZ(), 0))
+ return &npc;
+ }
+
+ return nullptr;
+}
+
+bool Combat::playerAttackCanReachTarget(Entity *targetEntity, int engageDistance) const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!playerState.entity || !targetEntity)
+ return false;
+ if (Player::isProjectileCombatLoadout(playerState.combatLoadout))
+ return true;
+
+ return areCombatantsWithinRoomCombatReach(_scene.state,
+ *playerState.entity, playerState.z, *targetEntity, targetEntity->getZ(), engageDistance);
+}
+
+MonsterRecord *Combat::findProjectileContactMonsterTarget() const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!playerState.entity)
+ return nullptr;
+
+ NativeAttackSide attackSide = kNativeAttackSideNone;
+ NativeAttackBand attackBand = kNativeAttackBandMid;
+ if (!resolveNativePlayerAttackGeometry(playerState.attackFirstFrame, attackSide, attackBand))
+ return nullptr;
+
+ const Common::Rect playerRect = playerState.entity->getScreenRect();
+ const int playerLeft = playerRect.left;
+ const int playerCenterX = playerLeft + playerRect.width() / 2;
+ MonsterRecord *bestMonster = nullptr;
+ Entity *bestEntity = nullptr;
+ int bestLeadingEdge = attackSide == kNativeAttackSideLeft ? -0x7fffffff : 0x7fffffff;
+
+ for (MonsterRecord &monster : _scene.state.roomMonsters) {
+ if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
+ continue;
+
+ Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
+ if (!entity || !entity->isVisible() ||
+ !doRuntimeEntityDepthExtentsOverlap(*playerState.entity, *entity)) {
+ continue;
+ }
+
+ const Common::Rect targetRect = entity->getScreenRect();
+ const int targetLeft = targetRect.left;
+ if (!isBetterDirectionalAttackTarget(playerRect, playerCenterX,
+ targetRect, attackSide, attackBand, bestLeadingEdge))
+ continue;
+
+ bestMonster = &monster;
+ bestEntity = entity;
+ bestLeadingEdge = targetLeft;
+ }
+
+ if (bestMonster && bestEntity) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player projectile contact fallback target='%s' attack_frame=%d player_rect=(%d,%d)-(%d,%d) target_rect=(%d,%d)-(%d,%d)",
+ bestMonster->monsterName.c_str(), playerState.attackFirstFrame,
+ playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
+ bestEntity->getScreenRect().left, bestEntity->getScreenRect().top,
+ bestEntity->getScreenRect().right, bestEntity->getScreenRect().bottom);
+ }
+
+ return bestMonster;
+}
+
+void Combat::capturePlayerAttackTargetForKeyboard(bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
+ const RoomPlayerState &playerState = _player.getState();
+ _player.clearAttackTarget();
+ if (!playerState.entity)
+ return;
+
+ const NativeAttackGeometry geometry = resolveKeyboardAttackGeometry(
+ playerState.facing, attackLeft, attackRight, attackUp, attackDown);
+ if (geometry.side == kNativeAttackSideNone) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player keyboard target capture class='none' input=(L=%d R=%d U=%d D=%d) facing=%d reason='no native attack family'",
+ attackLeft, attackRight, attackUp, attackDown, playerState.facing);
+ return;
+ }
+
+ const Common::Rect playerRect = playerState.entity->getScreenRect();
+ // Keyboard selection uses the cached center; projectile fallback uses the frame rectangle.
+ const int playerCenterX = playerState.centerX;
+ Entity *bestEntity = nullptr;
+ Common::String bestName;
+ int bestClassId = -1;
+ int bestLeadingEdge = geometry.side == kNativeAttackSideLeft ? -0x7fffffff : 0x7fffffff;
+
+ auto considerTarget = [&](Entity *targetEntity, const Common::String &targetName, int classId) {
+ if (!targetEntity || !targetEntity->isVisible() ||
+ !doRuntimeEntityDepthExtentsOverlap(*playerState.entity, *targetEntity)) {
+ return;
+ }
+
+ const Common::Rect targetRect = targetEntity->getScreenRect();
+ const int targetLeft = targetRect.left;
+ if (!isBetterDirectionalAttackTarget(playerRect, playerCenterX,
+ targetRect, geometry.side, geometry.band, bestLeadingEdge))
+ return;
+
+ bestEntity = targetEntity;
+ bestName = targetName;
+ bestClassId = classId;
+ bestLeadingEdge = targetLeft;
+ };
+
+ for (NpcRecord &npc : _scene.state.roomNpcs) {
+ if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
+ continue;
+ considerTarget(findSceneRuntimeEntity(_entityManager, npc.npcName), npc.npcName, kEntityClassNpc);
+ }
+ for (MonsterRecord &monster : _scene.state.roomMonsters) {
+ if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
+ continue;
+ considerTarget(findSceneRuntimeEntity(_entityManager, monster.monsterName), monster.monsterName, kEntityClassMonster);
+ }
+
+ if (!bestEntity) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player keyboard target capture class='none' input=(L=%d R=%d U=%d D=%d) facing=%d reason='no directional target'",
+ attackLeft, attackRight, attackUp, attackDown, playerState.facing);
+ return;
+ }
+
+ _player.captureAttackTarget(bestName, bestClassId);
+ debugC(1, kDebugCombat,
+ "Harvester: combat player keyboard target capture class='%s' name='%s' input=(L=%d R=%d U=%d D=%d) facing=%d target_rect=(%d,%d)-(%d,%d)",
+ describeCombatTargetClass(bestClassId), bestName.c_str(),
+ attackLeft, attackRight, attackUp, attackDown, playerState.facing,
+ bestEntity->getScreenRect().left, bestEntity->getScreenRect().top,
+ bestEntity->getScreenRect().right, bestEntity->getScreenRect().bottom);
+}
+
+MonsterRecord *Combat::resolveKillTarget(const Common::Point &mousePos) const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (playerState.attackTargetClassId == kEntityClassMonster) {
+ MonsterRecord *monster = findRoomMonsterRecordByName(_scene.state, playerState.attackTargetName);
+ if (isKillableMonster(monster) && selectMonsterEntity(_entityManager, *monster))
+ return monster;
+ }
+
+ if (MonsterRecord *monster = findMonsterTargetAtPoint(mousePos)) {
+ if (isKillableMonster(monster) && selectMonsterEntity(_entityManager, *monster))
+ return monster;
+ }
+
+ if (playerState.entity) {
+ for (uint i = 0; i < _scene.state.roomMonsters.size() && i < _monsterCombatStates.size(); ++i) {
+ MonsterRecord &monster = _scene.state.roomMonsters[i];
+ const MonsterCombatState &combatState = _monsterCombatStates[i];
+ if (!combatState.attackActive || !isKillableMonster(&monster) || !selectMonsterEntity(_entityManager, monster))
+ continue;
+ if (!combatState.attackTargetName.empty() &&
+ playerState.entity->getName().equalsIgnoreCase(combatState.attackTargetName)) {
+ return &monster;
+ }
+ }
+ }
+
+ if (!playerState.entity)
+ return nullptr;
+
+ const Common::Rect playerRect = playerState.entity->getScreenRect();
+ const int playerCenterX = playerRect.left + playerRect.width() / 2;
+ MonsterRecord *bestMonster = nullptr;
+ int bestScore = 0x7fffffff;
+ for (MonsterRecord &monster : _scene.state.roomMonsters) {
+ if (!isKillableMonster(&monster))
+ continue;
+
+ Entity *entity = selectMonsterEntity(_entityManager, monster);
+ if (!entity)
+ continue;
+
+ const Common::Rect monsterRect = entity->getScreenRect();
+ const int monsterCenterX = monsterRect.left + monsterRect.width() / 2;
+ const int score = ABS(monsterCenterX - playerCenterX) +
+ ABS(monster.posZ - roundRoomCombatFloat(playerState.z)) * 8;
+ if (!bestMonster || score < bestScore) {
+ bestMonster = &monster;
+ bestScore = score;
+ }
+ }
+
+ return bestMonster;
+}
+
+void Combat::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (hoverNpc) {
+ _player.captureAttackTarget(hoverNpc->npcName, kEntityClassNpc);
+ debugC(1, kDebugCombat,
+ "Harvester: combat player target capture class='npc' name='%s' cursor=(%d,%d)",
+ playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
+ return;
+ }
+
+ if (MonsterRecord *monster = findMonsterTargetAtPoint(mousePos)) {
+ _player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
+ debugC(1, kDebugCombat,
+ "Harvester: combat player target capture class='monster' name='%s' cursor=(%d,%d)",
+ playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
+ return;
+ }
+
+ debugC(1, kDebugCombat,
+ "Harvester: combat player target capture class='none' cursor=(%d,%d)",
+ mousePos.x, mousePos.y);
+}
+
+void Combat::captureOverlappingNpcAttackTarget() const {
+ const RoomPlayerState &playerState = _player.getState();
+ if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
+ if (NpcRecord *fallbackNpc = findOverlappingNpcTarget()) {
+ _player.captureAttackTarget(fallbackNpc->npcName, kEntityClassNpc);
+ }
+ }
+}
+
+NpcRecord *Combat::selectNpcAttackContactTarget(Entity *&npcEntity) const {
+ const RoomPlayerState &playerState = _player.getState();
+ NpcRecord *npc = findRoomNpcRecordByName(_scene.state, playerState.attackTargetName);
+ npcEntity = npc ? findSceneRuntimeEntity(_entityManager, npc->npcName) : nullptr;
+ if (!npc || !npcEntity || !playerAttackCanReachTarget(npcEntity))
+ npc = Player::isProjectileCombatLoadout(playerState.combatLoadout) ? nullptr : findOverlappingNpcTarget();
+ npcEntity = npc ? findSceneRuntimeEntity(_entityManager, npc->npcName) : nullptr;
+ return npc;
+}
+
+MonsterRecord *Combat::selectMonsterAttackContactTarget(Entity *&monsterEntity) const {
+ const RoomPlayerState &playerState = _player.getState();
+ MonsterRecord *monster = nullptr;
+ if (playerState.attackTargetClassId == kEntityClassMonster)
+ monster = findRoomMonsterRecordByName(_scene.state, playerState.attackTargetName);
+ monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
+ if (!monster || !monsterEntity || !playerAttackCanReachTarget(monsterEntity, monster->engageDistance)) {
+ if (!Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
+ monster = findOverlappingMonsterTarget();
+ } else if (playerState.attackTargetClassId < 0) {
+ monster = findProjectileContactMonsterTarget();
+ if (monster) {
+ _player.captureAttackTarget(monster->monsterName, kEntityClassMonster);
+ }
+ } else {
+ monster = nullptr;
+ }
+ monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
+ }
+ return monster;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 6342e00f125..47eb793b898 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -24,6 +24,8 @@ MODULE_OBJS = \
player.o \
resources.o \
combat.o \
+ combat_geometry.o \
+ combat_targeting.o \
interaction.o \
input.o \
room_presentation.o \
Commit: 211040816241f526044f8c84bf6f3a4c8f2c3c35
https://github.com/scummvm/scummvm/commit/211040816241f526044f8c84bf6f3a4c8f2c3c35
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate combat hit and death transitions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/combat_reactions.cpp
engines/harvester/combat.cpp
engines/harvester/combat.h
engines/harvester/module.mk
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index a7c64505576..d5e69c0be80 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -36,10 +36,7 @@
namespace Harvester {
const int kRoomMonsterAnimationRate = 17;
-// update_actor_runtime_state (0x4d750), states 0x16..0x1b: the block at
-// 0x53525 forces a three-centisecond interval by assigning animation rate 0x1e.
const int kNativeMonsterAttackAnimationRate = 30;
-const int kNativeMonsterHitAnimationRate = 5;
const uint32 kNativeMonsterAttackCooldownBaseTicks = 50;
const float kNativeNpcMonsterZExtent = 5.0f;
const int kRoomNpcAmbientLastFrame = 0x3b;
@@ -48,19 +45,12 @@ const int kMuckeyAttackContactFrameOffset = 7;
const float kNativeMonsterPursuitZTolerance = 2.0f;
const float kNativeMonsterHorizontalWaypointTolerance = 50.0f;
const uint32 kNativeKeyboardAttackRepeatTicks = 25;
-const int kNativeCombatHitKnockbackDistance = 18;
-const int kNativeCombatHitKnockbackDecayStep = 3;
const char *const kNativeHitEffectResourcePath = "blood.abm";
const int kNativeHitEffectAnimationRate = 10;
const float kNativeHitEffectRenderZBias = 0.01f;
const uint32 kCombatDebugDamagePopupDurationMs = 750;
const int kCombatDebugDamagePopupRisePixels = 18;
const int kCombatDebugDamagePopupVerticalGap = 6;
-const char *const kInnerSanctumRoomName = "INNERSANCTUM";
-const char *const kHerrillLogNpcName = "HERRILL_LOG";
-const char *const kMuckeyMonsterName = "MUCKEY";
-const char *const kActivateInnerSanctumDoorActionTag = "ACTV_INNERSAN_DOOR";
-const int kNativeDefaultNpcDeathDamageType = 1;
namespace {
@@ -176,17 +166,6 @@ RoomMonsterFacingAnimationRange resolveRoomMonsterFacingAnimationRange(int facin
} // End of anonymous namespace
-bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc) {
- return npc.monsterfyTargetName.empty();
-}
-
-bool isRetainedCurrentRoomNpcDeathRecord(const NpcRecord &npc) {
- return npc.deathOrMonsterfyFlag &&
- npc.monsterfyTargetName.empty() &&
- npc.deathDamageType != 0 &&
- npc.runtimeState >= 0;
-}
-
bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info) {
switch (loadout) {
case 2:
@@ -260,18 +239,6 @@ bool setRoomMonsterAnimation(Entity &entity, int facing, bool walking) {
return changed;
}
-bool runtimeEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
- if (!entity.hasFrames() || firstFrame < 0 || lastFrame < firstFrame)
- return false;
-
- return static_cast<uint>(lastFrame) < entity.getFrameCount();
-}
-
-bool runtimeEntityHasDrawableFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
- return runtimeEntityHasFrameRange(entity, firstFrame, lastFrame) &&
- entity.hasOpaqueFramesInRange(firstFrame, lastFrame);
-}
-
bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
const Entity &entity, int actorCenterX, int targetCenterX,
AttackAnimationRange &range) {
@@ -307,66 +274,6 @@ bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
return true;
}
-bool resolveMonsterDeathAnimationRange(const Entity &entity, int facing,
- int deathDamageType, bool goreEnabled, DeathAnimationRange &range) {
- const bool preferLeftBank = facing == 1;
- const DeathAnimationRange rightBludge(0xb0, 0xb9);
- const DeathAnimationRange leftBludge(0xba, 0xc3);
- const DeathAnimationRange rightSlash(0x100, 0x109);
- const DeathAnimationRange leftSlash(0x10a, 0x113);
- const DeathAnimationRange rightProjectile(0x114, 0x11d);
- const DeathAnimationRange leftProjectile(0x11e, 0x127);
-
- const DeathAnimationRange primaryBludge = preferLeftBank ? leftBludge : rightBludge;
- const DeathAnimationRange primarySlash = preferLeftBank ? leftSlash : rightSlash;
- const DeathAnimationRange primaryProjectile = preferLeftBank ? leftProjectile : rightProjectile;
- const DeathAnimationRange fallbackBludge = preferLeftBank ? rightBludge : leftBludge;
- const DeathAnimationRange fallbackSlash = preferLeftBank ? rightSlash : leftSlash;
- const DeathAnimationRange fallbackProjectile = preferLeftBank ? rightProjectile : leftProjectile;
-
- auto chooseIfAvailable = [&](const DeathAnimationRange &candidate) {
- if (!runtimeEntityHasDrawableFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
- return false;
- range = candidate;
- return true;
- };
-
- if ((!goreEnabled || deathDamageType == 1) &&
- (chooseIfAvailable(primaryBludge) || chooseIfAvailable(fallbackBludge)))
- return true;
- if (deathDamageType == 2 &&
- (chooseIfAvailable(primarySlash) || chooseIfAvailable(fallbackSlash)))
- return true;
- if (deathDamageType == 4 &&
- (chooseIfAvailable(primaryProjectile) || chooseIfAvailable(fallbackProjectile)))
- return true;
-
- return chooseIfAvailable(primaryBludge) || chooseIfAvailable(primarySlash) ||
- chooseIfAvailable(primaryProjectile) || chooseIfAvailable(fallbackBludge) ||
- chooseIfAvailable(fallbackSlash) || chooseIfAvailable(fallbackProjectile);
-}
-
-bool resolveNpcDeathAnimationRange(const Entity &entity, bool hasMonsterfyTarget,
- int deathDamageType, bool goreEnabled, DeathAnimationRange &range) {
- const DeathAnimationRange goreBank(0x3c, 0x45);
- const DeathAnimationRange bludgeBank(0x46, 0x4f);
- const bool preferGoreBank = hasMonsterfyTarget || (goreEnabled && deathDamageType != 1);
-
- auto chooseIfAvailable = [&](const DeathAnimationRange &candidate) {
- if (!runtimeEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
- return false;
- range = candidate;
- return true;
- };
-
- if (preferGoreBank && chooseIfAvailable(goreBank))
- return true;
- if (chooseIfAvailable(bludgeBank))
- return true;
-
- return chooseIfAvailable(goreBank);
-}
-
int stepTowardsRoomCombatInt(int current, int target, int step) {
if (step <= 0 || current == target)
return current;
@@ -394,39 +301,6 @@ bool playRandomRoomAttackSound(HarvesterEngine &engine, const Common::String &so
return engine.playSound(*availableSounds[soundIndex]);
}
-bool resolveRoomMonsterHitAnimationRange(const Entity &entity,
- int attackerAttackFirstFrame, HitAnimationRange &range) {
- HitAnimationRange candidate;
- switch (attackerAttackFirstFrame) {
- case 0x64:
- candidate = HitAnimationRange(0x92, 0x94, 2, -kNativeCombatHitKnockbackDistance);
- break;
- case 0x5a:
- candidate = HitAnimationRange(0x8f, 0x91, 2, -kNativeCombatHitKnockbackDistance);
- break;
- case 0x50:
- candidate = HitAnimationRange(0x8c, 0x8e, 2, -kNativeCombatHitKnockbackDistance);
- break;
- case 0x82:
- candidate = HitAnimationRange(0x9b, 0x9d, 1, kNativeCombatHitKnockbackDistance);
- break;
- case 0x78:
- candidate = HitAnimationRange(0x98, 0x9a, 1, kNativeCombatHitKnockbackDistance);
- break;
- case 0x6e:
- candidate = HitAnimationRange(0x95, 0x97, 1, kNativeCombatHitKnockbackDistance);
- break;
- default:
- return false;
- }
-
- if (!runtimeEntityHasDrawableFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
- return false;
-
- range = candidate;
- return true;
-}
-
int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fallbackY) {
if (state.roomMaxZScreenY < 0 || state.roomMinZScreenY < state.roomMaxZScreenY)
return fallbackY;
@@ -468,205 +342,6 @@ Combat::Combat(HarvesterEngine &engine, SceneState &scene, Player &player,
_callbacks(callbacks), _interactionProcessor(interactionProcessor) {
}
-NpcCombatUpdateResult Combat::updateRoomNpcCombat(bool &needsRedraw) {
- NpcCombatUpdateResult result;
- for (uint i = 0; i < _scene.state.roomNpcs.size(); ++i) {
- NpcRecord &npc = _scene.state.roomNpcs[i];
- NpcCombatState &combatState = _npcCombatStates[i];
- Entity *entity = findSceneRuntimeEntity(_entityManager, npc.npcName);
- if (!entity || !npc.visible || npc.deathOrMonsterfyFlag) {
- clearRoomNpcCombatState(combatState);
- continue;
- }
- if (!combatState.deathActive && npc.active && npc.deathDamageType != 0) {
- if (beginNpcDeathTransition(npc, *entity, combatState, npc.deathDamageType, needsRedraw))
- continue;
-
- debugC(1, kDebugCombat,
- "Harvester: room npc death fallback target='%s' damage_type=%d on_death='%s' reason='no death bank'",
- npc.npcName.c_str(), npc.deathDamageType, npc.onDeathActionTag.c_str());
- combatState.deathDamageType = npc.deathDamageType;
- combatState.deathLastFrame = entity->getCurrentFrame();
- result.finalizedDeath = true;
- result.error = finalizeNpcDeathTransition(npc, combatState, needsRedraw);
- return result;
- }
- if (!combatState.deathActive)
- continue;
- if (entity->getCurrentFrame() != combatState.deathLastFrame)
- continue;
-
- entity->setAnimationRate(0);
- entity->setAnimationFrameRange(combatState.deathLastFrame, combatState.deathLastFrame, false);
- entity->setCurrentFrame(combatState.deathLastFrame);
- entity->setAnimationEnabled(false);
- result.finalizedDeath = true;
- result.error = finalizeNpcDeathTransition(npc, combatState, needsRedraw);
- return result;
- }
- return result;
-}
-
-bool Combat::queueInnerSanctumHerrillDeathAfterMuck(const MonsterRecord &monster) const {
- if (!monster.roomName.equalsIgnoreCase(kInnerSanctumRoomName) ||
- !monster.monsterName.equalsIgnoreCase(kMuckeyMonsterName) ||
- !monster.onDeathActionTag.equalsIgnoreCase(kActivateInnerSanctumDoorActionTag)) {
- return false;
- }
-
- Script *script = _engine.getScript();
- NpcRecord *herrill = findRoomNpcRecordByName(_scene.state, kHerrillLogNpcName);
- if (!script || !herrill || !herrill->visible ||
- herrill->deathOrMonsterfyFlag || herrill->deathDamageType != 0) {
- return false;
- }
-
- // Native update_actor_runtime_state has a hard-coded HERRILL_LOG removal path
- // while the Inner Sanctum Muckey combat state is active; queue the ordinary
- // live NPC death so the room's timer delay can play Herrill's death bank.
- if (!script->queueRuntimeNpcDeathOrMonsterfy(
- kHerrillLogNpcName, kNativeDefaultNpcDeathDamageType)) {
- return false;
- }
-
- herrill->deathDamageType = kNativeDefaultNpcDeathDamageType;
- debugC(1, kDebugCombat,
- "Harvester: queued Inner Sanctum Herrill death after monster='%s'",
- monster.monsterName.c_str());
- return true;
-}
-
-bool Combat::beginNpcDeathTransition(NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState,
- int deathDamageType, bool &needsRedraw) const {
- DeathAnimationRange deathRange;
- if (!resolveNpcDeathAnimationRange(npcEntity, !npc.monsterfyTargetName.empty(),
- deathDamageType, _engine.isGoreEnabled(), deathRange))
- return false;
-
- combatState.deathActive = true;
- combatState.deathFirstFrame = deathRange.firstFrame;
- combatState.deathLastFrame = deathRange.lastFrame;
- combatState.deathDamageType = deathDamageType;
- npc.deathDamageType = deathDamageType;
- npcEntity.setAnimationFrameRange(deathRange.firstFrame, deathRange.lastFrame, false);
- npcEntity.setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
- npcEntity.setAnimationEnabled(true);
- npcEntity.setCurrentFrame(deathRange.firstFrame);
- if (!npc.audioPath.empty())
- (void)_engine.playSound(npc.audioPath);
- debugC(1, kDebugCombat,
- "Harvester: combat npc death start target='%s' damage_type=%d frames=%d..%d monsterfy='%s' on_death='%s'",
- npc.npcName.c_str(), deathDamageType, deathRange.firstFrame, deathRange.lastFrame,
- npc.monsterfyTargetName.c_str(), npc.onDeathActionTag.c_str());
- needsRedraw = true;
- return true;
-}
-
-Common::Error Combat::finalizeNpcDeathTransition(NpcRecord &npc, NpcCombatState &combatState, bool &needsRedraw) const {
- Script *script = _engine.getScript();
- Entity *npcEntity = findSceneRuntimeEntity(_entityManager, npc.npcName);
- // Native state 0x35 leaves non-monsterfied NPCs as a dead state-0x38 actor
- // in the current room, while the NpcRecord death flag suppresses later setup.
- const bool retainDeathEntity =
- shouldRetainNpcDeathEntityInCurrentRoom(npc) &&
- combatState.deathLastFrame >= 0 &&
- npcEntity;
- const int monsterfyPosZ = (!npc.monsterfyTargetName.empty() && npcEntity)
- ? roundRoomCombatFloat(npcEntity->getZ())
- : Script::kNoMonsterfyPosZOverride;
- if (retainDeathEntity) {
- npcEntity->setAnimationRate(0);
- npcEntity->setAnimationFrameRange(combatState.deathLastFrame, combatState.deathLastFrame, false);
- npcEntity->setCurrentFrame(combatState.deathLastFrame);
- npcEntity->setAnimationEnabled(false);
- npcEntity->setHitTestMode(kEntityHitTestNone);
- npcEntity->setVisible(true);
- } else {
- _sceneRuntime.removeSceneEntityByName(npc.npcName);
- }
- npc.active = false;
- npc.visible = false;
- npc.savedVisible = false;
- npc.runtimeSpawned = retainDeathEntity;
- npc.runtimeState = retainDeathEntity ? combatState.deathLastFrame : -1;
- npc.deathOrMonsterfyFlag = true;
- if (combatState.deathDamageType != 0)
- npc.deathDamageType = combatState.deathDamageType;
- const bool runtimeChanged = script
- ? script->finalizeRuntimeNpcDeathOrMonsterfy(
- npc.npcName, npc.deathDamageType, false, -1, monsterfyPosZ)
- : false;
- debugC(1, kDebugCombat,
- "Harvester: combat npc death complete target='%s' damage_type=%d last_frame=%d preserve_runtime_actor=%d monsterfy='%s' on_death='%s'",
- npc.npcName.c_str(), combatState.deathDamageType, combatState.deathLastFrame,
- retainDeathEntity, npc.monsterfyTargetName.c_str(), npc.onDeathActionTag.c_str());
- clearRoomNpcCombatState(combatState);
-
- InteractionResult interaction;
- interaction.mutatedRuntimeState = runtimeChanged;
- interaction.visualRuntimeStateChanged = runtimeChanged;
- if (script && !npc.onDeathActionTag.empty()) {
- InteractionResult deathInteraction;
- if (script->executeActionTag(npc.onDeathActionTag, deathInteraction, true,
- ScriptExecutionContext::forNpcDeath(npc))) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
-
- return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
-}
-
-void Combat::startMonsterHitReaction(MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
- int attackerAttackFirstFrame, bool &needsRedraw) const {
- if (_combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
- needsRedraw = true;
-
- if (!combatState) {
- (void)playRandomRoomAttackSound(_engine,
- monster.hitSound1, monster.hitSound2, monster.hitSound3);
- return;
- }
-
- HitAnimationRange range;
- if (!resolveRoomMonsterHitAnimationRange(monsterEntity, attackerAttackFirstFrame, range)) {
- (void)playRandomRoomAttackSound(_engine,
- monster.hitSound1, monster.hitSound2, monster.hitSound3);
- debugC(1, kDebugCombat,
- "Harvester: combat monster hit reaction skipped target='%s' attack_frame=%d reason='no native hit bank'",
- monster.monsterName.c_str(), attackerAttackFirstFrame);
- return;
- }
-
- clearRoomMonsterAttackState(*combatState);
- clearRoomMonsterHitState(*combatState);
- combatState->hitActive = true;
- combatState->hitFirstFrame = range.firstFrame;
- combatState->hitLastFrame = range.lastFrame;
- combatState->hitResumeFacing = range.resumeFacing;
- combatState->hitKnockbackRemainingX = range.knockbackX;
- combatState->hitKnockbackDecayStep = kNativeCombatHitKnockbackDecayStep;
- combatState->hitSoundPlayed =
- monster.hitSound1.empty() && monster.hitSound2.empty() && monster.hitSound3.empty();
- combatState->nextMovementTick = 0;
- monster.facing = range.resumeFacing;
- monsterEntity.setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- monsterEntity.setAnimationRate(kNativeMonsterHitAnimationRate);
- monsterEntity.setAnimationEnabled(true);
- monsterEntity.setCurrentFrame(range.firstFrame);
- if (!combatState->hitSoundPlayed && monster.hitSoundTriggerFrame <= 0) {
- (void)playRandomRoomAttackSound(_engine,
- monster.hitSound1, monster.hitSound2, monster.hitSound3);
- combatState->hitSoundPlayed = true;
- }
- debugC(1, kDebugCombat,
- "Harvester: combat monster hit reaction target='%s' attack_frame=%d frames=%d..%d resume_facing=%d knockback=%d rate=%d",
- monster.monsterName.c_str(), attackerAttackFirstFrame, range.firstFrame, range.lastFrame,
- range.resumeFacing, range.knockbackX, kNativeMonsterHitAnimationRate);
- needsRedraw = true;
-}
-
Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
const RoomPlayerState &playerState = _player.getState();
if (!_engine.isCombatDebugEnabled())
@@ -715,17 +390,7 @@ Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool
DeathAnimationRange deathRange;
if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
debugDamageType, _engine.isGoreEnabled(), deathRange)) {
- combatState->deathActive = true;
- combatState->deathFirstFrame = deathRange.firstFrame;
- combatState->deathLastFrame = deathRange.lastFrame;
- combatState->deathDamageType = debugDamageType;
- combatState->nextMovementTick = 0;
- monsterEntity->setAnimationFrameRange(deathRange.firstFrame, deathRange.lastFrame, false);
- monsterEntity->setAnimationRate(kRoomMonsterAnimationRate);
- monsterEntity->setAnimationEnabled(true);
- monsterEntity->setCurrentFrame(deathRange.firstFrame);
- if (!monster->deathSound.empty())
- (void)_engine.playSound(monster->deathSound);
+ startMonsterDeathAnimation(*monster, *monsterEntity, *combatState, deathRange, debugDamageType);
needsRedraw = true;
return Common::kNoError;
}
@@ -776,44 +441,8 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
continue;
}
if (combatState.deathActive) {
- if (entity->getCurrentFrame() == combatState.deathLastFrame) {
- combatState.deathActive = false;
- monster.active = false;
- monster.visible = true;
- monster.runtimeSpawned = true;
- monster.runtimeState = entity->getCurrentFrame();
- entity->setAnimationRate(0);
- entity->setAnimationFrameRange(monster.runtimeState, monster.runtimeState, false);
- entity->setCurrentFrame(monster.runtimeState);
- entity->setAnimationEnabled(false);
- (void)script.syncRuntimeMonsterRecord(monster);
- debugC(1, kDebugCombat,
- "Harvester: combat monster death complete target='%s' damage_type=%d corpse_frame=%d on_death='%s'",
- monster.monsterName.c_str(), combatState.deathDamageType,
- monster.runtimeState,
- monster.onDeathActionTag.c_str());
- clearRoomMonsterCombatState(combatState);
- const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(monster);
-
- InteractionResult interaction;
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- if (!monster.onDeathActionTag.empty()) {
- InteractionResult deathInteraction;
- if (script.executeActionTag(
- monster.onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(monster))) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
- if (herrillDeathQueued) {
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
-
- return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
- }
+ if (entity->getCurrentFrame() == combatState.deathLastFrame)
+ return finalizeMonsterDeathTransition(script, monster, *entity, combatState, needsRedraw);
continue;
}
if (monster.currentHitPoints <= 0) {
@@ -828,40 +457,7 @@ Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
}
if (combatState.hitActive) {
- const int currentFrame = entity->getCurrentFrame();
- if (!combatState.hitSoundPlayed &&
- monster.hitSoundTriggerFrame >= 0 &&
- currentFrame >= combatState.hitFirstFrame + monster.hitSoundTriggerFrame) {
- (void)playRandomRoomAttackSound(_engine,
- monster.hitSound1, monster.hitSound2, monster.hitSound3);
- combatState.hitSoundPlayed = true;
- }
- if (combatState.hitKnockbackRemainingX != 0) {
- const int step = MAX(1, ABS(combatState.hitKnockbackDecayStep));
- const int knockbackStep = combatState.hitKnockbackRemainingX > 0
- ? MIN(combatState.hitKnockbackRemainingX, step)
- : MAX(combatState.hitKnockbackRemainingX, -step);
- const int previousX = monster.posX;
- monster.posX = CLIP<int>(
- monster.posX + knockbackStep, monster.screenMinXBound, monster.screenMaxXBound);
- if (monster.posX != previousX) {
- (void)applyRoomActorPlacement(
- _scene.state, *entity, monster.posX, monster.posY, static_cast<float>(monster.posZ));
- (void)script.syncRuntimeMonsterRecord(monster);
- debugC(1, kDebugCombat,
- "Harvester: combat monster hit knockback target='%s' from_x=%d to_x=%d remaining=%d",
- monster.monsterName.c_str(), previousX, monster.posX,
- combatState.hitKnockbackRemainingX - (monster.posX - previousX));
- needsRedraw = true;
- combatState.hitKnockbackRemainingX -= monster.posX - previousX;
- } else {
- combatState.hitKnockbackRemainingX = 0;
- }
- }
- if (currentFrame == combatState.hitLastFrame) {
- clearRoomMonsterHitState(combatState);
- needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, false) || needsRedraw;
- }
+ updateMonsterHitReaction(script, monster, *entity, combatState, needsRedraw);
continue;
}
@@ -1178,17 +774,7 @@ Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
DeathAnimationRange deathRange;
if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
damageType, _engine.isGoreEnabled(), deathRange)) {
- combatState->deathActive = true;
- combatState->deathFirstFrame = deathRange.firstFrame;
- combatState->deathLastFrame = deathRange.lastFrame;
- combatState->deathDamageType = damageType;
- combatState->nextMovementTick = 0;
- monsterEntity->setAnimationFrameRange(deathRange.firstFrame, deathRange.lastFrame, false);
- monsterEntity->setAnimationRate(kRoomMonsterAnimationRate);
- monsterEntity->setAnimationEnabled(true);
- monsterEntity->setCurrentFrame(deathRange.firstFrame);
- if (!monster->deathSound.empty())
- (void)_engine.playSound(monster->deathSound);
+ startMonsterDeathAnimation(*monster, *monsterEntity, *combatState, deathRange, damageType);
debugC(1, kDebugCombat,
"Harvester: combat monster death start target='%s' damage_type=%d frames=%d..%d gore=%d on_death='%s'",
monster->monsterName.c_str(), damageType,
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index 5d49b6c6f9f..567c6e09fa2 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -170,6 +170,13 @@ private:
void startMonsterHitReaction(MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
int attackerAttackFirstFrame, bool &needsRedraw) const;
+ void updateMonsterHitReaction(Script &script, MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, bool &needsRedraw) const;
+ void startMonsterDeathAnimation(const MonsterRecord &monster, Entity &monsterEntity,
+ MonsterCombatState &combatState, const DeathAnimationRange &deathRange, int deathDamageType) const;
+ Common::Error finalizeMonsterDeathTransition(Script &script, MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, bool &needsRedraw) const;
+
static Entity *findSceneRuntimeEntity(EntityManager *entityManager, const Common::String &name);
static NpcRecord *findRoomNpcRecordByName(RoomSetupState &state, const Common::String &name);
static Entity *selectMonsterEntity(EntityManager *entityManager, const MonsterRecord &monster);
diff --git a/engines/harvester/combat_reactions.cpp b/engines/harvester/combat_reactions.cpp
new file mode 100644
index 00000000000..e6704967707
--- /dev/null
+++ b/engines/harvester/combat_reactions.cpp
@@ -0,0 +1,451 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/interaction.h"
+#include "harvester/scene_entities.h"
+
+namespace Harvester {
+
+const int kNativeMonsterHitAnimationRate = 5;
+const int kNativeCombatHitKnockbackDistance = 18;
+const int kNativeCombatHitKnockbackDecayStep = 3;
+const char *const kInnerSanctumRoomName = "INNERSANCTUM";
+const char *const kHerrillLogNpcName = "HERRILL_LOG";
+const char *const kMuckeyMonsterName = "MUCKEY";
+const char *const kActivateInnerSanctumDoorActionTag = "ACTV_INNERSAN_DOOR";
+const int kNativeDefaultNpcDeathDamageType = 1;
+
+bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc) {
+ return npc.monsterfyTargetName.empty();
+}
+
+bool isRetainedCurrentRoomNpcDeathRecord(const NpcRecord &npc) {
+ return npc.deathOrMonsterfyFlag &&
+ npc.monsterfyTargetName.empty() &&
+ npc.deathDamageType != 0 &&
+ npc.runtimeState >= 0;
+}
+
+bool runtimeEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
+ if (!entity.hasFrames() || firstFrame < 0 || lastFrame < firstFrame)
+ return false;
+
+ return static_cast<uint>(lastFrame) < entity.getFrameCount();
+}
+
+bool runtimeEntityHasDrawableFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
+ return runtimeEntityHasFrameRange(entity, firstFrame, lastFrame) &&
+ entity.hasOpaqueFramesInRange(firstFrame, lastFrame);
+}
+
+bool resolveMonsterDeathAnimationRange(const Entity &entity, int facing,
+ int deathDamageType, bool goreEnabled, DeathAnimationRange &range) {
+ const bool preferLeftBank = facing == 1;
+ const DeathAnimationRange rightBludge(0xb0, 0xb9);
+ const DeathAnimationRange leftBludge(0xba, 0xc3);
+ const DeathAnimationRange rightSlash(0x100, 0x109);
+ const DeathAnimationRange leftSlash(0x10a, 0x113);
+ const DeathAnimationRange rightProjectile(0x114, 0x11d);
+ const DeathAnimationRange leftProjectile(0x11e, 0x127);
+
+ const DeathAnimationRange primaryBludge = preferLeftBank ? leftBludge : rightBludge;
+ const DeathAnimationRange primarySlash = preferLeftBank ? leftSlash : rightSlash;
+ const DeathAnimationRange primaryProjectile = preferLeftBank ? leftProjectile : rightProjectile;
+ const DeathAnimationRange fallbackBludge = preferLeftBank ? rightBludge : leftBludge;
+ const DeathAnimationRange fallbackSlash = preferLeftBank ? rightSlash : leftSlash;
+ const DeathAnimationRange fallbackProjectile = preferLeftBank ? rightProjectile : leftProjectile;
+
+ auto chooseIfAvailable = [&](const DeathAnimationRange &candidate) {
+ if (!runtimeEntityHasDrawableFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
+ return false;
+ range = candidate;
+ return true;
+ };
+
+ if ((!goreEnabled || deathDamageType == 1) &&
+ (chooseIfAvailable(primaryBludge) || chooseIfAvailable(fallbackBludge)))
+ return true;
+ if (deathDamageType == 2 &&
+ (chooseIfAvailable(primarySlash) || chooseIfAvailable(fallbackSlash)))
+ return true;
+ if (deathDamageType == 4 &&
+ (chooseIfAvailable(primaryProjectile) || chooseIfAvailable(fallbackProjectile)))
+ return true;
+
+ return chooseIfAvailable(primaryBludge) || chooseIfAvailable(primarySlash) ||
+ chooseIfAvailable(primaryProjectile) || chooseIfAvailable(fallbackBludge) ||
+ chooseIfAvailable(fallbackSlash) || chooseIfAvailable(fallbackProjectile);
+}
+
+bool resolveNpcDeathAnimationRange(const Entity &entity, bool hasMonsterfyTarget,
+ int deathDamageType, bool goreEnabled, DeathAnimationRange &range) {
+ const DeathAnimationRange goreBank(0x3c, 0x45);
+ const DeathAnimationRange bludgeBank(0x46, 0x4f);
+ const bool preferGoreBank = hasMonsterfyTarget || (goreEnabled && deathDamageType != 1);
+
+ auto chooseIfAvailable = [&](const DeathAnimationRange &candidate) {
+ if (!runtimeEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
+ return false;
+ range = candidate;
+ return true;
+ };
+
+ if (preferGoreBank && chooseIfAvailable(goreBank))
+ return true;
+ if (chooseIfAvailable(bludgeBank))
+ return true;
+
+ return chooseIfAvailable(goreBank);
+}
+
+bool resolveRoomMonsterHitAnimationRange(const Entity &entity,
+ int attackerAttackFirstFrame, HitAnimationRange &range) {
+ HitAnimationRange candidate;
+ switch (attackerAttackFirstFrame) {
+ case 0x64:
+ candidate = HitAnimationRange(0x92, 0x94, 2, -kNativeCombatHitKnockbackDistance);
+ break;
+ case 0x5a:
+ candidate = HitAnimationRange(0x8f, 0x91, 2, -kNativeCombatHitKnockbackDistance);
+ break;
+ case 0x50:
+ candidate = HitAnimationRange(0x8c, 0x8e, 2, -kNativeCombatHitKnockbackDistance);
+ break;
+ case 0x82:
+ candidate = HitAnimationRange(0x9b, 0x9d, 1, kNativeCombatHitKnockbackDistance);
+ break;
+ case 0x78:
+ candidate = HitAnimationRange(0x98, 0x9a, 1, kNativeCombatHitKnockbackDistance);
+ break;
+ case 0x6e:
+ candidate = HitAnimationRange(0x95, 0x97, 1, kNativeCombatHitKnockbackDistance);
+ break;
+ default:
+ return false;
+ }
+
+ if (!runtimeEntityHasDrawableFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
+ return false;
+
+ range = candidate;
+ return true;
+}
+
+NpcCombatUpdateResult Combat::updateRoomNpcCombat(bool &needsRedraw) {
+ NpcCombatUpdateResult result;
+ for (uint i = 0; i < _scene.state.roomNpcs.size(); ++i) {
+ NpcRecord &npc = _scene.state.roomNpcs[i];
+ NpcCombatState &combatState = _npcCombatStates[i];
+ Entity *entity = findSceneRuntimeEntity(_entityManager, npc.npcName);
+ if (!entity || !npc.visible || npc.deathOrMonsterfyFlag) {
+ clearRoomNpcCombatState(combatState);
+ continue;
+ }
+ if (!combatState.deathActive && npc.active && npc.deathDamageType != 0) {
+ if (beginNpcDeathTransition(npc, *entity, combatState, npc.deathDamageType, needsRedraw))
+ continue;
+
+ debugC(1, kDebugCombat,
+ "Harvester: room npc death fallback target='%s' damage_type=%d on_death='%s' reason='no death bank'",
+ npc.npcName.c_str(), npc.deathDamageType, npc.onDeathActionTag.c_str());
+ combatState.deathDamageType = npc.deathDamageType;
+ combatState.deathLastFrame = entity->getCurrentFrame();
+ result.finalizedDeath = true;
+ result.error = finalizeNpcDeathTransition(npc, combatState, needsRedraw);
+ return result;
+ }
+ if (!combatState.deathActive)
+ continue;
+ if (entity->getCurrentFrame() != combatState.deathLastFrame)
+ continue;
+
+ entity->setAnimationRate(0);
+ entity->setAnimationFrameRange(combatState.deathLastFrame, combatState.deathLastFrame, false);
+ entity->setCurrentFrame(combatState.deathLastFrame);
+ entity->setAnimationEnabled(false);
+ result.finalizedDeath = true;
+ result.error = finalizeNpcDeathTransition(npc, combatState, needsRedraw);
+ return result;
+ }
+ return result;
+}
+
+bool Combat::queueInnerSanctumHerrillDeathAfterMuck(const MonsterRecord &monster) const {
+ if (!monster.roomName.equalsIgnoreCase(kInnerSanctumRoomName) ||
+ !monster.monsterName.equalsIgnoreCase(kMuckeyMonsterName) ||
+ !monster.onDeathActionTag.equalsIgnoreCase(kActivateInnerSanctumDoorActionTag)) {
+ return false;
+ }
+
+ Script *script = _engine.getScript();
+ NpcRecord *herrill = findRoomNpcRecordByName(_scene.state, kHerrillLogNpcName);
+ if (!script || !herrill || !herrill->visible ||
+ herrill->deathOrMonsterfyFlag || herrill->deathDamageType != 0) {
+ return false;
+ }
+
+ // There's a hard-coded HERRILL_LOG removal path
+ // while the Inner Sanctum Muckey combat state is active; queue the ordinary
+ // live NPC death so the room's timer delay can play Herrill's death bank.
+ if (!script->queueRuntimeNpcDeathOrMonsterfy(
+ kHerrillLogNpcName, kNativeDefaultNpcDeathDamageType)) {
+ return false;
+ }
+
+ herrill->deathDamageType = kNativeDefaultNpcDeathDamageType;
+ debugC(1, kDebugCombat,
+ "Harvester: queued Inner Sanctum Herrill death after monster='%s'",
+ monster.monsterName.c_str());
+ return true;
+}
+
+bool Combat::beginNpcDeathTransition(NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState,
+ int deathDamageType, bool &needsRedraw) const {
+ DeathAnimationRange deathRange;
+ if (!resolveNpcDeathAnimationRange(npcEntity, !npc.monsterfyTargetName.empty(),
+ deathDamageType, _engine.isGoreEnabled(), deathRange))
+ return false;
+
+ combatState.deathActive = true;
+ combatState.deathFirstFrame = deathRange.firstFrame;
+ combatState.deathLastFrame = deathRange.lastFrame;
+ combatState.deathDamageType = deathDamageType;
+ npc.deathDamageType = deathDamageType;
+ npcEntity.setAnimationFrameRange(deathRange.firstFrame, deathRange.lastFrame, false);
+ npcEntity.setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
+ npcEntity.setAnimationEnabled(true);
+ npcEntity.setCurrentFrame(deathRange.firstFrame);
+ if (!npc.audioPath.empty())
+ (void)_engine.playSound(npc.audioPath);
+ debugC(1, kDebugCombat,
+ "Harvester: combat npc death start target='%s' damage_type=%d frames=%d..%d monsterfy='%s' on_death='%s'",
+ npc.npcName.c_str(), deathDamageType, deathRange.firstFrame, deathRange.lastFrame,
+ npc.monsterfyTargetName.c_str(), npc.onDeathActionTag.c_str());
+ needsRedraw = true;
+ return true;
+}
+
+Common::Error Combat::finalizeNpcDeathTransition(NpcRecord &npc, NpcCombatState &combatState, bool &needsRedraw) const {
+ Script *script = _engine.getScript();
+ Entity *npcEntity = findSceneRuntimeEntity(_entityManager, npc.npcName);
+ // State 0x35 leaves non-monsterfied NPCs as a dead state-0x38 actor
+ // in the current room, while the NpcRecord death flag suppresses later setup.
+ const bool retainDeathEntity =
+ shouldRetainNpcDeathEntityInCurrentRoom(npc) &&
+ combatState.deathLastFrame >= 0 &&
+ npcEntity;
+ const int monsterfyPosZ = (!npc.monsterfyTargetName.empty() && npcEntity)
+ ? roundRoomCombatFloat(npcEntity->getZ())
+ : Script::kNoMonsterfyPosZOverride;
+ if (retainDeathEntity) {
+ npcEntity->setAnimationRate(0);
+ npcEntity->setAnimationFrameRange(combatState.deathLastFrame, combatState.deathLastFrame, false);
+ npcEntity->setCurrentFrame(combatState.deathLastFrame);
+ npcEntity->setAnimationEnabled(false);
+ npcEntity->setHitTestMode(kEntityHitTestNone);
+ npcEntity->setVisible(true);
+ } else {
+ _sceneRuntime.removeSceneEntityByName(npc.npcName);
+ }
+ npc.active = false;
+ npc.visible = false;
+ npc.savedVisible = false;
+ npc.runtimeSpawned = retainDeathEntity;
+ npc.runtimeState = retainDeathEntity ? combatState.deathLastFrame : -1;
+ npc.deathOrMonsterfyFlag = true;
+ if (combatState.deathDamageType != 0)
+ npc.deathDamageType = combatState.deathDamageType;
+ const bool runtimeChanged = script
+ ? script->finalizeRuntimeNpcDeathOrMonsterfy(
+ npc.npcName, npc.deathDamageType, false, -1, monsterfyPosZ)
+ : false;
+ debugC(1, kDebugCombat,
+ "Harvester: combat npc death complete target='%s' damage_type=%d last_frame=%d preserve_runtime_actor=%d monsterfy='%s' on_death='%s'",
+ npc.npcName.c_str(), combatState.deathDamageType, combatState.deathLastFrame,
+ retainDeathEntity, npc.monsterfyTargetName.c_str(), npc.onDeathActionTag.c_str());
+ clearRoomNpcCombatState(combatState);
+
+ InteractionResult interaction;
+ interaction.mutatedRuntimeState = runtimeChanged;
+ interaction.visualRuntimeStateChanged = runtimeChanged;
+ if (script && !npc.onDeathActionTag.empty()) {
+ InteractionResult deathInteraction;
+ if (script->executeActionTag(npc.onDeathActionTag, deathInteraction, true,
+ ScriptExecutionContext::forNpcDeath(npc))) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+
+ return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
+void Combat::startMonsterHitReaction(MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
+ int attackerAttackFirstFrame, bool &needsRedraw) const {
+ if (_combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
+ needsRedraw = true;
+
+ if (!combatState) {
+ (void)playRandomRoomAttackSound(_engine,
+ monster.hitSound1, monster.hitSound2, monster.hitSound3);
+ return;
+ }
+
+ HitAnimationRange range;
+ if (!resolveRoomMonsterHitAnimationRange(monsterEntity, attackerAttackFirstFrame, range)) {
+ (void)playRandomRoomAttackSound(_engine,
+ monster.hitSound1, monster.hitSound2, monster.hitSound3);
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster hit reaction skipped target='%s' attack_frame=%d reason='no native hit bank'",
+ monster.monsterName.c_str(), attackerAttackFirstFrame);
+ return;
+ }
+
+ clearRoomMonsterAttackState(*combatState);
+ clearRoomMonsterHitState(*combatState);
+ combatState->hitActive = true;
+ combatState->hitFirstFrame = range.firstFrame;
+ combatState->hitLastFrame = range.lastFrame;
+ combatState->hitResumeFacing = range.resumeFacing;
+ combatState->hitKnockbackRemainingX = range.knockbackX;
+ combatState->hitKnockbackDecayStep = kNativeCombatHitKnockbackDecayStep;
+ combatState->hitSoundPlayed =
+ monster.hitSound1.empty() && monster.hitSound2.empty() && monster.hitSound3.empty();
+ combatState->nextMovementTick = 0;
+ monster.facing = range.resumeFacing;
+ monsterEntity.setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
+ monsterEntity.setAnimationRate(kNativeMonsterHitAnimationRate);
+ monsterEntity.setAnimationEnabled(true);
+ monsterEntity.setCurrentFrame(range.firstFrame);
+ if (!combatState->hitSoundPlayed && monster.hitSoundTriggerFrame <= 0) {
+ (void)playRandomRoomAttackSound(_engine,
+ monster.hitSound1, monster.hitSound2, monster.hitSound3);
+ combatState->hitSoundPlayed = true;
+ }
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster hit reaction target='%s' attack_frame=%d frames=%d..%d resume_facing=%d knockback=%d rate=%d",
+ monster.monsterName.c_str(), attackerAttackFirstFrame, range.firstFrame, range.lastFrame,
+ range.resumeFacing, range.knockbackX, kNativeMonsterHitAnimationRate);
+ needsRedraw = true;
+}
+
+void Combat::startMonsterDeathAnimation(const MonsterRecord &monster, Entity &monsterEntity,
+ MonsterCombatState &combatState, const DeathAnimationRange &deathRange, int deathDamageType) const {
+ combatState.deathActive = true;
+ combatState.deathFirstFrame = deathRange.firstFrame;
+ combatState.deathLastFrame = deathRange.lastFrame;
+ combatState.deathDamageType = deathDamageType;
+ combatState.nextMovementTick = 0;
+ monsterEntity.setAnimationFrameRange(deathRange.firstFrame, deathRange.lastFrame, false);
+ monsterEntity.setAnimationRate(kRoomMonsterAnimationRate);
+ monsterEntity.setAnimationEnabled(true);
+ monsterEntity.setCurrentFrame(deathRange.firstFrame);
+ if (!monster.deathSound.empty())
+ (void)_engine.playSound(monster.deathSound);
+}
+
+Common::Error Combat::finalizeMonsterDeathTransition(Script &script, MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, bool &needsRedraw) const {
+ combatState.deathActive = false;
+ monster.active = false;
+ monster.visible = true;
+ monster.runtimeSpawned = true;
+ monster.runtimeState = entity.getCurrentFrame();
+ entity.setAnimationRate(0);
+ entity.setAnimationFrameRange(monster.runtimeState, monster.runtimeState, false);
+ entity.setCurrentFrame(monster.runtimeState);
+ entity.setAnimationEnabled(false);
+ (void)script.syncRuntimeMonsterRecord(monster);
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster death complete target='%s' damage_type=%d corpse_frame=%d on_death='%s'",
+ monster.monsterName.c_str(), combatState.deathDamageType,
+ monster.runtimeState,
+ monster.onDeathActionTag.c_str());
+ clearRoomMonsterCombatState(combatState);
+ const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(monster);
+
+ InteractionResult interaction;
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ if (!monster.onDeathActionTag.empty()) {
+ InteractionResult deathInteraction;
+ if (script.executeActionTag(
+ monster.onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(monster))) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+ if (herrillDeathQueued) {
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+
+ return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
+void Combat::updateMonsterHitReaction(Script &script, MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, bool &needsRedraw) const {
+ const int currentFrame = entity.getCurrentFrame();
+ if (!combatState.hitSoundPlayed &&
+ monster.hitSoundTriggerFrame >= 0 &&
+ currentFrame >= combatState.hitFirstFrame + monster.hitSoundTriggerFrame) {
+ (void)playRandomRoomAttackSound(_engine,
+ monster.hitSound1, monster.hitSound2, monster.hitSound3);
+ combatState.hitSoundPlayed = true;
+ }
+ if (combatState.hitKnockbackRemainingX != 0) {
+ const int step = MAX(1, ABS(combatState.hitKnockbackDecayStep));
+ const int knockbackStep = combatState.hitKnockbackRemainingX > 0
+ ? MIN(combatState.hitKnockbackRemainingX, step)
+ : MAX(combatState.hitKnockbackRemainingX, -step);
+ const int previousX = monster.posX;
+ monster.posX = CLIP<int>(
+ monster.posX + knockbackStep, monster.screenMinXBound, monster.screenMaxXBound);
+ if (monster.posX != previousX) {
+ (void)applyRoomActorPlacement(
+ _scene.state, entity, monster.posX, monster.posY, static_cast<float>(monster.posZ));
+ (void)script.syncRuntimeMonsterRecord(monster);
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster hit knockback target='%s' from_x=%d to_x=%d remaining=%d",
+ monster.monsterName.c_str(), previousX, monster.posX,
+ combatState.hitKnockbackRemainingX - (monster.posX - previousX));
+ needsRedraw = true;
+ combatState.hitKnockbackRemainingX -= monster.posX - previousX;
+ } else {
+ combatState.hitKnockbackRemainingX = 0;
+ }
+ }
+ if (currentFrame == combatState.hitLastFrame) {
+ clearRoomMonsterHitState(combatState);
+ needsRedraw = setRoomMonsterAnimation(entity, monster.facing, false) || needsRedraw;
+ }
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 47eb793b898..781e96f3e6f 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -25,6 +25,7 @@ MODULE_OBJS = \
resources.o \
combat.o \
combat_geometry.o \
+ combat_reactions.o \
combat_targeting.o \
interaction.o \
input.o \
Commit: ab8709bc895d782cc8ff7423e83b0af63ee79423
https://github.com/scummvm/scummvm/commit/ab8709bc895d782cc8ff7423e83b0af63ee79423
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate monster pursuit and attack progression
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/combat_monsters.cpp
engines/harvester/combat.cpp
engines/harvester/combat.h
engines/harvester/module.mk
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index d5e69c0be80..02f6d46c8f4 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -35,15 +35,8 @@
namespace Harvester {
-const int kRoomMonsterAnimationRate = 17;
-const int kNativeMonsterAttackAnimationRate = 30;
-const uint32 kNativeMonsterAttackCooldownBaseTicks = 50;
const float kNativeNpcMonsterZExtent = 5.0f;
const int kRoomNpcAmbientLastFrame = 0x3b;
-const int kDefaultMonsterAttackContactFrameOffset = 2;
-const int kMuckeyAttackContactFrameOffset = 7;
-const float kNativeMonsterPursuitZTolerance = 2.0f;
-const float kNativeMonsterHorizontalWaypointTolerance = 50.0f;
const uint32 kNativeKeyboardAttackRepeatTicks = 25;
const char *const kNativeHitEffectResourcePath = "blood.abm";
const int kNativeHitEffectAnimationRate = 10;
@@ -104,16 +97,6 @@ static void drawShadowedRoomText(Graphics::Screen &screen, const Graphics::Font
}
-struct RoomMonsterFacingAnimationRange {
- RoomMonsterFacingAnimationRange() {}
- RoomMonsterFacingAnimationRange(int walkFirstFrame, int walkLastFrame, int idleFrame)
- : walkFirstFrame(walkFirstFrame), walkLastFrame(walkLastFrame), idleFrame(idleFrame) {}
-
- int walkFirstFrame = 0;
- int walkLastFrame = 0;
- int idleFrame = 0;
-};
-
Common::String resolveCombatLoadoutStatusLabel(Script &script, const ObjectRecord &object) {
Common::String label = script.resolveInventoryTooltipText(object);
if (!label.empty()) {
@@ -149,21 +132,6 @@ Common::String resolveCombatLoadoutStatusLabel(Script &script, const ObjectRecor
return label;
}
-RoomMonsterFacingAnimationRange resolveRoomMonsterFacingAnimationRange(int facing) {
- switch (facing) {
- case 0:
- return RoomMonsterFacingAnimationRange(0x00, 0x09, 0x3b);
- case 1:
- return RoomMonsterFacingAnimationRange(0x0f, 0x18, 0x0e);
- case 2:
- return RoomMonsterFacingAnimationRange(0x2d, 0x36, 0x2c);
- case 3:
- return RoomMonsterFacingAnimationRange(0x1e, 0x27, 0x28);
- default:
- return RoomMonsterFacingAnimationRange(0x00, 0x09, 0x3b);
- }
-}
-
} // End of anonymous namespace
bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info) {
@@ -215,112 +183,6 @@ Common::String buildCombatLoadoutStatusMessage(Script &script, const ObjectRecor
return Common::String::format("Arming %s...", label.c_str());
}
-bool setRoomMonsterAnimation(Entity &entity, int facing, bool walking) {
- const RoomMonsterFacingAnimationRange range = resolveRoomMonsterFacingAnimationRange(facing);
- if (walking) {
- const bool changed = entity.getAnimationRate() != kRoomMonsterAnimationRate ||
- entity.getCurrentFrame() < range.walkFirstFrame ||
- entity.getCurrentFrame() > range.walkLastFrame;
- entity.setAnimationRate(kRoomMonsterAnimationRate);
- entity.setAnimationFrameRange(range.walkFirstFrame, range.walkLastFrame, true);
- entity.setAnimationEnabled(true);
- return changed;
- }
-
- const int standingFrame = entity.hasOpaqueFramesInRange(range.idleFrame, range.idleFrame)
- ? range.idleFrame
- : range.walkFirstFrame;
- const bool changed = entity.getAnimationRate() != 0 ||
- entity.getCurrentFrame() != standingFrame;
- entity.setAnimationRate(0);
- entity.setAnimationFrameRange(standingFrame, standingFrame, false);
- entity.setCurrentFrame(standingFrame);
- entity.setAnimationEnabled(false);
- return changed;
-}
-
-bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
- const Entity &entity, int actorCenterX, int targetCenterX,
- AttackAnimationRange &range) {
- // Native class-6 attackers pick one of the 0x16..0x1b attack states, which map to
- // the 0x50..0x8b strike banks. The 0x8c..0x9d banks are hit reactions, not attacks.
- static const AttackAnimationRange kLeftAttackRanges[3] = {
- AttackAnimationRange(0x64, 0x6d, 1),
- AttackAnimationRange(0x5a, 0x63, 1),
- AttackAnimationRange(0x50, 0x59, 1)
- };
- static const AttackAnimationRange kRightAttackRanges[3] = {
- AttackAnimationRange(0x82, 0x8b, 2),
- AttackAnimationRange(0x78, 0x81, 2),
- AttackAnimationRange(0x6e, 0x77, 2)
- };
-
- const AttackAnimationRange *candidates =
- targetCenterX < actorCenterX ? kLeftAttackRanges : kRightAttackRanges;
- int availableCandidateIndices[3];
- int availableCandidateCount = 0;
- for (int i = 0; i < 3; ++i) {
- if (!runtimeEntityHasDrawableFrameRange(entity, candidates[i].firstFrame, candidates[i].lastFrame))
- continue;
- availableCandidateIndices[availableCandidateCount++] = i;
- }
-
- if (availableCandidateCount == 0)
- return false;
-
- const int selectedIndex = availableCandidateIndices[
- availableCandidateCount > 1 ? engine.getRandomNumber(availableCandidateCount - 1) : 0];
- range = candidates[selectedIndex];
- return true;
-}
-
-int stepTowardsRoomCombatInt(int current, int target, int step) {
- if (step <= 0 || current == target)
- return current;
- if (current < target)
- return MIN(current + step, target);
- return MAX(current - step, target);
-}
-
-bool playRandomRoomAttackSound(HarvesterEngine &engine, const Common::String &sound1,
- const Common::String &sound2, const Common::String &sound3) {
- const Common::String *availableSounds[3];
- uint availableCount = 0;
- if (!sound1.empty())
- availableSounds[availableCount++] = &sound1;
- if (!sound2.empty())
- availableSounds[availableCount++] = &sound2;
- if (!sound3.empty())
- availableSounds[availableCount++] = &sound3;
- if (availableCount == 0)
- return false;
-
- const uint soundIndex = availableCount > 1
- ? engine.getRandomNumber(availableCount - 1)
- : 0;
- return engine.playSound(*availableSounds[soundIndex]);
-}
-
-int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fallbackY) {
- if (state.roomMaxZScreenY < 0 || state.roomMinZScreenY < state.roomMaxZScreenY)
- return fallbackY;
- if (state.roomMaxZ == state.roomMinZ)
- return CLIP<int>(fallbackY, state.roomMaxZScreenY, state.roomMinZScreenY);
-
- const float clampedZ = clampRoomDepthForEvent(state, z);
- const float offset = (static_cast<float>(state.roomMaxZ) - clampedZ) *
- static_cast<float>(state.roomMinZScreenY - state.roomMaxZScreenY) /
- static_cast<float>(state.roomMaxZ - state.roomMinZ);
- return CLIP<int>(state.roomMaxZScreenY + roundRoomCombatFloat(offset),
- state.roomMaxZScreenY, state.roomMinZScreenY);
-}
-
-int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster) {
- return monster.monsterName.equalsIgnoreCase("MUCKEY")
- ? kMuckeyAttackContactFrameOffset
- : kDefaultMonsterAttackContactFrameOffset;
-}
-
const char *describeCombatTargetClass(int classId) {
switch (classId) {
case kEntityClassNpc:
@@ -413,253 +275,6 @@ Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool
return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
}
-Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
- const RoomPlayerState &playerState = _player.getState();
- if (!_entityManager)
- return Common::kNoError;
-
- Script &script = *_engine.getScript();
- const uint32 now = Player::getRuntimeClockTicks();
- const uint32 moveInterval = MAX<uint32>(1, 100U / static_cast<uint32>(kRoomMonsterAnimationRate));
- const int horizontalStepBase = 8;
- const int depthStep = MAX<int>(1, roundRoomCombatFloat(
- _scene.state.roomZVelocityStep > 0.0f ? _scene.state.roomZVelocityStep : 1.0f));
- bool playerAlive = script.getPlayerCurrentHitPoints() > 0;
-
- resizeCombatStates();
-
- const NpcCombatUpdateResult npcResult = updateRoomNpcCombat(needsRedraw);
- if (npcResult.finalizedDeath)
- return npcResult.error;
-
- for (uint i = 0; i < _scene.state.roomMonsters.size(); ++i) {
- MonsterRecord &monster = _scene.state.roomMonsters[i];
- MonsterCombatState &combatState = _monsterCombatStates[i];
- Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
- if (!entity || !monster.visible) {
- clearRoomMonsterCombatState(combatState);
- continue;
- }
- if (combatState.deathActive) {
- if (entity->getCurrentFrame() == combatState.deathLastFrame)
- return finalizeMonsterDeathTransition(script, monster, *entity, combatState, needsRedraw);
- continue;
- }
- if (monster.currentHitPoints <= 0) {
- clearRoomMonsterCombatState(combatState);
- continue;
- }
-
- if (!monster.active || !playerState.entity || !playerAlive) {
- clearRoomMonsterCombatState(combatState);
- needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, false) || needsRedraw;
- continue;
- }
-
- if (combatState.hitActive) {
- updateMonsterHitReaction(script, monster, *entity, combatState, needsRedraw);
- continue;
- }
-
- if (combatState.attackActive) {
- const int currentFrame = entity->getCurrentFrame();
- if (!combatState.attackSoundPlayed &&
- monster.attackSoundTriggerFrame >= 0 &&
- currentFrame >= combatState.attackFirstFrame + monster.attackSoundTriggerFrame) {
- (void)playRandomRoomAttackSound(_engine,
- monster.attackSound1, monster.attackSound2, monster.attackSound3);
- combatState.attackSoundPlayed = true;
- }
- if (!combatState.attackContactResolved &&
- combatState.attackContactFrame >= 0 &&
- currentFrame >= combatState.attackContactFrame) {
- combatState.attackContactResolved = true;
- combatState.attackContactFrame = -1;
- const bool hasTrackedPlayerTarget =
- playerState.entity &&
- !combatState.attackTargetName.empty() &&
- playerState.entity->getName().equalsIgnoreCase(combatState.attackTargetName);
- if (!playerAlive || !hasTrackedPlayerTarget) {
- debugC(1, kDebugCombat,
- "Harvester: combat monster attack miss monster='%s' frame=%d player_hp=%d reason='no live target'",
- monster.monsterName.c_str(), currentFrame, script.getPlayerCurrentHitPoints());
- } else {
- const int playerHitPointsBefore = script.getPlayerCurrentHitPoints();
- const bool damageDisabled = _cheats.isPlayerDamageDisabled();
- const bool changed = damageDisabled
- ? false
- : script.adjustPlayerCurrentHitPoints(-monster.damageAmount);
- const int playerHitPointsAfter = script.getPlayerCurrentHitPoints();
- const int damageLanded = playerHitPointsBefore - playerHitPointsAfter;
- debugC(1, kDebugCombat,
- "Harvester: combat monster attack hit monster='%s' frame=%d damage=%d damage_type='%s' player_hp=%d->%d changed=%d damage_disabled=%d",
- monster.monsterName.c_str(), currentFrame, monster.damageAmount,
- Player::describeCombatDamageType(monster.damageType),
- playerHitPointsBefore, playerHitPointsAfter, changed,
- damageDisabled ? 1 : 0);
- if (damageLanded > 0 && playerState.entity) {
- _combatEffects.spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
- _player.cancelIdleAnimation();
- if (playerHitPointsAfter > 0 &&
- _player.startHitAnimation(_engine, combatState.attackFirstFrame)) {
- _callbacks.notePlayerActivity();
- }
- }
- if (changed && playerHitPointsAfter <= 0) {
- playerAlive = false;
- _callbacks.stopPlayerRegionInteraction();
- _callbacks.startPlayerDefeatSequence(
- "combat_monster_hit", monster.monsterName, monster.damageType);
- return Common::kNoError;
- }
- }
- needsRedraw = true;
- }
- if (currentFrame == combatState.attackLastFrame) {
- clearRoomMonsterAttackState(combatState);
- needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, false) || needsRedraw;
- }
- continue;
- }
-
- const bool animationDrivesMovement =
- entity->isAnimationEnabled() && entity->getAnimationRate() != 0;
- if (animationDrivesMovement) {
- combatState.nextMovementTick = 0;
- if (!combatState.allowInitialPursuitStep && !entity->didAnimationAdvanceLastTick())
- continue;
- } else {
- if (combatState.nextMovementTick != 0 && (int32)(now - combatState.nextMovementTick) < 0)
- continue;
- combatState.nextMovementTick = now + moveInterval;
- }
- combatState.allowInitialPursuitStep = false;
-
- const int previousX = monster.posX;
- const int previousY = monster.posY;
- const int previousZ = monster.posZ;
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- const Common::Rect monsterRect = entity->getScreenRect();
- const int playerCenterX = playerRect.left + playerRect.width() / 2;
- const int monsterCenterX = monsterRect.left + monsterRect.width() / 2;
- const int liveCenterDx = playerCenterX - monsterCenterX;
- const int absLiveCenterDx = ABS(liveCenterDx);
- const int playerTargetZ = roundRoomCombatFloat(playerState.z);
- const float zDelta = playerState.z - static_cast<float>(monster.posZ);
- const float absZDelta = zDelta >= 0.0f ? zDelta : -zDelta;
- const int engageDistance = MAX(0, monster.engageDistance);
- const int liveWaypointTolerance = roundRoomCombatFloat(
- Player::computeDepthScale(_scene.state,
- MIN<float>(playerState.z, static_cast<float>(monster.posZ))) * kNativeMonsterHorizontalWaypointTolerance);
- bool attemptedMoveX = false;
- bool attemptedMoveZ = false;
- if (absZDelta > kNativeMonsterPursuitZTolerance) {
- attemptedMoveZ = true;
- monster.posZ = stepTowardsRoomCombatInt(monster.posZ, playerTargetZ, depthStep);
- monster.posZ = static_cast<int>(clampRoomDepthForEvent(_scene.state, static_cast<float>(monster.posZ)));
- monster.posY = mapRoomDepthToScreenYForCombat(_scene.state, static_cast<float>(monster.posZ), monster.posY);
- } else if (absLiveCenterDx > engageDistance) {
- // Native class-6 pursuit stores a target frame-left X, not a center X.
- const int desiredFrameLeftX = monsterCenterX < playerCenterX
- ? playerRect.right + engageDistance - monsterRect.width() / 2
- : playerRect.left - engageDistance + monsterRect.width() / 2;
- if (ABS(monsterRect.left - desiredFrameLeftX) > liveWaypointTolerance) {
- attemptedMoveX = true;
- const int step = MAX<int>(1, roundRoomCombatFloat(
- Player::computeDepthScale(_scene.state, static_cast<float>(monster.posZ)) * static_cast<float>(horizontalStepBase)));
- const int monsterFrameLeftOffset = monsterRect.left - monster.posX;
- const int desiredLogicalCenterX = desiredFrameLeftX - monsterFrameLeftOffset;
- monster.posX = stepTowardsRoomCombatInt(monster.posX, desiredLogicalCenterX, step);
- monster.posX = CLIP<int>(monster.posX, monster.screenMinXBound, monster.screenMaxXBound);
- }
- }
-
- const bool moved = monster.posX != previousX || monster.posY != previousY || monster.posZ != previousZ;
- const int stalledDirections = moved ? 0 : ((attemptedMoveX ? 1 : 0) | (attemptedMoveZ ? 2 : 0));
- const bool stallChanged = combatState.loggedPursuitStallDirections != stalledDirections;
- combatState.loggedPursuitStallDirections = stalledDirections;
- if (moved) {
- const int movedX = monster.posX - previousX;
- const int movedY = monster.posY - previousY;
- if (ABS(movedX) > ABS(movedY))
- monster.facing = movedX < 0 ? 1 : 2;
- else if (movedY != 0)
- monster.facing = movedY < 0 ? 3 : 0;
- (void)applyRoomActorPlacement(_scene.state, *entity, monster.posX, monster.posY, static_cast<float>(monster.posZ));
- if (monster.posZ != previousZ)
- _entityManager->reinsertSceneEntity(entity);
- (void)script.syncRuntimeMonsterRecord(monster);
- debugC(3, kDebugCombat,
- "Harvester: combat monster chase move monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d from=(%d,%d,z=%d) to=(%d,%d,z=%d) facing=%d",
- monster.monsterName.c_str(), liveCenterDx, static_cast<double>(zDelta), engageDistance, liveWaypointTolerance,
- previousX, previousY, previousZ, monster.posX, monster.posY, monster.posZ, monster.facing);
- needsRedraw = true;
- needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, true) || needsRedraw;
- continue;
- }
-
- if (stalledDirections != 0 && stallChanged) {
- debugC(2, kDebugCombat,
- "Harvester: combat monster chase stalled monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d pos=(%d,%d,z=%d) attempted_x=%d attempted_z=%d",
- monster.monsterName.c_str(), liveCenterDx, static_cast<double>(zDelta), engageDistance, liveWaypointTolerance,
- monster.posX, monster.posY, monster.posZ, attemptedMoveX, attemptedMoveZ);
- }
-
- needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, false) || needsRedraw;
- const bool closeEnoughForAttack = isWithinNativeMonsterAttackEntryRange(
- *entity, static_cast<float>(monster.posZ), *playerState.entity, playerState.z, engageDistance);
- // Class-6 monsters store `now + random(99)` when they arm an attack, then
- // apply a 0x32-tick gate when deciding whether a later strike may start.
- // A fresh zeroed spawn therefore attacks immediately once it reaches engage range.
- const uint32 attackCooldownDeadline =
- combatState.attackCooldownSeedTick + kNativeMonsterAttackCooldownBaseTicks;
- const bool attackCooldownExpired = combatState.attackCooldownSeedTick == 0 ||
- (int32)(now - attackCooldownDeadline) > 0;
- const int closeRangeBypassDistance = MAX(1, engageDistance / 3);
- if (!closeEnoughForAttack)
- continue;
- if (!attackCooldownExpired && absLiveCenterDx > closeRangeBypassDistance)
- continue;
-
- AttackAnimationRange range;
- if (!resolveMonsterAttackAnimationRange(_engine, *entity, monsterCenterX, playerCenterX, range)) {
- debugC(1, kDebugCombat,
- "Harvester: combat monster attack skipped monster='%s' reason='no native attack bank' target_side=%s",
- monster.monsterName.c_str(), playerCenterX < monsterCenterX ? "left" : "right");
- continue;
- }
- combatState.attackActive = true;
- combatState.attackFirstFrame = range.firstFrame;
- combatState.attackLastFrame = range.lastFrame;
- combatState.attackContactFrame =
- range.firstFrame + resolveMonsterAttackContactFrameOffset(monster);
- combatState.attackResumeFacing = range.resumeFacing;
- combatState.attackSoundPlayed =
- monster.attackSound1.empty() && monster.attackSound2.empty() && monster.attackSound3.empty();
- combatState.attackContactResolved = false;
- combatState.attackTargetName = playerState.entity->getName();
- combatState.attackCooldownSeedTick = now + _engine.getRandomNumber(99);
- combatState.loggedPursuitStallDirections = 0;
- monster.facing = range.resumeFacing;
- entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- entity->setAnimationRate(kNativeMonsterAttackAnimationRate);
- entity->setAnimationEnabled(true);
- entity->setCurrentFrame(range.firstFrame);
- debugC(2, kDebugCombat,
- "Harvester: combat monster attack start monster='%s' target='%s'",
- monster.monsterName.c_str(), combatState.attackTargetName.c_str());
- debugC(3, kDebugCombat,
- "Harvester: combat monster attack timing monster='%s' target='%s' frames=%d..%d contact=%d resume_facing=%d live_center_dx=%d z_delta=%.2f engage=%d rate=%d attack_seed_tick=%u close_bypass=%d",
- monster.monsterName.c_str(), combatState.attackTargetName.c_str(), range.firstFrame, range.lastFrame,
- combatState.attackContactFrame, range.resumeFacing, liveCenterDx, static_cast<double>(zDelta),
- engageDistance, kNativeMonsterAttackAnimationRate, combatState.attackCooldownSeedTick,
- closeRangeBypassDistance);
- needsRedraw = true;
- }
-
- return Common::kNoError;
-}
-
Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
const RoomPlayerState &playerState = _player.getState();
if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index 567c6e09fa2..722d279cca7 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -158,6 +158,16 @@ public:
Common::Error resolvePlayerAttackContact(bool &needsRedraw);
private:
+ struct MonsterPursuitResult;
+
+ // Returns true when player defeat must end the entire combat update.
+ bool updateMonsterAttack(Script &script, const MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, bool &playerAlive, bool &needsRedraw);
+ MonsterPursuitResult updateMonsterPursuit(Script &script, MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, int depthStep, bool &needsRedraw) const;
+ void beginMonsterAttack(MonsterRecord &monster, Entity &entity, MonsterCombatState &combatState,
+ const MonsterPursuitResult &pursuit, uint32 now, bool &needsRedraw) const;
+
void resizeCombatStates();
bool queueInnerSanctumHerrillDeathAfterMuck(const MonsterRecord &monster) const;
// Called after resizeCombatStates; NPC finalization ends the entire combat update.
diff --git a/engines/harvester/combat_monsters.cpp b/engines/harvester/combat_monsters.cpp
new file mode 100644
index 00000000000..71cc0853b42
--- /dev/null
+++ b/engines/harvester/combat_monsters.cpp
@@ -0,0 +1,461 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/cheats.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/interaction.h"
+#include "harvester/player.h"
+
+namespace Harvester {
+
+const int kRoomMonsterAnimationRate = 17;
+const int kNativeMonsterAttackAnimationRate = 30;
+const uint32 kNativeMonsterAttackCooldownBaseTicks = 50;
+const int kDefaultMonsterAttackContactFrameOffset = 2;
+const int kMuckeyAttackContactFrameOffset = 7;
+const float kNativeMonsterPursuitZTolerance = 2.0f;
+const float kNativeMonsterHorizontalWaypointTolerance = 50.0f;
+
+namespace {
+
+struct RoomMonsterFacingAnimationRange {
+ RoomMonsterFacingAnimationRange() {}
+ RoomMonsterFacingAnimationRange(int walkFirstFrame, int walkLastFrame, int idleFrame)
+ : walkFirstFrame(walkFirstFrame), walkLastFrame(walkLastFrame), idleFrame(idleFrame) {}
+
+ int walkFirstFrame = 0;
+ int walkLastFrame = 0;
+ int idleFrame = 0;
+};
+
+RoomMonsterFacingAnimationRange resolveRoomMonsterFacingAnimationRange(int facing) {
+ switch (facing) {
+ case 0:
+ return RoomMonsterFacingAnimationRange(0x00, 0x09, 0x3b);
+ case 1:
+ return RoomMonsterFacingAnimationRange(0x0f, 0x18, 0x0e);
+ case 2:
+ return RoomMonsterFacingAnimationRange(0x2d, 0x36, 0x2c);
+ case 3:
+ return RoomMonsterFacingAnimationRange(0x1e, 0x27, 0x28);
+ default:
+ return RoomMonsterFacingAnimationRange(0x00, 0x09, 0x3b);
+ }
+}
+
+} // End of anonymous namespace
+
+struct Combat::MonsterPursuitResult {
+ bool moved;
+ int playerCenterX;
+ int monsterCenterX;
+ int liveCenterDx;
+ int absLiveCenterDx;
+ float zDelta;
+ int engageDistance;
+};
+
+bool setRoomMonsterAnimation(Entity &entity, int facing, bool walking) {
+ const RoomMonsterFacingAnimationRange range = resolveRoomMonsterFacingAnimationRange(facing);
+ if (walking) {
+ const bool changed = entity.getAnimationRate() != kRoomMonsterAnimationRate ||
+ entity.getCurrentFrame() < range.walkFirstFrame ||
+ entity.getCurrentFrame() > range.walkLastFrame;
+ entity.setAnimationRate(kRoomMonsterAnimationRate);
+ entity.setAnimationFrameRange(range.walkFirstFrame, range.walkLastFrame, true);
+ entity.setAnimationEnabled(true);
+ return changed;
+ }
+
+ const int standingFrame = entity.hasOpaqueFramesInRange(range.idleFrame, range.idleFrame)
+ ? range.idleFrame
+ : range.walkFirstFrame;
+ const bool changed = entity.getAnimationRate() != 0 ||
+ entity.getCurrentFrame() != standingFrame;
+ entity.setAnimationRate(0);
+ entity.setAnimationFrameRange(standingFrame, standingFrame, false);
+ entity.setCurrentFrame(standingFrame);
+ entity.setAnimationEnabled(false);
+ return changed;
+}
+
+bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
+ const Entity &entity, int actorCenterX, int targetCenterX,
+ AttackAnimationRange &range) {
+ // Class-6 attackers pick one of the 0x16..0x1b attack states, which map to
+ // the 0x50..0x8b strike banks. The 0x8c..0x9d banks are hit reactions, not attacks.
+ static const AttackAnimationRange kLeftAttackRanges[3] = {
+ AttackAnimationRange(0x64, 0x6d, 1),
+ AttackAnimationRange(0x5a, 0x63, 1),
+ AttackAnimationRange(0x50, 0x59, 1)
+ };
+ static const AttackAnimationRange kRightAttackRanges[3] = {
+ AttackAnimationRange(0x82, 0x8b, 2),
+ AttackAnimationRange(0x78, 0x81, 2),
+ AttackAnimationRange(0x6e, 0x77, 2)
+ };
+
+ const AttackAnimationRange *candidates =
+ targetCenterX < actorCenterX ? kLeftAttackRanges : kRightAttackRanges;
+ int availableCandidateIndices[3];
+ int availableCandidateCount = 0;
+ for (int i = 0; i < 3; ++i) {
+ if (!runtimeEntityHasDrawableFrameRange(entity, candidates[i].firstFrame, candidates[i].lastFrame))
+ continue;
+ availableCandidateIndices[availableCandidateCount++] = i;
+ }
+
+ if (availableCandidateCount == 0)
+ return false;
+
+ const int selectedIndex = availableCandidateIndices[
+ availableCandidateCount > 1 ? engine.getRandomNumber(availableCandidateCount - 1) : 0];
+ range = candidates[selectedIndex];
+ return true;
+}
+
+int stepTowardsRoomCombatInt(int current, int target, int step) {
+ if (step <= 0 || current == target)
+ return current;
+ if (current < target)
+ return MIN(current + step, target);
+ return MAX(current - step, target);
+}
+
+bool playRandomRoomAttackSound(HarvesterEngine &engine, const Common::String &sound1,
+ const Common::String &sound2, const Common::String &sound3) {
+ const Common::String *availableSounds[3];
+ uint availableCount = 0;
+ if (!sound1.empty())
+ availableSounds[availableCount++] = &sound1;
+ if (!sound2.empty())
+ availableSounds[availableCount++] = &sound2;
+ if (!sound3.empty())
+ availableSounds[availableCount++] = &sound3;
+ if (availableCount == 0)
+ return false;
+
+ const uint soundIndex = availableCount > 1
+ ? engine.getRandomNumber(availableCount - 1)
+ : 0;
+ return engine.playSound(*availableSounds[soundIndex]);
+}
+
+int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fallbackY) {
+ if (state.roomMaxZScreenY < 0 || state.roomMinZScreenY < state.roomMaxZScreenY)
+ return fallbackY;
+ if (state.roomMaxZ == state.roomMinZ)
+ return CLIP<int>(fallbackY, state.roomMaxZScreenY, state.roomMinZScreenY);
+
+ const float clampedZ = clampRoomDepthForEvent(state, z);
+ const float offset = (static_cast<float>(state.roomMaxZ) - clampedZ) *
+ static_cast<float>(state.roomMinZScreenY - state.roomMaxZScreenY) /
+ static_cast<float>(state.roomMaxZ - state.roomMinZ);
+ return CLIP<int>(state.roomMaxZScreenY + roundRoomCombatFloat(offset),
+ state.roomMaxZScreenY, state.roomMinZScreenY);
+}
+
+int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster) {
+ return monster.monsterName.equalsIgnoreCase("MUCKEY")
+ ? kMuckeyAttackContactFrameOffset
+ : kDefaultMonsterAttackContactFrameOffset;
+}
+
+Common::Error Combat::updateRoomMonsterCombat(bool &needsRedraw) {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!_entityManager)
+ return Common::kNoError;
+
+ Script &script = *_engine.getScript();
+ const uint32 now = Player::getRuntimeClockTicks();
+ const uint32 moveInterval = MAX<uint32>(1, 100U / static_cast<uint32>(kRoomMonsterAnimationRate));
+ const int depthStep = MAX<int>(1, roundRoomCombatFloat(
+ _scene.state.roomZVelocityStep > 0.0f ? _scene.state.roomZVelocityStep : 1.0f));
+ bool playerAlive = script.getPlayerCurrentHitPoints() > 0;
+
+ resizeCombatStates();
+
+ const NpcCombatUpdateResult npcResult = updateRoomNpcCombat(needsRedraw);
+ if (npcResult.finalizedDeath)
+ return npcResult.error;
+
+ for (uint i = 0; i < _scene.state.roomMonsters.size(); ++i) {
+ MonsterRecord &monster = _scene.state.roomMonsters[i];
+ MonsterCombatState &combatState = _monsterCombatStates[i];
+ Entity *entity = findSceneRuntimeEntity(_entityManager, monster.monsterName);
+ if (!entity || !monster.visible) {
+ clearRoomMonsterCombatState(combatState);
+ continue;
+ }
+ if (combatState.deathActive) {
+ if (entity->getCurrentFrame() == combatState.deathLastFrame)
+ return finalizeMonsterDeathTransition(script, monster, *entity, combatState, needsRedraw);
+ continue;
+ }
+ if (monster.currentHitPoints <= 0) {
+ clearRoomMonsterCombatState(combatState);
+ continue;
+ }
+
+ if (!monster.active || !playerState.entity || !playerAlive) {
+ clearRoomMonsterCombatState(combatState);
+ needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, false) || needsRedraw;
+ continue;
+ }
+
+ if (combatState.hitActive) {
+ updateMonsterHitReaction(script, monster, *entity, combatState, needsRedraw);
+ continue;
+ }
+
+ if (combatState.attackActive) {
+ if (updateMonsterAttack(script, monster, *entity, combatState, playerAlive, needsRedraw))
+ return Common::kNoError;
+ continue;
+ }
+
+ const bool animationDrivesMovement =
+ entity->isAnimationEnabled() && entity->getAnimationRate() != 0;
+ if (animationDrivesMovement) {
+ combatState.nextMovementTick = 0;
+ if (!combatState.allowInitialPursuitStep && !entity->didAnimationAdvanceLastTick())
+ continue;
+ } else {
+ if (combatState.nextMovementTick != 0 && (int32)(now - combatState.nextMovementTick) < 0)
+ continue;
+ combatState.nextMovementTick = now + moveInterval;
+ }
+ combatState.allowInitialPursuitStep = false;
+
+ const MonsterPursuitResult pursuit = updateMonsterPursuit(
+ script, monster, *entity, combatState, depthStep, needsRedraw);
+ if (pursuit.moved)
+ continue;
+ beginMonsterAttack(monster, *entity, combatState, pursuit, now, needsRedraw);
+ }
+
+ return Common::kNoError;
+}
+
+bool Combat::updateMonsterAttack(Script &script, const MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, bool &playerAlive, bool &needsRedraw) {
+ const RoomPlayerState &playerState = _player.getState();
+ const int currentFrame = entity.getCurrentFrame();
+ if (!combatState.attackSoundPlayed &&
+ monster.attackSoundTriggerFrame >= 0 &&
+ currentFrame >= combatState.attackFirstFrame + monster.attackSoundTriggerFrame) {
+ (void)playRandomRoomAttackSound(_engine,
+ monster.attackSound1, monster.attackSound2, monster.attackSound3);
+ combatState.attackSoundPlayed = true;
+ }
+ if (!combatState.attackContactResolved &&
+ combatState.attackContactFrame >= 0 &&
+ currentFrame >= combatState.attackContactFrame) {
+ combatState.attackContactResolved = true;
+ combatState.attackContactFrame = -1;
+ const bool hasTrackedPlayerTarget =
+ playerState.entity &&
+ !combatState.attackTargetName.empty() &&
+ playerState.entity->getName().equalsIgnoreCase(combatState.attackTargetName);
+ if (!playerAlive || !hasTrackedPlayerTarget) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster attack miss monster='%s' frame=%d player_hp=%d reason='no live target'",
+ monster.monsterName.c_str(), currentFrame, script.getPlayerCurrentHitPoints());
+ } else {
+ const int playerHitPointsBefore = script.getPlayerCurrentHitPoints();
+ const bool damageDisabled = _cheats.isPlayerDamageDisabled();
+ const bool changed = damageDisabled
+ ? false
+ : script.adjustPlayerCurrentHitPoints(-monster.damageAmount);
+ const int playerHitPointsAfter = script.getPlayerCurrentHitPoints();
+ const int damageLanded = playerHitPointsBefore - playerHitPointsAfter;
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster attack hit monster='%s' frame=%d damage=%d damage_type='%s' player_hp=%d->%d changed=%d damage_disabled=%d",
+ monster.monsterName.c_str(), currentFrame, monster.damageAmount,
+ Player::describeCombatDamageType(monster.damageType),
+ playerHitPointsBefore, playerHitPointsAfter, changed,
+ damageDisabled ? 1 : 0);
+ if (damageLanded > 0 && playerState.entity) {
+ _combatEffects.spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
+ _player.cancelIdleAnimation();
+ if (playerHitPointsAfter > 0 &&
+ _player.startHitAnimation(_engine, combatState.attackFirstFrame)) {
+ _callbacks.notePlayerActivity();
+ }
+ }
+ if (changed && playerHitPointsAfter <= 0) {
+ playerAlive = false;
+ _callbacks.stopPlayerRegionInteraction();
+ _callbacks.startPlayerDefeatSequence(
+ "combat_monster_hit", monster.monsterName, monster.damageType);
+ return true;
+ }
+ }
+ needsRedraw = true;
+ }
+ if (currentFrame == combatState.attackLastFrame) {
+ clearRoomMonsterAttackState(combatState);
+ needsRedraw = setRoomMonsterAnimation(entity, monster.facing, false) || needsRedraw;
+ }
+ return false;
+}
+
+Combat::MonsterPursuitResult Combat::updateMonsterPursuit(Script &script, MonsterRecord &monster,
+ Entity &entity, MonsterCombatState &combatState, int depthStep, bool &needsRedraw) const {
+ const RoomPlayerState &playerState = _player.getState();
+ const int horizontalStepBase = 8;
+ const int previousX = monster.posX;
+ const int previousY = monster.posY;
+ const int previousZ = monster.posZ;
+ const Common::Rect playerRect = playerState.entity->getScreenRect();
+ const Common::Rect monsterRect = entity.getScreenRect();
+ const int playerCenterX = playerRect.left + playerRect.width() / 2;
+ const int monsterCenterX = monsterRect.left + monsterRect.width() / 2;
+ const int liveCenterDx = playerCenterX - monsterCenterX;
+ const int absLiveCenterDx = ABS(liveCenterDx);
+ const int playerTargetZ = roundRoomCombatFloat(playerState.z);
+ const float zDelta = playerState.z - static_cast<float>(monster.posZ);
+ const float absZDelta = zDelta >= 0.0f ? zDelta : -zDelta;
+ const int engageDistance = MAX(0, monster.engageDistance);
+ const int liveWaypointTolerance = roundRoomCombatFloat(
+ Player::computeDepthScale(_scene.state,
+ MIN<float>(playerState.z, static_cast<float>(monster.posZ))) * kNativeMonsterHorizontalWaypointTolerance);
+ bool attemptedMoveX = false;
+ bool attemptedMoveZ = false;
+ if (absZDelta > kNativeMonsterPursuitZTolerance) {
+ attemptedMoveZ = true;
+ monster.posZ = stepTowardsRoomCombatInt(monster.posZ, playerTargetZ, depthStep);
+ monster.posZ = static_cast<int>(clampRoomDepthForEvent(_scene.state, static_cast<float>(monster.posZ)));
+ monster.posY = mapRoomDepthToScreenYForCombat(_scene.state, static_cast<float>(monster.posZ), monster.posY);
+ } else if (absLiveCenterDx > engageDistance) {
+ // Native class-6 pursuit stores a target frame-left X, not a center X.
+ const int desiredFrameLeftX = monsterCenterX < playerCenterX
+ ? playerRect.right + engageDistance - monsterRect.width() / 2
+ : playerRect.left - engageDistance + monsterRect.width() / 2;
+ if (ABS(monsterRect.left - desiredFrameLeftX) > liveWaypointTolerance) {
+ attemptedMoveX = true;
+ const int step = MAX<int>(1, roundRoomCombatFloat(
+ Player::computeDepthScale(_scene.state, static_cast<float>(monster.posZ)) * static_cast<float>(horizontalStepBase)));
+ const int monsterFrameLeftOffset = monsterRect.left - monster.posX;
+ const int desiredLogicalCenterX = desiredFrameLeftX - monsterFrameLeftOffset;
+ monster.posX = stepTowardsRoomCombatInt(monster.posX, desiredLogicalCenterX, step);
+ monster.posX = CLIP<int>(monster.posX, monster.screenMinXBound, monster.screenMaxXBound);
+ }
+ }
+
+ const bool moved = monster.posX != previousX || monster.posY != previousY || monster.posZ != previousZ;
+ const MonsterPursuitResult result = {
+ moved, playerCenterX, monsterCenterX, liveCenterDx, absLiveCenterDx, zDelta, engageDistance
+ };
+ const int stalledDirections = moved ? 0 : ((attemptedMoveX ? 1 : 0) | (attemptedMoveZ ? 2 : 0));
+ const bool stallChanged = combatState.loggedPursuitStallDirections != stalledDirections;
+ combatState.loggedPursuitStallDirections = stalledDirections;
+ if (moved) {
+ const int movedX = monster.posX - previousX;
+ const int movedY = monster.posY - previousY;
+ if (ABS(movedX) > ABS(movedY))
+ monster.facing = movedX < 0 ? 1 : 2;
+ else if (movedY != 0)
+ monster.facing = movedY < 0 ? 3 : 0;
+ (void)applyRoomActorPlacement(_scene.state, entity, monster.posX, monster.posY, static_cast<float>(monster.posZ));
+ if (monster.posZ != previousZ)
+ _entityManager->reinsertSceneEntity(&entity);
+ (void)script.syncRuntimeMonsterRecord(monster);
+ debugC(3, kDebugCombat,
+ "Harvester: combat monster chase move monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d from=(%d,%d,z=%d) to=(%d,%d,z=%d) facing=%d",
+ monster.monsterName.c_str(), liveCenterDx, static_cast<double>(zDelta), engageDistance, liveWaypointTolerance,
+ previousX, previousY, previousZ, monster.posX, monster.posY, monster.posZ, monster.facing);
+ needsRedraw = true;
+ needsRedraw = setRoomMonsterAnimation(entity, monster.facing, true) || needsRedraw;
+ return result;
+ }
+
+ if (stalledDirections != 0 && stallChanged) {
+ debugC(2, kDebugCombat,
+ "Harvester: combat monster chase stalled monster='%s' live_center_dx=%d z_delta=%.2f engage=%d waypoint_tol=%d pos=(%d,%d,z=%d) attempted_x=%d attempted_z=%d",
+ monster.monsterName.c_str(), liveCenterDx, static_cast<double>(zDelta), engageDistance, liveWaypointTolerance,
+ monster.posX, monster.posY, monster.posZ, attemptedMoveX, attemptedMoveZ);
+ }
+ return result;
+}
+
+void Combat::beginMonsterAttack(MonsterRecord &monster, Entity &entity, MonsterCombatState &combatState,
+ const MonsterPursuitResult &pursuit, uint32 now, bool &needsRedraw) const {
+ const RoomPlayerState &playerState = _player.getState();
+ needsRedraw = setRoomMonsterAnimation(entity, monster.facing, false) || needsRedraw;
+ const bool closeEnoughForAttack = isWithinNativeMonsterAttackEntryRange(
+ entity, static_cast<float>(monster.posZ), *playerState.entity, playerState.z, pursuit.engageDistance);
+ // Class-6 monsters store `now + random(99)` when they arm an attack, then
+ // apply a 0x32-tick gate when deciding whether a later strike may start.
+ // A fresh zeroed spawn therefore attacks immediately once it reaches engage range.
+ const uint32 attackCooldownDeadline =
+ combatState.attackCooldownSeedTick + kNativeMonsterAttackCooldownBaseTicks;
+ const bool attackCooldownExpired = combatState.attackCooldownSeedTick == 0 ||
+ (int32)(now - attackCooldownDeadline) > 0;
+ const int closeRangeBypassDistance = MAX(1, pursuit.engageDistance / 3);
+ if (!closeEnoughForAttack)
+ return;
+ if (!attackCooldownExpired && pursuit.absLiveCenterDx > closeRangeBypassDistance)
+ return;
+
+ AttackAnimationRange range;
+ if (!resolveMonsterAttackAnimationRange(_engine, entity, pursuit.monsterCenterX, pursuit.playerCenterX, range)) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster attack skipped monster='%s' reason='no native attack bank' target_side=%s",
+ monster.monsterName.c_str(), pursuit.playerCenterX < pursuit.monsterCenterX ? "left" : "right");
+ return;
+ }
+ combatState.attackActive = true;
+ combatState.attackFirstFrame = range.firstFrame;
+ combatState.attackLastFrame = range.lastFrame;
+ combatState.attackContactFrame =
+ range.firstFrame + resolveMonsterAttackContactFrameOffset(monster);
+ combatState.attackResumeFacing = range.resumeFacing;
+ combatState.attackSoundPlayed =
+ monster.attackSound1.empty() && monster.attackSound2.empty() && monster.attackSound3.empty();
+ combatState.attackContactResolved = false;
+ combatState.attackTargetName = playerState.entity->getName();
+ combatState.attackCooldownSeedTick = now + _engine.getRandomNumber(99);
+ combatState.loggedPursuitStallDirections = 0;
+ monster.facing = range.resumeFacing;
+ entity.setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
+ entity.setAnimationRate(kNativeMonsterAttackAnimationRate);
+ entity.setAnimationEnabled(true);
+ entity.setCurrentFrame(range.firstFrame);
+ debugC(2, kDebugCombat,
+ "Harvester: combat monster attack start monster='%s' target='%s'",
+ monster.monsterName.c_str(), combatState.attackTargetName.c_str());
+ debugC(3, kDebugCombat,
+ "Harvester: combat monster attack timing monster='%s' target='%s' frames=%d..%d contact=%d resume_facing=%d live_center_dx=%d z_delta=%.2f engage=%d rate=%d attack_seed_tick=%u close_bypass=%d",
+ monster.monsterName.c_str(), combatState.attackTargetName.c_str(), range.firstFrame, range.lastFrame,
+ combatState.attackContactFrame, range.resumeFacing, pursuit.liveCenterDx, static_cast<double>(pursuit.zDelta),
+ pursuit.engageDistance, kNativeMonsterAttackAnimationRate, combatState.attackCooldownSeedTick,
+ closeRangeBypassDistance);
+ needsRedraw = true;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 781e96f3e6f..a9a24c3dacb 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -25,6 +25,7 @@ MODULE_OBJS = \
resources.o \
combat.o \
combat_geometry.o \
+ combat_monsters.o \
combat_reactions.o \
combat_targeting.o \
interaction.o \
Commit: edf006a38103d5f6c21e8a4e906909113bff6af3
https://github.com/scummvm/scummvm/commit/edf006a38103d5f6c21e8a4e906909113bff6af3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate player attack contact handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/combat_contact.cpp
engines/harvester/combat.cpp
engines/harvester/combat.h
engines/harvester/module.mk
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index 02f6d46c8f4..8b7e3ba8013 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -47,24 +47,6 @@ const int kCombatDebugDamagePopupVerticalGap = 6;
namespace {
-static int findRoomNpcIndexByName(const RoomSetupState &state, const Common::String &name) {
- for (uint i = 0; i < state.roomNpcs.size(); ++i) {
- if (state.roomNpcs[i].npcName.equalsIgnoreCase(name))
- return static_cast<int>(i);
- }
-
- return -1;
-}
-
-static int findRoomMonsterIndexByName(const RoomSetupState &state, const Common::String &name) {
- for (uint i = 0; i < state.roomMonsters.size(); ++i) {
- if (state.roomMonsters[i].monsterName.equalsIgnoreCase(name))
- return static_cast<int>(i);
- }
-
- return -1;
-}
-
static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
if (!palette)
return 0;
@@ -204,227 +186,6 @@ Combat::Combat(HarvesterEngine &engine, SceneState &scene, Player &player,
_callbacks(callbacks), _interactionProcessor(interactionProcessor) {
}
-Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
- const RoomPlayerState &playerState = _player.getState();
- if (!_engine.isCombatDebugEnabled())
- return Common::kNoError;
- MonsterRecord *monster = resolveKillTarget(mousePos);
- Entity *monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
- if (!monster || !monsterEntity || !monsterEntity->isVisible()) {
- debugC(1, kDebugCombat,
- "Harvester: debug combat kill skipped reason='no active monster' cursor=(%d,%d)",
- mousePos.x, mousePos.y);
- return Common::kNoError;
- }
-
- Script *script = _engine.getScript();
- if (!script) {
- debugC(1, kDebugCombat,
- "Harvester: debug combat kill skipped target='%s' reason='no startup script'",
- monster->monsterName.c_str());
- return Common::kNoError;
- }
-
- const int debugDamageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
- const int damageLanded = monster->currentHitPoints;
- monster->currentHitPoints = 0;
- if (damageLanded > 0)
- _combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
-
- if (playerState.attackTargetClassId == kEntityClassMonster &&
- playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
- _player.clearAttackTarget();
- }
-
- debugC(1, kDebugCombat,
- "Harvester: debug combat kill target='%s' damage=%d damage_type=%d cursor=(%d,%d)",
- monster->monsterName.c_str(), damageLanded, debugDamageType, mousePos.x, mousePos.y);
-
- const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
- MonsterCombatState *combatState =
- monsterIndex >= 0 && monsterIndex < static_cast<int>(_monsterCombatStates.size())
- ? &_monsterCombatStates[static_cast<uint>(monsterIndex)]
- : nullptr;
- if (combatState)
- clearRoomMonsterCombatState(*combatState);
- (void)script->syncRuntimeMonsterRecord(*monster);
-
- DeathAnimationRange deathRange;
- if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
- debugDamageType, _engine.isGoreEnabled(), deathRange)) {
- startMonsterDeathAnimation(*monster, *monsterEntity, *combatState, deathRange, debugDamageType);
- needsRedraw = true;
- return Common::kNoError;
- }
-
- monster->active = false;
- monster->visible = false;
- InteractionResult interaction;
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- if (!monster->onDeathActionTag.empty()) {
- InteractionResult deathInteraction;
- if (script->executeActionTag(
- monster->onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(*monster))) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
-
- return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
-}
-
-Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
- const RoomPlayerState &playerState = _player.getState();
- if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
- playerState.attackContactFrame < 0 ||
- playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
- return Common::kNoError;
- }
-
- const bool projectile = Player::isProjectileCombatLoadout(playerState.combatLoadout);
- if (projectile)
- _player.markAttackContactResolved();
- const int damageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
- const int damageAmount = Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout);
- Script *script = _engine.getScript();
- if (!script)
- return Common::kNoError;
-
- captureOverlappingNpcAttackTarget();
- if (playerState.attackTargetClassId == kEntityClassNpc) {
- Entity *npcEntity = nullptr;
- NpcRecord *npc = selectNpcAttackContactTarget(npcEntity);
- if (!npc) {
- if (playerState.entity->didAnimationAdvanceLastTick()) {
- debugC(3, kDebugCombat,
- "Harvester: combat player attack missed class='npc' target='%s' frame=%d reason='no reachable npc'",
- playerState.attackTargetName.c_str(), playerState.entity->getCurrentFrame());
- }
- return Common::kNoError;
- }
-
- _player.markAttackContactResolved();
- const bool runtimeChanged =
- script->queueRuntimeNpcDeathOrMonsterfy(npc->npcName, damageType);
- const bool jimmyFlagChanged = npc->npcName.equalsIgnoreCase("JIMMY")
- ? script->setRuntimeFlagValue("JIMMY_ATTACKED", true)
- : false;
- const int npcIndex = findRoomNpcIndexByName(_scene.state, npc->npcName);
- NpcCombatState *combatState =
- npcIndex >= 0 && npcIndex < static_cast<int>(_npcCombatStates.size())
- ? &_npcCombatStates[static_cast<uint>(npcIndex)]
- : nullptr;
- if (runtimeChanged) {
- npc->deathDamageType = damageType;
- if (combatState)
- clearRoomNpcCombatState(*combatState);
- }
- debugC(1, kDebugCombat,
- "Harvester: combat player npc hit target='%s' damage_type=%d runtime_changed=%d jimmy_flag=%d",
- npc->npcName.c_str(), damageType, runtimeChanged, jimmyFlagChanged);
- if (!runtimeChanged && !jimmyFlagChanged)
- return Common::kNoError;
- if (runtimeChanged && npcEntity) {
- const Common::String followTargetName = npc->monsterfyTargetName;
- if (_combatEffects.spawnCombatHitEffect(*npcEntity, followTargetName))
- needsRedraw = true;
- }
- if (runtimeChanged && combatState && npcEntity &&
- beginNpcDeathTransition(*npc, *npcEntity, *combatState, damageType, needsRedraw))
- return Common::kNoError;
- if (runtimeChanged) {
- debugC(1, kDebugCombat,
- "Harvester: combat npc defeated target='%s' damage_type=%d on_death='%s' fallback='no death bank'",
- npc->npcName.c_str(), damageType, npc->onDeathActionTag.c_str());
- NpcCombatState fallbackState;
- fallbackState.deathDamageType = damageType;
- return finalizeNpcDeathTransition(*npc, combatState ? *combatState : fallbackState, needsRedraw);
- }
-
- return Common::kNoError;
- }
-
- Entity *monsterEntity = nullptr;
- MonsterRecord *monster = selectMonsterAttackContactTarget(monsterEntity);
- if (!monster || !monsterEntity) {
- if (playerState.entity->didAnimationAdvanceLastTick()) {
- debugC(3, kDebugCombat,
- "Harvester: combat player attack missed class='monster' target='%s' frame=%d reason='no reachable monster'",
- playerState.attackTargetName.c_str(), playerState.entity->getCurrentFrame());
- }
- return Common::kNoError;
- }
-
- // update_actor_runtime_state (0x4d750) latches melee contact only after a hit.
- _player.markAttackContactResolved();
- const int hitPointsBefore = monster->currentHitPoints;
- monster->currentHitPoints = MAX(0,
- monster->currentHitPoints - damageAmount);
- const int damageLanded = hitPointsBefore - monster->currentHitPoints;
- const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
- MonsterCombatState *combatState =
- monsterIndex >= 0 && monsterIndex < static_cast<int>(_monsterCombatStates.size())
- ? &_monsterCombatStates[static_cast<uint>(monsterIndex)]
- : nullptr;
- debugC(1, kDebugCombat,
- "Harvester: combat player monster hit target='%s' loadout=%d weapon='%s' damage=%d damage_type='%s' hp=%d->%d reachable=1",
- monster->monsterName.c_str(), playerState.combatLoadout,
- Player::describeCombatLoadout(playerState.combatLoadout), damageAmount,
- Player::describeCombatDamageType(damageType), hitPointsBefore, monster->currentHitPoints);
- if (damageLanded > 0) {
- _combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
- startMonsterHitReaction(*monster, *monsterEntity, combatState, playerState.attackFirstFrame, needsRedraw);
- }
- if (monster->currentHitPoints > 0) {
- (void)script->syncRuntimeMonsterRecord(*monster);
- needsRedraw = true;
- return Common::kNoError;
- }
- if (combatState)
- clearRoomMonsterCombatState(*combatState);
- (void)script->syncRuntimeMonsterRecord(*monster);
-
- DeathAnimationRange deathRange;
- if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
- damageType, _engine.isGoreEnabled(), deathRange)) {
- startMonsterDeathAnimation(*monster, *monsterEntity, *combatState, deathRange, damageType);
- debugC(1, kDebugCombat,
- "Harvester: combat monster death start target='%s' damage_type=%d frames=%d..%d gore=%d on_death='%s'",
- monster->monsterName.c_str(), damageType,
- deathRange.firstFrame, deathRange.lastFrame,
- _engine.isGoreEnabled(), monster->onDeathActionTag.c_str());
- needsRedraw = true;
- return Common::kNoError;
- }
-
- monster->active = false;
- monster->visible = false;
- const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(*monster);
- debugC(1, kDebugCombat,
- "Harvester: combat monster defeated target='%s' on_death='%s' fallback='no death bank'",
- monster->monsterName.c_str(), monster->onDeathActionTag.c_str());
- InteractionResult interaction;
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- if (!monster->onDeathActionTag.empty()) {
- InteractionResult deathInteraction;
- if (script->executeActionTag(
- monster->onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(*monster))) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
- if (herrillDeathQueued) {
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
-
- return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
-}
-
void Combat::initializeCombatStates() {
_monsterCombatStates.resize(_scene.state.roomMonsters.size());
_npcCombatStates.resize(_scene.state.roomNpcs.size());
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index 722d279cca7..af7c5fd8799 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -158,6 +158,10 @@ public:
Common::Error resolvePlayerAttackContact(bool &needsRedraw);
private:
+ Common::Error resolvePlayerNpcAttackContact(Script &script, int damageType, bool &needsRedraw);
+ Common::Error resolvePlayerMonsterAttackContact(Script &script,
+ int damageType, int damageAmount, bool &needsRedraw);
+
struct MonsterPursuitResult;
// Returns true when player defeat must end the entire combat update.
diff --git a/engines/harvester/combat_contact.cpp b/engines/harvester/combat_contact.cpp
new file mode 100644
index 00000000000..b2fb878f4a2
--- /dev/null
+++ b/engines/harvester/combat_contact.cpp
@@ -0,0 +1,285 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/interaction.h"
+#include "harvester/player.h"
+
+namespace Harvester {
+
+namespace {
+
+static int findRoomNpcIndexByName(const RoomSetupState &state, const Common::String &name) {
+ for (uint i = 0; i < state.roomNpcs.size(); ++i) {
+ if (state.roomNpcs[i].npcName.equalsIgnoreCase(name))
+ return static_cast<int>(i);
+ }
+
+ return -1;
+}
+
+static int findRoomMonsterIndexByName(const RoomSetupState &state, const Common::String &name) {
+ for (uint i = 0; i < state.roomMonsters.size(); ++i) {
+ if (state.roomMonsters[i].monsterName.equalsIgnoreCase(name))
+ return static_cast<int>(i);
+ }
+
+ return -1;
+}
+
+} // End of anonymous namespace
+
+Common::Error Combat::resolvePlayerAttackContact(bool &needsRedraw) {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
+ playerState.attackContactFrame < 0 ||
+ playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
+ return Common::kNoError;
+ }
+
+ const bool projectile = Player::isProjectileCombatLoadout(playerState.combatLoadout);
+ if (projectile)
+ _player.markAttackContactResolved();
+ const int damageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
+ const int damageAmount = Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout);
+ Script *script = _engine.getScript();
+ if (!script)
+ return Common::kNoError;
+
+ captureOverlappingNpcAttackTarget();
+ if (playerState.attackTargetClassId == kEntityClassNpc)
+ return resolvePlayerNpcAttackContact(*script, damageType, needsRedraw);
+ return resolvePlayerMonsterAttackContact(*script, damageType, damageAmount, needsRedraw);
+}
+
+Common::Error Combat::resolvePlayerNpcAttackContact(Script &script, int damageType, bool &needsRedraw) {
+ const RoomPlayerState &playerState = _player.getState();
+ Entity *npcEntity = nullptr;
+ NpcRecord *npc = selectNpcAttackContactTarget(npcEntity);
+ if (!npc) {
+ if (playerState.entity->didAnimationAdvanceLastTick()) {
+ debugC(3, kDebugCombat,
+ "Harvester: combat player attack missed class='npc' target='%s' frame=%d reason='no reachable npc'",
+ playerState.attackTargetName.c_str(), playerState.entity->getCurrentFrame());
+ }
+ return Common::kNoError;
+ }
+
+ _player.markAttackContactResolved();
+ const bool runtimeChanged =
+ script.queueRuntimeNpcDeathOrMonsterfy(npc->npcName, damageType);
+ const bool jimmyFlagChanged = npc->npcName.equalsIgnoreCase("JIMMY")
+ ? script.setRuntimeFlagValue("JIMMY_ATTACKED", true)
+ : false;
+ const int npcIndex = findRoomNpcIndexByName(_scene.state, npc->npcName);
+ NpcCombatState *combatState =
+ npcIndex >= 0 && npcIndex < static_cast<int>(_npcCombatStates.size())
+ ? &_npcCombatStates[static_cast<uint>(npcIndex)]
+ : nullptr;
+ if (runtimeChanged) {
+ npc->deathDamageType = damageType;
+ if (combatState)
+ clearRoomNpcCombatState(*combatState);
+ }
+ debugC(1, kDebugCombat,
+ "Harvester: combat player npc hit target='%s' damage_type=%d runtime_changed=%d jimmy_flag=%d",
+ npc->npcName.c_str(), damageType, runtimeChanged, jimmyFlagChanged);
+ if (!runtimeChanged && !jimmyFlagChanged)
+ return Common::kNoError;
+ if (runtimeChanged && npcEntity) {
+ const Common::String followTargetName = npc->monsterfyTargetName;
+ if (_combatEffects.spawnCombatHitEffect(*npcEntity, followTargetName))
+ needsRedraw = true;
+ }
+ if (runtimeChanged && combatState && npcEntity &&
+ beginNpcDeathTransition(*npc, *npcEntity, *combatState, damageType, needsRedraw))
+ return Common::kNoError;
+ if (runtimeChanged) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat npc defeated target='%s' damage_type=%d on_death='%s' fallback='no death bank'",
+ npc->npcName.c_str(), damageType, npc->onDeathActionTag.c_str());
+ NpcCombatState fallbackState;
+ fallbackState.deathDamageType = damageType;
+ return finalizeNpcDeathTransition(*npc, combatState ? *combatState : fallbackState, needsRedraw);
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error Combat::resolvePlayerMonsterAttackContact(Script &script,
+ int damageType, int damageAmount, bool &needsRedraw) {
+ const RoomPlayerState &playerState = _player.getState();
+ Entity *monsterEntity = nullptr;
+ MonsterRecord *monster = selectMonsterAttackContactTarget(monsterEntity);
+ if (!monster || !monsterEntity) {
+ if (playerState.entity->didAnimationAdvanceLastTick()) {
+ debugC(3, kDebugCombat,
+ "Harvester: combat player attack missed class='monster' target='%s' frame=%d reason='no reachable monster'",
+ playerState.attackTargetName.c_str(), playerState.entity->getCurrentFrame());
+ }
+ return Common::kNoError;
+ }
+
+ _player.markAttackContactResolved();
+ const int hitPointsBefore = monster->currentHitPoints;
+ monster->currentHitPoints = MAX(0,
+ monster->currentHitPoints - damageAmount);
+ const int damageLanded = hitPointsBefore - monster->currentHitPoints;
+ const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
+ MonsterCombatState *combatState =
+ monsterIndex >= 0 && monsterIndex < static_cast<int>(_monsterCombatStates.size())
+ ? &_monsterCombatStates[static_cast<uint>(monsterIndex)]
+ : nullptr;
+ debugC(1, kDebugCombat,
+ "Harvester: combat player monster hit target='%s' loadout=%d weapon='%s' damage=%d damage_type='%s' hp=%d->%d reachable=1",
+ monster->monsterName.c_str(), playerState.combatLoadout,
+ Player::describeCombatLoadout(playerState.combatLoadout), damageAmount,
+ Player::describeCombatDamageType(damageType), hitPointsBefore, monster->currentHitPoints);
+ if (damageLanded > 0) {
+ _combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
+ startMonsterHitReaction(*monster, *monsterEntity, combatState, playerState.attackFirstFrame, needsRedraw);
+ }
+ if (monster->currentHitPoints > 0) {
+ (void)script.syncRuntimeMonsterRecord(*monster);
+ needsRedraw = true;
+ return Common::kNoError;
+ }
+ if (combatState)
+ clearRoomMonsterCombatState(*combatState);
+ (void)script.syncRuntimeMonsterRecord(*monster);
+
+ DeathAnimationRange deathRange;
+ if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
+ damageType, _engine.isGoreEnabled(), deathRange)) {
+ startMonsterDeathAnimation(*monster, *monsterEntity, *combatState, deathRange, damageType);
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster death start target='%s' damage_type=%d frames=%d..%d gore=%d on_death='%s'",
+ monster->monsterName.c_str(), damageType,
+ deathRange.firstFrame, deathRange.lastFrame,
+ _engine.isGoreEnabled(), monster->onDeathActionTag.c_str());
+ needsRedraw = true;
+ return Common::kNoError;
+ }
+
+ monster->active = false;
+ monster->visible = false;
+ const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(*monster);
+ debugC(1, kDebugCombat,
+ "Harvester: combat monster defeated target='%s' on_death='%s' fallback='no death bank'",
+ monster->monsterName.c_str(), monster->onDeathActionTag.c_str());
+ InteractionResult interaction;
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ if (!monster->onDeathActionTag.empty()) {
+ InteractionResult deathInteraction;
+ if (script.executeActionTag(
+ monster->onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(*monster))) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+ if (herrillDeathQueued) {
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+
+ return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
+Common::Error Combat::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
+ const RoomPlayerState &playerState = _player.getState();
+ if (!_engine.isCombatDebugEnabled())
+ return Common::kNoError;
+ MonsterRecord *monster = resolveKillTarget(mousePos);
+ Entity *monsterEntity = monster ? findSceneRuntimeEntity(_entityManager, monster->monsterName) : nullptr;
+ if (!monster || !monsterEntity || !monsterEntity->isVisible()) {
+ debugC(1, kDebugCombat,
+ "Harvester: debug combat kill skipped reason='no active monster' cursor=(%d,%d)",
+ mousePos.x, mousePos.y);
+ return Common::kNoError;
+ }
+
+ Script *script = _engine.getScript();
+ if (!script) {
+ debugC(1, kDebugCombat,
+ "Harvester: debug combat kill skipped target='%s' reason='no startup script'",
+ monster->monsterName.c_str());
+ return Common::kNoError;
+ }
+
+ const int debugDamageType = Player::resolveCombatLoadoutDamageType(playerState.combatLoadout);
+ const int damageLanded = monster->currentHitPoints;
+ monster->currentHitPoints = 0;
+ if (damageLanded > 0)
+ _combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
+
+ if (playerState.attackTargetClassId == kEntityClassMonster &&
+ playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
+ _player.clearAttackTarget();
+ }
+
+ debugC(1, kDebugCombat,
+ "Harvester: debug combat kill target='%s' damage=%d damage_type=%d cursor=(%d,%d)",
+ monster->monsterName.c_str(), damageLanded, debugDamageType, mousePos.x, mousePos.y);
+
+ const int monsterIndex = findRoomMonsterIndexByName(_scene.state, monster->monsterName);
+ MonsterCombatState *combatState =
+ monsterIndex >= 0 && monsterIndex < static_cast<int>(_monsterCombatStates.size())
+ ? &_monsterCombatStates[static_cast<uint>(monsterIndex)]
+ : nullptr;
+ if (combatState)
+ clearRoomMonsterCombatState(*combatState);
+ (void)script->syncRuntimeMonsterRecord(*monster);
+
+ DeathAnimationRange deathRange;
+ if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
+ debugDamageType, _engine.isGoreEnabled(), deathRange)) {
+ startMonsterDeathAnimation(*monster, *monsterEntity, *combatState, deathRange, debugDamageType);
+ needsRedraw = true;
+ return Common::kNoError;
+ }
+
+ monster->active = false;
+ monster->visible = false;
+ InteractionResult interaction;
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ if (!monster->onDeathActionTag.empty()) {
+ InteractionResult deathInteraction;
+ if (script->executeActionTag(
+ monster->onDeathActionTag, deathInteraction, true, ScriptExecutionContext::forMonsterDeath(*monster))) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+
+ return _interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index a9a24c3dacb..8e1c23ccab5 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -24,6 +24,7 @@ MODULE_OBJS = \
player.o \
resources.o \
combat.o \
+ combat_contact.o \
combat_geometry.o \
combat_monsters.o \
combat_reactions.o \
Commit: 00f6720ad46cf410107066c64e2a1fe3e98fe1dd
https://github.com/scummvm/scummvm/commit/00f6720ad46cf410107066c64e2a1fe3e98fe1dd
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:02-04:00
Commit Message:
HARVESTER: Separate combat effects and HUD text
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/combat_effects.cpp
A engines/harvester/combat_hud.cpp
engines/harvester/combat.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index 8b7e3ba8013..efe589c4955 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -21,149 +21,17 @@
#include "harvester/combat.h"
+#include "common/debug.h"
#include "common/util.h"
-#include "graphics/font.h"
-#include "graphics/fontman.h"
-#include "graphics/screen.h"
-#include "harvester/cheats.h"
#include "harvester/detection.h"
-#include "harvester/harvester.h"
-#include "harvester/player.h"
-#include "harvester/interaction.h"
-#include "harvester/scene_entities.h"
#include "harvester/entity.h"
+#include "harvester/scene_entities.h"
namespace Harvester {
const float kNativeNpcMonsterZExtent = 5.0f;
const int kRoomNpcAmbientLastFrame = 0x3b;
const uint32 kNativeKeyboardAttackRepeatTicks = 25;
-const char *const kNativeHitEffectResourcePath = "blood.abm";
-const int kNativeHitEffectAnimationRate = 10;
-const float kNativeHitEffectRenderZBias = 0.01f;
-const uint32 kCombatDebugDamagePopupDurationMs = 750;
-const int kCombatDebugDamagePopupRisePixels = 18;
-const int kCombatDebugDamagePopupVerticalGap = 6;
-
-namespace {
-
-static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
- if (!palette)
- return 0;
-
- byte bestIndex = 0;
- uint32 bestDistance = 0xffffffffu;
- for (int i = 0; i < 256; ++i) {
- const int paletteOffset = i * 3;
- const int dr = static_cast<int>(palette[paletteOffset]) - red;
- const int dg = static_cast<int>(palette[paletteOffset + 1]) - green;
- const int db = static_cast<int>(palette[paletteOffset + 2]) - blue;
- const uint32 distance = static_cast<uint32>(dr * dr + dg * dg + db * db);
- if (distance < bestDistance) {
- bestDistance = distance;
- bestIndex = (byte)i;
- }
- }
-
- return bestIndex;
-}
-
-static void drawShadowedRoomText(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::String &text, int x, int y, byte textColor, byte shadowColor) {
- if (text.empty())
- return;
-
- const int width = font.getStringWidth(text);
- font.drawString(&screen, text, x + 1, y + 1, width, shadowColor);
- font.drawString(&screen, text, x, y, width, textColor);
-}
-
-
-Common::String resolveCombatLoadoutStatusLabel(Script &script, const ObjectRecord &object) {
- Common::String label = script.resolveInventoryTooltipText(object);
- if (!label.empty()) {
- for (uint i = 0; i < label.size(); ++i) {
- if (label[i] == '_')
- label.setChar(' ', i);
- }
-
- uint trimAt = label.size();
- for (uint i = 0; i < label.size(); ++i) {
- if (label[i] == ',' || label[i] == ';') {
- trimAt = i;
- break;
- }
- }
- label = label.substr(0, trimAt);
- while (!label.empty() && label.lastChar() == ' ')
- label.deleteLastChar();
- if (!label.empty())
- return label;
- }
-
- label = script.resolveObjectLabel(object);
- if (!label.empty())
- return label;
-
- label = object.objectName;
- for (uint i = 0; i < label.size(); ++i) {
- if (label[i] == '_')
- label.setChar(' ', i);
- }
-
- return label;
-}
-
-} // End of anonymous namespace
-
-bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info) {
- switch (loadout) {
- case 2:
- info.iconIndex = 2;
- info.maxVisibleCount = 16;
- info.unitLabel = "nails";
- return true;
- case 3:
- info.iconIndex = 1;
- info.maxVisibleCount = 16;
- info.unitLabel = "shells";
- return true;
- case 4:
- info.iconIndex = 0;
- info.maxVisibleCount = 8;
- info.unitLabel = "bullets";
- return true;
- case 5:
- info.iconIndex = 0;
- info.maxVisibleCount = 6;
- info.unitLabel = "bullets";
- return true;
- case 14:
- info.iconIndex = 3;
- info.maxVisibleCount = 16;
- info.unitLabel = "gas";
- return true;
- default:
- return false;
- }
-}
-
-Common::String buildCombatLoadoutStatusMessage(Script &script, const ObjectRecord &object,
- int previousLoadout, int currentLoadout) {
- const Common::String label = resolveCombatLoadoutStatusLabel(script, object);
- if (label.empty())
- return Common::String();
-
- if (currentLoadout == 0 && previousLoadout != 0)
- return Common::String::format("Disarming %s...", label.c_str());
-
- CombatLoadoutHudInfo info;
- if (resolveCombatLoadoutHudInfo(currentLoadout, info))
- return Common::String::format("Arming %s, %d %s...", label.c_str(),
- script.getPlayerCombatResourceCount(currentLoadout), info.unitLabel);
-
- return Common::String::format("Arming %s...", label.c_str());
-}
const char *describeCombatTargetClass(int classId) {
switch (classId) {
@@ -262,177 +130,4 @@ void clearRoomMonsterHitState(MonsterCombatState &combatState) {
combatState.hitSoundPlayed = false;
}
-CombatEffects::CombatEffects(EntityManager *entityManager) : _entityManager(entityManager) {
- debugC(1, kDebugCombat, "Harvester: created room combat effects entities=%d", _entityManager != nullptr);
-}
-
-void CombatEffects::clearHitEffects() {
- debugC(2, kDebugCombat, "Harvester: resetting room blood effects records=%u next_id=%u",
- static_cast<uint>(_hitEffectStates.size()), _nextCombatEffectId);
- _hitEffectStates.clear();
- _nextCombatEffectId = 0;
-}
-
-void CombatEffects::resolveHitEffectAnchor(const Entity &targetEntity, Common::Point &anchorPoint, float &renderZ) {
- const Common::Rect targetRect = targetEntity.getScreenRect();
- anchorPoint.x = targetRect.left + targetRect.width() / 2;
- anchorPoint.y = targetRect.top + targetRect.height() / 3;
- renderZ = targetEntity.getZ() - kNativeHitEffectRenderZBias;
-}
-
-void CombatEffects::resolveCombatDamagePopupAnchor(const Entity &targetEntity, Common::Point &anchorPoint) {
- const Common::Rect targetRect = targetEntity.getScreenRect();
- anchorPoint.x = targetRect.left + targetRect.width() / 2;
- anchorPoint.y = targetRect.top - kCombatDebugDamagePopupVerticalGap;
-}
-
-bool CombatEffects::spawnCombatHitEffect(Entity &sourceEntity, const Common::String &followTargetName) {
- if (!_entityManager)
- return false;
-
- HitEffectState effectState;
- effectState.entityName = Common::String::format("__combat_blood_%u", _nextCombatEffectId++);
- effectState.followTargetName = followTargetName;
- resolveHitEffectAnchor(sourceEntity, effectState.anchorPoint, effectState.renderZ);
-
- Entity *effectEntity = _entityManager->spawnSceneAnimationEntity(
- effectState.entityName, kNativeHitEffectResourcePath, effectState.anchorPoint,
- effectState.renderZ, kNativeHitEffectAnimationRate, true, true, false,
- false, false, 0);
- if (!effectEntity) {
- debugC(1, kDebugCombat, "Harvester: unable to spawn blood effect entity='%s' target='%s'",
- effectState.entityName.c_str(), followTargetName.c_str());
- return false;
- }
-
- _hitEffectStates.push_back(Common::move(effectState));
- return true;
-}
-
-void CombatEffects::spawnCombatDamagePopup(Entity &targetEntity, const Common::String &followTargetName, int damageAmount) {
- if (damageAmount <= 0)
- return;
-
- CombatDamagePopupState popupState;
- popupState.followTargetName = followTargetName;
- popupState.startTick = Player::getRuntimeClockTicks();
- popupState.damageAmount = damageAmount;
- resolveCombatDamagePopupAnchor(targetEntity, popupState.anchorPoint);
- _damagePopupStates.push_back(Common::move(popupState));
-}
-
-bool CombatEffects::syncCombatHitEffects() {
- if (!_entityManager)
- return false;
-
- bool changed = false;
- for (HitEffectState &effectState : _hitEffectStates) {
- Entity *effectEntity = _entityManager->findSceneEntityByName(effectState.entityName);
- if (!effectEntity)
- continue;
-
- if (!effectState.followTargetName.empty()) {
- Entity *targetEntity =
- _entityManager->findSceneEntityByName(effectState.followTargetName);
- if (targetEntity && targetEntity->isVisible()) {
- resolveHitEffectAnchor(*targetEntity, effectState.anchorPoint, effectState.renderZ);
- }
- }
-
- if (effectEntity->getX() != effectState.anchorPoint.x ||
- effectEntity->getY() != effectState.anchorPoint.y ||
- fabsf(effectEntity->getZ() - effectState.renderZ) > 0.0001f) {
- effectEntity->setPosition(
- effectState.anchorPoint.x, effectState.anchorPoint.y, effectState.renderZ);
- _entityManager->reinsertSceneEntity(effectEntity);
- changed = true;
- }
- }
-
- return changed;
-}
-
-void CombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool combatDebugEnabled) {
- if (!combatDebugEnabled || _damagePopupStates.empty())
- return;
-
- const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- if (!font)
- return;
-
- byte displayPalette[256 * 3];
- screen.getPalette(displayPalette);
- const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
- const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
- const uint32 now = Player::getRuntimeClockTicks();
- for (CombatDamagePopupState &popupState : _damagePopupStates) {
- if (_entityManager && !popupState.followTargetName.empty()) {
- Entity *targetEntity =
- _entityManager->findSceneEntityByName(popupState.followTargetName);
- if (targetEntity && targetEntity->isVisible())
- resolveCombatDamagePopupAnchor(*targetEntity, popupState.anchorPoint);
- }
-
- const uint32 elapsed = now - popupState.startTick;
- const uint32 clampedElapsed = MIN<uint32>(elapsed, kCombatDebugDamagePopupDurationMs);
- const int rise = static_cast<int>(
- clampedElapsed * static_cast<uint32>(kCombatDebugDamagePopupRisePixels) /
- kCombatDebugDamagePopupDurationMs);
- const Common::String popupText = Common::String::format("+%d", popupState.damageAmount);
- const int textWidth = font->getStringWidth(popupText);
- const int drawX = CLIP<int>(popupState.anchorPoint.x - textWidth / 2,
- 0, MAX(0, screen.w - textWidth));
- const int drawY = CLIP<int>(
- popupState.anchorPoint.y - font->getFontHeight() - rise,
- 0, MAX(0, screen.h - font->getFontHeight()));
- drawShadowedRoomText(screen, *font, popupText, drawX, drawY, white, black);
- }
-}
-
-bool CombatEffects::pruneCombatHitEffects() {
- bool removedAny = false;
- for (uint i = 0; i < _hitEffectStates.size();) {
- const Common::String effectName = _hitEffectStates[i].entityName;
- Entity *effectEntity = _entityManager
- ? _entityManager->findSceneEntityByName(effectName)
- : nullptr;
- if (!effectEntity) {
- _hitEffectStates.remove_at(i);
- continue;
- }
-
- const bool finished =
- !effectEntity->isAnimationEnabled() &&
- effectEntity->getCurrentFrame() == effectEntity->getLastFrame();
- if (finished && _hitEffectStates[i].finished) {
- Entity *removedEntity = _entityManager->detachSceneEntityByName(effectName);
- delete removedEntity;
- _hitEffectStates.remove_at(i);
- removedAny = true;
- continue;
- }
-
- _hitEffectStates[i].finished = finished;
- ++i;
- }
-
- return removedAny;
-}
-
-bool CombatEffects::pruneCombatDamagePopups() {
- const uint32 now = Player::getRuntimeClockTicks();
- bool removedAny = false;
- for (uint i = 0; i < _damagePopupStates.size();) {
- if (now - _damagePopupStates[i].startTick < kCombatDebugDamagePopupDurationMs) {
- ++i;
- continue;
- }
-
- _damagePopupStates.remove_at(i);
- removedAny = true;
- }
-
- return removedAny;
-}
-
} // End of namespace Harvester
diff --git a/engines/harvester/combat_effects.cpp b/engines/harvester/combat_effects.cpp
new file mode 100644
index 00000000000..7ba3eb0f699
--- /dev/null
+++ b/engines/harvester/combat_effects.cpp
@@ -0,0 +1,250 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/screen.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/player.h"
+
+namespace Harvester {
+
+const char *const kNativeHitEffectResourcePath = "blood.abm";
+const int kNativeHitEffectAnimationRate = 10;
+const float kNativeHitEffectRenderZBias = 0.01f;
+const uint32 kCombatDebugDamagePopupDurationMs = 750;
+const int kCombatDebugDamagePopupRisePixels = 18;
+const int kCombatDebugDamagePopupVerticalGap = 6;
+
+namespace {
+
+static byte findNearestPaletteColor(const byte *palette, byte red, byte green, byte blue) {
+ if (!palette)
+ return 0;
+
+ byte bestIndex = 0;
+ uint32 bestDistance = 0xffffffffu;
+ for (int i = 0; i < 256; ++i) {
+ const int paletteOffset = i * 3;
+ const int dr = static_cast<int>(palette[paletteOffset]) - red;
+ const int dg = static_cast<int>(palette[paletteOffset + 1]) - green;
+ const int db = static_cast<int>(palette[paletteOffset + 2]) - blue;
+ const uint32 distance = static_cast<uint32>(dr * dr + dg * dg + db * db);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestIndex = (byte)i;
+ }
+ }
+
+ return bestIndex;
+}
+
+static void drawShadowedRoomText(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &text, const Common::Point32 &position, byte textColor, byte shadowColor) {
+ if (text.empty())
+ return;
+
+ const int width = font.getStringWidth(text);
+ font.drawString(&screen, text, position.x + 1, position.y + 1, width, shadowColor);
+ font.drawString(&screen, text, position.x, position.y, width, textColor);
+}
+
+} // End of anonymous namespace
+
+CombatEffects::CombatEffects(EntityManager *entityManager) : _entityManager(entityManager) {
+ debugC(1, kDebugCombat, "Harvester: created room combat effects entities=%d", _entityManager != nullptr);
+}
+
+void CombatEffects::clearHitEffects() {
+ debugC(2, kDebugCombat, "Harvester: resetting room blood effects records=%u next_id=%u",
+ static_cast<uint>(_hitEffectStates.size()), _nextCombatEffectId);
+ _hitEffectStates.clear();
+ _nextCombatEffectId = 0;
+}
+
+void CombatEffects::resolveHitEffectAnchor(const Entity &targetEntity, Common::Point &anchorPoint, float &renderZ) {
+ const Common::Rect targetRect = targetEntity.getScreenRect();
+ anchorPoint.x = targetRect.left + targetRect.width() / 2;
+ anchorPoint.y = targetRect.top + targetRect.height() / 3;
+ renderZ = targetEntity.getZ() - kNativeHitEffectRenderZBias;
+}
+
+void CombatEffects::resolveCombatDamagePopupAnchor(const Entity &targetEntity, Common::Point &anchorPoint) {
+ const Common::Rect targetRect = targetEntity.getScreenRect();
+ anchorPoint.x = targetRect.left + targetRect.width() / 2;
+ anchorPoint.y = targetRect.top - kCombatDebugDamagePopupVerticalGap;
+}
+
+bool CombatEffects::spawnCombatHitEffect(Entity &sourceEntity, const Common::String &followTargetName) {
+ if (!_entityManager)
+ return false;
+
+ HitEffectState effectState;
+ effectState.entityName = Common::String::format("__combat_blood_%u", _nextCombatEffectId++);
+ effectState.followTargetName = followTargetName;
+ resolveHitEffectAnchor(sourceEntity, effectState.anchorPoint, effectState.renderZ);
+
+ Entity *effectEntity = _entityManager->spawnSceneAnimationEntity(
+ effectState.entityName, kNativeHitEffectResourcePath, effectState.anchorPoint,
+ effectState.renderZ, kNativeHitEffectAnimationRate, true, true, false,
+ false, false, 0);
+ if (!effectEntity) {
+ debugC(1, kDebugCombat, "Harvester: unable to spawn blood effect entity='%s' target='%s'",
+ effectState.entityName.c_str(), followTargetName.c_str());
+ return false;
+ }
+
+ _hitEffectStates.push_back(Common::move(effectState));
+ return true;
+}
+
+void CombatEffects::spawnCombatDamagePopup(Entity &targetEntity, const Common::String &followTargetName, int damageAmount) {
+ if (damageAmount <= 0)
+ return;
+
+ CombatDamagePopupState popupState;
+ popupState.followTargetName = followTargetName;
+ popupState.startTick = Player::getRuntimeClockTicks();
+ popupState.damageAmount = damageAmount;
+ resolveCombatDamagePopupAnchor(targetEntity, popupState.anchorPoint);
+ _damagePopupStates.push_back(Common::move(popupState));
+}
+
+bool CombatEffects::syncCombatHitEffects() {
+ if (!_entityManager)
+ return false;
+
+ bool changed = false;
+ for (HitEffectState &effectState : _hitEffectStates) {
+ Entity *effectEntity = _entityManager->findSceneEntityByName(effectState.entityName);
+ if (!effectEntity)
+ continue;
+
+ if (!effectState.followTargetName.empty()) {
+ Entity *targetEntity =
+ _entityManager->findSceneEntityByName(effectState.followTargetName);
+ if (targetEntity && targetEntity->isVisible()) {
+ resolveHitEffectAnchor(*targetEntity, effectState.anchorPoint, effectState.renderZ);
+ }
+ }
+
+ if (effectEntity->getX() != effectState.anchorPoint.x ||
+ effectEntity->getY() != effectState.anchorPoint.y ||
+ fabsf(effectEntity->getZ() - effectState.renderZ) > 0.0001f) {
+ effectEntity->setPosition(
+ effectState.anchorPoint.x, effectState.anchorPoint.y, effectState.renderZ);
+ _entityManager->reinsertSceneEntity(effectEntity);
+ changed = true;
+ }
+ }
+
+ return changed;
+}
+
+void CombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool combatDebugEnabled) {
+ if (!combatDebugEnabled || _damagePopupStates.empty())
+ return;
+
+ const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ if (!font)
+ return;
+
+ byte displayPalette[256 * 3];
+ screen.getPalette(displayPalette);
+ const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
+ const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
+ const uint32 now = Player::getRuntimeClockTicks();
+ for (CombatDamagePopupState &popupState : _damagePopupStates) {
+ if (_entityManager && !popupState.followTargetName.empty()) {
+ Entity *targetEntity =
+ _entityManager->findSceneEntityByName(popupState.followTargetName);
+ if (targetEntity && targetEntity->isVisible())
+ resolveCombatDamagePopupAnchor(*targetEntity, popupState.anchorPoint);
+ }
+
+ const uint32 elapsed = now - popupState.startTick;
+ const uint32 clampedElapsed = MIN<uint32>(elapsed, kCombatDebugDamagePopupDurationMs);
+ const int rise = static_cast<int>(
+ clampedElapsed * static_cast<uint32>(kCombatDebugDamagePopupRisePixels) /
+ kCombatDebugDamagePopupDurationMs);
+ const Common::String popupText = Common::String::format("+%d", popupState.damageAmount);
+ const int textWidth = font->getStringWidth(popupText);
+ const int drawX = CLIP<int>(popupState.anchorPoint.x - textWidth / 2,
+ 0, MAX(0, screen.w - textWidth));
+ const int drawY = CLIP<int>(
+ popupState.anchorPoint.y - font->getFontHeight() - rise,
+ 0, MAX(0, screen.h - font->getFontHeight()));
+ drawShadowedRoomText(screen, *font, popupText, Common::Point32(drawX, drawY), white, black);
+ }
+}
+
+bool CombatEffects::pruneCombatHitEffects() {
+ bool removedAny = false;
+ for (uint i = 0; i < _hitEffectStates.size();) {
+ const Common::String effectName = _hitEffectStates[i].entityName;
+ Entity *effectEntity = _entityManager
+ ? _entityManager->findSceneEntityByName(effectName)
+ : nullptr;
+ if (!effectEntity) {
+ _hitEffectStates.remove_at(i);
+ continue;
+ }
+
+ const bool finished =
+ !effectEntity->isAnimationEnabled() &&
+ effectEntity->getCurrentFrame() == effectEntity->getLastFrame();
+ if (finished && _hitEffectStates[i].finished) {
+ Entity *removedEntity = _entityManager->detachSceneEntityByName(effectName);
+ delete removedEntity;
+ _hitEffectStates.remove_at(i);
+ removedAny = true;
+ continue;
+ }
+
+ _hitEffectStates[i].finished = finished;
+ ++i;
+ }
+
+ return removedAny;
+}
+
+bool CombatEffects::pruneCombatDamagePopups() {
+ const uint32 now = Player::getRuntimeClockTicks();
+ bool removedAny = false;
+ for (uint i = 0; i < _damagePopupStates.size();) {
+ if (now - _damagePopupStates[i].startTick < kCombatDebugDamagePopupDurationMs) {
+ ++i;
+ continue;
+ }
+
+ _damagePopupStates.remove_at(i);
+ removedAny = true;
+ }
+
+ return removedAny;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/combat_hud.cpp b/engines/harvester/combat_hud.cpp
new file mode 100644
index 00000000000..a1e4c63ea6d
--- /dev/null
+++ b/engines/harvester/combat_hud.cpp
@@ -0,0 +1,116 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/combat.h"
+
+#include "harvester/script.h"
+
+namespace Harvester {
+
+namespace {
+
+Common::String resolveCombatLoadoutStatusLabel(Script &script, const ObjectRecord &object) {
+ Common::String label = script.resolveInventoryTooltipText(object);
+ if (!label.empty()) {
+ for (uint i = 0; i < label.size(); ++i) {
+ if (label[i] == '_')
+ label.setChar(' ', i);
+ }
+
+ uint trimAt = label.size();
+ for (uint i = 0; i < label.size(); ++i) {
+ if (label[i] == ',' || label[i] == ';') {
+ trimAt = i;
+ break;
+ }
+ }
+ label = label.substr(0, trimAt);
+ while (!label.empty() && label.lastChar() == ' ')
+ label.deleteLastChar();
+ if (!label.empty())
+ return label;
+ }
+
+ label = script.resolveObjectLabel(object);
+ if (!label.empty())
+ return label;
+
+ label = object.objectName;
+ for (uint i = 0; i < label.size(); ++i) {
+ if (label[i] == '_')
+ label.setChar(' ', i);
+ }
+
+ return label;
+}
+
+} // End of anonymous namespace
+
+bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info) {
+ switch (loadout) {
+ case 2:
+ info.iconIndex = 2;
+ info.maxVisibleCount = 16;
+ info.unitLabel = "nails";
+ return true;
+ case 3:
+ info.iconIndex = 1;
+ info.maxVisibleCount = 16;
+ info.unitLabel = "shells";
+ return true;
+ case 4:
+ info.iconIndex = 0;
+ info.maxVisibleCount = 8;
+ info.unitLabel = "bullets";
+ return true;
+ case 5:
+ info.iconIndex = 0;
+ info.maxVisibleCount = 6;
+ info.unitLabel = "bullets";
+ return true;
+ case 14:
+ info.iconIndex = 3;
+ info.maxVisibleCount = 16;
+ info.unitLabel = "gas";
+ return true;
+ default:
+ return false;
+ }
+}
+
+Common::String buildCombatLoadoutStatusMessage(Script &script, const ObjectRecord &object,
+ int previousLoadout, int currentLoadout) {
+ const Common::String label = resolveCombatLoadoutStatusLabel(script, object);
+ if (label.empty())
+ return Common::String();
+
+ if (currentLoadout == 0 && previousLoadout != 0)
+ return Common::String::format("Disarming %s...", label.c_str());
+
+ CombatLoadoutHudInfo info;
+ if (resolveCombatLoadoutHudInfo(currentLoadout, info))
+ return Common::String::format("Arming %s, %d %s...", label.c_str(),
+ script.getPlayerCombatResourceCount(currentLoadout), info.unitLabel);
+
+ return Common::String::format("Arming %s...", label.c_str());
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 8e1c23ccab5..39bf20e2166 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -25,7 +25,9 @@ MODULE_OBJS = \
resources.o \
combat.o \
combat_contact.o \
+ combat_effects.o \
combat_geometry.o \
+ combat_hud.o \
combat_monsters.o \
combat_reactions.o \
combat_targeting.o \
Commit: a56f1982e47baff9b3705b95c831a9783d6b6af3
https://github.com/scummvm/scummvm/commit/a56f1982e47baff9b3705b95c831a9783d6b6af3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate player movement and collision helpers
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/player_movement.cpp
A engines/harvester/player_movement.h
engines/harvester/module.mk
engines/harvester/player.cpp
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 39bf20e2166..048ee050d48 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -22,6 +22,7 @@ MODULE_OBJS = \
metaengine.o \
monster.o \
player.o \
+ player_movement.o \
resources.o \
combat.o \
combat_contact.o \
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 1ec1826d225..c96dee572f0 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -20,6 +20,7 @@
*/
#include "harvester/player.h"
+#include "harvester/player_movement.h"
#include "harvester/combat.h"
#include "common/debug.h"
@@ -31,24 +32,21 @@
namespace Harvester {
+using namespace PlayerMovement;
+
namespace {
-static void syncPlayerAnimationPlacement(RoomPlayerState &playerState);
static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing);
static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState);
static const char *const kPlayerActorResourcePath = "1:/GRAPHIC/MONSTERS/PC/PC0.ABM";
-static const float kRoomPlayerHorizontalMoveBase = 8.0f;
static const float kRoomPlayerHorizontalTargetSlackBase = 50.0f;
static const float kRoomPlayerDepthTargetSlack = 8.0f;
static const int kRoomRegionTargetXBias = 10;
-static const float kRoomDepthCompareEpsilon = 0.01f;
static const int kRoomPlayerWalkAnimationRate = 17;
static const int kRoomPlayerFastAnimationRate = 30;
static const int kRoomPlayerHitAnimationRate = 4;
static const int kNativePlayerDeathEntryAnimationRate = 14;
-static const int kRoomPlayerMinOpaqueLeftX = 4;
-static const int kRoomPlayerMaxOpaqueRightX = 0x27c;
static const float kRoomPlayerAttackUpperYOffset = 144.44f;
static const float kRoomPlayerAttackMidYOffset = 75.36f;
static const char *const kPlayerIdleAnimationEntityName = "IDLE_ANIM";
@@ -62,16 +60,6 @@ static const int kRoomPlayerIdleLoopLastFrame = 0xb2;
static const int kRoomPlayerIdleExitLastFrame = 0xbf;
static const int kRoomPlayerIdleYOffset = 4;
-struct PlayerAnimationRange {
- PlayerAnimationRange() {}
- PlayerAnimationRange(int walkFirstFrame, int walkLastFrame, int idleFrame)
- : walkFirstFrame(walkFirstFrame), walkLastFrame(walkLastFrame), idleFrame(idleFrame) {}
-
- int walkFirstFrame = 0;
- int walkLastFrame = 0;
- int idleFrame = 0;
-};
-
struct PlayerTurnAnimationRange {
PlayerTurnAnimationRange() {}
PlayerTurnAnimationRange(int firstFrame, int lastFrame, bool playBackwards)
@@ -179,25 +167,6 @@ static const PlayerCombatLoadoutTuning kPlayerCombatLoadoutTunings[] = {
{ "POOLSTICK", 1, 2, 2 }
};
-static int roundToInt(float value) {
- return value >= 0.0f ? (int)floorf(value + 0.5f) : (int)ceilf(value - 0.5f);
-}
-
-static PlayerAnimationRange resolvePlayerAnimationRange(int facing) {
- switch (facing) {
- case 0:
- return PlayerAnimationRange{ 0x00, 0x09, 0x3b };
- case 1:
- return PlayerAnimationRange{ 0x0f, 0x18, 0x0e };
- case 2:
- return PlayerAnimationRange{ 0x2d, 0x36, 0x2c };
- case 3:
- return PlayerAnimationRange{ 0x1e, 0x27, 0x28 };
- default:
- return PlayerAnimationRange{ 0x00, 0x09, 0x3b };
- }
-}
-
static bool resolvePlayerTurnAnimationRange(int fromFacing, int toFacing, PlayerTurnAnimationRange &range) {
// Native desired-state transitions play these banks forward or backward depending on the source facing.
if (fromFacing == 0 && toFacing == 1) {
@@ -508,72 +477,6 @@ static bool startResolvedAttackAnimation(RoomPlayerState &playerState,
return true;
}
-static int clampRoomMovementY(const RoomSetupState &state, int screenY) {
- if (!Player::supportsMovementBand(state))
- return screenY;
-
- return CLIP<int>(screenY, state.roomMaxZScreenY, state.roomMinZScreenY);
-}
-
-static float mapRoomScreenYToDepth(const RoomSetupState &state, int screenY) {
- if (!Player::supportsMovementBand(state))
- return (float)state.playerSpawnZ;
-
- if (screenY <= state.roomMaxZScreenY)
- return (float)state.roomMaxZ;
- if (screenY >= state.roomMinZScreenY)
- return (float)state.roomMinZ;
-
- return (float)(state.roomMaxZ - ((state.roomMaxZ - state.roomMinZ) *
- (screenY - state.roomMaxZScreenY)) /
- (state.roomMinZScreenY - state.roomMaxZScreenY));
-}
-
-static float clampRoomDepth(const RoomSetupState &state, float z) {
- if (!Player::supportsMovementBand(state))
- return z;
-
- return CLIP<float>(z,
- (float)MIN(state.roomMinZ, state.roomMaxZ),
- (float)MAX(state.roomMinZ, state.roomMaxZ));
-}
-
-static int mapRoomDepthToScreenY(const RoomSetupState &state, float z) {
- if (!Player::supportsMovementBand(state))
- return state.playerSpawnY;
-
- if (state.roomMaxZ == state.roomMinZ)
- return clampRoomMovementY(state, state.playerSpawnY);
-
- const float clampedZ = clampRoomDepth(state, z);
- const float offset = ((float)state.roomMaxZ - clampedZ) *
- (float)(state.roomMinZScreenY - state.roomMaxZScreenY) /
- (float)(state.roomMaxZ - state.roomMinZ);
- return clampRoomMovementY(state, state.roomMaxZScreenY + roundToInt(offset));
-}
-
-static int computeRoomPlayerHorizontalStep(const RoomSetupState &state, float z) {
- return MAX<int>(1, (int)(Player::computeDepthScale(state, z) * kRoomPlayerHorizontalMoveBase));
-}
-
-static float computeRoomPlayerDepthStep(const RoomSetupState &state) {
- return state.roomZVelocityStep > 0.0f ? state.roomZVelocityStep : 1.0f;
-}
-
-static int computePlayerFrameLeftX(const RoomPlayerState &playerState) {
- return playerState.entity ? playerState.entity->getScreenRect().left : playerState.centerX;
-}
-
-static float computePlayerRenderZ(const RoomPlayerState &playerState) {
- return playerState.entity ? playerState.entity->getZ() : playerState.z;
-}
-
-static float clampPlayerMovementDepth(const RoomSetupState &state,
- const RoomPlayerState &playerState, float z) {
- const float placementOffset = playerState.z - computePlayerRenderZ(playerState);
- return clampRoomDepth(state, z - placementOffset) + placementOffset;
-}
-
static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
playerState.moveTargetXReached = false;
playerState.moveTargetZReached = false;
@@ -618,200 +521,6 @@ static void updatePlayerMoveTargetProgress(const RoomSetupState &state,
}
}
-static int clampPlayerCenterXToNativeBounds(const RoomPlayerState &playerState, int centerX) {
- if (!playerState.entity)
- return CLIP<int>(centerX, 0, 639);
-
- int width = 0;
- int height = 0;
- int xOffset = 0;
- int yOffset = 0;
- if (!playerState.entity->getCurrentFrameMetrics(width, height, xOffset, yOffset))
- return CLIP<int>(centerX, 0, 639);
-
- const int minCenterX = kRoomPlayerMinOpaqueLeftX + width / 2;
- const int maxCenterX = MAX(minCenterX,
- kRoomPlayerMaxOpaqueRightX - (width - width / 2));
- return CLIP<int>(centerX, minCenterX, maxCenterX);
-}
-
-static uint32 computeAnimationTickInterval(int rate) {
- return rate > 0 ? MAX<uint32>(1, 100U / (uint32)rate) : 0;
-}
-
-static bool consumePlayerMovementTick(RoomPlayerState &playerState) {
- if (playerState.entity &&
- playerState.entity->isAnimationEnabled() &&
- playerState.entity->getAnimationRate() != 0) {
- playerState.nextMovementTick = 0;
- return playerState.entity->didAnimationAdvanceLastTick();
- }
-
- const uint32 interval = computeAnimationTickInterval(playerState.animationRate);
- if (interval == 0) {
- playerState.nextMovementTick = 0;
- return true;
- }
-
- const uint32 now = Player::getRuntimeClockTicks();
- if (playerState.nextMovementTick != 0 &&
- (int32)(now - playerState.nextMovementTick) < 0) {
- return false;
- }
-
- playerState.nextMovementTick = now + interval;
- return true;
-}
-
-static bool shouldIgnoreMovementBlocker(const Entity &entity, const Common::String *ignoredBlockerName) {
- return ignoredBlockerName && !ignoredBlockerName->empty() &&
- entity.getName().equalsIgnoreCase(*ignoredBlockerName);
-}
-
-static bool isSameBlockerName(const Common::String &history, const Entity &blocker) {
- return !history.empty() && history.equalsIgnoreCase(blocker.getName());
-}
-
-static void clearPlayerBlockerHistory(RoomPlayerState &playerState) {
- playerState.currentBlockerName.clear();
- playerState.upwardBlockerHistory.clear();
- playerState.downwardBlockerHistory.clear();
- playerState.leftwardBlockerHistory.clear();
- playerState.rightwardBlockerHistory.clear();
-}
-
-static void setPlayerCurrentBlocker(RoomPlayerState &playerState, const Entity *blocker) {
- if (blocker) {
- playerState.currentBlockerName = blocker->getName();
- return;
- }
-
- clearPlayerBlockerHistory(playerState);
-}
-
-static const Entity *findPlayerRoomBlocker(HarvesterEngine &engine,
- const Common::Array<ObjectRecord> &sceneObjects,
- const Common::Array<AnimRecord> &sceneAnimations, const Entity &playerEntity,
- const Common::String *ignoredBlockerName) {
- EntityManager *entityManager = engine.getRuntimeEntities();
- if (!entityManager)
- return nullptr;
-
- for (const ObjectRecord &object : sceneObjects) {
- const Entity *entity = entityManager->findSceneEntityByName(object.objectName);
- if (entity && !shouldIgnoreMovementBlocker(*entity, ignoredBlockerName) &&
- playerEntity.overlapsEntity(*entity)) {
- return entity;
- }
- }
-
- for (const AnimRecord &anim : sceneAnimations) {
- const Entity *entity = entityManager->findSceneEntityByName(anim.animName);
- if (entity && !shouldIgnoreMovementBlocker(*entity, ignoredBlockerName) &&
- playerEntity.overlapsEntity(*entity)) {
- return entity;
- }
- }
-
- return nullptr;
-}
-
-static void refreshPlayerCurrentBlocker(HarvesterEngine &engine,
- const Common::Array<ObjectRecord> &sceneObjects,
- const Common::Array<AnimRecord> &sceneAnimations,
- RoomPlayerState &playerState) {
- if (!playerState.entity) {
- clearPlayerBlockerHistory(playerState);
- return;
- }
-
- setPlayerCurrentBlocker(playerState, findPlayerRoomBlocker(engine, sceneObjects,
- sceneAnimations, *playerState.entity, nullptr));
-}
-
-static void reinsertPlayerSceneEntity(HarvesterEngine &engine, const RoomPlayerState &playerState) {
- if (!playerState.entity)
- return;
-
- EntityManager *entityManager = engine.getRuntimeEntities();
- if (entityManager &&
- entityManager->findSceneEntityByName(playerState.entity->getName()) == playerState.entity) {
- entityManager->reinsertSceneEntity(playerState.entity);
- }
-}
-
-static void applyPlayerMovementPlacement(const RoomSetupState &state, RoomPlayerState &playerState,
- int candidateCenterX, int candidateBottomY, float candidateZ) {
- Entity &entity = *playerState.entity;
- 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,
- RoomPlayerState &playerState, int candidateCenterX, int candidateBottomY, float candidateZ,
- const Common::String *ignoredBlockerName = nullptr, const Entity **blockerOut = nullptr) {
- if (!playerState.entity)
- return true;
-
- if (blockerOut)
- *blockerOut = nullptr;
-
- 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,
- entity, ignoredBlockerName);
- entity.setPosition(oldX, oldY, oldZ);
- entity.setDepthScale(oldScale);
- if (blockerOut)
- *blockerOut = blocker;
- return blocker != nullptr;
-}
-
-static bool tryApplyPlayerMovement(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects,
- const Common::Array<AnimRecord> &sceneAnimations,
- RoomPlayerState &playerState, int candidateCenterX, int candidateBottomY, float candidateZ,
- const Common::String *ignoredBlockerName = nullptr, const Entity **blockerOut = nullptr) {
- if (!playerState.entity)
- return false;
-
- if (blockerOut)
- *blockerOut = nullptr;
-
- candidateCenterX = clampPlayerCenterXToNativeBounds(playerState, candidateCenterX);
- candidateZ = clampRoomDepth(state, candidateZ);
- if (candidateCenterX == playerState.centerX &&
- candidateBottomY == playerState.bottomY &&
- fabsf(candidateZ - playerState.z) <= kRoomDepthCompareEpsilon) {
- return false;
- }
- if (isPlayerMovementBlocked(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, candidateBottomY, candidateZ,
- ignoredBlockerName, blockerOut)) {
- return false;
- }
-
- applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
- playerState.z = candidateZ;
- syncPlayerAnimationPlacement(playerState);
-
- reinsertPlayerSceneEntity(engine, playerState);
- return true;
-}
-
-static bool isPathfindingDebugEnabled() {
- return g_engine && g_engine->isPathfindingDebugEnabled();
-}
-
static bool isHorizontalDirectionBlockedByHistory(const RoomPlayerState &playerState,
int horizontalDirection) {
if (horizontalDirection < 0)
@@ -855,137 +564,6 @@ static int resolveFacingFromMoveDirection(int horizontalDirection, int depthDire
return fallbackFacing >= 0 ? fallbackFacing : 0;
}
-static void logPlayerBlockerHistory(const RoomSetupState &state,
- const RoomPlayerState &playerState, const Entity &blocker,
- const char *direction) {
- if (!isPathfindingDebugEnabled())
- return;
-
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding blocker history room='%s' blocker='%s' direction='%s' histories=(up='%s',down='%s',left='%s',right='%s')",
- state.roomName.c_str(), blocker.getName().c_str(), direction,
- playerState.upwardBlockerHistory.c_str(),
- playerState.downwardBlockerHistory.c_str(),
- playerState.leftwardBlockerHistory.c_str(),
- playerState.rightwardBlockerHistory.c_str());
-}
-
-static bool recordNativePrimaryWalkBlockerHistory(const RoomSetupState &state,
- RoomPlayerState &playerState, const Entity &blocker,
- int horizontalDirection, int depthDirection) {
- if (horizontalDirection < 0) {
- const int frameLeftX = computePlayerFrameLeftX(playerState);
- const Common::Rect blockerRect = blocker.getScreenRect();
- if (isSameBlockerName(playerState.rightwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.upwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.downwardBlockerHistory, blocker) ||
- frameLeftX <= blockerRect.left) {
- return false;
- }
-
- playerState.leftwardBlockerHistory = blocker.getName();
- playerState.moveTargetXReached = true;
- logPlayerBlockerHistory(state, playerState, blocker, "left");
- return true;
- }
-
- if (horizontalDirection > 0) {
- const int frameLeftX = computePlayerFrameLeftX(playerState);
- const Common::Rect blockerRect = blocker.getScreenRect();
- if (isSameBlockerName(playerState.leftwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.upwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.downwardBlockerHistory, blocker) ||
- blockerRect.left <= frameLeftX) {
- return false;
- }
-
- playerState.rightwardBlockerHistory = blocker.getName();
- playerState.moveTargetXReached = true;
- logPlayerBlockerHistory(state, playerState, blocker, "right");
- return true;
- }
-
- if (depthDirection > 0) {
- if (isSameBlockerName(playerState.rightwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.leftwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.downwardBlockerHistory, blocker)) {
- return false;
- }
-
- playerState.upwardBlockerHistory = blocker.getName();
- playerState.moveTargetZReached = true;
- logPlayerBlockerHistory(state, playerState, blocker, "upward");
- return true;
- }
-
- if (depthDirection < 0) {
- if (isSameBlockerName(playerState.rightwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.upwardBlockerHistory, blocker) ||
- isSameBlockerName(playerState.leftwardBlockerHistory, blocker)) {
- return false;
- }
-
- playerState.downwardBlockerHistory = blocker.getName();
- playerState.moveTargetZReached = true;
- logPlayerBlockerHistory(state, playerState, blocker, "downward");
- return true;
- }
-
- return false;
-}
-
-static void playPlayerBlockedWalkSounds(HarvesterEngine &engine, const RoomSetupState &state,
- const RoomPlayerState &playerState, const Entity &blocker) {
- const Entity &entity = *playerState.entity;
- const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
- if (entity.getAnimationRate() != playerState.animationRate ||
- entity.getCurrentFrame() < range.walkFirstFrame ||
- entity.getCurrentFrame() > range.walkLastFrame)
- return;
-
- const int frameLeftX = computePlayerFrameLeftX(playerState);
- if ((playerState.facing == 1 && frameLeftX < 30) ||
- (playerState.facing == 2 && frameLeftX >= 611) ||
- (playerState.facing == 0 && entity.getZ() <= state.roomMinZ) ||
- (playerState.facing == 3 && entity.getZ() >= state.roomMaxZ))
- return;
-
- (void)engine.playSound("2:/SOUND/EFFECTS/FOOTLFT1.WAV");
- (void)engine.playSound("2:/SOUND/EFFECTS/FOOTRHT1.WAV");
- debugC(2, kDebugPlayer, "Harvester: player blocked walk sound blocker='%s' facing=%d",
- blocker.getName().c_str(), playerState.facing);
-}
-
-static bool tryApplyPlayerWalkMovement(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects,
- const Common::Array<AnimRecord> &sceneAnimations, RoomPlayerState &playerState,
- int candidateCenterX, int candidateBottomY, float candidateZ,
- int horizontalDirection, int depthDirection, const Entity **blockerOut = nullptr) {
- const Entity *blocker = findPlayerRoomBlocker(engine, sceneObjects, sceneAnimations,
- *playerState.entity, nullptr);
- setPlayerCurrentBlocker(playerState, blocker);
- if (blockerOut)
- *blockerOut = nullptr;
- if (blocker && recordNativePrimaryWalkBlockerHistory(state, playerState, *blocker,
- horizontalDirection, depthDirection)) {
- playPlayerBlockedWalkSounds(engine, state, playerState, *blocker);
- if (blockerOut)
- *blockerOut = blocker;
- return false;
- }
-
- if (candidateCenterX == playerState.centerX &&
- candidateBottomY == playerState.bottomY &&
- fabsf(candidateZ - playerState.z) <= kRoomDepthCompareEpsilon)
- return false;
-
- applyPlayerMovementPlacement(state, playerState, candidateCenterX, candidateBottomY, candidateZ);
- playerState.z = candidateZ;
- syncPlayerAnimationPlacement(playerState);
- reinsertPlayerSceneEntity(engine, playerState);
- return true;
-}
-
static void setMoveTargetInternal(const RoomSetupState &state, RoomPlayerState &playerState,
int targetX, float targetZ, int targetBottomY) {
playerState.hasMoveTarget = true;
@@ -1099,22 +677,6 @@ static bool finishPlayerIdleAnimation(const RoomSetupState &state, RoomPlayerSta
return true;
}
-static void syncPlayerAnimationPlacement(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;
-}
-
static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState) {
idleState.triggerTick = MAX(idleState.activityTick, idleState.resetTick) + kRoomPlayerIdleDelayTicks;
}
@@ -1442,57 +1004,6 @@ void Player::setMoveTargetFromScreenPoint(const RoomSetupState &state, int targe
mapRoomScreenYToDepth(state, clampedTargetBottomY), clampedTargetBottomY);
}
-bool Player::resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity)
- return false;
-
- if (!isPlayerMovementBlocked(engine, state, sceneObjects, sceneAnimations,
- playerState, playerState.centerX, playerState.bottomY, playerState.z)) {
- return false;
- }
-
- const int originalCenterX = playerState.centerX;
- const int originalBottomY = playerState.bottomY;
- const int preferredDirection = playerState.facing == 1 ? -1 : 1;
- for (int distance = 1; distance < 640; ++distance) {
- int previousCandidateX = originalCenterX;
- for (int pass = 0; pass < 2; ++pass) {
- const int direction = pass == 0 ? preferredDirection : -preferredDirection;
- const int candidateX = clampPlayerCenterXToNativeBounds(
- playerState, originalCenterX + direction * distance);
- if (candidateX == originalCenterX || candidateX == previousCandidateX)
- continue;
- previousCandidateX = candidateX;
- if (isPlayerMovementBlocked(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateX, playerState.bottomY, playerState.z)) {
- continue;
- }
-
- playerState.centerX = candidateX;
- if (!applyRoomActorPlacement(state, *playerState.entity,
- playerState.centerX, playerState.bottomY, playerState.z)) {
- return false;
- }
- reinsertPlayerSceneEntity(engine, playerState);
-
- debugC(1, kDebugPlayer,
- "Harvester: adjusted blocked startup spawn room='%s' facing=%d from=(%d,%d,z=%.2f) to=(%d,%d,z=%.2f)",
- state.roomName.c_str(), playerState.facing,
- originalCenterX, originalBottomY, (double)playerState.z,
- playerState.centerX, playerState.bottomY, (double)playerState.z);
- return true;
- }
- }
-
- debugC(1, kDebugPlayer,
- "Harvester: startup spawn remains blocked room='%s' pos=(%d,%d,z=%.2f)",
- state.roomName.c_str(), playerState.centerX, playerState.bottomY,
- (double)playerState.z);
- return false;
-}
-
int Player::resolveRegionTargetX(const RoomSetupState &state, const RegionRecord ®ion,
const RoomPlayerState &playerState) {
const Common::Rect bounds(region.left, region.top, region.right, region.bottom);
@@ -1809,6 +1320,7 @@ bool Player::updateHitAnimationState(HarvesterEngine &engine, const RoomSetupSta
if (!playerState.hitActive || !playerState.entity)
return false;
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
bool changed = false;
if (playerState.hitKnockbackRemainingX != 0) {
const int step = MAX(1, ABS(playerState.hitKnockbackDecayStep));
@@ -1816,8 +1328,8 @@ bool Player::updateHitAnimationState(HarvesterEngine &engine, const RoomSetupSta
? MIN(playerState.hitKnockbackRemainingX, step)
: MAX(playerState.hitKnockbackRemainingX, -step);
if (knockbackStep != 0) {
- if (!tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, playerState.centerX + knockbackStep, playerState.bottomY, playerState.z)) {
+ if (!tryApplyPlayerMovement(movementContext,
+ { playerState.centerX + knockbackStep, playerState.bottomY, playerState.z })) {
playerState.hitKnockbackRemainingX = 0;
} else {
changed = true;
@@ -1988,7 +1500,8 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
playerState.hitActive || playerState.evasiveState >= 0)
return false;
- refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, playerState);
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
+ refreshPlayerCurrentBlocker(movementContext);
const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
const int currentFrame = playerState.entity->getCurrentFrame();
@@ -2080,8 +1593,8 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
}
const Entity *blocker = nullptr;
- bool moved = tryApplyPlayerWalkMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, candidateBottomY, candidateZ,
+ bool moved = tryApplyPlayerWalkMovement(movementContext,
+ { candidateCenterX, candidateBottomY, candidateZ },
horizontalDirection, depthDirection, &blocker);
if (!moved) {
if (engine.isPathfindingDebugEnabled()) {
@@ -2123,7 +1636,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
horizontalDirection, depthDirection);
}
syncAnimationPlacement();
- refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, playerState);
+ refreshPlayerCurrentBlocker(movementContext);
if (engine.isPathfindingDebugEnabled()) {
debugC(1, kDebugPathfinding,
"Harvester: pathfinding applied room='%s' previous=(%d,%d) current=(%d,%d,z=%.2f) facing=%d",
@@ -2149,6 +1662,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
if (playerState.turnActive || playerState.hitActive || playerState.evasiveState >= 0)
return false;
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
int horizontalInput = (moveRight ? 1 : 0) - (moveLeft ? 1 : 0);
int verticalInput = (moveDown ? 1 : 0) - (moveUp ? 1 : 0);
const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
@@ -2220,8 +1734,8 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
return false;
playerState.hasMoveTarget = false;
- bool moved = tryApplyPlayerWalkMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, candidateBottomY, candidateZ,
+ bool moved = tryApplyPlayerWalkMovement(movementContext,
+ { candidateCenterX, candidateBottomY, candidateZ },
horizontalInput, verticalInput);
if (!moved)
return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
diff --git a/engines/harvester/player_movement.cpp b/engines/harvester/player_movement.cpp
new file mode 100644
index 00000000000..b19cf29b5d4
--- /dev/null
+++ b/engines/harvester/player_movement.cpp
@@ -0,0 +1,508 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player_movement.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+
+namespace Harvester {
+namespace PlayerMovement {
+
+static const float kRoomPlayerHorizontalMoveBase = 8.0f;
+const float kRoomDepthCompareEpsilon = 0.01f;
+static const int kRoomPlayerMinOpaqueLeftX = 4;
+static const int kRoomPlayerMaxOpaqueRightX = 0x27c;
+
+static int roundToInt(float value) {
+ return value >= 0.0f ? (int)floorf(value + 0.5f) : (int)ceilf(value - 0.5f);
+}
+
+PlayerAnimationRange resolvePlayerAnimationRange(int facing) {
+ switch (facing) {
+ case 0:
+ return PlayerAnimationRange{ 0x00, 0x09, 0x3b };
+ case 1:
+ return PlayerAnimationRange{ 0x0f, 0x18, 0x0e };
+ case 2:
+ return PlayerAnimationRange{ 0x2d, 0x36, 0x2c };
+ case 3:
+ return PlayerAnimationRange{ 0x1e, 0x27, 0x28 };
+ default:
+ return PlayerAnimationRange{ 0x00, 0x09, 0x3b };
+ }
+}
+
+int clampRoomMovementY(const RoomSetupState &state, int screenY) {
+ if (!Player::supportsMovementBand(state))
+ return screenY;
+
+ return CLIP<int>(screenY, state.roomMaxZScreenY, state.roomMinZScreenY);
+}
+
+float mapRoomScreenYToDepth(const RoomSetupState &state, int screenY) {
+ if (!Player::supportsMovementBand(state))
+ return (float)state.playerSpawnZ;
+
+ if (screenY <= state.roomMaxZScreenY)
+ return (float)state.roomMaxZ;
+ if (screenY >= state.roomMinZScreenY)
+ return (float)state.roomMinZ;
+
+ return (float)(state.roomMaxZ - ((state.roomMaxZ - state.roomMinZ) *
+ (screenY - state.roomMaxZScreenY)) /
+ (state.roomMinZScreenY - state.roomMaxZScreenY));
+}
+
+float clampRoomDepth(const RoomSetupState &state, float z) {
+ if (!Player::supportsMovementBand(state))
+ return z;
+
+ return CLIP<float>(z,
+ (float)MIN(state.roomMinZ, state.roomMaxZ),
+ (float)MAX(state.roomMinZ, state.roomMaxZ));
+}
+
+int mapRoomDepthToScreenY(const RoomSetupState &state, float z) {
+ if (!Player::supportsMovementBand(state))
+ return state.playerSpawnY;
+
+ if (state.roomMaxZ == state.roomMinZ)
+ return clampRoomMovementY(state, state.playerSpawnY);
+
+ const float clampedZ = clampRoomDepth(state, z);
+ const float offset = ((float)state.roomMaxZ - clampedZ) *
+ (float)(state.roomMinZScreenY - state.roomMaxZScreenY) /
+ (float)(state.roomMaxZ - state.roomMinZ);
+ return clampRoomMovementY(state, state.roomMaxZScreenY + roundToInt(offset));
+}
+
+int computeRoomPlayerHorizontalStep(const RoomSetupState &state, float z) {
+ return MAX<int>(1, (int)(Player::computeDepthScale(state, z) * kRoomPlayerHorizontalMoveBase));
+}
+
+float computeRoomPlayerDepthStep(const RoomSetupState &state) {
+ return state.roomZVelocityStep > 0.0f ? state.roomZVelocityStep : 1.0f;
+}
+
+int computePlayerFrameLeftX(const RoomPlayerState &playerState) {
+ return playerState.entity ? playerState.entity->getScreenRect().left : playerState.centerX;
+}
+
+float computePlayerRenderZ(const RoomPlayerState &playerState) {
+ return playerState.entity ? playerState.entity->getZ() : playerState.z;
+}
+
+float clampPlayerMovementDepth(const RoomSetupState &state,
+ const RoomPlayerState &playerState, float z) {
+ const float placementOffset = playerState.z - computePlayerRenderZ(playerState);
+ return clampRoomDepth(state, z - placementOffset) + placementOffset;
+}
+
+int clampPlayerCenterXToNativeBounds(const RoomPlayerState &playerState, int centerX) {
+ if (!playerState.entity)
+ return CLIP<int>(centerX, 0, 639);
+
+ int width = 0;
+ int height = 0;
+ int xOffset = 0;
+ int yOffset = 0;
+ if (!playerState.entity->getCurrentFrameMetrics(width, height, xOffset, yOffset))
+ return CLIP<int>(centerX, 0, 639);
+
+ const int minCenterX = kRoomPlayerMinOpaqueLeftX + width / 2;
+ const int maxCenterX = MAX(minCenterX,
+ kRoomPlayerMaxOpaqueRightX - (width - width / 2));
+ return CLIP<int>(centerX, minCenterX, maxCenterX);
+}
+
+static uint32 computeAnimationTickInterval(int rate) {
+ return rate > 0 ? MAX<uint32>(1, 100U / (uint32)rate) : 0;
+}
+
+bool consumePlayerMovementTick(RoomPlayerState &playerState) {
+ if (playerState.entity &&
+ playerState.entity->isAnimationEnabled() &&
+ playerState.entity->getAnimationRate() != 0) {
+ playerState.nextMovementTick = 0;
+ return playerState.entity->didAnimationAdvanceLastTick();
+ }
+
+ const uint32 interval = computeAnimationTickInterval(playerState.animationRate);
+ if (interval == 0) {
+ playerState.nextMovementTick = 0;
+ return true;
+ }
+
+ const uint32 now = Player::getRuntimeClockTicks();
+ if (playerState.nextMovementTick != 0 &&
+ (int32)(now - playerState.nextMovementTick) < 0) {
+ return false;
+ }
+
+ playerState.nextMovementTick = now + interval;
+ return true;
+}
+
+static bool shouldIgnoreMovementBlocker(const Entity &entity, const Common::String *ignoredBlockerName) {
+ return ignoredBlockerName && !ignoredBlockerName->empty() &&
+ entity.getName().equalsIgnoreCase(*ignoredBlockerName);
+}
+
+static bool isSameBlockerName(const Common::String &history, const Entity &blocker) {
+ return !history.empty() && history.equalsIgnoreCase(blocker.getName());
+}
+
+static void clearPlayerBlockerHistory(RoomPlayerState &playerState) {
+ playerState.currentBlockerName.clear();
+ playerState.upwardBlockerHistory.clear();
+ playerState.downwardBlockerHistory.clear();
+ playerState.leftwardBlockerHistory.clear();
+ playerState.rightwardBlockerHistory.clear();
+}
+
+void setPlayerCurrentBlocker(RoomPlayerState &playerState, const Entity *blocker) {
+ if (blocker) {
+ playerState.currentBlockerName = blocker->getName();
+ return;
+ }
+
+ clearPlayerBlockerHistory(playerState);
+}
+
+static const Entity *findPlayerRoomBlocker(const MovementContext &context, const Entity &playerEntity,
+ const Common::String *ignoredBlockerName) {
+ EntityManager *entityManager = context.engine.getRuntimeEntities();
+ if (!entityManager)
+ return nullptr;
+
+ for (const ObjectRecord &object : context.sceneObjects) {
+ const Entity *entity = entityManager->findSceneEntityByName(object.objectName);
+ if (entity && !shouldIgnoreMovementBlocker(*entity, ignoredBlockerName) &&
+ playerEntity.overlapsEntity(*entity)) {
+ return entity;
+ }
+ }
+
+ for (const AnimRecord &anim : context.sceneAnimations) {
+ const Entity *entity = entityManager->findSceneEntityByName(anim.animName);
+ if (entity && !shouldIgnoreMovementBlocker(*entity, ignoredBlockerName) &&
+ playerEntity.overlapsEntity(*entity)) {
+ return entity;
+ }
+ }
+
+ return nullptr;
+}
+
+void refreshPlayerCurrentBlocker(const MovementContext &context) {
+ RoomPlayerState &playerState = context.playerState;
+ if (!playerState.entity) {
+ clearPlayerBlockerHistory(playerState);
+ return;
+ }
+
+ setPlayerCurrentBlocker(playerState, findPlayerRoomBlocker(context, *playerState.entity, nullptr));
+}
+
+void reinsertPlayerSceneEntity(HarvesterEngine &engine, const RoomPlayerState &playerState) {
+ if (!playerState.entity)
+ return;
+
+ EntityManager *entityManager = engine.getRuntimeEntities();
+ if (entityManager &&
+ entityManager->findSceneEntityByName(playerState.entity->getName()) == playerState.entity) {
+ entityManager->reinsertSceneEntity(playerState.entity);
+ }
+}
+
+static void applyPlayerMovementPlacement(const RoomSetupState &state, RoomPlayerState &playerState,
+ int candidateCenterX, int candidateBottomY, float candidateZ) {
+ Entity &entity = *playerState.entity;
+ 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));
+}
+
+bool isPlayerMovementBlocked(const MovementContext &context, const MovementPosition &candidate,
+ const Common::String *ignoredBlockerName, const Entity **blockerOut) {
+ const RoomSetupState &state = context.state;
+ RoomPlayerState &playerState = context.playerState;
+ if (!playerState.entity)
+ return true;
+
+ if (blockerOut)
+ *blockerOut = nullptr;
+
+ 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, candidate.centerX, candidate.bottomY, candidate.z);
+
+ const Entity *blocker = findPlayerRoomBlocker(context, entity, ignoredBlockerName);
+ entity.setPosition(oldX, oldY, oldZ);
+ entity.setDepthScale(oldScale);
+ if (blockerOut)
+ *blockerOut = blocker;
+ return blocker != nullptr;
+}
+
+bool tryApplyPlayerMovement(const MovementContext &context, MovementPosition candidate,
+ const Common::String *ignoredBlockerName, const Entity **blockerOut) {
+ HarvesterEngine &engine = context.engine;
+ const RoomSetupState &state = context.state;
+ RoomPlayerState &playerState = context.playerState;
+ if (!playerState.entity)
+ return false;
+
+ if (blockerOut)
+ *blockerOut = nullptr;
+
+ candidate.centerX = clampPlayerCenterXToNativeBounds(playerState, candidate.centerX);
+ candidate.z = clampRoomDepth(state, candidate.z);
+ if (candidate.centerX == playerState.centerX &&
+ candidate.bottomY == playerState.bottomY &&
+ fabsf(candidate.z - playerState.z) <= kRoomDepthCompareEpsilon) {
+ return false;
+ }
+ if (isPlayerMovementBlocked(context, candidate, ignoredBlockerName, blockerOut)) {
+ return false;
+ }
+
+ applyPlayerMovementPlacement(state, playerState, candidate.centerX, candidate.bottomY, candidate.z);
+ playerState.z = candidate.z;
+ syncPlayerAnimationPlacement(playerState);
+
+ reinsertPlayerSceneEntity(engine, playerState);
+ return true;
+}
+
+bool isPathfindingDebugEnabled() {
+ return g_engine && g_engine->isPathfindingDebugEnabled();
+}
+
+static void logPlayerBlockerHistory(const RoomSetupState &state,
+ const RoomPlayerState &playerState, const Entity &blocker,
+ const char *direction) {
+ if (!isPathfindingDebugEnabled())
+ return;
+
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding blocker history room='%s' blocker='%s' direction='%s' histories=(up='%s',down='%s',left='%s',right='%s')",
+ state.roomName.c_str(), blocker.getName().c_str(), direction,
+ playerState.upwardBlockerHistory.c_str(),
+ playerState.downwardBlockerHistory.c_str(),
+ playerState.leftwardBlockerHistory.c_str(),
+ playerState.rightwardBlockerHistory.c_str());
+}
+
+static bool recordNativePrimaryWalkBlockerHistory(const RoomSetupState &state,
+ RoomPlayerState &playerState, const Entity &blocker,
+ int horizontalDirection, int depthDirection) {
+ if (horizontalDirection < 0) {
+ const int frameLeftX = computePlayerFrameLeftX(playerState);
+ const Common::Rect blockerRect = blocker.getScreenRect();
+ if (isSameBlockerName(playerState.rightwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.upwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.downwardBlockerHistory, blocker) ||
+ frameLeftX <= blockerRect.left) {
+ return false;
+ }
+
+ playerState.leftwardBlockerHistory = blocker.getName();
+ playerState.moveTargetXReached = true;
+ logPlayerBlockerHistory(state, playerState, blocker, "left");
+ return true;
+ }
+
+ if (horizontalDirection > 0) {
+ const int frameLeftX = computePlayerFrameLeftX(playerState);
+ const Common::Rect blockerRect = blocker.getScreenRect();
+ if (isSameBlockerName(playerState.leftwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.upwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.downwardBlockerHistory, blocker) ||
+ blockerRect.left <= frameLeftX) {
+ return false;
+ }
+
+ playerState.rightwardBlockerHistory = blocker.getName();
+ playerState.moveTargetXReached = true;
+ logPlayerBlockerHistory(state, playerState, blocker, "right");
+ return true;
+ }
+
+ if (depthDirection > 0) {
+ if (isSameBlockerName(playerState.rightwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.leftwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.downwardBlockerHistory, blocker)) {
+ return false;
+ }
+
+ playerState.upwardBlockerHistory = blocker.getName();
+ playerState.moveTargetZReached = true;
+ logPlayerBlockerHistory(state, playerState, blocker, "upward");
+ return true;
+ }
+
+ if (depthDirection < 0) {
+ if (isSameBlockerName(playerState.rightwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.upwardBlockerHistory, blocker) ||
+ isSameBlockerName(playerState.leftwardBlockerHistory, blocker)) {
+ return false;
+ }
+
+ playerState.downwardBlockerHistory = blocker.getName();
+ playerState.moveTargetZReached = true;
+ logPlayerBlockerHistory(state, playerState, blocker, "downward");
+ return true;
+ }
+
+ return false;
+}
+
+static void playPlayerBlockedWalkSounds(HarvesterEngine &engine, const RoomSetupState &state,
+ const RoomPlayerState &playerState, const Entity &blocker) {
+ const Entity &entity = *playerState.entity;
+ const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
+ if (entity.getAnimationRate() != playerState.animationRate ||
+ entity.getCurrentFrame() < range.walkFirstFrame ||
+ entity.getCurrentFrame() > range.walkLastFrame)
+ return;
+
+ const int frameLeftX = computePlayerFrameLeftX(playerState);
+ if ((playerState.facing == 1 && frameLeftX < 30) ||
+ (playerState.facing == 2 && frameLeftX >= 611) ||
+ (playerState.facing == 0 && entity.getZ() <= state.roomMinZ) ||
+ (playerState.facing == 3 && entity.getZ() >= state.roomMaxZ))
+ return;
+
+ (void)engine.playSound("2:/SOUND/EFFECTS/FOOTLFT1.WAV");
+ (void)engine.playSound("2:/SOUND/EFFECTS/FOOTRHT1.WAV");
+ debugC(2, kDebugPlayer, "Harvester: player blocked walk sound blocker='%s' facing=%d",
+ blocker.getName().c_str(), playerState.facing);
+}
+
+bool tryApplyPlayerWalkMovement(const MovementContext &context, const MovementPosition &candidate,
+ int horizontalDirection, int depthDirection, const Entity **blockerOut) {
+ HarvesterEngine &engine = context.engine;
+ const RoomSetupState &state = context.state;
+ RoomPlayerState &playerState = context.playerState;
+ const Entity *blocker = findPlayerRoomBlocker(context, *playerState.entity, nullptr);
+ setPlayerCurrentBlocker(playerState, blocker);
+ if (blockerOut)
+ *blockerOut = nullptr;
+ if (blocker && recordNativePrimaryWalkBlockerHistory(state, playerState, *blocker,
+ horizontalDirection, depthDirection)) {
+ playPlayerBlockedWalkSounds(engine, state, playerState, *blocker);
+ if (blockerOut)
+ *blockerOut = blocker;
+ return false;
+ }
+
+ if (candidate.centerX == playerState.centerX &&
+ candidate.bottomY == playerState.bottomY &&
+ fabsf(candidate.z - playerState.z) <= kRoomDepthCompareEpsilon)
+ return false;
+
+ applyPlayerMovementPlacement(state, playerState, candidate.centerX, candidate.bottomY, candidate.z);
+ playerState.z = candidate.z;
+ syncPlayerAnimationPlacement(playerState);
+ reinsertPlayerSceneEntity(engine, playerState);
+ return true;
+}
+
+void syncPlayerAnimationPlacement(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;
+}
+
+} // End of namespace PlayerMovement
+
+using namespace PlayerMovement;
+
+bool Player::resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetupState &state,
+ const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity)
+ return false;
+
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
+ if (!isPlayerMovementBlocked(movementContext, { playerState.centerX, playerState.bottomY, playerState.z })) {
+ return false;
+ }
+
+ const int originalCenterX = playerState.centerX;
+ const int originalBottomY = playerState.bottomY;
+ const int preferredDirection = playerState.facing == 1 ? -1 : 1;
+ for (int distance = 1; distance < 640; ++distance) {
+ int previousCandidateX = originalCenterX;
+ for (int pass = 0; pass < 2; ++pass) {
+ const int direction = pass == 0 ? preferredDirection : -preferredDirection;
+ const int candidateX = clampPlayerCenterXToNativeBounds(
+ playerState, originalCenterX + direction * distance);
+ if (candidateX == originalCenterX || candidateX == previousCandidateX)
+ continue;
+ previousCandidateX = candidateX;
+ if (isPlayerMovementBlocked(movementContext, { candidateX, playerState.bottomY, playerState.z })) {
+ continue;
+ }
+
+ playerState.centerX = candidateX;
+ if (!applyRoomActorPlacement(state, *playerState.entity,
+ playerState.centerX, playerState.bottomY, playerState.z)) {
+ return false;
+ }
+ reinsertPlayerSceneEntity(engine, playerState);
+
+ debugC(1, kDebugPlayer,
+ "Harvester: adjusted blocked startup spawn room='%s' facing=%d from=(%d,%d,z=%.2f) to=(%d,%d,z=%.2f)",
+ state.roomName.c_str(), playerState.facing,
+ originalCenterX, originalBottomY, (double)playerState.z,
+ playerState.centerX, playerState.bottomY, (double)playerState.z);
+ return true;
+ }
+ }
+
+ debugC(1, kDebugPlayer,
+ "Harvester: startup spawn remains blocked room='%s' pos=(%d,%d,z=%.2f)",
+ state.roomName.c_str(), playerState.centerX, playerState.bottomY,
+ (double)playerState.z);
+ return false;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/player_movement.h b/engines/harvester/player_movement.h
new file mode 100644
index 00000000000..6802908f189
--- /dev/null
+++ b/engines/harvester/player_movement.h
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef HARVESTER_PLAYER_MOVEMENT_H
+#define HARVESTER_PLAYER_MOVEMENT_H
+
+#include "harvester/player.h"
+
+namespace Harvester {
+namespace PlayerMovement {
+
+struct MovementContext {
+ HarvesterEngine &engine;
+ const RoomSetupState &state;
+ const Common::Array<ObjectRecord> &sceneObjects;
+ const Common::Array<AnimRecord> &sceneAnimations;
+ RoomPlayerState &playerState;
+};
+
+struct MovementPosition {
+ int centerX;
+ int bottomY;
+ float z;
+};
+
+struct PlayerAnimationRange {
+ PlayerAnimationRange() {}
+ PlayerAnimationRange(int walkFirstFrame, int walkLastFrame, int idleFrame)
+ : walkFirstFrame(walkFirstFrame), walkLastFrame(walkLastFrame), idleFrame(idleFrame) {}
+
+ int walkFirstFrame = 0;
+ int walkLastFrame = 0;
+ int idleFrame = 0;
+};
+
+extern const float kRoomDepthCompareEpsilon;
+
+PlayerAnimationRange resolvePlayerAnimationRange(int facing);
+int clampRoomMovementY(const RoomSetupState &state, int screenY);
+float mapRoomScreenYToDepth(const RoomSetupState &state, int screenY);
+float clampRoomDepth(const RoomSetupState &state, float z);
+int mapRoomDepthToScreenY(const RoomSetupState &state, float z);
+int computeRoomPlayerHorizontalStep(const RoomSetupState &state, float z);
+float computeRoomPlayerDepthStep(const RoomSetupState &state);
+int computePlayerFrameLeftX(const RoomPlayerState &playerState);
+float computePlayerRenderZ(const RoomPlayerState &playerState);
+float clampPlayerMovementDepth(const RoomSetupState &state,
+ const RoomPlayerState &playerState, float z);
+int clampPlayerCenterXToNativeBounds(const RoomPlayerState &playerState, int centerX);
+bool consumePlayerMovementTick(RoomPlayerState &playerState);
+void setPlayerCurrentBlocker(RoomPlayerState &playerState, const Entity *blocker);
+void refreshPlayerCurrentBlocker(const MovementContext &context);
+void reinsertPlayerSceneEntity(HarvesterEngine &engine, const RoomPlayerState &playerState);
+bool isPlayerMovementBlocked(const MovementContext &context, const MovementPosition &candidate,
+ const Common::String *ignoredBlockerName = nullptr, const Entity **blockerOut = nullptr);
+bool tryApplyPlayerMovement(const MovementContext &context, MovementPosition candidate,
+ const Common::String *ignoredBlockerName = nullptr, const Entity **blockerOut = nullptr);
+bool tryApplyPlayerWalkMovement(const MovementContext &context, const MovementPosition &candidate,
+ int horizontalDirection, int depthDirection, const Entity **blockerOut = nullptr);
+bool isPathfindingDebugEnabled();
+void syncPlayerAnimationPlacement(RoomPlayerState &playerState);
+
+} // End of namespace PlayerMovement
+} // End of namespace Harvester
+
+#endif // HARVESTER_PLAYER_MOVEMENT_H
Commit: 6be6653618f1cca75834a73594da321f92bfff4f
https://github.com/scummvm/scummvm/commit/6be6653618f1cca75834a73594da321f92bfff4f
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate player target walking
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/player_target.cpp
engines/harvester/module.mk
engines/harvester/player.cpp
engines/harvester/player.h
engines/harvester/player_movement.cpp
engines/harvester/player_movement.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 048ee050d48..80bfb1457be 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -23,6 +23,7 @@ MODULE_OBJS = \
monster.o \
player.o \
player_movement.o \
+ player_target.o \
resources.o \
combat.o \
combat_contact.o \
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index c96dee572f0..89d692bc8de 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -40,9 +40,6 @@ static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing);
static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState);
static const char *const kPlayerActorResourcePath = "1:/GRAPHIC/MONSTERS/PC/PC0.ABM";
-static const float kRoomPlayerHorizontalTargetSlackBase = 50.0f;
-static const float kRoomPlayerDepthTargetSlack = 8.0f;
-static const int kRoomRegionTargetXBias = 10;
static const int kRoomPlayerWalkAnimationRate = 17;
static const int kRoomPlayerFastAnimationRate = 30;
static const int kRoomPlayerHitAnimationRate = 4;
@@ -477,141 +474,11 @@ static bool startResolvedAttackAnimation(RoomPlayerState &playerState,
return true;
}
-static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
- playerState.moveTargetXReached = false;
- playerState.moveTargetZReached = false;
-}
-
-static int resolveMoveTargetHorizontalDirection(const RoomSetupState &state,
- const RoomPlayerState &playerState, int regionFacing) {
- const float slack = Player::computeDepthScale(state, computePlayerRenderZ(playerState)) *
- kRoomPlayerHorizontalTargetSlackBase;
- const float frameLeftX = (float)computePlayerFrameLeftX(playerState);
- if (frameLeftX < (float)playerState.targetX - slack || regionFacing == 2)
- return 1;
- if ((float)playerState.targetX + slack < frameLeftX || regionFacing == 1)
- return -1;
-
- return 0;
-}
-
-static int resolveMoveTargetDepthDirection(const RoomPlayerState &playerState, int regionFacing) {
- const float z = computePlayerRenderZ(playerState);
- if (playerState.targetZ < z - kRoomPlayerDepthTargetSlack || regionFacing == 0)
- return 1;
- if (z + kRoomPlayerDepthTargetSlack < playerState.targetZ || regionFacing == 3)
- return -1;
-
- return 0;
-}
-
-static bool hasPlayerReachedMoveTarget(const RoomPlayerState &playerState) {
- return playerState.moveTargetXReached && playerState.moveTargetZReached;
-}
-
-static void updatePlayerMoveTargetProgress(const RoomSetupState &state,
- RoomPlayerState &playerState, int regionFacing) {
- if (!playerState.moveTargetXReached &&
- resolveMoveTargetHorizontalDirection(state, playerState, regionFacing) == 0) {
- playerState.moveTargetXReached = true;
- }
- if (!playerState.moveTargetZReached &&
- resolveMoveTargetDepthDirection(playerState, regionFacing) == 0) {
- playerState.moveTargetZReached = true;
- }
-}
-
-static bool isHorizontalDirectionBlockedByHistory(const RoomPlayerState &playerState,
- int horizontalDirection) {
- if (horizontalDirection < 0)
- return !playerState.leftwardBlockerHistory.empty();
- if (horizontalDirection > 0)
- return !playerState.rightwardBlockerHistory.empty();
- return false;
-}
-
-static bool isDepthDirectionBlockedByHistory(const RoomPlayerState &playerState,
- int depthDirection) {
- if (depthDirection > 0)
- return !playerState.upwardBlockerHistory.empty();
- if (depthDirection < 0)
- return !playerState.downwardBlockerHistory.empty();
- return false;
-}
-
-static const char *describeBlockedDirection(int horizontalDirection, int depthDirection) {
- if (horizontalDirection < 0)
- return "left";
- if (horizontalDirection > 0)
- return "right";
- if (depthDirection > 0)
- return "upward";
- if (depthDirection < 0)
- return "downward";
- return "";
-}
-
-static int resolveFacingFromMoveDirection(int horizontalDirection, int depthDirection,
- int fallbackFacing) {
- if (horizontalDirection < 0)
- return 1;
- if (horizontalDirection > 0)
- return 2;
- if (depthDirection < 0)
- return 3;
- if (depthDirection > 0)
- return 0;
- return fallbackFacing >= 0 ? fallbackFacing : 0;
-}
-
-static void setMoveTargetInternal(const RoomSetupState &state, RoomPlayerState &playerState,
- int targetX, float targetZ, int targetBottomY) {
- playerState.hasMoveTarget = true;
- playerState.nextMovementTick = 0;
- playerState.targetX = CLIP<int>(targetX, 0, 639);
- playerState.targetBottomY = clampRoomMovementY(state, targetBottomY);
- playerState.targetZ = targetZ;
- resetPlayerMoveTargetProgress(playerState);
- debugC(1, kDebugPlayer,
- "Harvester: player move target room='%s' current=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f)",
- state.roomName.c_str(), playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
- if (isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding target set room='%s' movement_band=(y=%d..%d,z=%d..%d) current=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f)",
- state.roomName.c_str(), state.roomMaxZScreenY, state.roomMinZScreenY,
- state.roomMinZ, state.roomMaxZ,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
- }
-}
-
static void notePlayerIdleReset(RoomIdleAnimationState &idleState) {
idleState.resetTick = Player::getRuntimeClockTicks();
updatePlayerIdleTrigger(idleState);
}
-static bool setPlayerWalkAnimation(RoomPlayerState &playerState, int facing) {
- if (!playerState.entity)
- return false;
-
- const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
- const int currentFrame = playerState.entity->getCurrentFrame();
- const bool alreadyWalking = playerState.facing == facing &&
- playerState.entity->getAnimationRate() == playerState.animationRate &&
- currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
- if (alreadyWalking)
- return false;
-
- playerState.facing = facing;
- playerState.entity->setAnimationFrameRange(range.walkFirstFrame, range.walkLastFrame, true);
- playerState.entity->setAnimationRate(playerState.animationRate);
- debugC(1, kDebugPlayer,
- "Harvester: player walk animation facing=%d frames=%d..%d rate=%d",
- facing, range.walkFirstFrame, range.walkLastFrame, playerState.animationRate);
- return true;
-}
-
static void positionPlayerIdleAnimationEntity(const RoomSetupState &state,
const RoomPlayerState &playerState, Entity &idleEntity) {
if (!playerState.entity)
@@ -985,48 +852,6 @@ bool Player::isIdleAnimationExcludedRoom(const Common::String &roomName) {
}
-void Player::setMoveTarget(const RoomSetupState &state, int targetX, float targetZ) {
- RoomPlayerState &playerState = _state;
- setMoveTargetInternal(state, playerState, targetX, clampRoomDepth(state, targetZ),
- mapRoomDepthToScreenY(state, targetZ));
-}
-
-void Player::setRegionMoveTarget(const RoomSetupState &state, int targetX, float targetZ) {
- RoomPlayerState &playerState = _state;
- setMoveTargetInternal(state, playerState, targetX, targetZ,
- mapRoomDepthToScreenY(state, targetZ));
-}
-
-void Player::setMoveTargetFromScreenPoint(const RoomSetupState &state, int targetX, int targetBottomY) {
- RoomPlayerState &playerState = _state;
- const int clampedTargetBottomY = clampRoomMovementY(state, targetBottomY);
- setMoveTargetInternal(state, playerState, targetX,
- mapRoomScreenYToDepth(state, clampedTargetBottomY), clampedTargetBottomY);
-}
-
-int Player::resolveRegionTargetX(const RoomSetupState &state, const RegionRecord ®ion,
- const RoomPlayerState &playerState) {
- const Common::Rect bounds(region.left, region.top, region.right, region.bottom);
- int targetX = bounds.left + bounds.width() / 2;
- if (!playerState.entity)
- return CLIP<int>(targetX, 0, 639);
-
- const int frameLeftX = computePlayerFrameLeftX(playerState);
- if (targetX + kRoomRegionTargetXBias < frameLeftX)
- targetX -= kRoomRegionTargetXBias;
-
- return CLIP<int>(targetX, 0, 639);
-}
-
-float Player::resolveRegionTargetZ(const RegionRecord ®ion) {
- if (region.desiredFacing == 0)
- return (float)region.maxZ;
- if (region.desiredFacing == 3)
- return (float)region.minZ;
-
- return (float)(region.minZ + (region.maxZ - region.minZ) / 2);
-}
-
void Player::syncCombatLoadout(HarvesterEngine &engine, const RoomSetupState &state) {
if (_state.entity) {
(void)syncCombatLoadoutVisual(engine, state, engine.getScript()->getPlayerCombatLoadout());
@@ -1492,167 +1317,6 @@ bool Player::updateTurnAnimationState() {
return true;
}
-bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
- int regionFacing) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || !playerState.hasMoveTarget || playerState.turnActive ||
- playerState.hitActive || playerState.evasiveState >= 0)
- return false;
-
- const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
- refreshPlayerCurrentBlocker(movementContext);
-
- const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
- const int currentFrame = playerState.entity->getCurrentFrame();
- const bool walking = playerState.entity->getAnimationRate() == playerState.animationRate &&
- currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
- const bool updateTarget = !walking || currentFrame == range.walkLastFrame;
- if (updateTarget)
- updatePlayerMoveTargetProgress(state, playerState, regionFacing);
- if (hasPlayerReachedMoveTarget(playerState)) {
- playerState.hasMoveTarget = false;
- return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
- }
-
- const int previousCenterX = playerState.centerX;
- const int previousBottomY = playerState.bottomY;
- int horizontalDirection = updateTarget
- ? (playerState.moveTargetXReached ? 0 : resolveMoveTargetHorizontalDirection(state, playerState, regionFacing))
- : playerState.walkHorizontalDirection;
- if (horizontalDirection != 0 && isHorizontalDirectionBlockedByHistory(playerState, horizontalDirection)) {
- if (engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding direction held by blocker history room='%s' direction='%s' history='%s'",
- state.roomName.c_str(),
- describeBlockedDirection(horizontalDirection, 0),
- horizontalDirection < 0
- ? playerState.leftwardBlockerHistory.c_str()
- : playerState.rightwardBlockerHistory.c_str());
- }
- horizontalDirection = 0;
- }
- int depthDirection = updateTarget
- ? (playerState.moveTargetZReached ? 0 : resolveMoveTargetDepthDirection(playerState, regionFacing))
- : playerState.walkDepthDirection;
- if (depthDirection != 0 && isDepthDirectionBlockedByHistory(playerState, depthDirection)) {
- if (engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding direction held by blocker history room='%s' direction='%s' history='%s'",
- state.roomName.c_str(),
- describeBlockedDirection(0, depthDirection),
- depthDirection > 0
- ? playerState.upwardBlockerHistory.c_str()
- : playerState.downwardBlockerHistory.c_str());
- }
- depthDirection = 0;
- }
- if (horizontalDirection == 0 && depthDirection == 0) {
- playerState.hasMoveTarget = false;
- return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
- }
- const int desiredFacing = resolveFacingFromMoveDirection(
- horizontalDirection, depthDirection, playerState.facing);
- if (regionFacing >= 0 && desiredFacing != playerState.facing &&
- startTurnAnimation(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;
-
- int candidateCenterX = playerState.centerX;
- const int candidateBottomY = playerState.bottomY;
- float candidateZ = playerState.z;
- if (horizontalDirection != 0) {
- const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
- candidateCenterX = clampPlayerCenterXToNativeBounds(
- playerState, playerState.centerX + horizontalDirection * horizontalStep);
- candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - depthDirection);
- } else if (depthDirection != 0) {
- const float depthStep = computeRoomPlayerDepthStep(state);
- const bool depthBoundaryReached = depthDirection < 0
- ? computePlayerRenderZ(playerState) >= state.roomMaxZ
- : computePlayerRenderZ(playerState) <= state.roomMinZ;
- if (!depthBoundaryReached) {
- candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - depthDirection * depthStep);
- }
- }
-
- if (engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding step room='%s' current=(%d,%d,z=%.2f) target=(x=%d,y=%d,z=%.2f) progress=(x=%d,z=%d) direction=(x=%d,z=%d) candidate=(%d,%d,z=%.2f)",
- state.roomName.c_str(), playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ,
- playerState.moveTargetXReached, playerState.moveTargetZReached,
- horizontalDirection, depthDirection,
- candidateCenterX, candidateBottomY, (double)candidateZ);
- }
-
- const Entity *blocker = nullptr;
- bool moved = tryApplyPlayerWalkMovement(movementContext,
- { candidateCenterX, candidateBottomY, candidateZ },
- horizontalDirection, depthDirection, &blocker);
- if (!moved) {
- if (engine.isPathfindingDebugEnabled()) {
- const Common::Rect blockerRect = blocker ? blocker->getScreenRect() : Common::Rect();
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding blocked room='%s' candidate=(%d,%d,z=%.2f) blocker='%s' class=0x%x rect=(%d,%d)-(%d,%d) z=%.2f z_extent=%.2f",
- state.roomName.c_str(), candidateCenterX, candidateBottomY, (double)candidateZ,
- blocker ? blocker->getName().c_str() : "",
- blocker ? blocker->getClassId() : -1,
- blockerRect.left, blockerRect.top, blockerRect.right, blockerRect.bottom,
- blocker ? (double)blocker->getZ() : 0.0,
- blocker ? (double)blocker->getZExtent() : 0.0);
- }
- setPlayerCurrentBlocker(playerState, blocker);
- if (blocker) {
- return setIdleAnimation(resolveFacingFromMoveDirection(horizontalDirection, depthDirection, playerState.facing));
- }
-
- playerState.hasMoveTarget = false;
- int idleFacing = playerState.facing;
- if (idleFacing < 0) {
- if (horizontalDirection != 0)
- idleFacing = horizontalDirection < 0 ? 1 : 2;
- else
- idleFacing = depthDirection < 0 ? 3 : 0;
- }
- return setIdleAnimation(idleFacing);
- }
-
- const int actualFacing = desiredFacing;
- playerState.walkHorizontalDirection = horizontalDirection;
- playerState.walkDepthDirection = depthDirection;
- (void)setPlayerWalkAnimation(playerState, actualFacing);
- if (walking && currentFrame == range.walkLastFrame) {
- playerState.entity->setCurrentFrame(resolvePlayerAnimationRange(actualFacing).walkFirstFrame);
- debugC(2, kDebugPlayer,
- "Harvester: player target walk bank restart facing=%d frame=%d render_z=%.2f direction=(%d,%d)",
- actualFacing, playerState.entity->getCurrentFrame(), (double)computePlayerRenderZ(playerState),
- horizontalDirection, depthDirection);
- }
- syncAnimationPlacement();
- refreshPlayerCurrentBlocker(movementContext);
- if (engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding applied room='%s' previous=(%d,%d) current=(%d,%d,z=%.2f) facing=%d",
- state.roomName.c_str(), previousCenterX, previousBottomY,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- actualFacing);
- }
- debugC(playerState.hasMoveTarget ? 2 : 1, kDebugPlayer,
- "Harvester: player move step room='%s' pos=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f) facing=%d frame=%d active=%d moved=%d",
- state.roomName.c_str(), playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ,
- playerState.facing, playerState.entity->getCurrentFrame(),
- playerState.hasMoveTarget, moved);
- return true;
-}
-
bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index d9d81b80abd..355095ca416 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -28,6 +28,11 @@ namespace Harvester {
class EntityManager;
+namespace PlayerMovement {
+struct MovementContext;
+struct MovementPosition;
+}
+
struct RoomPlayerState {
Entity *entity = nullptr;
int centerX = 0;
@@ -168,6 +173,10 @@ public:
bool updateIdleAnimation(const RoomSetupState &state);
private:
+ bool stopMoveTargetStep(const PlayerMovement::MovementContext &context,
+ const PlayerMovement::MovementPosition &candidate, int horizontalDirection, int depthDirection,
+ const Entity *blocker);
+
RoomPlayerState _state;
RoomIdleAnimationState _idleState;
};
diff --git a/engines/harvester/player_movement.cpp b/engines/harvester/player_movement.cpp
index b19cf29b5d4..b60a0eafcb2 100644
--- a/engines/harvester/player_movement.cpp
+++ b/engines/harvester/player_movement.cpp
@@ -451,6 +451,40 @@ void syncPlayerAnimationPlacement(RoomPlayerState &playerState) {
playerState.bottomY = rect.bottom;
}
+int resolveFacingFromMoveDirection(int horizontalDirection, int depthDirection,
+ int fallbackFacing) {
+ if (horizontalDirection < 0)
+ return 1;
+ if (horizontalDirection > 0)
+ return 2;
+ if (depthDirection < 0)
+ return 3;
+ if (depthDirection > 0)
+ return 0;
+ return fallbackFacing >= 0 ? fallbackFacing : 0;
+}
+
+bool setPlayerWalkAnimation(RoomPlayerState &playerState, int facing) {
+ if (!playerState.entity)
+ return false;
+
+ const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
+ const int currentFrame = playerState.entity->getCurrentFrame();
+ const bool alreadyWalking = playerState.facing == facing &&
+ playerState.entity->getAnimationRate() == playerState.animationRate &&
+ currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
+ if (alreadyWalking)
+ return false;
+
+ playerState.facing = facing;
+ playerState.entity->setAnimationFrameRange(range.walkFirstFrame, range.walkLastFrame, true);
+ playerState.entity->setAnimationRate(playerState.animationRate);
+ debugC(1, kDebugPlayer,
+ "Harvester: player walk animation facing=%d frames=%d..%d rate=%d",
+ facing, range.walkFirstFrame, range.walkLastFrame, playerState.animationRate);
+ return true;
+}
+
} // End of namespace PlayerMovement
using namespace PlayerMovement;
diff --git a/engines/harvester/player_movement.h b/engines/harvester/player_movement.h
index 6802908f189..a2304296de9 100644
--- a/engines/harvester/player_movement.h
+++ b/engines/harvester/player_movement.h
@@ -78,6 +78,9 @@ bool tryApplyPlayerWalkMovement(const MovementContext &context, const MovementPo
bool isPathfindingDebugEnabled();
void syncPlayerAnimationPlacement(RoomPlayerState &playerState);
+int resolveFacingFromMoveDirection(int horizontalDirection, int depthDirection, int fallbackFacing);
+bool setPlayerWalkAnimation(RoomPlayerState &playerState, int facing);
+
} // End of namespace PlayerMovement
} // End of namespace Harvester
diff --git a/engines/harvester/player_target.cpp b/engines/harvester/player_target.cpp
new file mode 100644
index 00000000000..3428f81cfeb
--- /dev/null
+++ b/engines/harvester/player_target.cpp
@@ -0,0 +1,376 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player.h"
+#include "harvester/player_movement.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+
+namespace Harvester {
+
+using namespace PlayerMovement;
+
+namespace {
+
+static const float kRoomPlayerHorizontalTargetSlackBase = 50.0f;
+static const float kRoomPlayerDepthTargetSlack = 8.0f;
+static const int kRoomRegionTargetXBias = 10;
+
+static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
+ playerState.moveTargetXReached = false;
+ playerState.moveTargetZReached = false;
+}
+
+static int resolveMoveTargetHorizontalDirection(const RoomSetupState &state,
+ const RoomPlayerState &playerState, int regionFacing) {
+ const float slack = Player::computeDepthScale(state, computePlayerRenderZ(playerState)) *
+ kRoomPlayerHorizontalTargetSlackBase;
+ const float frameLeftX = (float)computePlayerFrameLeftX(playerState);
+ if (frameLeftX < (float)playerState.targetX - slack || regionFacing == 2)
+ return 1;
+ if ((float)playerState.targetX + slack < frameLeftX || regionFacing == 1)
+ return -1;
+
+ return 0;
+}
+
+static int resolveMoveTargetDepthDirection(const RoomPlayerState &playerState, int regionFacing) {
+ const float z = computePlayerRenderZ(playerState);
+ if (playerState.targetZ < z - kRoomPlayerDepthTargetSlack || regionFacing == 0)
+ return 1;
+ if (z + kRoomPlayerDepthTargetSlack < playerState.targetZ || regionFacing == 3)
+ return -1;
+
+ return 0;
+}
+
+static bool hasPlayerReachedMoveTarget(const RoomPlayerState &playerState) {
+ return playerState.moveTargetXReached && playerState.moveTargetZReached;
+}
+
+static void updatePlayerMoveTargetProgress(const RoomSetupState &state,
+ RoomPlayerState &playerState, int regionFacing) {
+ if (!playerState.moveTargetXReached &&
+ resolveMoveTargetHorizontalDirection(state, playerState, regionFacing) == 0) {
+ playerState.moveTargetXReached = true;
+ }
+ if (!playerState.moveTargetZReached &&
+ resolveMoveTargetDepthDirection(playerState, regionFacing) == 0) {
+ playerState.moveTargetZReached = true;
+ }
+}
+
+static bool isHorizontalDirectionBlockedByHistory(const RoomPlayerState &playerState,
+ int horizontalDirection) {
+ if (horizontalDirection < 0)
+ return !playerState.leftwardBlockerHistory.empty();
+ if (horizontalDirection > 0)
+ return !playerState.rightwardBlockerHistory.empty();
+ return false;
+}
+
+static bool isDepthDirectionBlockedByHistory(const RoomPlayerState &playerState,
+ int depthDirection) {
+ if (depthDirection > 0)
+ return !playerState.upwardBlockerHistory.empty();
+ if (depthDirection < 0)
+ return !playerState.downwardBlockerHistory.empty();
+ return false;
+}
+
+static const char *describeBlockedDirection(int horizontalDirection, int depthDirection) {
+ if (horizontalDirection < 0)
+ return "left";
+ if (horizontalDirection > 0)
+ return "right";
+ if (depthDirection > 0)
+ return "upward";
+ if (depthDirection < 0)
+ return "downward";
+ return "";
+}
+
+static void setMoveTargetInternal(const RoomSetupState &state, RoomPlayerState &playerState,
+ int targetX, float targetZ, int targetBottomY) {
+ playerState.hasMoveTarget = true;
+ playerState.nextMovementTick = 0;
+ playerState.targetX = CLIP<int>(targetX, 0, 639);
+ playerState.targetBottomY = clampRoomMovementY(state, targetBottomY);
+ playerState.targetZ = targetZ;
+ resetPlayerMoveTargetProgress(playerState);
+ debugC(1, kDebugPlayer,
+ "Harvester: player move target room='%s' current=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f)",
+ state.roomName.c_str(), playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
+ if (isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding target set room='%s' movement_band=(y=%d..%d,z=%d..%d) current=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f)",
+ state.roomName.c_str(), state.roomMaxZScreenY, state.roomMinZScreenY,
+ state.roomMinZ, state.roomMaxZ,
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
+ }
+}
+
+struct TargetWalkDirection {
+ int horizontal;
+ int depth;
+};
+
+static TargetWalkDirection resolveTargetWalkDirection(HarvesterEngine &engine, const RoomSetupState &state,
+ const RoomPlayerState &playerState, int regionFacing, bool updateTarget) {
+ int horizontalDirection = updateTarget
+ ? (playerState.moveTargetXReached ? 0 : resolveMoveTargetHorizontalDirection(state, playerState, regionFacing))
+ : playerState.walkHorizontalDirection;
+ if (horizontalDirection != 0 && isHorizontalDirectionBlockedByHistory(playerState, horizontalDirection)) {
+ if (engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding direction held by blocker history room='%s' direction='%s' history='%s'",
+ state.roomName.c_str(),
+ describeBlockedDirection(horizontalDirection, 0),
+ horizontalDirection < 0
+ ? playerState.leftwardBlockerHistory.c_str()
+ : playerState.rightwardBlockerHistory.c_str());
+ }
+ horizontalDirection = 0;
+ }
+ int depthDirection = updateTarget
+ ? (playerState.moveTargetZReached ? 0 : resolveMoveTargetDepthDirection(playerState, regionFacing))
+ : playerState.walkDepthDirection;
+ if (depthDirection != 0 && isDepthDirectionBlockedByHistory(playerState, depthDirection)) {
+ if (engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding direction held by blocker history room='%s' direction='%s' history='%s'",
+ state.roomName.c_str(),
+ describeBlockedDirection(0, depthDirection),
+ depthDirection > 0
+ ? playerState.upwardBlockerHistory.c_str()
+ : playerState.downwardBlockerHistory.c_str());
+ }
+ depthDirection = 0;
+ }
+ return { horizontalDirection, depthDirection };
+}
+
+static MovementPosition computeMoveTargetStep(const RoomSetupState &state,
+ const RoomPlayerState &playerState, const TargetWalkDirection &direction) {
+ int candidateCenterX = playerState.centerX;
+ const int candidateBottomY = playerState.bottomY;
+ float candidateZ = playerState.z;
+ if (direction.horizontal != 0) {
+ const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
+ candidateCenterX = clampPlayerCenterXToNativeBounds(
+ playerState, playerState.centerX + direction.horizontal * horizontalStep);
+ candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - direction.depth);
+ } else if (direction.depth != 0) {
+ const float depthStep = computeRoomPlayerDepthStep(state);
+ const bool depthBoundaryReached = direction.depth < 0
+ ? computePlayerRenderZ(playerState) >= state.roomMaxZ
+ : computePlayerRenderZ(playerState) <= state.roomMinZ;
+ if (!depthBoundaryReached) {
+ candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - direction.depth * depthStep);
+ }
+ }
+
+ return { candidateCenterX, candidateBottomY, candidateZ };
+}
+
+static void logAppliedMoveTargetStep(const MovementContext &context,
+ int previousCenterX, int previousBottomY, int actualFacing, bool moved) {
+ HarvesterEngine &engine = context.engine;
+ const RoomSetupState &state = context.state;
+ const RoomPlayerState &playerState = context.playerState;
+ if (engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding applied room='%s' previous=(%d,%d) current=(%d,%d,z=%.2f) facing=%d",
+ state.roomName.c_str(), previousCenterX, previousBottomY,
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ actualFacing);
+ }
+ debugC(playerState.hasMoveTarget ? 2 : 1, kDebugPlayer,
+ "Harvester: player move step room='%s' pos=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f) facing=%d frame=%d active=%d moved=%d",
+ state.roomName.c_str(), playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ,
+ playerState.facing, playerState.entity->getCurrentFrame(),
+ playerState.hasMoveTarget, moved);
+}
+
+} // End of anonymous namespace
+
+void Player::setMoveTarget(const RoomSetupState &state, int targetX, float targetZ) {
+ RoomPlayerState &playerState = _state;
+ setMoveTargetInternal(state, playerState, targetX, clampRoomDepth(state, targetZ),
+ mapRoomDepthToScreenY(state, targetZ));
+}
+
+void Player::setRegionMoveTarget(const RoomSetupState &state, int targetX, float targetZ) {
+ RoomPlayerState &playerState = _state;
+ setMoveTargetInternal(state, playerState, targetX, targetZ,
+ mapRoomDepthToScreenY(state, targetZ));
+}
+
+void Player::setMoveTargetFromScreenPoint(const RoomSetupState &state, int targetX, int targetBottomY) {
+ RoomPlayerState &playerState = _state;
+ const int clampedTargetBottomY = clampRoomMovementY(state, targetBottomY);
+ setMoveTargetInternal(state, playerState, targetX,
+ mapRoomScreenYToDepth(state, clampedTargetBottomY), clampedTargetBottomY);
+}
+
+int Player::resolveRegionTargetX(const RoomSetupState &state, const RegionRecord ®ion,
+ const RoomPlayerState &playerState) {
+ const Common::Rect bounds(region.left, region.top, region.right, region.bottom);
+ int targetX = bounds.left + bounds.width() / 2;
+ if (!playerState.entity)
+ return CLIP<int>(targetX, 0, 639);
+
+ const int frameLeftX = computePlayerFrameLeftX(playerState);
+ if (targetX + kRoomRegionTargetXBias < frameLeftX)
+ targetX -= kRoomRegionTargetXBias;
+
+ return CLIP<int>(targetX, 0, 639);
+}
+
+float Player::resolveRegionTargetZ(const RegionRecord ®ion) {
+ if (region.desiredFacing == 0)
+ return (float)region.maxZ;
+ if (region.desiredFacing == 3)
+ return (float)region.minZ;
+
+ return (float)(region.minZ + (region.maxZ - region.minZ) / 2);
+}
+
+bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state,
+ const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
+ int regionFacing) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || !playerState.hasMoveTarget || playerState.turnActive ||
+ playerState.hitActive || playerState.evasiveState >= 0)
+ return false;
+
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
+ refreshPlayerCurrentBlocker(movementContext);
+
+ const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
+ const int currentFrame = playerState.entity->getCurrentFrame();
+ const bool walking = playerState.entity->getAnimationRate() == playerState.animationRate &&
+ currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
+ const bool updateTarget = !walking || currentFrame == range.walkLastFrame;
+ if (updateTarget)
+ updatePlayerMoveTargetProgress(state, playerState, regionFacing);
+ if (hasPlayerReachedMoveTarget(playerState)) {
+ playerState.hasMoveTarget = false;
+ return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
+ }
+
+ const int previousCenterX = playerState.centerX;
+ const int previousBottomY = playerState.bottomY;
+ const TargetWalkDirection direction = resolveTargetWalkDirection(
+ engine, state, playerState, regionFacing, updateTarget);
+ if (direction.horizontal == 0 && direction.depth == 0) {
+ playerState.hasMoveTarget = false;
+ return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
+ }
+ const int desiredFacing = resolveFacingFromMoveDirection(
+ direction.horizontal, direction.depth, playerState.facing);
+ if (regionFacing >= 0 && desiredFacing != playerState.facing &&
+ startTurnAnimation(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;
+
+ const MovementPosition candidate = computeMoveTargetStep(state, playerState, direction);
+
+ if (engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding step room='%s' current=(%d,%d,z=%.2f) target=(x=%d,y=%d,z=%.2f) progress=(x=%d,z=%d) direction=(x=%d,z=%d) candidate=(%d,%d,z=%.2f)",
+ state.roomName.c_str(), playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ,
+ playerState.moveTargetXReached, playerState.moveTargetZReached,
+ direction.horizontal, direction.depth,
+ candidate.centerX, candidate.bottomY, (double)candidate.z);
+ }
+
+ const Entity *blocker = nullptr;
+ bool moved = tryApplyPlayerWalkMovement(movementContext, candidate,
+ direction.horizontal, direction.depth, &blocker);
+ if (!moved)
+ return stopMoveTargetStep(movementContext, candidate, direction.horizontal, direction.depth, blocker);
+
+ const int actualFacing = desiredFacing;
+ playerState.walkHorizontalDirection = direction.horizontal;
+ playerState.walkDepthDirection = direction.depth;
+ (void)setPlayerWalkAnimation(playerState, actualFacing);
+ if (walking && currentFrame == range.walkLastFrame) {
+ playerState.entity->setCurrentFrame(resolvePlayerAnimationRange(actualFacing).walkFirstFrame);
+ debugC(2, kDebugPlayer,
+ "Harvester: player target walk bank restart facing=%d frame=%d render_z=%.2f direction=(%d,%d)",
+ actualFacing, playerState.entity->getCurrentFrame(), (double)computePlayerRenderZ(playerState),
+ direction.horizontal, direction.depth);
+ }
+ syncAnimationPlacement();
+ refreshPlayerCurrentBlocker(movementContext);
+ logAppliedMoveTargetStep(movementContext, previousCenterX, previousBottomY, actualFacing, moved);
+ return true;
+}
+
+bool Player::stopMoveTargetStep(const PlayerMovement::MovementContext &context,
+ const PlayerMovement::MovementPosition &candidate, int horizontalDirection, int depthDirection,
+ const Entity *blocker) {
+ HarvesterEngine &engine = context.engine;
+ const RoomSetupState &state = context.state;
+ RoomPlayerState &playerState = _state;
+ if (engine.isPathfindingDebugEnabled()) {
+ const Common::Rect blockerRect = blocker ? blocker->getScreenRect() : Common::Rect();
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding blocked room='%s' candidate=(%d,%d,z=%.2f) blocker='%s' class=0x%x rect=(%d,%d)-(%d,%d) z=%.2f z_extent=%.2f",
+ state.roomName.c_str(), candidate.centerX, candidate.bottomY, (double)candidate.z,
+ blocker ? blocker->getName().c_str() : "",
+ blocker ? blocker->getClassId() : -1,
+ blockerRect.left, blockerRect.top, blockerRect.right, blockerRect.bottom,
+ blocker ? (double)blocker->getZ() : 0.0,
+ blocker ? (double)blocker->getZExtent() : 0.0);
+ }
+ setPlayerCurrentBlocker(playerState, blocker);
+ if (blocker) {
+ return setIdleAnimation(resolveFacingFromMoveDirection(horizontalDirection, depthDirection, playerState.facing));
+ }
+
+ playerState.hasMoveTarget = false;
+ int idleFacing = playerState.facing;
+ if (idleFacing < 0) {
+ if (horizontalDirection != 0)
+ idleFacing = horizontalDirection < 0 ? 1 : 2;
+ else
+ idleFacing = depthDirection < 0 ? 3 : 0;
+ }
+ return setIdleAnimation(idleFacing);
+}
+
+} // End of namespace Harvester
Commit: 3d8586a32d3e47ebb48933e150c3f7c0890caff3
https://github.com/scummvm/scummvm/commit/3d8586a32d3e47ebb48933e150c3f7c0890caff3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate player keyboard walking
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/player_keyboard.cpp
engines/harvester/module.mk
engines/harvester/player.cpp
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 80bfb1457be..608e4ea3127 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -22,6 +22,7 @@ MODULE_OBJS = \
metaengine.o \
monster.o \
player.o \
+ player_keyboard.o \
player_movement.o \
player_target.o \
resources.o \
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 89d692bc8de..1238f3aec50 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -1317,114 +1317,6 @@ bool Player::updateTurnAnimationState() {
return true;
}
-bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
- bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || !supportsMovementBand(state))
- return false;
- if (playerState.turnActive || playerState.hitActive || playerState.evasiveState >= 0)
- return false;
-
- const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
- 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() == playerState.animationRate &&
- 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.facing);
- }
-
- const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
- const float depthStep = computeRoomPlayerDepthStep(state);
- int candidateCenterX = clampPlayerCenterXToNativeBounds(
- playerState, playerState.centerX + horizontalInput * horizontalStep);
- const int candidateBottomY = playerState.bottomY;
- float candidateZ = playerState.z;
- bool depthBoundaryReached = false;
- if (verticalInput != 0) {
-
- depthBoundaryReached = verticalInput < 0
- ? computePlayerRenderZ(playerState) >= state.roomMaxZ
- : computePlayerRenderZ(playerState) <= state.roomMinZ;
- if (!depthBoundaryReached) {
- candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - verticalInput * depthStep);
- }
- }
- if (candidateCenterX == playerState.centerX &&
- candidateBottomY == playerState.bottomY &&
- fabsf(candidateZ - playerState.z) <= kRoomDepthCompareEpsilon) {
- playerState.hasMoveTarget = false;
- if (depthBoundaryReached) {
- const bool stopped = setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
- if (stopped) {
- debugC(3, kDebugPathfinding,
- "Harvester: player keyboard depth boundary room='%s' input_y=%d pos=(%d,%d,z=%.2f) bounds=(%d,%d)",
- state.roomName.c_str(), verticalInput,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- state.roomMinZ, state.roomMaxZ);
- }
- return stopped;
- }
- return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
- }
-
- const int desiredFacing = resolveFacingFromMoveDirection(
- horizontalInput, verticalInput, playerState.facing);
- if (desiredFacing != playerState.facing && startTurnAnimation(desiredFacing)) {
- playerState.keyboardWalkRequest = -1;
- return true;
- }
-
- if (!consumePlayerMovementTick(playerState))
- return false;
-
- playerState.hasMoveTarget = false;
- bool moved = tryApplyPlayerWalkMovement(movementContext,
- { candidateCenterX, candidateBottomY, candidateZ },
- horizontalInput, verticalInput);
- if (!moved)
- return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
-
- const int actualFacing = desiredFacing;
- playerState.walkHorizontalDirection = actualFacing == 1 ? -1 : actualFacing == 2 ? 1 : 0;
- playerState.walkDepthDirection = actualFacing == 3 ? -1 : actualFacing == 0 ? 1 : 0;
- (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());
- }
- syncAnimationPlacement();
- 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),
- (moveDown ? 1 : 0) - (moveUp ? 1 : 0), horizontalInput, verticalInput,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerState.facing, playerState.entity->getCurrentFrame(), moved);
- return true;
-}
-
bool Player::requestIdleAnimationExit(const RoomSetupState &state) {
RoomPlayerState &playerState = _state;
RoomIdleAnimationState &idleState = _idleState;
diff --git a/engines/harvester/player_keyboard.cpp b/engines/harvester/player_keyboard.cpp
new file mode 100644
index 00000000000..47ee1b7bd23
--- /dev/null
+++ b/engines/harvester/player_keyboard.cpp
@@ -0,0 +1,167 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player.h"
+#include "harvester/player_movement.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+
+namespace Harvester {
+
+using namespace PlayerMovement;
+
+namespace {
+
+static bool resolveKeyboardWalkInput(RoomPlayerState &playerState, const PlayerAnimationRange &range,
+ int currentFrame, bool keyboardWalking, int &horizontalInput, int &verticalInput) {
+ 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;
+ }
+ return true;
+}
+
+struct KeyboardWalkStep {
+ KeyboardWalkStep(const MovementPosition &candidate, bool depthBoundaryReached)
+ : candidate(candidate), depthBoundaryReached(depthBoundaryReached) {}
+
+ MovementPosition candidate;
+ bool depthBoundaryReached;
+};
+
+static KeyboardWalkStep computeKeyboardWalkStep(const RoomSetupState &state, const RoomPlayerState &playerState,
+ int horizontalInput, int verticalInput) {
+ const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
+ const float depthStep = computeRoomPlayerDepthStep(state);
+ int candidateCenterX = clampPlayerCenterXToNativeBounds(
+ playerState, playerState.centerX + horizontalInput * horizontalStep);
+ const int candidateBottomY = playerState.bottomY;
+ float candidateZ = playerState.z;
+ bool depthBoundaryReached = false;
+ if (verticalInput != 0) {
+
+ depthBoundaryReached = verticalInput < 0
+ ? computePlayerRenderZ(playerState) >= state.roomMaxZ
+ : computePlayerRenderZ(playerState) <= state.roomMinZ;
+ if (!depthBoundaryReached) {
+ candidateZ = clampPlayerMovementDepth(state, playerState, playerState.z - verticalInput * depthStep);
+ }
+ }
+ return { { candidateCenterX, candidateBottomY, candidateZ }, depthBoundaryReached };
+}
+
+} // End of anonymous namespace
+
+bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState &state,
+ const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations,
+ bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || !supportsMovementBand(state))
+ return false;
+ if (playerState.turnActive || playerState.hitActive || playerState.evasiveState >= 0)
+ return false;
+
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
+ 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() == playerState.animationRate &&
+ currentFrame >= range.walkFirstFrame && currentFrame <= range.walkLastFrame;
+ if (!resolveKeyboardWalkInput(playerState, range, currentFrame, keyboardWalking, horizontalInput, verticalInput))
+ return false;
+ if (horizontalInput == 0 && verticalInput == 0) {
+ if (playerState.hasMoveTarget || playerState.facing < 0)
+ return false;
+ return setIdleAnimation(playerState.facing);
+ }
+
+ const KeyboardWalkStep step = computeKeyboardWalkStep(state, playerState, horizontalInput, verticalInput);
+ if (step.candidate.centerX == playerState.centerX &&
+ step.candidate.bottomY == playerState.bottomY &&
+ fabsf(step.candidate.z - playerState.z) <= kRoomDepthCompareEpsilon) {
+ playerState.hasMoveTarget = false;
+ if (step.depthBoundaryReached) {
+ const bool stopped = setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
+ if (stopped) {
+ debugC(3, kDebugPathfinding,
+ "Harvester: player keyboard depth boundary room='%s' input_y=%d pos=(%d,%d,z=%.2f) bounds=(%d,%d)",
+ state.roomName.c_str(), verticalInput,
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ state.roomMinZ, state.roomMaxZ);
+ }
+ return stopped;
+ }
+ return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
+ }
+
+ const int desiredFacing = resolveFacingFromMoveDirection(
+ horizontalInput, verticalInput, playerState.facing);
+ if (desiredFacing != playerState.facing && startTurnAnimation(desiredFacing)) {
+ playerState.keyboardWalkRequest = -1;
+ return true;
+ }
+
+ if (!consumePlayerMovementTick(playerState))
+ return false;
+
+ playerState.hasMoveTarget = false;
+ bool moved = tryApplyPlayerWalkMovement(movementContext,
+ step.candidate,
+ horizontalInput, verticalInput);
+ if (!moved)
+ return setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
+
+ const int actualFacing = desiredFacing;
+ playerState.walkHorizontalDirection = actualFacing == 1 ? -1 : actualFacing == 2 ? 1 : 0;
+ playerState.walkDepthDirection = actualFacing == 3 ? -1 : actualFacing == 0 ? 1 : 0;
+ (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());
+ }
+ syncAnimationPlacement();
+ 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),
+ (moveDown ? 1 : 0) - (moveUp ? 1 : 0), horizontalInput, verticalInput,
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerState.facing, playerState.entity->getCurrentFrame(), moved);
+ return true;
+}
+
+} // End of namespace Harvester
Commit: 19a46f3107d80d6d864ca4967a32e5cc55175b2d
https://github.com/scummvm/scummvm/commit/19a46f3107d80d6d864ca4967a32e5cc55175b2d
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate player combat animation
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/player_animation.cpp
A engines/harvester/player_attack.cpp
A engines/harvester/player_reactions.cpp
engines/harvester/module.mk
engines/harvester/player.cpp
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 608e4ea3127..40f096ed98d 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -22,8 +22,11 @@ MODULE_OBJS = \
metaengine.o \
monster.o \
player.o \
+ player_animation.o \
+ player_attack.o \
player_keyboard.o \
player_movement.o \
+ player_reactions.o \
player_target.o \
resources.o \
combat.o \
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 1238f3aec50..7aabf1295d5 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -39,441 +39,15 @@ namespace {
static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing);
static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState);
-static const char *const kPlayerActorResourcePath = "1:/GRAPHIC/MONSTERS/PC/PC0.ABM";
-static const int kRoomPlayerWalkAnimationRate = 17;
-static const int kRoomPlayerFastAnimationRate = 30;
-static const int kRoomPlayerHitAnimationRate = 4;
-static const int kNativePlayerDeathEntryAnimationRate = 14;
-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 kRoomPlayerIdleDelayTicks = 3000;
-static const int kPlayerHitKnockbackDistance = 18;
-static const int kPlayerHitKnockbackDecayStep = 3;
static const int kRoomPlayerIdleAnimationRate = 14;
static const int kRoomPlayerIdleLoopStartFrame = 0x0f;
static const int kRoomPlayerIdleLoopLastFrame = 0xb2;
static const int kRoomPlayerIdleExitLastFrame = 0xbf;
static const int kRoomPlayerIdleYOffset = 4;
-struct PlayerTurnAnimationRange {
- PlayerTurnAnimationRange() {}
- PlayerTurnAnimationRange(int firstFrame, int lastFrame, bool playBackwards)
- : firstFrame(firstFrame), lastFrame(lastFrame), playBackwards(playBackwards) {}
-
- int firstFrame = 0;
- int lastFrame = 0;
- bool playBackwards = false;
-};
-
-struct PlayerAttackAnimationRange {
- PlayerAttackAnimationRange() {}
- PlayerAttackAnimationRange(int firstFrame, int lastFrame, int resumeFacing)
- : firstFrame(firstFrame), lastFrame(lastFrame), resumeFacing(resumeFacing) {}
-
- int firstFrame = 0;
- int lastFrame = 0;
- int resumeFacing = 0;
-};
-
-struct PlayerDeathAnimationRange {
- PlayerDeathAnimationRange() {}
- PlayerDeathAnimationRange(int firstFrame, int lastFrame)
- : firstFrame(firstFrame), lastFrame(lastFrame) {}
-
- int firstFrame = -1;
- int lastFrame = -1;
-};
-
-struct PlayerHitAnimationRange {
- PlayerHitAnimationRange() {}
- PlayerHitAnimationRange(int firstFrame, int lastFrame, int resumeFacing, int knockbackX)
- : firstFrame(firstFrame), lastFrame(lastFrame), resumeFacing(resumeFacing), knockbackX(knockbackX) {}
-
- int firstFrame = -1;
- int lastFrame = -1;
- int resumeFacing = -1;
- int knockbackX = 0;
-};
-
-struct PlayerAttackSoundSet {
- uint soundCount;
- const char *soundPaths[3];
-};
-
-struct PlayerCombatLoadoutTuning {
- const char *label;
- int damageType;
- int damageAmount;
- int contactFrameOffset;
-};
-
-static const int kPlayerAttackSoundTriggerFrameOffset = 1;
-static const PlayerAttackSoundSet kPlayerAttackSoundSets[] = {
- { 3, { "2:/sound/effects/swoosh1.wav", "2:/sound/effects/swoosh2.wav", "2:/sound/effects/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 1, { "2:/SOUND/EFFECTS/NAILGUN1.wav", nullptr, nullptr } },
- { 1, { "1:/sound/effects/shotgun2.wav", nullptr, nullptr } },
- { 1, { "2:/SOUND/EFFECTS/gunshot.wav", nullptr, nullptr } },
- { 1, { "2:/SOUND/EFFECTS/gunshot.wav", nullptr, nullptr } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/sound/effects/swoosh1.wav", "2:/sound/effects/swoosh2.wav", "2:/sound/effects/swoosh3.wav" } },
- { 1, { "1:/sound/effects/CLN_atk0.wav", nullptr, nullptr } },
- { 3, { "2:/SOUND/EFFECTS/swoosh3.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
- { 3, { "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } }
-};
-
-static const char *const kPlayerHitSoundPaths[] = {
- "1:/sound/effects/PC0_hit0.wav",
- "1:/sound/effects/PC0_hit1.wav",
- "1:/sound/effects/PC0_hit2.wav"
-};
-
-static const PlayerCombatLoadoutTuning kPlayerCombatLoadoutTunings[] = {
- { "FISTS", 1, 1, 5 },
- { "CLEAVER", 2, 4, 2 },
- { "NAILGUN", 4, 2, 2 },
- { "SHOTGUN", 4, 7, 2 },
- { "9GUN", 4, 6, 2 },
- { "38GUN", 4, 6, 2 },
- { "TOMAHAWK", 2, 3, 2 },
- { "KNIFE", 2, 3, 2 },
- { "FLAIL", 2, 3, 2 },
- { "HANDAXE", 2, 3, 2 },
- { "WRENCH", 1, 3, 2 },
- { "PITCHFORK", 2, 3, 2 },
- { "SCYTHE", 2, 4, 2 },
- { "SWORD", 2, 5, 2 },
- { "CHAINSAW", 2, 8, 2 },
- { "HARVEST_BLADE", 2, 3, 2 },
- { "SHOVEL", 1, 3, 2 },
- { "FIREAXE", 2, 3, 2 },
- { "BAT", 1, 3, 2 },
- { "RAZOR", 2, 3, 2 },
- { "POOLSTICK", 1, 2, 2 }
-};
-
-static bool resolvePlayerTurnAnimationRange(int fromFacing, int toFacing, PlayerTurnAnimationRange &range) {
- // Native desired-state transitions play these banks forward or backward depending on the source facing.
- if (fromFacing == 0 && toFacing == 1) {
- range = PlayerTurnAnimationRange{ 0x0a, 0x0e, false };
- return true;
- }
- if (fromFacing == 1 && toFacing == 0) {
- range = PlayerTurnAnimationRange{ 0x0a, 0x0e, true };
- return true;
- }
- if (fromFacing == 1 && toFacing == 3) {
- range = PlayerTurnAnimationRange{ 0x19, 0x1d, false };
- return true;
- }
- if (fromFacing == 3 && toFacing == 1) {
- range = PlayerTurnAnimationRange{ 0x19, 0x1d, true };
- return true;
- }
- if (fromFacing == 3 && toFacing == 2) {
- range = PlayerTurnAnimationRange{ 0x28, 0x2c, false };
- return true;
- }
- if (fromFacing == 2 && toFacing == 3) {
- range = PlayerTurnAnimationRange{ 0x28, 0x2c, true };
- return true;
- }
- if (fromFacing == 2 && toFacing == 0) {
- range = PlayerTurnAnimationRange{ 0x37, 0x3b, false };
- return true;
- }
- if (fromFacing == 0 && toFacing == 2) {
- range = PlayerTurnAnimationRange{ 0x37, 0x3b, true };
- return true;
- }
-
- return false;
-}
-
-static const PlayerAttackSoundSet *resolvePlayerAttackSoundSet(int loadout) {
- if (loadout < 0 || loadout >= (int)ARRAYSIZE(kPlayerAttackSoundSets))
- return nullptr;
-
- if (kPlayerAttackSoundSets[loadout].soundCount == 0)
- return nullptr;
-
- return &kPlayerAttackSoundSets[loadout];
-}
-
-static const PlayerCombatLoadoutTuning &resolvePlayerCombatLoadoutTuning(int loadout) {
- static const PlayerCombatLoadoutTuning kDefaultTuning = { "FISTS", 1, 1, 5 };
-
- if (loadout < 0 || loadout >= (int)ARRAYSIZE(kPlayerCombatLoadoutTunings))
- return kDefaultTuning;
-
- return kPlayerCombatLoadoutTunings[loadout];
-}
-
-static bool playPlayerAttackSound(HarvesterEngine &engine, int loadout) {
- const PlayerAttackSoundSet *soundSet = resolvePlayerAttackSoundSet(loadout);
- if (!soundSet)
- return false;
-
- uint soundIndex = 0;
- if (soundSet->soundCount > 1)
- soundIndex = engine.getRandomNumber(soundSet->soundCount - 1);
-
- const char *soundPath = soundSet->soundPaths[soundIndex];
- return soundPath && engine.playSound(soundPath);
-}
-
-static bool loadoutUsesConsumableCombatResource(int loadout) {
- switch (loadout) {
- case 2:
- case 3:
- case 4:
- case 5:
- case 14:
- return true;
- default:
- return false;
- }
-}
-
-static bool playPlayerHitSound(HarvesterEngine &engine) {
- const uint soundIndex = ARRAYSIZE(kPlayerHitSoundPaths) > 1
- ? engine.getRandomNumber(ARRAYSIZE(kPlayerHitSoundPaths) - 1)
- : 0;
- return engine.playSound(kPlayerHitSoundPaths[soundIndex]);
-}
-
-static bool playerEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFrame) {
- if (!entity.hasFrames() || firstFrame < 0 || lastFrame < firstFrame)
- return false;
-
- return (uint)lastFrame < entity.getFrameCount();
-}
-
-static bool choosePlayerDeathAnimationRange(const Entity &entity, int firstFrame, int lastFrame,
- PlayerDeathAnimationRange &range) {
- if (!playerEntityHasFrameRange(entity, firstFrame, lastFrame))
- return false;
-
- range = PlayerDeathAnimationRange(firstFrame, lastFrame);
- return true;
-}
-
-static bool resolvePlayerDeathAnimationRange(const RoomPlayerState &playerState,
- int damageType, bool goreEnabled, PlayerDeathAnimationRange &range) {
- if (!playerState.entity)
- return false;
-
- enum PlayerDeathBankFamily {
- kPlayerDeathBankHorizontalRight,
- kPlayerDeathBankHorizontalLeft,
- kPlayerDeathBankVerticalDown,
- kPlayerDeathBankVerticalUp
- };
-
- PlayerDeathBankFamily family = kPlayerDeathBankHorizontalLeft;
- if (playerState.attackActive) {
- if (playerState.attackFirstFrame >= 0x6e && playerState.attackFirstFrame <= 0x8b)
- family = kPlayerDeathBankHorizontalRight;
- else
- family = kPlayerDeathBankHorizontalLeft;
- } else if (playerState.turnActive) {
- if (playerState.turnFirstFrame >= 0x28)
- family = kPlayerDeathBankHorizontalRight;
- else
- family = kPlayerDeathBankHorizontalLeft;
- } else {
- switch (playerState.facing) {
- case 0:
- family = kPlayerDeathBankVerticalDown;
- break;
- case 2:
- family = kPlayerDeathBankHorizontalRight;
- break;
- case 3:
- family = kPlayerDeathBankVerticalUp;
- break;
- case 1:
- default:
- family = kPlayerDeathBankHorizontalLeft;
- break;
- }
- }
-
- const auto selectByFamily = [&](int bludgeFirst, int bludgeLast, int slashFirst, int slashLast,
- int projectileFirst, int projectileLast) {
- if ((!goreEnabled || damageType == 1) &&
- choosePlayerDeathAnimationRange(*playerState.entity, bludgeFirst, bludgeLast, range))
- return true;
- if (damageType == 4 &&
- choosePlayerDeathAnimationRange(*playerState.entity, projectileFirst, projectileLast, range))
- return true;
- if (damageType == 2 &&
- choosePlayerDeathAnimationRange(*playerState.entity, slashFirst, slashLast, range))
- return true;
-
- return choosePlayerDeathAnimationRange(*playerState.entity, bludgeFirst, bludgeLast, range) ||
- choosePlayerDeathAnimationRange(*playerState.entity, slashFirst, slashLast, range) ||
- choosePlayerDeathAnimationRange(*playerState.entity, projectileFirst, projectileLast, range);
- };
-
- switch (family) {
- case kPlayerDeathBankHorizontalRight:
- return selectByFamily(0xb0, 0xb9, 0x100, 0x109, 0x114, 0x11d);
- case kPlayerDeathBankVerticalDown:
- return selectByFamily(0xc4, 0xcd, 0xd8, 0xe1, 0xec, 0xf5);
- case kPlayerDeathBankVerticalUp:
- return selectByFamily(0xce, 0xd7, 0xe2, 0xeb, 0xf6, 0xff);
- case kPlayerDeathBankHorizontalLeft:
- default:
- return selectByFamily(0xba, 0xc3, 0x10a, 0x113, 0x11e, 0x127);
- }
-}
-
-static bool resolvePlayerAttackAnimationRange(const RoomSetupState &state,
- const RoomPlayerState &playerState, const Common::Point &mousePos,
- PlayerAttackAnimationRange &range) {
- if (!playerState.entity)
- return false;
-
- const float depthScale = Player::computeDepthScale(state, playerState.z);
- const float upperThresholdY =
- (float)playerState.bottomY - depthScale * kRoomPlayerAttackUpperYOffset;
- const float midThresholdY =
- (float)playerState.bottomY - depthScale * kRoomPlayerAttackMidYOffset;
-
- if (mousePos.x < playerState.centerX) {
- if ((float)mousePos.y < upperThresholdY)
- range = PlayerAttackAnimationRange(0x50, 0x59, 1);
- else if ((float)mousePos.y < midThresholdY)
- range = PlayerAttackAnimationRange(0x5a, 0x63, 1);
- else
- range = PlayerAttackAnimationRange(0x64, 0x6d, 1);
- } else {
- if ((float)mousePos.y < upperThresholdY)
- range = PlayerAttackAnimationRange(0x6e, 0x77, 2);
- else if ((float)mousePos.y < midThresholdY)
- range = PlayerAttackAnimationRange(0x78, 0x81, 2);
- else
- range = PlayerAttackAnimationRange(0x82, 0x8b, 2);
- }
-
- return true;
-}
-
-static bool resolveKeyboardAttackAnimationRange(const RoomPlayerState &playerState,
- bool attackLeft, bool attackRight, bool attackUp, bool attackDown,
- PlayerAttackAnimationRange &range) {
- if (attackRight) {
- range = PlayerAttackAnimationRange(0x78, 0x81, 2);
- return true;
- }
- if (attackLeft) {
- range = PlayerAttackAnimationRange(0x5a, 0x63, 1);
- return true;
- }
- if (attackUp) {
- if (playerState.facing == 2) {
- range = PlayerAttackAnimationRange(0x6e, 0x77, 2);
- return true;
- }
- if (playerState.facing == 1) {
- range = PlayerAttackAnimationRange(0x50, 0x59, 1);
- return true;
- }
- return false;
- }
- if (attackDown) {
- if (playerState.facing == 2) {
- range = PlayerAttackAnimationRange(0x82, 0x8b, 2);
- return true;
- }
- if (playerState.facing == 1) {
- range = PlayerAttackAnimationRange(0x64, 0x6d, 1);
- return true;
- }
- return false;
- }
-
- return false;
-}
-
-static bool resolvePlayerHitAnimationRange(const Entity &entity,
- int monsterAttackFirstFrame, PlayerHitAnimationRange &range) {
- PlayerHitAnimationRange candidate;
- switch (monsterAttackFirstFrame) {
- case 0x64:
- candidate = PlayerHitAnimationRange(0x92, 0x94, 2, -kPlayerHitKnockbackDistance);
- break;
- case 0x5a:
- candidate = PlayerHitAnimationRange(0x8f, 0x91, 2, -kPlayerHitKnockbackDistance);
- break;
- case 0x50:
- candidate = PlayerHitAnimationRange(0x8c, 0x8e, 2, -kPlayerHitKnockbackDistance);
- break;
- case 0x82:
- candidate = PlayerHitAnimationRange(0x9b, 0x9d, 1, kPlayerHitKnockbackDistance);
- break;
- case 0x78:
- candidate = PlayerHitAnimationRange(0x98, 0x9a, 1, kPlayerHitKnockbackDistance);
- break;
- case 0x6e:
- candidate = PlayerHitAnimationRange(0x95, 0x97, 1, kPlayerHitKnockbackDistance);
- break;
- default:
- return false;
- }
-
- if (!playerEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
- return false;
-
- range = candidate;
- return true;
-}
-
-static bool startResolvedAttackAnimation(RoomPlayerState &playerState,
- const PlayerAttackAnimationRange &range) {
- if (!playerState.entity || playerState.attackActive || playerState.hitActive)
- return false;
-
- playerState.hasMoveTarget = false;
- playerState.evasiveState = -1;
- playerState.evasiveXStep = 0;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- playerState.turnFirstFrame = -1;
- playerState.turnLastFrame = -1;
- playerState.turnEndFrame = -1;
- playerState.turnPlayBackwards = false;
- playerState.attackActive = true;
- playerState.attackFirstFrame = range.firstFrame;
- playerState.attackLastFrame = range.lastFrame;
- playerState.attackContactFrame =
- range.firstFrame + Player::resolveCombatLoadoutContactFrameOffset(playerState.combatLoadout);
- playerState.attackResumeFacing = range.resumeFacing;
- playerState.attackSoundPlayed = resolvePlayerAttackSoundSet(playerState.combatLoadout) == nullptr;
- playerState.attackSoundPlaybackFrame = playerState.attackSoundPlayed
- ? -1
- : (range.firstFrame + kPlayerAttackSoundTriggerFrameOffset);
- playerState.attackContactResolved = false;
- playerState.nextMovementTick = 0;
- playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- playerState.entity->setAnimationRate(playerState.animationRate);
- playerState.entity->setCurrentFrame(range.firstFrame);
- return true;
-}
-
static void notePlayerIdleReset(RoomIdleAnimationState &idleState) {
idleState.resetTick = Player::getRuntimeClockTicks();
updatePlayerIdleTrigger(idleState);
@@ -602,51 +176,6 @@ void Player::cancelMovement() {
_state.turnTargetFacing = -1;
}
-void Player::cancelMovementForAttack() {
- cancelMovement();
- _state.turnFirstFrame = -1;
- _state.turnLastFrame = -1;
- _state.turnEndFrame = -1;
- _state.turnPlayBackwards = false;
-}
-
-bool Player::cancelAttackAnimation() {
- RoomPlayerState &playerState = _state;
- if (!playerState.attackActive)
- return false;
-
- playerState.attackActive = false;
- playerState.attackFirstFrame = -1;
- playerState.attackLastFrame = -1;
- playerState.attackContactFrame = -1;
- if (!playerState.entity)
- return false;
- playerState.attackSoundPlayed = false;
- playerState.attackSoundPlaybackFrame = -1;
- playerState.attackContactResolved = false;
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- const int resumeFacing = playerState.attackResumeFacing >= 0
- ? playerState.attackResumeFacing
- : (playerState.facing >= 0 ? playerState.facing : 0);
- playerState.attackResumeFacing = -1;
- return setIdleAnimation(resumeFacing);
-}
-
-void Player::clearAttackTarget() {
- _state.attackTargetName.clear();
- _state.attackTargetClassId = -1;
-}
-
-void Player::captureAttackTarget(const Common::String &name, int classId) {
- _state.attackTargetName = name;
- _state.attackTargetClassId = classId;
-}
-
-void Player::markAttackContactResolved() {
- _state.attackContactResolved = true;
-}
-
void Player::resetIdleState() {
RoomIdleAnimationState &idleState = _idleState;
idleState = RoomIdleAnimationState();
@@ -782,46 +311,6 @@ int Player::resolveFacingFrame(int facing) {
}
}
-Common::String Player::resolveCombatLoadoutResourcePath(int loadout) {
- if (loadout <= 0)
- return Common::String(kPlayerActorResourcePath);
-
- return Common::String::format("1:/GRAPHIC/MONSTERS/PC/PC%02d.ABM", loadout);
-}
-
-const char *Player::describeCombatLoadout(int loadout) {
- return resolvePlayerCombatLoadoutTuning(loadout).label;
-}
-
-int Player::resolveCombatLoadoutDamageAmount(int loadout) {
- return resolvePlayerCombatLoadoutTuning(loadout).damageAmount;
-}
-
-int Player::resolveCombatLoadoutDamageType(int loadout) {
- return resolvePlayerCombatLoadoutTuning(loadout).damageType;
-}
-
-const char *Player::describeCombatDamageType(int damageType) {
- switch (damageType) {
- case 1:
- return "BLUDGE";
- case 2:
- return "SLASH";
- case 4:
- return "PROJ";
- default:
- return "UNKNOWN";
- }
-}
-
-int Player::resolveCombatLoadoutContactFrameOffset(int loadout) {
- return resolvePlayerCombatLoadoutTuning(loadout).contactFrameOffset;
-}
-
-bool Player::isProjectileCombatLoadout(int loadout) {
- return resolveCombatLoadoutDamageType(loadout) == 4;
-}
-
bool Player::supportsMovementBand(const RoomSetupState &state) {
return state.roomMaxZScreenY >= 0 &&
state.roomMinZScreenY >= state.roomMaxZScreenY;
@@ -852,471 +341,10 @@ bool Player::isIdleAnimationExcludedRoom(const Common::String &roomName) {
}
-void Player::syncCombatLoadout(HarvesterEngine &engine, const RoomSetupState &state) {
- if (_state.entity) {
- (void)syncCombatLoadoutVisual(engine, state, engine.getScript()->getPlayerCombatLoadout());
- } else if (engine.getScript()) {
- _state.combatLoadout = engine.getScript()->getPlayerCombatLoadout();
- }
-}
-
-bool Player::syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state, int loadout) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity)
- return false;
-
- ResourceManager *resources = engine.getResources();
- if (!resources)
- return false;
-
- const int clampedLoadout = CLIP<int>(loadout, 0, 0x14);
- if (playerState.combatLoadout == clampedLoadout)
- return false;
-
- const Common::String resourcePath = resolveCombatLoadoutResourcePath(clampedLoadout);
- if (!playerState.entity->loadAbmResource(*resources, resourcePath))
- return false;
-
- playerState.combatLoadout = clampedLoadout;
- playerState.attackActive = false;
- playerState.attackFirstFrame = -1;
- playerState.attackLastFrame = -1;
- playerState.attackContactFrame = -1;
- playerState.attackResumeFacing = -1;
- playerState.attackSoundPlayed = false;
- playerState.attackSoundPlaybackFrame = -1;
- playerState.attackContactResolved = false;
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- playerState.hitActive = false;
- playerState.hitFirstFrame = -1;
- playerState.hitLastFrame = -1;
- playerState.hitResumeFacing = -1;
- playerState.hitKnockbackRemainingX = 0;
- playerState.hitKnockbackDecayStep = 0;
- (void)setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
- if (applyRoomActorPlacement(state, *playerState.entity,
- playerState.centerX, playerState.bottomY, playerState.z)) {
- reinsertPlayerSceneEntity(engine, playerState);
- }
- debugC(1, kDebugPlayer,
- "Harvester: player combat loadout visual loadout=%d weapon='%s' damage=%d damage_type='%s' contact=%d resource='%s' facing=%d pos=(%d,%d,z=%.2f)",
- playerState.combatLoadout, describeCombatLoadout(playerState.combatLoadout),
- resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
- describeCombatDamageType(resolveCombatLoadoutDamageType(playerState.combatLoadout)),
- resolveCombatLoadoutContactFrameOffset(playerState.combatLoadout), resourcePath.c_str(), playerState.facing,
- playerState.centerX, playerState.bottomY, (double)playerState.z);
- return true;
-}
-
bool Player::setIdleAnimation(int facing) {
return setPlayerIdleAnimation(_state, facing);
}
-bool Player::updateAnimationRate(bool speedUp) {
- RoomPlayerState &playerState = _state;
- const int rate = speedUp ? kRoomPlayerFastAnimationRate : kRoomPlayerWalkAnimationRate;
- if (playerState.animationRate == rate)
- return false;
- playerState.animationRate = rate;
- if (playerState.entity && !playerState.hitActive && !playerState.deathActive &&
- playerState.entity->getAnimationRate() != 0)
- playerState.entity->setAnimationRate(rate);
- debugC(2, kDebugPlayer, "Harvester: player actor rate=%d", rate);
- return true;
-}
-
-bool Player::startEvasiveAnimation(bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || playerState.attackActive || playerState.hitActive || playerState.deathActive ||
- playerState.evasiveState >= 0)
- return false;
- playerState.hasMoveTarget = false;
- playerState.keyboardWalkRequest = -1;
- if (playerState.turnActive)
- return false;
- const PlayerAnimationRange walk = resolvePlayerAnimationRange(playerState.facing);
- const int frame = playerState.entity->getCurrentFrame();
- if (playerState.entity->getAnimationRate() != 0 &&
- frame >= walk.walkFirstFrame && frame < walk.walkLastFrame)
- return false;
-
- // Right Shift branch:
- // vertical requests select duck/leap banks; horizontal requests can override them.
- int actorState = -1;
- if (playerState.facing == 1 || playerState.facing == 2) {
- if (moveUp)
- actorState = playerState.facing == 2 ? 0x12 : 0x13;
- else if (moveDown)
- actorState = playerState.facing == 2 ? 0x14 : 0x15;
- if (moveLeft) {
- if (playerState.facing == 2)
- actorState = 0x08;
- } else if (moveRight && playerState.facing == 1) {
- actorState = 0x0f;
- }
- }
- if (actorState < 0)
- return false;
-
- const bool backwards = actorState == 0x08 || actorState == 0x0f;
- const int firstFrame = actorState == 0x08 ? 0x2d : actorState == 0x0f ? 0x0f :
- 0x3c + (actorState - 0x12) * 5;
- const int lastFrame = firstFrame + (backwards ? 9 : 4);
- if (!playerEntityHasFrameRange(*playerState.entity, firstFrame, lastFrame))
- return false;
- playerState.evasiveState = actorState;
- playerState.evasiveFirstFrame = firstFrame;
- playerState.evasiveLastFrame = lastFrame;
- playerState.evasiveXStep = backwards
- ? (int)(playerState.walkHorizontalDirection * playerState.entity->getDepthScale() * 8.0f) : 0;
- playerState.walkHorizontalDirection = 0;
- playerState.walkDepthDirection = 0;
- playerState.nextMovementTick = 0;
- playerState.entity->setAnimationFrameRange(firstFrame, lastFrame, false);
- playerState.entity->setPlayBackwards(backwards);
- playerState.entity->setAnimationRate(playerState.animationRate);
- playerState.entity->setCurrentFrame(backwards ? lastFrame : firstFrame);
- debugC(2, kDebugPlayer, "Harvester: player evasive state=0x%x facing=%d frames=%d..%d backwards=%d",
- actorState, playerState.facing, firstFrame, lastFrame, backwards);
- return true;
-}
-
-bool Player::updateEvasiveAnimation(HarvesterEngine &engine) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || playerState.evasiveState < 0 ||
- !playerState.entity->didAnimationAdvanceLastTick())
- return false;
-
- Entity &entity = *playerState.entity;
- const int actorState = playerState.evasiveState;
- if (playerState.evasiveXStep != 0) {
- entity.setPosition(entity.getX() + playerState.evasiveXStep, entity.getY(), entity.getZ());
- syncAnimationPlacement();
- reinsertPlayerSceneEntity(engine, playerState);
- debugC(3, kDebugPlayer, "Harvester: player evasive step state=0x%x frame=%d dx=%d",
- actorState, entity.getCurrentFrame(), playerState.evasiveXStep);
- }
- const bool backwards = actorState == 0x08 || actorState == 0x0f;
- if (entity.getCurrentFrame() == (backwards ? playerState.evasiveFirstFrame : playerState.evasiveLastFrame)) {
- debugC(2, kDebugPlayer, "Harvester: player evasive complete state=0x%x facing=%d",
- actorState, playerState.facing);
- return setIdleAnimation(playerState.facing);
- }
-
- const int frameLeft = computePlayerFrameLeftX(playerState);
- if ((actorState == 0x08 && frameLeft > 20) || (actorState == 0x0f && frameLeft < 620)) {
- playerState.evasiveXStep = (int)(entity.getDepthScale() * (actorState == 0x08 ? -8.0f : 8.0f));
- } else if (entity.getCurrentFrame() > playerState.evasiveFirstFrame + 2 &&
- ((actorState == 0x12 && frameLeft > 36) || (actorState == 0x13 && frameLeft < 616))) {
- playerState.evasiveXStep = (int)(entity.getDepthScale() * (actorState == 0x12 ? -16.0f : 16.0f));
- }
- return true;
-}
-
-bool Player::startAttackAnimation(const RoomSetupState &state, const Common::Point &mousePos) {
- RoomPlayerState &playerState = _state;
- PlayerAttackAnimationRange range;
- if (!resolvePlayerAttackAnimationRange(state, playerState, mousePos, range))
- return false;
- if (!startResolvedAttackAnimation(playerState, range))
- return false;
- debugC(1, kDebugPlayer,
- "Harvester: player attack animation frames=%d..%d resume_facing=%d cursor=(%d,%d)",
- range.firstFrame, range.lastFrame, range.resumeFacing, mousePos.x, mousePos.y);
- return true;
-}
-
-bool Player::startKeyboardAttackAnimation(const RoomSetupState &state,
- bool attackLeft, bool attackRight, bool attackUp, bool attackDown) {
- RoomPlayerState &playerState = _state;
- (void)state;
-
- PlayerAttackAnimationRange range;
- if (!resolveKeyboardAttackAnimationRange(playerState, attackLeft, attackRight, attackUp, attackDown, range))
- return false;
- if (!startResolvedAttackAnimation(playerState, range))
- return false;
- debugC(1, kDebugPlayer,
- "Harvester: player keyboard attack animation frames=%d..%d resume_facing=%d input=(L=%d R=%d U=%d D=%d) facing=%d",
- range.firstFrame, range.lastFrame, range.resumeFacing,
- attackLeft, attackRight, attackUp, attackDown, playerState.facing);
- return true;
-}
-
-bool Player::updateAttackAnimationState(HarvesterEngine &engine) {
- RoomPlayerState &playerState = _state;
- if (!playerState.attackActive || !playerState.entity)
- return false;
- bool changed = false;
- if (!playerState.attackSoundPlayed &&
- playerState.attackSoundPlaybackFrame >= 0 &&
- playerState.entity->getCurrentFrame() >= playerState.attackSoundPlaybackFrame) {
- if (loadoutUsesConsumableCombatResource(playerState.combatLoadout)) {
- Script *script = engine.getScript();
- if (!script || !script->consumePlayerCombatResourceUnit(playerState.combatLoadout)) {
- debugC(1, kDebugCombat,
- "Harvester: combat player attack dry fire loadout=%d weapon='%s'",
- playerState.combatLoadout,
- describeCombatLoadout(playerState.combatLoadout));
- playerState.attackContactResolved = true;
- playerState.attackContactFrame = -1;
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- } else {
- changed = true;
- (void)playPlayerAttackSound(engine, playerState.combatLoadout);
- }
- } else {
- (void)playPlayerAttackSound(engine, playerState.combatLoadout);
- }
- playerState.attackSoundPlayed = true;
- }
- if (playerState.entity->getCurrentFrame() != playerState.attackLastFrame)
- return changed;
-
- const int resumeFacing = playerState.attackResumeFacing >= 0
- ? playerState.attackResumeFacing
- : (playerState.facing >= 0 ? playerState.facing : 0);
- playerState.attackActive = false;
- playerState.attackFirstFrame = -1;
- playerState.attackLastFrame = -1;
- playerState.attackContactFrame = -1;
- playerState.attackResumeFacing = -1;
- playerState.attackSoundPlayed = false;
- playerState.attackSoundPlaybackFrame = -1;
- playerState.attackContactResolved = false;
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- return setIdleAnimation(resumeFacing) || changed;
-}
-
-bool Player::startHitAnimation(HarvesterEngine &engine, int monsterAttackFirstFrame) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || playerState.deathActive)
- return false;
-
- PlayerHitAnimationRange range;
- if (!resolvePlayerHitAnimationRange(*playerState.entity, monsterAttackFirstFrame, range))
- return false;
-
- playerState.hasMoveTarget = false;
- playerState.evasiveState = -1;
- playerState.evasiveXStep = 0;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- playerState.turnFirstFrame = -1;
- playerState.turnLastFrame = -1;
- playerState.turnEndFrame = -1;
- playerState.turnPlayBackwards = false;
- playerState.attackActive = false;
- playerState.attackFirstFrame = -1;
- playerState.attackLastFrame = -1;
- playerState.attackContactFrame = -1;
- playerState.attackResumeFacing = -1;
- playerState.attackSoundPlayed = false;
- playerState.attackSoundPlaybackFrame = -1;
- playerState.attackContactResolved = false;
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- playerState.nextMovementTick = 0;
- playerState.hitActive = true;
- playerState.hitFirstFrame = range.firstFrame;
- playerState.hitLastFrame = range.lastFrame;
- playerState.hitResumeFacing = range.resumeFacing;
- playerState.hitKnockbackRemainingX = range.knockbackX;
- playerState.hitKnockbackDecayStep = kPlayerHitKnockbackDecayStep;
- playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- playerState.entity->setAnimationRate(kRoomPlayerHitAnimationRate);
- playerState.entity->setCurrentFrame(range.firstFrame);
- playerState.entity->setAnimationEnabled(true);
- playerState.entity->setVisible(true);
- (void)playPlayerHitSound(engine);
- debugC(1, kDebugCombat,
- "Harvester: player hit animation attack_frame=%d frames=%d..%d resume_facing=%d knockback=%d rate=%d",
- monsterAttackFirstFrame, range.firstFrame, range.lastFrame, range.resumeFacing,
- range.knockbackX, kRoomPlayerHitAnimationRate);
- return true;
-}
-
-bool Player::updateHitAnimationState(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations) {
- RoomPlayerState &playerState = _state;
- if (!playerState.hitActive || !playerState.entity)
- return false;
-
- const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
- bool changed = false;
- if (playerState.hitKnockbackRemainingX != 0) {
- const int step = MAX(1, ABS(playerState.hitKnockbackDecayStep));
- const int knockbackStep = playerState.hitKnockbackRemainingX > 0
- ? MIN(playerState.hitKnockbackRemainingX, step)
- : MAX(playerState.hitKnockbackRemainingX, -step);
- if (knockbackStep != 0) {
- if (!tryApplyPlayerMovement(movementContext,
- { playerState.centerX + knockbackStep, playerState.bottomY, playerState.z })) {
- playerState.hitKnockbackRemainingX = 0;
- } else {
- changed = true;
- playerState.hitKnockbackRemainingX -= knockbackStep;
- }
- }
- }
-
- if (playerState.entity->getCurrentFrame() != playerState.hitLastFrame)
- return changed;
-
- const int resumeFacing = playerState.hitResumeFacing >= 0
- ? playerState.hitResumeFacing
- : (playerState.facing >= 0 ? playerState.facing : 0);
- playerState.hitActive = false;
- playerState.hitFirstFrame = -1;
- playerState.hitLastFrame = -1;
- playerState.hitResumeFacing = -1;
- playerState.hitKnockbackRemainingX = 0;
- playerState.hitKnockbackDecayStep = 0;
- return setIdleAnimation(resumeFacing) || changed;
-}
-
-bool Player::startDeathAnimation(int damageType, bool goreEnabled) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || playerState.deathActive)
- return false;
-
- PlayerDeathAnimationRange range;
- if (!resolvePlayerDeathAnimationRange(playerState, damageType, goreEnabled, range))
- return false;
-
- playerState.hasMoveTarget = false;
- playerState.evasiveState = -1;
- playerState.evasiveXStep = 0;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- playerState.turnFirstFrame = -1;
- playerState.turnLastFrame = -1;
- playerState.turnEndFrame = -1;
- playerState.turnPlayBackwards = false;
- playerState.attackActive = false;
- playerState.attackFirstFrame = -1;
- playerState.attackLastFrame = -1;
- playerState.attackContactFrame = -1;
- playerState.attackResumeFacing = -1;
- playerState.attackSoundPlayed = false;
- playerState.attackSoundPlaybackFrame = -1;
- playerState.attackContactResolved = false;
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- playerState.hitActive = false;
- playerState.hitFirstFrame = -1;
- playerState.hitLastFrame = -1;
- playerState.hitResumeFacing = -1;
- playerState.hitKnockbackRemainingX = 0;
- playerState.hitKnockbackDecayStep = 0;
- playerState.nextMovementTick = 0;
- playerState.deathActive = true;
- playerState.deathFirstFrame = range.firstFrame;
- playerState.deathLastFrame = range.lastFrame;
- playerState.deathDamageType = damageType;
- playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- playerState.entity->setAnimationRate(kNativePlayerDeathEntryAnimationRate);
- playerState.entity->setCurrentFrame(range.firstFrame);
- playerState.entity->setVisible(true);
- debugC(2, kDebugCombat,
- "Harvester: player death animation start damage_type=%d gore=%d facing=%d frames=%d..%d",
- damageType, goreEnabled, playerState.facing, range.firstFrame, range.lastFrame);
- debugC(3, kDebugCombat,
- "Harvester: player death animation timing frames=%d..%d entry_rate=%d entry_interval_ticks=%d running_rate=%d running_interval_ticks=%d",
- range.firstFrame, range.lastFrame,
- kNativePlayerDeathEntryAnimationRate, 100 / kNativePlayerDeathEntryAnimationRate,
- playerState.animationRate, 100 / playerState.animationRate);
- return true;
-}
-
-bool Player::updateDeathAnimationState() {
- RoomPlayerState &playerState = _state;
- if (!playerState.deathActive || !playerState.entity)
- return false;
-
- const int currentFrame = playerState.entity->getCurrentFrame();
- if (currentFrame > playerState.deathFirstFrame &&
- currentFrame < playerState.deathLastFrame &&
- playerState.entity->getAnimationRate() != playerState.animationRate) {
- playerState.entity->setAnimationRate(playerState.animationRate);
- debugC(3, kDebugCombat,
- "Harvester: player death animation running frame=%d rate=%d interval_ticks=%d",
- currentFrame, playerState.animationRate,
- 100 / playerState.animationRate);
- }
-
- if (currentFrame < playerState.deathLastFrame)
- return false;
-
- playerState.deathActive = false;
- debugC(2, kDebugCombat,
- "Harvester: player death animation complete damage_type=%d frame=%d",
- playerState.deathDamageType, currentFrame);
- return true;
-}
-
-bool Player::startTurnAnimation(int targetFacing) {
- RoomPlayerState &playerState = _state;
- if (!playerState.entity || playerState.hitActive || playerState.evasiveState >= 0 ||
- playerState.facing < 0 || playerState.facing == targetFacing)
- return false;
-
- int nextFacing = targetFacing;
- if ((playerState.facing == 1 && targetFacing == 2) ||
- (playerState.facing == 2 && targetFacing == 1))
- nextFacing = 0;
- else if ((playerState.facing == 0 && targetFacing == 3) ||
- (playerState.facing == 3 && targetFacing == 0))
- nextFacing = 2;
- PlayerTurnAnimationRange range;
- if (playerState.facing == 3 && targetFacing == 0) {
- range = PlayerTurnAnimationRange{ 0x28, 0x28, false };
- } else if (!resolvePlayerTurnAnimationRange(playerState.facing, nextFacing, range)) {
- return false;
- }
-
- playerState.turnActive = true;
- playerState.turnTargetFacing = nextFacing;
- playerState.turnFirstFrame = range.firstFrame;
- playerState.turnLastFrame = range.lastFrame;
- playerState.turnEndFrame = range.playBackwards ? range.firstFrame : range.lastFrame;
- playerState.turnPlayBackwards = range.playBackwards;
- playerState.nextMovementTick = 0;
- playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
- playerState.entity->setPlayBackwards(range.playBackwards);
- playerState.entity->setAnimationRate(playerState.animationRate);
- 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 requested_facing=%d",
- playerState.facing, nextFacing, range.firstFrame, range.lastFrame,
- range.playBackwards, playerState.animationRate, targetFacing);
- return true;
-}
-
-bool Player::updateTurnAnimationState() {
- RoomPlayerState &playerState = _state;
- if (!playerState.turnActive || !playerState.entity)
- return false;
- if (playerState.entity->getCurrentFrame() != playerState.turnEndFrame)
- return false;
-
- playerState.turnActive = false;
- playerState.facing = playerState.turnTargetFacing;
- playerState.turnTargetFacing = -1;
- playerState.turnFirstFrame = -1;
- playerState.turnLastFrame = -1;
- playerState.turnEndFrame = -1;
- playerState.turnPlayBackwards = false;
- playerState.entity->setAnimationRate(0);
- debugC(1, kDebugPlayer,
- "Harvester: player turn complete facing=%d frame=%d",
- playerState.facing, playerState.entity->getCurrentFrame());
- return true;
-}
-
bool Player::requestIdleAnimationExit(const RoomSetupState &state) {
RoomPlayerState &playerState = _state;
RoomIdleAnimationState &idleState = _idleState;
diff --git a/engines/harvester/player_animation.cpp b/engines/harvester/player_animation.cpp
new file mode 100644
index 00000000000..d72323859fe
--- /dev/null
+++ b/engines/harvester/player_animation.cpp
@@ -0,0 +1,251 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player.h"
+#include "harvester/player_movement.h"
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+
+namespace Harvester {
+
+using namespace PlayerMovement;
+
+namespace {
+
+static const int kRoomPlayerWalkAnimationRate = 17;
+static const int kRoomPlayerFastAnimationRate = 30;
+
+struct PlayerTurnAnimationRange {
+ PlayerTurnAnimationRange() {}
+ PlayerTurnAnimationRange(int firstFrame, int lastFrame, bool playBackwards)
+ : firstFrame(firstFrame), lastFrame(lastFrame), playBackwards(playBackwards) {}
+
+ int firstFrame = 0;
+ int lastFrame = 0;
+ bool playBackwards = false;
+};
+
+static bool resolvePlayerTurnAnimationRange(int fromFacing, int toFacing, PlayerTurnAnimationRange &range) {
+ // Desired-state transitions play these banks forward or backward depending on the source facing.
+ if (fromFacing == 0 && toFacing == 1) {
+ range = PlayerTurnAnimationRange{ 0x0a, 0x0e, false };
+ return true;
+ }
+ if (fromFacing == 1 && toFacing == 0) {
+ range = PlayerTurnAnimationRange{ 0x0a, 0x0e, true };
+ return true;
+ }
+ if (fromFacing == 1 && toFacing == 3) {
+ range = PlayerTurnAnimationRange{ 0x19, 0x1d, false };
+ return true;
+ }
+ if (fromFacing == 3 && toFacing == 1) {
+ range = PlayerTurnAnimationRange{ 0x19, 0x1d, true };
+ return true;
+ }
+ if (fromFacing == 3 && toFacing == 2) {
+ range = PlayerTurnAnimationRange{ 0x28, 0x2c, false };
+ return true;
+ }
+ if (fromFacing == 2 && toFacing == 3) {
+ range = PlayerTurnAnimationRange{ 0x28, 0x2c, true };
+ return true;
+ }
+ if (fromFacing == 2 && toFacing == 0) {
+ range = PlayerTurnAnimationRange{ 0x37, 0x3b, false };
+ return true;
+ }
+ if (fromFacing == 0 && toFacing == 2) {
+ range = PlayerTurnAnimationRange{ 0x37, 0x3b, true };
+ return true;
+ }
+
+ return false;
+}
+
+} // End of anonymous namespace
+
+bool Player::updateAnimationRate(bool speedUp) {
+ RoomPlayerState &playerState = _state;
+ const int rate = speedUp ? kRoomPlayerFastAnimationRate : kRoomPlayerWalkAnimationRate;
+ if (playerState.animationRate == rate)
+ return false;
+ playerState.animationRate = rate;
+ if (playerState.entity && !playerState.hitActive && !playerState.deathActive &&
+ playerState.entity->getAnimationRate() != 0)
+ playerState.entity->setAnimationRate(rate);
+ debugC(2, kDebugPlayer, "Harvester: player actor rate=%d", rate);
+ return true;
+}
+
+bool Player::startEvasiveAnimation(bool moveLeft, bool moveRight, bool moveUp, bool moveDown) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.attackActive || playerState.hitActive || playerState.deathActive ||
+ playerState.evasiveState >= 0)
+ return false;
+ playerState.hasMoveTarget = false;
+ playerState.keyboardWalkRequest = -1;
+ if (playerState.turnActive)
+ return false;
+ const PlayerAnimationRange walk = resolvePlayerAnimationRange(playerState.facing);
+ const int frame = playerState.entity->getCurrentFrame();
+ if (playerState.entity->getAnimationRate() != 0 &&
+ frame >= walk.walkFirstFrame && frame < walk.walkLastFrame)
+ return false;
+
+ // Right Shift branch:
+ // vertical requests select duck/leap banks; horizontal requests can override them.
+ int actorState = -1;
+ if (playerState.facing == 1 || playerState.facing == 2) {
+ if (moveUp)
+ actorState = playerState.facing == 2 ? 0x12 : 0x13;
+ else if (moveDown)
+ actorState = playerState.facing == 2 ? 0x14 : 0x15;
+ if (moveLeft) {
+ if (playerState.facing == 2)
+ actorState = 0x08;
+ } else if (moveRight && playerState.facing == 1) {
+ actorState = 0x0f;
+ }
+ }
+ if (actorState < 0)
+ return false;
+
+ const bool backwards = actorState == 0x08 || actorState == 0x0f;
+ const int firstFrame = actorState == 0x08 ? 0x2d : actorState == 0x0f ? 0x0f :
+ 0x3c + (actorState - 0x12) * 5;
+ const int lastFrame = firstFrame + (backwards ? 9 : 4);
+ if (!runtimeEntityHasFrameRange(*playerState.entity, firstFrame, lastFrame))
+ return false;
+ playerState.evasiveState = actorState;
+ playerState.evasiveFirstFrame = firstFrame;
+ playerState.evasiveLastFrame = lastFrame;
+ playerState.evasiveXStep = backwards
+ ? (int)(playerState.walkHorizontalDirection * playerState.entity->getDepthScale() * 8.0f) : 0;
+ playerState.walkHorizontalDirection = 0;
+ playerState.walkDepthDirection = 0;
+ playerState.nextMovementTick = 0;
+ playerState.entity->setAnimationFrameRange(firstFrame, lastFrame, false);
+ playerState.entity->setPlayBackwards(backwards);
+ playerState.entity->setAnimationRate(playerState.animationRate);
+ playerState.entity->setCurrentFrame(backwards ? lastFrame : firstFrame);
+ debugC(2, kDebugPlayer, "Harvester: player evasive state=0x%x facing=%d frames=%d..%d backwards=%d",
+ actorState, playerState.facing, firstFrame, lastFrame, backwards);
+ return true;
+}
+
+bool Player::updateEvasiveAnimation(HarvesterEngine &engine) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.evasiveState < 0 ||
+ !playerState.entity->didAnimationAdvanceLastTick())
+ return false;
+
+ Entity &entity = *playerState.entity;
+ const int actorState = playerState.evasiveState;
+ if (playerState.evasiveXStep != 0) {
+ entity.setPosition(entity.getX() + playerState.evasiveXStep, entity.getY(), entity.getZ());
+ syncAnimationPlacement();
+ reinsertPlayerSceneEntity(engine, playerState);
+ debugC(3, kDebugPlayer, "Harvester: player evasive step state=0x%x frame=%d dx=%d",
+ actorState, entity.getCurrentFrame(), playerState.evasiveXStep);
+ }
+ const bool backwards = actorState == 0x08 || actorState == 0x0f;
+ if (entity.getCurrentFrame() == (backwards ? playerState.evasiveFirstFrame : playerState.evasiveLastFrame)) {
+ debugC(2, kDebugPlayer, "Harvester: player evasive complete state=0x%x facing=%d",
+ actorState, playerState.facing);
+ return setIdleAnimation(playerState.facing);
+ }
+
+ const int frameLeft = computePlayerFrameLeftX(playerState);
+ if ((actorState == 0x08 && frameLeft > 20) || (actorState == 0x0f && frameLeft < 620)) {
+ playerState.evasiveXStep = (int)(entity.getDepthScale() * (actorState == 0x08 ? -8.0f : 8.0f));
+ } else if (entity.getCurrentFrame() > playerState.evasiveFirstFrame + 2 &&
+ ((actorState == 0x12 && frameLeft > 36) || (actorState == 0x13 && frameLeft < 616))) {
+ playerState.evasiveXStep = (int)(entity.getDepthScale() * (actorState == 0x12 ? -16.0f : 16.0f));
+ }
+ return true;
+}
+
+bool Player::startTurnAnimation(int targetFacing) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.hitActive || playerState.evasiveState >= 0 ||
+ playerState.facing < 0 || playerState.facing == targetFacing)
+ return false;
+
+ int nextFacing = targetFacing;
+ if ((playerState.facing == 1 && targetFacing == 2) ||
+ (playerState.facing == 2 && targetFacing == 1))
+ nextFacing = 0;
+ else if ((playerState.facing == 0 && targetFacing == 3) ||
+ (playerState.facing == 3 && targetFacing == 0))
+ nextFacing = 2;
+ PlayerTurnAnimationRange range;
+ if (playerState.facing == 3 && targetFacing == 0) {
+ range = PlayerTurnAnimationRange{ 0x28, 0x28, false };
+ } else if (!resolvePlayerTurnAnimationRange(playerState.facing, nextFacing, range)) {
+ return false;
+ }
+
+ playerState.turnActive = true;
+ playerState.turnTargetFacing = nextFacing;
+ playerState.turnFirstFrame = range.firstFrame;
+ playerState.turnLastFrame = range.lastFrame;
+ playerState.turnEndFrame = range.playBackwards ? range.firstFrame : range.lastFrame;
+ playerState.turnPlayBackwards = range.playBackwards;
+ playerState.nextMovementTick = 0;
+ playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
+ playerState.entity->setPlayBackwards(range.playBackwards);
+ playerState.entity->setAnimationRate(playerState.animationRate);
+ 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 requested_facing=%d",
+ playerState.facing, nextFacing, range.firstFrame, range.lastFrame,
+ range.playBackwards, playerState.animationRate, targetFacing);
+ return true;
+}
+
+bool Player::updateTurnAnimationState() {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.turnActive || !playerState.entity)
+ return false;
+ if (playerState.entity->getCurrentFrame() != playerState.turnEndFrame)
+ return false;
+
+ playerState.turnActive = false;
+ playerState.facing = playerState.turnTargetFacing;
+ playerState.turnTargetFacing = -1;
+ playerState.turnFirstFrame = -1;
+ playerState.turnLastFrame = -1;
+ playerState.turnEndFrame = -1;
+ playerState.turnPlayBackwards = false;
+ playerState.entity->setAnimationRate(0);
+ debugC(1, kDebugPlayer,
+ "Harvester: player turn complete facing=%d frame=%d",
+ playerState.facing, playerState.entity->getCurrentFrame());
+ return true;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/player_attack.cpp b/engines/harvester/player_attack.cpp
new file mode 100644
index 00000000000..e4698488be5
--- /dev/null
+++ b/engines/harvester/player_attack.cpp
@@ -0,0 +1,480 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player.h"
+#include "harvester/player_movement.h"
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+using namespace PlayerMovement;
+
+namespace {
+
+static const char *const kPlayerActorResourcePath = "1:/GRAPHIC/MONSTERS/PC/PC0.ABM";
+static const float kRoomPlayerAttackUpperYOffset = 144.44f;
+static const float kRoomPlayerAttackMidYOffset = 75.36f;
+static const int kPlayerAttackSoundTriggerFrameOffset = 1;
+
+struct PlayerAttackAnimationRange {
+ PlayerAttackAnimationRange() {}
+ PlayerAttackAnimationRange(int firstFrame, int lastFrame, int resumeFacing)
+ : firstFrame(firstFrame), lastFrame(lastFrame), resumeFacing(resumeFacing) {}
+
+ int firstFrame = 0;
+ int lastFrame = 0;
+ int resumeFacing = 0;
+};
+
+struct PlayerAttackSoundSet {
+ uint soundCount;
+ const char *soundPaths[3];
+};
+
+struct PlayerCombatLoadoutTuning {
+ const char *label;
+ int damageType;
+ int damageAmount;
+ int contactFrameOffset;
+};
+
+static const PlayerAttackSoundSet kPlayerAttackSoundSets[] = {
+ { 3, { "2:/sound/effects/swoosh1.wav", "2:/sound/effects/swoosh2.wav", "2:/sound/effects/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 1, { "2:/SOUND/EFFECTS/NAILGUN1.wav", nullptr, nullptr } },
+ { 1, { "1:/sound/effects/shotgun2.wav", nullptr, nullptr } },
+ { 1, { "2:/SOUND/EFFECTS/gunshot.wav", nullptr, nullptr } },
+ { 1, { "2:/SOUND/EFFECTS/gunshot.wav", nullptr, nullptr } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/sound/effects/swoosh1.wav", "2:/sound/effects/swoosh2.wav", "2:/sound/effects/swoosh3.wav" } },
+ { 1, { "1:/sound/effects/CLN_atk0.wav", nullptr, nullptr } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh3.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh1.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } },
+ { 3, { "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh2.wav", "2:/SOUND/EFFECTS/swoosh3.wav" } }
+};
+
+static const PlayerCombatLoadoutTuning kPlayerCombatLoadoutTunings[] = {
+ { "FISTS", 1, 1, 5 },
+ { "CLEAVER", 2, 4, 2 },
+ { "NAILGUN", 4, 2, 2 },
+ { "SHOTGUN", 4, 7, 2 },
+ { "9GUN", 4, 6, 2 },
+ { "38GUN", 4, 6, 2 },
+ { "TOMAHAWK", 2, 3, 2 },
+ { "KNIFE", 2, 3, 2 },
+ { "FLAIL", 2, 3, 2 },
+ { "HANDAXE", 2, 3, 2 },
+ { "WRENCH", 1, 3, 2 },
+ { "PITCHFORK", 2, 3, 2 },
+ { "SCYTHE", 2, 4, 2 },
+ { "SWORD", 2, 5, 2 },
+ { "CHAINSAW", 2, 8, 2 },
+ { "HARVEST_BLADE", 2, 3, 2 },
+ { "SHOVEL", 1, 3, 2 },
+ { "FIREAXE", 2, 3, 2 },
+ { "BAT", 1, 3, 2 },
+ { "RAZOR", 2, 3, 2 },
+ { "POOLSTICK", 1, 2, 2 }
+};
+
+static const PlayerAttackSoundSet *resolvePlayerAttackSoundSet(int loadout) {
+ if (loadout < 0 || loadout >= (int)ARRAYSIZE(kPlayerAttackSoundSets))
+ return nullptr;
+
+ if (kPlayerAttackSoundSets[loadout].soundCount == 0)
+ return nullptr;
+
+ return &kPlayerAttackSoundSets[loadout];
+}
+
+static const PlayerCombatLoadoutTuning &resolvePlayerCombatLoadoutTuning(int loadout) {
+ static const PlayerCombatLoadoutTuning kDefaultTuning = { "FISTS", 1, 1, 5 };
+
+ if (loadout < 0 || loadout >= (int)ARRAYSIZE(kPlayerCombatLoadoutTunings))
+ return kDefaultTuning;
+
+ return kPlayerCombatLoadoutTunings[loadout];
+}
+
+static bool playPlayerAttackSound(HarvesterEngine &engine, int loadout) {
+ const PlayerAttackSoundSet *soundSet = resolvePlayerAttackSoundSet(loadout);
+ if (!soundSet)
+ return false;
+
+ uint soundIndex = 0;
+ if (soundSet->soundCount > 1)
+ soundIndex = engine.getRandomNumber(soundSet->soundCount - 1);
+
+ const char *soundPath = soundSet->soundPaths[soundIndex];
+ return soundPath && engine.playSound(soundPath);
+}
+
+static bool loadoutUsesConsumableCombatResource(int loadout) {
+ switch (loadout) {
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 14:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool resolvePlayerAttackAnimationRange(const RoomSetupState &state,
+ const RoomPlayerState &playerState, const Common::Point &mousePos,
+ PlayerAttackAnimationRange &range) {
+ if (!playerState.entity)
+ return false;
+
+ const float depthScale = Player::computeDepthScale(state, playerState.z);
+ const float upperThresholdY =
+ (float)playerState.bottomY - depthScale * kRoomPlayerAttackUpperYOffset;
+ const float midThresholdY =
+ (float)playerState.bottomY - depthScale * kRoomPlayerAttackMidYOffset;
+
+ if (mousePos.x < playerState.centerX) {
+ if ((float)mousePos.y < upperThresholdY)
+ range = PlayerAttackAnimationRange(0x50, 0x59, 1);
+ else if ((float)mousePos.y < midThresholdY)
+ range = PlayerAttackAnimationRange(0x5a, 0x63, 1);
+ else
+ range = PlayerAttackAnimationRange(0x64, 0x6d, 1);
+ } else {
+ if ((float)mousePos.y < upperThresholdY)
+ range = PlayerAttackAnimationRange(0x6e, 0x77, 2);
+ else if ((float)mousePos.y < midThresholdY)
+ range = PlayerAttackAnimationRange(0x78, 0x81, 2);
+ else
+ range = PlayerAttackAnimationRange(0x82, 0x8b, 2);
+ }
+
+ return true;
+}
+
+static bool resolveKeyboardAttackAnimationRange(const RoomPlayerState &playerState,
+ bool attackLeft, bool attackRight, bool attackUp, bool attackDown,
+ PlayerAttackAnimationRange &range) {
+ if (attackRight) {
+ range = PlayerAttackAnimationRange(0x78, 0x81, 2);
+ return true;
+ }
+ if (attackLeft) {
+ range = PlayerAttackAnimationRange(0x5a, 0x63, 1);
+ return true;
+ }
+ if (attackUp) {
+ if (playerState.facing == 2) {
+ range = PlayerAttackAnimationRange(0x6e, 0x77, 2);
+ return true;
+ }
+ if (playerState.facing == 1) {
+ range = PlayerAttackAnimationRange(0x50, 0x59, 1);
+ return true;
+ }
+ return false;
+ }
+ if (attackDown) {
+ if (playerState.facing == 2) {
+ range = PlayerAttackAnimationRange(0x82, 0x8b, 2);
+ return true;
+ }
+ if (playerState.facing == 1) {
+ range = PlayerAttackAnimationRange(0x64, 0x6d, 1);
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+}
+
+static bool startResolvedAttackAnimation(RoomPlayerState &playerState,
+ const PlayerAttackAnimationRange &range) {
+ if (!playerState.entity || playerState.attackActive || playerState.hitActive)
+ return false;
+
+ playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ playerState.turnFirstFrame = -1;
+ playerState.turnLastFrame = -1;
+ playerState.turnEndFrame = -1;
+ playerState.turnPlayBackwards = false;
+ playerState.attackActive = true;
+ playerState.attackFirstFrame = range.firstFrame;
+ playerState.attackLastFrame = range.lastFrame;
+ playerState.attackContactFrame =
+ range.firstFrame + Player::resolveCombatLoadoutContactFrameOffset(playerState.combatLoadout);
+ playerState.attackResumeFacing = range.resumeFacing;
+ playerState.attackSoundPlayed = resolvePlayerAttackSoundSet(playerState.combatLoadout) == nullptr;
+ playerState.attackSoundPlaybackFrame = playerState.attackSoundPlayed
+ ? -1
+ : (range.firstFrame + kPlayerAttackSoundTriggerFrameOffset);
+ playerState.attackContactResolved = false;
+ playerState.nextMovementTick = 0;
+ playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
+ playerState.entity->setAnimationRate(playerState.animationRate);
+ playerState.entity->setCurrentFrame(range.firstFrame);
+ return true;
+}
+
+} // End of anonymous namespace
+
+void Player::cancelMovementForAttack() {
+ cancelMovement();
+ _state.turnFirstFrame = -1;
+ _state.turnLastFrame = -1;
+ _state.turnEndFrame = -1;
+ _state.turnPlayBackwards = false;
+}
+
+bool Player::cancelAttackAnimation() {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.attackActive)
+ return false;
+
+ playerState.attackActive = false;
+ playerState.attackFirstFrame = -1;
+ playerState.attackLastFrame = -1;
+ playerState.attackContactFrame = -1;
+ if (!playerState.entity)
+ return false;
+ playerState.attackSoundPlayed = false;
+ playerState.attackSoundPlaybackFrame = -1;
+ playerState.attackContactResolved = false;
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ const int resumeFacing = playerState.attackResumeFacing >= 0
+ ? playerState.attackResumeFacing
+ : (playerState.facing >= 0 ? playerState.facing : 0);
+ playerState.attackResumeFacing = -1;
+ return setIdleAnimation(resumeFacing);
+}
+
+void Player::clearAttackTarget() {
+ _state.attackTargetName.clear();
+ _state.attackTargetClassId = -1;
+}
+
+void Player::captureAttackTarget(const Common::String &name, int classId) {
+ _state.attackTargetName = name;
+ _state.attackTargetClassId = classId;
+}
+
+void Player::markAttackContactResolved() {
+ _state.attackContactResolved = true;
+}
+
+Common::String Player::resolveCombatLoadoutResourcePath(int loadout) {
+ if (loadout <= 0)
+ return Common::String(kPlayerActorResourcePath);
+
+ return Common::String::format("1:/GRAPHIC/MONSTERS/PC/PC%02d.ABM", loadout);
+}
+
+const char *Player::describeCombatLoadout(int loadout) {
+ return resolvePlayerCombatLoadoutTuning(loadout).label;
+}
+
+int Player::resolveCombatLoadoutDamageAmount(int loadout) {
+ return resolvePlayerCombatLoadoutTuning(loadout).damageAmount;
+}
+
+int Player::resolveCombatLoadoutDamageType(int loadout) {
+ return resolvePlayerCombatLoadoutTuning(loadout).damageType;
+}
+
+const char *Player::describeCombatDamageType(int damageType) {
+ switch (damageType) {
+ case 1:
+ return "BLUDGE";
+ case 2:
+ return "SLASH";
+ case 4:
+ return "PROJ";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+int Player::resolveCombatLoadoutContactFrameOffset(int loadout) {
+ return resolvePlayerCombatLoadoutTuning(loadout).contactFrameOffset;
+}
+
+bool Player::isProjectileCombatLoadout(int loadout) {
+ return resolveCombatLoadoutDamageType(loadout) == 4;
+}
+
+void Player::syncCombatLoadout(HarvesterEngine &engine, const RoomSetupState &state) {
+ if (_state.entity) {
+ (void)syncCombatLoadoutVisual(engine, state, engine.getScript()->getPlayerCombatLoadout());
+ } else if (engine.getScript()) {
+ _state.combatLoadout = engine.getScript()->getPlayerCombatLoadout();
+ }
+}
+
+bool Player::syncCombatLoadoutVisual(HarvesterEngine &engine, const RoomSetupState &state, int loadout) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity)
+ return false;
+
+ ResourceManager *resources = engine.getResources();
+ if (!resources)
+ return false;
+
+ const int clampedLoadout = CLIP<int>(loadout, 0, 0x14);
+ if (playerState.combatLoadout == clampedLoadout)
+ return false;
+
+ const Common::String resourcePath = resolveCombatLoadoutResourcePath(clampedLoadout);
+ if (!playerState.entity->loadAbmResource(*resources, resourcePath))
+ return false;
+
+ playerState.combatLoadout = clampedLoadout;
+ playerState.attackActive = false;
+ playerState.attackFirstFrame = -1;
+ playerState.attackLastFrame = -1;
+ playerState.attackContactFrame = -1;
+ playerState.attackResumeFacing = -1;
+ playerState.attackSoundPlayed = false;
+ playerState.attackSoundPlaybackFrame = -1;
+ playerState.attackContactResolved = false;
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ playerState.hitActive = false;
+ playerState.hitFirstFrame = -1;
+ playerState.hitLastFrame = -1;
+ playerState.hitResumeFacing = -1;
+ playerState.hitKnockbackRemainingX = 0;
+ playerState.hitKnockbackDecayStep = 0;
+ (void)setIdleAnimation(playerState.facing >= 0 ? playerState.facing : 0);
+ if (applyRoomActorPlacement(state, *playerState.entity,
+ playerState.centerX, playerState.bottomY, playerState.z)) {
+ reinsertPlayerSceneEntity(engine, playerState);
+ }
+ debugC(1, kDebugPlayer,
+ "Harvester: player combat loadout visual loadout=%d weapon='%s' damage=%d damage_type='%s' contact=%d resource='%s' facing=%d pos=(%d,%d,z=%.2f)",
+ playerState.combatLoadout, describeCombatLoadout(playerState.combatLoadout),
+ resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
+ describeCombatDamageType(resolveCombatLoadoutDamageType(playerState.combatLoadout)),
+ resolveCombatLoadoutContactFrameOffset(playerState.combatLoadout), resourcePath.c_str(), playerState.facing,
+ playerState.centerX, playerState.bottomY, (double)playerState.z);
+ return true;
+}
+
+bool Player::startAttackAnimation(const RoomSetupState &state, const Common::Point &mousePos) {
+ RoomPlayerState &playerState = _state;
+ PlayerAttackAnimationRange range;
+ if (!resolvePlayerAttackAnimationRange(state, playerState, mousePos, range))
+ return false;
+ if (!startResolvedAttackAnimation(playerState, range))
+ return false;
+ debugC(1, kDebugPlayer,
+ "Harvester: player attack animation frames=%d..%d resume_facing=%d cursor=(%d,%d)",
+ range.firstFrame, range.lastFrame, range.resumeFacing, mousePos.x, mousePos.y);
+ return true;
+}
+
+bool Player::startKeyboardAttackAnimation(const RoomSetupState &state,
+ bool attackLeft, bool attackRight, bool attackUp, bool attackDown) {
+ RoomPlayerState &playerState = _state;
+ (void)state;
+
+ PlayerAttackAnimationRange range;
+ if (!resolveKeyboardAttackAnimationRange(playerState, attackLeft, attackRight, attackUp, attackDown, range))
+ return false;
+ if (!startResolvedAttackAnimation(playerState, range))
+ return false;
+ debugC(1, kDebugPlayer,
+ "Harvester: player keyboard attack animation frames=%d..%d resume_facing=%d input=(L=%d R=%d U=%d D=%d) facing=%d",
+ range.firstFrame, range.lastFrame, range.resumeFacing,
+ attackLeft, attackRight, attackUp, attackDown, playerState.facing);
+ return true;
+}
+
+bool Player::updateAttackAnimationState(HarvesterEngine &engine) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.attackActive || !playerState.entity)
+ return false;
+ bool changed = false;
+ if (!playerState.attackSoundPlayed &&
+ playerState.attackSoundPlaybackFrame >= 0 &&
+ playerState.entity->getCurrentFrame() >= playerState.attackSoundPlaybackFrame) {
+ if (loadoutUsesConsumableCombatResource(playerState.combatLoadout)) {
+ Script *script = engine.getScript();
+ if (!script || !script->consumePlayerCombatResourceUnit(playerState.combatLoadout)) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player attack dry fire loadout=%d weapon='%s'",
+ playerState.combatLoadout,
+ describeCombatLoadout(playerState.combatLoadout));
+ playerState.attackContactResolved = true;
+ playerState.attackContactFrame = -1;
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ } else {
+ changed = true;
+ (void)playPlayerAttackSound(engine, playerState.combatLoadout);
+ }
+ } else {
+ (void)playPlayerAttackSound(engine, playerState.combatLoadout);
+ }
+ playerState.attackSoundPlayed = true;
+ }
+ if (playerState.entity->getCurrentFrame() != playerState.attackLastFrame)
+ return changed;
+
+ const int resumeFacing = playerState.attackResumeFacing >= 0
+ ? playerState.attackResumeFacing
+ : (playerState.facing >= 0 ? playerState.facing : 0);
+ playerState.attackActive = false;
+ playerState.attackFirstFrame = -1;
+ playerState.attackLastFrame = -1;
+ playerState.attackContactFrame = -1;
+ playerState.attackResumeFacing = -1;
+ playerState.attackSoundPlayed = false;
+ playerState.attackSoundPlaybackFrame = -1;
+ playerState.attackContactResolved = false;
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ return setIdleAnimation(resumeFacing) || changed;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/player_reactions.cpp b/engines/harvester/player_reactions.cpp
new file mode 100644
index 00000000000..57bdf929e03
--- /dev/null
+++ b/engines/harvester/player_reactions.cpp
@@ -0,0 +1,358 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player.h"
+#include "harvester/player_movement.h"
+#include "harvester/combat.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+
+namespace Harvester {
+
+using namespace PlayerMovement;
+
+namespace {
+
+static const int kRoomPlayerHitAnimationRate = 4;
+static const int kNativePlayerDeathEntryAnimationRate = 14;
+static const int kPlayerHitKnockbackDistance = 18;
+static const int kPlayerHitKnockbackDecayStep = 3;
+
+struct PlayerDeathAnimationRange {
+ PlayerDeathAnimationRange() {}
+ PlayerDeathAnimationRange(int firstFrame, int lastFrame)
+ : firstFrame(firstFrame), lastFrame(lastFrame) {}
+
+ int firstFrame = -1;
+ int lastFrame = -1;
+};
+
+struct PlayerHitAnimationRange {
+ PlayerHitAnimationRange() {}
+ PlayerHitAnimationRange(int firstFrame, int lastFrame, int resumeFacing, int knockbackX)
+ : firstFrame(firstFrame), lastFrame(lastFrame), resumeFacing(resumeFacing), knockbackX(knockbackX) {}
+
+ int firstFrame = -1;
+ int lastFrame = -1;
+ int resumeFacing = -1;
+ int knockbackX = 0;
+};
+
+static const char *const kPlayerHitSoundPaths[] = {
+ "1:/sound/effects/PC0_hit0.wav",
+ "1:/sound/effects/PC0_hit1.wav",
+ "1:/sound/effects/PC0_hit2.wav"
+};
+
+static bool playPlayerHitSound(HarvesterEngine &engine) {
+ const uint soundIndex = ARRAYSIZE(kPlayerHitSoundPaths) > 1
+ ? engine.getRandomNumber(ARRAYSIZE(kPlayerHitSoundPaths) - 1)
+ : 0;
+ return engine.playSound(kPlayerHitSoundPaths[soundIndex]);
+}
+
+static bool choosePlayerDeathAnimationRange(const Entity &entity, int firstFrame, int lastFrame,
+ PlayerDeathAnimationRange &range) {
+ if (!runtimeEntityHasFrameRange(entity, firstFrame, lastFrame))
+ return false;
+
+ range = PlayerDeathAnimationRange(firstFrame, lastFrame);
+ return true;
+}
+
+static bool resolvePlayerDeathAnimationRange(const RoomPlayerState &playerState,
+ int damageType, bool goreEnabled, PlayerDeathAnimationRange &range) {
+ if (!playerState.entity)
+ return false;
+
+ enum PlayerDeathBankFamily {
+ kPlayerDeathBankHorizontalRight,
+ kPlayerDeathBankHorizontalLeft,
+ kPlayerDeathBankVerticalDown,
+ kPlayerDeathBankVerticalUp
+ };
+
+ PlayerDeathBankFamily family = kPlayerDeathBankHorizontalLeft;
+ if (playerState.attackActive) {
+ if (playerState.attackFirstFrame >= 0x6e && playerState.attackFirstFrame <= 0x8b)
+ family = kPlayerDeathBankHorizontalRight;
+ else
+ family = kPlayerDeathBankHorizontalLeft;
+ } else if (playerState.turnActive) {
+ if (playerState.turnFirstFrame >= 0x28)
+ family = kPlayerDeathBankHorizontalRight;
+ else
+ family = kPlayerDeathBankHorizontalLeft;
+ } else {
+ switch (playerState.facing) {
+ case 0:
+ family = kPlayerDeathBankVerticalDown;
+ break;
+ case 2:
+ family = kPlayerDeathBankHorizontalRight;
+ break;
+ case 3:
+ family = kPlayerDeathBankVerticalUp;
+ break;
+ case 1:
+ default:
+ family = kPlayerDeathBankHorizontalLeft;
+ break;
+ }
+ }
+
+ const auto selectByFamily = [&](int bludgeFirst, int bludgeLast, int slashFirst, int slashLast,
+ int projectileFirst, int projectileLast) {
+ if ((!goreEnabled || damageType == 1) &&
+ choosePlayerDeathAnimationRange(*playerState.entity, bludgeFirst, bludgeLast, range))
+ return true;
+ if (damageType == 4 &&
+ choosePlayerDeathAnimationRange(*playerState.entity, projectileFirst, projectileLast, range))
+ return true;
+ if (damageType == 2 &&
+ choosePlayerDeathAnimationRange(*playerState.entity, slashFirst, slashLast, range))
+ return true;
+
+ return choosePlayerDeathAnimationRange(*playerState.entity, bludgeFirst, bludgeLast, range) ||
+ choosePlayerDeathAnimationRange(*playerState.entity, slashFirst, slashLast, range) ||
+ choosePlayerDeathAnimationRange(*playerState.entity, projectileFirst, projectileLast, range);
+ };
+
+ switch (family) {
+ case kPlayerDeathBankHorizontalRight:
+ return selectByFamily(0xb0, 0xb9, 0x100, 0x109, 0x114, 0x11d);
+ case kPlayerDeathBankVerticalDown:
+ return selectByFamily(0xc4, 0xcd, 0xd8, 0xe1, 0xec, 0xf5);
+ case kPlayerDeathBankVerticalUp:
+ return selectByFamily(0xce, 0xd7, 0xe2, 0xeb, 0xf6, 0xff);
+ case kPlayerDeathBankHorizontalLeft:
+ default:
+ return selectByFamily(0xba, 0xc3, 0x10a, 0x113, 0x11e, 0x127);
+ }
+}
+
+static bool resolvePlayerHitAnimationRange(const Entity &entity,
+ int monsterAttackFirstFrame, PlayerHitAnimationRange &range) {
+ PlayerHitAnimationRange candidate;
+ switch (monsterAttackFirstFrame) {
+ case 0x64:
+ candidate = PlayerHitAnimationRange(0x92, 0x94, 2, -kPlayerHitKnockbackDistance);
+ break;
+ case 0x5a:
+ candidate = PlayerHitAnimationRange(0x8f, 0x91, 2, -kPlayerHitKnockbackDistance);
+ break;
+ case 0x50:
+ candidate = PlayerHitAnimationRange(0x8c, 0x8e, 2, -kPlayerHitKnockbackDistance);
+ break;
+ case 0x82:
+ candidate = PlayerHitAnimationRange(0x9b, 0x9d, 1, kPlayerHitKnockbackDistance);
+ break;
+ case 0x78:
+ candidate = PlayerHitAnimationRange(0x98, 0x9a, 1, kPlayerHitKnockbackDistance);
+ break;
+ case 0x6e:
+ candidate = PlayerHitAnimationRange(0x95, 0x97, 1, kPlayerHitKnockbackDistance);
+ break;
+ default:
+ return false;
+ }
+
+ if (!runtimeEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
+ return false;
+
+ range = candidate;
+ return true;
+}
+
+} // End of anonymous namespace
+
+bool Player::startHitAnimation(HarvesterEngine &engine, int monsterAttackFirstFrame) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.deathActive)
+ return false;
+
+ PlayerHitAnimationRange range;
+ if (!resolvePlayerHitAnimationRange(*playerState.entity, monsterAttackFirstFrame, range))
+ return false;
+
+ playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ playerState.turnFirstFrame = -1;
+ playerState.turnLastFrame = -1;
+ playerState.turnEndFrame = -1;
+ playerState.turnPlayBackwards = false;
+ playerState.attackActive = false;
+ playerState.attackFirstFrame = -1;
+ playerState.attackLastFrame = -1;
+ playerState.attackContactFrame = -1;
+ playerState.attackResumeFacing = -1;
+ playerState.attackSoundPlayed = false;
+ playerState.attackSoundPlaybackFrame = -1;
+ playerState.attackContactResolved = false;
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ playerState.nextMovementTick = 0;
+ playerState.hitActive = true;
+ playerState.hitFirstFrame = range.firstFrame;
+ playerState.hitLastFrame = range.lastFrame;
+ playerState.hitResumeFacing = range.resumeFacing;
+ playerState.hitKnockbackRemainingX = range.knockbackX;
+ playerState.hitKnockbackDecayStep = kPlayerHitKnockbackDecayStep;
+ playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
+ playerState.entity->setAnimationRate(kRoomPlayerHitAnimationRate);
+ playerState.entity->setCurrentFrame(range.firstFrame);
+ playerState.entity->setAnimationEnabled(true);
+ playerState.entity->setVisible(true);
+ (void)playPlayerHitSound(engine);
+ debugC(1, kDebugCombat,
+ "Harvester: player hit animation attack_frame=%d frames=%d..%d resume_facing=%d knockback=%d rate=%d",
+ monsterAttackFirstFrame, range.firstFrame, range.lastFrame, range.resumeFacing,
+ range.knockbackX, kRoomPlayerHitAnimationRate);
+ return true;
+}
+
+bool Player::updateHitAnimationState(HarvesterEngine &engine, const RoomSetupState &state,
+ const Common::Array<ObjectRecord> &sceneObjects, const Common::Array<AnimRecord> &sceneAnimations) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.hitActive || !playerState.entity)
+ return false;
+
+ const MovementContext movementContext = { engine, state, sceneObjects, sceneAnimations, playerState };
+ bool changed = false;
+ if (playerState.hitKnockbackRemainingX != 0) {
+ const int step = MAX(1, ABS(playerState.hitKnockbackDecayStep));
+ const int knockbackStep = playerState.hitKnockbackRemainingX > 0
+ ? MIN(playerState.hitKnockbackRemainingX, step)
+ : MAX(playerState.hitKnockbackRemainingX, -step);
+ if (knockbackStep != 0) {
+ if (!tryApplyPlayerMovement(movementContext,
+ { playerState.centerX + knockbackStep, playerState.bottomY, playerState.z })) {
+ playerState.hitKnockbackRemainingX = 0;
+ } else {
+ changed = true;
+ playerState.hitKnockbackRemainingX -= knockbackStep;
+ }
+ }
+ }
+
+ if (playerState.entity->getCurrentFrame() != playerState.hitLastFrame)
+ return changed;
+
+ const int resumeFacing = playerState.hitResumeFacing >= 0
+ ? playerState.hitResumeFacing
+ : (playerState.facing >= 0 ? playerState.facing : 0);
+ playerState.hitActive = false;
+ playerState.hitFirstFrame = -1;
+ playerState.hitLastFrame = -1;
+ playerState.hitResumeFacing = -1;
+ playerState.hitKnockbackRemainingX = 0;
+ playerState.hitKnockbackDecayStep = 0;
+ return setIdleAnimation(resumeFacing) || changed;
+}
+
+bool Player::startDeathAnimation(int damageType, bool goreEnabled) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity || playerState.deathActive)
+ return false;
+
+ PlayerDeathAnimationRange range;
+ if (!resolvePlayerDeathAnimationRange(playerState, damageType, goreEnabled, range))
+ return false;
+
+ playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ playerState.turnFirstFrame = -1;
+ playerState.turnLastFrame = -1;
+ playerState.turnEndFrame = -1;
+ playerState.turnPlayBackwards = false;
+ playerState.attackActive = false;
+ playerState.attackFirstFrame = -1;
+ playerState.attackLastFrame = -1;
+ playerState.attackContactFrame = -1;
+ playerState.attackResumeFacing = -1;
+ playerState.attackSoundPlayed = false;
+ playerState.attackSoundPlaybackFrame = -1;
+ playerState.attackContactResolved = false;
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ playerState.hitActive = false;
+ playerState.hitFirstFrame = -1;
+ playerState.hitLastFrame = -1;
+ playerState.hitResumeFacing = -1;
+ playerState.hitKnockbackRemainingX = 0;
+ playerState.hitKnockbackDecayStep = 0;
+ playerState.nextMovementTick = 0;
+ playerState.deathActive = true;
+ playerState.deathFirstFrame = range.firstFrame;
+ playerState.deathLastFrame = range.lastFrame;
+ playerState.deathDamageType = damageType;
+ playerState.entity->setAnimationFrameRange(range.firstFrame, range.lastFrame, false);
+ playerState.entity->setAnimationRate(kNativePlayerDeathEntryAnimationRate);
+ playerState.entity->setCurrentFrame(range.firstFrame);
+ playerState.entity->setVisible(true);
+ debugC(2, kDebugCombat,
+ "Harvester: player death animation start damage_type=%d gore=%d facing=%d frames=%d..%d",
+ damageType, goreEnabled, playerState.facing, range.firstFrame, range.lastFrame);
+ debugC(3, kDebugCombat,
+ "Harvester: player death animation timing frames=%d..%d entry_rate=%d entry_interval_ticks=%d running_rate=%d running_interval_ticks=%d",
+ range.firstFrame, range.lastFrame,
+ kNativePlayerDeathEntryAnimationRate, 100 / kNativePlayerDeathEntryAnimationRate,
+ playerState.animationRate, 100 / playerState.animationRate);
+ return true;
+}
+
+bool Player::updateDeathAnimationState() {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.deathActive || !playerState.entity)
+ return false;
+
+ const int currentFrame = playerState.entity->getCurrentFrame();
+ if (currentFrame > playerState.deathFirstFrame &&
+ currentFrame < playerState.deathLastFrame &&
+ playerState.entity->getAnimationRate() != playerState.animationRate) {
+ playerState.entity->setAnimationRate(playerState.animationRate);
+ debugC(3, kDebugCombat,
+ "Harvester: player death animation running frame=%d rate=%d interval_ticks=%d",
+ currentFrame, playerState.animationRate,
+ 100 / playerState.animationRate);
+ }
+
+ if (currentFrame < playerState.deathLastFrame)
+ return false;
+
+ playerState.deathActive = false;
+ debugC(2, kDebugCombat,
+ "Harvester: player death animation complete damage_type=%d frame=%d",
+ playerState.deathDamageType, currentFrame);
+ return true;
+}
+
+} // End of namespace Harvester
Commit: 58a5bca82ed76e608e2199794c6413d46f63c8e2
https://github.com/scummvm/scummvm/commit/58a5bca82ed76e608e2199794c6413d46f63c8e2
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate player lifecycle and idle animation
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/player_idle.cpp
engines/harvester/module.mk
engines/harvester/player.cpp
engines/harvester/player.h
engines/harvester/player_animation.cpp
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 40f096ed98d..55da1d403ab 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -24,6 +24,7 @@ MODULE_OBJS = \
player.o \
player_animation.o \
player_attack.o \
+ player_idle.o \
player_keyboard.o \
player_movement.o \
player_reactions.o \
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 7aabf1295d5..867739a4113 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -24,132 +24,14 @@
#include "harvester/combat.h"
#include "common/debug.h"
-#include "common/system.h"
#include "harvester/detection.h"
#include "harvester/harvester.h"
-#include "harvester/resources.h"
#include "harvester/entity.h"
namespace Harvester {
using namespace PlayerMovement;
-namespace {
-
-static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing);
-static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState);
-
-static const char *const kPlayerIdleAnimationEntityName = "IDLE_ANIM";
-static const char *const kPlayerIdleAnimationResourcePath = "1:/GRAPHIC/ROOMANIM/PCLOUN02.ABM";
-static const uint32 kRoomPlayerIdleDelayTicks = 3000;
-static const int kRoomPlayerIdleAnimationRate = 14;
-static const int kRoomPlayerIdleLoopStartFrame = 0x0f;
-static const int kRoomPlayerIdleLoopLastFrame = 0xb2;
-static const int kRoomPlayerIdleExitLastFrame = 0xbf;
-static const int kRoomPlayerIdleYOffset = 4;
-
-static void notePlayerIdleReset(RoomIdleAnimationState &idleState) {
- idleState.resetTick = Player::getRuntimeClockTicks();
- updatePlayerIdleTrigger(idleState);
-}
-
-static void positionPlayerIdleAnimationEntity(const RoomSetupState &state,
- const RoomPlayerState &playerState, Entity &idleEntity) {
- if (!playerState.entity)
- return;
-
- idleEntity.setAnchorMode(kEntityAnchorTopLeft);
- idleEntity.setDepthScale(Player::computeDepthScale(state, playerState.z));
- idleEntity.setPosition(playerState.entity->getX(),
- playerState.entity->getY() + kRoomPlayerIdleYOffset, playerState.z);
-}
-
-static Entity *ensurePlayerIdleAnimationEntity(HarvesterEngine &engine,
- const RoomSetupState &state, const RoomPlayerState &playerState,
- RoomIdleAnimationState &idleState) {
- if (idleState.entity || !playerState.entity)
- return idleState.entity;
-
- EntityManager *entityManager = engine.getRuntimeEntities();
- if (!entityManager)
- return nullptr;
-
- idleState.entity = entityManager->spawnSceneAnimationEntity(
- kPlayerIdleAnimationEntityName, kPlayerIdleAnimationResourcePath,
- Common::Point(playerState.entity->getX(),
- playerState.entity->getY() + kRoomPlayerIdleYOffset),
- playerState.z, 0, false, false, false, false, false);
- if (!idleState.entity)
- return nullptr;
-
- positionPlayerIdleAnimationEntity(state, playerState, *idleState.entity);
- idleState.entity->setVisible(false);
- debugC(1, kDebugPlayer,
- "Harvester: spawned player idle animation '%s' room='%s' frames=0..%d",
- kPlayerIdleAnimationResourcePath, state.roomName.c_str(), idleState.entity->getLastFrame());
- return idleState.entity;
-}
-
-static bool finishPlayerIdleAnimation(const RoomSetupState &state, RoomPlayerState &playerState,
- RoomIdleAnimationState &idleState) {
- if (!idleState.active)
- return false;
-
- if (idleState.entity) {
- idleState.entity->setVisible(false);
- idleState.entity->setAnimationRate(0);
- idleState.entity->setCurrentFrame(0);
- positionPlayerIdleAnimationEntity(state, playerState, *idleState.entity);
- }
- if (playerState.entity) {
- playerState.entity->setVisible(true);
- if (idleState.restoreFacing >= 0)
- (void)setPlayerIdleAnimation(playerState, idleState.restoreFacing);
- }
-
- idleState.active = false;
- idleState.loopStarted = false;
- idleState.exiting = false;
- idleState.restoreFacing = -1;
- notePlayerIdleReset(idleState);
- debugC(1, kDebugPlayer,
- "Harvester: player idle animation finished room='%s' facing=%d",
- state.roomName.c_str(), playerState.facing);
- return true;
-}
-
-static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState) {
- idleState.triggerTick = MAX(idleState.activityTick, idleState.resetTick) + kRoomPlayerIdleDelayTicks;
-}
-
-static bool setPlayerIdleAnimation(RoomPlayerState &playerState, int facing) {
- if (!playerState.entity)
- return false;
-
- const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
- const bool changed = playerState.facing != facing ||
- playerState.entity->getCurrentFrame() != range.idleFrame ||
- playerState.entity->getAnimationRate() != 0;
- playerState.facing = facing;
- playerState.evasiveState = -1;
- playerState.evasiveXStep = 0;
- playerState.nextMovementTick = 0;
- playerState.keyboardWalkRequest = -1;
- playerState.walkHorizontalDirection = 0;
- playerState.walkDepthDirection = 0;
- playerState.entity->setAnimationRate(0);
- playerState.entity->setAnimationFrameRange(range.idleFrame, range.idleFrame, false);
- playerState.entity->setCurrentFrame(range.idleFrame);
- if (changed) {
- debugC(1, kDebugPlayer,
- "Harvester: player idle animation facing=%d frame=%d",
- facing, range.idleFrame);
- }
- return changed;
-}
-
-} // End of anonymous namespace
-
void Player::initialize(const RoomSetupState &state, Entity *entity, int combatLoadout) {
RoomPlayerState &playerState = _state;
playerState.entity = entity;
@@ -176,35 +58,6 @@ void Player::cancelMovement() {
_state.turnTargetFacing = -1;
}
-void Player::resetIdleState() {
- RoomIdleAnimationState &idleState = _idleState;
- idleState = RoomIdleAnimationState();
- idleState.activityTick = Player::getRuntimeClockTicks();
- idleState.resetTick = idleState.activityTick;
- updatePlayerIdleTrigger(idleState);
-}
-
-void Player::noteActivity() {
- RoomIdleAnimationState &idleState = _idleState;
- idleState.activityTick = Player::getRuntimeClockTicks();
- updatePlayerIdleTrigger(idleState);
-}
-
-void Player::cancelIdleAnimation() {
- RoomPlayerState &playerState = _state;
- RoomIdleAnimationState &idleState = _idleState;
- if (idleState.entity) {
- idleState.entity->setVisible(false);
- idleState.entity->setAnimationRate(0);
- }
- idleState.active = false;
- idleState.loopStarted = false;
- idleState.exiting = false;
- idleState.restoreFacing = -1;
- if (playerState.entity)
- playerState.entity->setVisible(true);
-}
-
void Player::applyGotoXZ(const RoomSetupState &state, EntityManager *entityManager, int x, int z) {
RoomPlayerState &playerState = _state;
playerState.hasMoveTarget = false;
@@ -320,119 +173,4 @@ uint32 Player::getRuntimeClockTicks() {
return Harvester::getRuntimeClockTicks();
}
-bool Player::isIdleAnimationExcludedRoom(const Common::String &roomName) {
- static const char *const kExcludedRooms[] = {
- "JAWS",
- "SUPPLY1",
- "SUPPLY2",
- "SUPPLY3",
- "LAVAPIT",
- "RMNBATH",
- "SERGENTRM",
- "CHAP2"
- };
-
- for (const char *excludedRoom : kExcludedRooms) {
- if (roomName.equalsIgnoreCase(excludedRoom))
- return true;
- }
-
- return false;
-}
-
-
-bool Player::setIdleAnimation(int facing) {
- return setPlayerIdleAnimation(_state, facing);
-}
-
-bool Player::requestIdleAnimationExit(const RoomSetupState &state) {
- RoomPlayerState &playerState = _state;
- RoomIdleAnimationState &idleState = _idleState;
- if (!idleState.active || idleState.exiting || !idleState.entity)
- return false;
-
- const int lastFrame = idleState.entity->getLastFrame();
- const int exitStartFrame = MIN(lastFrame, kRoomPlayerIdleLoopLastFrame);
- const int exitLastFrame = MIN(lastFrame, kRoomPlayerIdleExitLastFrame);
- if (exitLastFrame <= exitStartFrame)
- return finishPlayerIdleAnimation(state, playerState, idleState);
-
- positionPlayerIdleAnimationEntity(state, playerState, *idleState.entity);
- idleState.exiting = true;
- idleState.entity->setAnimationFrameRange(exitStartFrame, exitLastFrame, false);
- idleState.entity->setCurrentFrame(exitStartFrame);
- idleState.entity->setAnimationRate(kRoomPlayerIdleAnimationRate);
- debugC(1, kDebugPlayer,
- "Harvester: player idle animation exit room='%s' frames=%d..%d rate=%d",
- state.roomName.c_str(), exitStartFrame, exitLastFrame, kRoomPlayerIdleAnimationRate);
- return true;
-}
-
-bool Player::startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &state) {
- RoomPlayerState &playerState = _state;
- RoomIdleAnimationState &idleState = _idleState;
- if (!playerState.entity || idleState.active || isIdleAnimationExcludedRoom(state.roomName))
- return false;
-
- Entity *idleEntity = ensurePlayerIdleAnimationEntity(engine, state, playerState, idleState);
- if (!idleEntity)
- return false;
-
- const int lastFrame = idleEntity->getLastFrame();
- const int loopLastFrame = MIN(lastFrame, kRoomPlayerIdleLoopLastFrame);
- positionPlayerIdleAnimationEntity(state, playerState, *idleEntity);
- idleState.restoreFacing = playerState.facing;
- idleState.active = true;
- idleState.loopStarted = false;
- idleState.exiting = false;
- idleEntity->setVisible(true);
- idleEntity->setAnimationFrameRange(0, loopLastFrame, false);
- idleEntity->setCurrentFrame(0);
- idleEntity->setAnimationRate(kRoomPlayerIdleAnimationRate);
- playerState.hasMoveTarget = false;
- playerState.evasiveState = -1;
- playerState.evasiveXStep = 0;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- playerState.entity->setVisible(false);
- debugC(1, kDebugPlayer,
- "Harvester: player idle animation start room='%s' facing=%d frames=%d..%d rate=%d trigger_tick=%u",
- state.roomName.c_str(), idleState.restoreFacing, 0, loopLastFrame,
- kRoomPlayerIdleAnimationRate, idleState.triggerTick);
- return true;
-}
-
-bool Player::updateIdleAnimation(const RoomSetupState &state) {
- RoomPlayerState &playerState = _state;
- RoomIdleAnimationState &idleState = _idleState;
- if (!idleState.active || !idleState.entity)
- return false;
-
- const int lastFrame = idleState.entity->getLastFrame();
- const int loopLastFrame = MIN(lastFrame, kRoomPlayerIdleLoopLastFrame);
- if (!idleState.exiting && !idleState.loopStarted &&
- idleState.entity->getCurrentFrame() >= loopLastFrame) {
- const int loopStartFrame = MIN(lastFrame, kRoomPlayerIdleLoopStartFrame);
- idleState.loopStarted = true;
- if (loopStartFrame < loopLastFrame) {
- idleState.entity->setAnimationFrameRange(loopStartFrame, loopLastFrame, true);
- idleState.entity->setCurrentFrame(loopStartFrame);
- idleState.entity->setAnimationRate(kRoomPlayerIdleAnimationRate);
- }
- debugC(1, kDebugPlayer,
- "Harvester: player idle animation loop room='%s' frames=%d..%d rate=%d",
- state.roomName.c_str(), loopStartFrame, loopLastFrame, kRoomPlayerIdleAnimationRate);
- return true;
- }
-
- if (!idleState.exiting)
- return false;
-
- const int exitLastFrame = MIN(lastFrame, kRoomPlayerIdleExitLastFrame);
- if (idleState.entity->getCurrentFrame() < exitLastFrame)
- return false;
-
- return finishPlayerIdleAnimation(state, playerState, idleState);
-}
-
} // End of namespace Harvester
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index 355095ca416..ef0dcd81b1d 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -173,6 +173,8 @@ public:
bool updateIdleAnimation(const RoomSetupState &state);
private:
+ bool finishIdleAnimation(const RoomSetupState &state);
+
bool stopMoveTargetStep(const PlayerMovement::MovementContext &context,
const PlayerMovement::MovementPosition &candidate, int horizontalDirection, int depthDirection,
const Entity *blocker);
diff --git a/engines/harvester/player_animation.cpp b/engines/harvester/player_animation.cpp
index d72323859fe..f6afcca8e94 100644
--- a/engines/harvester/player_animation.cpp
+++ b/engines/harvester/player_animation.cpp
@@ -88,6 +88,33 @@ static bool resolvePlayerTurnAnimationRange(int fromFacing, int toFacing, Player
} // End of anonymous namespace
+bool Player::setIdleAnimation(int facing) {
+ RoomPlayerState &playerState = _state;
+ if (!playerState.entity)
+ return false;
+
+ const PlayerAnimationRange range = resolvePlayerAnimationRange(facing);
+ const bool changed = playerState.facing != facing ||
+ playerState.entity->getCurrentFrame() != range.idleFrame ||
+ playerState.entity->getAnimationRate() != 0;
+ playerState.facing = facing;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
+ playerState.nextMovementTick = 0;
+ playerState.keyboardWalkRequest = -1;
+ playerState.walkHorizontalDirection = 0;
+ playerState.walkDepthDirection = 0;
+ playerState.entity->setAnimationRate(0);
+ playerState.entity->setAnimationFrameRange(range.idleFrame, range.idleFrame, false);
+ playerState.entity->setCurrentFrame(range.idleFrame);
+ if (changed) {
+ debugC(1, kDebugPlayer,
+ "Harvester: player idle animation facing=%d frame=%d",
+ facing, range.idleFrame);
+ }
+ return changed;
+}
+
bool Player::updateAnimationRate(bool speedUp) {
RoomPlayerState &playerState = _state;
const int rate = speedUp ? kRoomPlayerFastAnimationRate : kRoomPlayerWalkAnimationRate;
diff --git a/engines/harvester/player_idle.cpp b/engines/harvester/player_idle.cpp
new file mode 100644
index 00000000000..c2e2e63e77f
--- /dev/null
+++ b/engines/harvester/player_idle.cpp
@@ -0,0 +1,258 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/player.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kPlayerIdleAnimationEntityName = "IDLE_ANIM";
+static const char *const kPlayerIdleAnimationResourcePath = "1:/GRAPHIC/ROOMANIM/PCLOUN02.ABM";
+static const uint32 kRoomPlayerIdleDelayTicks = 3000;
+static const int kRoomPlayerIdleAnimationRate = 14;
+static const int kRoomPlayerIdleLoopStartFrame = 0x0f;
+static const int kRoomPlayerIdleLoopLastFrame = 0xb2;
+static const int kRoomPlayerIdleExitLastFrame = 0xbf;
+static const int kRoomPlayerIdleYOffset = 4;
+
+static void updatePlayerIdleTrigger(RoomIdleAnimationState &idleState) {
+ idleState.triggerTick = MAX(idleState.activityTick, idleState.resetTick) + kRoomPlayerIdleDelayTicks;
+}
+
+static void notePlayerIdleReset(RoomIdleAnimationState &idleState) {
+ idleState.resetTick = Player::getRuntimeClockTicks();
+ updatePlayerIdleTrigger(idleState);
+}
+
+static void positionPlayerIdleAnimationEntity(const RoomSetupState &state,
+ const RoomPlayerState &playerState, Entity &idleEntity) {
+ if (!playerState.entity)
+ return;
+
+ idleEntity.setAnchorMode(kEntityAnchorTopLeft);
+ idleEntity.setDepthScale(Player::computeDepthScale(state, playerState.z));
+ idleEntity.setPosition(playerState.entity->getX(),
+ playerState.entity->getY() + kRoomPlayerIdleYOffset, playerState.z);
+}
+
+static Entity *ensurePlayerIdleAnimationEntity(HarvesterEngine &engine,
+ const RoomSetupState &state, const RoomPlayerState &playerState,
+ RoomIdleAnimationState &idleState) {
+ if (idleState.entity || !playerState.entity)
+ return idleState.entity;
+
+ EntityManager *entityManager = engine.getRuntimeEntities();
+ if (!entityManager)
+ return nullptr;
+
+ idleState.entity = entityManager->spawnSceneAnimationEntity(
+ kPlayerIdleAnimationEntityName, kPlayerIdleAnimationResourcePath,
+ Common::Point(playerState.entity->getX(),
+ playerState.entity->getY() + kRoomPlayerIdleYOffset),
+ playerState.z, 0, false, false, false, false, false);
+ if (!idleState.entity)
+ return nullptr;
+
+ positionPlayerIdleAnimationEntity(state, playerState, *idleState.entity);
+ idleState.entity->setVisible(false);
+ debugC(1, kDebugPlayer,
+ "Harvester: spawned player idle animation '%s' room='%s' frames=0..%d",
+ kPlayerIdleAnimationResourcePath, state.roomName.c_str(), idleState.entity->getLastFrame());
+ return idleState.entity;
+}
+
+} // End of anonymous namespace
+
+bool Player::finishIdleAnimation(const RoomSetupState &state) {
+ RoomPlayerState &playerState = _state;
+ RoomIdleAnimationState &idleState = _idleState;
+ if (!idleState.active)
+ return false;
+
+ if (idleState.entity) {
+ idleState.entity->setVisible(false);
+ idleState.entity->setAnimationRate(0);
+ idleState.entity->setCurrentFrame(0);
+ positionPlayerIdleAnimationEntity(state, playerState, *idleState.entity);
+ }
+ if (playerState.entity) {
+ playerState.entity->setVisible(true);
+ if (idleState.restoreFacing >= 0)
+ (void)setIdleAnimation(idleState.restoreFacing);
+ }
+
+ idleState.active = false;
+ idleState.loopStarted = false;
+ idleState.exiting = false;
+ idleState.restoreFacing = -1;
+ notePlayerIdleReset(idleState);
+ debugC(1, kDebugPlayer,
+ "Harvester: player idle animation finished room='%s' facing=%d",
+ state.roomName.c_str(), playerState.facing);
+ return true;
+}
+
+void Player::resetIdleState() {
+ RoomIdleAnimationState &idleState = _idleState;
+ idleState = RoomIdleAnimationState();
+ idleState.activityTick = Player::getRuntimeClockTicks();
+ idleState.resetTick = idleState.activityTick;
+ updatePlayerIdleTrigger(idleState);
+}
+
+void Player::noteActivity() {
+ RoomIdleAnimationState &idleState = _idleState;
+ idleState.activityTick = Player::getRuntimeClockTicks();
+ updatePlayerIdleTrigger(idleState);
+}
+
+void Player::cancelIdleAnimation() {
+ RoomPlayerState &playerState = _state;
+ RoomIdleAnimationState &idleState = _idleState;
+ if (idleState.entity) {
+ idleState.entity->setVisible(false);
+ idleState.entity->setAnimationRate(0);
+ }
+ idleState.active = false;
+ idleState.loopStarted = false;
+ idleState.exiting = false;
+ idleState.restoreFacing = -1;
+ if (playerState.entity)
+ playerState.entity->setVisible(true);
+}
+
+bool Player::isIdleAnimationExcludedRoom(const Common::String &roomName) {
+ static const char *const kExcludedRooms[] = {
+ "JAWS",
+ "SUPPLY1",
+ "SUPPLY2",
+ "SUPPLY3",
+ "LAVAPIT",
+ "RMNBATH",
+ "SERGENTRM",
+ "CHAP2"
+ };
+
+ for (const char *excludedRoom : kExcludedRooms) {
+ if (roomName.equalsIgnoreCase(excludedRoom))
+ return true;
+ }
+
+ return false;
+}
+
+bool Player::requestIdleAnimationExit(const RoomSetupState &state) {
+ RoomPlayerState &playerState = _state;
+ RoomIdleAnimationState &idleState = _idleState;
+ if (!idleState.active || idleState.exiting || !idleState.entity)
+ return false;
+
+ const int lastFrame = idleState.entity->getLastFrame();
+ const int exitStartFrame = MIN(lastFrame, kRoomPlayerIdleLoopLastFrame);
+ const int exitLastFrame = MIN(lastFrame, kRoomPlayerIdleExitLastFrame);
+ if (exitLastFrame <= exitStartFrame)
+ return finishIdleAnimation(state);
+
+ positionPlayerIdleAnimationEntity(state, playerState, *idleState.entity);
+ idleState.exiting = true;
+ idleState.entity->setAnimationFrameRange(exitStartFrame, exitLastFrame, false);
+ idleState.entity->setCurrentFrame(exitStartFrame);
+ idleState.entity->setAnimationRate(kRoomPlayerIdleAnimationRate);
+ debugC(1, kDebugPlayer,
+ "Harvester: player idle animation exit room='%s' frames=%d..%d rate=%d",
+ state.roomName.c_str(), exitStartFrame, exitLastFrame, kRoomPlayerIdleAnimationRate);
+ return true;
+}
+
+bool Player::startIdleAnimation(HarvesterEngine &engine, const RoomSetupState &state) {
+ RoomPlayerState &playerState = _state;
+ RoomIdleAnimationState &idleState = _idleState;
+ if (!playerState.entity || idleState.active || isIdleAnimationExcludedRoom(state.roomName))
+ return false;
+
+ Entity *idleEntity = ensurePlayerIdleAnimationEntity(engine, state, playerState, idleState);
+ if (!idleEntity)
+ return false;
+
+ const int lastFrame = idleEntity->getLastFrame();
+ const int loopLastFrame = MIN(lastFrame, kRoomPlayerIdleLoopLastFrame);
+ positionPlayerIdleAnimationEntity(state, playerState, *idleEntity);
+ idleState.restoreFacing = playerState.facing;
+ idleState.active = true;
+ idleState.loopStarted = false;
+ idleState.exiting = false;
+ idleEntity->setVisible(true);
+ idleEntity->setAnimationFrameRange(0, loopLastFrame, false);
+ idleEntity->setCurrentFrame(0);
+ idleEntity->setAnimationRate(kRoomPlayerIdleAnimationRate);
+ playerState.hasMoveTarget = false;
+ playerState.evasiveState = -1;
+ playerState.evasiveXStep = 0;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ playerState.entity->setVisible(false);
+ debugC(1, kDebugPlayer,
+ "Harvester: player idle animation start room='%s' facing=%d frames=%d..%d rate=%d trigger_tick=%u",
+ state.roomName.c_str(), idleState.restoreFacing, 0, loopLastFrame,
+ kRoomPlayerIdleAnimationRate, idleState.triggerTick);
+ return true;
+}
+
+bool Player::updateIdleAnimation(const RoomSetupState &state) {
+ RoomIdleAnimationState &idleState = _idleState;
+ if (!idleState.active || !idleState.entity)
+ return false;
+
+ const int lastFrame = idleState.entity->getLastFrame();
+ const int loopLastFrame = MIN(lastFrame, kRoomPlayerIdleLoopLastFrame);
+ if (!idleState.exiting && !idleState.loopStarted &&
+ idleState.entity->getCurrentFrame() >= loopLastFrame) {
+ const int loopStartFrame = MIN(lastFrame, kRoomPlayerIdleLoopStartFrame);
+ idleState.loopStarted = true;
+ if (loopStartFrame < loopLastFrame) {
+ idleState.entity->setAnimationFrameRange(loopStartFrame, loopLastFrame, true);
+ idleState.entity->setCurrentFrame(loopStartFrame);
+ idleState.entity->setAnimationRate(kRoomPlayerIdleAnimationRate);
+ }
+ debugC(1, kDebugPlayer,
+ "Harvester: player idle animation loop room='%s' frames=%d..%d rate=%d",
+ state.roomName.c_str(), loopStartFrame, loopLastFrame, kRoomPlayerIdleAnimationRate);
+ return true;
+ }
+
+ if (!idleState.exiting)
+ return false;
+
+ const int exitLastFrame = MIN(lastFrame, kRoomPlayerIdleExitLastFrame);
+ if (idleState.entity->getCurrentFrame() < exitLastFrame)
+ return false;
+
+ return finishIdleAnimation(state);
+}
+
+} // End of namespace Harvester
Commit: b8478f45224c349ce0572ec4d5aab426d25dead9
https://github.com/scummvm/scummvm/commit/b8478f45224c349ce0572ec4d5aab426d25dead9
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate flow startup diagnostics
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/flow_debug.cpp
engines/harvester/flow.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 26c6cfe1994..3487e98b744 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -22,7 +22,6 @@
#include "harvester/flow.h"
#include "common/algorithm.h"
-#include "common/config-manager.h"
#include "common/debug.h"
#include "common/events.h"
#include "common/formats/ini-file.h"
@@ -75,7 +74,6 @@ static const char *const kTownMapBitmapPaths[] = {
static const int kCursorSequence7 = 7;
static const int kNativeIdentTextLineSpacing = 3;
static const char *const kPlayerActorEntityName = "PLAYER";
-// 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;
@@ -92,14 +90,13 @@ static const char *const kDemoIntroductionText =
"the Order of the Harvest Moon, a bizarre institution of secretive knowledge. "
"However, the only thing you really care about is escaping from this hellish reality...";
static const float kTownMapNightPaletteBrightness = 0.6f;
-// Native keeps WAIT_BM visible while room construction runs; fast local loads need a short floor.
+// Keep WAIT_BM visible while room construction runs
static const uint32 kTransitionWaitFrameMinMs = 120;
static const int kTownMapEdgeThreshold = 9;
static const int kTownMapCursorHitExtent = 5;
static const byte kTownMapLabelColor = 0x28;
-static const int kCursorSequenceWalk = 0;
} // End of anonymous namespace
static int clampTownMapPanelIndex(int panelIndex) {
@@ -234,11 +231,6 @@ static bool applyStartupActorPlacement(const RoomSetupState &state, Entity &enti
state.playerSpawnX, state.playerSpawnY, (float)state.playerSpawnZ, &state.entranceName);
}
-void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness) {
- debugC(2, kDebugRoom, "Harvester: %s room='%s' palette='%s' brightness=%.2f",
- label, scene.state.roomName.c_str(), scene.state.palettePath.c_str(), (double)brightness);
-}
-
bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap) {
if (screen.w <= 0 || screen.h <= 0 || screen.format.bytesPerPixel != 1)
return false;
@@ -283,11 +275,6 @@ bool loadBitmapResource(ResourceManager &resources, const Common::String &path,
return decodeBitmap(data, bitmap);
}
-bool shouldRunStartupRoomProbe() {
- return ConfMan.hasKey("harvester_debug_probe_startup_room") &&
- ConfMan.getBool("harvester_debug_probe_startup_room");
-}
-
bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord ®ion) {
return region.desiredFacing < 0 || playerFacing == region.desiredFacing;
}
@@ -297,10 +284,6 @@ bool doesPlayerOverlapRegion(const Entity &playerEntity, const RegionRecord ®
if (regionBounds.isEmpty())
return false;
- // Native do_entity_screen_bounds_overlap (0x4b700) compares each entity's
- // origin plus width/height and rejects only when one edge is strictly before
- // the other. Common::Rect uses exclusive right/bottom edges, so intersects()
- // would incorrectly reject the edge contact that activates narrow exits.
const Common::Rect playerBounds = playerEntity.getScreenRect();
if (playerBounds.right < regionBounds.left ||
regionBounds.right < playerBounds.left ||
@@ -318,153 +301,6 @@ RoomHoverState Flow::getRoomHoverState(const SceneState &scene) const {
scene.state.roomNpcs, scene.sceneRegions, _mousePos, _menuTextConfig, &_dialogue);
}
-static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Array<ObjectRecord> &sceneObjects,
- const ObjectRecord &object, Common::Point &probePoint) {
- const Common::Rect bounds = SceneEntities::getSceneObjectBounds(object);
- if (bounds.isEmpty())
- return false;
-
- const Common::Point center((bounds.left + bounds.right - 1) / 2, (bounds.top + bounds.bottom - 1) / 2);
- const Common::Point corners[] = {
- center,
- Common::Point(bounds.left, bounds.top),
- Common::Point(bounds.right - 1, bounds.top),
- Common::Point(bounds.left, bounds.bottom - 1),
- Common::Point(bounds.right - 1, bounds.bottom - 1)
- };
- for (const Common::Point &candidate : corners) {
- const ObjectRecord *hit = Input::findRoomObjectAtPoint(engine, sceneObjects, candidate);
- if (hit && hit->objectName.equalsIgnoreCase(object.objectName)) {
- probePoint = candidate;
- return true;
- }
- }
-
- const int xStep = MAX<int>(1, bounds.width() / 6);
- const int yStep = MAX<int>(1, bounds.height() / 6);
- for (int y = bounds.top; y < bounds.bottom; y += yStep) {
- for (int x = bounds.left; x < bounds.right; x += xStep) {
- const Common::Point candidate(x, y);
- const ObjectRecord *hit = Input::findRoomObjectAtPoint(engine, sceneObjects, candidate);
- if (hit && hit->objectName.equalsIgnoreCase(object.objectName)) {
- probePoint = candidate;
- return true;
- }
- }
- }
-
- return false;
-}
-
-void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &entranceName) {
- EntityManager *entityManager = _engine.getRuntimeEntities();
- Script *script = _engine.getScript();
- if (!entityManager || !script)
- return;
-
- if (const Entity *cursor = entityManager->getCursorEntity()) {
- uint32 framePixels = 0;
- uint32 transparentPixels = 0;
- uint32 preservedPixels = 0;
- if (cursor->measureCurrentFrameTransparency(framePixels, transparentPixels, preservedPixels)) {
- debugC(1, kDebugCursor,
- "Harvester: startup probe cursor sequence=%d frame=%d pixels=%u transparent_pixels=%u preserved_pixels=%u",
- cursor->getAnimationSequence(), cursor->getCurrentFrame(), framePixels, transparentPixels, preservedPixels);
- }
- }
-
- if (const Entity *player = entityManager->findSceneEntityByName(kPlayerActorEntityName)) {
- uint32 framePixels = 0;
- uint32 transparentPixels = 0;
- uint32 preservedPixels = 0;
- (void)player->measureCurrentFrameTransparency(framePixels, transparentPixels, preservedPixels);
- const Common::Rect playerRect = player->getScreenRect();
- const Common::Rect screenRect(0, 0, 640, 480);
- debugC(1, kDebugPlayer,
- "Harvester: startup probe player room='%s' entrance='%s' frame=%d rect=(%d,%d)-(%d,%d) visible=%d intersects_screen=%d opaque_pixels=%u",
- scene.state.roomName.c_str(), entranceName.c_str(), player->getCurrentFrame(),
- playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
- player->isVisible(), playerRect.intersects(screenRect), framePixels - transparentPixels);
- }
-
- for (const ObjectRecord &object : scene.sceneObjects) {
- if (SceneEntities::getSceneObjectBounds(object).isEmpty())
- continue;
-
- Common::Point probePoint;
- if (!findRoomObjectProbePoint(_engine, scene.sceneObjects, object, probePoint))
- continue;
-
- const ObjectRecord *hoveredObject = Input::findRoomObjectAtPoint(_engine, scene.sceneObjects, probePoint);
- if (!hoveredObject)
- continue;
-
- _mousePos = probePoint;
- const Common::String objectLabel = script->resolveObjectLabel(*hoveredObject);
- ResolvedText inspectText;
- const RoomHoverState hoverState = Input::resolveRoomHoverState(
- _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
- probePoint, _menuTextConfig);
- const bool hasInteraction = script->hasObjectInteraction(*hoveredObject);
- const bool hasInspectText = script->resolveObjectInspectText(*hoveredObject, inspectText);
- debugC(1, kDebugRoom,
- "Harvester: startup probe hotspot room='%s' object='%s' point=(%d,%d) label='%s' prompt='%s' cursor_sequence=%d action_tag='%s' interaction=%d next_room='%s' inspect=%d",
- scene.state.roomName.c_str(), hoveredObject->objectName.c_str(), probePoint.x, probePoint.y,
- objectLabel.c_str(), hoverState.promptText.c_str(), hoverState.cursorSequence,
- hoveredObject->actionTag.c_str(), hasInteraction,
- "", hasInspectText);
-
- if (Player::supportsMovementBand(scene.state)) {
- Common::Point floorProbe;
- bool foundFloorProbe = false;
- for (int y = scene.state.roomMaxZScreenY; y <= scene.state.roomMinZScreenY && !foundFloorProbe; y += 12) {
- for (int x = 48; x < 592; x += 16) {
- const RoomHoverState candidateHover = Input::resolveRoomHoverState(
- _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
- Common::Point(x, y), _menuTextConfig);
- if (candidateHover.cursorSequence == kCursorSequenceWalk) {
- floorProbe = Common::Point(x, y);
- foundFloorProbe = true;
- break;
- }
- }
- }
-
- const RoomHoverState floorHover = foundFloorProbe
- ? Input::resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
- scene.sceneRegions, floorProbe, _menuTextConfig)
- : RoomHoverState();
- debugC(1, kDebugRoom,
- "Harvester: startup probe floor room='%s' point=(%d,%d) found=%d cursor_sequence=%d prompt='%s'",
- scene.state.roomName.c_str(), floorProbe.x, floorProbe.y, foundFloorProbe,
- floorHover.cursorSequence, floorHover.promptText.c_str());
-
- Entity *player = entityManager->findSceneEntityByName(kPlayerActorEntityName);
- if (player && foundFloorProbe && floorHover.cursorSequence == kCursorSequenceWalk) {
- Player probePlayer;
- probePlayer.initialize(scene.state, player, 0);
- probePlayer.setMoveTargetFromScreenPoint(scene.state, floorProbe.x, floorProbe.y);
- for (int i = 0; i < 32 && probePlayer.getState().hasMoveTarget; ++i)
- (void)probePlayer.stepMoveTarget(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations);
-
- const Common::Rect movedRect = player->getScreenRect();
- debugC(1, kDebugPlayer,
- "Harvester: startup probe movement room='%s' target=(%d,%d) final_rect=(%d,%d)-(%d,%d) z=%.2f",
- scene.state.roomName.c_str(), floorProbe.x, floorProbe.y,
- movedRect.left, movedRect.top, movedRect.right, movedRect.bottom, (double)probePlayer.getState().z);
-
- player->setCurrentFrame(Player::resolveFacingFrame(scene.state.playerFacing));
- (void)applyRoomActorPlacement(scene.state, *player,
- scene.state.playerSpawnX, scene.state.playerSpawnY, (float)scene.state.playerSpawnZ);
- }
- }
- return;
- }
-
- debugC(1, kDebugRoom, "Harvester: startup probe found no interactive hotspot in room '%s'",
- scene.state.roomName.c_str());
-}
-
static Common::String trimAsciiLine(const Common::String &value) {
uint start = 0;
uint end = value.size();
diff --git a/engines/harvester/flow_debug.cpp b/engines/harvester/flow_debug.cpp
new file mode 100644
index 00000000000..82f382fd813
--- /dev/null
+++ b/engines/harvester/flow_debug.cpp
@@ -0,0 +1,210 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/flow.h"
+
+#include "common/config-manager.h"
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/input.h"
+#include "harvester/player.h"
+#include "harvester/scene_entities.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kPlayerActorEntityName = "PLAYER";
+static const int kCursorSequenceWalk = 0;
+
+static void logStartupRoomEntities(EntityManager &entityManager, const SceneState &scene,
+ const Common::String &entranceName) {
+ if (const Entity *cursor = entityManager.getCursorEntity()) {
+ uint32 framePixels = 0;
+ uint32 transparentPixels = 0;
+ uint32 preservedPixels = 0;
+ if (cursor->measureCurrentFrameTransparency(framePixels, transparentPixels, preservedPixels)) {
+ debugC(1, kDebugCursor,
+ "Harvester: startup probe cursor sequence=%d frame=%d pixels=%u transparent_pixels=%u preserved_pixels=%u",
+ cursor->getAnimationSequence(), cursor->getCurrentFrame(), framePixels, transparentPixels, preservedPixels);
+ }
+ }
+
+ if (const Entity *player = entityManager.findSceneEntityByName(kPlayerActorEntityName)) {
+ uint32 framePixels = 0;
+ uint32 transparentPixels = 0;
+ uint32 preservedPixels = 0;
+ (void)player->measureCurrentFrameTransparency(framePixels, transparentPixels, preservedPixels);
+ const Common::Rect playerRect = player->getScreenRect();
+ const Common::Rect screenRect(0, 0, 640, 480);
+ debugC(1, kDebugPlayer,
+ "Harvester: startup probe player room='%s' entrance='%s' frame=%d rect=(%d,%d)-(%d,%d) visible=%d intersects_screen=%d opaque_pixels=%u",
+ scene.state.roomName.c_str(), entranceName.c_str(), player->getCurrentFrame(),
+ playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
+ player->isVisible(), playerRect.intersects(screenRect), framePixels - transparentPixels);
+ }
+}
+
+static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Array<ObjectRecord> &sceneObjects,
+ const ObjectRecord &object, Common::Point &probePoint) {
+ const Common::Rect bounds = SceneEntities::getSceneObjectBounds(object);
+ if (bounds.isEmpty())
+ return false;
+
+ const Common::Point center((bounds.left + bounds.right - 1) / 2, (bounds.top + bounds.bottom - 1) / 2);
+ const Common::Point corners[] = {
+ center,
+ Common::Point(bounds.left, bounds.top),
+ Common::Point(bounds.right - 1, bounds.top),
+ Common::Point(bounds.left, bounds.bottom - 1),
+ Common::Point(bounds.right - 1, bounds.bottom - 1)
+ };
+ for (const Common::Point &candidate : corners) {
+ const ObjectRecord *hit = Input::findRoomObjectAtPoint(engine, sceneObjects, candidate);
+ if (hit && hit->objectName.equalsIgnoreCase(object.objectName)) {
+ probePoint = candidate;
+ return true;
+ }
+ }
+
+ const int xStep = MAX<int>(1, bounds.width() / 6);
+ const int yStep = MAX<int>(1, bounds.height() / 6);
+ for (int y = bounds.top; y < bounds.bottom; y += yStep) {
+ for (int x = bounds.left; x < bounds.right; x += xStep) {
+ const Common::Point candidate(x, y);
+ const ObjectRecord *hit = Input::findRoomObjectAtPoint(engine, sceneObjects, candidate);
+ if (hit && hit->objectName.equalsIgnoreCase(object.objectName)) {
+ probePoint = candidate;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+static void logStartupRoomMovement(HarvesterEngine &engine, const SceneState &scene,
+ const MenuTextConfig &menuTextConfig, EntityManager &entityManager) {
+ if (Player::supportsMovementBand(scene.state)) {
+ Common::Point floorProbe;
+ bool foundFloorProbe = false;
+ for (int y = scene.state.roomMaxZScreenY; y <= scene.state.roomMinZScreenY && !foundFloorProbe; y += 12) {
+ for (int x = 48; x < 592; x += 16) {
+ const RoomHoverState candidateHover = Input::resolveRoomHoverState(
+ engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
+ Common::Point(x, y), menuTextConfig);
+ if (candidateHover.cursorSequence == kCursorSequenceWalk) {
+ floorProbe = Common::Point(x, y);
+ foundFloorProbe = true;
+ break;
+ }
+ }
+ }
+
+ const RoomHoverState floorHover = foundFloorProbe
+ ? Input::resolveRoomHoverState(engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
+ scene.sceneRegions, floorProbe, menuTextConfig)
+ : RoomHoverState();
+ debugC(1, kDebugRoom,
+ "Harvester: startup probe floor room='%s' point=(%d,%d) found=%d cursor_sequence=%d prompt='%s'",
+ scene.state.roomName.c_str(), floorProbe.x, floorProbe.y, foundFloorProbe,
+ floorHover.cursorSequence, floorHover.promptText.c_str());
+
+ Entity *player = entityManager.findSceneEntityByName(kPlayerActorEntityName);
+ if (player && foundFloorProbe && floorHover.cursorSequence == kCursorSequenceWalk) {
+ Player probePlayer;
+ probePlayer.initialize(scene.state, player, 0);
+ probePlayer.setMoveTargetFromScreenPoint(scene.state, floorProbe.x, floorProbe.y);
+ for (int i = 0; i < 32 && probePlayer.getState().hasMoveTarget; ++i)
+ (void)probePlayer.stepMoveTarget(engine, scene.state, scene.sceneObjects, scene.sceneAnimations);
+
+ const Common::Rect movedRect = player->getScreenRect();
+ debugC(1, kDebugPlayer,
+ "Harvester: startup probe movement room='%s' target=(%d,%d) final_rect=(%d,%d)-(%d,%d) z=%.2f",
+ scene.state.roomName.c_str(), floorProbe.x, floorProbe.y,
+ movedRect.left, movedRect.top, movedRect.right, movedRect.bottom, (double)probePlayer.getState().z);
+
+ player->setCurrentFrame(Player::resolveFacingFrame(scene.state.playerFacing));
+ (void)applyRoomActorPlacement(scene.state, *player,
+ scene.state.playerSpawnX, scene.state.playerSpawnY, (float)scene.state.playerSpawnZ);
+ }
+ }
+}
+
+} // End of anonymous namespace
+
+void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness) {
+ debugC(2, kDebugRoom, "Harvester: %s room='%s' palette='%s' brightness=%.2f",
+ label, scene.state.roomName.c_str(), scene.state.palettePath.c_str(), (double)brightness);
+}
+
+bool shouldRunStartupRoomProbe() {
+ return ConfMan.hasKey("harvester_debug_probe_startup_room") &&
+ ConfMan.getBool("harvester_debug_probe_startup_room");
+}
+
+void Flow::logStartupRoomProbe(const SceneState &scene, const Common::String &entranceName) {
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ Script *script = _engine.getScript();
+ if (!entityManager || !script)
+ return;
+
+ logStartupRoomEntities(*entityManager, scene, entranceName);
+
+ for (const ObjectRecord &object : scene.sceneObjects) {
+ if (SceneEntities::getSceneObjectBounds(object).isEmpty())
+ continue;
+
+ Common::Point probePoint;
+ if (!findRoomObjectProbePoint(_engine, scene.sceneObjects, object, probePoint))
+ continue;
+
+ const ObjectRecord *hoveredObject = Input::findRoomObjectAtPoint(_engine, scene.sceneObjects, probePoint);
+ if (!hoveredObject)
+ continue;
+
+ _mousePos = probePoint;
+ const Common::String objectLabel = script->resolveObjectLabel(*hoveredObject);
+ ResolvedText inspectText;
+ const RoomHoverState hoverState = Input::resolveRoomHoverState(
+ _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
+ probePoint, _menuTextConfig);
+ const bool hasInteraction = script->hasObjectInteraction(*hoveredObject);
+ const bool hasInspectText = script->resolveObjectInspectText(*hoveredObject, inspectText);
+ debugC(1, kDebugRoom,
+ "Harvester: startup probe hotspot room='%s' object='%s' point=(%d,%d) label='%s' prompt='%s' cursor_sequence=%d action_tag='%s' interaction=%d next_room='%s' inspect=%d",
+ scene.state.roomName.c_str(), hoveredObject->objectName.c_str(), probePoint.x, probePoint.y,
+ objectLabel.c_str(), hoverState.promptText.c_str(), hoverState.cursorSequence,
+ hoveredObject->actionTag.c_str(), hasInteraction,
+ "", hasInspectText);
+
+ logStartupRoomMovement(_engine, scene, _menuTextConfig, *entityManager);
+ return;
+ }
+
+ debugC(1, kDebugRoom, "Harvester: startup probe found no interactive hotspot in room '%s'",
+ scene.state.roomName.c_str());
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 55da1d403ab..eee964e72e2 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -8,6 +8,7 @@ MODULE_OBJS = \
console.o \
dialogue.o \
flow.o \
+ flow_debug.o \
fst_player.o \
harvester.o \
inventory.o \
Commit: bef83f3ebf9622e98e59db1350fec49632bd105d
https://github.com/scummvm/scummvm/commit/bef83f3ebf9622e98e59db1350fec49632bd105d
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate town map selection and event handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/flow_map.cpp
A engines/harvester/flow_support.h
engines/harvester/flow.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 3487e98b744..2e666ecce88 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -20,6 +20,7 @@
*/
#include "harvester/flow.h"
+#include "harvester/flow_support.h"
#include "common/algorithm.h"
#include "common/debug.h"
@@ -62,14 +63,6 @@ static const char *const kBuiltInMenuItems[] = {
"HELP",
"QUIT GAME"
};
-static const char *const kTownMapPalettePath = "1:/GRAPHIC/PAL/HARVMAP.PAL";
-static const char *const kTownMapMusicPath = "SOUND/MUSIC/MENACE.CMP";
-static const char *const kTownMapBitmapPaths[] = {
- "1:/GRAPHIC/TOWN/HARVMAP1.BM",
- "1:/GRAPHIC/TOWN/HARVMAP2.BM",
- "1:/GRAPHIC/TOWN/HARVMAP3.BM",
- "1:/GRAPHIC/TOWN/HARVMAP4.BM"
-};
static const int kCursorSequence7 = 7;
static const int kNativeIdentTextLineSpacing = 3;
@@ -89,101 +82,11 @@ static const char *const kDemoIntroductionText =
"After solving the enigmas of the Harvest town, you are thrust into the realm of "
"the Order of the Harvest Moon, a bizarre institution of secretive knowledge. "
"However, the only thing you really care about is escaping from this hellish reality...";
-static const float kTownMapNightPaletteBrightness = 0.6f;
// Keep WAIT_BM visible while room construction runs
static const uint32 kTransitionWaitFrameMinMs = 120;
-static const int kTownMapEdgeThreshold = 9;
-static const int kTownMapCursorHitExtent = 5;
-
-static const byte kTownMapLabelColor = 0x28;
} // End of anonymous namespace
-static int clampTownMapPanelIndex(int panelIndex) {
- return CLIP<int>(panelIndex, 0, ARRAYSIZE(kTownMapBitmapPaths) - 1);
-}
-
-static int resolveTownMapEdgePanel(int currentPanel, const Common::Point &mousePos, int width, int height) {
- if (mousePos.x <= kTownMapEdgeThreshold) {
- if (currentPanel == 1)
- return 0;
- if (currentPanel == 3)
- return 2;
- } else if (mousePos.x >= width - 1 - kTownMapEdgeThreshold) {
- if (currentPanel == 0)
- return 1;
- if (currentPanel == 2)
- return 3;
- } else if (mousePos.y <= kTownMapEdgeThreshold) {
- if (currentPanel == 2)
- return 0;
- if (currentPanel == 3)
- return 1;
- } else if (mousePos.y >= height - 1 - kTownMapEdgeThreshold) {
- if (currentPanel == 0)
- return 2;
- if (currentPanel == 1)
- return 3;
- }
-
- return currentPanel;
-}
-
-static int resolveTownMapKeyPanel(int currentPanel, Common::KeyCode keycode) {
- switch (keycode) {
- case Common::KEYCODE_LEFT:
- if (currentPanel == 1)
- return 0;
- if (currentPanel == 3)
- return 2;
- break;
- case Common::KEYCODE_RIGHT:
- if (currentPanel == 0)
- return 1;
- if (currentPanel == 2)
- return 3;
- break;
- case Common::KEYCODE_UP:
- if (currentPanel == 2)
- return 0;
- if (currentPanel == 3)
- return 1;
- break;
- case Common::KEYCODE_DOWN:
- if (currentPanel == 0)
- return 2;
- if (currentPanel == 1)
- return 3;
- break;
- default:
- break;
- }
-
- return currentPanel;
-}
-
-static bool isTownMapLocationHovered(const MapLocationRecord &location, int currentPanel,
- const Common::Point &mousePos) {
- if (location.panelIndex != currentPanel)
- return false;
-
- const int cursorRight = mousePos.x + kTownMapCursorHitExtent;
- const int cursorBottom = mousePos.y + kTownMapCursorHitExtent;
- return mousePos.x <= location.maxX && cursorRight >= location.minX &&
- mousePos.y <= location.maxY && cursorBottom >= location.minY;
-}
-
-static const MapLocationRecord *findTownMapLocationAt(
- const Common::Array<MapLocationRecord> &locations, int currentPanel,
- const Common::Point &mousePos) {
- for (const MapLocationRecord &location : locations) {
- if (isTownMapLocationHovered(location, currentPanel, mousePos))
- return &location;
- }
-
- return nullptr;
-}
-
static void setRoomActorScreenPosition(Entity &entity, int centerX, int bottomY, float z,
int width, int height, int xOffset, int yOffset) {
entity.setPosition(centerX - xOffset - width / 2, bottomY - height - yOffset, z);
@@ -244,7 +147,7 @@ bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap
return true;
}
-static void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
+void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
byte scaledPalette[256 * 3];
const float gammaBrightness = g_engine ? g_engine->getGammaBrightnessScale() : 1.0f;
buildHarvesterDisplayPalette(palette, brightness * gammaBrightness, scaledPalette);
@@ -780,159 +683,6 @@ Common::Error Flow::runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte
executionContext, npc, usedItemName, *this);
}
-Common::Error Flow::runTownMapSelector(const Common::String &mapEntryName,
- Common::String &destinationEntranceName) {
- destinationEntranceName.clear();
-
- Script *script = _engine.getScript();
- ResourceManager *resources = _engine.getResources();
- Graphics::Screen *screen = _engine.getScreen();
- const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- Common::ScopedPtr<HarvesterCftFont> townMapFont;
- const Text *text = _engine.getText();
- if (const CftFontResource *townMapFontResource = text ? text->findFont("TEXTFONT") : nullptr) {
- townMapFont.reset(new HarvesterCftFont(*townMapFontResource));
- if (townMapFont->isValid())
- font = townMapFont.get();
- else
- townMapFont.reset();
- }
- if (!script || !resources || !screen || !font)
- return Common::kReadingFailed;
-
- const MapEntranceRecord *mapEntrance = script->findMapEntranceRecord(mapEntryName);
- if (!mapEntrance) {
- warning("Harvester: unresolved town map entry '%s'", mapEntryName.c_str());
- return Common::kReadingFailed;
- }
- const float paletteBrightness = script->getFlagValue("DAY_FLAG")
- ? 1.0f
- : kTownMapNightPaletteBrightness;
-
- byte palette[256 * 3];
- if (!Art::loadPaletteResource(*resources, kTownMapPalettePath, palette))
- return Common::kReadingFailed;
-
- Common::Array<IndexedBitmap> panels;
- panels.resize(ARRAYSIZE(kTownMapBitmapPaths));
- for (uint i = 0; i < ARRAYSIZE(kTownMapBitmapPaths); ++i) {
- if (!loadBitmapResource(*resources, kTownMapBitmapPaths[i], panels[i]))
- return Common::kReadingFailed;
- }
-
- const Common::String previousMusicPath = _engine.getMusicPath();
- (void)_engine.playMusic(kTownMapMusicPath);
-
- if (!ensureCursorEntity())
- return Common::kReadingFailed;
- resetCursorAnimationSequence();
-
- if (_mousePos.x < 0 || _mousePos.y < 0 || _mousePos.x >= screen->w || _mousePos.y >= screen->h)
- _mousePos = Common::Point(screen->w / 2, screen->h / 2);
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- (void)entityManager->syncCursorEntityPosition(_mousePos);
-
- int currentPanel = clampTownMapPanelIndex(mapEntrance->initialPanelIndex);
- bool needsRedraw = true;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- auto centerCursor = [&]() {
- _mousePos = Common::Point(screen->w / 2, screen->h / 2);
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- (void)entityManager->syncCursorEntityPosition(_mousePos);
- };
- auto restorePreviousMusic = [&]() {
- if (!destinationEntranceName.empty())
- return;
- if (!previousMusicPath.empty())
- (void)_engine.playMusic(previousMusicPath);
- else
- _engine.stopMusic();
- };
-
- while (!_engine.shouldQuit()) {
- const int edgePanel = resolveTownMapEdgePanel(currentPanel, _mousePos, screen->w, screen->h);
- if (edgePanel != currentPanel) {
- currentPanel = edgePanel;
- centerCursor();
- needsRedraw = true;
- }
-
- const MapLocationRecord *hoveredLocation =
- findTownMapLocationAt(script->getMapLocations(), currentPanel, _mousePos);
- if (needsRedraw) {
- setScaledPalette(*screen, palette, paletteBrightness);
- screen->fillRect(screen->getBounds(), 0);
- RoomPresentation::blitBitmap(*screen, panels[(uint)currentPanel], 0, 0);
- if (hoveredLocation) {
- RoomPresentation::drawShadowedString(*screen, *font, hoveredLocation->labelText,
- hoveredLocation->labelX, hoveredLocation->labelY, screen->w, kTownMapLabelColor);
- }
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- entityManager->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (handleSystemEvent(event, result)) {
- restorePreviousMusic();
- return result;
- }
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN: {
- const MapLocationRecord *clickedLocation =
- findTownMapLocationAt(script->getMapLocations(), currentPanel, _mousePos);
- if (!clickedLocation)
- break;
-
- const EntranceRecord *destinationEntrance =
- script->findEntranceRecord(clickedLocation->destinationEntranceName);
- if (!destinationEntrance) {
- warning("Harvester: unresolved town map destination '%s' from '%s'",
- clickedLocation->destinationEntranceName.c_str(), mapEntryName.c_str());
- break;
- }
-
- destinationEntranceName = destinationEntrance->entranceName;
- debugC(1, kDebugGeneral,
- "Harvester: town map selection entry='%s' panel=%d label='%s' destination='%s'",
- mapEntryName.c_str(), currentPanel, clickedLocation->labelText.c_str(),
- destinationEntranceName.c_str());
- return Common::kNoError;
- }
- case Common::EVENT_KEYDOWN: {
- const int nextPanel = resolveTownMapKeyPanel(currentPanel, event.kbd.keycode);
- if (nextPanel != currentPanel) {
- currentPanel = nextPanel;
- centerCursor();
- needsRedraw = true;
- }
- break;
- }
- default:
- break;
- }
- }
-
- if (tickRuntimeEntities())
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- restorePreviousMusic();
- return Common::kNoError;
-}
-
Common::Error Flow::resolveRoomTransitionTarget(const Common::String &targetName,
Common::String &resolvedTargetName) {
resolvedTargetName = targetName;
diff --git a/engines/harvester/flow_map.cpp b/engines/harvester/flow_map.cpp
new file mode 100644
index 00000000000..fc3eaa61859
--- /dev/null
+++ b/engines/harvester/flow_map.cpp
@@ -0,0 +1,352 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/flow.h"
+#include "harvester/flow_support.h"
+
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/ptr.h"
+#include "common/system.h"
+#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
+#include "harvester/cft_font.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/room_presentation.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kTownMapPalettePath = "1:/GRAPHIC/PAL/HARVMAP.PAL";
+static const char *const kTownMapMusicPath = "SOUND/MUSIC/MENACE.CMP";
+static const char *const kTownMapBitmapPaths[] = {
+ "1:/GRAPHIC/TOWN/HARVMAP1.BM",
+ "1:/GRAPHIC/TOWN/HARVMAP2.BM",
+ "1:/GRAPHIC/TOWN/HARVMAP3.BM",
+ "1:/GRAPHIC/TOWN/HARVMAP4.BM"
+};
+static const float kTownMapNightPaletteBrightness = 0.6f;
+static const int kTownMapEdgeThreshold = 9;
+static const int kTownMapCursorHitExtent = 5;
+static const byte kTownMapLabelColor = 0x28;
+
+static int clampTownMapPanelIndex(int panelIndex) {
+ return CLIP<int>(panelIndex, 0, ARRAYSIZE(kTownMapBitmapPaths) - 1);
+}
+
+static int resolveTownMapEdgePanel(int currentPanel, const Common::Point &mousePos, int width, int height) {
+ if (mousePos.x <= kTownMapEdgeThreshold) {
+ if (currentPanel == 1)
+ return 0;
+ if (currentPanel == 3)
+ return 2;
+ } else if (mousePos.x >= width - 1 - kTownMapEdgeThreshold) {
+ if (currentPanel == 0)
+ return 1;
+ if (currentPanel == 2)
+ return 3;
+ } else if (mousePos.y <= kTownMapEdgeThreshold) {
+ if (currentPanel == 2)
+ return 0;
+ if (currentPanel == 3)
+ return 1;
+ } else if (mousePos.y >= height - 1 - kTownMapEdgeThreshold) {
+ if (currentPanel == 0)
+ return 2;
+ if (currentPanel == 1)
+ return 3;
+ }
+
+ return currentPanel;
+}
+
+static int resolveTownMapKeyPanel(int currentPanel, Common::KeyCode keycode) {
+ switch (keycode) {
+ case Common::KEYCODE_LEFT:
+ if (currentPanel == 1)
+ return 0;
+ if (currentPanel == 3)
+ return 2;
+ break;
+ case Common::KEYCODE_RIGHT:
+ if (currentPanel == 0)
+ return 1;
+ if (currentPanel == 2)
+ return 3;
+ break;
+ case Common::KEYCODE_UP:
+ if (currentPanel == 2)
+ return 0;
+ if (currentPanel == 3)
+ return 1;
+ break;
+ case Common::KEYCODE_DOWN:
+ if (currentPanel == 0)
+ return 2;
+ if (currentPanel == 1)
+ return 3;
+ break;
+ default:
+ break;
+ }
+
+ return currentPanel;
+}
+
+static bool isTownMapLocationHovered(const MapLocationRecord &location, int currentPanel,
+ const Common::Point &mousePos) {
+ if (location.panelIndex != currentPanel)
+ return false;
+
+ const int cursorRight = mousePos.x + kTownMapCursorHitExtent;
+ const int cursorBottom = mousePos.y + kTownMapCursorHitExtent;
+ return mousePos.x <= location.maxX && cursorRight >= location.minX &&
+ mousePos.y <= location.maxY && cursorBottom >= location.minY;
+}
+
+static const MapLocationRecord *findTownMapLocationAt(
+ const Common::Array<MapLocationRecord> &locations, int currentPanel,
+ const Common::Point &mousePos) {
+ for (const MapLocationRecord &location : locations) {
+ if (isTownMapLocationHovered(location, currentPanel, mousePos))
+ return &location;
+ }
+
+ return nullptr;
+}
+
+class TownMapSelector {
+public:
+ TownMapSelector(HarvesterEngine &engine, Flow &flow, Common::Point &mousePos,
+ const Common::String &mapEntryName, Common::String &destinationEntranceName)
+ : _engine(engine), _flow(flow), _mousePos(mousePos), _mapEntryName(mapEntryName),
+ _destinationEntranceName(destinationEntranceName) {}
+
+ bool load();
+ Common::Error run();
+
+private:
+ void centerCursor();
+ void restorePreviousMusic();
+ void draw(const MapLocationRecord *hoveredLocation);
+ bool handleEvent(const Common::Event &event);
+
+ HarvesterEngine &_engine;
+ Flow &_flow;
+ Common::Point &_mousePos;
+ const Common::String &_mapEntryName;
+ Common::String &_destinationEntranceName;
+ Script *_script = nullptr;
+ Graphics::Screen *_screen = nullptr;
+ const Graphics::Font *_font = nullptr;
+ Common::ScopedPtr<HarvesterCftFont> _townMapFont;
+ const MapEntranceRecord *_mapEntrance = nullptr;
+ float _paletteBrightness = 1.0f;
+ byte _palette[256 * 3];
+ Common::Array<IndexedBitmap> _panels;
+ Common::String _previousMusicPath;
+ int _currentPanel = 0;
+ bool _needsRedraw = true;
+};
+
+bool TownMapSelector::load() {
+ _script = _engine.getScript();
+ ResourceManager *resources = _engine.getResources();
+ _screen = _engine.getScreen();
+ _font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ const Text *text = _engine.getText();
+ if (const CftFontResource *townMapFontResource = text ? text->findFont("TEXTFONT") : nullptr) {
+ _townMapFont.reset(new HarvesterCftFont(*townMapFontResource));
+ if (_townMapFont->isValid())
+ _font = _townMapFont.get();
+ else
+ _townMapFont.reset();
+ }
+ if (!_script || !resources || !_screen || !_font)
+ return false;
+
+ _mapEntrance = _script->findMapEntranceRecord(_mapEntryName);
+ if (!_mapEntrance) {
+ warning("Harvester: unresolved town map entry '%s'", _mapEntryName.c_str());
+ return false;
+ }
+ _paletteBrightness = _script->getFlagValue("DAY_FLAG")
+ ? 1.0f
+ : kTownMapNightPaletteBrightness;
+
+ if (!Art::loadPaletteResource(*resources, kTownMapPalettePath, _palette))
+ return false;
+
+ _panels.resize(ARRAYSIZE(kTownMapBitmapPaths));
+ for (uint i = 0; i < ARRAYSIZE(kTownMapBitmapPaths); ++i) {
+ if (!loadBitmapResource(*resources, kTownMapBitmapPaths[i], _panels[i]))
+ return false;
+ }
+
+ _previousMusicPath = _engine.getMusicPath();
+ (void)_engine.playMusic(kTownMapMusicPath);
+
+ return true;
+}
+
+void TownMapSelector::centerCursor() {
+ _mousePos = Common::Point(_screen->w / 2, _screen->h / 2);
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ (void)entityManager->syncCursorEntityPosition(_mousePos);
+}
+
+void TownMapSelector::restorePreviousMusic() {
+ if (!_destinationEntranceName.empty())
+ return;
+ if (!_previousMusicPath.empty())
+ (void)_engine.playMusic(_previousMusicPath);
+ else
+ _engine.stopMusic();
+}
+
+void TownMapSelector::draw(const MapLocationRecord *hoveredLocation) {
+ setScaledPalette(*_screen, _palette, _paletteBrightness);
+ _screen->fillRect(_screen->getBounds(), 0);
+ RoomPresentation::blitBitmap(*_screen, _panels[(uint)_currentPanel], 0, 0);
+ if (hoveredLocation) {
+ RoomPresentation::drawShadowedString(*_screen, *_font, hoveredLocation->labelText,
+ hoveredLocation->labelX, hoveredLocation->labelY, _screen->w, kTownMapLabelColor);
+ }
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ entityManager->drawCursor(*_screen);
+ _screen->makeAllDirty();
+ _screen->update();
+}
+
+bool TownMapSelector::handleEvent(const Common::Event &event) {
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ _needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN: {
+ const MapLocationRecord *clickedLocation =
+ findTownMapLocationAt(_script->getMapLocations(), _currentPanel, _mousePos);
+ if (!clickedLocation)
+ break;
+
+ const EntranceRecord *destinationEntrance =
+ _script->findEntranceRecord(clickedLocation->destinationEntranceName);
+ if (!destinationEntrance) {
+ warning("Harvester: unresolved town map destination '%s' from '%s'",
+ clickedLocation->destinationEntranceName.c_str(), _mapEntryName.c_str());
+ break;
+ }
+
+ _destinationEntranceName = destinationEntrance->entranceName;
+ debugC(1, kDebugGeneral,
+ "Harvester: town map selection entry='%s' panel=%d label='%s' destination='%s'",
+ _mapEntryName.c_str(), _currentPanel, clickedLocation->labelText.c_str(),
+ _destinationEntranceName.c_str());
+ return true;
+ }
+ case Common::EVENT_KEYDOWN: {
+ const int nextPanel = resolveTownMapKeyPanel(_currentPanel, event.kbd.keycode);
+ if (nextPanel != _currentPanel) {
+ _currentPanel = nextPanel;
+ centerCursor();
+ _needsRedraw = true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+Common::Error TownMapSelector::run() {
+ if (_mousePos.x < 0 || _mousePos.y < 0 || _mousePos.x >= _screen->w || _mousePos.y >= _screen->h)
+ _mousePos = Common::Point(_screen->w / 2, _screen->h / 2);
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ (void)entityManager->syncCursorEntityPosition(_mousePos);
+
+ _currentPanel = clampTownMapPanelIndex(_mapEntrance->initialPanelIndex);
+ _needsRedraw = true;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit()) {
+ const int edgePanel = resolveTownMapEdgePanel(_currentPanel, _mousePos, _screen->w, _screen->h);
+ if (edgePanel != _currentPanel) {
+ _currentPanel = edgePanel;
+ centerCursor();
+ _needsRedraw = true;
+ }
+
+ const MapLocationRecord *hoveredLocation =
+ findTownMapLocationAt(_script->getMapLocations(), _currentPanel, _mousePos);
+ if (_needsRedraw) {
+ draw(hoveredLocation);
+ _needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result)) {
+ restorePreviousMusic();
+ return result;
+ }
+
+ if (handleEvent(event))
+ return Common::kNoError;
+ }
+
+ if (_flow.tickRuntimeEntities())
+ _needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ restorePreviousMusic();
+ return Common::kNoError;
+}
+
+} // End of anonymous namespace
+
+Common::Error Flow::runTownMapSelector(const Common::String &mapEntryName,
+ Common::String &destinationEntranceName) {
+ destinationEntranceName.clear();
+
+ TownMapSelector selector(_engine, *this, _mousePos, mapEntryName, destinationEntranceName);
+ if (!selector.load())
+ return Common::kReadingFailed;
+
+ if (!ensureCursorEntity())
+ return Common::kReadingFailed;
+ resetCursorAnimationSequence();
+
+ return selector.run();
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/flow_support.h b/engines/harvester/flow_support.h
new file mode 100644
index 00000000000..3693f99a901
--- /dev/null
+++ b/engines/harvester/flow_support.h
@@ -0,0 +1,41 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef HARVESTER_FLOW_SUPPORT_H
+#define HARVESTER_FLOW_SUPPORT_H
+
+#include "common/str.h"
+
+namespace Graphics {
+class Screen;
+}
+
+namespace Harvester {
+
+class ResourceManager;
+struct IndexedBitmap;
+
+void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness);
+bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap);
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_FLOW_SUPPORT_H
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index eee964e72e2..88572fb12af 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -9,6 +9,7 @@ MODULE_OBJS = \
dialogue.o \
flow.o \
flow_debug.o \
+ flow_map.o \
fst_player.o \
harvester.o \
inventory.o \
Commit: 998e763b93043cdd3ca7b911d03011dfbdb559ae
https://github.com/scummvm/scummvm/commit/998e763b93043cdd3ca7b911d03011dfbdb559ae
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate startup overlays and transitions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/flow_startup.cpp
A engines/harvester/flow_transition.cpp
engines/harvester/flow.cpp
engines/harvester/flow.h
engines/harvester/module.mk
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 2e666ecce88..b758f609e27 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -20,31 +20,18 @@
*/
#include "harvester/flow.h"
-#include "harvester/flow_support.h"
#include "common/algorithm.h"
#include "common/debug.h"
#include "common/events.h"
-#include "common/formats/ini-file.h"
#include "common/memstream.h"
-#include "common/ptr.h"
#include "common/serializer.h"
-#include "common/system.h"
-#include "graphics/font.h"
-#include "graphics/fontman.h"
-#include "graphics/framelimiter.h"
-#include "harvester/cft_font.h"
#include "harvester/detection.h"
-#include "harvester/fst_player.h"
#include "harvester/harvester.h"
#include "harvester/input.h"
-#include "harvester/palette_utils.h"
#include "harvester/resources.h"
#include "harvester/entity.h"
#include "harvester/scene_entities.h"
-#include "harvester/room_presentation.h"
-#include "harvester/art.h"
-#include "harvester/monster.h"
#include "harvester/player.h"
#include "harvester/script.h"
@@ -52,38 +39,8 @@ namespace Harvester {
namespace {
-static const char *const kQuickTipsPath = "ADJHEAD.RCS";
-static const char *const kMenuPath = "MENU.INI";
-static const char *const kMenuSectionName = "menu";
-static const char *const kBuiltInMenuItems[] = {
- "NEW GAME",
- "SAVE GAME",
- "LOAD GAME",
- "OPTIONS",
- "HELP",
- "QUIT GAME"
-};
-
static const int kCursorSequence7 = 7;
-static const int kNativeIdentTextLineSpacing = 3;
static const char *const kPlayerActorEntityName = "PLAYER";
-static const uint32 kPaletteFadeIntervalTicks = 4;
-static const float kPaletteBrightnessBlack = 0.0f;
-static const float kPaletteBrightnessFull = 1.0f;
-static const uint32 kDemoEndingScreenDurationMs = 10000;
-static const int kDemoEndingScreenNumbers[] = { 1, 2, 3, 4, 6 };
-static const char *const kDemoEndingMusicPath = "SOUND/MUSIC/ROCKFITE.CMP";
-static const uint32 kDemoIntroductionDurationMs = 17000;
-static const char *const kDemoIntroductionBitmapPath = "1:/GRAPHIC/OTHER/MAINMENU.BM";
-static const char *const kDemoIntroductionPalettePath = "1:/GRAPHIC/PAL/MAINMENU.PAL";
-static const char *const kDemoIntroductionMusicPath = "SOUND/MUSIC/LODGERB.CMP";
-static const char *const kDemoIntroductionFstPath = "GRAPHIC/FST/PCFALL.FST";
-static const char *const kDemoIntroductionText =
- "After solving the enigmas of the Harvest town, you are thrust into the realm of "
- "the Order of the Harvest Moon, a bizarre institution of secretive knowledge. "
- "However, the only thing you really care about is escaping from this hellish reality...";
-// Keep WAIT_BM visible while room construction runs
-static const uint32 kTransitionWaitFrameMinMs = 120;
} // End of anonymous namespace
@@ -147,21 +104,6 @@ bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap
return true;
}
-void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
- byte scaledPalette[256 * 3];
- const float gammaBrightness = g_engine ? g_engine->getGammaBrightnessScale() : 1.0f;
- buildHarvesterDisplayPalette(palette, brightness * gammaBrightness, scaledPalette);
- screen.setPalette(scaledPalette);
-}
-
-static void setScaledDisplayPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
- byte scaledPalette[256 * 3];
- const float clampedBrightness = CLIP<float>(brightness, 0.0f, 1.0f);
- for (uint i = 0; i < ARRAYSIZE(scaledPalette); ++i)
- scaledPalette[i] = (byte)CLIP<int>((int)(palette[i] * clampedBrightness + 0.5f), 0, 255);
- screen.setPalette(scaledPalette);
-}
-
static Common::Rect getRegionBounds(const RegionRecord ®ion) {
if (region.right > region.left && region.bottom > region.top)
return Common::Rect(region.left, region.top, region.right, region.bottom);
@@ -169,15 +111,6 @@ static Common::Rect getRegionBounds(const RegionRecord ®ion) {
return Common::Rect();
}
-bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
- Common::Array<byte> data;
- if (!resources.loadFile(path, data) || data.size() < 12)
- return false;
-
- bitmap = IndexedBitmap();
- return decodeBitmap(data, bitmap);
-}
-
bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord ®ion) {
return region.desiredFacing < 0 || playerFacing == region.desiredFacing;
}
@@ -204,54 +137,12 @@ RoomHoverState Flow::getRoomHoverState(const SceneState &scene) const {
scene.state.roomNpcs, scene.sceneRegions, _mousePos, _menuTextConfig, &_dialogue);
}
-static Common::String trimAsciiLine(const Common::String &value) {
- uint start = 0;
- uint end = value.size();
-
- while (start < end && (value[start] == ' ' || value[start] == '\t'))
- ++start;
- while (end > start && (value[end - 1] == ' ' || value[end - 1] == '\t' || value[end - 1] == '\r'))
- --end;
-
- return value.substr(start, end - start);
-}
-
-static bool loadQuickTipsScene(HarvesterEngine &engine, SceneState &scene) {
- RoomSetupState state;
- // The original startup path enters the START room first and overlays quick tips on top of it.
- if (!engine.getScript()->resolveRoomSetupState("START", state, *engine.getResources()))
- return false;
-
- return SceneEntities::loadRoomSceneResources(state, *engine.getResources(), scene);
-}
-
-static void renderQuickTipsScreen(HarvesterEngine &engine, const SceneState &scene,
- const MenuTextConfig &config, const QuickTipsLayout &layout,
- const Common::String &tipText) {
- Graphics::Screen *screen = engine.getScreen();
- if (!screen)
- return;
-
- RoomPresentation::drawRoomScene(engine, *screen, scene, scene.targetPaletteBrightness);
- drawQuickTipsPanel(engine, config, layout, tipText);
-
- if (engine.getRuntimeEntities())
- engine.getRuntimeEntities()->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
-}
-
Flow::Flow(HarvesterEngine &engine)
: _engine(engine), _mousePos(320, 200), _dialogue(engine, _mousePos), _inventory(engine),
_menu(engine, _mousePos, _menuItems), _cheats(engine),
_room(engine, _inventory, _cheats) {
}
-bool Flow::load() {
- (void)loadMenuTextConfig(_engine, _menuTextConfig);
- return loadQuickTips() && loadMenuItems();
-}
-
bool Flow::buildDialogueSaveStateBlob(Common::Array<byte> &blob, uint32 saveVersion) {
Common::MemoryWriteStreamDynamic stream(DisposeAfterUse::NO);
Common::Serializer serializer(nullptr, &stream);
@@ -322,353 +213,10 @@ Common::Error Flow::run() {
return runMainMenu();
}
-bool Flow::loadQuickTips() {
- _quickTips.clear();
-
- Common::Array<byte> data;
- if (!_engine.getResources()->loadFile(kQuickTipsPath, data)) {
- warning("Harvester: unable to load quick tips '%s'", kQuickTipsPath);
- return true;
- }
-
- Common::String currentLine;
- for (uint i = 0; i < data.size(); ++i) {
- const char ch = (char)data[i];
- if (ch == '\r')
- continue;
- if (ch == '\n') {
- Common::String trimmed = trimAsciiLine(currentLine);
- if (!trimmed.empty())
- _quickTips.push_back(Common::move(trimmed));
- currentLine.clear();
- continue;
- }
-
- currentLine += ch;
- }
-
- Common::String trimmed = trimAsciiLine(currentLine);
- if (!trimmed.empty())
- _quickTips.push_back(Common::move(trimmed));
-
- debugC(1, kDebugGeneral, "Harvester: loaded %u quick tips from '%s'", (uint)_quickTips.size(), kQuickTipsPath);
- return true;
-}
-
-bool Flow::loadMenuItems() {
- _menuItems.clear();
-
- Common::Array<byte> data;
- if (!_engine.getResources()->loadFile(kMenuPath, data)) {
- for (const char *item : kBuiltInMenuItems)
- _menuItems.push_back(item);
- debugC(2, kDebugGeneral,
- "Harvester: using %u built-in startup menu items because '%s' is unavailable",
- (uint)_menuItems.size(), kMenuPath);
- return true;
- }
-
- Common::MemoryReadStream stream(data.data(), data.size());
- Common::INIFile menu;
- menu.setDefaultSectionName(kMenuSectionName);
- menu.requireKeyValueDelimiter();
- menu.suppressValuelessLineWarning();
- if (!menu.loadFromStream(stream)) {
- warning("Harvester: unable to parse startup menu '%s'", kMenuPath);
- return true;
- }
-
- for (uint i = 1; i <= 6; ++i) {
- Common::String key = Common::String::format("main_menu_%u", i);
- Common::String value;
- if (menu.getKey(key, kMenuSectionName, value) && !value.empty())
- _menuItems.push_back(Common::move(value));
- }
-
- debugC(1, kDebugGeneral, "Harvester: loaded %u startup menu items from '%s'", (uint)_menuItems.size(), kMenuPath);
- return true;
-}
-
-Common::Error Flow::runQuickTips() {
- if (!_engine.getScript()->isQuickTipsEnabled() || _quickTips.empty())
- return Common::kNoError;
-
- Common::Error transitionError = beginRoomSetupTransition();
- if (transitionError.getCode() != Common::kNoError)
- return transitionError;
-
- SceneState scene;
- if (!loadQuickTipsScene(_engine, scene))
- return Common::kReadingFailed;
-
- Graphics::Screen *screen = _engine.getScreen();
- if (screen) {
- if (!populateRoomSceneEntities(scene.state, scene.sceneObjects, scene.sceneAnimations))
- return Common::kReadingFailed;
-
- transitionError = waitForRoomSetupTransitionHold();
- if (transitionError.getCode() != Common::kNoError)
- return transitionError;
-
- logScenePaletteSummary("quick tips scene palette", scene, kPaletteBrightnessBlack);
- RoomPresentation::drawRoomScene(_engine, *screen, scene, kPaletteBrightnessBlack);
- screen->makeAllDirty();
- screen->update();
-
- logScenePaletteSummary("quick tips fade target", scene, scene.targetPaletteBrightness);
- transitionError = fadeInRoomScene(scene.palette, scene.targetPaletteBrightness);
- if (transitionError.getCode() != Common::kNoError)
- return transitionError;
- }
-
- resetCursorAnimationSequence();
-
- QuickTipsLayout quickTipsLayout;
- if (!resolveQuickTipsLayout(_engine, _menuTextConfig, quickTipsLayout))
- return Common::kReadingFailed;
- const Common::String &toggleLabel = _engine.getScript()->isQuickTipsEnabled()
- ? _menuTextConfig.quickTipsOnLabel : _menuTextConfig.quickTipsOffLabel;
- debugC(1, kDebugGeneral,
- "Harvester: quick tips panel='%s' header='%s' exit='%s' next='%s' toggle='%s'",
- _menuTextConfig.hasQuickTipsHeader() ? "TEXTBOX6.BM" : "TIPS.BM",
- _menuTextConfig.quickTipsHeader.c_str(), _menuTextConfig.quickTipsExitLabel.c_str(),
- _menuTextConfig.quickTipsNextLabel.c_str(), toggleLabel.c_str());
-
- uint tipIndex = _engine.getRandomNumber(_quickTips.size() - 1);
- bool needsRedraw = true;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit()) {
- if (needsRedraw) {
- renderQuickTipsScreen(_engine, scene, _menuTextConfig, quickTipsLayout, _quickTips[tipIndex]);
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN:
- if (quickTipsLayout.exitRect.contains(_mousePos))
- return Common::kNoError;
-
- if (quickTipsLayout.nextRect.contains(_mousePos)) {
- tipIndex = (tipIndex + 1) % _quickTips.size();
- needsRedraw = true;
- } else if (quickTipsLayout.toggleRect.contains(_mousePos)) {
- _engine.getScript()->setQuickTipsEnabled(!_engine.getScript()->isQuickTipsEnabled());
- if (!resolveQuickTipsLayout(_engine, _menuTextConfig, quickTipsLayout))
- return Common::kReadingFailed;
- needsRedraw = true;
- }
- break;
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
- return Common::kNoError;
- break;
- default:
- break;
- }
- }
-
- if (tickRuntimeEntities())
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
-Common::Error Flow::runDemoEnding() {
- Graphics::Screen *screen = _engine.getScreen();
- ResourceManager *resources = _engine.getResources();
- EntityManager *entityManager = _engine.getRuntimeEntities();
- if (!screen || !resources)
- return Common::kReadingFailed;
-
- if (entityManager)
- entityManager->hideCursor();
- _engine.stopSound();
- if (!_engine.playMusic(kDemoEndingMusicPath))
- return Common::kReadingFailed;
-
- for (uint i = 0; i < ARRAYSIZE(kDemoEndingScreenNumbers) && !_engine.shouldQuit(); ++i) {
- const int screenNumber = kDemoEndingScreenNumbers[i];
- const Common::String bitmapPath = Common::String::format(
- "1:/GRAPHIC/OTHER/SCREEN%d.BM", screenNumber);
- const Common::String palettePath = Common::String::format(
- "1:/GRAPHIC/PAL/SCREEN%d.PAL", screenNumber);
- IndexedBitmap bitmap;
- byte palette[256 * 3];
- if (!loadBitmapResource(*resources, bitmapPath, bitmap) ||
- !Art::loadPaletteResource(*resources, palettePath, palette)) {
- warning("Harvester: unable to load DOS demo ending screen %d", screenNumber);
- return Common::kReadingFailed;
- }
-
- setScaledPalette(*screen, palette, kPaletteBrightnessBlack);
- screen->fillRect(Common::Rect(0, 0, screen->w, screen->h), 0);
- RoomPresentation::blitBitmap(*screen, bitmap, 0, 0);
- screen->makeAllDirty();
- screen->update();
-
- Common::Error fadeError = fadePalette(
- palette, kPaletteBrightnessBlack, kPaletteBrightnessFull);
- if (fadeError.getCode() != Common::kNoError)
- return fadeError;
-
- const uint32 deadline = g_system->getMillis() + kDemoEndingScreenDurationMs;
- bool skipScreen = false;
- while (!skipScreen && !_engine.shouldQuit() &&
- (int32)(deadline - g_system->getMillis()) > 0) {
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error eventError = Common::kNoError;
- if (handleSystemEvent(event, eventError))
- return eventError;
- if (event.type == Common::EVENT_LBUTTONDOWN ||
- (event.type == Common::EVENT_KEYDOWN &&
- (event.kbd.keycode == Common::KEYCODE_SPACE ||
- event.kbd.keycode == Common::KEYCODE_ESCAPE ||
- event.kbd.keycode == Common::KEYCODE_RETURN))) {
- skipScreen = true;
- }
- }
- g_system->delayMillis(1);
- }
-
- if (i + 1 == ARRAYSIZE(kDemoEndingScreenNumbers))
- _engine.stopMusic();
- fadeError = fadePalette(
- palette, kPaletteBrightnessFull, kPaletteBrightnessBlack);
- if (fadeError.getCode() != Common::kNoError)
- return fadeError;
- }
-
- _engine.quitGame();
- return Common::kNoError;
-}
-
-Common::Error Flow::runDemoIntroduction() {
- Graphics::Screen *screen = _engine.getScreen();
- ResourceManager *resources = _engine.getResources();
- EntityManager *entityManager = _engine.getRuntimeEntities();
- const Art *art = _engine.getArt();
- const Text *text = _engine.getText();
- const CftFontResource *fontResource = text ? text->findFont("TEXTFONT") : nullptr;
- if (!screen || !resources || !art || !fontResource)
- return Common::kReadingFailed;
-
- IndexedBitmap bitmap;
- byte palette[256 * 3];
- if (!loadBitmapResource(*resources, kDemoIntroductionBitmapPath, bitmap) ||
- !Art::loadPaletteResource(*resources, kDemoIntroductionPalettePath, palette)) {
- return Common::kReadingFailed;
- }
-
- HarvesterCftFont font(*fontResource);
- if (!font.isValid())
- return Common::kReadingFailed;
- debugC(1, kDebugGeneral,
- "Harvester: showing DOS demo introduction for %u ms",
- kDemoIntroductionDurationMs);
-
- if (entityManager)
- entityManager->hideCursor();
- setScaledPalette(*screen, palette, kPaletteBrightnessBlack);
- screen->fillRect(Common::Rect(0, 0, screen->w, screen->h), 0);
- RoomPresentation::blitBitmap(*screen, bitmap, 0, 0);
- blitTransparentBitmap(*screen, art->getLogoBitmap(), 160, 0);
- RoomPresentation::drawWrappedText(*screen, font, kDemoIntroductionText, 90, 200, 420, 0xce,
- kNativeIdentTextLineSpacing);
- screen->makeAllDirty();
- screen->update();
-
- if (!_engine.playMusic(kDemoIntroductionMusicPath))
- return Common::kReadingFailed;
- Common::Error fadeError = fadePalette(
- palette, kPaletteBrightnessBlack, kPaletteBrightnessFull);
- if (fadeError.getCode() != Common::kNoError)
- return fadeError;
-
- const uint32 deadline = g_system->getMillis() + kDemoIntroductionDurationMs;
- bool skipIntroduction = false;
- while (!skipIntroduction && !_engine.shouldQuit() &&
- (int32)(deadline - g_system->getMillis()) > 0) {
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error eventError = Common::kNoError;
- if (handleSystemEvent(event, eventError))
- return eventError;
- if (event.type == Common::EVENT_LBUTTONDOWN ||
- event.type == Common::EVENT_RBUTTONDOWN ||
- (event.type == Common::EVENT_KEYDOWN &&
- (event.kbd.keycode == Common::KEYCODE_SPACE ||
- event.kbd.keycode == Common::KEYCODE_ESCAPE ||
- event.kbd.keycode == Common::KEYCODE_RETURN))) {
- skipIntroduction = true;
- }
- }
- g_system->delayMillis(1);
- }
-
- fadeError = fadePalette(palette, kPaletteBrightnessFull, kPaletteBrightnessBlack);
- if (fadeError.getCode() != Common::kNoError)
- return fadeError;
- _engine.stopMusic();
-
- FstPlayer fstPlayer(_engine);
- if (!fstPlayer.play(kDemoIntroductionFstPath))
- return Common::kReadingFailed;
- return Common::kNoError;
-}
-
Common::Error Flow::runMainMenu() {
return _menu.runMainMenu(*this);
}
-Common::Error Flow::showCdChangePrompt(int discNumber) {
- if (discNumber <= 0)
- return Common::kNoError;
-
- ResourceManager *resources = _engine.getResources();
- if (!resources)
- return Common::kReadingFailed;
- const int previousDisc = resources->getCurrentDisc();
-
- if (_engine.shouldShowCdChangePrompts()) {
- bool promptCompleted = false;
- Common::Error promptError = _menu.showCdChangePrompt(discNumber, *resources, *this, promptCompleted);
- // System-event exits can return kNoError without completing the prompt.
- if (promptError.getCode() != Common::kNoError || !promptCompleted)
- return promptError;
- }
-
- if (!_engine.activateDisc(discNumber)) {
- warning("Harvester: unable to activate disc %d resources", discNumber);
- return Common::kReadingFailed;
- }
- if ((discNumber == 1 || discNumber == 3) &&
- previousDisc > 0 && previousDisc != resources->getCurrentDisc()) {
- Script *script = _engine.getScript();
- if (!script || !script->reloadTownWorld(*resources)) {
- warning("Harvester: unable to reload town script after disc prompt %d -> %d",
- previousDisc, discNumber);
- return Common::kReadingFailed;
- }
- }
-
- return Common::kNoError;
-}
-
Common::Error Flow::runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, bool canSaveGame) {
return _menu.runRoomMenu(backdrop, palette, paletteBrightness, *this, canSaveGame);
@@ -976,140 +524,6 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
return true;
}
-Common::Error Flow::beginRoomSetupTransition() {
- Graphics::Screen *screen = _engine.getScreen();
- if (screen) {
- byte displayPalette[256 * 3];
- screen->getPalette(displayPalette);
- bool hasVisiblePalette = false;
- for (byte value : displayPalette) {
- if (value != 0) {
- hasVisiblePalette = true;
- break;
- }
- }
-
- if (hasVisiblePalette) {
- for (double brightness = 1.0;
- brightness > kPaletteBrightnessBlack; brightness -= 0.1) {
- const uint32 startTick = getRuntimeClockTicks();
- setScaledDisplayPalette(*screen, displayPalette, brightness);
- screen->makeAllDirty();
- screen->update();
-
- Common::Error fadeError = Common::kNoError;
- if (waitForPaletteFadeTick(startTick, fadeError))
- return fadeError;
- }
-
- setScaledDisplayPalette(*screen, displayPalette, kPaletteBrightnessBlack);
- screen->makeAllDirty();
- screen->update();
- }
- }
-
- if (_engine.getRuntimeEntities())
- _engine.getRuntimeEntities()->hideCursor();
-
- const Art *art = _engine.getArt();
- if (art && screen)
- art->drawWaitFrame(*screen);
- _roomSetupTransitionShownTick = g_system ? g_system->getMillis() : 0;
-
- Common::Error result = Common::kNoError;
- if (pumpTransitionEvents(result))
- return result;
-
- return Common::kNoError;
-}
-
-Common::Error Flow::waitForRoomSetupTransitionHold() {
- if (_roomSetupTransitionShownTick == 0 || !g_system)
- return Common::kNoError;
-
- const uint32 deadline = _roomSetupTransitionShownTick + kTransitionWaitFrameMinMs;
- while ((int32)(deadline - g_system->getMillis()) > 0) {
- Common::Error result = Common::kNoError;
- if (pumpTransitionEvents(result))
- return result;
- g_system->delayMillis(1);
- }
-
- _roomSetupTransitionShownTick = 0;
- return Common::kNoError;
-}
-
-Common::Error Flow::fadeInRoomScene(const byte *palette, float targetBrightness) {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen || !palette)
- return Common::kNoError;
-
- for (double brightness = 0.0; brightness < targetBrightness; brightness += 0.1) {
- const uint32 startTick = getRuntimeClockTicks();
- setScaledPalette(*screen, palette, brightness);
- screen->makeAllDirty();
- screen->update();
-
- Common::Error fadeError = Common::kNoError;
- if (waitForPaletteFadeTick(startTick, fadeError))
- return fadeError;
- }
-
- setScaledPalette(*screen, palette, targetBrightness);
- screen->makeAllDirty();
- screen->update();
- return Common::kNoError;
-}
-
-Common::Error Flow::fadePalette(const byte *palette, float fromBrightness,
- float toBrightness) {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen || !palette)
- return Common::kNoError;
-
- 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();
-
- Common::Error fadeError = Common::kNoError;
- if (waitForPaletteFadeTick(startTick, fadeError))
- return fadeError;
- }
-
- setScaledPalette(*screen, palette, toBrightness);
- screen->makeAllDirty();
- screen->update();
- 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(4, 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)) {
- if (handleSystemEvent(event, result))
- return true;
- }
-
- return false;
-}
-
void Flow::executeStartupAudioCommands(const Common::Array<AudioCommand> &commands) {
for (const AudioCommand &command : commands)
(void)_engine.executeAudioCommand(command);
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index ca464b9535c..f3d7b35917d 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -107,6 +107,7 @@ private:
bool loadQuickTips();
bool loadMenuItems();
Common::Error runQuickTips();
+ Common::Error runQuickTipsOverlay(const SceneState &scene);
Common::Error runDemoIntroduction();
Common::Error runMainMenu();
Common::Error runTownMapSelector(const Common::String &mapEntryName, Common::String &destinationEntranceName);
diff --git a/engines/harvester/flow_startup.cpp b/engines/harvester/flow_startup.cpp
new file mode 100644
index 00000000000..46c8ebe7339
--- /dev/null
+++ b/engines/harvester/flow_startup.cpp
@@ -0,0 +1,441 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/flow.h"
+#include "harvester/flow_support.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/formats/ini-file.h"
+#include "common/memstream.h"
+#include "common/system.h"
+#include "common/util.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
+#include "harvester/cft_font.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/fst_player.h"
+#include "harvester/harvester.h"
+#include "harvester/resources.h"
+#include "harvester/room_presentation.h"
+#include "harvester/scene_entities.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kQuickTipsPath = "ADJHEAD.RCS";
+static const char *const kMenuPath = "MENU.INI";
+static const char *const kMenuSectionName = "menu";
+static const char *const kBuiltInMenuItems[] = {
+ "NEW GAME",
+ "SAVE GAME",
+ "LOAD GAME",
+ "OPTIONS",
+ "HELP",
+ "QUIT GAME"
+};
+static const int kNativeIdentTextLineSpacing = 3;
+static const float kPaletteBrightnessBlack = 0.0f;
+static const float kPaletteBrightnessFull = 1.0f;
+static const uint32 kDemoEndingScreenDurationMs = 10000;
+static const int kDemoEndingScreenNumbers[] = { 1, 2, 3, 4, 6 };
+static const char *const kDemoEndingMusicPath = "SOUND/MUSIC/ROCKFITE.CMP";
+static const uint32 kDemoIntroductionDurationMs = 17000;
+static const char *const kDemoIntroductionBitmapPath = "1:/GRAPHIC/OTHER/MAINMENU.BM";
+static const char *const kDemoIntroductionPalettePath = "1:/GRAPHIC/PAL/MAINMENU.PAL";
+static const char *const kDemoIntroductionMusicPath = "SOUND/MUSIC/LODGERB.CMP";
+static const char *const kDemoIntroductionFstPath = "GRAPHIC/FST/PCFALL.FST";
+static const char *const kDemoIntroductionText =
+ "After solving the enigmas of the Harvest town, you are thrust into the realm of "
+ "the Order of the Harvest Moon, a bizarre institution of secretive knowledge. "
+ "However, the only thing you really care about is escaping from this hellish reality...";
+
+static Common::String trimAsciiLine(const Common::String &value) {
+ uint start = 0;
+ uint end = value.size();
+
+ while (start < end && (value[start] == ' ' || value[start] == '\t'))
+ ++start;
+ while (end > start && (value[end - 1] == ' ' || value[end - 1] == '\t' || value[end - 1] == '\r'))
+ --end;
+
+ return value.substr(start, end - start);
+}
+
+static bool loadQuickTipsScene(HarvesterEngine &engine, SceneState &scene) {
+ RoomSetupState state;
+ // The original startup path enters the START room first and overlays quick tips on top of it.
+ if (!engine.getScript()->resolveRoomSetupState("START", state, *engine.getResources()))
+ return false;
+
+ return SceneEntities::loadRoomSceneResources(state, *engine.getResources(), scene);
+}
+
+static void renderQuickTipsScreen(HarvesterEngine &engine, const SceneState &scene,
+ const MenuTextConfig &config, const QuickTipsLayout &layout,
+ const Common::String &tipText) {
+ Graphics::Screen *screen = engine.getScreen();
+ if (!screen)
+ return;
+
+ RoomPresentation::drawRoomScene(engine, *screen, scene, scene.targetPaletteBrightness);
+ drawQuickTipsPanel(engine, config, layout, tipText);
+
+ if (engine.getRuntimeEntities())
+ engine.getRuntimeEntities()->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+}
+
+} // End of anonymous namespace
+
+bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
+ Common::Array<byte> data;
+ if (!resources.loadFile(path, data) || data.size() < 12)
+ return false;
+
+ bitmap = IndexedBitmap();
+ return decodeBitmap(data, bitmap);
+}
+
+bool Flow::load() {
+ (void)loadMenuTextConfig(_engine, _menuTextConfig);
+ return loadQuickTips() && loadMenuItems();
+}
+
+bool Flow::loadQuickTips() {
+ _quickTips.clear();
+
+ Common::Array<byte> data;
+ if (!_engine.getResources()->loadFile(kQuickTipsPath, data)) {
+ warning("Harvester: unable to load quick tips '%s'", kQuickTipsPath);
+ return true;
+ }
+
+ Common::String currentLine;
+ for (uint i = 0; i < data.size(); ++i) {
+ const char ch = (char)data[i];
+ if (ch == '\r')
+ continue;
+ if (ch == '\n') {
+ Common::String trimmed = trimAsciiLine(currentLine);
+ if (!trimmed.empty())
+ _quickTips.push_back(Common::move(trimmed));
+ currentLine.clear();
+ continue;
+ }
+
+ currentLine += ch;
+ }
+
+ Common::String trimmed = trimAsciiLine(currentLine);
+ if (!trimmed.empty())
+ _quickTips.push_back(Common::move(trimmed));
+
+ debugC(1, kDebugGeneral, "Harvester: loaded %u quick tips from '%s'", (uint)_quickTips.size(), kQuickTipsPath);
+ return true;
+}
+
+bool Flow::loadMenuItems() {
+ _menuItems.clear();
+
+ Common::Array<byte> data;
+ if (!_engine.getResources()->loadFile(kMenuPath, data)) {
+ for (const char *item : kBuiltInMenuItems)
+ _menuItems.push_back(item);
+ debugC(2, kDebugGeneral,
+ "Harvester: using %u built-in startup menu items because '%s' is unavailable",
+ (uint)_menuItems.size(), kMenuPath);
+ return true;
+ }
+
+ Common::MemoryReadStream stream(data.data(), data.size());
+ Common::INIFile menu;
+ menu.setDefaultSectionName(kMenuSectionName);
+ menu.requireKeyValueDelimiter();
+ menu.suppressValuelessLineWarning();
+ if (!menu.loadFromStream(stream)) {
+ warning("Harvester: unable to parse startup menu '%s'", kMenuPath);
+ return true;
+ }
+
+ for (uint i = 1; i <= 6; ++i) {
+ Common::String key = Common::String::format("main_menu_%u", i);
+ Common::String value;
+ if (menu.getKey(key, kMenuSectionName, value) && !value.empty())
+ _menuItems.push_back(Common::move(value));
+ }
+
+ debugC(1, kDebugGeneral, "Harvester: loaded %u startup menu items from '%s'", (uint)_menuItems.size(), kMenuPath);
+ return true;
+}
+
+Common::Error Flow::runQuickTips() {
+ if (!_engine.getScript()->isQuickTipsEnabled() || _quickTips.empty())
+ return Common::kNoError;
+
+ Common::Error transitionError = beginRoomSetupTransition();
+ if (transitionError.getCode() != Common::kNoError)
+ return transitionError;
+
+ SceneState scene;
+ if (!loadQuickTipsScene(_engine, scene))
+ return Common::kReadingFailed;
+
+ Graphics::Screen *screen = _engine.getScreen();
+ if (screen) {
+ if (!populateRoomSceneEntities(scene.state, scene.sceneObjects, scene.sceneAnimations))
+ return Common::kReadingFailed;
+
+ transitionError = waitForRoomSetupTransitionHold();
+ if (transitionError.getCode() != Common::kNoError)
+ return transitionError;
+
+ logScenePaletteSummary("quick tips scene palette", scene, kPaletteBrightnessBlack);
+ RoomPresentation::drawRoomScene(_engine, *screen, scene, kPaletteBrightnessBlack);
+ screen->makeAllDirty();
+ screen->update();
+
+ logScenePaletteSummary("quick tips fade target", scene, scene.targetPaletteBrightness);
+ transitionError = fadeInRoomScene(scene.palette, scene.targetPaletteBrightness);
+ if (transitionError.getCode() != Common::kNoError)
+ return transitionError;
+ }
+
+ return runQuickTipsOverlay(scene);
+}
+
+Common::Error Flow::runQuickTipsOverlay(const SceneState &scene) {
+ resetCursorAnimationSequence();
+
+ QuickTipsLayout quickTipsLayout;
+ if (!resolveQuickTipsLayout(_engine, _menuTextConfig, quickTipsLayout))
+ return Common::kReadingFailed;
+ const Common::String &toggleLabel = _engine.getScript()->isQuickTipsEnabled()
+ ? _menuTextConfig.quickTipsOnLabel : _menuTextConfig.quickTipsOffLabel;
+ debugC(1, kDebugGeneral,
+ "Harvester: quick tips panel='%s' header='%s' exit='%s' next='%s' toggle='%s'",
+ _menuTextConfig.hasQuickTipsHeader() ? "TEXTBOX6.BM" : "TIPS.BM",
+ _menuTextConfig.quickTipsHeader.c_str(), _menuTextConfig.quickTipsExitLabel.c_str(),
+ _menuTextConfig.quickTipsNextLabel.c_str(), toggleLabel.c_str());
+
+ uint tipIndex = _engine.getRandomNumber(_quickTips.size() - 1);
+ bool needsRedraw = true;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit()) {
+ if (needsRedraw) {
+ renderQuickTipsScreen(_engine, scene, _menuTextConfig, quickTipsLayout, _quickTips[tipIndex]);
+ needsRedraw = false;
+ }
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (handleSystemEvent(event, result))
+ return result;
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ if (quickTipsLayout.exitRect.contains(_mousePos))
+ return Common::kNoError;
+
+ if (quickTipsLayout.nextRect.contains(_mousePos)) {
+ tipIndex = (tipIndex + 1) % _quickTips.size();
+ needsRedraw = true;
+ } else if (quickTipsLayout.toggleRect.contains(_mousePos)) {
+ _engine.getScript()->setQuickTipsEnabled(!_engine.getScript()->isQuickTipsEnabled());
+ if (!resolveQuickTipsLayout(_engine, _menuTextConfig, quickTipsLayout))
+ return Common::kReadingFailed;
+ needsRedraw = true;
+ }
+ break;
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return Common::kNoError;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (tickRuntimeEntities())
+ needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error Flow::runDemoEnding() {
+ Graphics::Screen *screen = _engine.getScreen();
+ ResourceManager *resources = _engine.getResources();
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ if (!screen || !resources)
+ return Common::kReadingFailed;
+
+ if (entityManager)
+ entityManager->hideCursor();
+ _engine.stopSound();
+ if (!_engine.playMusic(kDemoEndingMusicPath))
+ return Common::kReadingFailed;
+
+ for (uint i = 0; i < ARRAYSIZE(kDemoEndingScreenNumbers) && !_engine.shouldQuit(); ++i) {
+ const int screenNumber = kDemoEndingScreenNumbers[i];
+ const Common::String bitmapPath = Common::String::format(
+ "1:/GRAPHIC/OTHER/SCREEN%d.BM", screenNumber);
+ const Common::String palettePath = Common::String::format(
+ "1:/GRAPHIC/PAL/SCREEN%d.PAL", screenNumber);
+ IndexedBitmap bitmap;
+ byte palette[256 * 3];
+ if (!loadBitmapResource(*resources, bitmapPath, bitmap) ||
+ !Art::loadPaletteResource(*resources, palettePath, palette)) {
+ warning("Harvester: unable to load DOS demo ending screen %d", screenNumber);
+ return Common::kReadingFailed;
+ }
+
+ setScaledPalette(*screen, palette, kPaletteBrightnessBlack);
+ screen->fillRect(Common::Rect(0, 0, screen->w, screen->h), 0);
+ RoomPresentation::blitBitmap(*screen, bitmap, 0, 0);
+ screen->makeAllDirty();
+ screen->update();
+
+ Common::Error fadeError = fadePalette(
+ palette, kPaletteBrightnessBlack, kPaletteBrightnessFull);
+ if (fadeError.getCode() != Common::kNoError)
+ return fadeError;
+
+ const uint32 deadline = g_system->getMillis() + kDemoEndingScreenDurationMs;
+ bool skipScreen = false;
+ while (!skipScreen && !_engine.shouldQuit() &&
+ (int32)(deadline - g_system->getMillis()) > 0) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error eventError = Common::kNoError;
+ if (handleSystemEvent(event, eventError))
+ return eventError;
+ if (event.type == Common::EVENT_LBUTTONDOWN ||
+ (event.type == Common::EVENT_KEYDOWN &&
+ (event.kbd.keycode == Common::KEYCODE_SPACE ||
+ event.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ event.kbd.keycode == Common::KEYCODE_RETURN))) {
+ skipScreen = true;
+ }
+ }
+ g_system->delayMillis(1);
+ }
+
+ if (i + 1 == ARRAYSIZE(kDemoEndingScreenNumbers))
+ _engine.stopMusic();
+ fadeError = fadePalette(
+ palette, kPaletteBrightnessFull, kPaletteBrightnessBlack);
+ if (fadeError.getCode() != Common::kNoError)
+ return fadeError;
+ }
+
+ _engine.quitGame();
+ return Common::kNoError;
+}
+
+Common::Error Flow::runDemoIntroduction() {
+ Graphics::Screen *screen = _engine.getScreen();
+ ResourceManager *resources = _engine.getResources();
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ const Art *art = _engine.getArt();
+ const Text *text = _engine.getText();
+ const CftFontResource *fontResource = text ? text->findFont("TEXTFONT") : nullptr;
+ if (!screen || !resources || !art || !fontResource)
+ return Common::kReadingFailed;
+
+ IndexedBitmap bitmap;
+ byte palette[256 * 3];
+ if (!loadBitmapResource(*resources, kDemoIntroductionBitmapPath, bitmap) ||
+ !Art::loadPaletteResource(*resources, kDemoIntroductionPalettePath, palette)) {
+ return Common::kReadingFailed;
+ }
+
+ HarvesterCftFont font(*fontResource);
+ if (!font.isValid())
+ return Common::kReadingFailed;
+ debugC(1, kDebugGeneral,
+ "Harvester: showing DOS demo introduction for %u ms",
+ kDemoIntroductionDurationMs);
+
+ if (entityManager)
+ entityManager->hideCursor();
+ setScaledPalette(*screen, palette, kPaletteBrightnessBlack);
+ screen->fillRect(Common::Rect(0, 0, screen->w, screen->h), 0);
+ RoomPresentation::blitBitmap(*screen, bitmap, 0, 0);
+ blitTransparentBitmap(*screen, art->getLogoBitmap(), 160, 0);
+ RoomPresentation::drawWrappedText(*screen, font, kDemoIntroductionText, 90, 200, 420, 0xce,
+ kNativeIdentTextLineSpacing);
+ screen->makeAllDirty();
+ screen->update();
+
+ if (!_engine.playMusic(kDemoIntroductionMusicPath))
+ return Common::kReadingFailed;
+ Common::Error fadeError = fadePalette(
+ palette, kPaletteBrightnessBlack, kPaletteBrightnessFull);
+ if (fadeError.getCode() != Common::kNoError)
+ return fadeError;
+
+ const uint32 deadline = g_system->getMillis() + kDemoIntroductionDurationMs;
+ bool skipIntroduction = false;
+ while (!skipIntroduction && !_engine.shouldQuit() &&
+ (int32)(deadline - g_system->getMillis()) > 0) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error eventError = Common::kNoError;
+ if (handleSystemEvent(event, eventError))
+ return eventError;
+ if (event.type == Common::EVENT_LBUTTONDOWN ||
+ event.type == Common::EVENT_RBUTTONDOWN ||
+ (event.type == Common::EVENT_KEYDOWN &&
+ (event.kbd.keycode == Common::KEYCODE_SPACE ||
+ event.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ event.kbd.keycode == Common::KEYCODE_RETURN))) {
+ skipIntroduction = true;
+ }
+ }
+ g_system->delayMillis(1);
+ }
+
+ fadeError = fadePalette(palette, kPaletteBrightnessFull, kPaletteBrightnessBlack);
+ if (fadeError.getCode() != Common::kNoError)
+ return fadeError;
+ _engine.stopMusic();
+
+ FstPlayer fstPlayer(_engine);
+ if (!fstPlayer.play(kDemoIntroductionFstPath))
+ return Common::kReadingFailed;
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/flow_transition.cpp b/engines/harvester/flow_transition.cpp
new file mode 100644
index 00000000000..6bb412eb0e5
--- /dev/null
+++ b/engines/harvester/flow_transition.cpp
@@ -0,0 +1,230 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/flow.h"
+#include "harvester/flow_support.h"
+
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "common/util.h"
+#include "harvester/art.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/palette_utils.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+namespace {
+
+static const uint32 kPaletteFadeIntervalTicks = 4;
+static const float kPaletteBrightnessBlack = 0.0f;
+// Keep WAIT_BM visible while room construction runs
+static const uint32 kTransitionWaitFrameMinMs = 120;
+
+static void setScaledDisplayPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
+ byte scaledPalette[256 * 3];
+ const float clampedBrightness = CLIP<float>(brightness, 0.0f, 1.0f);
+ for (uint i = 0; i < ARRAYSIZE(scaledPalette); ++i)
+ scaledPalette[i] = (byte)CLIP<int>((int)(palette[i] * clampedBrightness + 0.5f), 0, 255);
+ screen.setPalette(scaledPalette);
+}
+
+} // End of anonymous namespace
+
+void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
+ byte scaledPalette[256 * 3];
+ const float gammaBrightness = g_engine ? g_engine->getGammaBrightnessScale() : 1.0f;
+ buildHarvesterDisplayPalette(palette, brightness * gammaBrightness, scaledPalette);
+ screen.setPalette(scaledPalette);
+}
+
+Common::Error Flow::showCdChangePrompt(int discNumber) {
+ if (discNumber <= 0)
+ return Common::kNoError;
+
+ ResourceManager *resources = _engine.getResources();
+ if (!resources)
+ return Common::kReadingFailed;
+ const int previousDisc = resources->getCurrentDisc();
+
+ if (_engine.shouldShowCdChangePrompts()) {
+ bool promptCompleted = false;
+ Common::Error promptError = _menu.showCdChangePrompt(discNumber, *resources, *this, promptCompleted);
+ // System-event exits can return kNoError without completing the prompt.
+ if (promptError.getCode() != Common::kNoError || !promptCompleted)
+ return promptError;
+ }
+
+ if (!_engine.activateDisc(discNumber)) {
+ warning("Harvester: unable to activate disc %d resources", discNumber);
+ return Common::kReadingFailed;
+ }
+ if ((discNumber == 1 || discNumber == 3) &&
+ previousDisc > 0 && previousDisc != resources->getCurrentDisc()) {
+ Script *script = _engine.getScript();
+ if (!script || !script->reloadTownWorld(*resources)) {
+ warning("Harvester: unable to reload town script after disc prompt %d -> %d",
+ previousDisc, discNumber);
+ return Common::kReadingFailed;
+ }
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error Flow::beginRoomSetupTransition() {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (screen) {
+ byte displayPalette[256 * 3];
+ screen->getPalette(displayPalette);
+ bool hasVisiblePalette = false;
+ for (byte value : displayPalette) {
+ if (value != 0) {
+ hasVisiblePalette = true;
+ break;
+ }
+ }
+
+ if (hasVisiblePalette) {
+ for (double brightness = 1.0;
+ brightness > kPaletteBrightnessBlack; brightness -= 0.1) {
+ const uint32 startTick = getRuntimeClockTicks();
+ setScaledDisplayPalette(*screen, displayPalette, brightness);
+ screen->makeAllDirty();
+ screen->update();
+
+ Common::Error fadeError = Common::kNoError;
+ if (waitForPaletteFadeTick(startTick, fadeError))
+ return fadeError;
+ }
+
+ setScaledDisplayPalette(*screen, displayPalette, kPaletteBrightnessBlack);
+ screen->makeAllDirty();
+ screen->update();
+ }
+ }
+
+ if (_engine.getRuntimeEntities())
+ _engine.getRuntimeEntities()->hideCursor();
+
+ const Art *art = _engine.getArt();
+ if (art && screen)
+ art->drawWaitFrame(*screen);
+ _roomSetupTransitionShownTick = g_system ? g_system->getMillis() : 0;
+
+ Common::Error result = Common::kNoError;
+ if (pumpTransitionEvents(result))
+ return result;
+
+ return Common::kNoError;
+}
+
+Common::Error Flow::waitForRoomSetupTransitionHold() {
+ if (_roomSetupTransitionShownTick == 0 || !g_system)
+ return Common::kNoError;
+
+ const uint32 deadline = _roomSetupTransitionShownTick + kTransitionWaitFrameMinMs;
+ while ((int32)(deadline - g_system->getMillis()) > 0) {
+ Common::Error result = Common::kNoError;
+ if (pumpTransitionEvents(result))
+ return result;
+ g_system->delayMillis(1);
+ }
+
+ _roomSetupTransitionShownTick = 0;
+ return Common::kNoError;
+}
+
+Common::Error Flow::fadeInRoomScene(const byte *palette, float targetBrightness) {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen || !palette)
+ return Common::kNoError;
+
+ for (double brightness = 0.0; brightness < targetBrightness; brightness += 0.1) {
+ const uint32 startTick = getRuntimeClockTicks();
+ setScaledPalette(*screen, palette, brightness);
+ screen->makeAllDirty();
+ screen->update();
+
+ Common::Error fadeError = Common::kNoError;
+ if (waitForPaletteFadeTick(startTick, fadeError))
+ return fadeError;
+ }
+
+ setScaledPalette(*screen, palette, targetBrightness);
+ screen->makeAllDirty();
+ screen->update();
+ return Common::kNoError;
+}
+
+Common::Error Flow::fadePalette(const byte *palette, float fromBrightness,
+ float toBrightness) {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen || !palette)
+ return Common::kNoError;
+
+ 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();
+
+ Common::Error fadeError = Common::kNoError;
+ if (waitForPaletteFadeTick(startTick, fadeError))
+ return fadeError;
+ }
+
+ setScaledPalette(*screen, palette, toBrightness);
+ screen->makeAllDirty();
+ screen->update();
+ 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(4, 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)) {
+ if (handleSystemEvent(event, result))
+ return true;
+ }
+
+ return false;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 88572fb12af..9b153d3d27c 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -10,6 +10,8 @@ MODULE_OBJS = \
flow.o \
flow_debug.o \
flow_map.o \
+ flow_startup.o \
+ flow_transition.o \
fst_player.o \
harvester.o \
inventory.o \
Commit: 1e5757bcf2d467028bf3735d3afa6f279f79986f
https://github.com/scummvm/scummvm/commit/1e5757bcf2d467028bf3735d3afa6f279f79986f
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Separate room population and actor placement
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/flow_scene.cpp
engines/harvester/flow.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index b758f609e27..ac67db0b40e 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -22,17 +22,12 @@
#include "harvester/flow.h"
#include "common/algorithm.h"
-#include "common/debug.h"
#include "common/events.h"
#include "common/memstream.h"
#include "common/serializer.h"
-#include "harvester/detection.h"
#include "harvester/harvester.h"
#include "harvester/input.h"
-#include "harvester/resources.h"
#include "harvester/entity.h"
-#include "harvester/scene_entities.h"
-#include "harvester/player.h"
#include "harvester/script.h"
namespace Harvester {
@@ -40,57 +35,9 @@ namespace Harvester {
namespace {
static const int kCursorSequence7 = 7;
-static const char *const kPlayerActorEntityName = "PLAYER";
} // End of anonymous namespace
-static void setRoomActorScreenPosition(Entity &entity, int centerX, int bottomY, float z,
- int width, int height, int xOffset, int yOffset) {
- entity.setPosition(centerX - xOffset - width / 2, bottomY - height - yOffset, z);
-}
-
-static float computeRoomActorRenderZ(float z, const Entity &entity) {
- // Native actor spawns lower the render-list depth anchor by half the z extent
- // before insertion. Without that adjustment, cemetery Karin lands one layer
- // too far back and gets occluded by the grave mask in CEM10.
- return z - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
-}
-
-static bool applyRoomActorPlacementInternal(const RoomSetupState &state, Entity &entity,
- int centerX, int bottomY, float z, const Common::String *entranceName, bool applyDepthScale) {
- int width = 0;
- int height = 0;
- int xOffset = 0;
- int yOffset = 0;
- if (!entity.getCurrentFrameMetrics(width, height, xOffset, yOffset))
- return false;
-
- const float renderZ = computeRoomActorRenderZ(z, entity);
- entity.setAnchorMode(kEntityAnchorTopLeft);
- setRoomActorScreenPosition(entity, centerX, bottomY, renderZ, width, height, xOffset, yOffset);
-
- float depthScale = 1.0f;
- if (applyDepthScale) {
- depthScale = Player::computeDepthScale(state, renderZ);
- entity.setDepthScale(depthScale);
- if (!entity.getCurrentFrameMetrics(width, height, xOffset, yOffset))
- return false;
- setRoomActorScreenPosition(entity, centerX, bottomY, renderZ, width, height, xOffset, yOffset);
- }
-
- return true;
-}
-
-bool applyRoomActorPlacement(const RoomSetupState &state, Entity &entity,
- int centerX, int bottomY, float z, const Common::String *entranceName) {
- return applyRoomActorPlacementInternal(state, entity, centerX, bottomY, z, entranceName, true);
-}
-
-static bool applyStartupActorPlacement(const RoomSetupState &state, Entity &entity) {
- return applyRoomActorPlacement(state, entity,
- state.playerSpawnX, state.playerSpawnY, (float)state.playerSpawnZ, &state.entranceName);
-}
-
bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap) {
if (screen.w <= 0 || screen.h <= 0 || screen.format.bytesPerPixel != 1)
return false;
@@ -421,109 +368,6 @@ bool Flow::ensureCursorEntity() {
return entityManager->spawnCursorEntity(_mousePos) != nullptr;
}
-bool Flow::populateRoomSceneEntities(RoomSetupState &state,
- const Common::Array<ObjectRecord> &drawableObjects,
- const Common::Array<AnimRecord> &drawableAnimations) {
- EntityManager *entityManager = _engine.getRuntimeEntities();
- if (!entityManager)
- return false;
-
- SceneEntities sceneRuntime(state, _engine.getScript(), _engine.getResources(), entityManager);
- Entity *preservedPlayer = entityManager->detachSceneEntityByName(kPlayerActorEntityName);
- entityManager->clearSceneEntities(true);
- sceneRuntime.populateRoomRegions();
- for (const TimerRecord &timer : state.roomTimers) {
- Entity *existingTimer = entityManager->findSceneEntityByName(timer.timerName);
- if (existingTimer && existingTimer->getClassId() == kEntityClassTimer) {
- debugC(1, kDebugRoom,
- "Harvester: scene timer reused room='%s' timer='%s' current=%d initial=%d enabled=%d loop=%d global=%d",
- state.roomName.c_str(), timer.timerName.c_str(), existingTimer->getTimerCurrentValue(),
- existingTimer->getTimerInitialValue(), existingTimer->isTimerEnabled(),
- existingTimer->isTimerLooping(), existingTimer->isTimerGlobal());
- continue;
- }
-
- if (!entityManager->spawnSceneTimerEntity(timer.timerName,
- timer.initialValue, timer.currentValue, timer.enabled, timer.looping, timer.global)) {
- debug(1, "Harvester: unable to spawn room timer entity '%s'",
- timer.timerName.c_str());
- continue;
- }
-
- debugC(1, kDebugRoom,
- "Harvester: scene timer spawned room='%s' timer='%s' current=%d initial=%d enabled=%d loop=%d global=%d",
- state.roomName.c_str(), timer.timerName.c_str(), timer.currentValue, timer.initialValue,
- timer.enabled, timer.looping, timer.global);
- }
- sceneRuntime.populateRoomObjects(drawableObjects);
- for (const AnimRecord &anim : drawableAnimations) {
- const bool displayVisible = shouldDisplaySceneAnimation(anim);
- if (!entityManager->spawnSceneAnimationEntity(anim.animName, anim.resourcePath,
- Common::Point(anim.x, anim.y), (float)anim.z, anim.frameDelay, anim.active, displayVisible,
- anim.looping, anim.backward, anim.pingPong, anim.runtimeState)) {
- debug(1, "Harvester: unable to spawn room anim entity '%s' from '%s'",
- anim.animName.c_str(), anim.resourcePath.c_str());
- }
- }
- sceneRuntime.populateRoomNpcs();
- sceneRuntime.populateRoomMonsters();
- if (state.hasEntrance) {
- const int playerFrame = Player::resolveFacingFrame(state.playerFacing);
- Script *script = _engine.getScript();
- const int playerCombatLoadout = script ? script->getPlayerCombatLoadout() : 0;
- const Common::String playerResourcePath =
- Player::resolveCombatLoadoutResourcePath(playerCombatLoadout);
- const bool reusedPlayer = preservedPlayer != nullptr;
- Entity *player = preservedPlayer;
- if (!reusedPlayer) {
- player = entityManager->spawnSceneActorEntity(kPlayerActorEntityName,
- playerResourcePath, Common::Point(state.playerSpawnX, state.playerSpawnY),
- (float)state.playerSpawnZ, playerFrame);
- }
- if (!player) {
- debug(1, "Harvester: unable to spawn startup player actor from '%s'", playerResourcePath.c_str());
- } else {
- player->setClassId(kEntityClassPlayer);
- player->setAnchorMode(kEntityAnchorTopLeft);
- player->setHitTestMode(kEntityHitTestOpaquePixels);
- player->setVisible(true);
- player->setAnimationRate(0);
- player->setAnimationFrameRange(playerFrame, playerFrame, false);
- player->setCurrentFrame(playerFrame);
- if (!applyStartupActorPlacement(state, *player)) {
- debug(1, "Harvester: unable to apply startup player placement for entrance '%s'",
- state.entranceName.c_str());
- }
- if (reusedPlayer) {
- Player loadoutPlayer;
- loadoutPlayer.initialize(state, player, -1);
- (void)loadoutPlayer.syncCombatLoadoutVisual(_engine, state, playerCombatLoadout);
- }
- Player spawnPlayer;
- spawnPlayer.initialize(state, player, 0);
- if (spawnPlayer.resolveBlockedStartupSpawn(_engine, state, drawableObjects, drawableAnimations)) {
- state.playerSpawnX = spawnPlayer.getState().centerX;
- state.playerSpawnY = spawnPlayer.getState().bottomY;
- state.playerSpawnZ = (int)spawnPlayer.getState().z;
- }
- if (reusedPlayer)
- entityManager->adoptSceneEntity(player);
- else
- entityManager->reinsertSceneEntity(player);
- debugC(1, kDebugPlayer,
- "Harvester: %s startup player actor '%s' entrance='%s' pos=(%d,%d,%d) facing=%d frame=%d",
- reusedPlayer ? "reused" : "spawned", playerResourcePath.c_str(), state.entranceName.c_str(),
- state.playerSpawnX, state.playerSpawnY, state.playerSpawnZ,
- state.playerFacing, playerFrame);
- }
- preservedPlayer = nullptr;
- } else if (preservedPlayer) {
- delete preservedPlayer;
- }
-
- return true;
-}
-
void Flow::executeStartupAudioCommands(const Common::Array<AudioCommand> &commands) {
for (const AudioCommand &command : commands)
(void)_engine.executeAudioCommand(command);
diff --git a/engines/harvester/flow_scene.cpp b/engines/harvester/flow_scene.cpp
new file mode 100644
index 00000000000..86adc616662
--- /dev/null
+++ b/engines/harvester/flow_scene.cpp
@@ -0,0 +1,200 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/flow.h"
+
+#include "common/debug.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/harvester.h"
+#include "harvester/player.h"
+#include "harvester/scene_entities.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kPlayerActorEntityName = "PLAYER";
+
+struct RoomActorFrameMetrics {
+ int width = 0;
+ int height = 0;
+ int xOffset = 0;
+ int yOffset = 0;
+};
+
+static void setRoomActorScreenPosition(Entity &entity, int centerX, int bottomY, float z,
+ const RoomActorFrameMetrics &metrics) {
+ entity.setPosition(centerX - metrics.xOffset - metrics.width / 2, bottomY - metrics.height - metrics.yOffset, z);
+}
+
+static float computeRoomActorRenderZ(float z, const Entity &entity) {
+ // Native actor spawns lower the render-list depth anchor by half the z extent
+ // before insertion. Without that adjustment, cemetery Karin lands one layer
+ // too far back and gets occluded by the grave mask in CEM10.
+ return z - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
+}
+
+static bool applyStartupActorPlacement(const RoomSetupState &state, Entity &entity) {
+ return applyRoomActorPlacement(state, entity,
+ state.playerSpawnX, state.playerSpawnY, (float)state.playerSpawnZ, &state.entranceName);
+}
+
+static void populateRoomTimers(const RoomSetupState &state, EntityManager &entityManager) {
+ for (const TimerRecord &timer : state.roomTimers) {
+ Entity *existingTimer = entityManager.findSceneEntityByName(timer.timerName);
+ if (existingTimer && existingTimer->getClassId() == kEntityClassTimer) {
+ debugC(1, kDebugRoom,
+ "Harvester: scene timer reused room='%s' timer='%s' current=%d initial=%d enabled=%d loop=%d global=%d",
+ state.roomName.c_str(), timer.timerName.c_str(), existingTimer->getTimerCurrentValue(),
+ existingTimer->getTimerInitialValue(), existingTimer->isTimerEnabled(),
+ existingTimer->isTimerLooping(), existingTimer->isTimerGlobal());
+ continue;
+ }
+
+ if (!entityManager.spawnSceneTimerEntity(timer.timerName,
+ timer.initialValue, timer.currentValue, timer.enabled, timer.looping, timer.global)) {
+ debug(1, "Harvester: unable to spawn room timer entity '%s'",
+ timer.timerName.c_str());
+ continue;
+ }
+
+ debugC(1, kDebugRoom,
+ "Harvester: scene timer spawned room='%s' timer='%s' current=%d initial=%d enabled=%d loop=%d global=%d",
+ state.roomName.c_str(), timer.timerName.c_str(), timer.currentValue, timer.initialValue,
+ timer.enabled, timer.looping, timer.global);
+ }
+}
+
+static void populateRoomAnimations(EntityManager &entityManager,
+ const Common::Array<AnimRecord> &drawableAnimations) {
+ for (const AnimRecord &anim : drawableAnimations) {
+ const bool displayVisible = shouldDisplaySceneAnimation(anim);
+ if (!entityManager.spawnSceneAnimationEntity(anim.animName, anim.resourcePath,
+ Common::Point(anim.x, anim.y), (float)anim.z, anim.frameDelay, anim.active, displayVisible,
+ anim.looping, anim.backward, anim.pingPong, anim.runtimeState)) {
+ debug(1, "Harvester: unable to spawn room anim entity '%s' from '%s'",
+ anim.animName.c_str(), anim.resourcePath.c_str());
+ }
+ }
+}
+
+static void populateRoomPlayer(HarvesterEngine &engine, RoomSetupState &state,
+ EntityManager &entityManager, Entity *preservedPlayer,
+ const Common::Array<ObjectRecord> &drawableObjects,
+ const Common::Array<AnimRecord> &drawableAnimations) {
+ if (state.hasEntrance) {
+ const int playerFrame = Player::resolveFacingFrame(state.playerFacing);
+ Script *script = engine.getScript();
+ const int playerCombatLoadout = script ? script->getPlayerCombatLoadout() : 0;
+ const Common::String playerResourcePath =
+ Player::resolveCombatLoadoutResourcePath(playerCombatLoadout);
+ const bool reusedPlayer = preservedPlayer != nullptr;
+ Entity *player = preservedPlayer;
+ if (!reusedPlayer) {
+ player = entityManager.spawnSceneActorEntity(kPlayerActorEntityName,
+ playerResourcePath, Common::Point(state.playerSpawnX, state.playerSpawnY),
+ (float)state.playerSpawnZ, playerFrame);
+ }
+ if (!player) {
+ debug(1, "Harvester: unable to spawn startup player actor from '%s'", playerResourcePath.c_str());
+ } else {
+ player->setClassId(kEntityClassPlayer);
+ player->setAnchorMode(kEntityAnchorTopLeft);
+ player->setHitTestMode(kEntityHitTestOpaquePixels);
+ player->setVisible(true);
+ player->setAnimationRate(0);
+ player->setAnimationFrameRange(playerFrame, playerFrame, false);
+ player->setCurrentFrame(playerFrame);
+ if (!applyStartupActorPlacement(state, *player)) {
+ debug(1, "Harvester: unable to apply startup player placement for entrance '%s'",
+ state.entranceName.c_str());
+ }
+ if (reusedPlayer) {
+ Player loadoutPlayer;
+ loadoutPlayer.initialize(state, player, -1);
+ (void)loadoutPlayer.syncCombatLoadoutVisual(engine, state, playerCombatLoadout);
+ }
+ Player spawnPlayer;
+ spawnPlayer.initialize(state, player, 0);
+ if (spawnPlayer.resolveBlockedStartupSpawn(engine, state, drawableObjects, drawableAnimations)) {
+ state.playerSpawnX = spawnPlayer.getState().centerX;
+ state.playerSpawnY = spawnPlayer.getState().bottomY;
+ state.playerSpawnZ = (int)spawnPlayer.getState().z;
+ }
+ if (reusedPlayer)
+ entityManager.adoptSceneEntity(player);
+ else
+ entityManager.reinsertSceneEntity(player);
+ debugC(1, kDebugPlayer,
+ "Harvester: %s startup player actor '%s' entrance='%s' pos=(%d,%d,%d) facing=%d frame=%d",
+ reusedPlayer ? "reused" : "spawned", playerResourcePath.c_str(), state.entranceName.c_str(),
+ state.playerSpawnX, state.playerSpawnY, state.playerSpawnZ,
+ state.playerFacing, playerFrame);
+ }
+ } else if (preservedPlayer) {
+ delete preservedPlayer;
+ }
+}
+
+} // End of anonymous namespace
+
+bool applyRoomActorPlacement(const RoomSetupState &state, Entity &entity,
+ int centerX, int bottomY, float z, const Common::String *entranceName) {
+ RoomActorFrameMetrics metrics;
+ if (!entity.getCurrentFrameMetrics(metrics.width, metrics.height, metrics.xOffset, metrics.yOffset))
+ return false;
+
+ const float renderZ = computeRoomActorRenderZ(z, entity);
+ entity.setAnchorMode(kEntityAnchorTopLeft);
+ setRoomActorScreenPosition(entity, centerX, bottomY, renderZ, metrics);
+
+ entity.setDepthScale(Player::computeDepthScale(state, renderZ));
+ if (!entity.getCurrentFrameMetrics(metrics.width, metrics.height, metrics.xOffset, metrics.yOffset))
+ return false;
+ setRoomActorScreenPosition(entity, centerX, bottomY, renderZ, metrics);
+
+ return true;
+}
+
+bool Flow::populateRoomSceneEntities(RoomSetupState &state,
+ const Common::Array<ObjectRecord> &drawableObjects,
+ const Common::Array<AnimRecord> &drawableAnimations) {
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ if (!entityManager)
+ return false;
+
+ SceneEntities sceneRuntime(state, _engine.getScript(), _engine.getResources(), entityManager);
+ Entity *preservedPlayer = entityManager->detachSceneEntityByName(kPlayerActorEntityName);
+ entityManager->clearSceneEntities(true);
+ sceneRuntime.populateRoomRegions();
+ populateRoomTimers(state, *entityManager);
+ sceneRuntime.populateRoomObjects(drawableObjects);
+ populateRoomAnimations(*entityManager, drawableAnimations);
+ sceneRuntime.populateRoomNpcs();
+ sceneRuntime.populateRoomMonsters();
+ populateRoomPlayer(_engine, state, *entityManager, preservedPlayer, drawableObjects, drawableAnimations);
+
+ return true;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 9b153d3d27c..5371cde46e3 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -10,6 +10,7 @@ MODULE_OBJS = \
flow.o \
flow_debug.o \
flow_map.o \
+ flow_scene.o \
flow_startup.o \
flow_transition.o \
fst_player.o \
Commit: c2412793813a3aacc1023ee63b25cdb44ba028cd
https://github.com/scummvm/scummvm/commit/c2412793813a3aacc1023ee63b25cdb44ba028cd
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-19T19:03:03-04:00
Commit Message:
HARVESTER: Organize subsystem sources into directories
Changed paths:
A engines/harvester/combat/combat.cpp
A engines/harvester/combat/combat.h
A engines/harvester/combat/combat_contact.cpp
A engines/harvester/combat/combat_effects.cpp
A engines/harvester/combat/combat_geometry.cpp
A engines/harvester/combat/combat_hud.cpp
A engines/harvester/combat/combat_monsters.cpp
A engines/harvester/combat/combat_reactions.cpp
A engines/harvester/combat/combat_targeting.cpp
A engines/harvester/flow/flow.cpp
A engines/harvester/flow/flow.h
A engines/harvester/flow/flow_debug.cpp
A engines/harvester/flow/flow_map.cpp
A engines/harvester/flow/flow_scene.cpp
A engines/harvester/flow/flow_startup.cpp
A engines/harvester/flow/flow_support.h
A engines/harvester/flow/flow_transition.cpp
A engines/harvester/menu/menu.cpp
A engines/harvester/menu/menu.h
A engines/harvester/menu/menu_config.cpp
A engines/harvester/menu/menu_navigation.cpp
A engines/harvester/menu/menu_options.cpp
A engines/harvester/menu/menu_prompts.cpp
A engines/harvester/menu/menu_render.cpp
A engines/harvester/menu/menu_render.h
A engines/harvester/menu/menu_save.cpp
A engines/harvester/player/player.cpp
A engines/harvester/player/player.h
A engines/harvester/player/player_animation.cpp
A engines/harvester/player/player_attack.cpp
A engines/harvester/player/player_idle.cpp
A engines/harvester/player/player_keyboard.cpp
A engines/harvester/player/player_movement.cpp
A engines/harvester/player/player_movement.h
A engines/harvester/player/player_reactions.cpp
A engines/harvester/player/player_target.cpp
A engines/harvester/scene/input.cpp
A engines/harvester/scene/input.h
A engines/harvester/scene/interaction.cpp
A engines/harvester/scene/interaction.h
A engines/harvester/scene/room_presentation.cpp
A engines/harvester/scene/room_presentation.h
A engines/harvester/scene/room_support.h
A engines/harvester/scene/scene.cpp
A engines/harvester/scene/scene.h
A engines/harvester/scene/scene_entities.cpp
A engines/harvester/scene/scene_entities.h
A engines/harvester/script/script.cpp
A engines/harvester/script/script.h
A engines/harvester/script/script_actors.cpp
A engines/harvester/script/script_commands.cpp
A engines/harvester/script/script_records.cpp
A engines/harvester/script/script_room.cpp
A engines/harvester/script/script_runtime.cpp
A engines/harvester/script/script_save.cpp
R engines/harvester/combat.cpp
R engines/harvester/combat.h
R engines/harvester/combat_contact.cpp
R engines/harvester/combat_effects.cpp
R engines/harvester/combat_geometry.cpp
R engines/harvester/combat_hud.cpp
R engines/harvester/combat_monsters.cpp
R engines/harvester/combat_reactions.cpp
R engines/harvester/combat_targeting.cpp
R engines/harvester/flow.cpp
R engines/harvester/flow.h
R engines/harvester/flow_debug.cpp
R engines/harvester/flow_map.cpp
R engines/harvester/flow_scene.cpp
R engines/harvester/flow_startup.cpp
R engines/harvester/flow_support.h
R engines/harvester/flow_transition.cpp
R engines/harvester/input.cpp
R engines/harvester/input.h
R engines/harvester/interaction.cpp
R engines/harvester/interaction.h
R engines/harvester/menu.cpp
R engines/harvester/menu.h
R engines/harvester/menu_config.cpp
R engines/harvester/menu_navigation.cpp
R engines/harvester/menu_options.cpp
R engines/harvester/menu_prompts.cpp
R engines/harvester/menu_render.cpp
R engines/harvester/menu_render.h
R engines/harvester/menu_save.cpp
R engines/harvester/player.cpp
R engines/harvester/player.h
R engines/harvester/player_animation.cpp
R engines/harvester/player_attack.cpp
R engines/harvester/player_idle.cpp
R engines/harvester/player_keyboard.cpp
R engines/harvester/player_movement.cpp
R engines/harvester/player_movement.h
R engines/harvester/player_reactions.cpp
R engines/harvester/player_target.cpp
R engines/harvester/room_presentation.cpp
R engines/harvester/room_presentation.h
R engines/harvester/room_support.h
R engines/harvester/scene.cpp
R engines/harvester/scene.h
R engines/harvester/scene_entities.cpp
R engines/harvester/scene_entities.h
R engines/harvester/script.cpp
R engines/harvester/script.h
R engines/harvester/script_actors.cpp
R engines/harvester/script_commands.cpp
R engines/harvester/script_records.cpp
R engines/harvester/script_room.cpp
R engines/harvester/script_runtime.cpp
R engines/harvester/script_save.cpp
engines/harvester/cheats.cpp
engines/harvester/dialogue.cpp
engines/harvester/dialogue.h
engines/harvester/harvester.cpp
engines/harvester/harvester.h
engines/harvester/inventory.cpp
engines/harvester/inventory.h
engines/harvester/media_manager.cpp
engines/harvester/metaengine.cpp
engines/harvester/module.mk
engines/harvester/npc/dialogue_runtime.h
engines/harvester/npc/mom_dialogue.cpp
engines/harvester/saveload.cpp
diff --git a/engines/harvester/cheats.cpp b/engines/harvester/cheats.cpp
index 8f315166d1d..6f18ee1fdb7 100644
--- a/engines/harvester/cheats.cpp
+++ b/engines/harvester/cheats.cpp
@@ -25,7 +25,7 @@
#include "common/util.h"
#include "harvester/detection.h"
#include "harvester/harvester.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat/combat.cpp
similarity index 98%
rename from engines/harvester/combat.cpp
rename to engines/harvester/combat/combat.cpp
index efe589c4955..c3704778b74 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat/combat.cpp
@@ -19,13 +19,13 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
-#include "harvester/scene_entities.h"
+#include "harvester/scene/scene_entities.h"
namespace Harvester {
diff --git a/engines/harvester/combat.h b/engines/harvester/combat/combat.h
similarity index 99%
rename from engines/harvester/combat.h
rename to engines/harvester/combat/combat.h
index af7c5fd8799..d5cf6e2e745 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat/combat.h
@@ -23,7 +23,7 @@
#define HARVESTER_COMBAT_H
#include "common/error.h"
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
namespace Harvester {
diff --git a/engines/harvester/combat_contact.cpp b/engines/harvester/combat/combat_contact.cpp
similarity index 99%
rename from engines/harvester/combat_contact.cpp
rename to engines/harvester/combat/combat_contact.cpp
index b2fb878f4a2..b31f3a6a20a 100644
--- a/engines/harvester/combat_contact.cpp
+++ b/engines/harvester/combat/combat_contact.cpp
@@ -19,15 +19,15 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
#include "harvester/harvester.h"
-#include "harvester/interaction.h"
-#include "harvester/player.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/player/player.h"
namespace Harvester {
diff --git a/engines/harvester/combat_effects.cpp b/engines/harvester/combat/combat_effects.cpp
similarity index 99%
rename from engines/harvester/combat_effects.cpp
rename to engines/harvester/combat/combat_effects.cpp
index 7ba3eb0f699..361f38df1c5 100644
--- a/engines/harvester/combat_effects.cpp
+++ b/engines/harvester/combat/combat_effects.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
@@ -28,7 +28,7 @@
#include "graphics/screen.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
-#include "harvester/player.h"
+#include "harvester/player/player.h"
namespace Harvester {
diff --git a/engines/harvester/combat_geometry.cpp b/engines/harvester/combat/combat_geometry.cpp
similarity index 97%
rename from engines/harvester/combat_geometry.cpp
rename to engines/harvester/combat/combat_geometry.cpp
index 4a472c28e8d..b4baec4d5d4 100644
--- a/engines/harvester/combat_geometry.cpp
+++ b/engines/harvester/combat/combat_geometry.cpp
@@ -19,10 +19,10 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/util.h"
-#include "harvester/player.h"
+#include "harvester/player/player.h"
#include "harvester/entity.h"
namespace Harvester {
diff --git a/engines/harvester/combat_hud.cpp b/engines/harvester/combat/combat_hud.cpp
similarity index 97%
rename from engines/harvester/combat_hud.cpp
rename to engines/harvester/combat/combat_hud.cpp
index a1e4c63ea6d..5b7204cd79e 100644
--- a/engines/harvester/combat_hud.cpp
+++ b/engines/harvester/combat/combat_hud.cpp
@@ -19,9 +19,9 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/combat_monsters.cpp b/engines/harvester/combat/combat_monsters.cpp
similarity index 99%
rename from engines/harvester/combat_monsters.cpp
rename to engines/harvester/combat/combat_monsters.cpp
index 71cc0853b42..519ec555df3 100644
--- a/engines/harvester/combat_monsters.cpp
+++ b/engines/harvester/combat/combat_monsters.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
@@ -27,8 +27,8 @@
#include "harvester/detection.h"
#include "harvester/entity.h"
#include "harvester/harvester.h"
-#include "harvester/interaction.h"
-#include "harvester/player.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/player/player.h"
namespace Harvester {
diff --git a/engines/harvester/combat_reactions.cpp b/engines/harvester/combat/combat_reactions.cpp
similarity index 99%
rename from engines/harvester/combat_reactions.cpp
rename to engines/harvester/combat/combat_reactions.cpp
index e6704967707..a646700eb05 100644
--- a/engines/harvester/combat_reactions.cpp
+++ b/engines/harvester/combat/combat_reactions.cpp
@@ -19,15 +19,15 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
#include "harvester/harvester.h"
-#include "harvester/interaction.h"
-#include "harvester/scene_entities.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/scene/scene_entities.h"
namespace Harvester {
diff --git a/engines/harvester/combat_targeting.cpp b/engines/harvester/combat/combat_targeting.cpp
similarity index 99%
rename from engines/harvester/combat_targeting.cpp
rename to engines/harvester/combat/combat_targeting.cpp
index 93ac3fb9423..a70d64de389 100644
--- a/engines/harvester/combat_targeting.cpp
+++ b/engines/harvester/combat/combat_targeting.cpp
@@ -19,12 +19,12 @@
*
*/
-#include "harvester/combat.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
#include "harvester/detection.h"
-#include "harvester/player.h"
+#include "harvester/player/player.h"
#include "harvester/entity.h"
namespace Harvester {
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index a67dc6c09ac..2a687180268 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -87,7 +87,7 @@
#include "harvester/resources.h"
#include "harvester/entity.h"
#include "harvester/art.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/dialogue.h b/engines/harvester/dialogue.h
index 5ef2c8b8664..064cda3a253 100644
--- a/engines/harvester/dialogue.h
+++ b/engines/harvester/dialogue.h
@@ -27,7 +27,7 @@
#include "common/rect.h"
#include "common/str.h"
#include "harvester/npc/dialogue_handler.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Common {
struct Event;
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow/flow.cpp
similarity index 99%
rename from engines/harvester/flow.cpp
rename to engines/harvester/flow/flow.cpp
index ac67db0b40e..db68aea317a 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow/flow.cpp
@@ -19,16 +19,16 @@
*
*/
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "common/algorithm.h"
#include "common/events.h"
#include "common/memstream.h"
#include "common/serializer.h"
#include "harvester/harvester.h"
-#include "harvester/input.h"
+#include "harvester/scene/input.h"
#include "harvester/entity.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/flow.h b/engines/harvester/flow/flow.h
similarity index 98%
rename from engines/harvester/flow.h
rename to engines/harvester/flow/flow.h
index f3d7b35917d..b056fe15f08 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow/flow.h
@@ -29,9 +29,9 @@
#include "harvester/cheats.h"
#include "harvester/dialogue.h"
#include "harvester/inventory.h"
-#include "harvester/menu.h"
-#include "harvester/scene.h"
-#include "harvester/script.h"
+#include "harvester/menu/menu.h"
+#include "harvester/scene/scene.h"
+#include "harvester/script/script.h"
namespace Common {
struct Event;
diff --git a/engines/harvester/flow_debug.cpp b/engines/harvester/flow/flow_debug.cpp
similarity index 98%
rename from engines/harvester/flow_debug.cpp
rename to engines/harvester/flow/flow_debug.cpp
index 82f382fd813..51f3678ba90 100644
--- a/engines/harvester/flow_debug.cpp
+++ b/engines/harvester/flow/flow_debug.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "common/config-manager.h"
#include "common/debug.h"
@@ -27,9 +27,9 @@
#include "harvester/detection.h"
#include "harvester/entity.h"
#include "harvester/harvester.h"
-#include "harvester/input.h"
-#include "harvester/player.h"
-#include "harvester/scene_entities.h"
+#include "harvester/scene/input.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/scene_entities.h"
namespace Harvester {
diff --git a/engines/harvester/flow_map.cpp b/engines/harvester/flow/flow_map.cpp
similarity index 98%
rename from engines/harvester/flow_map.cpp
rename to engines/harvester/flow/flow_map.cpp
index fc3eaa61859..4571657cb74 100644
--- a/engines/harvester/flow_map.cpp
+++ b/engines/harvester/flow/flow_map.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/flow.h"
-#include "harvester/flow_support.h"
+#include "harvester/flow/flow.h"
+#include "harvester/flow/flow_support.h"
#include "common/debug.h"
#include "common/events.h"
@@ -35,7 +35,7 @@
#include "harvester/detection.h"
#include "harvester/entity.h"
#include "harvester/harvester.h"
-#include "harvester/room_presentation.h"
+#include "harvester/scene/room_presentation.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/flow_scene.cpp b/engines/harvester/flow/flow_scene.cpp
similarity index 98%
rename from engines/harvester/flow_scene.cpp
rename to engines/harvester/flow/flow_scene.cpp
index 86adc616662..c33218013e0 100644
--- a/engines/harvester/flow_scene.cpp
+++ b/engines/harvester/flow/flow_scene.cpp
@@ -19,15 +19,15 @@
*
*/
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "common/debug.h"
#include "common/util.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
#include "harvester/harvester.h"
-#include "harvester/player.h"
-#include "harvester/scene_entities.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/scene_entities.h"
namespace Harvester {
diff --git a/engines/harvester/flow_startup.cpp b/engines/harvester/flow/flow_startup.cpp
similarity index 98%
rename from engines/harvester/flow_startup.cpp
rename to engines/harvester/flow/flow_startup.cpp
index 46c8ebe7339..d0fef9a71c1 100644
--- a/engines/harvester/flow_startup.cpp
+++ b/engines/harvester/flow/flow_startup.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/flow.h"
-#include "harvester/flow_support.h"
+#include "harvester/flow/flow.h"
+#include "harvester/flow/flow_support.h"
#include "common/algorithm.h"
#include "common/debug.h"
@@ -37,8 +37,8 @@
#include "harvester/fst_player.h"
#include "harvester/harvester.h"
#include "harvester/resources.h"
-#include "harvester/room_presentation.h"
-#include "harvester/scene_entities.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/scene/scene_entities.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/flow_support.h b/engines/harvester/flow/flow_support.h
similarity index 100%
rename from engines/harvester/flow_support.h
rename to engines/harvester/flow/flow_support.h
diff --git a/engines/harvester/flow_transition.cpp b/engines/harvester/flow/flow_transition.cpp
similarity index 98%
rename from engines/harvester/flow_transition.cpp
rename to engines/harvester/flow/flow_transition.cpp
index 6bb412eb0e5..00e226a1f47 100644
--- a/engines/harvester/flow_transition.cpp
+++ b/engines/harvester/flow/flow_transition.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/flow.h"
-#include "harvester/flow_support.h"
+#include "harvester/flow/flow.h"
+#include "harvester/flow/flow_support.h"
#include "common/debug.h"
#include "common/events.h"
diff --git a/engines/harvester/harvester.cpp b/engines/harvester/harvester.cpp
index 6deb9bf7309..8969ce3153c 100644
--- a/engines/harvester/harvester.cpp
+++ b/engines/harvester/harvester.cpp
@@ -28,8 +28,8 @@
#include "harvester/fst_player.h"
#include "harvester/media_manager.h"
#include "harvester/resources.h"
-#include "harvester/flow.h"
-#include "harvester/script.h"
+#include "harvester/flow/flow.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/harvester.h b/engines/harvester/harvester.h
index b2d9c066a33..761205cbcad 100644
--- a/engines/harvester/harvester.h
+++ b/engines/harvester/harvester.h
@@ -30,7 +30,7 @@
#include "engines/engine.h"
#include "graphics/screen.h"
#include "harvester/saveload.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
struct ADGameDescription;
diff --git a/engines/harvester/inventory.cpp b/engines/harvester/inventory.cpp
index 5670dc0a88c..a2a0405b283 100644
--- a/engines/harvester/inventory.cpp
+++ b/engines/harvester/inventory.cpp
@@ -27,8 +27,8 @@
#include "graphics/screen.h"
#include "harvester/detection.h"
#include "harvester/harvester.h"
-#include "harvester/menu.h"
-#include "harvester/player.h"
+#include "harvester/menu/menu.h"
+#include "harvester/player/player.h"
#include "harvester/resources.h"
#include "harvester/art.h"
diff --git a/engines/harvester/inventory.h b/engines/harvester/inventory.h
index b7e5f590b3a..416630e0199 100644
--- a/engines/harvester/inventory.h
+++ b/engines/harvester/inventory.h
@@ -28,7 +28,7 @@
#include "common/rect.h"
#include "common/str.h"
#include "harvester/art.h"
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
namespace Graphics {
class Screen;
diff --git a/engines/harvester/media_manager.cpp b/engines/harvester/media_manager.cpp
index 7e4980b3cec..e7d2b2c8a77 100644
--- a/engines/harvester/media_manager.cpp
+++ b/engines/harvester/media_manager.cpp
@@ -32,7 +32,7 @@
#include "harvester/art.h"
#include "harvester/resources.h"
#include "harvester/entity.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu/menu.cpp
similarity index 98%
rename from engines/harvester/menu.cpp
rename to engines/harvester/menu/menu.cpp
index 67db206a4ef..8c36537d174 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu/menu.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/menu.h"
-#include "harvester/menu_render.h"
+#include "harvester/menu/menu.h"
+#include "harvester/menu/menu_render.h"
#include "common/algorithm.h"
#include "common/events.h"
@@ -32,7 +32,7 @@
#include "harvester/resources.h"
#include "harvester/entity.h"
#include "harvester/art.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
namespace Harvester {
diff --git a/engines/harvester/menu.h b/engines/harvester/menu/menu.h
similarity index 100%
rename from engines/harvester/menu.h
rename to engines/harvester/menu/menu.h
diff --git a/engines/harvester/menu_config.cpp b/engines/harvester/menu/menu_config.cpp
similarity index 99%
rename from engines/harvester/menu_config.cpp
rename to engines/harvester/menu/menu_config.cpp
index 11c7f086e28..86ad8668c7a 100644
--- a/engines/harvester/menu_config.cpp
+++ b/engines/harvester/menu/menu_config.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/menu.h"
+#include "harvester/menu/menu.h"
#include "common/algorithm.h"
#include "common/debug.h"
diff --git a/engines/harvester/menu_navigation.cpp b/engines/harvester/menu/menu_navigation.cpp
similarity index 99%
rename from engines/harvester/menu_navigation.cpp
rename to engines/harvester/menu/menu_navigation.cpp
index bae825e2ea7..2c6e1f69e94 100644
--- a/engines/harvester/menu_navigation.cpp
+++ b/engines/harvester/menu/menu_navigation.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/menu.h"
-#include "harvester/menu_render.h"
+#include "harvester/menu/menu.h"
+#include "harvester/menu/menu_render.h"
#include "common/algorithm.h"
#include "common/events.h"
@@ -30,9 +30,9 @@
#include "harvester/cft_font.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/menu_options.cpp b/engines/harvester/menu/menu_options.cpp
similarity index 99%
rename from engines/harvester/menu_options.cpp
rename to engines/harvester/menu/menu_options.cpp
index 743ef42f384..a48b6469d56 100644
--- a/engines/harvester/menu_options.cpp
+++ b/engines/harvester/menu/menu_options.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/menu.h"
-#include "harvester/menu_render.h"
+#include "harvester/menu/menu.h"
+#include "harvester/menu/menu_render.h"
#include "common/algorithm.h"
#include "common/config-manager.h"
@@ -30,9 +30,9 @@
#include "harvester/cft_font.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/menu_prompts.cpp b/engines/harvester/menu/menu_prompts.cpp
similarity index 97%
rename from engines/harvester/menu_prompts.cpp
rename to engines/harvester/menu/menu_prompts.cpp
index e15a4d85f7c..2bb0f9482f4 100644
--- a/engines/harvester/menu_prompts.cpp
+++ b/engines/harvester/menu/menu_prompts.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/menu.h"
-#include "harvester/menu_render.h"
+#include "harvester/menu/menu.h"
+#include "harvester/menu/menu_render.h"
#include "common/events.h"
#include "common/system.h"
@@ -28,10 +28,10 @@
#include "harvester/cft_font.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
-#include "harvester/room_support.h"
-#include "harvester/script.h"
+#include "harvester/scene/room_support.h"
+#include "harvester/script/script.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/menu_render.cpp b/engines/harvester/menu/menu_render.cpp
similarity index 99%
rename from engines/harvester/menu_render.cpp
rename to engines/harvester/menu/menu_render.cpp
index 4de241de250..54aecee7c9d 100644
--- a/engines/harvester/menu_render.cpp
+++ b/engines/harvester/menu/menu_render.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/menu_render.h"
+#include "harvester/menu/menu_render.h"
#include "common/algorithm.h"
#include "graphics/fontman.h"
@@ -27,7 +27,7 @@
#include "harvester/entity.h"
#include "harvester/harvester.h"
#include "harvester/palette_utils.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "harvester/text.h"
namespace Harvester {
diff --git a/engines/harvester/menu_render.h b/engines/harvester/menu/menu_render.h
similarity index 99%
rename from engines/harvester/menu_render.h
rename to engines/harvester/menu/menu_render.h
index b64ab5280af..5ec5dfe9ca7 100644
--- a/engines/harvester/menu_render.h
+++ b/engines/harvester/menu/menu_render.h
@@ -23,7 +23,7 @@
#define HARVESTER_MENU_RENDER_H
#include "graphics/font.h"
-#include "harvester/menu.h"
+#include "harvester/menu/menu.h"
namespace Graphics {
class Screen;
diff --git a/engines/harvester/menu_save.cpp b/engines/harvester/menu/menu_save.cpp
similarity index 99%
rename from engines/harvester/menu_save.cpp
rename to engines/harvester/menu/menu_save.cpp
index 607f52d3213..c32694aaab6 100644
--- a/engines/harvester/menu_save.cpp
+++ b/engines/harvester/menu/menu_save.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/menu.h"
-#include "harvester/menu_render.h"
+#include "harvester/menu/menu.h"
+#include "harvester/menu/menu_render.h"
#include "common/algorithm.h"
#include "common/config-manager.h"
@@ -30,7 +30,7 @@
#include "graphics/framelimiter.h"
#include "harvester/cft_font.h"
#include "harvester/entity.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
#include "harvester/palette_utils.h"
#include "harvester/text.h"
diff --git a/engines/harvester/metaengine.cpp b/engines/harvester/metaengine.cpp
index a51f6ff6be3..08ec130bde0 100644
--- a/engines/harvester/metaengine.cpp
+++ b/engines/harvester/metaengine.cpp
@@ -28,7 +28,7 @@
#include "harvester/detection.h"
#include "harvester/metaengine.h"
#include "harvester/harvester.h"
-#include "harvester/input.h"
+#include "harvester/scene/input.h"
static const ADExtraGuiOptionsMap gameGuiOptions[] = {
{
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 5371cde46e3..3faf628a78a 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -7,56 +7,56 @@ MODULE_OBJS = \
cheats.o \
console.o \
dialogue.o \
- flow.o \
- flow_debug.o \
- flow_map.o \
- flow_scene.o \
- flow_startup.o \
- flow_transition.o \
+ flow/flow.o \
+ flow/flow_debug.o \
+ flow/flow_map.o \
+ flow/flow_scene.o \
+ flow/flow_startup.o \
+ flow/flow_transition.o \
fst_player.o \
harvester.o \
inventory.o \
media_manager.o \
- menu.o \
- menu_config.o \
- menu_navigation.o \
- menu_options.o \
- menu_prompts.o \
- menu_render.o \
- menu_save.o \
+ menu/menu.o \
+ menu/menu_config.o \
+ menu/menu_navigation.o \
+ menu/menu_options.o \
+ menu/menu_prompts.o \
+ menu/menu_render.o \
+ menu/menu_save.o \
metaengine.o \
monster.o \
- player.o \
- player_animation.o \
- player_attack.o \
- player_idle.o \
- player_keyboard.o \
- player_movement.o \
- player_reactions.o \
- player_target.o \
+ player/player.o \
+ player/player_animation.o \
+ player/player_attack.o \
+ player/player_idle.o \
+ player/player_keyboard.o \
+ player/player_movement.o \
+ player/player_reactions.o \
+ player/player_target.o \
resources.o \
- combat.o \
- combat_contact.o \
- combat_effects.o \
- combat_geometry.o \
- combat_hud.o \
- combat_monsters.o \
- combat_reactions.o \
- combat_targeting.o \
- interaction.o \
- input.o \
- room_presentation.o \
- scene_entities.o \
- scene.o \
+ combat/combat.o \
+ combat/combat_contact.o \
+ combat/combat_effects.o \
+ combat/combat_geometry.o \
+ combat/combat_hud.o \
+ combat/combat_monsters.o \
+ combat/combat_reactions.o \
+ combat/combat_targeting.o \
+ scene/interaction.o \
+ scene/input.o \
+ scene/room_presentation.o \
+ scene/scene_entities.o \
+ scene/scene.o \
entity.o \
saveload.o \
- script.o \
- script_actors.o \
- script_commands.o \
- script_records.o \
- script_room.o \
- script_runtime.o \
- script_save.o \
+ script/script.o \
+ script/script_actors.o \
+ script/script_commands.o \
+ script/script_records.o \
+ script/script_room.o \
+ script/script_runtime.o \
+ script/script_save.o \
text.o \
xfile_archive.o \
npc/authority_dialogue.o \
diff --git a/engines/harvester/npc/dialogue_runtime.h b/engines/harvester/npc/dialogue_runtime.h
index 2439d201869..7acca760b06 100644
--- a/engines/harvester/npc/dialogue_runtime.h
+++ b/engines/harvester/npc/dialogue_runtime.h
@@ -3,7 +3,7 @@
#include "common/error.h"
#include "common/str.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/npc/mom_dialogue.cpp b/engines/harvester/npc/mom_dialogue.cpp
index be474d65cae..4f3e7a430db 100644
--- a/engines/harvester/npc/mom_dialogue.cpp
+++ b/engines/harvester/npc/mom_dialogue.cpp
@@ -24,7 +24,7 @@
#include "harvester/npc/dialogue_flags.h"
#include "harvester/npc/dialogue_runtime.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/player.cpp b/engines/harvester/player/player.cpp
similarity index 98%
rename from engines/harvester/player.cpp
rename to engines/harvester/player/player.cpp
index 867739a4113..0809297a20c 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player/player.cpp
@@ -19,9 +19,9 @@
*
*/
-#include "harvester/player.h"
-#include "harvester/player_movement.h"
-#include "harvester/combat.h"
+#include "harvester/player/player.h"
+#include "harvester/player/player_movement.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "harvester/detection.h"
diff --git a/engines/harvester/player.h b/engines/harvester/player/player.h
similarity index 99%
rename from engines/harvester/player.h
rename to engines/harvester/player/player.h
index ef0dcd81b1d..23883e64a26 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player/player.h
@@ -22,7 +22,7 @@
#ifndef HARVESTER_PLAYER_H
#define HARVESTER_PLAYER_H
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
namespace Harvester {
diff --git a/engines/harvester/player_animation.cpp b/engines/harvester/player/player_animation.cpp
similarity index 98%
rename from engines/harvester/player_animation.cpp
rename to engines/harvester/player/player_animation.cpp
index f6afcca8e94..ab1d2ff48a3 100644
--- a/engines/harvester/player_animation.cpp
+++ b/engines/harvester/player/player_animation.cpp
@@ -19,9 +19,9 @@
*
*/
-#include "harvester/player.h"
-#include "harvester/player_movement.h"
-#include "harvester/combat.h"
+#include "harvester/player/player.h"
+#include "harvester/player/player_movement.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/player_attack.cpp b/engines/harvester/player/player_attack.cpp
similarity index 99%
rename from engines/harvester/player_attack.cpp
rename to engines/harvester/player/player_attack.cpp
index e4698488be5..2f1fd0191c2 100644
--- a/engines/harvester/player_attack.cpp
+++ b/engines/harvester/player/player_attack.cpp
@@ -19,9 +19,9 @@
*
*/
-#include "harvester/player.h"
-#include "harvester/player_movement.h"
-#include "harvester/combat.h"
+#include "harvester/player/player.h"
+#include "harvester/player/player_movement.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/player_idle.cpp b/engines/harvester/player/player_idle.cpp
similarity index 99%
rename from engines/harvester/player_idle.cpp
rename to engines/harvester/player/player_idle.cpp
index c2e2e63e77f..016ad972b98 100644
--- a/engines/harvester/player_idle.cpp
+++ b/engines/harvester/player/player_idle.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/player.h"
+#include "harvester/player/player.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/player_keyboard.cpp b/engines/harvester/player/player_keyboard.cpp
similarity index 98%
rename from engines/harvester/player_keyboard.cpp
rename to engines/harvester/player/player_keyboard.cpp
index 47ee1b7bd23..265c1700e5d 100644
--- a/engines/harvester/player_keyboard.cpp
+++ b/engines/harvester/player/player_keyboard.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/player.h"
-#include "harvester/player_movement.h"
+#include "harvester/player/player.h"
+#include "harvester/player/player_movement.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/player_movement.cpp b/engines/harvester/player/player_movement.cpp
similarity index 99%
rename from engines/harvester/player_movement.cpp
rename to engines/harvester/player/player_movement.cpp
index b60a0eafcb2..d124f267250 100644
--- a/engines/harvester/player_movement.cpp
+++ b/engines/harvester/player/player_movement.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/player_movement.h"
+#include "harvester/player/player_movement.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/player_movement.h b/engines/harvester/player/player_movement.h
similarity index 98%
rename from engines/harvester/player_movement.h
rename to engines/harvester/player/player_movement.h
index a2304296de9..5d586ea6958 100644
--- a/engines/harvester/player_movement.h
+++ b/engines/harvester/player/player_movement.h
@@ -22,7 +22,7 @@
#ifndef HARVESTER_PLAYER_MOVEMENT_H
#define HARVESTER_PLAYER_MOVEMENT_H
-#include "harvester/player.h"
+#include "harvester/player/player.h"
namespace Harvester {
namespace PlayerMovement {
diff --git a/engines/harvester/player_reactions.cpp b/engines/harvester/player/player_reactions.cpp
similarity index 99%
rename from engines/harvester/player_reactions.cpp
rename to engines/harvester/player/player_reactions.cpp
index 57bdf929e03..9ca61143fcd 100644
--- a/engines/harvester/player_reactions.cpp
+++ b/engines/harvester/player/player_reactions.cpp
@@ -19,9 +19,9 @@
*
*/
-#include "harvester/player.h"
-#include "harvester/player_movement.h"
-#include "harvester/combat.h"
+#include "harvester/player/player.h"
+#include "harvester/player/player_movement.h"
+#include "harvester/combat/combat.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/player_target.cpp b/engines/harvester/player/player_target.cpp
similarity index 99%
rename from engines/harvester/player_target.cpp
rename to engines/harvester/player/player_target.cpp
index 3428f81cfeb..03c3da0a3a9 100644
--- a/engines/harvester/player_target.cpp
+++ b/engines/harvester/player/player_target.cpp
@@ -19,8 +19,8 @@
*
*/
-#include "harvester/player.h"
-#include "harvester/player_movement.h"
+#include "harvester/player/player.h"
+#include "harvester/player/player_movement.h"
#include "common/debug.h"
#include "common/util.h"
diff --git a/engines/harvester/saveload.cpp b/engines/harvester/saveload.cpp
index d3ad83d8de2..748782e7b6d 100644
--- a/engines/harvester/saveload.cpp
+++ b/engines/harvester/saveload.cpp
@@ -24,9 +24,9 @@
#include "common/algorithm.h"
#include "common/serializer.h"
#include "harvester/detection.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/resources.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Harvester {
diff --git a/engines/harvester/input.cpp b/engines/harvester/scene/input.cpp
similarity index 99%
rename from engines/harvester/input.cpp
rename to engines/harvester/scene/input.cpp
index 4aa4bca1928..2402be6c3ad 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/scene/input.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/input.h"
+#include "harvester/scene/input.h"
#include "common/debug.h"
#include "graphics/screen.h"
@@ -27,15 +27,15 @@
#include "harvester/cheats.h"
#include "harvester/detection.h"
#include "harvester/dialogue.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
-#include "harvester/player.h"
-#include "harvester/interaction.h"
-#include "harvester/room_presentation.h"
-#include "harvester/scene.h"
-#include "harvester/room_support.h"
-#include "harvester/scene_entities.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/scene/scene.h"
+#include "harvester/scene/room_support.h"
+#include "harvester/scene/scene_entities.h"
namespace Harvester {
diff --git a/engines/harvester/input.h b/engines/harvester/scene/input.h
similarity index 100%
rename from engines/harvester/input.h
rename to engines/harvester/scene/input.h
diff --git a/engines/harvester/interaction.cpp b/engines/harvester/scene/interaction.cpp
similarity index 99%
rename from engines/harvester/interaction.cpp
rename to engines/harvester/scene/interaction.cpp
index a5f33947652..37156a5d23f 100644
--- a/engines/harvester/interaction.cpp
+++ b/engines/harvester/scene/interaction.cpp
@@ -19,19 +19,19 @@
*
*/
-#include "harvester/interaction.h"
+#include "harvester/scene/interaction.h"
#include "common/algorithm.h"
#include "common/debug.h"
#include "common/textconsole.h"
#include "harvester/art.h"
#include "harvester/detection.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/fst_player.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
-#include "harvester/player.h"
-#include "harvester/scene_entities.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/scene_entities.h"
#include "harvester/resources.h"
#include "harvester/entity.h"
diff --git a/engines/harvester/interaction.h b/engines/harvester/scene/interaction.h
similarity index 99%
rename from engines/harvester/interaction.h
rename to engines/harvester/scene/interaction.h
index 23150d27f58..16c1b125500 100644
--- a/engines/harvester/interaction.h
+++ b/engines/harvester/scene/interaction.h
@@ -24,7 +24,7 @@
#include "common/error.h"
#include "harvester/bitmap.h"
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
#include "harvester/saveload.h"
namespace Harvester {
diff --git a/engines/harvester/room_presentation.cpp b/engines/harvester/scene/room_presentation.cpp
similarity index 99%
rename from engines/harvester/room_presentation.cpp
rename to engines/harvester/scene/room_presentation.cpp
index 936a77c136f..5f3fb7bd4dd 100644
--- a/engines/harvester/room_presentation.cpp
+++ b/engines/harvester/scene/room_presentation.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/room_presentation.h"
+#include "harvester/scene/room_presentation.h"
#include "common/algorithm.h"
#include "common/debug.h"
@@ -33,12 +33,12 @@
#include "harvester/cft_font.h"
#include "harvester/detection.h"
#include "harvester/entity.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
#include "harvester/palette_utils.h"
-#include "harvester/player.h"
+#include "harvester/player/player.h"
#include "harvester/resources.h"
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
namespace Harvester {
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/scene/room_presentation.h
similarity index 98%
rename from engines/harvester/room_presentation.h
rename to engines/harvester/scene/room_presentation.h
index 008531dbe41..65f73694c29 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/scene/room_presentation.h
@@ -24,7 +24,7 @@
#include "common/error.h"
#include "graphics/font.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Graphics {
class Screen;
diff --git a/engines/harvester/room_support.h b/engines/harvester/scene/room_support.h
similarity index 98%
rename from engines/harvester/room_support.h
rename to engines/harvester/scene/room_support.h
index baeb8da57e0..2e6a217b1ab 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/scene/room_support.h
@@ -26,7 +26,7 @@
#include "common/rect.h"
#include "common/scummsys.h"
#include "common/str.h"
-#include "harvester/script.h"
+#include "harvester/script/script.h"
namespace Graphics {
class Font;
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene/scene.cpp
similarity index 99%
rename from engines/harvester/scene.cpp
rename to engines/harvester/scene/scene.cpp
index 3f61575dbae..55f0f82ab7f 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene/scene.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/scene.h"
+#include "harvester/scene/scene.h"
#include "backends/keymapper/keymapper.h"
#include "common/algorithm.h"
@@ -30,11 +30,11 @@
#include "harvester/art.h"
#include "harvester/detection.h"
#include "harvester/fst_player.h"
-#include "harvester/flow.h"
+#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
-#include "harvester/player.h"
-#include "harvester/combat.h"
+#include "harvester/player/player.h"
+#include "harvester/combat/combat.h"
#include "harvester/entity.h"
#include "common/ptr.h"
@@ -44,11 +44,11 @@
#include "harvester/cheats.h"
#include "harvester/resources.h"
#include "harvester/monster.h"
-#include "harvester/interaction.h"
-#include "harvester/input.h"
-#include "harvester/room_presentation.h"
-#include "harvester/room_support.h"
-#include "harvester/scene_entities.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/scene/input.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/scene/room_support.h"
+#include "harvester/scene/scene_entities.h"
namespace Harvester {
diff --git a/engines/harvester/scene.h b/engines/harvester/scene/scene.h
similarity index 94%
rename from engines/harvester/scene.h
rename to engines/harvester/scene/scene.h
index f8a157b8d2f..76f25876ecc 100644
--- a/engines/harvester/scene.h
+++ b/engines/harvester/scene/scene.h
@@ -24,13 +24,13 @@
#include "common/error.h"
#include "common/str.h"
-#include "harvester/combat.h"
-#include "harvester/input.h"
-#include "harvester/player.h"
-#include "harvester/interaction.h"
-#include "harvester/room_presentation.h"
-#include "harvester/scene_entities.h"
-#include "harvester/room_support.h"
+#include "harvester/combat/combat.h"
+#include "harvester/scene/input.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/scene/scene_entities.h"
+#include "harvester/scene/room_support.h"
#include "harvester/entity.h"
namespace Harvester {
diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene/scene_entities.cpp
similarity index 99%
rename from engines/harvester/scene_entities.cpp
rename to engines/harvester/scene/scene_entities.cpp
index c01b91c5df5..632f83c362a 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene/scene_entities.cpp
@@ -19,14 +19,14 @@
*
*/
-#include "harvester/scene_entities.h"
+#include "harvester/scene/scene_entities.h"
#include "harvester/art.h"
#include "harvester/detection.h"
#include "harvester/resources.h"
#include "harvester/monster.h"
-#include "harvester/player.h"
-#include "harvester/combat.h"
+#include "harvester/player/player.h"
+#include "harvester/combat/combat.h"
namespace Harvester {
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene/scene_entities.h
similarity index 98%
rename from engines/harvester/scene_entities.h
rename to engines/harvester/scene/scene_entities.h
index c6ef70c4154..0c348fa1165 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene/scene_entities.h
@@ -22,7 +22,7 @@
#ifndef HARVESTER_SCENE_ENTITIES_H
#define HARVESTER_SCENE_ENTITIES_H
-#include "harvester/room_support.h"
+#include "harvester/scene/room_support.h"
#include "harvester/entity.h"
namespace Harvester {
diff --git a/engines/harvester/script.cpp b/engines/harvester/script/script.cpp
similarity index 99%
rename from engines/harvester/script.cpp
rename to engines/harvester/script/script.cpp
index 11523f22d04..02179c5641f 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script/script.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/config-manager.h"
#include "common/debug.h"
diff --git a/engines/harvester/script.h b/engines/harvester/script/script.h
similarity index 100%
rename from engines/harvester/script.h
rename to engines/harvester/script/script.h
diff --git a/engines/harvester/script_actors.cpp b/engines/harvester/script/script_actors.cpp
similarity index 99%
rename from engines/harvester/script_actors.cpp
rename to engines/harvester/script/script_actors.cpp
index 959de57a459..2486989ebe5 100644
--- a/engines/harvester/script_actors.cpp
+++ b/engines/harvester/script/script_actors.cpp
@@ -19,12 +19,12 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/algorithm.h"
#include "common/debug.h"
#include "harvester/detection.h"
-#include "harvester/player.h"
+#include "harvester/player/player.h"
namespace Harvester {
diff --git a/engines/harvester/script_commands.cpp b/engines/harvester/script/script_commands.cpp
similarity index 99%
rename from engines/harvester/script_commands.cpp
rename to engines/harvester/script/script_commands.cpp
index 5452e162e95..7308c12b5df 100644
--- a/engines/harvester/script_commands.cpp
+++ b/engines/harvester/script/script_commands.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/algorithm.h"
#include "common/debug.h"
diff --git a/engines/harvester/script_records.cpp b/engines/harvester/script/script_records.cpp
similarity index 99%
rename from engines/harvester/script_records.cpp
rename to engines/harvester/script/script_records.cpp
index 28550c473d4..7033d7745a9 100644
--- a/engines/harvester/script_records.cpp
+++ b/engines/harvester/script/script_records.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/algorithm.h"
#include "common/debug.h"
diff --git a/engines/harvester/script_room.cpp b/engines/harvester/script/script_room.cpp
similarity index 99%
rename from engines/harvester/script_room.cpp
rename to engines/harvester/script/script_room.cpp
index de311cdffa3..2f79d32839c 100644
--- a/engines/harvester/script_room.cpp
+++ b/engines/harvester/script/script_room.cpp
@@ -19,12 +19,12 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/debug.h"
#include "common/endian.h"
#include "harvester/detection.h"
-#include "harvester/player.h"
+#include "harvester/player/player.h"
#include "harvester/resources.h"
namespace Harvester {
diff --git a/engines/harvester/script_runtime.cpp b/engines/harvester/script/script_runtime.cpp
similarity index 99%
rename from engines/harvester/script_runtime.cpp
rename to engines/harvester/script/script_runtime.cpp
index 70d529703d9..45e553f48da 100644
--- a/engines/harvester/script_runtime.cpp
+++ b/engines/harvester/script/script_runtime.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/algorithm.h"
#include "common/debug.h"
diff --git a/engines/harvester/script_save.cpp b/engines/harvester/script/script_save.cpp
similarity index 99%
rename from engines/harvester/script_save.cpp
rename to engines/harvester/script/script_save.cpp
index 0af41d760d5..102e2d7707c 100644
--- a/engines/harvester/script_save.cpp
+++ b/engines/harvester/script/script_save.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/script.h"
+#include "harvester/script/script.h"
#include "common/algorithm.h"
#include "common/debug.h"
More information about the Scummvm-git-logs
mailing list