[Scummvm-git-logs] scummvm master -> 0b24b9b5b0caa395a68bde42e7508a3544d34533
alexbevi
noreply at scummvm.org
Tue Oct 13 21:40:30 UTC 2026
This automated email contains information about 28 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
2c50e80242 HARVESTER: refactor BM/ABM resource handling
4f581772ae HARVESTER: Make command execution inputs and outputs explicit
3be8724e8b HARVESTER: extract RoomSession for managing room state and player interactions
febc916db5 HARVESTER: refactor to manage room state and synchronize runtime entities
8ee387d338 HARVESTER: refactor RoomSession to manage scene entity lifecycle and interactions
2be5442bcc HARVESTER: move inventory management and runtime mutations to RoomSession
a4044055a3 HARVESTER: refactor RoomSession to include Flow for enhanced scene management
59e4140ebc HARVESTER: RoomCombatEffects for managing hit effects and damage popups
f59e75bbdb HARVESTER: Add RoomSceneRuntime class to manage scene entities and interactions
36b5ca56de HARVESTER: Refactor RoomSession and RoomSceneRuntime for improved timer synchronization and state management
ef370dd63a HARVESTER: Refactor combat state management in RoomCombat and RoomSession
88b55c3962 HARVESTER: Refactor entity state comparison functions and consolidate some scene entity logic
91f5b74f02 HARVESTER: Implement player rebind after scene refresh and restore monster animations
4a19173857 HARVESTER: Refactor combat target selection and interaction methods
60d34b8f08 HARVESTER: Refactor combat interaction handling and NPC death transitions
79ef709056 HARVESTER: Refactor NPC combat state handling
1f2fc6454e HARVESTER: more RoomCombat refactoring
dd34002fcc HARVESTER: Refactor room item interactions some inventory handling
47ccd38593 HARVESTER: Refactor region interaction handling in RoomInteractionProcessor
a38bca9f3d HARVESTER: Refactor to separate presentation and modal operations
1d3c209f66 HARVESTER: Extract input handling
3238191c58 HARVESTER: Refactor session and input classes for improved interaction handling
ccfabd608b HARVESTER: begin splitting room/scene handling
d8ab1acf31 HARVESTER: refactor/rename Entity, Interaction, Combat and Input
939d4f10ea HARVESTER: Update walk direction/depth after animation
11452159f5 HARVESTER: fix player bounds checking with room exits
0cca8a4c2c HARVESTER: play walking blocked sounds
0b24b9b5b0 HARVESTER: cleanup and log verbosity adjustments
Commit: 2c50e80242f8b055b451c7d5c9de6688c270df84
https://github.com/scummvm/scummvm/commit/2c50e80242f8b055b451c7d5c9de6688c270df84
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: refactor BM/ABM resource handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/bitmap.cpp
A engines/harvester/bitmap.h
engines/harvester/art.cpp
engines/harvester/art.h
engines/harvester/dialogue.cpp
engines/harvester/flow.cpp
engines/harvester/inventory.cpp
engines/harvester/menu.cpp
engines/harvester/module.mk
engines/harvester/room.cpp
engines/harvester/runtime_entity.cpp
engines/harvester/runtime_entity.h
diff --git a/engines/harvester/art.cpp b/engines/harvester/art.cpp
index 776264f9fc6..1ed1a09eeef 100644
--- a/engines/harvester/art.cpp
+++ b/engines/harvester/art.cpp
@@ -22,7 +22,6 @@
#include "harvester/art.h"
#include "common/debug.h"
-#include "common/endian.h"
#include "graphics/blit.h"
#include "graphics/screen.h"
#include "harvester/detection.h"
@@ -174,16 +173,10 @@ bool Art::loadBitmap(ResourceManager &resources, const Common::String &path, Ind
return false;
}
- bitmap.width = READ_LE_UINT32(data.data());
- bitmap.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = bitmap.width * bitmap.height;
- if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount) {
+ if (!decodeBitmap(data, bitmap)) {
warning("Harvester: invalid bitmap '%s'", path.c_str());
return false;
}
-
- bitmap.pixels.resize(pixelCount);
- memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
return true;
}
@@ -194,74 +187,15 @@ bool Art::loadAnimation(ResourceManager &resources, const Common::String &path,
return false;
}
- const uint32 frameCount = READ_LE_UINT32(data.data());
- uint32 offset = 8;
- frames.resize(frameCount);
-
- for (uint32 i = 0; i < frameCount; ++i) {
- if (data.size() < offset + 25) {
- warning("Harvester: short ABM header in '%s'", path.c_str());
- return false;
- }
-
- AbmFrame &frame = frames[i];
- frame.xOffset = (int32)READ_LE_UINT32(data.data() + offset);
- frame.yOffset = (int32)READ_LE_UINT32(data.data() + offset + 4);
- frame.width = READ_LE_UINT32(data.data() + offset + 8);
- frame.height = READ_LE_UINT32(data.data() + offset + 12);
- const bool compressed = data[offset + 16] != 0;
- const uint32 sourceSize = READ_LE_UINT32(data.data() + offset + 17);
- const uint32 pixelCount = frame.width * frame.height;
- const uint32 payloadOffset = offset + 25;
-
- if (frame.width == 0 || frame.height == 0 || data.size() < payloadOffset + sourceSize) {
- warning("Harvester: invalid ABM frame %u in '%s'", i, path.c_str());
- return false;
- }
-
- frame.pixels.resize(pixelCount);
- if (!decodeAnimationFrame(data.data() + payloadOffset, sourceSize, compressed, frame.pixels))
- return false;
-
- offset = payloadOffset + sourceSize;
+ if (!decodeAbm(data, frames)) {
+ warning("Harvester: unable to decode animation '%s'", path.c_str());
+ return false;
}
debug(1, "Harvester: loaded %u frames from '%s'", (uint)frames.size(), path.c_str());
return true;
}
-bool Art::decodeAnimationFrame(const byte *source, uint32 sourceSize, bool compressed, Common::Array<byte> &dest) const {
- if (!compressed) {
- if (sourceSize < dest.size())
- return false;
-
- memcpy(dest.data(), source, dest.size());
- return true;
- }
-
- uint32 srcOffset = 0;
- uint32 dstOffset = 0;
-
- while (srcOffset < sourceSize && dstOffset < dest.size()) {
- const byte control = source[srcOffset++];
- if ((control & 0x80) == 0) {
- const uint32 literalCount = MIN<uint32>(control, MIN<uint32>(sourceSize - srcOffset, dest.size() - dstOffset));
- memcpy(dest.data() + dstOffset, source + srcOffset, literalCount);
- srcOffset += literalCount;
- dstOffset += literalCount;
- } else {
- if (srcOffset >= sourceSize)
- return false;
-
- const uint32 repeatCount = MIN<uint32>(control & 0x7f, dest.size() - dstOffset);
- memset(dest.data() + dstOffset, source[srcOffset++], repeatCount);
- dstOffset += repeatCount;
- }
- }
-
- return dstOffset == dest.size();
-}
-
void Art::blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) const {
if (!bitmap.isValid())
return;
diff --git a/engines/harvester/art.h b/engines/harvester/art.h
index d3460b09447..3877523750e 100644
--- a/engines/harvester/art.h
+++ b/engines/harvester/art.h
@@ -22,7 +22,7 @@
#ifndef HARVESTER_ART_H
#define HARVESTER_ART_H
-#include "common/array.h"
+#include "harvester/bitmap.h"
namespace Graphics {
class Screen;
@@ -32,21 +32,6 @@ namespace Harvester {
class ResourceManager;
-struct IndexedBitmap {
- uint32 width = 0;
- uint32 height = 0;
- Common::Array<byte> pixels;
-
- bool isValid() const {
- return width != 0 && height != 0 && pixels.size() >= width * height;
- }
-};
-
-struct AbmFrame : IndexedBitmap {
- int32 xOffset = 0;
- int32 yOffset = 0;
-};
-
class Art {
public:
bool load(ResourceManager &resources);
@@ -68,7 +53,6 @@ private:
bool loadPalette(ResourceManager &resources, const Common::String &path, byte *dest) const;
bool loadBitmap(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) const;
bool loadAnimation(ResourceManager &resources, const Common::String &path, Common::Array<AbmFrame> &frames) const;
- bool decodeAnimationFrame(const byte *source, uint32 sourceSize, bool compressed, Common::Array<byte> &dest) const;
void blitTransparentBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) const;
void blitTransparentAnimationFrame(Graphics::Screen &screen, const Common::Array<AbmFrame> &frames,
uint frameIndex, int x, int y) const;
diff --git a/engines/harvester/bitmap.cpp b/engines/harvester/bitmap.cpp
new file mode 100644
index 00000000000..1d1d8df4143
--- /dev/null
+++ b/engines/harvester/bitmap.cpp
@@ -0,0 +1,137 @@
+/* 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/bitmap.h"
+
+#include "common/debug.h"
+#include "common/endian.h"
+#include "harvester/detection.h"
+
+namespace Harvester {
+
+namespace {
+
+static bool decodeAnimationFrame(const byte *source, uint32 sourceSize, bool compressed, Common::Array<byte> &dest) {
+ if (!compressed) {
+ if (sourceSize < dest.size())
+ return false;
+
+ memcpy(dest.data(), source, dest.size());
+ return true;
+ }
+
+ uint32 srcOffset = 0;
+ uint32 dstOffset = 0;
+ while (srcOffset < sourceSize && dstOffset < dest.size()) {
+ const byte control = source[srcOffset++];
+ if ((control & 0x80) == 0) {
+ const uint32 literalCount = MIN<uint32>(control,
+ MIN<uint32>(sourceSize - srcOffset, dest.size() - dstOffset));
+ memcpy(dest.data() + dstOffset, source + srcOffset, literalCount);
+ srcOffset += literalCount;
+ dstOffset += literalCount;
+ } else {
+ if (srcOffset >= sourceSize)
+ return false;
+
+ const uint32 repeatCount = MIN<uint32>(control & 0x7f, dest.size() - dstOffset);
+ memset(dest.data() + dstOffset, source[srcOffset++], repeatCount);
+ dstOffset += repeatCount;
+ }
+ }
+
+ return dstOffset == dest.size();
+}
+
+} // End of anonymous namespace
+
+bool decodeBitmap(const Common::Array<byte> &data, IndexedBitmap &bitmap) {
+ if (data.size() < 12) {
+ debugC(1, kDebugResources, "Harvester: short BM header bytes=%u", data.size());
+ return false;
+ }
+
+ bitmap.width = READ_LE_UINT32(data.data());
+ bitmap.height = READ_LE_UINT32(data.data() + 4);
+ const uint32 pixelCount = bitmap.width * bitmap.height;
+ if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount) {
+ debugC(1, kDebugResources, "Harvester: invalid BM size=%ux%u bytes=%u",
+ bitmap.width, bitmap.height, data.size());
+ return false;
+ }
+
+ bitmap.pixels.resize(pixelCount);
+ memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
+ debugC(3, kDebugResources, "Harvester: decoded BM size=%ux%u pixels=%u",
+ bitmap.width, bitmap.height, pixelCount);
+ return true;
+}
+
+bool decodeAbm(const Common::Array<byte> &data, Common::Array<AbmFrame> &frames) {
+ if (data.size() < 8) {
+ debugC(1, kDebugResources, "Harvester: short ABM header bytes=%u", data.size());
+ return false;
+ }
+
+ const uint32 frameCount = READ_LE_UINT32(data.data());
+ uint32 offset = 8;
+ frames.resize(frameCount);
+
+ for (uint32 i = 0; i < frameCount; ++i) {
+ if (data.size() < offset + 25) {
+ debugC(1, kDebugResources, "Harvester: short ABM frame header frame=%u offset=%u bytes=%u",
+ i, offset, data.size());
+ return false;
+ }
+
+ AbmFrame &frame = frames[i];
+ frame.xOffset = (int32)READ_LE_UINT32(data.data() + offset);
+ frame.yOffset = (int32)READ_LE_UINT32(data.data() + offset + 4);
+ frame.width = READ_LE_UINT32(data.data() + offset + 8);
+ frame.height = READ_LE_UINT32(data.data() + offset + 12);
+ const bool compressed = data[offset + 16] != 0;
+ const uint32 sourceSize = READ_LE_UINT32(data.data() + offset + 17);
+ const uint32 pixelCount = frame.width * frame.height;
+ const uint32 payloadOffset = offset + 25;
+
+ if (frame.width == 0 || frame.height == 0 || data.size() < payloadOffset + sourceSize) {
+ debugC(1, kDebugResources, "Harvester: invalid ABM frame=%u size=%ux%u offset=%u source_bytes=%u bytes=%u",
+ i, frame.width, frame.height, payloadOffset, sourceSize, data.size());
+ return false;
+ }
+
+ frame.pixels.resize(pixelCount);
+ if (!decodeAnimationFrame(data.data() + payloadOffset, sourceSize, compressed, frame.pixels)) {
+ debugC(1, kDebugResources, "Harvester: incomplete ABM pixels frame=%u source_bytes=%u pixels=%u",
+ i, sourceSize, pixelCount);
+ return false;
+ }
+
+ debugC(3, kDebugResources, "Harvester: decoded ABM frame=%u size=%ux%u position=(%d,%d) offset=%u source_bytes=%u compressed=%d",
+ i, frame.width, frame.height, frame.xOffset, frame.yOffset, payloadOffset, sourceSize, compressed);
+ offset = payloadOffset + sourceSize;
+ }
+
+ debugC(2, kDebugResources, "Harvester: decoded ABM frames=%u bytes=%u", frameCount, data.size());
+ return true;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/bitmap.h b/engines/harvester/bitmap.h
new file mode 100644
index 00000000000..9a87c2cbcaa
--- /dev/null
+++ b/engines/harvester/bitmap.h
@@ -0,0 +1,50 @@
+/* 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_BITMAP_H
+#define HARVESTER_BITMAP_H
+
+#include "common/array.h"
+
+namespace Harvester {
+
+struct IndexedBitmap {
+ uint32 width = 0;
+ uint32 height = 0;
+ Common::Array<byte> pixels;
+
+ bool isValid() const {
+ return width != 0 && height != 0 && pixels.size() >= width * height;
+ }
+};
+
+struct AbmFrame : IndexedBitmap {
+ int32 xOffset = 0;
+ int32 yOffset = 0;
+};
+
+// Callers choose when to clear output. Failed decoding can leave partial header or frame data.
+bool decodeBitmap(const Common::Array<byte> &data, IndexedBitmap &bitmap);
+bool decodeAbm(const Common::Array<byte> &data, Common::Array<AbmFrame> &frames);
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_BITMAP_H
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 5267bbea0f5..830861b16c4 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -23,7 +23,6 @@
#include "common/algorithm.h"
#include "common/debug.h"
-#include "common/endian.h"
#include "common/events.h"
#include "common/ptr.h"
#include "common/system.h"
@@ -284,15 +283,7 @@ static bool loadBitmapResource(ResourceManager &resources, const Common::String
return false;
bitmap = IndexedBitmap();
- bitmap.width = READ_LE_UINT32(data.data());
- bitmap.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = bitmap.width * bitmap.height;
- if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount)
- return false;
-
- bitmap.pixels.resize(pixelCount);
- memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
- return true;
+ return decodeBitmap(data, bitmap);
}
static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 955890d4e88..29138acfb87 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -26,7 +26,6 @@
#include "common/algorithm.h"
#include "common/config-manager.h"
#include "common/debug.h"
-#include "common/endian.h"
#include "common/events.h"
#include "common/formats/ini-file.h"
#include "common/memstream.h"
@@ -726,15 +725,7 @@ bool loadBitmapResource(ResourceManager &resources, const Common::String &path,
return false;
bitmap = IndexedBitmap();
- bitmap.width = READ_LE_UINT32(data.data());
- bitmap.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = bitmap.width * bitmap.height;
- if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount)
- return false;
-
- bitmap.pixels.resize(pixelCount);
- memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
- return true;
+ return decodeBitmap(data, bitmap);
}
static void logSceneObjectSelection(const char *decision, const char *source, const ObjectRecord &object,
diff --git a/engines/harvester/inventory.cpp b/engines/harvester/inventory.cpp
index 4c60875122e..81a646b8640 100644
--- a/engines/harvester/inventory.cpp
+++ b/engines/harvester/inventory.cpp
@@ -23,7 +23,6 @@
#include "common/algorithm.h"
#include "common/debug.h"
-#include "common/endian.h"
#include "graphics/blit.h"
#include "graphics/font.h"
#include "graphics/screen.h"
@@ -173,15 +172,7 @@ static bool loadBitmapResource(ResourceManager &resources, const Common::String
return false;
bitmap = IndexedBitmap();
- bitmap.width = READ_LE_UINT32(data.data());
- bitmap.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = bitmap.width * bitmap.height;
- if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount)
- return false;
-
- bitmap.pixels.resize(pixelCount);
- memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
- return true;
+ return decodeBitmap(data, bitmap);
}
static bool resolveInventoryCombatLoadoutId(const Common::String &objectName, int &loadoutId) {
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index a788bad1412..c9485fb58f8 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -23,7 +23,6 @@
#include "common/algorithm.h"
#include "common/config-manager.h"
-#include "common/endian.h"
#include "common/events.h"
#include "common/formats/ini-file.h"
#include "common/memstream.h"
@@ -421,15 +420,7 @@ static bool loadBitmapResource(ResourceManager &resources, const Common::String
if (!resources.loadFile(path, data) || data.size() < 12)
return false;
- bitmap.width = READ_LE_UINT32(data.data());
- bitmap.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = bitmap.width * bitmap.height;
- if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount)
- return false;
-
- bitmap.pixels.resize(pixelCount);
- memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
- return true;
+ return decodeBitmap(data, bitmap);
}
static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 7cbfefba482..456143ea073 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -2,6 +2,7 @@ MODULE := engines/harvester
MODULE_OBJS = \
art.o \
+ bitmap.o \
cft_font.o \
cheats.o \
console.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index c82a14372aa..ba4273598e8 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -22,7 +22,6 @@
#include "harvester/room.h"
#include "common/algorithm.h"
-#include "common/endian.h"
#include "common/events.h"
#include "common/ptr.h"
#include "common/system.h"
@@ -171,15 +170,7 @@ static bool loadBitmapResource(ResourceManager &resources, const Common::String
return false;
bitmap = IndexedBitmap();
- bitmap.width = READ_LE_UINT32(data.data());
- bitmap.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = bitmap.width * bitmap.height;
- if (bitmap.width == 0 || bitmap.height == 0 || data.size() < 12 + pixelCount)
- return false;
-
- bitmap.pixels.resize(pixelCount);
- memcpy(bitmap.pixels.data(), data.data() + 12, pixelCount);
- return true;
+ return decodeBitmap(data, bitmap);
}
static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/runtime_entity.cpp
index 7f5086a0754..509c8031693 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/runtime_entity.cpp
@@ -25,7 +25,6 @@
#include "common/algorithm.h"
#include "common/debug.h"
-#include "common/endian.h"
#include "common/system.h"
#include "graphics/blit.h"
#include "graphics/screen.h"
@@ -106,38 +105,6 @@ static void blitAnimationFrame(Graphics::Screen &screen, const Common::Array<Abm
screen.format.bytesPerPixel, kTransparentPaletteIndex);
}
-static bool decodeAnimationFrame(const byte *source, uint32 sourceSize, bool compressed, Common::Array<byte> &dest) {
- if (!compressed) {
- if (sourceSize < dest.size())
- return false;
-
- memcpy(dest.data(), source, dest.size());
- return true;
- }
-
- uint32 srcOffset = 0;
- uint32 dstOffset = 0;
- while (srcOffset < sourceSize && dstOffset < dest.size()) {
- const byte control = source[srcOffset++];
- if ((control & 0x80) == 0) {
- const uint32 literalCount = MIN<uint32>(control,
- MIN<uint32>(sourceSize - srcOffset, dest.size() - dstOffset));
- memcpy(dest.data() + dstOffset, source + srcOffset, literalCount);
- srcOffset += literalCount;
- dstOffset += literalCount;
- } else {
- if (srcOffset >= sourceSize)
- return false;
-
- const uint32 repeatCount = MIN<uint32>(control & 0x7f, dest.size() - dstOffset);
- memset(dest.data() + dstOffset, source[srcOffset++], repeatCount);
- dstOffset += repeatCount;
- }
- }
-
- return dstOffset == dest.size();
-}
-
} // End of anonymous namespace
uint32 getRuntimeClockTicks() {
@@ -166,17 +133,11 @@ bool Entity::loadBitmapResource(ResourceManager &resources, const Common::String
}
AbmFrame frame;
- frame.width = READ_LE_UINT32(data.data());
- frame.height = READ_LE_UINT32(data.data() + 4);
- const uint32 pixelCount = frame.width * frame.height;
- if (frame.width == 0 || frame.height == 0 || data.size() < 12 + pixelCount) {
+ if (!decodeBitmap(data, frame)) {
warning("Harvester: invalid runtime bitmap '%s'", path.c_str());
return false;
}
- frame.pixels.resize(pixelCount);
- memcpy(frame.pixels.data(), data.data() + 12, pixelCount);
-
_frames.clear();
_frames.push_back(Common::move(frame));
_baseFrames = _frames;
@@ -201,36 +162,9 @@ bool Entity::loadAbmResource(ResourceManager &resources, const Common::String &p
return false;
}
- const uint32 frameCount = READ_LE_UINT32(data.data());
- uint32 offset = 8;
- _frames.resize(frameCount);
-
- for (uint32 i = 0; i < frameCount; ++i) {
- if (data.size() < offset + 25) {
- warning("Harvester: short runtime ABM header in '%s'", path.c_str());
- return false;
- }
-
- AbmFrame &frame = _frames[i];
- frame.xOffset = (int32)READ_LE_UINT32(data.data() + offset);
- frame.yOffset = (int32)READ_LE_UINT32(data.data() + offset + 4);
- frame.width = READ_LE_UINT32(data.data() + offset + 8);
- frame.height = READ_LE_UINT32(data.data() + offset + 12);
- const bool compressed = data[offset + 16] != 0;
- const uint32 sourceSize = READ_LE_UINT32(data.data() + offset + 17);
- const uint32 pixelCount = frame.width * frame.height;
- const uint32 payloadOffset = offset + 25;
-
- if (frame.width == 0 || frame.height == 0 || data.size() < payloadOffset + sourceSize) {
- warning("Harvester: invalid runtime ABM frame %u in '%s'", i, path.c_str());
- return false;
- }
-
- frame.pixels.resize(pixelCount);
- if (!decodeAnimationFrame(data.data() + payloadOffset, sourceSize, compressed, frame.pixels))
- return false;
-
- offset = payloadOffset + sourceSize;
+ if (!decodeAbm(data, _frames)) {
+ warning("Harvester: unable to decode runtime entity animation '%s'", path.c_str());
+ return false;
}
_resourcePath = path;
diff --git a/engines/harvester/runtime_entity.h b/engines/harvester/runtime_entity.h
index 61fdd0f0b39..5a7f0c2186c 100644
--- a/engines/harvester/runtime_entity.h
+++ b/engines/harvester/runtime_entity.h
@@ -25,7 +25,7 @@
#include "common/array.h"
#include "common/rect.h"
#include "common/str.h"
-#include "harvester/art.h"
+#include "harvester/bitmap.h"
namespace Graphics {
class Screen;
Commit: 4f581772ae1683a5eed4650a2fe4d0d212d1ad63
https://github.com/scummvm/scummvm/commit/4f581772ae1683a5eed4650a2fe4d0d212d1ad63
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: Make command execution inputs and outputs explicit
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/script.cpp
engines/harvester/script.h
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index ea889b4ce03..6cf913b48c4 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -1712,18 +1712,10 @@ bool Script::executeRoomEnterCommands(const Common::String &roomName,
if (!room)
return false;
- executeCommandChain(room->onEnterCommand, "room entry command", room->roomName,
- room->roomName, true,
- &result.musicPath, &result.audioCommands, &result.nextRoomName, &result.roomTransition,
- &result.cutscenePath, &result.deathFlicPath, &result.requestMainMenu,
- &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag, &result.continuationTag,
- &result.modalText, &result.lightingCommand, &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const CommandExecutionContext context = {
+ "room entry command", room->roomName, room->roomName, true
+ };
+ executeCommandChain(room->onEnterCommand, context, kCommandOutputsInteraction, result);
return true;
}
@@ -1735,18 +1727,10 @@ bool Script::executeRoomExitCommands(const Common::String &roomName,
if (!room)
return false;
- executeCommandChain(room->onExitCommand, "room exit command", room->roomName,
- room->roomName, false,
- &result.musicPath, &result.audioCommands, nullptr, nullptr, &result.cutscenePath,
- nullptr, nullptr, &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag,
- &result.continuationTag, &result.modalText, &result.lightingCommand,
- &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const CommandExecutionContext context = {
+ "room exit command", room->roomName, room->roomName, false
+ };
+ executeCommandChain(room->onExitCommand, context, kCommandOutputsRoomExit, result);
return true;
}
@@ -1775,18 +1759,10 @@ bool Script::resolveObjectInteraction(const ObjectRecord &object, InteractionRes
if (object.actionTag.empty())
return result.mutatedRuntimeState;
- executeCommandChain(object.actionTag, "interaction command", object.objectName,
- commandRoomName, true,
- &result.musicPath, &result.audioCommands, &result.nextRoomName, &result.roomTransition,
- &result.cutscenePath, &result.deathFlicPath, &result.requestMainMenu,
- &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag, &result.continuationTag,
- &result.modalText, &result.lightingCommand, &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const CommandExecutionContext context = {
+ "interaction command", object.objectName, commandRoomName, true
+ };
+ executeCommandChain(object.actionTag, context, kCommandOutputsInteraction, result);
return !result.nextRoomName.empty() || !result.cutscenePath.empty() ||
!result.deathFlicPath.empty() || result.requestMainMenu || result.requestDemoEnding ||
@@ -1807,18 +1783,10 @@ bool Script::resolveRegionInteraction(const RegionRecord ®ion, InteractionRes
return false;
const Common::String &commandRoomName = roomName.empty() ? region.roomName : roomName;
- executeCommandChain(region.actionTag, "region command", region.regionName,
- commandRoomName, true,
- &result.musicPath, &result.audioCommands, &result.nextRoomName, &result.roomTransition,
- &result.cutscenePath, &result.deathFlicPath, &result.requestMainMenu,
- &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag, &result.continuationTag,
- &result.modalText, &result.lightingCommand, &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const CommandExecutionContext context = {
+ "region command", region.regionName, commandRoomName, true
+ };
+ executeCommandChain(region.actionTag, context, kCommandOutputsInteraction, result);
return !result.nextRoomName.empty() || !result.cutscenePath.empty() ||
!result.deathFlicPath.empty() || result.requestMainMenu || result.requestDemoEnding ||
!result.dialogueNpcName.empty() || !result.musicPath.empty() || !result.audioCommands.empty() ||
@@ -1845,19 +1813,12 @@ bool Script::resolveUseItemInteraction(const Common::String &itemName, const Obj
commandRoomName = target.currentOwnerOrRoom;
}
- executeCommandChain(useItem->actionTag, "useitem command",
- Common::String::format("%s -> %s", itemName.c_str(), target.objectName.c_str()),
- commandRoomName, true,
- &result.musicPath, &result.audioCommands, &result.nextRoomName, &result.roomTransition,
- &result.cutscenePath, &result.deathFlicPath, &result.requestMainMenu,
- &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag, &result.continuationTag,
- &result.modalText, &result.lightingCommand, &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const Common::String contextName = Common::String::format("%s -> %s",
+ itemName.c_str(), target.objectName.c_str());
+ const CommandExecutionContext context = {
+ "useitem command", contextName, commandRoomName, true
+ };
+ executeCommandChain(useItem->actionTag, context, kCommandOutputsInteraction, result);
return true;
}
@@ -1890,17 +1851,10 @@ bool Script::executeActionTag(const Common::String &tag, InteractionResult &resu
if (tag.empty())
return false;
- executeCommandChain(tag, "action tag", tag, roomName, allowTransitions,
- &result.musicPath, &result.audioCommands, &result.nextRoomName, &result.roomTransition,
- &result.cutscenePath, &result.deathFlicPath, &result.requestMainMenu,
- &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag, &result.continuationTag,
- &result.modalText, &result.lightingCommand, &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const CommandExecutionContext context = {
+ "action tag", tag, roomName, allowTransitions
+ };
+ executeCommandChain(tag, context, kCommandOutputsInteraction, result);
return !result.nextRoomName.empty() || !result.cutscenePath.empty() ||
!result.deathFlicPath.empty() || result.requestMainMenu || result.requestDemoEnding ||
@@ -1931,18 +1885,10 @@ bool Script::executeTimerAction(const Common::String &timerName, InteractionResu
}
const Common::String &commandRoomName = roomName.empty() ? timer->arg1 : roomName;
- executeCommandChain(timer->arg2, "timer command", timer->timerName, commandRoomName,
- allowTransitions,
- &result.musicPath, &result.audioCommands, &result.nextRoomName, &result.roomTransition,
- &result.cutscenePath, &result.deathFlicPath, &result.requestMainMenu,
- &result.requestDemoEnding, &result.cdChangeDisc,
- &result.dialogueNpcName, &result.dialogueContinuationTag, &result.continuationTag,
- &result.modalText, &result.lightingCommand, &result.moveEntityToPlayerZName,
- &result.requestPlayerGotoXZ, &result.requestPlayerGotoZ,
- &result.playerGotoX, &result.playerGotoZ,
- &result.requestPlayerDeath, &result.playerDeathDamageType,
- &result.mutatedRuntimeState, &result.visualRuntimeStateChanged,
- &result.previousTimerRecords, &result.requestCloseupExit);
+ const CommandExecutionContext context = {
+ "timer command", timer->timerName, commandRoomName, allowTransitions
+ };
+ executeCommandChain(timer->arg2, context, kCommandOutputsInteraction, result);
return !result.nextRoomName.empty() || !result.cutscenePath.empty() ||
!result.deathFlicPath.empty() || result.requestMainMenu || result.requestDemoEnding ||
@@ -2787,62 +2733,45 @@ bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord
return true;
}
-void Script::executeCommandChain(const Common::String &initialTag, const char *contextLabel,
- const Common::String &contextName, const Common::String &contextRoomName,
- bool allowTransitions, Common::String *musicPath,
- Common::Array<AudioCommand> *audioCommands, Common::String *nextRoomName,
- StartupRoomTransitionKind *roomTransition,
- Common::String *cutscenePath, Common::String *deathFlicPath, bool *requestMainMenu,
- bool *requestDemoEnding,
- int *cdChangeDisc,
- Common::String *dialogueNpcName, Common::String *dialogueContinuationTag,
- Common::String *continuationTag, ResolvedText *modalText,
- StartupLightingCommand *lightingCommand, Common::String *moveEntityToPlayerZName,
- bool *requestPlayerGotoXZ, bool *requestPlayerGotoZ,
- int *playerGotoX, int *playerGotoZ,
- bool *requestPlayerDeath, int *playerDeathDamageType,
- bool *mutatedRuntimeState, bool *visualRuntimeStateChanged,
- Common::Array<TimerRecord> *previousTimerRecords, bool *requestCloseupExit) {
+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 && mutatedRuntimeState)
- *mutatedRuntimeState = true;
+ if (changed)
+ result.mutatedRuntimeState = true;
};
auto noteCurrentRoomVisualMutation = [&](bool changed, bool affectsCurrentRoom) {
noteMutation(changed);
- if (changed && affectsCurrentRoom && visualRuntimeStateChanged)
- *visualRuntimeStateChanged = true;
+ if (changed && affectsCurrentRoom)
+ result.visualRuntimeStateChanged = true;
};
auto noteTimerMutation = [&](const TimerRecord *previousTimer, bool changed) {
- if (!changed || !previousTimerRecords || !previousTimer ||
- previousTimer->timerName.empty()) {
+ if (!changed || !previousTimer || previousTimer->timerName.empty())
return;
- }
- for (const TimerRecord &record : *previousTimerRecords) {
+ for (const TimerRecord &record : result.previousTimerRecords) {
if (record.timerName.equalsIgnoreCase(previousTimer->timerName))
return;
}
- previousTimerRecords->push_back(*previousTimer);
+ result.previousTimerRecords.push_back(*previousTimer);
};
auto belongsToContextRoom = [&](const Common::String &roomName) {
- return !contextRoomName.empty() && roomName.equalsIgnoreCase(contextRoomName);
+ return !context.roomName.empty() && roomName.equalsIgnoreCase(context.roomName);
};
auto hasDeferredInteractionOutputs = [&]() {
- return (nextRoomName && !nextRoomName->empty()) ||
- (cutscenePath && !cutscenePath->empty()) ||
- (deathFlicPath && !deathFlicPath->empty()) ||
- (requestMainMenu && *requestMainMenu) ||
- (requestDemoEnding && *requestDemoEnding) ||
- (dialogueNpcName && !dialogueNpcName->empty()) ||
- (continuationTag && !continuationTag->empty()) ||
- (modalText && !modalText->value.empty()) ||
- (lightingCommand && *lightingCommand != kStartupLightingCommandNone) ||
- (moveEntityToPlayerZName && !moveEntityToPlayerZName->empty()) ||
- (requestCloseupExit && *requestCloseupExit);
+ 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;
@@ -2850,20 +2779,20 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
const CommandRecord *command = findCommandRecord(currentTag);
if (!command) {
debug(1, "Harvester: unresolved %s tag '%s' for '%s'",
- contextLabel, currentTag.c_str(), contextName.c_str());
+ context.label, currentTag.c_str(), context.name.c_str());
break;
}
debugC(1, kDebugScene,
"Harvester: %s '%s' step=%u tag='%s' opcode='%s' args=['%s','%s','%s','%s']",
- contextLabel, contextName.c_str(), step, currentTag.c_str(), command->opcodeName.c_str(),
+ 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(1, kDebugScene, "Harvester: %s '%s' flag '%s' -> %d",
- contextLabel, contextName.c_str(), command->arg1.c_str(), flagValue);
+ context.label, context.name.c_str(), command->arg1.c_str(), flagValue);
currentTag = flagValue ? command->arg2 : command->arg3;
continue;
}
@@ -2894,7 +2823,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
}
debugC(1, kDebugGeneral,
"Harvester: %s '%s' SET_FLAG '%s' %d -> %d existed=%d changed=%d",
- contextLabel, contextName.c_str(), command->arg1.c_str(), oldValue, flagValue,
+ context.label, context.name.c_str(), command->arg1.c_str(), oldValue, flagValue,
existed, changed);
noteMutation(changed);
currentTag = command->arg4;
@@ -2902,23 +2831,14 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
}
if (command->opcodeName.equalsIgnoreCase("SPOOL_MUSIC")) {
- if (musicPath)
- *musicPath = command->arg1;
+ result.musicPath = command->arg1;
currentTag = command->arg4;
continue;
}
- if (audioCommands) {
- if (appendStartupAudioCommand(*command, *audioCommands)) {
- currentTag = command->arg4;
- continue;
- }
- } else {
- Common::Array<AudioCommand> ignoredAudioCommands;
- if (appendStartupAudioCommand(*command, ignoredAudioCommands)) {
- currentTag = command->arg4;
- continue;
- }
+ if (appendStartupAudioCommand(*command, result.audioCommands)) {
+ currentTag = command->arg4;
+ continue;
}
if (command->opcodeName.equalsIgnoreCase("ADD") ||
@@ -2926,7 +2846,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
ObjectRecord *currentObject = findRuntimeObject(command->arg1, command->arg2);
if (!currentObject) {
debug(1, "Harvester: unresolved object for %s '%s' owner='%s' object='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str(), command->arg2.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str(), command->arg2.c_str());
currentTag = command->arg4;
continue;
}
@@ -2958,7 +2878,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
AnimRecord *currentAnimation = findRuntimeAnim(command->arg1);
if (!currentAnimation) {
debug(1, "Harvester: unresolved anim for %s '%s' anim='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
@@ -2991,7 +2911,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
RegionRecord *currentRegion = findRuntimeRegion(command->arg1);
if (!currentRegion) {
debug(1, "Harvester: unresolved region for %s '%s' region='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
@@ -3009,7 +2929,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
NpcRecord *currentNpc = findRuntimeNpc(command->arg1);
if (!currentNpc) {
debug(1, "Harvester: unresolved npc for %s '%s' npc='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
@@ -3027,12 +2947,12 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
if (command->opcodeName.equalsIgnoreCase("SET_MONSTER")) {
MonsterRecord monsterLookup;
- monsterLookup.roomName = contextRoomName;
+ monsterLookup.roomName = context.roomName;
monsterLookup.monsterName = command->arg1;
MonsterRecord *currentMonster = findRuntimeMonster(monsterLookup);
if (!currentMonster) {
debug(1, "Harvester: unresolved monster for %s '%s' monster='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
@@ -3089,22 +3009,16 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
const ExecListRecord *execList = findExecListRecord(command->arg1);
if (!execList) {
debug(1, "Harvester: unresolved exec list for %s '%s' list='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
for (const Common::String &entry : execList->entries) {
- executeCommandChain(entry, "exec list entry", execList->listName,
- contextRoomName, allowTransitions,
- musicPath, audioCommands, nextRoomName, roomTransition, cutscenePath,
- deathFlicPath, requestMainMenu, requestDemoEnding, cdChangeDisc,
- dialogueNpcName, dialogueContinuationTag,
- continuationTag, modalText, lightingCommand, moveEntityToPlayerZName,
- requestPlayerGotoXZ, requestPlayerGotoZ,
- playerGotoX, playerGotoZ, requestPlayerDeath, playerDeathDamageType,
- mutatedRuntimeState, visualRuntimeStateChanged,
- previousTimerRecords, requestCloseupExit);
+ const CommandExecutionContext entryContext = {
+ "exec list entry", execList->listName, context.roomName, context.allowTransitions
+ };
+ executeCommandChain(entry, entryContext, outputPolicy, result);
if (hasDeferredInteractionOutputs())
return;
}
@@ -3113,55 +3027,41 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
}
if (command->opcodeName.equalsIgnoreCase("START_DIALOG")) {
- if (!dialogueNpcName || !dialogueContinuationTag) {
- debug(1, "Harvester: unsupported startup command '%s' for %s '%s' without dialogue context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- return;
- }
if (command->arg1.empty()) {
currentTag = command->arg4;
continue;
}
- *dialogueNpcName = command->arg1;
- *dialogueContinuationTag = command->arg4;
+ result.dialogueNpcName = command->arg1;
+ result.dialogueContinuationTag = command->arg4;
return;
}
if (command->opcodeName.equalsIgnoreCase("GOFLIC")) {
- if (!cutscenePath) {
- debug(1, "Harvester: deferred startup command '%s' for %s '%s' has no cutscene context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- *cutscenePath = command->arg1;
- if (continuationTag)
- *continuationTag = command->arg4;
+ result.cutscenePath = command->arg1;
+ result.continuationTag = command->arg4;
return;
}
if (command->opcodeName.equalsIgnoreCase("GODEATHFLIC")) {
- if (!deathFlicPath || !requestMainMenu) {
+ if (outputPolicy == kCommandOutputsRoomExit) {
debug(1, "Harvester: unsupported startup command '%s' for %s '%s' without menu-exit context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
+ command->opcodeName.c_str(), context.label, context.name.c_str());
return;
}
- if (allowTransitions) {
- *deathFlicPath = command->arg1;
- *requestMainMenu = true;
+ 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(), contextLabel, contextName.c_str());
+ command->opcodeName.c_str(), context.label, context.name.c_str());
}
return;
}
if (command->opcodeName.equalsIgnoreCase("END_DEMO")) {
- if (requestDemoEnding)
- *requestDemoEnding = true;
+ result.requestDemoEnding = true;
return;
}
@@ -3170,7 +3070,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
NpcRecord *currentNpc = findRuntimeNpc(command->arg1);
if (!currentNpc) {
debug(1, "Harvester: unresolved npc for %s '%s' npc='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
@@ -3178,8 +3078,8 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
const int deathDamageType = parseDeathDamageType(command->arg2);
const bool deferLiveNpcDeathTransition =
_deferredLiveNpcDeathTransitionDepth > 0 &&
- !contextRoomName.empty() &&
- currentNpc->roomName.equalsIgnoreCase(contextRoomName) &&
+ !context.roomName.empty() &&
+ currentNpc->roomName.equalsIgnoreCase(context.roomName) &&
currentNpc->active &&
currentNpc->visible &&
!currentNpc->deathOrMonsterfyFlag &&
@@ -3193,21 +3093,13 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
}
if (command->opcodeName.equalsIgnoreCase("SHOW_TEXT")) {
- if (!modalText) {
- debug(1, "Harvester: deferred startup command '%s' for %s '%s' has no modal-text context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- if (!resolveTextRecord(command->arg1, *modalText)) {
+ if (!resolveTextRecord(command->arg1, result.modalText)) {
debug(1, "Harvester: unresolved SHOW_TEXT record for %s '%s' key='%s'",
- contextLabel, contextName.c_str(), command->arg1.c_str());
+ context.label, context.name.c_str(), command->arg1.c_str());
currentTag = command->arg4;
continue;
}
- if (continuationTag)
- *continuationTag = command->arg4;
+ result.continuationTag = command->arg4;
return;
}
@@ -3237,17 +3129,14 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
}
if (command->opcodeName.equalsIgnoreCase("MOVE_BM2PCZ")) {
- if (moveEntityToPlayerZName)
- *moveEntityToPlayerZName = command->arg1;
+ result.moveEntityToPlayerZName = command->arg1;
currentTag = command->arg4;
continue;
}
if (command->opcodeName.equalsIgnoreCase("KILL_PC")) {
- if (requestPlayerDeath)
- *requestPlayerDeath = true;
- if (playerDeathDamageType)
- *playerDeathDamageType = parseDeathDamageType(command->arg1);
+ result.requestPlayerDeath = true;
+ result.playerDeathDamageType = parseDeathDamageType(command->arg1);
noteMutation(setPlayerCurrentHitPoints(0));
currentTag = command->arg4;
continue;
@@ -3266,106 +3155,70 @@ void Script::executeCommandChain(const Common::String &initialTag, const char *c
}
if (command->opcodeName.equalsIgnoreCase("PC_GOTO_XZ")) {
- if (!requestPlayerGotoXZ || !playerGotoX || !playerGotoZ) {
- debug(1, "Harvester: deferred startup command '%s' for %s '%s' has no player-move context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- *requestPlayerGotoXZ = true;
- *playerGotoX = parseAsciiIntOrZero(command->arg1);
- *playerGotoZ = parseAsciiIntOrZero(command->arg2);
+ result.requestPlayerGotoXZ = true;
+ result.playerGotoX = parseAsciiIntOrZero(command->arg1);
+ result.playerGotoZ = parseAsciiIntOrZero(command->arg2);
currentTag = command->arg4;
continue;
}
if (command->opcodeName.equalsIgnoreCase("PC_GOTO_Z")) {
- if (!requestPlayerGotoZ || !playerGotoZ) {
- debug(1, "Harvester: deferred startup command '%s' for %s '%s' has no player-move context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- currentTag = command->arg4;
- continue;
- }
-
- *requestPlayerGotoZ = true;
- *playerGotoZ = parseAsciiIntOrZero(command->arg1);
+ result.requestPlayerGotoZ = true;
+ result.playerGotoZ = parseAsciiIntOrZero(command->arg1);
currentTag = command->arg4;
continue;
}
if (command->opcodeName.equalsIgnoreCase("CHANGE_LIGHTING")) {
- if (!lightingCommand) {
- debug(1, "Harvester: deferred startup command '%s' for %s '%s' has no lighting context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- currentTag = command->arg4;
- continue;
- }
-
if (command->arg1.equalsIgnoreCase("DIM"))
- *lightingCommand = kStartupLightingCommandDim;
+ result.lightingCommand = kStartupLightingCommandDim;
else if (command->arg1.equalsIgnoreCase("NORMAL"))
- *lightingCommand = kStartupLightingCommandNormal;
+ result.lightingCommand = kStartupLightingCommandNormal;
else if (command->arg1.equalsIgnoreCase("NONE"))
- *lightingCommand = kStartupLightingCommandBlack;
+ result.lightingCommand = kStartupLightingCommandBlack;
else if (command->arg1.equalsIgnoreCase("FADE_IN"))
- *lightingCommand = kStartupLightingCommandFadeIn;
+ result.lightingCommand = kStartupLightingCommandFadeIn;
else
debug(1, "Harvester: unsupported CHANGE_LIGHTING mode '%s' for %s '%s', preserving control flow",
- command->arg1.c_str(), contextLabel, contextName.c_str());
- if (continuationTag)
- *continuationTag = command->arg4;
+ command->arg1.c_str(), context.label, context.name.c_str());
+ result.continuationTag = command->arg4;
return;
}
if (command->opcodeName.equalsIgnoreCase("CHANGE_CD")) {
- if (!cdChangeDisc) {
- debug(1, "Harvester: deferred startup command '%s' for %s '%s' has no CD-change context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- currentTag = command->arg4;
- continue;
- }
-
const int requestedDisc = parseAsciiIntOrZero(command->arg1);
if (requestedDisc > 0)
- *cdChangeDisc = requestedDisc;
+ result.cdChangeDisc = requestedDisc;
else
debug(1, "Harvester: invalid CHANGE_CD target '%s' for %s '%s'",
- command->arg1.c_str(), contextLabel, contextName.c_str());
+ 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 (allowTransitions && nextRoomName) {
- *nextRoomName = command->arg1;
- if (roomTransition) {
- *roomTransition = command->opcodeName.equalsIgnoreCase("CHANGE_ROOM")
- ? kStartupRoomTransitionChangeRoom
- : kStartupRoomTransitionCloseup;
- }
+ 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(), contextLabel, contextName.c_str());
+ command->opcodeName.c_str(), context.label, context.name.c_str());
}
currentTag = command->arg4;
continue;
}
if (command->opcodeName.equalsIgnoreCase("EXIT_CLOSEUP")) {
- if (requestCloseupExit) {
- *requestCloseupExit = true;
- } else {
- debug(1, "Harvester: unsupported startup command '%s' for %s '%s' without closeup-exit context",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
- }
+ result.requestCloseupExit = true;
currentTag = command->arg4;
continue;
}
debug(1, "Harvester: unsupported startup command '%s' for %s '%s', continuing",
- command->opcodeName.c_str(), contextLabel, contextName.c_str());
+ command->opcodeName.c_str(), context.label, context.name.c_str());
currentTag = command->arg4;
}
}
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index 1d721ec50dd..c3872803fae 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -434,6 +434,19 @@ public:
bool setRuntimeTimerEnabled(const Common::String &timerName, bool enabled);
private:
+ struct CommandExecutionContext {
+ const char *label;
+ const Common::String &name;
+ const Common::String &roomName;
+ bool allowTransitions;
+ };
+
+ enum CommandOutputPolicy {
+ kCommandOutputsInteraction,
+ // Suppress destination rooms, death movies and main-menu requests.
+ kCommandOutputsRoomExit
+ };
+
bool loadConfig(ResourceManager &resources);
void decode();
void parseTownRecords(ResourceManager &resources);
@@ -459,22 +472,8 @@ private:
const TimerRecord *findRuntimeTimer(const Common::String &timerName) const;
bool buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord *entrance,
ResourceManager &resources, RoomSetupState &state) const;
- void executeCommandChain(const Common::String &initialTag, const char *contextLabel,
- const Common::String &contextName, const Common::String &contextRoomName,
- bool allowTransitions, Common::String *musicPath,
- Common::Array<AudioCommand> *audioCommands, Common::String *nextRoomName,
- StartupRoomTransitionKind *roomTransition,
- Common::String *cutscenePath, Common::String *deathFlicPath, bool *requestMainMenu,
- bool *requestDemoEnding,
- int *cdChangeDisc,
- Common::String *dialogueNpcName, Common::String *dialogueContinuationTag,
- Common::String *continuationTag, ResolvedText *modalText,
- StartupLightingCommand *lightingCommand, Common::String *moveEntityToPlayerZName,
- bool *requestPlayerGotoXZ, bool *requestPlayerGotoZ,
- int *playerGotoX, int *playerGotoZ,
- bool *requestPlayerDeath, int *playerDeathDamageType,
- bool *mutatedRuntimeState, bool *visualRuntimeStateChanged,
- Common::Array<TimerRecord> *previousTimerRecords, bool *requestCloseupExit);
+ void executeCommandChain(const Common::String &initialTag, const CommandExecutionContext &context,
+ CommandOutputPolicy outputPolicy, InteractionResult &result);
bool probePickupBlockingCommandChain(const Common::String &initialTag,
const Common::String &contextName, InteractionResult &result, uint recursionDepth) const;
bool hasActionableCommandChain(const Common::String &initialTag) const;
Commit: 3be8724e8bcdba6ca2d9e25b837551727c307466
https://github.com/scummvm/scummvm/commit/3be8724e8bcdba6ca2d9e25b837551727c307466
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: extract RoomSession for managing room state and player interactions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/room_session.cpp
A engines/harvester/room_session.h
engines/harvester/module.mk
engines/harvester/room.cpp
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 456143ea073..af951392791 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -19,6 +19,7 @@ MODULE_OBJS = \
resources.o \
room_combat.o \
room_interaction.o \
+ room_session.o \
room.o \
runtime_entity.o \
saveload.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index ba4273598e8..29f261aca4b 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -44,6 +44,7 @@
#include "harvester/player.h"
#include "harvester/room_combat.h"
#include "harvester/room_interaction.h"
+#include "harvester/room_session.h"
#include "harvester/room_support.h"
namespace Harvester {
@@ -450,7 +451,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
flow.executeStartupAudioCommands(scene.state.audioCommands);
if (!scene.state.musicPath.empty())
(void)_engine.playMusic(scene.state.musicPath);
- RoomPlayerState playerState;
+ RoomSession session;
+ debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
+ RoomPlayerState &playerState = session.getPlayerState();
playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
playerState.centerX = scene.state.playerSpawnX;
playerState.bottomY = scene.state.playerSpawnY;
@@ -483,12 +486,12 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ResolvedText combatLoadoutStatusText;
uint32 combatLoadoutStatusHideTick = 0;
bool closeInventoryAfterCombatLoadoutStatus = false;
- bool moveLeft = false;
- bool moveRight = false;
- bool moveUp = false;
- bool moveDown = false;
+ bool &moveLeft = session.getMovementInput().moveLeft;
+ bool &moveRight = session.getMovementInput().moveRight;
+ bool &moveUp = session.getMovementInput().moveUp;
+ bool &moveDown = session.getMovementInput().moveDown;
bool attackModifierHeld = false;
- Common::String pendingRegionName;
+ Common::String &pendingRegionName = session.getPendingRegionName();
Common::String pendingRoomChange;
bool pendingRoomChangeIsRoomName = false;
bool pendingRoomChangeUsesSavedRoomState = false;
@@ -496,7 +499,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const bool canExitCloseupToParent = flow._roomLoopDepth > 1;
uint32 lastLeftButtonReleaseTick = 0;
uint32 nextKeyboardAttackAllowedTick = 0;
- RoomIdleAnimationState idleState;
+ RoomIdleAnimationState &idleState = session.getIdleState();
bool needsRedraw = true;
Common::String carriedRoomItemName;
Common::String carriedRoomItemLabel;
@@ -542,40 +545,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
Entity *entity = entityManager->detachSceneEntityByName(entityName);
delete entity;
};
- auto resetIdleState = [&]() {
- idleState = RoomIdleAnimationState();
- idleState.activityTick = Player::getRuntimeClockTicks();
- idleState.resetTick = idleState.activityTick;
- Player::updateIdleTrigger(idleState);
- };
- auto notePlayerActivity = [&]() {
- idleState.activityTick = Player::getRuntimeClockTicks();
- Player::updateIdleTrigger(idleState);
- };
- auto isPlayerCombatLocked = [&]() {
- return playerState.attackActive || playerState.hitActive || playerState.deathActive;
- };
- auto cancelPlayerAttackAnimation = [&]() {
- 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 Player::setIdleAnimation(playerState, resumeFacing);
- };
auto syncCurrentRoomRuntimeState = [&]() {
Script *script = _engine.getScript();
if (!script)
@@ -1361,21 +1331,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
captureCurrentSaveState();
return _inventory.refresh();
};
- resetIdleState();
- auto stopPlayerRegionInteraction = [&]() {
- moveLeft = false;
- moveRight = false;
- moveUp = false;
- moveDown = false;
- pendingRegionName.clear();
- (void)cancelPlayerAttackAnimation();
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- if (playerState.entity && playerState.facing >= 0)
- (void)Player::setIdleAnimation(playerState, playerState.facing);
- Player::syncAnimationPlacement(playerState);
- };
+ session.resetIdleState();
+
auto refreshCurrentScene = [&](bool preservePlayerPlacement) {
Common::Array<AudioCommand> entryAudioCommands = scene.state.audioCommands;
RoomSetupState refreshedState;
@@ -1438,7 +1395,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monsterCombatStates.clear();
monsterCombatStates.resize(scene.state.roomMonsters.size());
pendingRegionName.clear();
- resetIdleState();
+ session.resetIdleState();
flow.resetCursorAnimationSequence();
captureCurrentSaveState();
return _inventory.refresh();
@@ -1501,7 +1458,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
moveUp = false;
moveDown = false;
pendingRegionName.clear();
- (void)cancelPlayerAttackAnimation();
+ (void)session.cancelPlayerAttackAnimation();
playerState.hasMoveTarget = false;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
@@ -1732,7 +1689,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
hasScreenRect ? screenRect.left : 0, hasScreenRect ? screenRect.top : 0,
hasScreenRect ? screenRect.right : 0, hasScreenRect ? screenRect.bottom : 0,
(double)depthScale);
- resetIdleState();
+ session.resetIdleState();
captureCurrentSaveState();
};
auto runModalShowText = [&](const ResolvedText &modalText) -> Common::Error {
@@ -1856,12 +1813,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
typedef decltype(applyLightingCommand) ApplyLightingCommandFn;
typedef decltype(applyPlayerGotoXZ) ApplyPlayerGotoXZFn;
typedef decltype(runModalShowText) RunModalShowTextFn;
- typedef decltype(resetIdleState) ResetIdleStateFn;
- typedef decltype(stopPlayerRegionInteraction) StopPlayerRegionInteractionFn;
typedef decltype(startPlayerDefeatSequence) StartPlayerDefeatSequenceFn;
struct RoomInteractionCallbacksImpl : RoomInteractionCallbacks {
RoomInteractionCallbacksImpl(
+ RoomSession &session_,
RefreshCurrentSceneFn &refreshCurrentSceneFn_,
ApplyCurrentRoomRuntimeMutationsInPlaceFn &applyCurrentRoomRuntimeMutationsInPlaceFn_,
SyncGlobalTimerEntitiesFn &syncGlobalTimerEntitiesFn_,
@@ -1871,10 +1827,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ApplyLightingCommandFn &applyLightingCommandFn_,
ApplyPlayerGotoXZFn &applyPlayerGotoXZFn_,
RunModalShowTextFn &runModalShowTextFn_,
- ResetIdleStateFn &resetIdleStateFn_,
- StopPlayerRegionInteractionFn &stopPlayerRegionInteractionFn_,
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn_)
- : refreshCurrentSceneFn(refreshCurrentSceneFn_),
+ : session(session_), refreshCurrentSceneFn(refreshCurrentSceneFn_),
applyCurrentRoomRuntimeMutationsInPlaceFn(applyCurrentRoomRuntimeMutationsInPlaceFn_),
syncGlobalTimerEntitiesFn(syncGlobalTimerEntitiesFn_),
captureDialogueBackdropFn(captureDialogueBackdropFn_),
@@ -1883,8 +1837,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
applyLightingCommandFn(applyLightingCommandFn_),
applyPlayerGotoXZFn(applyPlayerGotoXZFn_),
runModalShowTextFn(runModalShowTextFn_),
- resetIdleStateFn(resetIdleStateFn_),
- stopPlayerRegionInteractionFn(stopPlayerRegionInteractionFn_),
startPlayerDefeatSequenceFn(startPlayerDefeatSequenceFn_) {
}
@@ -1925,11 +1877,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
void resetIdleState() override {
- resetIdleStateFn();
+ session.resetIdleState();
}
void stopPlayerRegionInteraction() override {
- stopPlayerRegionInteractionFn();
+ session.stopPlayerRegionInteraction();
}
void startPlayerDefeatSequence(const char *reason, const Common::String &sourceName,
@@ -1937,6 +1889,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
startPlayerDefeatSequenceFn(reason, sourceName, damageType);
}
+ RoomSession &session;
RefreshCurrentSceneFn &refreshCurrentSceneFn;
ApplyCurrentRoomRuntimeMutationsInPlaceFn &applyCurrentRoomRuntimeMutationsInPlaceFn;
SyncGlobalTimerEntitiesFn &syncGlobalTimerEntitiesFn;
@@ -1946,15 +1899,14 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ApplyLightingCommandFn &applyLightingCommandFn;
ApplyPlayerGotoXZFn &applyPlayerGotoXZFn;
RunModalShowTextFn &runModalShowTextFn;
- ResetIdleStateFn &resetIdleStateFn;
- StopPlayerRegionInteractionFn &stopPlayerRegionInteractionFn;
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn;
};
RoomInteractionCallbacksImpl interactionCallbacks(
+ session,
refreshCurrentScene, applyCurrentRoomRuntimeMutationsInPlace,
syncGlobalTimerEntities, captureDialogueBackdrop, showCdChangePrompt,
runRoomExitCommands, applyLightingCommand, applyPlayerGotoXZ, runModalShowText,
- resetIdleState, stopPlayerRegionInteraction, startPlayerDefeatSequence);
+ startPlayerDefeatSequence);
RoomInteractionProcessor interactionProcessor(
_engine, flow, scene, playerState, pendingRegionName, pendingRoomChange,
pendingRoomChangeIsRoomName, pendingRoomChangeUsesSavedRoomState,
@@ -2008,7 +1960,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
flow.resetCursorAnimationSequence();
- resetIdleState();
+ session.resetIdleState();
}
ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
_engine.getScript());
@@ -2272,7 +2224,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!flow.hasPendingMainMenuReturn() &&
_engine.getScript() &&
_engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
- stopPlayerRegionInteraction();
+ session.stopPlayerRegionInteraction();
startPlayerDefeatSequence("combat_interaction", Common::String(), 1);
}
@@ -3059,12 +3011,12 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.entity->setVisible(true);
if (playerHitPointsAfter > 0 &&
Player::startHitAnimation(_engine, playerState, combatState.attackFirstFrame)) {
- notePlayerActivity();
+ session.notePlayerActivity();
}
}
if (changed && playerHitPointsAfter <= 0) {
playerAlive = false;
- stopPlayerRegionInteraction();
+ session.stopPlayerRegionInteraction();
startPlayerDefeatSequence(
"combat_monster_hit", monster.monsterName, monster.damageType);
return Common::kNoError;
@@ -3468,7 +3420,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return Common::kNoError;
}
- stopPlayerRegionInteraction();
+ session.stopPlayerRegionInteraction();
return runRegionInteraction(*region);
};
auto tryActivatePassiveRegion = [&]() -> Common::Error {
@@ -3483,7 +3435,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!doesPlayerFacingMatchRegion(playerState.facing, region))
continue;
- stopPlayerRegionInteraction();
+ session.stopPlayerRegionInteraction();
return runRegionInteraction(region);
}
@@ -3578,7 +3530,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const bool activeCarry = inventorySelectionActive || roomCarryActive;
const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_mousePos);
const bool suppressHover = showingInspectText || idleState.active || idleState.exiting ||
- isPlayerCombatLocked() ||
+ session.isPlayerCombatLocked() ||
(_inventory.isOpen() && (inventoryPanelContainsMouse || !inventorySelectionActive));
RoomHoverState hoverState = suppressHover
? RoomHoverState()
@@ -3638,7 +3590,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (Entity *cursor = entityManager ? entityManager->getCursorEntity() : nullptr) {
cursor->setAnimationSequence(
(showingInspectText || idleState.active || idleState.exiting ||
- isPlayerCombatLocked() ||
+ session.isPlayerCombatLocked() ||
_inventory.isOpen() || activeCarry)
? 7
: hoverState.cursorSequence);
@@ -3704,13 +3656,13 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
break;
case Common::EVENT_RBUTTONDOWN: {
- notePlayerActivity();
+ session.notePlayerActivity();
if (idleState.active || idleState.exiting) {
if (Player::requestIdleAnimationExit(scene.state, playerState, idleState))
needsRedraw = true;
break;
}
- if (isPlayerCombatLocked())
+ if (session.isPlayerCombatLocked())
break;
pendingRegionName.clear();
if (_inventory.isOpen()) {
@@ -3824,7 +3776,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
break;
}
Script *script = _engine.getScript();
- if (!showingInspectText && !isPlayerCombatLocked() && script && playerState.entity &&
+ if (!showingInspectText && !session.isPlayerCombatLocked() && script && playerState.entity &&
script->getPlayerCurrentHitPoints() > 0 && !script->isPlayerControlPaused()) {
playerState.hasMoveTarget = false;
playerState.turnActive = false;
@@ -3863,7 +3815,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const bool isFastExitClick =
lastLeftButtonReleaseTick != 0 &&
now - lastLeftButtonReleaseTick < kRoomExitFastClickWindowTicks;
- notePlayerActivity();
+ session.notePlayerActivity();
if (idleState.active || idleState.exiting) {
if (Player::requestIdleAnimationExit(scene.state, playerState, idleState))
needsRedraw = true;
@@ -3879,7 +3831,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
break;
}
- if (isPlayerCombatLocked())
+ if (session.isPlayerCombatLocked())
break;
const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
@@ -4030,7 +3982,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (isFastExitClick &&
hoverState.region->startEnabled &&
roomAllowsImmediateExitClick(scene.state.roomName)) {
- stopPlayerRegionInteraction();
+ session.stopPlayerRegionInteraction();
Common::Error interactionError = runRegionInteraction(*hoverState.region);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
@@ -4164,7 +4116,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
break;
}
- notePlayerActivity();
+ session.notePlayerActivity();
if (event.kbd.hasFlags(Common::KBD_CTRL))
attackModifierHeld = true;
if (idleState.active || idleState.exiting) {
@@ -4233,7 +4185,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
break;
}
}
- if (isPlayerCombatLocked())
+ if (session.isPlayerCombatLocked())
break;
if (event.kbd.keycode == Common::KEYCODE_LCTRL ||
@@ -4258,7 +4210,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
break;
} else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- if (cancelPlayerAttackAnimation())
+ if (session.cancelPlayerAttackAnimation())
needsRedraw = true;
if (!stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
@@ -4284,7 +4236,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (menuError.getCode() != Common::kNoError)
return menuError;
flow.resetCursorAnimationSequence();
- resetIdleState();
+ session.resetIdleState();
needsRedraw = true;
break;
}
@@ -4330,7 +4282,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.hasMoveTarget || playerState.turnActive ||
playerState.attackActive || playerState.hitActive)) {
attackModifierHeld = false;
- stopPlayerRegionInteraction();
+ session.stopPlayerRegionInteraction();
}
// Native run_harvester_main_loop checks player/region overlap before
@@ -4338,7 +4290,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
// reaches a narrow exit: the actor update may otherwise satisfy the X
// target and replace that frame with a narrower idle frame first.
Common::Error pendingRegionError =
- isPlayerCombatLocked()
+ session.isPlayerCombatLocked()
? Common::kNoError
: tryActivatePendingRegion();
if (pendingRegionError.getCode() != Common::kNoError)
@@ -4350,7 +4302,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return Common::kReadingFailed;
break;
}
- if (!isPlayerCombatLocked()) {
+ if (!session.isPlayerCombatLocked()) {
pendingRegionError = tryActivatePassiveRegion();
if (pendingRegionError.getCode() != Common::kNoError)
return pendingRegionError;
@@ -4399,7 +4351,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (Player::startKeyboardAttackAnimation(
scene.state, playerState, moveLeft, moveRight, moveUp, moveDown)) {
nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
- notePlayerActivity();
+ session.notePlayerActivity();
debugC(1, kDebugCombat,
"Harvester: combat player keyboard attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' input=(L=%d R=%d U=%d D=%d) facing=%d",
playerState.combatLoadout,
@@ -4429,12 +4381,12 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
? pendingRegion->desiredFacing : -1;
if (Player::stepKeyboardMovement(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
playerState, moveLeft, moveRight, moveUp, moveDown)) {
- notePlayerActivity();
+ session.notePlayerActivity();
needsRedraw = true;
} else if (Player::stepMoveTarget(
_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
playerState, regionFacing)) {
- notePlayerActivity();
+ session.notePlayerActivity();
needsRedraw = true;
}
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
new file mode 100644
index 00000000000..8cfd5d24297
--- /dev/null
+++ b/engines/harvester/room_session.cpp
@@ -0,0 +1,81 @@
+/* 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/room_session.h"
+
+#include "harvester/player.h"
+
+namespace Harvester {
+
+void RoomSession::resetIdleState() {
+ _idleState = RoomIdleAnimationState();
+ _idleState.activityTick = Player::getRuntimeClockTicks();
+ _idleState.resetTick = _idleState.activityTick;
+ Player::updateIdleTrigger(_idleState);
+}
+
+void RoomSession::notePlayerActivity() {
+ _idleState.activityTick = Player::getRuntimeClockTicks();
+ Player::updateIdleTrigger(_idleState);
+}
+
+bool RoomSession::isPlayerCombatLocked() const {
+ return _playerState.attackActive || _playerState.hitActive || _playerState.deathActive;
+}
+
+bool RoomSession::cancelPlayerAttackAnimation() {
+ 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 Player::setIdleAnimation(_playerState, resumeFacing);
+}
+
+void RoomSession::stopPlayerRegionInteraction() {
+ _movementInput.moveLeft = false;
+ _movementInput.moveRight = false;
+ _movementInput.moveUp = false;
+ _movementInput.moveDown = false;
+ _pendingRegionName.clear();
+ (void)cancelPlayerAttackAnimation();
+ _playerState.hasMoveTarget = false;
+ _playerState.turnActive = false;
+ _playerState.turnTargetFacing = -1;
+ if (_playerState.entity && _playerState.facing >= 0)
+ (void)Player::setIdleAnimation(_playerState, _playerState.facing);
+ Player::syncAnimationPlacement(_playerState);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
new file mode 100644
index 00000000000..223bc6bc4ea
--- /dev/null
+++ b/engines/harvester/room_session.h
@@ -0,0 +1,60 @@
+/* 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_ROOM_SESSION_H
+#define HARVESTER_ROOM_SESSION_H
+
+#include "harvester/room_support.h"
+
+namespace Harvester {
+
+struct RoomMovementInputState {
+ bool moveLeft = false;
+ bool moveRight = false;
+ bool moveUp = false;
+ bool moveDown = false;
+};
+
+// Scoped to one runRoomLoop iteration, including time spent in nested closeups.
+// Entity pointers remain owned by EntityManager.
+class RoomSession {
+public:
+ RoomPlayerState &getPlayerState() { return _playerState; }
+ RoomIdleAnimationState &getIdleState() { return _idleState; }
+ RoomMovementInputState &getMovementInput() { return _movementInput; }
+ Common::String &getPendingRegionName() { return _pendingRegionName; }
+
+ void resetIdleState();
+ void notePlayerActivity();
+ bool isPlayerCombatLocked() const;
+ bool cancelPlayerAttackAnimation();
+ void stopPlayerRegionInteraction();
+
+private:
+ RoomPlayerState _playerState;
+ RoomIdleAnimationState _idleState;
+ RoomMovementInputState _movementInput;
+ Common::String _pendingRegionName;
+};
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_ROOM_SESSION_H
Commit: febc916db51dbda79024fc46a4600cf036a8f6ba
https://github.com/scummvm/scummvm/commit/febc916db51dbda79024fc46a4600cf036a8f6ba
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: refactor to manage room state and synchronize runtime entities
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 29f261aca4b..fd98818a96d 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -451,7 +451,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
flow.executeStartupAudioCommands(scene.state.audioCommands);
if (!scene.state.musicPath.empty())
(void)_engine.playMusic(scene.state.musicPath);
- RoomSession session;
+ RoomSession session(_engine, scene, entityManager);
debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
RoomPlayerState &playerState = session.getPlayerState();
playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
@@ -546,58 +546,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
delete entity;
};
- auto syncCurrentRoomRuntimeState = [&]() {
- Script *script = _engine.getScript();
- if (!script)
- return;
-
- // Region enable state is a persistent script mutation; keep the runtime record in lockstep
- // with the currently materialized scene so one-shot regions do not resurrect on revisit.
- for (const RegionRecord &baseRegion : script->getRegions()) {
- if (!baseRegion.roomName.equalsIgnoreCase(scene.state.roomName))
- continue;
-
- const bool enabled =
- findSceneRegionByName(scene.sceneRegions, baseRegion.regionName) != nullptr;
- (void)script->setRuntimeRegionEnabled(baseRegion.regionName, enabled);
- }
-
- if (entityManager) {
- for (AnimRecord &anim : scene.state.roomAnimations) {
- Entity *entity = entityManager->findSceneEntityByName(anim.animName);
- if (!entity)
- continue;
- (void)script->syncRuntimeAnimState(anim.animName,
- entity->isAnimationEnabled(), anim.visible, entity->getCurrentFrame());
- }
- }
- for (const MonsterRecord &monster : scene.state.roomMonsters)
- (void)script->syncRuntimeMonsterRecord(monster);
- if (!entityManager)
- return;
-
- for (const TimerRecord &baseTimer : script->getTimers()) {
- Entity *entity = entityManager->findSceneEntityByName(baseTimer.timerName);
- if (!entity || entity->getClassId() != kRuntimeEntityClassTimer)
- continue;
-
- const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(baseTimer.timerName);
- TimerRecord timer = runtimeTimer ? *runtimeTimer : baseTimer;
- timer.currentValue = entity->getTimerCurrentValue();
- timer.enabled = entity->isTimerEnabled();
- timer.looping = entity->isTimerLooping();
- timer.global = entity->isTimerGlobal();
- (void)script->syncRuntimeTimerRecord(timer);
- }
- };
- auto captureCurrentSaveState = [&]() {
- Player::syncAnimationPlacement(playerState);
- syncCurrentRoomRuntimeState();
- const int facing = playerState.facing >= 0 ? playerState.facing : scene.state.playerFacing;
- _engine.captureCurrentSaveRoomState(scene.state.entranceName, scene.state.roomName,
- playerState.centerX, playerState.bottomY, (int)playerState.z, facing,
- scene.state.discNumber, _engine.getMusicPath());
- };
auto getSceneObjectBounds = [&](const ObjectRecord &object) {
if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
@@ -1022,61 +970,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return removedAny;
};
- auto isTimerMaterializedInCurrentRoom = [&](const Common::String &timerName) {
- for (const TimerRecord &timer : scene.state.roomTimers) {
- if (timer.timerName.equalsIgnoreCase(timerName))
- return true;
- }
-
- return false;
- };
- auto syncGlobalTimerEntities =
- [&](const Common::Array<TimerRecord> &previousTimerRecords,
- bool includeCurrentRoomTimers = false) {
- Script *script = _engine.getScript();
- if (!script || !entityManager || previousTimerRecords.empty())
- return;
-
- for (const TimerRecord &previousTimer : previousTimerRecords) {
- const TimerRecord *runtimeTimer =
- script->findRuntimeTimerRecord(previousTimer.timerName);
- if (!runtimeTimer || !runtimeTimer->global ||
- (!includeCurrentRoomTimers &&
- isTimerMaterializedInCurrentRoom(runtimeTimer->timerName))) {
- continue;
- }
-
- Entity *entity = entityManager->findSceneEntityByName(runtimeTimer->timerName);
- if (entity && entity->getClassId() != kRuntimeEntityClassTimer)
- continue;
- if (!entity) {
- (void)script->syncRuntimeTimerRecord(previousTimer);
- debugC(1, kDebugRoom,
- "Harvester: global timer mutation ignored current_room='%s' timer='%s' owner_room='%s' reason='missing live timer entity'",
- scene.state.roomName.c_str(), previousTimer.timerName.c_str(),
- previousTimer.arg1.c_str());
- continue;
- }
-
- if (entity->getTimerInitialValue() == runtimeTimer->initialValue &&
- entity->isTimerEnabled() == runtimeTimer->enabled &&
- entity->isTimerLooping() == runtimeTimer->looping &&
- entity->isTimerGlobal() == runtimeTimer->global) {
- continue;
- }
-
- entityManager->configureSceneTimerEntity(*entity,
- runtimeTimer->initialValue, runtimeTimer->currentValue,
- runtimeTimer->enabled, runtimeTimer->looping, runtimeTimer->global);
- debugC(1, kDebugRoom,
- "Harvester: global timer synced current_room='%s' timer='%s' owner_room='%s' current=%d initial=%d enabled=%d loop=%d",
- scene.state.roomName.c_str(), runtimeTimer->timerName.c_str(),
- runtimeTimer->arg1.c_str(), runtimeTimer->currentValue,
- runtimeTimer->initialValue, runtimeTimer->enabled,
- runtimeTimer->looping);
- }
- };
auto applyCurrentRoomRuntimeMutationsInPlace = [&](bool preservePaletteState) {
Script *script = _engine.getScript();
ResourceManager *resources = _engine.getResources();
@@ -1328,7 +1222,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
memcpy(scene.palette, updatedScene.palette, sizeof(scene.palette));
scene.targetPaletteBrightness = updatedScene.targetPaletteBrightness;
}
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
return _inventory.refresh();
};
session.resetIdleState();
@@ -1397,7 +1291,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
pendingRegionName.clear();
session.resetIdleState();
flow.resetCursorAnimationSequence();
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
return _inventory.refresh();
};
auto syncAnimatedRoomActorPlacement = [&]() {
@@ -1582,7 +1476,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
(void)_engine.playMusic(exitInteraction.musicPath);
flow.executeStartupAudioCommands(exitInteraction.audioCommands);
if (exitInteraction.mutatedRuntimeState)
- syncGlobalTimerEntities(exitInteraction.previousTimerRecords, true);
+ session.syncGlobalTimerEntities(exitInteraction.previousTimerRecords, true);
if (!exitInteraction.cutscenePath.empty()) {
FstPlayer fstPlayer(_engine);
@@ -1690,7 +1584,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
hasScreenRect ? screenRect.right : 0, hasScreenRect ? screenRect.bottom : 0,
(double)depthScale);
session.resetIdleState();
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
};
auto runModalShowText = [&](const ResolvedText &modalText) -> Common::Error {
Graphics::Screen *activeScreen = getActiveScreen();
@@ -1806,7 +1700,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
};
typedef decltype(refreshCurrentScene) RefreshCurrentSceneFn;
typedef decltype(applyCurrentRoomRuntimeMutationsInPlace) ApplyCurrentRoomRuntimeMutationsInPlaceFn;
- typedef decltype(syncGlobalTimerEntities) SyncGlobalTimerEntitiesFn;
typedef decltype(captureDialogueBackdrop) CaptureDialogueBackdropFn;
typedef decltype(showCdChangePrompt) ShowCdChangePromptFn;
typedef decltype(runRoomExitCommands) RunRoomExitCommandsFn;
@@ -1820,7 +1713,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomSession &session_,
RefreshCurrentSceneFn &refreshCurrentSceneFn_,
ApplyCurrentRoomRuntimeMutationsInPlaceFn &applyCurrentRoomRuntimeMutationsInPlaceFn_,
- SyncGlobalTimerEntitiesFn &syncGlobalTimerEntitiesFn_,
CaptureDialogueBackdropFn &captureDialogueBackdropFn_,
ShowCdChangePromptFn &showCdChangePromptFn_,
RunRoomExitCommandsFn &runRoomExitCommandsFn_,
@@ -1830,7 +1722,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn_)
: session(session_), refreshCurrentSceneFn(refreshCurrentSceneFn_),
applyCurrentRoomRuntimeMutationsInPlaceFn(applyCurrentRoomRuntimeMutationsInPlaceFn_),
- syncGlobalTimerEntitiesFn(syncGlobalTimerEntitiesFn_),
captureDialogueBackdropFn(captureDialogueBackdropFn_),
showCdChangePromptFn(showCdChangePromptFn_),
runRoomExitCommandsFn(runRoomExitCommandsFn_),
@@ -1849,7 +1740,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) override {
- syncGlobalTimerEntitiesFn(previousTimerRecords);
+ session.syncGlobalTimerEntities(previousTimerRecords);
}
bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop) override {
@@ -1892,7 +1783,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomSession &session;
RefreshCurrentSceneFn &refreshCurrentSceneFn;
ApplyCurrentRoomRuntimeMutationsInPlaceFn &applyCurrentRoomRuntimeMutationsInPlaceFn;
- SyncGlobalTimerEntitiesFn &syncGlobalTimerEntitiesFn;
CaptureDialogueBackdropFn &captureDialogueBackdropFn;
ShowCdChangePromptFn &showCdChangePromptFn;
RunRoomExitCommandsFn &runRoomExitCommandsFn;
@@ -1904,7 +1794,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomInteractionCallbacksImpl interactionCallbacks(
session,
refreshCurrentScene, applyCurrentRoomRuntimeMutationsInPlace,
- syncGlobalTimerEntities, captureDialogueBackdrop, showCdChangePrompt,
+ captureDialogueBackdrop, showCdChangePrompt,
runRoomExitCommands, applyLightingCommand, applyPlayerGotoXZ, runModalShowText,
startPlayerDefeatSequence);
RoomInteractionProcessor interactionProcessor(
@@ -1921,7 +1811,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
// Native room_setup applies entry-time state changes before the first visible room frame.
if (roomEntryInteraction.mutatedRuntimeState) {
- syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
+ session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
if (!applyCurrentRoomRuntimeMutationsInPlace(false) &&
!refreshCurrentScene(true)) {
return Common::kReadingFailed;
@@ -3444,7 +3334,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!_inventory.refresh())
return Common::kReadingFailed;
Graphics::FrameLimiter limiter(g_system, 60);
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
if (shouldRunStartupRoomProbe())
logStartupRoomProbe(_engine, scene, currentRoomTarget, _mousePos, flow._menuTextConfig);
@@ -3503,7 +3393,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
needsRedraw = true;
}
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
if (_inventory.isOpen() && _inventory.refreshIfRuntimeStateChanged())
needsRedraw = true;
if (!combatLoadoutStatusText.value.empty() &&
@@ -3697,7 +3587,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
queueCombatLoadoutStatusMessage(statusMessage);
closeInventoryAfterCombatLoadoutStatus = true;
}
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
if (playerState.entity) {
(void)Player::syncCombatLoadoutVisual(_engine, scene.state, playerState,
_engine.getScript()->getPlayerCombatLoadout());
@@ -4222,7 +4112,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.hasMoveTarget = false;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
- captureCurrentSaveState();
+ session.captureCurrentSaveState();
Graphics::Screen *activeScreen = getActiveScreen();
if (!activeScreen)
return Common::kNoError;
@@ -4425,7 +4315,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (entityManager) {
Common::Array<Common::String> expiredTimerNames;
if (entityManager->takeExpiredTimerNames(expiredTimerNames)) {
- syncCurrentRoomRuntimeState();
+ session.syncCurrentRoomRuntimeState();
for (const Common::String &timerName : expiredTimerNames) {
InteractionResult timerInteraction;
if (!_engine.getScript()->executeTimerAction(
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index 8cfd5d24297..61ffb17367e 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -21,10 +21,17 @@
#include "harvester/room_session.h"
+#include "harvester/detection.h"
+#include "harvester/harvester.h"
#include "harvester/player.h"
+#include "harvester/runtime_entity.h"
namespace Harvester {
+RoomSession::RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager)
+ : _engine(engine), _scene(scene), _entityManager(entityManager) {
+}
+
void RoomSession::resetIdleState() {
_idleState = RoomIdleAnimationState();
_idleState.activityTick = Player::getRuntimeClockTicks();
@@ -78,4 +85,115 @@ void RoomSession::stopPlayerRegionInteraction() {
Player::syncAnimationPlacement(_playerState);
}
+void RoomSession::syncCurrentRoomRuntimeState() {
+ Script *script = _engine.getScript();
+ if (!script)
+ return;
+
+ debugC(3, kDebugRoom, "Harvester: syncing room runtime state room='%s' entities=%d",
+ _scene.state.roomName.c_str(), _entityManager != nullptr);
+
+ for (const RegionRecord &baseRegion : script->getRegions()) {
+ if (!baseRegion.roomName.equalsIgnoreCase(_scene.state.roomName))
+ continue;
+
+ const bool enabled =
+ findSceneRegionByName(_scene.sceneRegions, baseRegion.regionName) != nullptr;
+ (void)script->setRuntimeRegionEnabled(baseRegion.regionName, enabled);
+ }
+
+ if (_entityManager) {
+ for (AnimRecord &anim : _scene.state.roomAnimations) {
+ Entity *entity = _entityManager->findSceneEntityByName(anim.animName);
+ if (!entity)
+ continue;
+ (void)script->syncRuntimeAnimState(anim.animName,
+ entity->isAnimationEnabled(), anim.visible, entity->getCurrentFrame());
+ }
+ }
+ for (const MonsterRecord &monster : _scene.state.roomMonsters)
+ (void)script->syncRuntimeMonsterRecord(monster);
+ if (!_entityManager)
+ return;
+
+ for (const TimerRecord &baseTimer : script->getTimers()) {
+ Entity *entity = _entityManager->findSceneEntityByName(baseTimer.timerName);
+ if (!entity || entity->getClassId() != kRuntimeEntityClassTimer)
+ continue;
+
+ const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(baseTimer.timerName);
+ TimerRecord timer = runtimeTimer ? *runtimeTimer : baseTimer;
+ timer.currentValue = entity->getTimerCurrentValue();
+ timer.enabled = entity->isTimerEnabled();
+ timer.looping = entity->isTimerLooping();
+ timer.global = entity->isTimerGlobal();
+ (void)script->syncRuntimeTimerRecord(timer);
+ }
+}
+
+void RoomSession::captureCurrentSaveState() {
+ Player::syncAnimationPlacement(_playerState);
+ syncCurrentRoomRuntimeState();
+ const int facing = _playerState.facing >= 0 ? _playerState.facing : _scene.state.playerFacing;
+ _engine.captureCurrentSaveRoomState(_scene.state.entranceName, _scene.state.roomName,
+ _playerState.centerX, _playerState.bottomY, (int)_playerState.z, facing,
+ _scene.state.discNumber, _engine.getMusicPath());
+}
+
+bool RoomSession::isTimerMaterializedInCurrentRoom(const Common::String &timerName) const {
+ for (const TimerRecord &timer : _scene.state.roomTimers) {
+ if (timer.timerName.equalsIgnoreCase(timerName))
+ return true;
+ }
+
+ return false;
+}
+
+void RoomSession::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
+ bool includeCurrentRoomTimers) {
+ Script *script = _engine.getScript();
+ if (!script || !_entityManager || previousTimerRecords.empty())
+ return;
+
+ for (const TimerRecord &previousTimer : previousTimerRecords) {
+ const TimerRecord *runtimeTimer =
+ script->findRuntimeTimerRecord(previousTimer.timerName);
+ if (!runtimeTimer || !runtimeTimer->global ||
+ (!includeCurrentRoomTimers &&
+ isTimerMaterializedInCurrentRoom(runtimeTimer->timerName))) {
+ continue;
+ }
+
+ Entity *entity = _entityManager->findSceneEntityByName(runtimeTimer->timerName);
+ if (entity && entity->getClassId() != kRuntimeEntityClassTimer)
+ continue;
+
+ if (!entity) {
+ (void)script->syncRuntimeTimerRecord(previousTimer);
+ debugC(1, kDebugRoom,
+ "Harvester: global timer mutation ignored current_room='%s' timer='%s' owner_room='%s' reason='missing live timer entity'",
+ _scene.state.roomName.c_str(), previousTimer.timerName.c_str(),
+ previousTimer.arg1.c_str());
+ continue;
+ }
+
+ if (entity->getTimerInitialValue() == runtimeTimer->initialValue &&
+ entity->isTimerEnabled() == runtimeTimer->enabled &&
+ entity->isTimerLooping() == runtimeTimer->looping &&
+ entity->isTimerGlobal() == runtimeTimer->global) {
+ continue;
+ }
+
+ _entityManager->configureSceneTimerEntity(*entity,
+ runtimeTimer->initialValue, runtimeTimer->currentValue,
+ runtimeTimer->enabled, runtimeTimer->looping, runtimeTimer->global);
+ debugC(1, kDebugRoom,
+ "Harvester: global timer synced current_room='%s' timer='%s' owner_room='%s' current=%d initial=%d enabled=%d loop=%d",
+ _scene.state.roomName.c_str(), runtimeTimer->timerName.c_str(),
+ runtimeTimer->arg1.c_str(), runtimeTimer->currentValue,
+ runtimeTimer->initialValue, runtimeTimer->enabled,
+ runtimeTimer->looping);
+ }
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 223bc6bc4ea..5dc334ae051 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -26,6 +26,8 @@
namespace Harvester {
+class EntityManager;
+
struct RoomMovementInputState {
bool moveLeft = false;
bool moveRight = false;
@@ -37,6 +39,8 @@ struct RoomMovementInputState {
// Entity pointers remain owned by EntityManager.
class RoomSession {
public:
+ RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager);
+
RoomPlayerState &getPlayerState() { return _playerState; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
RoomMovementInputState &getMovementInput() { return _movementInput; }
@@ -47,8 +51,17 @@ public:
bool isPlayerCombatLocked() const;
bool cancelPlayerAttackAnimation();
void stopPlayerRegionInteraction();
+ void syncCurrentRoomRuntimeState();
+ void captureCurrentSaveState();
+ void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
+ bool includeCurrentRoomTimers = false);
private:
+ bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
+
+ HarvesterEngine &_engine;
+ RoomSceneResources &_scene;
+ EntityManager *_entityManager;
RoomPlayerState _playerState;
RoomIdleAnimationState _idleState;
RoomMovementInputState _movementInput;
Commit: 8ee387d338442748f365d8f66b4721f7f4a0d4d6
https://github.com/scummvm/scummvm/commit/8ee387d338442748f365d8f66b4721f7f4a0d4d6
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: refactor RoomSession to manage scene entity lifecycle and interactions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index fd98818a96d..76e9bcb9a2d 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -538,111 +538,12 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (ownerOrRoom)
sceneObject->currentOwnerOrRoom = *ownerOrRoom;
};
- auto removeSceneEntityByName = [&](const Common::String &entityName) {
- if (!entityManager)
- return;
-
- Entity *entity = entityManager->detachSceneEntityByName(entityName);
- delete entity;
- };
- auto getSceneObjectBounds = [&](const ObjectRecord &object) {
- if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
- return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
- return Common::Rect();
- };
- auto resolveSceneObjectSpritePathLocal = [&](const ObjectRecord &object) {
- const bool atInitialPlacement = object.currentX == object.initialX &&
- object.currentY == object.initialY &&
- object.currentOwnerOrRoom.equalsIgnoreCase(object.initialOwnerOrRoom);
- if (!object.altSpritePath.empty() &&
- (!atInitialPlacement || object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName)))
- return object.altSpritePath;
- return object.spritePath;
- };
- auto isInteractiveSceneHotspot = [&](const ObjectRecord &object) {
- if (object.operatable || !object.actionTag.empty())
- return true;
-
- Script *script = _engine.getScript();
- if (!script)
- return false;
- if (script->isPickupObject(object))
- return true;
-
- ResolvedText inspectText;
- return script->resolveObjectInspectText(object, inspectText);
- };
- auto resolveSceneObjectClassLocal = [&](const ObjectRecord &object,
- const Entity *entity) {
- if (entity) {
- return ((!scene.state.backgroundObjectName.empty() &&
- object.objectName.equalsIgnoreCase(scene.state.backgroundObjectName)) ||
- (object.initialX == 0 && object.initialY == 0 &&
- entity->getBoundsWidth() == 640 && entity->getBoundsHeight() == 480))
- ? kRuntimeEntityClassBackground
- : kRuntimeEntityClassObject;
- }
-
- return isInteractiveSceneHotspot(object)
- ? kRuntimeEntityClassRectHotspot
- : kRuntimeEntityClassDisabledHotspot;
- };
auto sameRect = [](const Common::Rect &lhs, const Common::Rect &rhs) {
return lhs.left == rhs.left && lhs.top == rhs.top &&
lhs.right == rhs.right && lhs.bottom == rhs.bottom;
};
- auto spawnSceneObjectEntityFromRecord = [&](const ObjectRecord &object) {
- if (!entityManager)
- return;
- const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
- const Common::Rect hotspotBounds = getSceneObjectBounds(object);
- Entity *entity = nullptr;
- if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
- entity = entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
- Common::Point(object.currentX, object.currentY), (float)object.currentZ);
- } else if (!hotspotBounds.isEmpty()) {
- entity = entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds,
- (float)object.currentZ);
- }
- if (!entity)
- return;
-
- entity->setClassId(resolveSceneObjectClassLocal(
- object, entity->hasFrames() ? entity : nullptr));
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity->setZExtent((float)object.zExtent);
- };
- auto getSceneRegionBounds = [](const RegionRecord ®ion) {
- if (region.right > region.left && region.bottom > region.top)
- return Common::Rect(region.left, region.top, region.right + 1, region.bottom + 1);
- return Common::Rect();
- };
- auto spawnSceneRegionEntityFromRecord = [&](const RegionRecord ®ion) {
- if (!entityManager)
- return;
-
- const Common::Rect bounds = getSceneRegionBounds(region);
- if (bounds.isEmpty())
- return;
-
- Entity *entity = entityManager->spawnSceneHotspotEntity(
- region.regionName, bounds, (float)region.minZ);
- if (!entity)
- return;
-
- entity->setClassId(kRuntimeEntityClassRectHotspot19);
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
- if (!region.cursorEnabled)
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
- debugC(1, kDebugRoom,
- "Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
- region.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
- bounds.left, bounds.top, bounds.right, bounds.bottom,
- region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
- };
auto findRoomAnimByNameConst = [](const Common::Array<AnimRecord> &anims,
const Common::String &animName) -> const AnimRecord * {
for (const AnimRecord &anim : anims) {
@@ -667,12 +568,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
return nullptr;
};
- auto shouldSpawnRoomNpcEntity = [](const NpcRecord &npc) {
- return npc.visible && !npc.deathOrMonsterfyFlag;
- };
- auto shouldSpawnRoomMonsterEntity = [](const MonsterRecord &monster) {
- return monster.visible;
- };
+
auto sameNpcEntityState = [&](const NpcRecord &lhs, const NpcRecord &rhs) {
return lhs.posX == rhs.posX &&
lhs.posY == rhs.posY &&
@@ -728,8 +624,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
- resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(resolveSceneObjectSpritePathLocal(rhs)) &&
- sameRect(getSceneObjectBounds(lhs), getSceneObjectBounds(rhs));
+ session.resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(session.resolveSceneObjectSpritePathLocal(rhs)) &&
+ sameRect(session.getSceneObjectBounds(lhs), session.getSceneObjectBounds(rhs));
};
auto sameSceneRegionState = [&](const RegionRecord &lhs, const RegionRecord &rhs) {
return lhs.left == rhs.left &&
@@ -743,79 +639,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
lhs.desiredFacing == rhs.desiredFacing &&
lhs.actionTag.equalsIgnoreCase(rhs.actionTag);
};
- auto applyRoomNpcPlacement = [&](Entity &entity, const NpcRecord &npc) {
- int width = 0;
- int height = 0;
- int xOffset = 0;
- int yOffset = 0;
- if (!entity.getCurrentFrameMetrics(width, height, xOffset, yOffset))
- return false;
-
- const float renderZ =
- (float)npc.posZ - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
- entity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity.setPosition(npc.posX - xOffset - width / 2, npc.posY - height - yOffset, renderZ);
- return true;
- };
- auto spawnSceneNpcEntityFromRecord = [&](const NpcRecord &npc) -> Entity * {
- const bool shouldSpawnNpc = shouldSpawnRoomNpcEntity(npc);
- const bool preservedCorpse = !npc.active &&
- npc.runtimeSpawned &&
- npc.deathDamageType != 0 &&
- npc.runtimeState >= 0;
- if (!entityManager || !shouldSpawnNpc)
- return nullptr;
-
- Entity *entity = entityManager->spawnSceneActorEntity(
- npc.npcName, npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
- if (!entity)
- return nullptr;
- entity->setClassId(kRuntimeEntityClassNpc);
- entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(preservedCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
- entity->setVisible(true);
- if (!applyRoomNpcPlacement(*entity, npc)) {
- removeSceneEntityByName(npc.npcName);
- return nullptr;
- }
- if (preservedCorpse) {
- const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
- entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
- entity->setAnimationRate(0);
- entity->setCurrentFrame(corpseFrame);
- entity->setAnimationEnabled(false);
- } else {
- entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
- entity->setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
- }
- entityManager->reinsertSceneEntity(entity);
- return entity;
- };
- auto spawnSceneMonsterEntityFromRecord = [&](const RoomSetupState &roomState,
- const MonsterRecord &monster) -> Entity * {
- if (!entityManager || !shouldSpawnRoomMonsterEntity(monster))
- return nullptr;
-
- Entity *entity = entityManager->spawnSceneActorEntity(
- monster.monsterName, monster.modelPath,
- Common::Point(monster.posX, monster.posY), (float)monster.posZ,
- Monster::resolveFacingFrame(monster.facing));
- if (!entity)
- return nullptr;
-
- entity->setClassId(kRuntimeEntityClassMonster);
- entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
- entity->setVisible(monster.visible);
- Monster::applyAnimation(*entity, monster);
- if (!applyRoomActorPlacement(roomState, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
- removeSceneEntityByName(monster.monsterName);
- return nullptr;
- }
- entityManager->reinsertSceneEntity(entity);
- return entity;
- };
auto resolveHitEffectAnchor = [&](const Entity &targetEntity,
Common::Point &anchorPoint, float &renderZ) {
const Common::Rect targetRect = targetEntity.getScreenRect();
@@ -943,7 +767,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
!effectEntity->isAnimationEnabled() &&
effectEntity->getCurrentFrame() == effectEntity->getLastFrame();
if (finished && hitEffectStates[i].finished) {
- removeSceneEntityByName(effectName);
+ session.removeSceneEntityByName(effectName);
hitEffectStates.remove_at(i);
removedAny = true;
continue;
@@ -1008,7 +832,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
for (const ObjectRecord &object : scene.sceneObjects) {
if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
- removeSceneEntityByName(object.objectName);
+ session.removeSceneEntityByName(object.objectName);
}
for (const ObjectRecord &object : updatedScene.sceneObjects) {
const ObjectRecord *previous = findSceneObjectByName(scene.sceneObjects, object.objectName);
@@ -1016,13 +840,13 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!changed)
continue;
- removeSceneEntityByName(object.objectName);
- spawnSceneObjectEntityFromRecord(object);
+ session.removeSceneEntityByName(object.objectName);
+ session.spawnSceneObjectEntityFromRecord(object);
}
for (const RegionRecord ®ion : scene.sceneRegions) {
if (!findSceneRegionByName(updatedScene.sceneRegions, region.regionName))
- removeSceneEntityByName(region.regionName);
+ session.removeSceneEntityByName(region.regionName);
}
for (const RegionRecord ®ion : updatedScene.sceneRegions) {
const RegionRecord *previous =
@@ -1031,8 +855,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!changed)
continue;
- removeSceneEntityByName(region.regionName);
- spawnSceneRegionEntityFromRecord(region);
+ session.removeSceneEntityByName(region.regionName);
+ session.spawnSceneRegionEntityFromRecord(region);
}
for (const TimerRecord &timer : scene.state.roomTimers) {
@@ -1044,7 +868,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
}
if (!found)
- removeSceneEntityByName(timer.timerName);
+ session.removeSceneEntityByName(timer.timerName);
}
for (const TimerRecord &timer : updatedState.roomTimers) {
const TimerRecord *previous = nullptr;
@@ -1063,7 +887,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!changed)
continue;
- removeSceneEntityByName(timer.timerName);
+ session.removeSceneEntityByName(timer.timerName);
if (entityManager) {
(void)entityManager->spawnSceneTimerEntity(
timer.timerName, timer.initialValue, timer.currentValue,
@@ -1084,7 +908,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
? entityManager->findSceneEntityByName(anim.animName)
: nullptr;
if (!isDisplayed) {
- removeSceneEntityByName(anim.animName);
+ session.removeSceneEntityByName(anim.animName);
continue;
}
@@ -1098,7 +922,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
previous->pingPong != anim.pingPong ||
previous->backward != anim.backward;
if (needsRespawn) {
- removeSceneEntityByName(anim.animName);
+ session.removeSceneEntityByName(anim.animName);
if (entityManager) {
entity = entityManager->spawnSceneAnimationEntity(
anim.animName, anim.resourcePath, Common::Point(anim.x, anim.y),
@@ -1125,7 +949,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
for (const AnimRecord &anim : scene.state.roomAnimations) {
if (!findRoomAnimByNameConst(updatedState.roomAnimations, anim.animName))
- removeSceneEntityByName(anim.animName);
+ session.removeSceneEntityByName(anim.animName);
}
for (const NpcRecord &npc : scene.state.roomNpcs) {
@@ -1138,7 +962,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
entityManager->findSceneEntityByName(npc.npcName);
const bool keepEntity =
updatedNpc &&
- shouldSpawnRoomNpcEntity(*updatedNpc) &&
+ session.shouldSpawnRoomNpcEntity(*updatedNpc) &&
(sameNpcEntityState(npc, *updatedNpc) ||
isQueuedLiveNpcDeathMutation(npc, *updatedNpc));
if (keepCurrentRoomDeathEntity) {
@@ -1148,10 +972,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
continue;
}
if (!keepEntity)
- removeSceneEntityByName(npc.npcName);
+ session.removeSceneEntityByName(npc.npcName);
}
for (const NpcRecord &npc : updatedState.roomNpcs) {
- if (!shouldSpawnRoomNpcEntity(npc))
+ if (!session.shouldSpawnRoomNpcEntity(npc))
continue;
Entity *entity = entityManager
@@ -1170,8 +994,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!needsRespawn)
continue;
- removeSceneEntityByName(npc.npcName);
- if (!spawnSceneNpcEntityFromRecord(npc))
+ session.removeSceneEntityByName(npc.npcName);
+ if (!session.spawnSceneNpcEntityFromRecord(npc))
return false;
}
@@ -1180,13 +1004,13 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
findRoomMonsterByNameConst(updatedState.roomMonsters, monster.monsterName);
const bool keepEntity =
updatedMonster &&
- shouldSpawnRoomMonsterEntity(*updatedMonster) &&
+ session.shouldSpawnRoomMonsterEntity(*updatedMonster) &&
sameMonsterEntityState(monster, *updatedMonster);
if (!keepEntity)
- removeSceneEntityByName(monster.monsterName);
+ session.removeSceneEntityByName(monster.monsterName);
}
for (const MonsterRecord &monster : updatedState.roomMonsters) {
- if (!shouldSpawnRoomMonsterEntity(monster))
+ if (!session.shouldSpawnRoomMonsterEntity(monster))
continue;
Entity *entity = entityManager
@@ -1199,8 +1023,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!needsRespawn)
continue;
- removeSceneEntityByName(monster.monsterName);
- if (!spawnSceneMonsterEntityFromRecord(updatedState, monster))
+ session.removeSceneEntityByName(monster.monsterName);
+ if (!session.spawnSceneMonsterEntityFromRecord(updatedState, monster))
return false;
}
@@ -2167,7 +1991,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
npcEntity->setHitTestMode(kRuntimeEntityHitTestNone);
npcEntity->setVisible(true);
} else {
- removeSceneEntityByName(npc.npcName);
+ session.removeSceneEntityByName(npc.npcName);
}
npc.active = false;
npc.visible = false;
@@ -3068,7 +2892,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
script->addRuntimeObjectToInventory(object.objectName);
const Common::String inventoryOwner(kInventoryOwnerName);
hideSceneObject(object.objectName, &inventoryOwner);
- removeSceneEntityByName(object.objectName);
+ session.removeSceneEntityByName(object.objectName);
bool didTransition = false;
if (shouldDispatchPickupActionOnDirectInventoryTransfer(object)) {
@@ -3114,7 +2938,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
script->setRuntimeObjectVisible(object.currentOwnerOrRoom, object.objectName, false);
hideSceneObject(object.objectName, nullptr);
- removeSceneEntityByName(object.objectName);
+ session.removeSceneEntityByName(object.objectName);
bool didTransition = false;
if (shouldDispatchPickupActionOnCarryStart(object)) {
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index 61ffb17367e..68726a168cb 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -23,11 +23,15 @@
#include "harvester/detection.h"
#include "harvester/harvester.h"
+#include "harvester/monster.h"
#include "harvester/player.h"
+#include "harvester/room_combat.h"
#include "harvester/runtime_entity.h"
namespace Harvester {
+static const char *const kInventoryOwnerName = "INVENTORY";
+
RoomSession::RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager)
: _engine(engine), _scene(scene), _entityManager(entityManager) {
}
@@ -196,4 +200,196 @@ void RoomSession::syncGlobalTimerEntities(const Common::Array<TimerRecord> &prev
}
}
+void RoomSession::removeSceneEntityByName(const Common::String &entityName) {
+ if (!_entityManager)
+ return;
+
+ Entity *entity = _entityManager->detachSceneEntityByName(entityName);
+ delete entity;
+}
+
+Common::Rect RoomSession::getSceneObjectBounds(const ObjectRecord &object) {
+ if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
+ return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
+ return Common::Rect();
+}
+
+Common::String RoomSession::resolveSceneObjectSpritePathLocal(const ObjectRecord &object) {
+ const bool atInitialPlacement = object.currentX == object.initialX &&
+ object.currentY == object.initialY &&
+ object.currentOwnerOrRoom.equalsIgnoreCase(object.initialOwnerOrRoom);
+ if (!object.altSpritePath.empty() &&
+ (!atInitialPlacement || object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName)))
+ return object.altSpritePath;
+ return object.spritePath;
+}
+
+bool RoomSession::isInteractiveSceneHotspot(const ObjectRecord &object) const {
+ if (object.operatable || !object.actionTag.empty())
+ return true;
+
+ Script *script = _engine.getScript();
+ if (!script)
+ return false;
+ if (script->isPickupObject(object))
+ return true;
+
+ ResolvedText inspectText;
+ return script->resolveObjectInspectText(object, inspectText);
+}
+
+RuntimeEntityClass RoomSession::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
+ if (entity) {
+ return ((!_scene.state.backgroundObjectName.empty() &&
+ object.objectName.equalsIgnoreCase(_scene.state.backgroundObjectName)) ||
+ (object.initialX == 0 && object.initialY == 0 &&
+ entity->getBoundsWidth() == 640 && entity->getBoundsHeight() == 480))
+ ? kRuntimeEntityClassBackground
+ : kRuntimeEntityClassObject;
+ }
+
+ return isInteractiveSceneHotspot(object)
+ ? kRuntimeEntityClassRectHotspot
+ : kRuntimeEntityClassDisabledHotspot;
+}
+
+void RoomSession::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
+ if (!_entityManager)
+ return;
+
+ const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
+ const Common::Rect hotspotBounds = getSceneObjectBounds(object);
+ Entity *entity = nullptr;
+ if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
+ entity = _entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
+ Common::Point(object.currentX, object.currentY), (float)object.currentZ);
+ } else if (!hotspotBounds.isEmpty()) {
+ entity = _entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds,
+ (float)object.currentZ);
+ }
+ if (!entity)
+ return;
+
+ entity->setClassId(resolveSceneObjectClassLocal(
+ object, entity->hasFrames() ? entity : nullptr));
+ entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setZExtent((float)object.zExtent);
+ debugC(3, kDebugRoom, "Harvester: scene object spawned object='%s' sprite='%s' class=0x%x",
+ object.objectName.c_str(), spritePath.c_str(), entity->getClassId());
+}
+
+Common::Rect RoomSession::getSceneRegionBounds(const RegionRecord ®ion) {
+ if (region.right > region.left && region.bottom > region.top)
+ return Common::Rect(region.left, region.top, region.right + 1, region.bottom + 1);
+ return Common::Rect();
+}
+
+void RoomSession::spawnSceneRegionEntityFromRecord(const RegionRecord ®ion) {
+ if (!_entityManager)
+ return;
+
+ const Common::Rect bounds = getSceneRegionBounds(region);
+ if (bounds.isEmpty())
+ return;
+
+ Entity *entity = _entityManager->spawnSceneHotspotEntity(
+ region.regionName, bounds, (float)region.minZ);
+ if (!entity)
+ return;
+
+ entity->setClassId(kRuntimeEntityClassRectHotspot19);
+ entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
+ if (!region.cursorEnabled)
+ entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ debugC(1, kDebugRoom,
+ "Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
+ region.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
+ bounds.left, bounds.top, bounds.right, bounds.bottom,
+ region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
+}
+
+bool RoomSession::shouldSpawnRoomNpcEntity(const NpcRecord &npc) {
+ return npc.visible && !npc.deathOrMonsterfyFlag;
+}
+
+bool RoomSession::shouldSpawnRoomMonsterEntity(const MonsterRecord &monster) {
+ return monster.visible;
+}
+
+bool RoomSession::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc) {
+ int width = 0;
+ int height = 0;
+ int xOffset = 0;
+ int yOffset = 0;
+ if (!entity.getCurrentFrameMetrics(width, height, xOffset, yOffset))
+ return false;
+
+ const float renderZ =
+ (float)npc.posZ - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
+ entity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity.setPosition(npc.posX - xOffset - width / 2, npc.posY - height - yOffset, renderZ);
+ return true;
+}
+
+Entity *RoomSession::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
+ const bool shouldSpawnNpc = shouldSpawnRoomNpcEntity(npc);
+ const bool preservedCorpse = !npc.active &&
+ npc.runtimeSpawned &&
+ npc.deathDamageType != 0 &&
+ npc.runtimeState >= 0;
+ if (!_entityManager || !shouldSpawnNpc)
+ return nullptr;
+
+ Entity *entity = _entityManager->spawnSceneActorEntity(
+ npc.npcName, npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
+ if (!entity)
+ return nullptr;
+
+ entity->setClassId(kRuntimeEntityClassNpc);
+ entity->setZExtent(kNativeNpcMonsterZExtent);
+ entity->setHitTestMode(preservedCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+ entity->setVisible(true);
+ if (!applyRoomNpcPlacement(*entity, npc)) {
+ removeSceneEntityByName(npc.npcName);
+ return nullptr;
+ }
+ if (preservedCorpse) {
+ const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
+ entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
+ entity->setAnimationRate(0);
+ entity->setCurrentFrame(corpseFrame);
+ entity->setAnimationEnabled(false);
+ } else {
+ entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
+ entity->setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
+ }
+ _entityManager->reinsertSceneEntity(entity);
+ return entity;
+}
+
+Entity *RoomSession::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
+ if (!_entityManager || !shouldSpawnRoomMonsterEntity(monster))
+ return nullptr;
+
+ Entity *entity = _entityManager->spawnSceneActorEntity(
+ monster.monsterName, monster.modelPath,
+ Common::Point(monster.posX, monster.posY), (float)monster.posZ,
+ Monster::resolveFacingFrame(monster.facing));
+ if (!entity)
+ return nullptr;
+
+ entity->setClassId(kRuntimeEntityClassMonster);
+ entity->setZExtent(kNativeNpcMonsterZExtent);
+ entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setVisible(monster.visible);
+ Monster::applyAnimation(*entity, monster);
+ if (!applyRoomActorPlacement(roomState, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
+ removeSceneEntityByName(monster.monsterName);
+ return nullptr;
+ }
+ _entityManager->reinsertSceneEntity(entity);
+ return entity;
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 5dc334ae051..93598e92643 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -23,6 +23,7 @@
#define HARVESTER_ROOM_SESSION_H
#include "harvester/room_support.h"
+#include "harvester/runtime_entity.h"
namespace Harvester {
@@ -56,7 +57,22 @@ public:
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers = false);
+ void removeSceneEntityByName(const Common::String &entityName);
+ static Common::Rect getSceneObjectBounds(const ObjectRecord &object);
+ static Common::String resolveSceneObjectSpritePathLocal(const ObjectRecord &object);
+ void spawnSceneObjectEntityFromRecord(const ObjectRecord &object);
+ void spawnSceneRegionEntityFromRecord(const RegionRecord ®ion);
+ static bool shouldSpawnRoomNpcEntity(const NpcRecord &npc);
+ static bool shouldSpawnRoomMonsterEntity(const MonsterRecord &monster);
+ Entity *spawnSceneNpcEntityFromRecord(const NpcRecord &npc);
+ Entity *spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster);
+
private:
+ bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
+ RuntimeEntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
+ static Common::Rect getSceneRegionBounds(const RegionRecord ®ion);
+ static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
+
bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
HarvesterEngine &_engine;
Commit: 2be5442bcc2ee00a6be295c385ca964d828d9384
https://github.com/scummvm/scummvm/commit/2be5442bcc2ee00a6be295c385ca964d828d9384
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: move inventory management and runtime mutations to RoomSession
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 76e9bcb9a2d..3566a43038d 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -451,7 +451,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
flow.executeStartupAudioCommands(scene.state.audioCommands);
if (!scene.state.musicPath.empty())
(void)_engine.playMusic(scene.state.musicPath);
- RoomSession session(_engine, scene, entityManager);
+ RoomSession session(_engine, scene, entityManager, _inventory);
debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
RoomPlayerState &playerState = session.getPlayerState();
playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
@@ -463,7 +463,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
- Common::Array<RoomMonsterCombatState> monsterCombatStates;
+ Common::Array<RoomMonsterCombatState> &monsterCombatStates = session.getMonsterCombatStates();
monsterCombatStates.resize(scene.state.roomMonsters.size());
Common::Array<RoomNpcCombatState> npcCombatStates;
npcCombatStates.resize(scene.state.roomNpcs.size());
@@ -539,107 +539,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
sceneObject->currentOwnerOrRoom = *ownerOrRoom;
};
- auto sameRect = [](const Common::Rect &lhs, const Common::Rect &rhs) {
- return lhs.left == rhs.left && lhs.top == rhs.top &&
- lhs.right == rhs.right && lhs.bottom == rhs.bottom;
- };
-
- auto findRoomAnimByNameConst = [](const Common::Array<AnimRecord> &anims,
- const Common::String &animName) -> const AnimRecord * {
- for (const AnimRecord &anim : anims) {
- if (anim.animName.equalsIgnoreCase(animName))
- return &anim;
- }
- return nullptr;
- };
- auto findRoomNpcByNameConst = [](const Common::Array<NpcRecord> &npcs,
- const Common::String &npcName) -> const NpcRecord * {
- for (const NpcRecord &npc : npcs) {
- if (npc.npcName.equalsIgnoreCase(npcName))
- return &npc;
- }
- return nullptr;
- };
- auto findRoomMonsterByNameConst = [](const Common::Array<MonsterRecord> &monsters,
- const Common::String &monsterName) -> const MonsterRecord * {
- for (const MonsterRecord &monster : monsters) {
- if (monster.monsterName.equalsIgnoreCase(monsterName))
- return &monster;
- }
- return nullptr;
- };
-
- auto sameNpcEntityState = [&](const NpcRecord &lhs, const NpcRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.frameDelay == rhs.frameDelay &&
- lhs.active == rhs.active &&
- lhs.visible == rhs.visible &&
- lhs.runtimeSpawned == rhs.runtimeSpawned &&
- lhs.runtimeState == rhs.runtimeState &&
- lhs.deathDamageType == rhs.deathDamageType &&
- lhs.deathOrMonsterfyFlag == rhs.deathOrMonsterfyFlag &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
- };
- auto isQueuedLiveNpcDeathMutation = [&](const NpcRecord &lhs, const NpcRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.frameDelay == rhs.frameDelay &&
- lhs.active &&
- rhs.active &&
- lhs.visible &&
- rhs.visible &&
- !lhs.runtimeSpawned &&
- !rhs.runtimeSpawned &&
- lhs.runtimeState < 0 &&
- rhs.runtimeState < 0 &&
- !lhs.deathOrMonsterfyFlag &&
- !rhs.deathOrMonsterfyFlag &&
- lhs.deathDamageType == 0 &&
- rhs.deathDamageType != 0 &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
- };
- auto sameMonsterEntityState = [&](const MonsterRecord &lhs, const MonsterRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.facing == rhs.facing &&
- lhs.active == rhs.active &&
- lhs.visible == rhs.visible &&
- lhs.runtimeSpawned == rhs.runtimeSpawned &&
- lhs.runtimeState == rhs.runtimeState &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
- };
- auto sameSceneObjectState = [&](const ObjectRecord &lhs, const ObjectRecord &rhs) {
- return lhs.currentX == rhs.currentX &&
- lhs.currentY == rhs.currentY &&
- lhs.currentZ == rhs.currentZ &&
- lhs.zExtent == rhs.zExtent &&
- lhs.operatable == rhs.operatable &&
- lhs.visible == rhs.visible &&
- lhs.runtimeVisible == rhs.runtimeVisible &&
- lhs.actionTag.equalsIgnoreCase(rhs.actionTag) &&
- lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
- lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
- lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
- session.resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(session.resolveSceneObjectSpritePathLocal(rhs)) &&
- sameRect(session.getSceneObjectBounds(lhs), session.getSceneObjectBounds(rhs));
- };
- auto sameSceneRegionState = [&](const RegionRecord &lhs, const RegionRecord &rhs) {
- return lhs.left == rhs.left &&
- lhs.top == rhs.top &&
- lhs.right == rhs.right &&
- lhs.bottom == rhs.bottom &&
- lhs.minZ == rhs.minZ &&
- lhs.maxZ == rhs.maxZ &&
- lhs.startEnabled == rhs.startEnabled &&
- lhs.cursorEnabled == rhs.cursorEnabled &&
- lhs.desiredFacing == rhs.desiredFacing &&
- lhs.actionTag.equalsIgnoreCase(rhs.actionTag);
- };
-
auto resolveHitEffectAnchor = [&](const Entity &targetEntity,
Common::Point &anchorPoint, float &renderZ) {
const Common::Rect targetRect = targetEntity.getScreenRect();
@@ -795,260 +694,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return removedAny;
};
- auto applyCurrentRoomRuntimeMutationsInPlace = [&](bool preservePaletteState) {
- Script *script = _engine.getScript();
- ResourceManager *resources = _engine.getResources();
- if (!script || !resources)
- return false;
-
- byte previousPalette[256 * 3];
- memcpy(previousPalette, scene.palette, sizeof(previousPalette));
- const float previousPaletteBrightness = scene.targetPaletteBrightness;
- Common::Array<AudioCommand> entryAudioCommands = scene.state.audioCommands;
- RoomSetupState updatedState;
- if (!script->materializeRoomState(
- scene.state.entranceName, scene.state.roomName, updatedState, *resources)) {
- return false;
- }
- updatedState.audioCommands = Common::move(entryAudioCommands);
-
- RoomSceneResources updatedScene;
- if (!loadRoomSceneResources(updatedState, *resources, updatedScene))
- return false;
- Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates;
- updatedMonsterCombatStates.resize(updatedState.roomMonsters.size());
- for (uint i = 0; i < updatedState.roomMonsters.size(); ++i) {
- const MonsterRecord &updatedMonster = updatedState.roomMonsters[i];
- for (uint j = 0; j < scene.state.roomMonsters.size() &&
- j < monsterCombatStates.size(); ++j) {
- const MonsterRecord &previousMonster = scene.state.roomMonsters[j];
- if (!previousMonster.monsterName.equalsIgnoreCase(updatedMonster.monsterName))
- continue;
- if (sameMonsterEntityState(previousMonster, updatedMonster))
- updatedMonsterCombatStates[i] = monsterCombatStates[j];
- break;
- }
- }
-
- for (const ObjectRecord &object : scene.sceneObjects) {
- if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
- session.removeSceneEntityByName(object.objectName);
- }
- for (const ObjectRecord &object : updatedScene.sceneObjects) {
- const ObjectRecord *previous = findSceneObjectByName(scene.sceneObjects, object.objectName);
- const bool changed = !previous || !sameSceneObjectState(*previous, object);
- if (!changed)
- continue;
-
- session.removeSceneEntityByName(object.objectName);
- session.spawnSceneObjectEntityFromRecord(object);
- }
-
- for (const RegionRecord ®ion : scene.sceneRegions) {
- if (!findSceneRegionByName(updatedScene.sceneRegions, region.regionName))
- session.removeSceneEntityByName(region.regionName);
- }
- for (const RegionRecord ®ion : updatedScene.sceneRegions) {
- const RegionRecord *previous =
- findSceneRegionByName(scene.sceneRegions, region.regionName);
- const bool changed = !previous || !sameSceneRegionState(*previous, region);
- if (!changed)
- continue;
-
- session.removeSceneEntityByName(region.regionName);
- session.spawnSceneRegionEntityFromRecord(region);
- }
-
- for (const TimerRecord &timer : scene.state.roomTimers) {
- bool found = false;
- for (const TimerRecord &updatedTimer : updatedState.roomTimers) {
- if (updatedTimer.timerName.equalsIgnoreCase(timer.timerName)) {
- found = true;
- break;
- }
- }
- if (!found)
- session.removeSceneEntityByName(timer.timerName);
- }
- for (const TimerRecord &timer : updatedState.roomTimers) {
- const TimerRecord *previous = nullptr;
- for (const TimerRecord &candidate : scene.state.roomTimers) {
- if (candidate.timerName.equalsIgnoreCase(timer.timerName)) {
- previous = &candidate;
- break;
- }
- }
- const bool changed = !previous ||
- previous->initialValue != timer.initialValue ||
- previous->currentValue != timer.currentValue ||
- previous->enabled != timer.enabled ||
- previous->looping != timer.looping ||
- previous->global != timer.global;
- if (!changed)
- continue;
-
- session.removeSceneEntityByName(timer.timerName);
- if (entityManager) {
- (void)entityManager->spawnSceneTimerEntity(
- timer.timerName, timer.initialValue, timer.currentValue,
- timer.enabled, timer.looping, timer.global);
- }
- }
-
- for (AnimRecord &anim : updatedState.roomAnimations) {
- const AnimRecord *previous =
- findRoomAnimByNameConst(scene.state.roomAnimations, anim.animName);
- const bool wasDisplayed = previous && shouldDisplaySceneAnimation(*previous);
- const bool isDisplayed = shouldDisplaySceneAnimation(anim);
- const bool becameVisible = !wasDisplayed && isDisplayed;
- if (becameVisible)
- anim.runtimeState = 0;
-
- Entity *entity = entityManager
- ? entityManager->findSceneEntityByName(anim.animName)
- : nullptr;
- if (!isDisplayed) {
- session.removeSceneEntityByName(anim.animName);
- continue;
- }
-
- const bool needsRespawn = !entity || !previous ||
- !previous->resourcePath.equalsIgnoreCase(anim.resourcePath) ||
- previous->x != anim.x ||
- previous->y != anim.y ||
- previous->z != anim.z ||
- previous->frameDelay != anim.frameDelay ||
- previous->looping != anim.looping ||
- previous->pingPong != anim.pingPong ||
- previous->backward != anim.backward;
- if (needsRespawn) {
- session.removeSceneEntityByName(anim.animName);
- if (entityManager) {
- entity = entityManager->spawnSceneAnimationEntity(
- anim.animName, anim.resourcePath, Common::Point(anim.x, anim.y),
- (float)anim.z, anim.frameDelay, anim.active, isDisplayed, anim.looping,
- anim.backward, anim.pingPong, anim.runtimeState);
- }
- } else if (entity) {
- const float renderZ =
- (float)anim.z - floorf(MAX<float>(entity->getZExtent(), 0.0f) * 0.5f);
- const bool zChanged = entity->getZ() != renderZ;
- entity->setPosition(anim.x, anim.y, renderZ);
- entity->setLooping(anim.looping);
- entity->setPingPong(anim.pingPong);
- entity->setPlayBackwards(anim.backward);
- entity->setAnimationRate(anim.frameDelay);
- entity->setVisible(isDisplayed);
- if (anim.active && anim.runtimeState >= 0 && !entity->isAnimationEnabled())
- entity->setCurrentFrame(anim.runtimeState);
- entity->setAnimationEnabled(anim.active);
- if (zChanged)
- entityManager->reinsertSceneEntity(entity);
- }
- }
-
- for (const AnimRecord &anim : scene.state.roomAnimations) {
- if (!findRoomAnimByNameConst(updatedState.roomAnimations, anim.animName))
- session.removeSceneEntityByName(anim.animName);
- }
-
- for (const NpcRecord &npc : scene.state.roomNpcs) {
- const NpcRecord *updatedNpc =
- findRoomNpcByNameConst(updatedState.roomNpcs, npc.npcName);
- const bool keepCurrentRoomDeathEntity =
- !updatedNpc &&
- isRetainedCurrentRoomNpcDeathRecord(npc) &&
- entityManager &&
- entityManager->findSceneEntityByName(npc.npcName);
- const bool keepEntity =
- updatedNpc &&
- session.shouldSpawnRoomNpcEntity(*updatedNpc) &&
- (sameNpcEntityState(npc, *updatedNpc) ||
- isQueuedLiveNpcDeathMutation(npc, *updatedNpc));
- if (keepCurrentRoomDeathEntity) {
- debugC(1, kDebugCombat,
- "Harvester: retained current-room npc death entity npc='%s' damage_type=%d frame=%d",
- npc.npcName.c_str(), npc.deathDamageType, npc.runtimeState);
- continue;
- }
- if (!keepEntity)
- session.removeSceneEntityByName(npc.npcName);
- }
- for (const NpcRecord &npc : updatedState.roomNpcs) {
- if (!session.shouldSpawnRoomNpcEntity(npc))
- continue;
-
- Entity *entity = entityManager
- ? entityManager->findSceneEntityByName(npc.npcName)
- : nullptr;
- const NpcRecord *previousNpc =
- findRoomNpcByNameConst(scene.state.roomNpcs, npc.npcName);
- const bool keepQueuedDeathEntity =
- entity &&
- previousNpc &&
- isQueuedLiveNpcDeathMutation(*previousNpc, npc);
- if (keepQueuedDeathEntity)
- continue;
- const bool needsRespawn =
- !entity || !previousNpc || !sameNpcEntityState(*previousNpc, npc);
- if (!needsRespawn)
- continue;
-
- session.removeSceneEntityByName(npc.npcName);
- if (!session.spawnSceneNpcEntityFromRecord(npc))
- return false;
- }
-
- for (const MonsterRecord &monster : scene.state.roomMonsters) {
- const MonsterRecord *updatedMonster =
- findRoomMonsterByNameConst(updatedState.roomMonsters, monster.monsterName);
- const bool keepEntity =
- updatedMonster &&
- session.shouldSpawnRoomMonsterEntity(*updatedMonster) &&
- sameMonsterEntityState(monster, *updatedMonster);
- if (!keepEntity)
- session.removeSceneEntityByName(monster.monsterName);
- }
- for (const MonsterRecord &monster : updatedState.roomMonsters) {
- if (!session.shouldSpawnRoomMonsterEntity(monster))
- continue;
-
- Entity *entity = entityManager
- ? entityManager->findSceneEntityByName(monster.monsterName)
- : nullptr;
- const MonsterRecord *previousMonster =
- findRoomMonsterByNameConst(scene.state.roomMonsters, monster.monsterName);
- const bool needsRespawn =
- !entity || !previousMonster || !sameMonsterEntityState(*previousMonster, monster);
- if (!needsRespawn)
- continue;
-
- session.removeSceneEntityByName(monster.monsterName);
- if (!session.spawnSceneMonsterEntityFromRecord(updatedState, monster))
- return false;
- }
-
- scene.state = updatedState;
- monsterCombatStates = Common::move(updatedMonsterCombatStates);
- scene.sceneObjects = updatedScene.sceneObjects;
- scene.sceneAnimations = updatedScene.sceneAnimations;
- scene.sceneRegions = updatedScene.sceneRegions;
- const int playerCombatLoadout = script->getPlayerCombatLoadout();
- if (playerState.entity && playerState.combatLoadout != playerCombatLoadout &&
- !Player::syncCombatLoadoutVisual(
- _engine, updatedState, playerState, playerCombatLoadout)) {
- return false;
- }
- if (preservePaletteState) {
- memcpy(scene.palette, previousPalette, sizeof(scene.palette));
- scene.targetPaletteBrightness = previousPaletteBrightness;
- } else {
- memcpy(scene.palette, updatedScene.palette, sizeof(scene.palette));
- scene.targetPaletteBrightness = updatedScene.targetPaletteBrightness;
- }
- session.captureCurrentSaveState();
- return _inventory.refresh();
- };
session.resetIdleState();
auto refreshCurrentScene = [&](bool preservePlayerPlacement) {
@@ -1163,7 +808,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (clearCarryState)
clearCarriedRoomItem();
- if (!applyCurrentRoomRuntimeMutationsInPlace(true) && !refreshCurrentScene(true))
+ if (!session.applyCurrentRoomRuntimeMutationsInPlace(true) && !refreshCurrentScene(true))
return Common::kReadingFailed;
if (!_inventory.refresh())
return Common::kReadingFailed;
@@ -1523,7 +1168,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
: 0);
};
typedef decltype(refreshCurrentScene) RefreshCurrentSceneFn;
- typedef decltype(applyCurrentRoomRuntimeMutationsInPlace) ApplyCurrentRoomRuntimeMutationsInPlaceFn;
typedef decltype(captureDialogueBackdrop) CaptureDialogueBackdropFn;
typedef decltype(showCdChangePrompt) ShowCdChangePromptFn;
typedef decltype(runRoomExitCommands) RunRoomExitCommandsFn;
@@ -1536,7 +1180,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomInteractionCallbacksImpl(
RoomSession &session_,
RefreshCurrentSceneFn &refreshCurrentSceneFn_,
- ApplyCurrentRoomRuntimeMutationsInPlaceFn &applyCurrentRoomRuntimeMutationsInPlaceFn_,
CaptureDialogueBackdropFn &captureDialogueBackdropFn_,
ShowCdChangePromptFn &showCdChangePromptFn_,
RunRoomExitCommandsFn &runRoomExitCommandsFn_,
@@ -1545,7 +1188,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RunModalShowTextFn &runModalShowTextFn_,
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn_)
: session(session_), refreshCurrentSceneFn(refreshCurrentSceneFn_),
- applyCurrentRoomRuntimeMutationsInPlaceFn(applyCurrentRoomRuntimeMutationsInPlaceFn_),
captureDialogueBackdropFn(captureDialogueBackdropFn_),
showCdChangePromptFn(showCdChangePromptFn_),
runRoomExitCommandsFn(runRoomExitCommandsFn_),
@@ -1560,7 +1202,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) override {
- return applyCurrentRoomRuntimeMutationsInPlaceFn(preservePaletteState);
+ return session.applyCurrentRoomRuntimeMutationsInPlace(preservePaletteState);
}
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) override {
@@ -1606,7 +1248,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomSession &session;
RefreshCurrentSceneFn &refreshCurrentSceneFn;
- ApplyCurrentRoomRuntimeMutationsInPlaceFn &applyCurrentRoomRuntimeMutationsInPlaceFn;
CaptureDialogueBackdropFn &captureDialogueBackdropFn;
ShowCdChangePromptFn &showCdChangePromptFn;
RunRoomExitCommandsFn &runRoomExitCommandsFn;
@@ -1617,7 +1258,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
};
RoomInteractionCallbacksImpl interactionCallbacks(
session,
- refreshCurrentScene, applyCurrentRoomRuntimeMutationsInPlace,
+ refreshCurrentScene,
captureDialogueBackdrop, showCdChangePrompt,
runRoomExitCommands, applyLightingCommand, applyPlayerGotoXZ, runModalShowText,
startPlayerDefeatSequence);
@@ -1636,7 +1277,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
// Native room_setup applies entry-time state changes before the first visible room frame.
if (roomEntryInteraction.mutatedRuntimeState) {
session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
- if (!applyCurrentRoomRuntimeMutationsInPlace(false) &&
+ if (!session.applyCurrentRoomRuntimeMutationsInPlace(false) &&
!refreshCurrentScene(true)) {
return Common::kReadingFailed;
}
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index 68726a168cb..6e999a7a150 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -23,6 +23,7 @@
#include "harvester/detection.h"
#include "harvester/harvester.h"
+#include "harvester/inventory.h"
#include "harvester/monster.h"
#include "harvester/player.h"
#include "harvester/room_combat.h"
@@ -32,8 +33,113 @@ namespace Harvester {
static const char *const kInventoryOwnerName = "INVENTORY";
-RoomSession::RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager)
- : _engine(engine), _scene(scene), _entityManager(entityManager) {
+static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
+ return lhs.left == rhs.left && lhs.top == rhs.top &&
+ lhs.right == rhs.right && lhs.bottom == rhs.bottom;
+}
+
+static const AnimRecord *findRoomAnimByNameConst(const Common::Array<AnimRecord> &anims, const Common::String &animName) {
+ for (const AnimRecord &anim : anims) {
+ if (anim.animName.equalsIgnoreCase(animName))
+ return &anim;
+ }
+ return nullptr;
+}
+
+static const NpcRecord *findRoomNpcByNameConst(const Common::Array<NpcRecord> &npcs, const Common::String &npcName) {
+ for (const NpcRecord &npc : npcs) {
+ if (npc.npcName.equalsIgnoreCase(npcName))
+ return &npc;
+ }
+ return nullptr;
+}
+
+static const MonsterRecord *findRoomMonsterByNameConst(const Common::Array<MonsterRecord> &monsters, const Common::String &monsterName) {
+ for (const MonsterRecord &monster : monsters) {
+ if (monster.monsterName.equalsIgnoreCase(monsterName))
+ return &monster;
+ }
+ return nullptr;
+}
+
+static bool sameNpcEntityState(const NpcRecord &lhs, const NpcRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.frameDelay == rhs.frameDelay &&
+ lhs.active == rhs.active &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeSpawned == rhs.runtimeSpawned &&
+ lhs.runtimeState == rhs.runtimeState &&
+ lhs.deathDamageType == rhs.deathDamageType &&
+ lhs.deathOrMonsterfyFlag == rhs.deathOrMonsterfyFlag &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
+static bool isQueuedLiveNpcDeathMutation(const NpcRecord &lhs, const NpcRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.frameDelay == rhs.frameDelay &&
+ lhs.active &&
+ rhs.active &&
+ lhs.visible &&
+ rhs.visible &&
+ !lhs.runtimeSpawned &&
+ !rhs.runtimeSpawned &&
+ lhs.runtimeState < 0 &&
+ rhs.runtimeState < 0 &&
+ !lhs.deathOrMonsterfyFlag &&
+ !rhs.deathOrMonsterfyFlag &&
+ lhs.deathDamageType == 0 &&
+ rhs.deathDamageType != 0 &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
+static bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.facing == rhs.facing &&
+ lhs.active == rhs.active &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeSpawned == rhs.runtimeSpawned &&
+ lhs.runtimeState == rhs.runtimeState &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
+static bool sameSceneObjectState(const ObjectRecord &lhs, const ObjectRecord &rhs) {
+ return lhs.currentX == rhs.currentX &&
+ lhs.currentY == rhs.currentY &&
+ lhs.currentZ == rhs.currentZ &&
+ lhs.zExtent == rhs.zExtent &&
+ lhs.operatable == rhs.operatable &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeVisible == rhs.runtimeVisible &&
+ lhs.actionTag.equalsIgnoreCase(rhs.actionTag) &&
+ lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
+ lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
+ lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
+ RoomSession::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(RoomSession::resolveSceneObjectSpritePathLocal(rhs)) &&
+ sameRect(RoomSession::getSceneObjectBounds(lhs), RoomSession::getSceneObjectBounds(rhs));
+}
+
+static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rhs) {
+ return lhs.left == rhs.left &&
+ lhs.top == rhs.top &&
+ lhs.right == rhs.right &&
+ lhs.bottom == rhs.bottom &&
+ lhs.minZ == rhs.minZ &&
+ lhs.maxZ == rhs.maxZ &&
+ lhs.startEnabled == rhs.startEnabled &&
+ lhs.cursorEnabled == rhs.cursorEnabled &&
+ lhs.desiredFacing == rhs.desiredFacing &&
+ lhs.actionTag.equalsIgnoreCase(rhs.actionTag);
+}
+
+RoomSession::RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager,
+ InventorySystem &inventory)
+ : _engine(engine), _scene(scene), _entityManager(entityManager), _inventory(inventory) {
}
void RoomSession::resetIdleState() {
@@ -392,4 +498,261 @@ Entity *RoomSession::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roo
return entity;
}
+bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
+ Script *script = _engine.getScript();
+ ResourceManager *resources = _engine.getResources();
+ if (!script || !resources)
+ return false;
+
+ byte previousPalette[256 * 3];
+ memcpy(previousPalette, _scene.palette, sizeof(previousPalette));
+ const float previousPaletteBrightness = _scene.targetPaletteBrightness;
+ Common::Array<AudioCommand> entryAudioCommands = _scene.state.audioCommands;
+ debugC(2, kDebugRoom, "Harvester: reconciling room runtime mutations room='%s' preserve_palette=%d",
+ _scene.state.roomName.c_str(), preservePaletteState);
+ RoomSetupState updatedState;
+ if (!script->materializeRoomState(
+ _scene.state.entranceName, _scene.state.roomName, updatedState, *resources)) {
+ return false;
+ }
+ updatedState.audioCommands = Common::move(entryAudioCommands);
+
+ RoomSceneResources updatedScene;
+ if (!loadRoomSceneResources(updatedState, *resources, updatedScene))
+ return false;
+ Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates;
+ updatedMonsterCombatStates.resize(updatedState.roomMonsters.size());
+ for (uint i = 0; i < updatedState.roomMonsters.size(); ++i) {
+ const MonsterRecord &updatedMonster = updatedState.roomMonsters[i];
+ for (uint j = 0; j < _scene.state.roomMonsters.size() &&
+ j < _monsterCombatStates.size(); ++j) {
+ const MonsterRecord &previousMonster = _scene.state.roomMonsters[j];
+ if (!previousMonster.monsterName.equalsIgnoreCase(updatedMonster.monsterName))
+ continue;
+ if (sameMonsterEntityState(previousMonster, updatedMonster))
+ updatedMonsterCombatStates[i] = _monsterCombatStates[j];
+ break;
+ }
+ }
+
+ for (const ObjectRecord &object : _scene.sceneObjects) {
+ if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
+ removeSceneEntityByName(object.objectName);
+ }
+ for (const ObjectRecord &object : updatedScene.sceneObjects) {
+ const ObjectRecord *previous = findSceneObjectByName(_scene.sceneObjects, object.objectName);
+ const bool changed = !previous || !sameSceneObjectState(*previous, object);
+ if (!changed)
+ continue;
+
+ removeSceneEntityByName(object.objectName);
+ spawnSceneObjectEntityFromRecord(object);
+ }
+
+ for (const RegionRecord ®ion : _scene.sceneRegions) {
+ if (!findSceneRegionByName(updatedScene.sceneRegions, region.regionName))
+ removeSceneEntityByName(region.regionName);
+ }
+ for (const RegionRecord ®ion : updatedScene.sceneRegions) {
+ const RegionRecord *previous =
+ findSceneRegionByName(_scene.sceneRegions, region.regionName);
+ const bool changed = !previous || !sameSceneRegionState(*previous, region);
+ if (!changed)
+ continue;
+
+ removeSceneEntityByName(region.regionName);
+ spawnSceneRegionEntityFromRecord(region);
+ }
+
+ for (const TimerRecord &timer : _scene.state.roomTimers) {
+ bool found = false;
+ for (const TimerRecord &updatedTimer : updatedState.roomTimers) {
+ if (updatedTimer.timerName.equalsIgnoreCase(timer.timerName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ removeSceneEntityByName(timer.timerName);
+ }
+ for (const TimerRecord &timer : updatedState.roomTimers) {
+ const TimerRecord *previous = nullptr;
+ for (const TimerRecord &candidate : _scene.state.roomTimers) {
+ if (candidate.timerName.equalsIgnoreCase(timer.timerName)) {
+ previous = &candidate;
+ break;
+ }
+ }
+ const bool changed = !previous ||
+ previous->initialValue != timer.initialValue ||
+ previous->currentValue != timer.currentValue ||
+ previous->enabled != timer.enabled ||
+ previous->looping != timer.looping ||
+ previous->global != timer.global;
+ if (!changed)
+ continue;
+
+ removeSceneEntityByName(timer.timerName);
+ if (_entityManager) {
+ (void)_entityManager->spawnSceneTimerEntity(
+ timer.timerName, timer.initialValue, timer.currentValue,
+ timer.enabled, timer.looping, timer.global);
+ }
+ }
+
+ for (AnimRecord &anim : updatedState.roomAnimations) {
+ const AnimRecord *previous =
+ findRoomAnimByNameConst(_scene.state.roomAnimations, anim.animName);
+ const bool wasDisplayed = previous && shouldDisplaySceneAnimation(*previous);
+ const bool isDisplayed = shouldDisplaySceneAnimation(anim);
+ const bool becameVisible = !wasDisplayed && isDisplayed;
+ if (becameVisible)
+ anim.runtimeState = 0;
+
+ Entity *entity = _entityManager
+ ? _entityManager->findSceneEntityByName(anim.animName)
+ : nullptr;
+ if (!isDisplayed) {
+ removeSceneEntityByName(anim.animName);
+ continue;
+ }
+
+ const bool needsRespawn = !entity || !previous ||
+ !previous->resourcePath.equalsIgnoreCase(anim.resourcePath) ||
+ previous->x != anim.x ||
+ previous->y != anim.y ||
+ previous->z != anim.z ||
+ previous->frameDelay != anim.frameDelay ||
+ previous->looping != anim.looping ||
+ previous->pingPong != anim.pingPong ||
+ previous->backward != anim.backward;
+ if (needsRespawn) {
+ removeSceneEntityByName(anim.animName);
+ if (_entityManager) {
+ entity = _entityManager->spawnSceneAnimationEntity(
+ anim.animName, anim.resourcePath, Common::Point(anim.x, anim.y),
+ (float)anim.z, anim.frameDelay, anim.active, isDisplayed, anim.looping,
+ anim.backward, anim.pingPong, anim.runtimeState);
+ }
+ } else if (entity) {
+ const float renderZ =
+ (float)anim.z - floorf(MAX<float>(entity->getZExtent(), 0.0f) * 0.5f);
+ const bool zChanged = entity->getZ() != renderZ;
+ entity->setPosition(anim.x, anim.y, renderZ);
+ entity->setLooping(anim.looping);
+ entity->setPingPong(anim.pingPong);
+ entity->setPlayBackwards(anim.backward);
+ entity->setAnimationRate(anim.frameDelay);
+ entity->setVisible(isDisplayed);
+ if (anim.active && anim.runtimeState >= 0 && !entity->isAnimationEnabled())
+ entity->setCurrentFrame(anim.runtimeState);
+ entity->setAnimationEnabled(anim.active);
+ if (zChanged)
+ _entityManager->reinsertSceneEntity(entity);
+ }
+ }
+
+ for (const AnimRecord &anim : _scene.state.roomAnimations) {
+ if (!findRoomAnimByNameConst(updatedState.roomAnimations, anim.animName))
+ removeSceneEntityByName(anim.animName);
+ }
+
+ for (const NpcRecord &npc : _scene.state.roomNpcs) {
+ const NpcRecord *updatedNpc =
+ findRoomNpcByNameConst(updatedState.roomNpcs, npc.npcName);
+ const bool keepCurrentRoomDeathEntity =
+ !updatedNpc &&
+ isRetainedCurrentRoomNpcDeathRecord(npc) &&
+ _entityManager &&
+ _entityManager->findSceneEntityByName(npc.npcName);
+ const bool keepEntity =
+ updatedNpc &&
+ shouldSpawnRoomNpcEntity(*updatedNpc) &&
+ (sameNpcEntityState(npc, *updatedNpc) ||
+ isQueuedLiveNpcDeathMutation(npc, *updatedNpc));
+ if (keepCurrentRoomDeathEntity) {
+ debugC(1, kDebugCombat,
+ "Harvester: retained current-room npc death entity npc='%s' damage_type=%d frame=%d",
+ npc.npcName.c_str(), npc.deathDamageType, npc.runtimeState);
+ continue;
+ }
+ if (!keepEntity)
+ removeSceneEntityByName(npc.npcName);
+ }
+ for (const NpcRecord &npc : updatedState.roomNpcs) {
+ if (!shouldSpawnRoomNpcEntity(npc))
+ continue;
+
+ Entity *entity = _entityManager
+ ? _entityManager->findSceneEntityByName(npc.npcName)
+ : nullptr;
+ const NpcRecord *previousNpc =
+ findRoomNpcByNameConst(_scene.state.roomNpcs, npc.npcName);
+ const bool keepQueuedDeathEntity =
+ entity &&
+ previousNpc &&
+ isQueuedLiveNpcDeathMutation(*previousNpc, npc);
+ if (keepQueuedDeathEntity)
+ continue;
+ const bool needsRespawn =
+ !entity || !previousNpc || !sameNpcEntityState(*previousNpc, npc);
+ if (!needsRespawn)
+ continue;
+
+ removeSceneEntityByName(npc.npcName);
+ if (!spawnSceneNpcEntityFromRecord(npc))
+ return false;
+ }
+
+ for (const MonsterRecord &monster : _scene.state.roomMonsters) {
+ const MonsterRecord *updatedMonster =
+ findRoomMonsterByNameConst(updatedState.roomMonsters, monster.monsterName);
+ const bool keepEntity =
+ updatedMonster &&
+ shouldSpawnRoomMonsterEntity(*updatedMonster) &&
+ sameMonsterEntityState(monster, *updatedMonster);
+ if (!keepEntity)
+ removeSceneEntityByName(monster.monsterName);
+ }
+ for (const MonsterRecord &monster : updatedState.roomMonsters) {
+ if (!shouldSpawnRoomMonsterEntity(monster))
+ continue;
+
+ Entity *entity = _entityManager
+ ? _entityManager->findSceneEntityByName(monster.monsterName)
+ : nullptr;
+ const MonsterRecord *previousMonster =
+ findRoomMonsterByNameConst(_scene.state.roomMonsters, monster.monsterName);
+ const bool needsRespawn =
+ !entity || !previousMonster || !sameMonsterEntityState(*previousMonster, monster);
+ if (!needsRespawn)
+ continue;
+
+ removeSceneEntityByName(monster.monsterName);
+ if (!spawnSceneMonsterEntityFromRecord(updatedState, monster))
+ return false;
+ }
+
+ _scene.state = updatedState;
+ _monsterCombatStates = Common::move(updatedMonsterCombatStates);
+ _scene.sceneObjects = updatedScene.sceneObjects;
+ _scene.sceneAnimations = updatedScene.sceneAnimations;
+ _scene.sceneRegions = updatedScene.sceneRegions;
+ const int playerCombatLoadout = script->getPlayerCombatLoadout();
+ if (_playerState.entity && _playerState.combatLoadout != playerCombatLoadout &&
+ !Player::syncCombatLoadoutVisual(
+ _engine, updatedState, _playerState, playerCombatLoadout)) {
+ return false;
+ }
+ if (preservePaletteState) {
+ memcpy(_scene.palette, previousPalette, sizeof(_scene.palette));
+ _scene.targetPaletteBrightness = previousPaletteBrightness;
+ } else {
+ memcpy(_scene.palette, updatedScene.palette, sizeof(_scene.palette));
+ _scene.targetPaletteBrightness = updatedScene.targetPaletteBrightness;
+ }
+ captureCurrentSaveState();
+ return _inventory.refresh();
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 93598e92643..2fecbcf9da2 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -22,12 +22,14 @@
#ifndef HARVESTER_ROOM_SESSION_H
#define HARVESTER_ROOM_SESSION_H
+#include "harvester/room_combat.h"
#include "harvester/room_support.h"
#include "harvester/runtime_entity.h"
namespace Harvester {
class EntityManager;
+class InventorySystem;
struct RoomMovementInputState {
bool moveLeft = false;
@@ -40,9 +42,11 @@ struct RoomMovementInputState {
// Entity pointers remain owned by EntityManager.
class RoomSession {
public:
- RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager);
+ RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager,
+ InventorySystem &inventory);
RoomPlayerState &getPlayerState() { return _playerState; }
+ Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
RoomMovementInputState &getMovementInput() { return _movementInput; }
Common::String &getPendingRegionName() { return _pendingRegionName; }
@@ -54,6 +58,7 @@ public:
void stopPlayerRegionInteraction();
void syncCurrentRoomRuntimeState();
void captureCurrentSaveState();
+ bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState);
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers = false);
@@ -78,7 +83,9 @@ private:
HarvesterEngine &_engine;
RoomSceneResources &_scene;
EntityManager *_entityManager;
+ InventorySystem &_inventory;
RoomPlayerState _playerState;
+ Common::Array<RoomMonsterCombatState> _monsterCombatStates;
RoomIdleAnimationState _idleState;
RoomMovementInputState _movementInput;
Common::String _pendingRegionName;
Commit: a4044055a35c79dec24548b6a2fdb1debf023f40
https://github.com/scummvm/scummvm/commit/a4044055a35c79dec24548b6a2fdb1debf023f40
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: refactor RoomSession to include Flow for enhanced scene management
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/flow.h
engines/harvester/room.cpp
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 8c42144c327..92d9e15dc5d 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -61,6 +61,7 @@ private:
friend class MenuSystem;
friend class RoomInteractionProcessor;
friend class RoomSystem;
+ friend class RoomSession;
bool loadQuickTips();
bool loadMenuItems();
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 3566a43038d..31c14b2a8a6 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -451,7 +451,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
flow.executeStartupAudioCommands(scene.state.audioCommands);
if (!scene.state.musicPath.empty())
(void)_engine.playMusic(scene.state.musicPath);
- RoomSession session(_engine, scene, entityManager, _inventory);
+ RoomSession session(_engine, flow, scene, entityManager, _inventory);
debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
RoomPlayerState &playerState = session.getPlayerState();
playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
@@ -467,9 +467,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monsterCombatStates.resize(scene.state.roomMonsters.size());
Common::Array<RoomNpcCombatState> npcCombatStates;
npcCombatStates.resize(scene.state.roomNpcs.size());
- Common::Array<RoomHitEffectState> hitEffectStates;
+ Common::Array<RoomHitEffectState> &hitEffectStates = session.getHitEffectStates();
Common::Array<RoomCombatDamagePopupState> damagePopupStates;
- uint nextCombatEffectId = 0;
+ uint &nextCombatEffectId = session.getNextCombatEffectId();
ResolvedText inspectText;
Common::String currentInteractionPromptText;
bool showingInspectText = false;
@@ -696,73 +696,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
session.resetIdleState();
- auto refreshCurrentScene = [&](bool preservePlayerPlacement) {
- Common::Array<AudioCommand> entryAudioCommands = scene.state.audioCommands;
- RoomSetupState refreshedState;
- if (!_engine.getScript()->materializeRoomState(
- scene.state.entranceName, scene.state.roomName, refreshedState, *_engine.getResources())) {
- return false;
- }
-
- refreshedState.audioCommands = Common::move(entryAudioCommands);
- if (!loadRoomSceneResources(refreshedState, *_engine.getResources(), scene))
- return false;
- hitEffectStates.clear();
- nextCombatEffectId = 0;
- if (!flow.populateRoomSceneEntities(scene.state, scene.sceneObjects, scene.sceneAnimations))
- return false;
- if (entityManager) {
- for (const MonsterRecord &monster : scene.state.roomMonsters) {
- Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
- if (entity)
- Monster::applyAnimation(*entity, monster);
- }
- }
-
- playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
- if (playerState.entity) {
- if (!preservePlayerPlacement || playerState.facing < 0) {
- playerState.centerX = scene.state.playerSpawnX;
- playerState.bottomY = scene.state.playerSpawnY;
- playerState.z = (float)scene.state.playerSpawnZ;
- playerState.facing = scene.state.playerFacing;
- }
-
- const int facing = playerState.facing >= 0 ? playerState.facing : scene.state.playerFacing;
- (void)Player::setIdleAnimation(playerState, facing);
- (void)applyRoomActorPlacement(scene.state, *playerState.entity,
- playerState.centerX, playerState.bottomY, playerState.z);
- }
-
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- 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.combatLoadout =
- _engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
- monsterCombatStates.clear();
- monsterCombatStates.resize(scene.state.roomMonsters.size());
- pendingRegionName.clear();
- session.resetIdleState();
- flow.resetCursorAnimationSequence();
- session.captureCurrentSaveState();
- return _inventory.refresh();
- };
auto syncAnimatedRoomActorPlacement = [&]() {
Player::syncAnimationPlacement(playerState);
if (!entityManager)
@@ -808,7 +741,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (clearCarryState)
clearCarriedRoomItem();
- if (!session.applyCurrentRoomRuntimeMutationsInPlace(true) && !refreshCurrentScene(true))
+ if (!session.applyCurrentRoomRuntimeMutationsInPlace(true) && !session.refreshCurrentScene(true))
return Common::kReadingFailed;
if (!_inventory.refresh())
return Common::kReadingFailed;
@@ -1167,7 +1100,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
? _engine.getScript()->getPlayerCurrentHitPoints()
: 0);
};
- typedef decltype(refreshCurrentScene) RefreshCurrentSceneFn;
typedef decltype(captureDialogueBackdrop) CaptureDialogueBackdropFn;
typedef decltype(showCdChangePrompt) ShowCdChangePromptFn;
typedef decltype(runRoomExitCommands) RunRoomExitCommandsFn;
@@ -1179,7 +1111,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
struct RoomInteractionCallbacksImpl : RoomInteractionCallbacks {
RoomInteractionCallbacksImpl(
RoomSession &session_,
- RefreshCurrentSceneFn &refreshCurrentSceneFn_,
CaptureDialogueBackdropFn &captureDialogueBackdropFn_,
ShowCdChangePromptFn &showCdChangePromptFn_,
RunRoomExitCommandsFn &runRoomExitCommandsFn_,
@@ -1187,7 +1118,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ApplyPlayerGotoXZFn &applyPlayerGotoXZFn_,
RunModalShowTextFn &runModalShowTextFn_,
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn_)
- : session(session_), refreshCurrentSceneFn(refreshCurrentSceneFn_),
+ : session(session_),
captureDialogueBackdropFn(captureDialogueBackdropFn_),
showCdChangePromptFn(showCdChangePromptFn_),
runRoomExitCommandsFn(runRoomExitCommandsFn_),
@@ -1198,7 +1129,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
bool refreshCurrentScene(bool preservePlayerPlacement) override {
- return refreshCurrentSceneFn(preservePlayerPlacement);
+ return session.refreshCurrentScene(preservePlayerPlacement);
}
bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) override {
@@ -1247,7 +1178,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
RoomSession &session;
- RefreshCurrentSceneFn &refreshCurrentSceneFn;
CaptureDialogueBackdropFn &captureDialogueBackdropFn;
ShowCdChangePromptFn &showCdChangePromptFn;
RunRoomExitCommandsFn &runRoomExitCommandsFn;
@@ -1258,7 +1188,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
};
RoomInteractionCallbacksImpl interactionCallbacks(
session,
- refreshCurrentScene,
captureDialogueBackdrop, showCdChangePrompt,
runRoomExitCommands, applyLightingCommand, applyPlayerGotoXZ, runModalShowText,
startPlayerDefeatSequence);
@@ -1278,7 +1207,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (roomEntryInteraction.mutatedRuntimeState) {
session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
if (!session.applyCurrentRoomRuntimeMutationsInPlace(false) &&
- !refreshCurrentScene(true)) {
+ !session.refreshCurrentScene(true)) {
return Common::kReadingFailed;
}
roomEntryInteraction.mutatedRuntimeState = false;
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index 6e999a7a150..d432c77209f 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -22,6 +22,7 @@
#include "harvester/room_session.h"
#include "harvester/detection.h"
+#include "harvester/flow.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
#include "harvester/monster.h"
@@ -137,9 +138,9 @@ static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rh
lhs.actionTag.equalsIgnoreCase(rhs.actionTag);
}
-RoomSession::RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager,
+RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
InventorySystem &inventory)
- : _engine(engine), _scene(scene), _entityManager(entityManager), _inventory(inventory) {
+ : _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory) {
}
void RoomSession::resetIdleState() {
@@ -755,4 +756,74 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
return _inventory.refresh();
}
+bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
+ debugC(2, kDebugRoom, "Harvester: refreshing room scene room='%s' preserve_player_placement=%d",
+ _scene.state.roomName.c_str(), preservePlayerPlacement);
+ Common::Array<AudioCommand> entryAudioCommands = _scene.state.audioCommands;
+ RoomSetupState refreshedState;
+ if (!_engine.getScript()->materializeRoomState(
+ _scene.state.entranceName, _scene.state.roomName, refreshedState, *_engine.getResources())) {
+ return false;
+ }
+
+ refreshedState.audioCommands = Common::move(entryAudioCommands);
+ if (!loadRoomSceneResources(refreshedState, *_engine.getResources(), _scene))
+ return false;
+ _hitEffectStates.clear();
+ _nextCombatEffectId = 0;
+ if (!_flow.populateRoomSceneEntities(_scene.state, _scene.sceneObjects, _scene.sceneAnimations))
+ return false;
+ if (_entityManager) {
+ for (const MonsterRecord &monster : _scene.state.roomMonsters) {
+ Entity *entity = _entityManager->findSceneEntityByName(monster.monsterName);
+ if (entity)
+ Monster::applyAnimation(*entity, monster);
+ }
+ }
+
+ _playerState.entity = _entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr;
+ if (_playerState.entity) {
+ if (!preservePlayerPlacement || _playerState.facing < 0) {
+ _playerState.centerX = _scene.state.playerSpawnX;
+ _playerState.bottomY = _scene.state.playerSpawnY;
+ _playerState.z = (float)_scene.state.playerSpawnZ;
+ _playerState.facing = _scene.state.playerFacing;
+ }
+
+ const int facing = _playerState.facing >= 0 ? _playerState.facing : _scene.state.playerFacing;
+ (void)Player::setIdleAnimation(_playerState, facing);
+ (void)applyRoomActorPlacement(_scene.state, *_playerState.entity,
+ _playerState.centerX, _playerState.bottomY, _playerState.z);
+ }
+
+ _playerState.hasMoveTarget = false;
+ _playerState.turnActive = false;
+ _playerState.turnTargetFacing = -1;
+ _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.combatLoadout =
+ _engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
+ _monsterCombatStates.clear();
+ _monsterCombatStates.resize(_scene.state.roomMonsters.size());
+ _pendingRegionName.clear();
+ resetIdleState();
+ _flow.resetCursorAnimationSequence();
+ captureCurrentSaveState();
+ return _inventory.refresh();
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 2fecbcf9da2..5a8b496d49e 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -29,6 +29,7 @@
namespace Harvester {
class EntityManager;
+class Flow;
class InventorySystem;
struct RoomMovementInputState {
@@ -42,11 +43,13 @@ struct RoomMovementInputState {
// Entity pointers remain owned by EntityManager.
class RoomSession {
public:
- RoomSession(HarvesterEngine &engine, RoomSceneResources &scene, EntityManager *entityManager,
+ RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
InventorySystem &inventory);
RoomPlayerState &getPlayerState() { return _playerState; }
Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
+ Common::Array<RoomHitEffectState> &getHitEffectStates() { return _hitEffectStates; }
+ uint &getNextCombatEffectId() { return _nextCombatEffectId; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
RoomMovementInputState &getMovementInput() { return _movementInput; }
Common::String &getPendingRegionName() { return _pendingRegionName; }
@@ -59,6 +62,7 @@ public:
void syncCurrentRoomRuntimeState();
void captureCurrentSaveState();
bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState);
+ bool refreshCurrentScene(bool preservePlayerPlacement);
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers = false);
@@ -81,11 +85,14 @@ private:
bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
HarvesterEngine &_engine;
+ Flow &_flow;
RoomSceneResources &_scene;
EntityManager *_entityManager;
InventorySystem &_inventory;
RoomPlayerState _playerState;
Common::Array<RoomMonsterCombatState> _monsterCombatStates;
+ Common::Array<RoomHitEffectState> _hitEffectStates;
+ uint _nextCombatEffectId = 0;
RoomIdleAnimationState _idleState;
RoomMovementInputState _movementInput;
Common::String _pendingRegionName;
Commit: 59e4140ebc6cccbcf3b8a71f7144da0ad05764f6
https://github.com/scummvm/scummvm/commit/59e4140ebc6cccbcf3b8a71f7144da0ad05764f6
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: RoomCombatEffects for managing hit effects and damage popups
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 31c14b2a8a6..82eac8f328d 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -276,37 +276,6 @@ static void drawCombatLoadoutResourceIcons(Graphics::Screen &screen, const Art &
kNativeCombatResourceIconsY);
}
-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 = (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);
- 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);
-}
-
static void setScaledRoomPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
byte scaledPalette[256 * 3];
const float gammaBrightness = g_engine ? g_engine->getGammaBrightnessScale() : 1.0f;
@@ -467,9 +436,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monsterCombatStates.resize(scene.state.roomMonsters.size());
Common::Array<RoomNpcCombatState> npcCombatStates;
npcCombatStates.resize(scene.state.roomNpcs.size());
- Common::Array<RoomHitEffectState> &hitEffectStates = session.getHitEffectStates();
- Common::Array<RoomCombatDamagePopupState> damagePopupStates;
- uint &nextCombatEffectId = session.getNextCombatEffectId();
+ RoomCombatEffects &combatEffects = session.getCombatEffects();
ResolvedText inspectText;
Common::String currentInteractionPromptText;
bool showingInspectText = false;
@@ -539,161 +506,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
sceneObject->currentOwnerOrRoom = *ownerOrRoom;
};
- auto 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;
- };
- auto 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;
- };
- auto spawnCombatHitEffect = [&](Entity &sourceEntity,
- const Common::String &followTargetName) {
- if (!entityManager)
- return false;
-
- RoomHitEffectState 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)
- return false;
-
- hitEffectStates.push_back(Common::move(effectState));
- return true;
- };
- auto spawnCombatDamagePopup = [&](Entity &targetEntity,
- const Common::String &followTargetName, int damageAmount) {
- if (damageAmount <= 0)
- return;
-
- RoomCombatDamagePopupState popupState;
- popupState.followTargetName = followTargetName;
- popupState.startTick = Player::getRuntimeClockTicks();
- popupState.damageAmount = damageAmount;
- resolveCombatDamagePopupAnchor(targetEntity, popupState.anchorPoint);
- damagePopupStates.push_back(Common::move(popupState));
- };
- auto syncCombatHitEffects = [&]() {
- if (!entityManager)
- return false;
-
- bool changed = false;
- for (RoomHitEffectState &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;
- };
- auto drawCombatDamagePopups = [&](Graphics::Screen &screen) {
- if (!_engine.isCombatDebugEnabled() || 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 (RoomCombatDamagePopupState &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 = (int)(
- clampedElapsed * (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);
- }
- };
- auto 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) {
- session.removeSceneEntityByName(effectName);
- hitEffectStates.remove_at(i);
- removedAny = true;
- continue;
- }
-
- hitEffectStates[i].finished = finished;
- ++i;
- }
-
- return removedAny;
- };
- auto 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;
- };
-
session.resetIdleState();
auto syncAnimatedRoomActorPlacement = [&]() {
@@ -1680,7 +1492,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const int damageLanded = monster->currentHitPoints;
monster->currentHitPoints = 0;
if (damageLanded > 0)
- spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
+ combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
if (playerState.attackTargetClassId == kRuntimeEntityClassMonster &&
playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
@@ -1875,7 +1687,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
};
auto startMonsterHitReaction = [&](MonsterRecord &monster, Entity &monsterEntity,
RoomMonsterCombatState *combatState, int attackerAttackFirstFrame) {
- if (spawnCombatHitEffect(monsterEntity, monster.monsterName))
+ if (combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
needsRedraw = true;
if (!combatState) {
@@ -1982,7 +1794,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return Common::kNoError;
if (runtimeChanged && npcEntity) {
const Common::String followTargetName = npc->monsterfyTargetName;
- if (spawnCombatHitEffect(*npcEntity, followTargetName))
+ if (combatEffects.spawnCombatHitEffect(*npcEntity, followTargetName))
needsRedraw = true;
}
if (runtimeChanged && combatState && npcEntity &&
@@ -2040,7 +1852,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
Player::describeCombatLoadout(playerState.combatLoadout), damageAmount,
Player::describeCombatDamageType(damageType), hitPointsBefore, monster->currentHitPoints);
if (damageLanded > 0) {
- spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
+ combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
startMonsterHitReaction(*monster, *monsterEntity, combatState, playerState.attackFirstFrame);
}
if (monster->currentHitPoints > 0) {
@@ -2283,7 +2095,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerHitPointsBefore, playerHitPointsAfter, changed,
damageDisabled ? 1 : 0);
if (damageLanded > 0 && playerState.entity) {
- spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
+ combatEffects.spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
if (idleState.entity) {
idleState.entity->setVisible(false);
idleState.entity->setAnimationRate(0);
@@ -2881,7 +2693,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
drawRoomScene(_engine, *activeScreen, scene, scene.targetPaletteBrightness);
- drawCombatDamagePopups(*activeScreen);
+ combatEffects.drawCombatDamagePopups(*activeScreen, _engine.isCombatDebugEnabled());
if (_engine.getScript() && playerState.entity) {
drawCombatLoadoutResourceIcons(*activeScreen, *art, *_engine.getScript(),
_engine.getScript()->getPlayerCombatLoadout());
@@ -3687,7 +3499,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combatError = updateRoomMonsterCombat();
if (combatError.getCode() != Common::kNoError)
return combatError;
- if (syncCombatHitEffects())
+ if (combatEffects.syncCombatHitEffects())
needsRedraw = true;
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
@@ -3702,9 +3514,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
- if (pruneCombatHitEffects())
+ if (combatEffects.pruneCombatHitEffects())
needsRedraw = true;
- if (pruneCombatDamagePopups())
+ if (combatEffects.pruneCombatDamagePopups())
needsRedraw = true;
if (entityManager) {
Common::Array<Common::String> expiredTimerNames;
@@ -3732,7 +3544,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
if (Player::updateIdleAnimation(scene.state, playerState, idleState))
needsRedraw = true;
- if (_engine.isCombatDebugEnabled() && !damagePopupStates.empty())
+ if (_engine.isCombatDebugEnabled() && combatEffects.hasDamagePopups())
needsRedraw = true;
limiter.delayBeforeSwap();
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index f336c2ac3cb..bea363a7d98 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -22,6 +22,10 @@
#include "harvester/room_combat.h"
#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/screen.h"
+#include "harvester/detection.h"
#include "harvester/harvester.h"
#include "harvester/player.h"
#include "harvester/runtime_entity.h"
@@ -58,6 +62,38 @@ const int kNativeDefaultNpcDeathDamageType = 1;
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 = (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);
+ 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);
+}
+
+
struct RoomMonsterFacingAnimationRange {
RoomMonsterFacingAnimationRange() {}
RoomMonsterFacingAnimationRange(int walkFirstFrame, int walkLastFrame, int idleFrame)
@@ -481,4 +517,177 @@ void clearRoomMonsterHitState(RoomMonsterCombatState &combatState) {
combatState.hitSoundPlayed = false;
}
+RoomCombatEffects::RoomCombatEffects(EntityManager *entityManager) : _entityManager(entityManager) {
+ debugC(1, kDebugCombat, "Harvester: created room combat effects entities=%d", _entityManager != nullptr);
+}
+
+void RoomCombatEffects::clearHitEffects() {
+ debugC(2, kDebugCombat, "Harvester: resetting room blood effects records=%u next_id=%u",
+ (uint)_hitEffectStates.size(), _nextCombatEffectId);
+ _hitEffectStates.clear();
+ _nextCombatEffectId = 0;
+}
+
+void RoomCombatEffects::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 RoomCombatEffects::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 RoomCombatEffects::spawnCombatHitEffect(Entity &sourceEntity, const Common::String &followTargetName) {
+ if (!_entityManager)
+ return false;
+
+ RoomHitEffectState 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 RoomCombatEffects::spawnCombatDamagePopup(Entity &targetEntity, const Common::String &followTargetName, int damageAmount) {
+ if (damageAmount <= 0)
+ return;
+
+ RoomCombatDamagePopupState popupState;
+ popupState.followTargetName = followTargetName;
+ popupState.startTick = Player::getRuntimeClockTicks();
+ popupState.damageAmount = damageAmount;
+ resolveCombatDamagePopupAnchor(targetEntity, popupState.anchorPoint);
+ _damagePopupStates.push_back(Common::move(popupState));
+}
+
+bool RoomCombatEffects::syncCombatHitEffects() {
+ if (!_entityManager)
+ return false;
+
+ bool changed = false;
+ for (RoomHitEffectState &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 RoomCombatEffects::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 (RoomCombatDamagePopupState &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 = (int)(
+ clampedElapsed * (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 RoomCombatEffects::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 RoomCombatEffects::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/room_combat.h b/engines/harvester/room_combat.h
index 833b47e58a3..45932372945 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -27,6 +27,7 @@
namespace Harvester {
class Entity;
+class EntityManager;
class HarvesterEngine;
extern const int kRoomMonsterAnimationRate;
@@ -130,6 +131,29 @@ struct RoomCombatDamagePopupState {
int damageAmount = 0;
};
+class RoomCombatEffects {
+public:
+ explicit RoomCombatEffects(EntityManager *entityManager);
+
+ void clearHitEffects();
+ bool hasDamagePopups() const { return !_damagePopupStates.empty(); }
+ bool spawnCombatHitEffect(Entity &sourceEntity, const Common::String &followTargetName);
+ void spawnCombatDamagePopup(Entity &targetEntity, const Common::String &followTargetName, int damageAmount);
+ bool syncCombatHitEffects();
+ void drawCombatDamagePopups(Graphics::Screen &screen, bool combatDebugEnabled);
+ bool pruneCombatHitEffects();
+ bool pruneCombatDamagePopups();
+
+private:
+ static void resolveHitEffectAnchor(const Entity &targetEntity, Common::Point &anchorPoint, float &renderZ);
+ static void resolveCombatDamagePopupAnchor(const Entity &targetEntity, Common::Point &anchorPoint);
+
+ EntityManager *_entityManager;
+ Common::Array<RoomHitEffectState> _hitEffectStates;
+ Common::Array<RoomCombatDamagePopupState> _damagePopupStates;
+ uint _nextCombatEffectId = 0;
+};
+
struct CombatLoadoutHudInfo {
uint iconIndex = 0;
int maxVisibleCount = 0;
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index d432c77209f..48af1b68d5e 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -140,7 +140,8 @@ static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rh
RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
InventorySystem &inventory)
- : _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory) {
+ : _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory),
+ _combatEffects(entityManager) {
}
void RoomSession::resetIdleState() {
@@ -769,8 +770,7 @@ bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
refreshedState.audioCommands = Common::move(entryAudioCommands);
if (!loadRoomSceneResources(refreshedState, *_engine.getResources(), _scene))
return false;
- _hitEffectStates.clear();
- _nextCombatEffectId = 0;
+ _combatEffects.clearHitEffects();
if (!_flow.populateRoomSceneEntities(_scene.state, _scene.sceneObjects, _scene.sceneAnimations))
return false;
if (_entityManager) {
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 5a8b496d49e..0da23e58e67 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -48,8 +48,7 @@ public:
RoomPlayerState &getPlayerState() { return _playerState; }
Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
- Common::Array<RoomHitEffectState> &getHitEffectStates() { return _hitEffectStates; }
- uint &getNextCombatEffectId() { return _nextCombatEffectId; }
+ RoomCombatEffects &getCombatEffects() { return _combatEffects; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
RoomMovementInputState &getMovementInput() { return _movementInput; }
Common::String &getPendingRegionName() { return _pendingRegionName; }
@@ -91,8 +90,7 @@ private:
InventorySystem &_inventory;
RoomPlayerState _playerState;
Common::Array<RoomMonsterCombatState> _monsterCombatStates;
- Common::Array<RoomHitEffectState> _hitEffectStates;
- uint _nextCombatEffectId = 0;
+ RoomCombatEffects _combatEffects;
RoomIdleAnimationState _idleState;
RoomMovementInputState _movementInput;
Common::String _pendingRegionName;
Commit: f59e75bbdb89a49a18cf8efcaef2b99da67b4198
https://github.com/scummvm/scummvm/commit/f59e75bbdb89a49a18cf8efcaef2b99da67b4198
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: Add RoomSceneRuntime class to manage scene entities and interactions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/room_scene_runtime.cpp
A engines/harvester/room_scene_runtime.h
engines/harvester/flow.cpp
engines/harvester/module.mk
engines/harvester/room.cpp
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 29138acfb87..6c3fc4fe55b 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -43,6 +43,7 @@
#include "harvester/palette_utils.h"
#include "harvester/resources.h"
#include "harvester/runtime_entity.h"
+#include "harvester/room_scene_runtime.h"
#include "harvester/art.h"
#include "harvester/monster.h"
#include "harvester/player.h"
@@ -98,8 +99,6 @@ static const char *const kDemoIntroductionText =
static const float kTownMapNightPaletteBrightness = 0.6f;
// Native keeps WAIT_BM visible while room construction runs; fast local loads need a short floor.
static const uint32 kTransitionWaitFrameMinMs = 120;
-static const int kRoomNpcAmbientLastFrame = 0x3b;
-static const float kNativeNpcMonsterZExtent = 5.0f;
static const int kTownMapEdgeThreshold = 9;
static const int kTownMapCursorHitExtent = 5;
@@ -742,41 +741,6 @@ static void logSceneObjectSelection(const char *decision, const char *source, co
object.actionTag.c_str(), detail.c_str());
}
-static bool isBackgroundSceneObject(const RoomSetupState &state,
- const ObjectRecord &object, const Entity &entity) {
- if (!state.backgroundObjectName.empty() &&
- object.objectName.equalsIgnoreCase(state.backgroundObjectName)) {
- return true;
- }
-
- return object.initialX == 0 && object.initialY == 0 &&
- entity.getBoundsWidth() == 640 && entity.getBoundsHeight() == 480;
-}
-
-static bool isInteractiveSceneHotspot(const ObjectRecord &object, Script *script) {
- if (object.operatable || !object.actionTag.empty())
- return true;
- if (!script)
- return false;
- if (script->isPickupObject(object))
- return true;
-
- ResolvedText inspectText;
- return script->resolveObjectInspectText(object, inspectText);
-}
-
-static int resolveSceneObjectClass(const RoomSetupState &state,
- const ObjectRecord &object, const Entity *entity, Script *script) {
- if (entity)
- return isBackgroundSceneObject(state, object, *entity)
- ? kRuntimeEntityClassBackground
- : kRuntimeEntityClassObject;
-
- return isInteractiveSceneHotspot(object, script)
- ? kRuntimeEntityClassRectHotspot
- : kRuntimeEntityClassDisabledHotspot;
-}
-
static bool shouldQueueSceneObject(const ObjectRecord &object) {
return object.visible;
}
@@ -2144,35 +2108,10 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
if (!entityManager)
return false;
+ RoomSceneRuntime sceneRuntime(state, _engine.getScript(), _engine.getResources(), entityManager);
Entity *preservedPlayer = entityManager->detachSceneEntityByName(kPlayerActorEntityName);
entityManager->clearSceneEntities(true);
- for (const RegionRecord ®ion : state.roomRegions) {
- const Common::Rect regionBounds = getRegionBounds(region);
- if (regionBounds.isEmpty())
- continue;
-
- Entity *entity = entityManager->spawnSceneHotspotEntity(
- region.regionName, regionBounds, (float)region.minZ);
- if (!entity) {
- debugC(1, kDebugRoom,
- "Harvester: scene region skipped room='%s' region='%s' bounds=(%d,%d)-(%d,%d) z=[%d,%d]",
- state.roomName.c_str(), region.regionName.c_str(),
- regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
- region.minZ, region.maxZ);
- continue;
- }
-
- entity->setClassId(kRuntimeEntityClassRectHotspot19);
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
- if (!region.cursorEnabled)
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
- debugC(1, kDebugRoom,
- "Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
- state.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
- regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
- region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
- }
+ sceneRuntime.populateRoomRegions();
for (const TimerRecord &timer : state.roomTimers) {
Entity *existingTimer = entityManager->findSceneEntityByName(timer.timerName);
if (existingTimer && existingTimer->getClassId() == kRuntimeEntityClassTimer) {
@@ -2196,44 +2135,7 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
state.roomName.c_str(), timer.timerName.c_str(), timer.currentValue, timer.initialValue,
timer.enabled, timer.looping, timer.global);
}
- for (const ObjectRecord &object : drawableObjects) {
- Entity *entity = nullptr;
- const Common::String spritePath = resolveSceneObjectSpritePath(object);
- const Common::Rect hotspotBounds = getRoomObjectHotspotBounds(object);
- if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
- entity = entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
- Common::Point(object.currentX, object.currentY), (float)object.currentZ);
- } else {
- if (!hotspotBounds.isEmpty())
- entity = entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds, (float)object.currentZ);
- }
-
- if (!entity) {
- debugC(1, kDebugRoom,
- "Harvester: scene entity skipped room='%s' object='%s' resolved='%s' bounds=(%d,%d)-(%d,%d) reason='%s'",
- state.roomName.c_str(), object.objectName.c_str(), spritePath.c_str(),
- hotspotBounds.left, hotspotBounds.top, hotspotBounds.right, hotspotBounds.bottom,
- spritePath.empty() || !spritePath.hasSuffixIgnoreCase(".BM")
- ? "no_bitmap_and_no_hotspot_bounds"
- : "spawn_failed");
- debug(1, "Harvester: unable to spawn room object entity '%s' from '%s'",
- object.objectName.c_str(), spritePath.c_str());
- continue;
- }
-
- entity->setClassId(resolveSceneObjectClass(
- state, object, entity->hasFrames() ? entity : nullptr, _engine.getScript()));
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity->setZExtent((float)object.zExtent);
- const Common::Rect entityRect = entity->getScreenRect();
- debugC(1, kDebugRoom,
- "Harvester: scene entity spawned room='%s' object='%s' type='%s' class=0x%x pos=(%d,%d,z=%.2f) rect=(%d,%d)-(%d,%d) sprite='%s' action='%s' operatable=%d ident='%s'",
- state.roomName.c_str(), object.objectName.c_str(),
- entity->hasFrames() ? "bitmap" : "hotspot", entity->getClassId(),
- entity->getX(), entity->getY(), (double)entity->getZ(),
- entityRect.left, entityRect.top, entityRect.right, entityRect.bottom,
- spritePath.c_str(), object.actionTag.c_str(), object.operatable, object.identTextKey.c_str());
- }
+ sceneRuntime.populateRoomObjects(drawableObjects);
for (const AnimRecord &anim : drawableAnimations) {
const bool displayVisible = shouldDisplaySceneAnimation(anim);
if (!entityManager->spawnSceneAnimationEntity(anim.animName, anim.resourcePath,
@@ -2243,76 +2145,8 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
anim.animName.c_str(), anim.resourcePath.c_str());
}
}
- for (const NpcRecord &npc : state.roomNpcs) {
- Entity *entity = entityManager->spawnSceneActorEntity(npc.npcName,
- npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
- if (!entity) {
- debug(1, "Harvester: unable to spawn room npc entity '%s' from '%s'",
- npc.npcName.c_str(), npc.modelPath.c_str());
- continue;
- }
-
- entity->setClassId(kRuntimeEntityClassNpc);
- entity->setZExtent(kNativeNpcMonsterZExtent);
- const bool isCorpse = !npc.active &&
- npc.runtimeSpawned &&
- npc.deathDamageType != 0 &&
- npc.runtimeState >= 0;
- entity->setHitTestMode(isCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
- // Native spawn positions frame 0 before selecting ambient or corpse frame banks.
- if (!applyRoomActorPlacementInternal(state, *entity,
- npc.posX, npc.posY, (float)npc.posZ, nullptr, false)) {
- debug(1, "Harvester: unable to apply room npc placement for '%s'",
- npc.npcName.c_str());
- }
- if (isCorpse) {
- const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
- entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
- entity->setAnimationRate(0);
- entity->setCurrentFrame(corpseFrame);
- entity->setAnimationEnabled(false);
- } else {
- // Native spawn_npc_entity_from_record seeds the passive room-NPC loop as frames 0..0x3b.
- entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
- if (npc.frameDelay > 0)
- entity->setAnimationRate(npc.frameDelay);
- }
- entityManager->reinsertSceneEntity(entity);
- debugC(1, kDebugRoom,
- "Harvester: scene npc spawned room='%s' npc='%s' class=0x%x pos=(%d,%d,z=%.2f) frame_delay=%d model='%s' active=%d visible=%d",
- state.roomName.c_str(), npc.npcName.c_str(), entity->getClassId(),
- entity->getX(), entity->getY(), (double)entity->getZ(),
- npc.frameDelay, npc.modelPath.c_str(), npc.active, npc.visible);
- }
- for (const MonsterRecord &monster : state.roomMonsters) {
- Entity *entity = entityManager->spawnSceneActorEntity(monster.monsterName,
- monster.modelPath, Common::Point(monster.posX, monster.posY), (float)monster.posZ,
- Monster::resolveFacingFrame(monster.facing));
- if (!entity) {
- debug(1, "Harvester: unable to spawn room monster entity '%s' from '%s'",
- monster.monsterName.c_str(), monster.modelPath.c_str());
- continue;
- }
-
- entity->setClassId(kRuntimeEntityClassMonster);
- entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
- entity->setVisible(monster.visible);
- // Native restores a saved monster corpse frame before set_entity_screen_position().
- Monster::applyAnimation(*entity, monster);
- if (!applyRoomActorPlacement(state, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
- debug(1, "Harvester: unable to apply room monster placement for '%s'",
- monster.monsterName.c_str());
- }
- entityManager->reinsertSceneEntity(entity);
- debugC(1, kDebugRoom,
- "Harvester: scene monster spawned room='%s' monster='%s' class=0x%x pos=(%d,%d,z=%.2f) facing=%d hp=%d/%d damage=%d engage=%d damage_type='%s' model='%s' active=%d visible=%d",
- state.roomName.c_str(), monster.monsterName.c_str(), entity->getClassId(),
- entity->getX(), entity->getY(), (double)entity->getZ(), monster.facing,
- monster.currentHitPoints, monster.initialHitPoints,
- monster.damageAmount, monster.engageDistance, Player::describeCombatDamageType(monster.damageType),
- monster.modelPath.c_str(), monster.active, monster.visible);
- }
+ sceneRuntime.populateRoomNpcs();
+ sceneRuntime.populateRoomMonsters();
if (state.hasEntrance) {
const int playerFrame = Player::resolveFacingFrame(state.playerFacing);
Script *script = _engine.getScript();
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index af951392791..c3cc27e9b8c 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -19,6 +19,7 @@ MODULE_OBJS = \
resources.o \
room_combat.o \
room_interaction.o \
+ room_scene_runtime.o \
room_session.o \
room.o \
runtime_entity.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 82eac8f328d..3f0d3ecc157 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -436,6 +436,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monsterCombatStates.resize(scene.state.roomMonsters.size());
Common::Array<RoomNpcCombatState> npcCombatStates;
npcCombatStates.resize(scene.state.roomNpcs.size());
+ RoomSceneRuntime &sceneRuntime = session.getSceneRuntime();
RoomCombatEffects &combatEffects = session.getCombatEffects();
ResolvedText inspectText;
Common::String currentInteractionPromptText;
@@ -1373,7 +1374,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
npcEntity->setHitTestMode(kRuntimeEntityHitTestNone);
npcEntity->setVisible(true);
} else {
- session.removeSceneEntityByName(npc.npcName);
+ sceneRuntime.removeSceneEntityByName(npc.npcName);
}
npc.active = false;
npc.visible = false;
@@ -2274,7 +2275,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
script->addRuntimeObjectToInventory(object.objectName);
const Common::String inventoryOwner(kInventoryOwnerName);
hideSceneObject(object.objectName, &inventoryOwner);
- session.removeSceneEntityByName(object.objectName);
+ sceneRuntime.removeSceneEntityByName(object.objectName);
bool didTransition = false;
if (shouldDispatchPickupActionOnDirectInventoryTransfer(object)) {
@@ -2320,7 +2321,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
script->setRuntimeObjectVisible(object.currentOwnerOrRoom, object.objectName, false);
hideSceneObject(object.objectName, nullptr);
- session.removeSceneEntityByName(object.objectName);
+ sceneRuntime.removeSceneEntityByName(object.objectName);
bool didTransition = false;
if (shouldDispatchPickupActionOnCarryStart(object)) {
diff --git a/engines/harvester/room_scene_runtime.cpp b/engines/harvester/room_scene_runtime.cpp
new file mode 100644
index 00000000000..cbfdd79ba87
--- /dev/null
+++ b/engines/harvester/room_scene_runtime.cpp
@@ -0,0 +1,428 @@
+/* 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/room_scene_runtime.h"
+
+#include "harvester/detection.h"
+#include "harvester/monster.h"
+#include "harvester/player.h"
+#include "harvester/room_combat.h"
+
+namespace Harvester {
+
+static const char *const kInventoryOwnerName = "INVENTORY";
+
+static bool isBackgroundSceneObject(const RoomSetupState &state,
+ const ObjectRecord &object, const Entity &entity) {
+ if (!state.backgroundObjectName.empty() &&
+ object.objectName.equalsIgnoreCase(state.backgroundObjectName)) {
+ return true;
+ }
+
+ return object.initialX == 0 && object.initialY == 0 &&
+ entity.getBoundsWidth() == 640 && entity.getBoundsHeight() == 480;
+}
+
+static bool isInteractiveSceneHotspot(const ObjectRecord &object, Script *script) {
+ if (object.operatable || !object.actionTag.empty())
+ return true;
+ if (!script)
+ return false;
+ if (script->isPickupObject(object))
+ return true;
+
+ ResolvedText inspectText;
+ return script->resolveObjectInspectText(object, inspectText);
+}
+
+static int resolveSceneObjectClass(const RoomSetupState &state,
+ const ObjectRecord &object, const Entity *entity, Script *script) {
+ if (entity)
+ return isBackgroundSceneObject(state, object, *entity)
+ ? kRuntimeEntityClassBackground
+ : kRuntimeEntityClassObject;
+
+ return isInteractiveSceneHotspot(object, script)
+ ? kRuntimeEntityClassRectHotspot
+ : kRuntimeEntityClassDisabledHotspot;
+}
+
+RoomSceneRuntime::RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager)
+ : _state(state), _script(script), _resources(resources), _entityManager(entityManager) {
+ debugC(1, kDebugRoom, "Harvester: created room scene runtime room='%s' entities=%d",
+ _state.roomName.c_str(), _entityManager != nullptr);
+}
+
+bool RoomSceneRuntime::loadSceneResources(const RoomSetupState &state, RoomSceneResources &scene) const {
+ return _resources && loadRoomSceneResources(state, *_resources, scene);
+}
+
+void RoomSceneRuntime::removeSceneEntityByName(const Common::String &entityName) {
+ if (!_entityManager)
+ return;
+
+ Entity *entity = _entityManager->detachSceneEntityByName(entityName);
+ delete entity;
+}
+
+Common::Rect RoomSceneRuntime::getSceneObjectBounds(const ObjectRecord &object) {
+ if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
+ return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
+ return Common::Rect();
+}
+
+Common::String RoomSceneRuntime::resolveSceneObjectSpritePathLocal(const ObjectRecord &object) {
+ const bool atInitialPlacement = object.currentX == object.initialX &&
+ object.currentY == object.initialY &&
+ object.currentOwnerOrRoom.equalsIgnoreCase(object.initialOwnerOrRoom);
+ if (!object.altSpritePath.empty() &&
+ (!atInitialPlacement || object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName)))
+ return object.altSpritePath;
+ return object.spritePath;
+}
+
+bool RoomSceneRuntime::isInteractiveSceneHotspot(const ObjectRecord &object) const {
+ if (object.operatable || !object.actionTag.empty())
+ return true;
+
+ Script *script = _script;
+ if (!script)
+ return false;
+ if (script->isPickupObject(object))
+ return true;
+
+ ResolvedText inspectText;
+ return script->resolveObjectInspectText(object, inspectText);
+}
+
+RuntimeEntityClass RoomSceneRuntime::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
+ if (entity) {
+ return ((!_state.backgroundObjectName.empty() &&
+ object.objectName.equalsIgnoreCase(_state.backgroundObjectName)) ||
+ (object.initialX == 0 && object.initialY == 0 &&
+ entity->getBoundsWidth() == 640 && entity->getBoundsHeight() == 480))
+ ? kRuntimeEntityClassBackground
+ : kRuntimeEntityClassObject;
+ }
+
+ return isInteractiveSceneHotspot(object)
+ ? kRuntimeEntityClassRectHotspot
+ : kRuntimeEntityClassDisabledHotspot;
+}
+
+void RoomSceneRuntime::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
+ if (!_entityManager)
+ return;
+
+ const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
+ const Common::Rect hotspotBounds = getSceneObjectBounds(object);
+ Entity *entity = nullptr;
+ if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
+ entity = _entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
+ Common::Point(object.currentX, object.currentY), (float)object.currentZ);
+ } else if (!hotspotBounds.isEmpty()) {
+ entity = _entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds,
+ (float)object.currentZ);
+ }
+ if (!entity)
+ return;
+
+ entity->setClassId(resolveSceneObjectClassLocal(
+ object, entity->hasFrames() ? entity : nullptr));
+ entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setZExtent((float)object.zExtent);
+ debugC(3, kDebugRoom, "Harvester: scene object spawned object='%s' sprite='%s' class=0x%x",
+ object.objectName.c_str(), spritePath.c_str(), entity->getClassId());
+}
+
+Common::Rect RoomSceneRuntime::getSceneRegionBounds(const RegionRecord ®ion) {
+ if (region.right > region.left && region.bottom > region.top)
+ return Common::Rect(region.left, region.top, region.right + 1, region.bottom + 1);
+ return Common::Rect();
+}
+
+void RoomSceneRuntime::spawnSceneRegionEntityFromRecord(const RegionRecord ®ion) {
+ if (!_entityManager)
+ return;
+
+ const Common::Rect bounds = getSceneRegionBounds(region);
+ if (bounds.isEmpty())
+ return;
+
+ Entity *entity = _entityManager->spawnSceneHotspotEntity(
+ region.regionName, bounds, (float)region.minZ);
+ if (!entity)
+ return;
+
+ entity->setClassId(kRuntimeEntityClassRectHotspot19);
+ entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
+ if (!region.cursorEnabled)
+ entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ debugC(1, kDebugRoom,
+ "Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
+ region.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
+ bounds.left, bounds.top, bounds.right, bounds.bottom,
+ region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
+}
+
+bool RoomSceneRuntime::shouldSpawnRoomNpcEntity(const NpcRecord &npc) {
+ return npc.visible && !npc.deathOrMonsterfyFlag;
+}
+
+bool RoomSceneRuntime::shouldSpawnRoomMonsterEntity(const MonsterRecord &monster) {
+ return monster.visible;
+}
+
+bool RoomSceneRuntime::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc) {
+ int width = 0;
+ int height = 0;
+ int xOffset = 0;
+ int yOffset = 0;
+ if (!entity.getCurrentFrameMetrics(width, height, xOffset, yOffset))
+ return false;
+
+ const float renderZ =
+ (float)npc.posZ - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
+ entity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity.setPosition(npc.posX - xOffset - width / 2, npc.posY - height - yOffset, renderZ);
+ return true;
+}
+
+Entity *RoomSceneRuntime::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
+ const bool shouldSpawnNpc = shouldSpawnRoomNpcEntity(npc);
+ const bool preservedCorpse = !npc.active &&
+ npc.runtimeSpawned &&
+ npc.deathDamageType != 0 &&
+ npc.runtimeState >= 0;
+ if (!_entityManager || !shouldSpawnNpc)
+ return nullptr;
+
+ Entity *entity = _entityManager->spawnSceneActorEntity(
+ npc.npcName, npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
+ if (!entity)
+ return nullptr;
+
+ entity->setClassId(kRuntimeEntityClassNpc);
+ entity->setZExtent(kNativeNpcMonsterZExtent);
+ entity->setHitTestMode(preservedCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+ entity->setVisible(true);
+ if (!applyRoomNpcPlacement(*entity, npc)) {
+ removeSceneEntityByName(npc.npcName);
+ return nullptr;
+ }
+ if (preservedCorpse) {
+ const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
+ entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
+ entity->setAnimationRate(0);
+ entity->setCurrentFrame(corpseFrame);
+ entity->setAnimationEnabled(false);
+ } else {
+ entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
+ entity->setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
+ }
+ _entityManager->reinsertSceneEntity(entity);
+ return entity;
+}
+
+Entity *RoomSceneRuntime::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
+ if (!_entityManager || !shouldSpawnRoomMonsterEntity(monster))
+ return nullptr;
+
+ Entity *entity = _entityManager->spawnSceneActorEntity(
+ monster.monsterName, monster.modelPath,
+ Common::Point(monster.posX, monster.posY), (float)monster.posZ,
+ Monster::resolveFacingFrame(monster.facing));
+ if (!entity)
+ return nullptr;
+
+ entity->setClassId(kRuntimeEntityClassMonster);
+ entity->setZExtent(kNativeNpcMonsterZExtent);
+ entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setVisible(monster.visible);
+ Monster::applyAnimation(*entity, monster);
+ if (!applyRoomActorPlacement(roomState, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
+ removeSceneEntityByName(monster.monsterName);
+ return nullptr;
+ }
+ _entityManager->reinsertSceneEntity(entity);
+ return entity;
+}
+
+void RoomSceneRuntime::populateRoomRegions() {
+ if (!_entityManager)
+ return;
+
+ for (const RegionRecord ®ion : _state.roomRegions) {
+ const Common::Rect regionBounds = getSceneRegionBounds(region);
+ if (regionBounds.isEmpty())
+ continue;
+
+ Entity *entity = _entityManager->spawnSceneHotspotEntity(
+ region.regionName, regionBounds, (float)region.minZ);
+ if (!entity) {
+ debugC(1, kDebugRoom,
+ "Harvester: scene region skipped room='%s' region='%s' bounds=(%d,%d)-(%d,%d) z=[%d,%d]",
+ _state.roomName.c_str(), region.regionName.c_str(),
+ regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
+ region.minZ, region.maxZ);
+ continue;
+ }
+
+ entity->setClassId(kRuntimeEntityClassRectHotspot19);
+ entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
+ if (!region.cursorEnabled)
+ entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ debugC(1, kDebugRoom,
+ "Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
+ _state.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
+ regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
+ region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
+ }
+}
+
+void RoomSceneRuntime::populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects) {
+ if (!_entityManager)
+ return;
+
+ for (const ObjectRecord &object : drawableObjects) {
+ Entity *entity = nullptr;
+ const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
+ const Common::Rect hotspotBounds = getRoomObjectHotspotBounds(object);
+ if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
+ entity = _entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
+ Common::Point(object.currentX, object.currentY), (float)object.currentZ);
+ } else {
+ if (!hotspotBounds.isEmpty())
+ entity = _entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds, (float)object.currentZ);
+ }
+
+ if (!entity) {
+ debugC(1, kDebugRoom,
+ "Harvester: scene entity skipped room='%s' object='%s' resolved='%s' bounds=(%d,%d)-(%d,%d) reason='%s'",
+ _state.roomName.c_str(), object.objectName.c_str(), spritePath.c_str(),
+ hotspotBounds.left, hotspotBounds.top, hotspotBounds.right, hotspotBounds.bottom,
+ spritePath.empty() || !spritePath.hasSuffixIgnoreCase(".BM")
+ ? "no_bitmap_and_no_hotspot_bounds"
+ : "spawn_failed");
+ debug(1, "Harvester: unable to spawn room object entity '%s' from '%s'",
+ object.objectName.c_str(), spritePath.c_str());
+ continue;
+ }
+
+ entity->setClassId(resolveSceneObjectClass(
+ _state, object, entity->hasFrames() ? entity : nullptr, _script));
+ entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setZExtent((float)object.zExtent);
+ const Common::Rect entityRect = entity->getScreenRect();
+ debugC(1, kDebugRoom,
+ "Harvester: scene entity spawned room='%s' object='%s' type='%s' class=0x%x pos=(%d,%d,z=%.2f) rect=(%d,%d)-(%d,%d) sprite='%s' action='%s' operatable=%d ident='%s'",
+ _state.roomName.c_str(), object.objectName.c_str(),
+ entity->hasFrames() ? "bitmap" : "hotspot", entity->getClassId(),
+ entity->getX(), entity->getY(), (double)entity->getZ(),
+ entityRect.left, entityRect.top, entityRect.right, entityRect.bottom,
+ spritePath.c_str(), object.actionTag.c_str(), object.operatable, object.identTextKey.c_str());
+ }
+}
+
+void RoomSceneRuntime::populateRoomNpcs() {
+ if (!_entityManager)
+ return;
+
+ for (const NpcRecord &npc : _state.roomNpcs) {
+ Entity *entity = _entityManager->spawnSceneActorEntity(npc.npcName,
+ npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
+ if (!entity) {
+ debug(1, "Harvester: unable to spawn room npc entity '%s' from '%s'",
+ npc.npcName.c_str(), npc.modelPath.c_str());
+ continue;
+ }
+
+ entity->setClassId(kRuntimeEntityClassNpc);
+ entity->setZExtent(kNativeNpcMonsterZExtent);
+ const bool isCorpse = !npc.active &&
+ npc.runtimeSpawned &&
+ npc.deathDamageType != 0 &&
+ npc.runtimeState >= 0;
+ entity->setHitTestMode(isCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+ // Native spawn positions frame 0 before selecting ambient or corpse frame banks.
+ if (!applyRoomNpcPlacement(*entity, npc)) {
+ debug(1, "Harvester: unable to apply room npc placement for '%s'",
+ npc.npcName.c_str());
+ }
+ if (isCorpse) {
+ const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
+ entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
+ entity->setAnimationRate(0);
+ entity->setCurrentFrame(corpseFrame);
+ entity->setAnimationEnabled(false);
+ } else {
+ // Native spawn_npc_entity_from_record seeds the passive room-NPC loop as frames 0..0x3b.
+ entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
+ if (npc.frameDelay > 0)
+ entity->setAnimationRate(npc.frameDelay);
+ }
+ _entityManager->reinsertSceneEntity(entity);
+ debugC(1, kDebugRoom,
+ "Harvester: scene npc spawned room='%s' npc='%s' class=0x%x pos=(%d,%d,z=%.2f) frame_delay=%d model='%s' active=%d visible=%d",
+ _state.roomName.c_str(), npc.npcName.c_str(), entity->getClassId(),
+ entity->getX(), entity->getY(), (double)entity->getZ(),
+ npc.frameDelay, npc.modelPath.c_str(), npc.active, npc.visible);
+ }
+}
+
+void RoomSceneRuntime::populateRoomMonsters() {
+ if (!_entityManager)
+ return;
+
+ for (const MonsterRecord &monster : _state.roomMonsters) {
+ Entity *entity = _entityManager->spawnSceneActorEntity(monster.monsterName,
+ monster.modelPath, Common::Point(monster.posX, monster.posY), (float)monster.posZ,
+ Monster::resolveFacingFrame(monster.facing));
+ if (!entity) {
+ debug(1, "Harvester: unable to spawn room monster entity '%s' from '%s'",
+ monster.monsterName.c_str(), monster.modelPath.c_str());
+ continue;
+ }
+
+ entity->setClassId(kRuntimeEntityClassMonster);
+ entity->setZExtent(kNativeNpcMonsterZExtent);
+ entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setVisible(monster.visible);
+ // Native restores a saved monster corpse frame before set_entity_screen_position().
+ Monster::applyAnimation(*entity, monster);
+ if (!applyRoomActorPlacement(_state, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
+ debug(1, "Harvester: unable to apply room monster placement for '%s'",
+ monster.monsterName.c_str());
+ }
+ _entityManager->reinsertSceneEntity(entity);
+ debugC(1, kDebugRoom,
+ "Harvester: scene monster spawned room='%s' monster='%s' class=0x%x pos=(%d,%d,z=%.2f) facing=%d hp=%d/%d damage=%d engage=%d damage_type='%s' model='%s' active=%d visible=%d",
+ _state.roomName.c_str(), monster.monsterName.c_str(), entity->getClassId(),
+ entity->getX(), entity->getY(), (double)entity->getZ(), monster.facing,
+ monster.currentHitPoints, monster.initialHitPoints,
+ monster.damageAmount, monster.engageDistance, Player::describeCombatDamageType(monster.damageType),
+ monster.modelPath.c_str(), monster.active, monster.visible);
+ }
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/room_scene_runtime.h b/engines/harvester/room_scene_runtime.h
new file mode 100644
index 00000000000..ad997302134
--- /dev/null
+++ b/engines/harvester/room_scene_runtime.h
@@ -0,0 +1,66 @@
+/* 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_ROOM_SCENE_RUNTIME_H
+#define HARVESTER_ROOM_SCENE_RUNTIME_H
+
+#include "harvester/room_support.h"
+#include "harvester/runtime_entity.h"
+
+namespace Harvester {
+
+// Borrows the materialized room state; live entities remain owned by EntityManager.
+class RoomSceneRuntime {
+public:
+ RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager);
+
+ bool loadSceneResources(const RoomSetupState &state, RoomSceneResources &scene) const;
+ // Initial population skips failed spawns; in-place callers handle null actor results.
+ void populateRoomRegions();
+ void populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects);
+ void populateRoomNpcs();
+ void populateRoomMonsters();
+
+ void removeSceneEntityByName(const Common::String &entityName);
+ static Common::Rect getSceneObjectBounds(const ObjectRecord &object);
+ static Common::String resolveSceneObjectSpritePathLocal(const ObjectRecord &object);
+ void spawnSceneObjectEntityFromRecord(const ObjectRecord &object);
+ void spawnSceneRegionEntityFromRecord(const RegionRecord ®ion);
+ static bool shouldSpawnRoomNpcEntity(const NpcRecord &npc);
+ static bool shouldSpawnRoomMonsterEntity(const MonsterRecord &monster);
+ Entity *spawnSceneNpcEntityFromRecord(const NpcRecord &npc);
+ Entity *spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster);
+
+private:
+ bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
+ RuntimeEntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
+ static Common::Rect getSceneRegionBounds(const RegionRecord ®ion);
+ static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
+
+ RoomSetupState &_state;
+ Script *_script;
+ ResourceManager *_resources;
+ EntityManager *_entityManager;
+};
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_ROOM_SCENE_RUNTIME_H
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index 48af1b68d5e..e47bec38136 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -32,8 +32,6 @@
namespace Harvester {
-static const char *const kInventoryOwnerName = "INVENTORY";
-
static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
return lhs.left == rhs.left && lhs.top == rhs.top &&
lhs.right == rhs.right && lhs.bottom == rhs.bottom;
@@ -121,8 +119,8 @@ static bool sameSceneObjectState(const ObjectRecord &lhs, const ObjectRecord &rh
lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
- RoomSession::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(RoomSession::resolveSceneObjectSpritePathLocal(rhs)) &&
- sameRect(RoomSession::getSceneObjectBounds(lhs), RoomSession::getSceneObjectBounds(rhs));
+ RoomSceneRuntime::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(RoomSceneRuntime::resolveSceneObjectSpritePathLocal(rhs)) &&
+ sameRect(RoomSceneRuntime::getSceneObjectBounds(lhs), RoomSceneRuntime::getSceneObjectBounds(rhs));
}
static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rhs) {
@@ -141,7 +139,7 @@ static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rh
RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
InventorySystem &inventory)
: _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory),
- _combatEffects(entityManager) {
+ _sceneRuntime(scene.state, engine.getScript(), engine.getResources(), entityManager), _combatEffects(entityManager) {
}
void RoomSession::resetIdleState() {
@@ -308,198 +306,6 @@ void RoomSession::syncGlobalTimerEntities(const Common::Array<TimerRecord> &prev
}
}
-void RoomSession::removeSceneEntityByName(const Common::String &entityName) {
- if (!_entityManager)
- return;
-
- Entity *entity = _entityManager->detachSceneEntityByName(entityName);
- delete entity;
-}
-
-Common::Rect RoomSession::getSceneObjectBounds(const ObjectRecord &object) {
- if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
- return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
- return Common::Rect();
-}
-
-Common::String RoomSession::resolveSceneObjectSpritePathLocal(const ObjectRecord &object) {
- const bool atInitialPlacement = object.currentX == object.initialX &&
- object.currentY == object.initialY &&
- object.currentOwnerOrRoom.equalsIgnoreCase(object.initialOwnerOrRoom);
- if (!object.altSpritePath.empty() &&
- (!atInitialPlacement || object.currentOwnerOrRoom.equalsIgnoreCase(kInventoryOwnerName)))
- return object.altSpritePath;
- return object.spritePath;
-}
-
-bool RoomSession::isInteractiveSceneHotspot(const ObjectRecord &object) const {
- if (object.operatable || !object.actionTag.empty())
- return true;
-
- Script *script = _engine.getScript();
- if (!script)
- return false;
- if (script->isPickupObject(object))
- return true;
-
- ResolvedText inspectText;
- return script->resolveObjectInspectText(object, inspectText);
-}
-
-RuntimeEntityClass RoomSession::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
- if (entity) {
- return ((!_scene.state.backgroundObjectName.empty() &&
- object.objectName.equalsIgnoreCase(_scene.state.backgroundObjectName)) ||
- (object.initialX == 0 && object.initialY == 0 &&
- entity->getBoundsWidth() == 640 && entity->getBoundsHeight() == 480))
- ? kRuntimeEntityClassBackground
- : kRuntimeEntityClassObject;
- }
-
- return isInteractiveSceneHotspot(object)
- ? kRuntimeEntityClassRectHotspot
- : kRuntimeEntityClassDisabledHotspot;
-}
-
-void RoomSession::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
- if (!_entityManager)
- return;
-
- const Common::String spritePath = resolveSceneObjectSpritePathLocal(object);
- const Common::Rect hotspotBounds = getSceneObjectBounds(object);
- Entity *entity = nullptr;
- if (!spritePath.empty() && spritePath.hasSuffixIgnoreCase(".BM")) {
- entity = _entityManager->spawnSceneBitmapEntity(object.objectName, spritePath,
- Common::Point(object.currentX, object.currentY), (float)object.currentZ);
- } else if (!hotspotBounds.isEmpty()) {
- entity = _entityManager->spawnSceneHotspotEntity(object.objectName, hotspotBounds,
- (float)object.currentZ);
- }
- if (!entity)
- return;
-
- entity->setClassId(resolveSceneObjectClassLocal(
- object, entity->hasFrames() ? entity : nullptr));
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity->setZExtent((float)object.zExtent);
- debugC(3, kDebugRoom, "Harvester: scene object spawned object='%s' sprite='%s' class=0x%x",
- object.objectName.c_str(), spritePath.c_str(), entity->getClassId());
-}
-
-Common::Rect RoomSession::getSceneRegionBounds(const RegionRecord ®ion) {
- if (region.right > region.left && region.bottom > region.top)
- return Common::Rect(region.left, region.top, region.right + 1, region.bottom + 1);
- return Common::Rect();
-}
-
-void RoomSession::spawnSceneRegionEntityFromRecord(const RegionRecord ®ion) {
- if (!_entityManager)
- return;
-
- const Common::Rect bounds = getSceneRegionBounds(region);
- if (bounds.isEmpty())
- return;
-
- Entity *entity = _entityManager->spawnSceneHotspotEntity(
- region.regionName, bounds, (float)region.minZ);
- if (!entity)
- return;
-
- entity->setClassId(kRuntimeEntityClassRectHotspot19);
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
- if (!region.cursorEnabled)
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
- debugC(1, kDebugRoom,
- "Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
- region.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
- bounds.left, bounds.top, bounds.right, bounds.bottom,
- region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
-}
-
-bool RoomSession::shouldSpawnRoomNpcEntity(const NpcRecord &npc) {
- return npc.visible && !npc.deathOrMonsterfyFlag;
-}
-
-bool RoomSession::shouldSpawnRoomMonsterEntity(const MonsterRecord &monster) {
- return monster.visible;
-}
-
-bool RoomSession::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc) {
- int width = 0;
- int height = 0;
- int xOffset = 0;
- int yOffset = 0;
- if (!entity.getCurrentFrameMetrics(width, height, xOffset, yOffset))
- return false;
-
- const float renderZ =
- (float)npc.posZ - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
- entity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
- entity.setPosition(npc.posX - xOffset - width / 2, npc.posY - height - yOffset, renderZ);
- return true;
-}
-
-Entity *RoomSession::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
- const bool shouldSpawnNpc = shouldSpawnRoomNpcEntity(npc);
- const bool preservedCorpse = !npc.active &&
- npc.runtimeSpawned &&
- npc.deathDamageType != 0 &&
- npc.runtimeState >= 0;
- if (!_entityManager || !shouldSpawnNpc)
- return nullptr;
-
- Entity *entity = _entityManager->spawnSceneActorEntity(
- npc.npcName, npc.modelPath, Common::Point(npc.posX, npc.posY), (float)npc.posZ, 0);
- if (!entity)
- return nullptr;
-
- entity->setClassId(kRuntimeEntityClassNpc);
- entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(preservedCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
- entity->setVisible(true);
- if (!applyRoomNpcPlacement(*entity, npc)) {
- removeSceneEntityByName(npc.npcName);
- return nullptr;
- }
- if (preservedCorpse) {
- const int corpseFrame = MIN(entity->getLastFrame(), npc.runtimeState);
- entity->setAnimationFrameRange(corpseFrame, corpseFrame, false);
- entity->setAnimationRate(0);
- entity->setCurrentFrame(corpseFrame);
- entity->setAnimationEnabled(false);
- } else {
- entity->setAnimationFrameRange(0, MIN(entity->getLastFrame(), kRoomNpcAmbientLastFrame), true);
- entity->setAnimationRate(npc.frameDelay > 0 ? npc.frameDelay : 0);
- }
- _entityManager->reinsertSceneEntity(entity);
- return entity;
-}
-
-Entity *RoomSession::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
- if (!_entityManager || !shouldSpawnRoomMonsterEntity(monster))
- return nullptr;
-
- Entity *entity = _entityManager->spawnSceneActorEntity(
- monster.monsterName, monster.modelPath,
- Common::Point(monster.posX, monster.posY), (float)monster.posZ,
- Monster::resolveFacingFrame(monster.facing));
- if (!entity)
- return nullptr;
-
- entity->setClassId(kRuntimeEntityClassMonster);
- entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
- entity->setVisible(monster.visible);
- Monster::applyAnimation(*entity, monster);
- if (!applyRoomActorPlacement(roomState, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
- removeSceneEntityByName(monster.monsterName);
- return nullptr;
- }
- _entityManager->reinsertSceneEntity(entity);
- return entity;
-}
-
bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
Script *script = _engine.getScript();
ResourceManager *resources = _engine.getResources();
@@ -520,7 +326,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
updatedState.audioCommands = Common::move(entryAudioCommands);
RoomSceneResources updatedScene;
- if (!loadRoomSceneResources(updatedState, *resources, updatedScene))
+ if (!_sceneRuntime.loadSceneResources(updatedState, updatedScene))
return false;
Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates;
updatedMonsterCombatStates.resize(updatedState.roomMonsters.size());
@@ -539,7 +345,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
for (const ObjectRecord &object : _scene.sceneObjects) {
if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
- removeSceneEntityByName(object.objectName);
+ _sceneRuntime.removeSceneEntityByName(object.objectName);
}
for (const ObjectRecord &object : updatedScene.sceneObjects) {
const ObjectRecord *previous = findSceneObjectByName(_scene.sceneObjects, object.objectName);
@@ -547,13 +353,13 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
if (!changed)
continue;
- removeSceneEntityByName(object.objectName);
- spawnSceneObjectEntityFromRecord(object);
+ _sceneRuntime.removeSceneEntityByName(object.objectName);
+ _sceneRuntime.spawnSceneObjectEntityFromRecord(object);
}
for (const RegionRecord ®ion : _scene.sceneRegions) {
if (!findSceneRegionByName(updatedScene.sceneRegions, region.regionName))
- removeSceneEntityByName(region.regionName);
+ _sceneRuntime.removeSceneEntityByName(region.regionName);
}
for (const RegionRecord ®ion : updatedScene.sceneRegions) {
const RegionRecord *previous =
@@ -562,8 +368,8 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
if (!changed)
continue;
- removeSceneEntityByName(region.regionName);
- spawnSceneRegionEntityFromRecord(region);
+ _sceneRuntime.removeSceneEntityByName(region.regionName);
+ _sceneRuntime.spawnSceneRegionEntityFromRecord(region);
}
for (const TimerRecord &timer : _scene.state.roomTimers) {
@@ -575,7 +381,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
}
}
if (!found)
- removeSceneEntityByName(timer.timerName);
+ _sceneRuntime.removeSceneEntityByName(timer.timerName);
}
for (const TimerRecord &timer : updatedState.roomTimers) {
const TimerRecord *previous = nullptr;
@@ -594,7 +400,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
if (!changed)
continue;
- removeSceneEntityByName(timer.timerName);
+ _sceneRuntime.removeSceneEntityByName(timer.timerName);
if (_entityManager) {
(void)_entityManager->spawnSceneTimerEntity(
timer.timerName, timer.initialValue, timer.currentValue,
@@ -615,7 +421,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
? _entityManager->findSceneEntityByName(anim.animName)
: nullptr;
if (!isDisplayed) {
- removeSceneEntityByName(anim.animName);
+ _sceneRuntime.removeSceneEntityByName(anim.animName);
continue;
}
@@ -629,7 +435,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
previous->pingPong != anim.pingPong ||
previous->backward != anim.backward;
if (needsRespawn) {
- removeSceneEntityByName(anim.animName);
+ _sceneRuntime.removeSceneEntityByName(anim.animName);
if (_entityManager) {
entity = _entityManager->spawnSceneAnimationEntity(
anim.animName, anim.resourcePath, Common::Point(anim.x, anim.y),
@@ -656,7 +462,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
for (const AnimRecord &anim : _scene.state.roomAnimations) {
if (!findRoomAnimByNameConst(updatedState.roomAnimations, anim.animName))
- removeSceneEntityByName(anim.animName);
+ _sceneRuntime.removeSceneEntityByName(anim.animName);
}
for (const NpcRecord &npc : _scene.state.roomNpcs) {
@@ -669,7 +475,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
_entityManager->findSceneEntityByName(npc.npcName);
const bool keepEntity =
updatedNpc &&
- shouldSpawnRoomNpcEntity(*updatedNpc) &&
+ _sceneRuntime.shouldSpawnRoomNpcEntity(*updatedNpc) &&
(sameNpcEntityState(npc, *updatedNpc) ||
isQueuedLiveNpcDeathMutation(npc, *updatedNpc));
if (keepCurrentRoomDeathEntity) {
@@ -679,10 +485,10 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
continue;
}
if (!keepEntity)
- removeSceneEntityByName(npc.npcName);
+ _sceneRuntime.removeSceneEntityByName(npc.npcName);
}
for (const NpcRecord &npc : updatedState.roomNpcs) {
- if (!shouldSpawnRoomNpcEntity(npc))
+ if (!_sceneRuntime.shouldSpawnRoomNpcEntity(npc))
continue;
Entity *entity = _entityManager
@@ -701,8 +507,8 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
if (!needsRespawn)
continue;
- removeSceneEntityByName(npc.npcName);
- if (!spawnSceneNpcEntityFromRecord(npc))
+ _sceneRuntime.removeSceneEntityByName(npc.npcName);
+ if (!_sceneRuntime.spawnSceneNpcEntityFromRecord(npc))
return false;
}
@@ -711,13 +517,13 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
findRoomMonsterByNameConst(updatedState.roomMonsters, monster.monsterName);
const bool keepEntity =
updatedMonster &&
- shouldSpawnRoomMonsterEntity(*updatedMonster) &&
+ _sceneRuntime.shouldSpawnRoomMonsterEntity(*updatedMonster) &&
sameMonsterEntityState(monster, *updatedMonster);
if (!keepEntity)
- removeSceneEntityByName(monster.monsterName);
+ _sceneRuntime.removeSceneEntityByName(monster.monsterName);
}
for (const MonsterRecord &monster : updatedState.roomMonsters) {
- if (!shouldSpawnRoomMonsterEntity(monster))
+ if (!_sceneRuntime.shouldSpawnRoomMonsterEntity(monster))
continue;
Entity *entity = _entityManager
@@ -730,8 +536,8 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
if (!needsRespawn)
continue;
- removeSceneEntityByName(monster.monsterName);
- if (!spawnSceneMonsterEntityFromRecord(updatedState, monster))
+ _sceneRuntime.removeSceneEntityByName(monster.monsterName);
+ if (!_sceneRuntime.spawnSceneMonsterEntityFromRecord(updatedState, monster))
return false;
}
@@ -768,7 +574,7 @@ bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
}
refreshedState.audioCommands = Common::move(entryAudioCommands);
- if (!loadRoomSceneResources(refreshedState, *_engine.getResources(), _scene))
+ if (!_sceneRuntime.loadSceneResources(refreshedState, _scene))
return false;
_combatEffects.clearHitEffects();
if (!_flow.populateRoomSceneEntities(_scene.state, _scene.sceneObjects, _scene.sceneAnimations))
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 0da23e58e67..72a7d17c6df 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -23,6 +23,7 @@
#define HARVESTER_ROOM_SESSION_H
#include "harvester/room_combat.h"
+#include "harvester/room_scene_runtime.h"
#include "harvester/room_support.h"
#include "harvester/runtime_entity.h"
@@ -48,6 +49,7 @@ public:
RoomPlayerState &getPlayerState() { return _playerState; }
Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
+ RoomSceneRuntime &getSceneRuntime() { return _sceneRuntime; }
RoomCombatEffects &getCombatEffects() { return _combatEffects; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
RoomMovementInputState &getMovementInput() { return _movementInput; }
@@ -65,22 +67,7 @@ public:
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers = false);
- void removeSceneEntityByName(const Common::String &entityName);
- static Common::Rect getSceneObjectBounds(const ObjectRecord &object);
- static Common::String resolveSceneObjectSpritePathLocal(const ObjectRecord &object);
- void spawnSceneObjectEntityFromRecord(const ObjectRecord &object);
- void spawnSceneRegionEntityFromRecord(const RegionRecord ®ion);
- static bool shouldSpawnRoomNpcEntity(const NpcRecord &npc);
- static bool shouldSpawnRoomMonsterEntity(const MonsterRecord &monster);
- Entity *spawnSceneNpcEntityFromRecord(const NpcRecord &npc);
- Entity *spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster);
-
private:
- bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
- RuntimeEntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
- static Common::Rect getSceneRegionBounds(const RegionRecord ®ion);
- static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
-
bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
HarvesterEngine &_engine;
@@ -88,6 +75,7 @@ private:
RoomSceneResources &_scene;
EntityManager *_entityManager;
InventorySystem &_inventory;
+ RoomSceneRuntime _sceneRuntime;
RoomPlayerState _playerState;
Common::Array<RoomMonsterCombatState> _monsterCombatStates;
RoomCombatEffects _combatEffects;
Commit: 36b5ca56de2ac6a6771cbe7070e83b52e4989ae1
https://github.com/scummvm/scummvm/commit/36b5ca56de2ac6a6771cbe7070e83b52e4989ae1
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: Refactor RoomSession and RoomSceneRuntime for improved timer synchronization and state management
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_scene_runtime.cpp
engines/harvester/room_scene_runtime.h
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 3f0d3ecc157..6b7700eab46 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -691,7 +691,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
(void)_engine.playMusic(exitInteraction.musicPath);
flow.executeStartupAudioCommands(exitInteraction.audioCommands);
if (exitInteraction.mutatedRuntimeState)
- session.syncGlobalTimerEntities(exitInteraction.previousTimerRecords, true);
+ sceneRuntime.syncGlobalTimerEntities(exitInteraction.previousTimerRecords, true);
if (!exitInteraction.cutscenePath.empty()) {
FstPlayer fstPlayer(_engine);
@@ -950,7 +950,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) override {
- session.syncGlobalTimerEntities(previousTimerRecords);
+ session.getSceneRuntime().syncGlobalTimerEntities(previousTimerRecords);
}
bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop) override {
@@ -1018,7 +1018,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
// Native room_setup applies entry-time state changes before the first visible room frame.
if (roomEntryInteraction.mutatedRuntimeState) {
- session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
+ sceneRuntime.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
if (!session.applyCurrentRoomRuntimeMutationsInPlace(false) &&
!session.refreshCurrentScene(true)) {
return Common::kReadingFailed;
@@ -3522,7 +3522,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (entityManager) {
Common::Array<Common::String> expiredTimerNames;
if (entityManager->takeExpiredTimerNames(expiredTimerNames)) {
- session.syncCurrentRoomRuntimeState();
+ sceneRuntime.syncCurrentRoomRuntimeState(scene.sceneRegions);
for (const Common::String &timerName : expiredTimerNames) {
InteractionResult timerInteraction;
if (!_engine.getScript()->executeTimerAction(
diff --git a/engines/harvester/room_scene_runtime.cpp b/engines/harvester/room_scene_runtime.cpp
index cbfdd79ba87..8d136813b2d 100644
--- a/engines/harvester/room_scene_runtime.cpp
+++ b/engines/harvester/room_scene_runtime.cpp
@@ -75,6 +75,108 @@ bool RoomSceneRuntime::loadSceneResources(const RoomSetupState &state, RoomScene
return _resources && loadRoomSceneResources(state, *_resources, scene);
}
+void RoomSceneRuntime::syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions) {
+ Script *script = _script;
+ if (!script)
+ return;
+
+ debugC(3, kDebugRoom, "Harvester: syncing room runtime state room='%s' entities=%d",
+ _state.roomName.c_str(), _entityManager != nullptr);
+
+ for (const RegionRecord &baseRegion : script->getRegions()) {
+ if (!baseRegion.roomName.equalsIgnoreCase(_state.roomName))
+ continue;
+
+ const bool enabled =
+ findSceneRegionByName(sceneRegions, baseRegion.regionName) != nullptr;
+ (void)script->setRuntimeRegionEnabled(baseRegion.regionName, enabled);
+ }
+
+ if (_entityManager) {
+ for (AnimRecord &anim : _state.roomAnimations) {
+ Entity *entity = _entityManager->findSceneEntityByName(anim.animName);
+ if (!entity)
+ continue;
+ (void)script->syncRuntimeAnimState(anim.animName,
+ entity->isAnimationEnabled(), anim.visible, entity->getCurrentFrame());
+ }
+ }
+ for (const MonsterRecord &monster : _state.roomMonsters)
+ (void)script->syncRuntimeMonsterRecord(monster);
+ if (!_entityManager)
+ return;
+
+ for (const TimerRecord &baseTimer : script->getTimers()) {
+ Entity *entity = _entityManager->findSceneEntityByName(baseTimer.timerName);
+ if (!entity || entity->getClassId() != kRuntimeEntityClassTimer)
+ continue;
+
+ const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(baseTimer.timerName);
+ TimerRecord timer = runtimeTimer ? *runtimeTimer : baseTimer;
+ timer.currentValue = entity->getTimerCurrentValue();
+ timer.enabled = entity->isTimerEnabled();
+ timer.looping = entity->isTimerLooping();
+ timer.global = entity->isTimerGlobal();
+ (void)script->syncRuntimeTimerRecord(timer);
+ }
+}
+
+bool RoomSceneRuntime::isTimerMaterializedInCurrentRoom(const Common::String &timerName) const {
+ for (const TimerRecord &timer : _state.roomTimers) {
+ if (timer.timerName.equalsIgnoreCase(timerName))
+ return true;
+ }
+
+ return false;
+}
+
+void RoomSceneRuntime::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
+ bool includeCurrentRoomTimers) {
+ Script *script = _script;
+ if (!script || !_entityManager || previousTimerRecords.empty())
+ return;
+
+ for (const TimerRecord &previousTimer : previousTimerRecords) {
+ const TimerRecord *runtimeTimer =
+ script->findRuntimeTimerRecord(previousTimer.timerName);
+ if (!runtimeTimer || !runtimeTimer->global ||
+ (!includeCurrentRoomTimers &&
+ isTimerMaterializedInCurrentRoom(runtimeTimer->timerName))) {
+ continue;
+ }
+
+ Entity *entity = _entityManager->findSceneEntityByName(runtimeTimer->timerName);
+ if (entity && entity->getClassId() != kRuntimeEntityClassTimer)
+ continue;
+
+ if (!entity) {
+ (void)script->syncRuntimeTimerRecord(previousTimer);
+ debugC(1, kDebugRoom,
+ "Harvester: global timer mutation ignored current_room='%s' timer='%s' owner_room='%s' reason='missing live timer entity'",
+ _state.roomName.c_str(), previousTimer.timerName.c_str(),
+ previousTimer.arg1.c_str());
+ continue;
+ }
+
+ if (entity->getTimerInitialValue() == runtimeTimer->initialValue &&
+ entity->isTimerEnabled() == runtimeTimer->enabled &&
+ entity->isTimerLooping() == runtimeTimer->looping &&
+ entity->isTimerGlobal() == runtimeTimer->global) {
+ continue;
+ }
+
+ _entityManager->configureSceneTimerEntity(*entity,
+ runtimeTimer->initialValue, runtimeTimer->currentValue,
+ runtimeTimer->enabled, runtimeTimer->looping, runtimeTimer->global);
+ debugC(1, kDebugRoom,
+ "Harvester: global timer synced current_room='%s' timer='%s' owner_room='%s' current=%d initial=%d enabled=%d loop=%d",
+ _state.roomName.c_str(), runtimeTimer->timerName.c_str(),
+ runtimeTimer->arg1.c_str(), runtimeTimer->currentValue,
+ runtimeTimer->initialValue, runtimeTimer->enabled,
+ runtimeTimer->looping);
+ }
+}
+
void RoomSceneRuntime::removeSceneEntityByName(const Common::String &entityName) {
if (!_entityManager)
return;
diff --git a/engines/harvester/room_scene_runtime.h b/engines/harvester/room_scene_runtime.h
index ad997302134..54c9f3e646a 100644
--- a/engines/harvester/room_scene_runtime.h
+++ b/engines/harvester/room_scene_runtime.h
@@ -33,6 +33,9 @@ public:
RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager);
bool loadSceneResources(const RoomSetupState &state, RoomSceneResources &scene) const;
+ void syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions);
+ void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
+ bool includeCurrentRoomTimers = false);
// Initial population skips failed spawns; in-place callers handle null actor results.
void populateRoomRegions();
void populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects);
@@ -50,6 +53,7 @@ public:
Entity *spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster);
private:
+ bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
RuntimeEntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
static Common::Rect getSceneRegionBounds(const RegionRecord ®ion);
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index e47bec38136..dea23a49793 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -192,120 +192,22 @@ void RoomSession::stopPlayerRegionInteraction() {
_playerState.turnTargetFacing = -1;
if (_playerState.entity && _playerState.facing >= 0)
(void)Player::setIdleAnimation(_playerState, _playerState.facing);
- Player::syncAnimationPlacement(_playerState);
+ syncPlayerPlacement();
}
-void RoomSession::syncCurrentRoomRuntimeState() {
- Script *script = _engine.getScript();
- if (!script)
- return;
-
- debugC(3, kDebugRoom, "Harvester: syncing room runtime state room='%s' entities=%d",
- _scene.state.roomName.c_str(), _entityManager != nullptr);
-
- for (const RegionRecord &baseRegion : script->getRegions()) {
- if (!baseRegion.roomName.equalsIgnoreCase(_scene.state.roomName))
- continue;
-
- const bool enabled =
- findSceneRegionByName(_scene.sceneRegions, baseRegion.regionName) != nullptr;
- (void)script->setRuntimeRegionEnabled(baseRegion.regionName, enabled);
- }
-
- if (_entityManager) {
- for (AnimRecord &anim : _scene.state.roomAnimations) {
- Entity *entity = _entityManager->findSceneEntityByName(anim.animName);
- if (!entity)
- continue;
- (void)script->syncRuntimeAnimState(anim.animName,
- entity->isAnimationEnabled(), anim.visible, entity->getCurrentFrame());
- }
- }
- for (const MonsterRecord &monster : _scene.state.roomMonsters)
- (void)script->syncRuntimeMonsterRecord(monster);
- if (!_entityManager)
- return;
-
- for (const TimerRecord &baseTimer : script->getTimers()) {
- Entity *entity = _entityManager->findSceneEntityByName(baseTimer.timerName);
- if (!entity || entity->getClassId() != kRuntimeEntityClassTimer)
- continue;
-
- const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(baseTimer.timerName);
- TimerRecord timer = runtimeTimer ? *runtimeTimer : baseTimer;
- timer.currentValue = entity->getTimerCurrentValue();
- timer.enabled = entity->isTimerEnabled();
- timer.looping = entity->isTimerLooping();
- timer.global = entity->isTimerGlobal();
- (void)script->syncRuntimeTimerRecord(timer);
- }
+void RoomSession::syncPlayerPlacement() {
+ Player::syncAnimationPlacement(_playerState);
}
void RoomSession::captureCurrentSaveState() {
- Player::syncAnimationPlacement(_playerState);
- syncCurrentRoomRuntimeState();
+ syncPlayerPlacement();
+ _sceneRuntime.syncCurrentRoomRuntimeState(_scene.sceneRegions);
const int facing = _playerState.facing >= 0 ? _playerState.facing : _scene.state.playerFacing;
_engine.captureCurrentSaveRoomState(_scene.state.entranceName, _scene.state.roomName,
_playerState.centerX, _playerState.bottomY, (int)_playerState.z, facing,
_scene.state.discNumber, _engine.getMusicPath());
}
-bool RoomSession::isTimerMaterializedInCurrentRoom(const Common::String &timerName) const {
- for (const TimerRecord &timer : _scene.state.roomTimers) {
- if (timer.timerName.equalsIgnoreCase(timerName))
- return true;
- }
-
- return false;
-}
-
-void RoomSession::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
- bool includeCurrentRoomTimers) {
- Script *script = _engine.getScript();
- if (!script || !_entityManager || previousTimerRecords.empty())
- return;
-
- for (const TimerRecord &previousTimer : previousTimerRecords) {
- const TimerRecord *runtimeTimer =
- script->findRuntimeTimerRecord(previousTimer.timerName);
- if (!runtimeTimer || !runtimeTimer->global ||
- (!includeCurrentRoomTimers &&
- isTimerMaterializedInCurrentRoom(runtimeTimer->timerName))) {
- continue;
- }
-
- Entity *entity = _entityManager->findSceneEntityByName(runtimeTimer->timerName);
- if (entity && entity->getClassId() != kRuntimeEntityClassTimer)
- continue;
-
- if (!entity) {
- (void)script->syncRuntimeTimerRecord(previousTimer);
- debugC(1, kDebugRoom,
- "Harvester: global timer mutation ignored current_room='%s' timer='%s' owner_room='%s' reason='missing live timer entity'",
- _scene.state.roomName.c_str(), previousTimer.timerName.c_str(),
- previousTimer.arg1.c_str());
- continue;
- }
-
- if (entity->getTimerInitialValue() == runtimeTimer->initialValue &&
- entity->isTimerEnabled() == runtimeTimer->enabled &&
- entity->isTimerLooping() == runtimeTimer->looping &&
- entity->isTimerGlobal() == runtimeTimer->global) {
- continue;
- }
-
- _entityManager->configureSceneTimerEntity(*entity,
- runtimeTimer->initialValue, runtimeTimer->currentValue,
- runtimeTimer->enabled, runtimeTimer->looping, runtimeTimer->global);
- debugC(1, kDebugRoom,
- "Harvester: global timer synced current_room='%s' timer='%s' owner_room='%s' current=%d initial=%d enabled=%d loop=%d",
- _scene.state.roomName.c_str(), runtimeTimer->timerName.c_str(),
- runtimeTimer->arg1.c_str(), runtimeTimer->currentValue,
- runtimeTimer->initialValue, runtimeTimer->enabled,
- runtimeTimer->looping);
- }
-}
-
bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
Script *script = _engine.getScript();
ResourceManager *resources = _engine.getResources();
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 72a7d17c6df..314f2bd5571 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -60,16 +60,12 @@ public:
bool isPlayerCombatLocked() const;
bool cancelPlayerAttackAnimation();
void stopPlayerRegionInteraction();
- void syncCurrentRoomRuntimeState();
+ void syncPlayerPlacement();
void captureCurrentSaveState();
bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState);
bool refreshCurrentScene(bool preservePlayerPlacement);
- void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
- bool includeCurrentRoomTimers = false);
private:
- bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
-
HarvesterEngine &_engine;
Flow &_flow;
RoomSceneResources &_scene;
Commit: ef370dd63a30ba93dacb19467617e235866d44cb
https://github.com/scummvm/scummvm/commit/ef370dd63a30ba93dacb19467617e235866d44cb
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: Refactor combat state management in RoomCombat and RoomSession
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 6b7700eab46..77dccb64a82 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -432,10 +432,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
- Common::Array<RoomMonsterCombatState> &monsterCombatStates = session.getMonsterCombatStates();
- monsterCombatStates.resize(scene.state.roomMonsters.size());
- Common::Array<RoomNpcCombatState> npcCombatStates;
- npcCombatStates.resize(scene.state.roomNpcs.size());
+ RoomCombat &combat = session.getCombat();
+ combat.initializeCombatStates(scene.state);
+ Common::Array<RoomMonsterCombatState> &monsterCombatStates = combat.getMonsterCombatStates();
+ Common::Array<RoomNpcCombatState> &npcCombatStates = combat.getNpcCombatStates();
RoomSceneRuntime &sceneRuntime = session.getSceneRuntime();
RoomCombatEffects &combatEffects = session.getCombatEffects();
ResolvedText inspectText;
@@ -1925,10 +1925,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
scene.state.roomZVelocityStep > 0.0f ? scene.state.roomZVelocityStep : 1.0f));
bool playerAlive = script.getPlayerCurrentHitPoints() > 0;
- if (npcCombatStates.size() != scene.state.roomNpcs.size())
- npcCombatStates.resize(scene.state.roomNpcs.size());
- if (monsterCombatStates.size() != scene.state.roomMonsters.size())
- monsterCombatStates.resize(scene.state.roomMonsters.size());
+ combat.resizeCombatStates(scene.state);
for (uint i = 0; i < scene.state.roomNpcs.size(); ++i) {
NpcRecord &npc = scene.state.roomNpcs[i];
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index bea363a7d98..255b9abf3a5 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -488,6 +488,65 @@ int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster) {
: kDefaultMonsterAttackContactFrameOffset;
}
+bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.facing == rhs.facing &&
+ lhs.active == rhs.active &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeSpawned == rhs.runtimeSpawned &&
+ lhs.runtimeState == rhs.runtimeState &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
+void RoomCombat::initializeCombatStates(const RoomSetupState &state) {
+ _monsterCombatStates.resize(state.roomMonsters.size());
+ _npcCombatStates.resize(state.roomNpcs.size());
+ debugC(1, kDebugCombat, "Harvester: initialized room combat states monsters=%u npcs=%u",
+ _monsterCombatStates.size(), _npcCombatStates.size());
+}
+
+void RoomCombat::resizeCombatStates(const RoomSetupState &state) {
+ if (_npcCombatStates.size() != state.roomNpcs.size())
+ _npcCombatStates.resize(state.roomNpcs.size());
+ if (_monsterCombatStates.size() != state.roomMonsters.size())
+ _monsterCombatStates.resize(state.roomMonsters.size());
+}
+
+Common::Array<RoomMonsterCombatState> RoomCombat::prepareMonsterCombatStatesForSceneChange(
+ const Common::Array<MonsterRecord> &previousMonsters, const Common::Array<MonsterRecord> &updatedMonsters) const {
+ Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates;
+ updatedMonsterCombatStates.resize(updatedMonsters.size());
+ for (uint i = 0; i < updatedMonsters.size(); ++i) {
+ const MonsterRecord &updatedMonster = updatedMonsters[i];
+ for (uint j = 0; j < previousMonsters.size() &&
+ j < _monsterCombatStates.size(); ++j) {
+ const MonsterRecord &previousMonster = previousMonsters[j];
+ if (!previousMonster.monsterName.equalsIgnoreCase(updatedMonster.monsterName))
+ continue;
+ if (sameMonsterEntityState(previousMonster, updatedMonster))
+ updatedMonsterCombatStates[i] = _monsterCombatStates[j];
+ break;
+ }
+ }
+
+ return updatedMonsterCombatStates;
+}
+
+void RoomCombat::commitMonsterCombatStatesForSceneChange(Common::Array<RoomMonsterCombatState> &&states) {
+ _monsterCombatStates = Common::move(states);
+ debugC(2, kDebugCombat, "Harvester: committed room monster combat states count=%u", _monsterCombatStates.size());
+}
+
+void RoomCombat::resetForSceneRefresh(const RoomSetupState &state) {
+ // NPC death state survives refresh and is resized by index at the next combat update.
+ _monsterCombatStates.clear();
+ _monsterCombatStates.resize(state.roomMonsters.size());
+ debugC(2, kDebugCombat, "Harvester: reset room monster combat states count=%u retained_npcs=%u",
+ _monsterCombatStates.size(), _npcCombatStates.size());
+}
+
void clearRoomMonsterCombatState(RoomMonsterCombatState &combatState) {
combatState = RoomMonsterCombatState();
}
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index 45932372945..8637d5aaea2 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -116,6 +116,24 @@ struct RoomNpcCombatState {
int deathDamageType = 0;
};
+class RoomCombat {
+public:
+ Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
+ Common::Array<RoomNpcCombatState> &getNpcCombatStates() { return _npcCombatStates; }
+
+ void initializeCombatStates(const RoomSetupState &state);
+ void resizeCombatStates(const RoomSetupState &state);
+ // Keep prepared state separate until scene-entity mutations succeed.
+ Common::Array<RoomMonsterCombatState> prepareMonsterCombatStatesForSceneChange(
+ const Common::Array<MonsterRecord> &previousMonsters, const Common::Array<MonsterRecord> &updatedMonsters) const;
+ void commitMonsterCombatStatesForSceneChange(Common::Array<RoomMonsterCombatState> &&states);
+ void resetForSceneRefresh(const RoomSetupState &state);
+
+private:
+ Common::Array<RoomMonsterCombatState> _monsterCombatStates;
+ Common::Array<RoomNpcCombatState> _npcCombatStates;
+};
+
struct RoomHitEffectState {
Common::String entityName;
Common::String followTargetName;
@@ -174,6 +192,7 @@ struct ScopedDeferredLiveNpcDeathTransitions {
Script *_script;
};
+bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs);
bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc);
bool isRetainedCurrentRoomNpcDeathRecord(const NpcRecord &npc);
bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info);
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index dea23a49793..ab2320f41b1 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -95,18 +95,6 @@ static bool isQueuedLiveNpcDeathMutation(const NpcRecord &lhs, const NpcRecord &
lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
}
-static bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.facing == rhs.facing &&
- lhs.active == rhs.active &&
- lhs.visible == rhs.visible &&
- lhs.runtimeSpawned == rhs.runtimeSpawned &&
- lhs.runtimeState == rhs.runtimeState &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
-}
-
static bool sameSceneObjectState(const ObjectRecord &lhs, const ObjectRecord &rhs) {
return lhs.currentX == rhs.currentX &&
lhs.currentY == rhs.currentY &&
@@ -230,20 +218,8 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
RoomSceneResources updatedScene;
if (!_sceneRuntime.loadSceneResources(updatedState, updatedScene))
return false;
- Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates;
- updatedMonsterCombatStates.resize(updatedState.roomMonsters.size());
- for (uint i = 0; i < updatedState.roomMonsters.size(); ++i) {
- const MonsterRecord &updatedMonster = updatedState.roomMonsters[i];
- for (uint j = 0; j < _scene.state.roomMonsters.size() &&
- j < _monsterCombatStates.size(); ++j) {
- const MonsterRecord &previousMonster = _scene.state.roomMonsters[j];
- if (!previousMonster.monsterName.equalsIgnoreCase(updatedMonster.monsterName))
- continue;
- if (sameMonsterEntityState(previousMonster, updatedMonster))
- updatedMonsterCombatStates[i] = _monsterCombatStates[j];
- break;
- }
- }
+ Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates =
+ _combat.prepareMonsterCombatStatesForSceneChange(_scene.state.roomMonsters, updatedState.roomMonsters);
for (const ObjectRecord &object : _scene.sceneObjects) {
if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
@@ -444,7 +420,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
}
_scene.state = updatedState;
- _monsterCombatStates = Common::move(updatedMonsterCombatStates);
+ _combat.commitMonsterCombatStatesForSceneChange(Common::move(updatedMonsterCombatStates));
_scene.sceneObjects = updatedScene.sceneObjects;
_scene.sceneAnimations = updatedScene.sceneAnimations;
_scene.sceneRegions = updatedScene.sceneRegions;
@@ -525,8 +501,7 @@ bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
_playerState.hitKnockbackDecayStep = 0;
_playerState.combatLoadout =
_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
- _monsterCombatStates.clear();
- _monsterCombatStates.resize(_scene.state.roomMonsters.size());
+ _combat.resetForSceneRefresh(_scene.state);
_pendingRegionName.clear();
resetIdleState();
_flow.resetCursorAnimationSequence();
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 314f2bd5571..1a132068bbb 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -48,7 +48,7 @@ public:
InventorySystem &inventory);
RoomPlayerState &getPlayerState() { return _playerState; }
- Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
+ RoomCombat &getCombat() { return _combat; }
RoomSceneRuntime &getSceneRuntime() { return _sceneRuntime; }
RoomCombatEffects &getCombatEffects() { return _combatEffects; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
@@ -73,7 +73,7 @@ private:
InventorySystem &_inventory;
RoomSceneRuntime _sceneRuntime;
RoomPlayerState _playerState;
- Common::Array<RoomMonsterCombatState> _monsterCombatStates;
+ RoomCombat _combat;
RoomCombatEffects _combatEffects;
RoomIdleAnimationState _idleState;
RoomMovementInputState _movementInput;
Commit: 88b55c39621ccde7fe71e231a188f3ab712e6d84
https://github.com/scummvm/scummvm/commit/88b55c39621ccde7fe71e231a188f3ab712e6d84
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: Refactor entity state comparison functions and consolidate some scene entity logic
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
engines/harvester/room_scene_runtime.cpp
engines/harvester/room_scene_runtime.h
engines/harvester/room_session.cpp
engines/harvester/room_support.h
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index 255b9abf3a5..1b05c868c2a 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -488,18 +488,6 @@ int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster) {
: kDefaultMonsterAttackContactFrameOffset;
}
-bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.facing == rhs.facing &&
- lhs.active == rhs.active &&
- lhs.visible == rhs.visible &&
- lhs.runtimeSpawned == rhs.runtimeSpawned &&
- lhs.runtimeState == rhs.runtimeState &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
-}
-
void RoomCombat::initializeCombatStates(const RoomSetupState &state) {
_monsterCombatStates.resize(state.roomMonsters.size());
_npcCombatStates.resize(state.roomNpcs.size());
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index 8637d5aaea2..8a262f7f0db 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -192,7 +192,6 @@ struct ScopedDeferredLiveNpcDeathTransitions {
Script *_script;
};
-bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs);
bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc);
bool isRetainedCurrentRoomNpcDeathRecord(const NpcRecord &npc);
bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info);
diff --git a/engines/harvester/room_scene_runtime.cpp b/engines/harvester/room_scene_runtime.cpp
index 8d136813b2d..5a476392e51 100644
--- a/engines/harvester/room_scene_runtime.cpp
+++ b/engines/harvester/room_scene_runtime.cpp
@@ -65,6 +65,110 @@ static int resolveSceneObjectClass(const RoomSetupState &state,
: kRuntimeEntityClassDisabledHotspot;
}
+static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
+ return lhs.left == rhs.left && lhs.top == rhs.top &&
+ lhs.right == rhs.right && lhs.bottom == rhs.bottom;
+}
+
+static const AnimRecord *findRoomAnimByNameConst(const Common::Array<AnimRecord> &anims, const Common::String &animName) {
+ for (const AnimRecord &anim : anims) {
+ if (anim.animName.equalsIgnoreCase(animName))
+ return &anim;
+ }
+ return nullptr;
+}
+
+static const NpcRecord *findRoomNpcByNameConst(const Common::Array<NpcRecord> &npcs, const Common::String &npcName) {
+ for (const NpcRecord &npc : npcs) {
+ if (npc.npcName.equalsIgnoreCase(npcName))
+ return &npc;
+ }
+ return nullptr;
+}
+
+static const MonsterRecord *findRoomMonsterByNameConst(const Common::Array<MonsterRecord> &monsters, const Common::String &monsterName) {
+ for (const MonsterRecord &monster : monsters) {
+ if (monster.monsterName.equalsIgnoreCase(monsterName))
+ return &monster;
+ }
+ return nullptr;
+}
+
+static bool sameNpcEntityState(const NpcRecord &lhs, const NpcRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.frameDelay == rhs.frameDelay &&
+ lhs.active == rhs.active &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeSpawned == rhs.runtimeSpawned &&
+ lhs.runtimeState == rhs.runtimeState &&
+ lhs.deathDamageType == rhs.deathDamageType &&
+ lhs.deathOrMonsterfyFlag == rhs.deathOrMonsterfyFlag &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
+static bool isQueuedLiveNpcDeathMutation(const NpcRecord &lhs, const NpcRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.frameDelay == rhs.frameDelay &&
+ lhs.active &&
+ rhs.active &&
+ lhs.visible &&
+ rhs.visible &&
+ !lhs.runtimeSpawned &&
+ !rhs.runtimeSpawned &&
+ lhs.runtimeState < 0 &&
+ rhs.runtimeState < 0 &&
+ !lhs.deathOrMonsterfyFlag &&
+ !rhs.deathOrMonsterfyFlag &&
+ lhs.deathDamageType == 0 &&
+ rhs.deathDamageType != 0 &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
+static bool sameSceneObjectState(const ObjectRecord &lhs, const ObjectRecord &rhs) {
+ return lhs.currentX == rhs.currentX &&
+ lhs.currentY == rhs.currentY &&
+ lhs.currentZ == rhs.currentZ &&
+ lhs.zExtent == rhs.zExtent &&
+ lhs.operatable == rhs.operatable &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeVisible == rhs.runtimeVisible &&
+ lhs.actionTag.equalsIgnoreCase(rhs.actionTag) &&
+ lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
+ lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
+ lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
+ RoomSceneRuntime::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(RoomSceneRuntime::resolveSceneObjectSpritePathLocal(rhs)) &&
+ sameRect(RoomSceneRuntime::getSceneObjectBounds(lhs), RoomSceneRuntime::getSceneObjectBounds(rhs));
+}
+
+static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rhs) {
+ return lhs.left == rhs.left &&
+ lhs.top == rhs.top &&
+ lhs.right == rhs.right &&
+ lhs.bottom == rhs.bottom &&
+ lhs.minZ == rhs.minZ &&
+ lhs.maxZ == rhs.maxZ &&
+ lhs.startEnabled == rhs.startEnabled &&
+ lhs.cursorEnabled == rhs.cursorEnabled &&
+ lhs.desiredFacing == rhs.desiredFacing &&
+ lhs.actionTag.equalsIgnoreCase(rhs.actionTag);
+}
+
+bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs) {
+ return lhs.posX == rhs.posX &&
+ lhs.posY == rhs.posY &&
+ lhs.posZ == rhs.posZ &&
+ lhs.facing == rhs.facing &&
+ lhs.active == rhs.active &&
+ lhs.visible == rhs.visible &&
+ lhs.runtimeSpawned == rhs.runtimeSpawned &&
+ lhs.runtimeState == rhs.runtimeState &&
+ lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
+}
+
RoomSceneRuntime::RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager)
: _state(state), _script(script), _resources(resources), _entityManager(entityManager) {
debugC(1, kDebugRoom, "Harvester: created room scene runtime room='%s' entities=%d",
@@ -75,6 +179,215 @@ bool RoomSceneRuntime::loadSceneResources(const RoomSetupState &state, RoomScene
return _resources && loadRoomSceneResources(state, *_resources, scene);
}
+bool RoomSceneRuntime::reconcileSceneEntitiesInPlace(const RoomSceneResources &previousScene,
+ RoomSetupState &updatedState, const RoomSceneResources &updatedScene) {
+ for (const ObjectRecord &object : previousScene.sceneObjects) {
+ if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
+ removeSceneEntityByName(object.objectName);
+ }
+ for (const ObjectRecord &object : updatedScene.sceneObjects) {
+ const ObjectRecord *previous = findSceneObjectByName(previousScene.sceneObjects, object.objectName);
+ const bool changed = !previous || !sameSceneObjectState(*previous, object);
+ if (!changed)
+ continue;
+
+ removeSceneEntityByName(object.objectName);
+ spawnSceneObjectEntityFromRecord(object);
+ }
+
+ for (const RegionRecord ®ion : previousScene.sceneRegions) {
+ if (!findSceneRegionByName(updatedScene.sceneRegions, region.regionName))
+ removeSceneEntityByName(region.regionName);
+ }
+ for (const RegionRecord ®ion : updatedScene.sceneRegions) {
+ const RegionRecord *previous =
+ findSceneRegionByName(previousScene.sceneRegions, region.regionName);
+ const bool changed = !previous || !sameSceneRegionState(*previous, region);
+ if (!changed)
+ continue;
+
+ removeSceneEntityByName(region.regionName);
+ spawnSceneRegionEntityFromRecord(region);
+ }
+
+ for (const TimerRecord &timer : _state.roomTimers) {
+ bool found = false;
+ for (const TimerRecord &updatedTimer : updatedState.roomTimers) {
+ if (updatedTimer.timerName.equalsIgnoreCase(timer.timerName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ removeSceneEntityByName(timer.timerName);
+ }
+ for (const TimerRecord &timer : updatedState.roomTimers) {
+ const TimerRecord *previous = nullptr;
+ for (const TimerRecord &candidate : _state.roomTimers) {
+ if (candidate.timerName.equalsIgnoreCase(timer.timerName)) {
+ previous = &candidate;
+ break;
+ }
+ }
+ const bool changed = !previous ||
+ previous->initialValue != timer.initialValue ||
+ previous->currentValue != timer.currentValue ||
+ previous->enabled != timer.enabled ||
+ previous->looping != timer.looping ||
+ previous->global != timer.global;
+ if (!changed)
+ continue;
+
+ removeSceneEntityByName(timer.timerName);
+ if (_entityManager) {
+ (void)_entityManager->spawnSceneTimerEntity(
+ timer.timerName, timer.initialValue, timer.currentValue,
+ timer.enabled, timer.looping, timer.global);
+ }
+ }
+
+ for (AnimRecord &anim : updatedState.roomAnimations) {
+ const AnimRecord *previous =
+ findRoomAnimByNameConst(_state.roomAnimations, anim.animName);
+ const bool wasDisplayed = previous && shouldDisplaySceneAnimation(*previous);
+ const bool isDisplayed = shouldDisplaySceneAnimation(anim);
+ const bool becameVisible = !wasDisplayed && isDisplayed;
+ if (becameVisible)
+ anim.runtimeState = 0;
+
+ Entity *entity = _entityManager
+ ? _entityManager->findSceneEntityByName(anim.animName)
+ : nullptr;
+ if (!isDisplayed) {
+ removeSceneEntityByName(anim.animName);
+ continue;
+ }
+
+ const bool needsRespawn = !entity || !previous ||
+ !previous->resourcePath.equalsIgnoreCase(anim.resourcePath) ||
+ previous->x != anim.x ||
+ previous->y != anim.y ||
+ previous->z != anim.z ||
+ previous->frameDelay != anim.frameDelay ||
+ previous->looping != anim.looping ||
+ previous->pingPong != anim.pingPong ||
+ previous->backward != anim.backward;
+ if (needsRespawn) {
+ removeSceneEntityByName(anim.animName);
+ if (_entityManager) {
+ entity = _entityManager->spawnSceneAnimationEntity(
+ anim.animName, anim.resourcePath, Common::Point(anim.x, anim.y),
+ (float)anim.z, anim.frameDelay, anim.active, isDisplayed, anim.looping,
+ anim.backward, anim.pingPong, anim.runtimeState);
+ }
+ } else if (entity) {
+ const float renderZ =
+ (float)anim.z - floorf(MAX<float>(entity->getZExtent(), 0.0f) * 0.5f);
+ const bool zChanged = entity->getZ() != renderZ;
+ entity->setPosition(anim.x, anim.y, renderZ);
+ entity->setLooping(anim.looping);
+ entity->setPingPong(anim.pingPong);
+ entity->setPlayBackwards(anim.backward);
+ entity->setAnimationRate(anim.frameDelay);
+ entity->setVisible(isDisplayed);
+ if (anim.active && anim.runtimeState >= 0 && !entity->isAnimationEnabled())
+ entity->setCurrentFrame(anim.runtimeState);
+ entity->setAnimationEnabled(anim.active);
+ if (zChanged)
+ _entityManager->reinsertSceneEntity(entity);
+ }
+ }
+
+ for (const AnimRecord &anim : _state.roomAnimations) {
+ if (!findRoomAnimByNameConst(updatedState.roomAnimations, anim.animName))
+ removeSceneEntityByName(anim.animName);
+ }
+
+ for (const NpcRecord &npc : _state.roomNpcs) {
+ const NpcRecord *updatedNpc =
+ findRoomNpcByNameConst(updatedState.roomNpcs, npc.npcName);
+ const bool keepCurrentRoomDeathEntity =
+ !updatedNpc &&
+ isRetainedCurrentRoomNpcDeathRecord(npc) &&
+ _entityManager &&
+ _entityManager->findSceneEntityByName(npc.npcName);
+ const bool keepEntity =
+ updatedNpc &&
+ shouldSpawnRoomNpcEntity(*updatedNpc) &&
+ (sameNpcEntityState(npc, *updatedNpc) ||
+ isQueuedLiveNpcDeathMutation(npc, *updatedNpc));
+ if (keepCurrentRoomDeathEntity) {
+ debugC(1, kDebugCombat,
+ "Harvester: retained current-room npc death entity npc='%s' damage_type=%d frame=%d",
+ npc.npcName.c_str(), npc.deathDamageType, npc.runtimeState);
+ continue;
+ }
+ if (!keepEntity)
+ removeSceneEntityByName(npc.npcName);
+ }
+ for (const NpcRecord &npc : updatedState.roomNpcs) {
+ if (!shouldSpawnRoomNpcEntity(npc))
+ continue;
+
+ Entity *entity = _entityManager
+ ? _entityManager->findSceneEntityByName(npc.npcName)
+ : nullptr;
+ const NpcRecord *previousNpc =
+ findRoomNpcByNameConst(_state.roomNpcs, npc.npcName);
+ const bool keepQueuedDeathEntity =
+ entity &&
+ previousNpc &&
+ isQueuedLiveNpcDeathMutation(*previousNpc, npc);
+ if (keepQueuedDeathEntity)
+ continue;
+ const bool needsRespawn =
+ !entity || !previousNpc || !sameNpcEntityState(*previousNpc, npc);
+ if (!needsRespawn)
+ continue;
+
+ removeSceneEntityByName(npc.npcName);
+ if (!spawnSceneNpcEntityFromRecord(npc)) {
+ debugC(1, kDebugRoom, "Harvester: scene reconciliation stopped room='%s' npc='%s'",
+ _state.roomName.c_str(), npc.npcName.c_str());
+ return false;
+ }
+ }
+
+ for (const MonsterRecord &monster : _state.roomMonsters) {
+ const MonsterRecord *updatedMonster =
+ findRoomMonsterByNameConst(updatedState.roomMonsters, monster.monsterName);
+ const bool keepEntity =
+ updatedMonster &&
+ shouldSpawnRoomMonsterEntity(*updatedMonster) &&
+ sameMonsterEntityState(monster, *updatedMonster);
+ if (!keepEntity)
+ removeSceneEntityByName(monster.monsterName);
+ }
+ for (const MonsterRecord &monster : updatedState.roomMonsters) {
+ if (!shouldSpawnRoomMonsterEntity(monster))
+ continue;
+
+ Entity *entity = _entityManager
+ ? _entityManager->findSceneEntityByName(monster.monsterName)
+ : nullptr;
+ const MonsterRecord *previousMonster =
+ findRoomMonsterByNameConst(_state.roomMonsters, monster.monsterName);
+ const bool needsRespawn =
+ !entity || !previousMonster || !sameMonsterEntityState(*previousMonster, monster);
+ if (!needsRespawn)
+ continue;
+
+ removeSceneEntityByName(monster.monsterName);
+ if (!spawnSceneMonsterEntityFromRecord(updatedState, monster)) {
+ debugC(1, kDebugRoom, "Harvester: scene reconciliation stopped room='%s' monster='%s'",
+ _state.roomName.c_str(), monster.monsterName.c_str());
+ return false;
+ }
+ }
+
+ return true;
+}
+
void RoomSceneRuntime::syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions) {
Script *script = _script;
if (!script)
diff --git a/engines/harvester/room_scene_runtime.h b/engines/harvester/room_scene_runtime.h
index 54c9f3e646a..8df8f83a3de 100644
--- a/engines/harvester/room_scene_runtime.h
+++ b/engines/harvester/room_scene_runtime.h
@@ -33,10 +33,11 @@ public:
RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager);
bool loadSceneResources(const RoomSetupState &state, RoomSceneResources &scene) const;
+ bool reconcileSceneEntitiesInPlace(const RoomSceneResources &previousScene,
+ RoomSetupState &updatedState, const RoomSceneResources &updatedScene);
void syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions);
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers = false);
- // Initial population skips failed spawns; in-place callers handle null actor results.
void populateRoomRegions();
void populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects);
void populateRoomNpcs();
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index ab2320f41b1..557da4e273f 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -32,98 +32,6 @@
namespace Harvester {
-static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
- return lhs.left == rhs.left && lhs.top == rhs.top &&
- lhs.right == rhs.right && lhs.bottom == rhs.bottom;
-}
-
-static const AnimRecord *findRoomAnimByNameConst(const Common::Array<AnimRecord> &anims, const Common::String &animName) {
- for (const AnimRecord &anim : anims) {
- if (anim.animName.equalsIgnoreCase(animName))
- return &anim;
- }
- return nullptr;
-}
-
-static const NpcRecord *findRoomNpcByNameConst(const Common::Array<NpcRecord> &npcs, const Common::String &npcName) {
- for (const NpcRecord &npc : npcs) {
- if (npc.npcName.equalsIgnoreCase(npcName))
- return &npc;
- }
- return nullptr;
-}
-
-static const MonsterRecord *findRoomMonsterByNameConst(const Common::Array<MonsterRecord> &monsters, const Common::String &monsterName) {
- for (const MonsterRecord &monster : monsters) {
- if (monster.monsterName.equalsIgnoreCase(monsterName))
- return &monster;
- }
- return nullptr;
-}
-
-static bool sameNpcEntityState(const NpcRecord &lhs, const NpcRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.frameDelay == rhs.frameDelay &&
- lhs.active == rhs.active &&
- lhs.visible == rhs.visible &&
- lhs.runtimeSpawned == rhs.runtimeSpawned &&
- lhs.runtimeState == rhs.runtimeState &&
- lhs.deathDamageType == rhs.deathDamageType &&
- lhs.deathOrMonsterfyFlag == rhs.deathOrMonsterfyFlag &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
-}
-
-static bool isQueuedLiveNpcDeathMutation(const NpcRecord &lhs, const NpcRecord &rhs) {
- return lhs.posX == rhs.posX &&
- lhs.posY == rhs.posY &&
- lhs.posZ == rhs.posZ &&
- lhs.frameDelay == rhs.frameDelay &&
- lhs.active &&
- rhs.active &&
- lhs.visible &&
- rhs.visible &&
- !lhs.runtimeSpawned &&
- !rhs.runtimeSpawned &&
- lhs.runtimeState < 0 &&
- rhs.runtimeState < 0 &&
- !lhs.deathOrMonsterfyFlag &&
- !rhs.deathOrMonsterfyFlag &&
- lhs.deathDamageType == 0 &&
- rhs.deathDamageType != 0 &&
- lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
-}
-
-static bool sameSceneObjectState(const ObjectRecord &lhs, const ObjectRecord &rhs) {
- return lhs.currentX == rhs.currentX &&
- lhs.currentY == rhs.currentY &&
- lhs.currentZ == rhs.currentZ &&
- lhs.zExtent == rhs.zExtent &&
- lhs.operatable == rhs.operatable &&
- lhs.visible == rhs.visible &&
- lhs.runtimeVisible == rhs.runtimeVisible &&
- lhs.actionTag.equalsIgnoreCase(rhs.actionTag) &&
- lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
- lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
- lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
- RoomSceneRuntime::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(RoomSceneRuntime::resolveSceneObjectSpritePathLocal(rhs)) &&
- sameRect(RoomSceneRuntime::getSceneObjectBounds(lhs), RoomSceneRuntime::getSceneObjectBounds(rhs));
-}
-
-static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rhs) {
- return lhs.left == rhs.left &&
- lhs.top == rhs.top &&
- lhs.right == rhs.right &&
- lhs.bottom == rhs.bottom &&
- lhs.minZ == rhs.minZ &&
- lhs.maxZ == rhs.maxZ &&
- lhs.startEnabled == rhs.startEnabled &&
- lhs.cursorEnabled == rhs.cursorEnabled &&
- lhs.desiredFacing == rhs.desiredFacing &&
- lhs.actionTag.equalsIgnoreCase(rhs.actionTag);
-}
-
RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
InventorySystem &inventory)
: _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory),
@@ -221,203 +129,8 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates =
_combat.prepareMonsterCombatStatesForSceneChange(_scene.state.roomMonsters, updatedState.roomMonsters);
- for (const ObjectRecord &object : _scene.sceneObjects) {
- if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
- _sceneRuntime.removeSceneEntityByName(object.objectName);
- }
- for (const ObjectRecord &object : updatedScene.sceneObjects) {
- const ObjectRecord *previous = findSceneObjectByName(_scene.sceneObjects, object.objectName);
- const bool changed = !previous || !sameSceneObjectState(*previous, object);
- if (!changed)
- continue;
-
- _sceneRuntime.removeSceneEntityByName(object.objectName);
- _sceneRuntime.spawnSceneObjectEntityFromRecord(object);
- }
-
- for (const RegionRecord ®ion : _scene.sceneRegions) {
- if (!findSceneRegionByName(updatedScene.sceneRegions, region.regionName))
- _sceneRuntime.removeSceneEntityByName(region.regionName);
- }
- for (const RegionRecord ®ion : updatedScene.sceneRegions) {
- const RegionRecord *previous =
- findSceneRegionByName(_scene.sceneRegions, region.regionName);
- const bool changed = !previous || !sameSceneRegionState(*previous, region);
- if (!changed)
- continue;
-
- _sceneRuntime.removeSceneEntityByName(region.regionName);
- _sceneRuntime.spawnSceneRegionEntityFromRecord(region);
- }
-
- for (const TimerRecord &timer : _scene.state.roomTimers) {
- bool found = false;
- for (const TimerRecord &updatedTimer : updatedState.roomTimers) {
- if (updatedTimer.timerName.equalsIgnoreCase(timer.timerName)) {
- found = true;
- break;
- }
- }
- if (!found)
- _sceneRuntime.removeSceneEntityByName(timer.timerName);
- }
- for (const TimerRecord &timer : updatedState.roomTimers) {
- const TimerRecord *previous = nullptr;
- for (const TimerRecord &candidate : _scene.state.roomTimers) {
- if (candidate.timerName.equalsIgnoreCase(timer.timerName)) {
- previous = &candidate;
- break;
- }
- }
- const bool changed = !previous ||
- previous->initialValue != timer.initialValue ||
- previous->currentValue != timer.currentValue ||
- previous->enabled != timer.enabled ||
- previous->looping != timer.looping ||
- previous->global != timer.global;
- if (!changed)
- continue;
-
- _sceneRuntime.removeSceneEntityByName(timer.timerName);
- if (_entityManager) {
- (void)_entityManager->spawnSceneTimerEntity(
- timer.timerName, timer.initialValue, timer.currentValue,
- timer.enabled, timer.looping, timer.global);
- }
- }
-
- for (AnimRecord &anim : updatedState.roomAnimations) {
- const AnimRecord *previous =
- findRoomAnimByNameConst(_scene.state.roomAnimations, anim.animName);
- const bool wasDisplayed = previous && shouldDisplaySceneAnimation(*previous);
- const bool isDisplayed = shouldDisplaySceneAnimation(anim);
- const bool becameVisible = !wasDisplayed && isDisplayed;
- if (becameVisible)
- anim.runtimeState = 0;
-
- Entity *entity = _entityManager
- ? _entityManager->findSceneEntityByName(anim.animName)
- : nullptr;
- if (!isDisplayed) {
- _sceneRuntime.removeSceneEntityByName(anim.animName);
- continue;
- }
-
- const bool needsRespawn = !entity || !previous ||
- !previous->resourcePath.equalsIgnoreCase(anim.resourcePath) ||
- previous->x != anim.x ||
- previous->y != anim.y ||
- previous->z != anim.z ||
- previous->frameDelay != anim.frameDelay ||
- previous->looping != anim.looping ||
- previous->pingPong != anim.pingPong ||
- previous->backward != anim.backward;
- if (needsRespawn) {
- _sceneRuntime.removeSceneEntityByName(anim.animName);
- if (_entityManager) {
- entity = _entityManager->spawnSceneAnimationEntity(
- anim.animName, anim.resourcePath, Common::Point(anim.x, anim.y),
- (float)anim.z, anim.frameDelay, anim.active, isDisplayed, anim.looping,
- anim.backward, anim.pingPong, anim.runtimeState);
- }
- } else if (entity) {
- const float renderZ =
- (float)anim.z - floorf(MAX<float>(entity->getZExtent(), 0.0f) * 0.5f);
- const bool zChanged = entity->getZ() != renderZ;
- entity->setPosition(anim.x, anim.y, renderZ);
- entity->setLooping(anim.looping);
- entity->setPingPong(anim.pingPong);
- entity->setPlayBackwards(anim.backward);
- entity->setAnimationRate(anim.frameDelay);
- entity->setVisible(isDisplayed);
- if (anim.active && anim.runtimeState >= 0 && !entity->isAnimationEnabled())
- entity->setCurrentFrame(anim.runtimeState);
- entity->setAnimationEnabled(anim.active);
- if (zChanged)
- _entityManager->reinsertSceneEntity(entity);
- }
- }
-
- for (const AnimRecord &anim : _scene.state.roomAnimations) {
- if (!findRoomAnimByNameConst(updatedState.roomAnimations, anim.animName))
- _sceneRuntime.removeSceneEntityByName(anim.animName);
- }
-
- for (const NpcRecord &npc : _scene.state.roomNpcs) {
- const NpcRecord *updatedNpc =
- findRoomNpcByNameConst(updatedState.roomNpcs, npc.npcName);
- const bool keepCurrentRoomDeathEntity =
- !updatedNpc &&
- isRetainedCurrentRoomNpcDeathRecord(npc) &&
- _entityManager &&
- _entityManager->findSceneEntityByName(npc.npcName);
- const bool keepEntity =
- updatedNpc &&
- _sceneRuntime.shouldSpawnRoomNpcEntity(*updatedNpc) &&
- (sameNpcEntityState(npc, *updatedNpc) ||
- isQueuedLiveNpcDeathMutation(npc, *updatedNpc));
- if (keepCurrentRoomDeathEntity) {
- debugC(1, kDebugCombat,
- "Harvester: retained current-room npc death entity npc='%s' damage_type=%d frame=%d",
- npc.npcName.c_str(), npc.deathDamageType, npc.runtimeState);
- continue;
- }
- if (!keepEntity)
- _sceneRuntime.removeSceneEntityByName(npc.npcName);
- }
- for (const NpcRecord &npc : updatedState.roomNpcs) {
- if (!_sceneRuntime.shouldSpawnRoomNpcEntity(npc))
- continue;
-
- Entity *entity = _entityManager
- ? _entityManager->findSceneEntityByName(npc.npcName)
- : nullptr;
- const NpcRecord *previousNpc =
- findRoomNpcByNameConst(_scene.state.roomNpcs, npc.npcName);
- const bool keepQueuedDeathEntity =
- entity &&
- previousNpc &&
- isQueuedLiveNpcDeathMutation(*previousNpc, npc);
- if (keepQueuedDeathEntity)
- continue;
- const bool needsRespawn =
- !entity || !previousNpc || !sameNpcEntityState(*previousNpc, npc);
- if (!needsRespawn)
- continue;
-
- _sceneRuntime.removeSceneEntityByName(npc.npcName);
- if (!_sceneRuntime.spawnSceneNpcEntityFromRecord(npc))
- return false;
- }
-
- for (const MonsterRecord &monster : _scene.state.roomMonsters) {
- const MonsterRecord *updatedMonster =
- findRoomMonsterByNameConst(updatedState.roomMonsters, monster.monsterName);
- const bool keepEntity =
- updatedMonster &&
- _sceneRuntime.shouldSpawnRoomMonsterEntity(*updatedMonster) &&
- sameMonsterEntityState(monster, *updatedMonster);
- if (!keepEntity)
- _sceneRuntime.removeSceneEntityByName(monster.monsterName);
- }
- for (const MonsterRecord &monster : updatedState.roomMonsters) {
- if (!_sceneRuntime.shouldSpawnRoomMonsterEntity(monster))
- continue;
-
- Entity *entity = _entityManager
- ? _entityManager->findSceneEntityByName(monster.monsterName)
- : nullptr;
- const MonsterRecord *previousMonster =
- findRoomMonsterByNameConst(_scene.state.roomMonsters, monster.monsterName);
- const bool needsRespawn =
- !entity || !previousMonster || !sameMonsterEntityState(*previousMonster, monster);
- if (!needsRespawn)
- continue;
-
- _sceneRuntime.removeSceneEntityByName(monster.monsterName);
- if (!_sceneRuntime.spawnSceneMonsterEntityFromRecord(updatedState, monster))
- return false;
- }
+ if (!_sceneRuntime.reconcileSceneEntitiesInPlace(_scene, updatedState, updatedScene))
+ return false;
_scene.state = updatedState;
_combat.commitMonsterCombatStatesForSceneChange(Common::move(updatedMonsterCombatStates));
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 2c546f64153..463efbda54e 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -127,6 +127,7 @@ struct RoomIdleAnimationState {
bool exiting = false;
};
+bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs);
bool applyRoomActorPlacement(const RoomSetupState &state, Entity &entity,
int centerX, int bottomY, float z, const Common::String *entranceName = nullptr);
void logScenePaletteSummary(const char *label, const RoomSceneResources &scene, float brightness);
Commit: 91f5b74f0258c00c29c5dccc175947dc745a8d1d
https://github.com/scummvm/scummvm/commit/91f5b74f0258c00c29c5dccc175947dc745a8d1d
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:13-04:00
Commit Message:
HARVESTER: Implement player rebind after scene refresh and restore monster animations
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
engines/harvester/player.h
engines/harvester/room_scene_runtime.cpp
engines/harvester/room_scene_runtime.h
engines/harvester/room_session.cpp
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 53b7a8bbbdc..a57b986cba5 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -1114,6 +1114,47 @@ static bool finishPlayerIdleAnimation(const RoomSetupState &state, RoomPlayerSta
} // End of anonymous namespace
+void Player::rebindAfterSceneRefresh(const RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, bool preservePlayerPlacement) {
+ playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
+ if (playerState.entity) {
+ if (!preservePlayerPlacement || playerState.facing < 0) {
+ playerState.centerX = state.playerSpawnX;
+ playerState.bottomY = state.playerSpawnY;
+ playerState.z = (float)state.playerSpawnZ;
+ playerState.facing = state.playerFacing;
+ }
+
+ const int facing = playerState.facing >= 0 ? playerState.facing : state.playerFacing;
+ (void)setIdleAnimation(playerState, facing);
+ (void)applyRoomActorPlacement(state, *playerState.entity,
+ playerState.centerX, playerState.bottomY, playerState.z);
+ }
+
+ // Death state survives a scene refresh.
+ playerState.hasMoveTarget = false;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ 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;
+ debugC(2, kDebugPlayer, "Harvester: rebound player after scene refresh room='%s' entity=%d preserve_placement=%d",
+ state.roomName.c_str(), playerState.entity != nullptr, preservePlayerPlacement);
+}
+
void Player::syncAnimationPlacement(RoomPlayerState &playerState) {
if (!playerState.entity)
return;
diff --git a/engines/harvester/player.h b/engines/harvester/player.h
index bdf7d89649a..414ac7337fd 100644
--- a/engines/harvester/player.h
+++ b/engines/harvester/player.h
@@ -26,8 +26,12 @@
namespace Harvester {
+class EntityManager;
+
class Player {
public:
+ static void rebindAfterSceneRefresh(const RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, bool preservePlayerPlacement);
static void syncAnimationPlacement(RoomPlayerState &playerState);
static float computeDepthScale(const RoomSetupState &state, float z);
static int resolveFacingFrame(int facing);
diff --git a/engines/harvester/room_scene_runtime.cpp b/engines/harvester/room_scene_runtime.cpp
index 5a476392e51..c4ab1749e8e 100644
--- a/engines/harvester/room_scene_runtime.cpp
+++ b/engines/harvester/room_scene_runtime.cpp
@@ -682,6 +682,16 @@ Entity *RoomSceneRuntime::spawnSceneMonsterEntityFromRecord(const RoomSetupState
return entity;
}
+void RoomSceneRuntime::restoreRoomMonsterAnimations() {
+ if (_entityManager) {
+ for (const MonsterRecord &monster : _state.roomMonsters) {
+ Entity *entity = _entityManager->findSceneEntityByName(monster.monsterName);
+ if (entity)
+ Monster::applyAnimation(*entity, monster);
+ }
+ }
+}
+
void RoomSceneRuntime::populateRoomRegions() {
if (!_entityManager)
return;
diff --git a/engines/harvester/room_scene_runtime.h b/engines/harvester/room_scene_runtime.h
index 8df8f83a3de..15febc6fbae 100644
--- a/engines/harvester/room_scene_runtime.h
+++ b/engines/harvester/room_scene_runtime.h
@@ -42,6 +42,7 @@ public:
void populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects);
void populateRoomNpcs();
void populateRoomMonsters();
+ void restoreRoomMonsterAnimations();
void removeSceneEntityByName(const Common::String &entityName);
static Common::Rect getSceneObjectBounds(const ObjectRecord &object);
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index 557da4e273f..fa83ff4c9c0 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -25,7 +25,6 @@
#include "harvester/flow.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
-#include "harvester/monster.h"
#include "harvester/player.h"
#include "harvester/room_combat.h"
#include "harvester/runtime_entity.h"
@@ -170,48 +169,8 @@ bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
_combatEffects.clearHitEffects();
if (!_flow.populateRoomSceneEntities(_scene.state, _scene.sceneObjects, _scene.sceneAnimations))
return false;
- if (_entityManager) {
- for (const MonsterRecord &monster : _scene.state.roomMonsters) {
- Entity *entity = _entityManager->findSceneEntityByName(monster.monsterName);
- if (entity)
- Monster::applyAnimation(*entity, monster);
- }
- }
-
- _playerState.entity = _entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr;
- if (_playerState.entity) {
- if (!preservePlayerPlacement || _playerState.facing < 0) {
- _playerState.centerX = _scene.state.playerSpawnX;
- _playerState.bottomY = _scene.state.playerSpawnY;
- _playerState.z = (float)_scene.state.playerSpawnZ;
- _playerState.facing = _scene.state.playerFacing;
- }
-
- const int facing = _playerState.facing >= 0 ? _playerState.facing : _scene.state.playerFacing;
- (void)Player::setIdleAnimation(_playerState, facing);
- (void)applyRoomActorPlacement(_scene.state, *_playerState.entity,
- _playerState.centerX, _playerState.bottomY, _playerState.z);
- }
-
- _playerState.hasMoveTarget = false;
- _playerState.turnActive = false;
- _playerState.turnTargetFacing = -1;
- _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;
+ _sceneRuntime.restoreRoomMonsterAnimations();
+ Player::rebindAfterSceneRefresh(_scene.state, _playerState, _entityManager, preservePlayerPlacement);
_playerState.combatLoadout =
_engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
_combat.resetForSceneRefresh(_scene.state);
Commit: 4a1917385768bf0252d5fa73067af10a534cd059
https://github.com/scummvm/scummvm/commit/4a1917385768bf0252d5fa73067af10a534cd059
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor combat target selection and interaction methods
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 77dccb64a82..4aff9a22223 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -1077,14 +1077,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return -1;
};
- auto findRoomMonsterRecordByName = [&](const Common::String &monsterName) -> MonsterRecord * {
- for (MonsterRecord &monster : scene.state.roomMonsters) {
- if (monster.monsterName.equalsIgnoreCase(monsterName))
- return &monster;
- }
-
- return nullptr;
- };
auto findRoomMonsterIndexByName = [&](const Common::String &monsterName) -> int {
for (uint i = 0; i < scene.state.roomMonsters.size(); ++i) {
if (scene.state.roomMonsters[i].monsterName.equalsIgnoreCase(monsterName))
@@ -1124,194 +1116,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monster.monsterName.c_str());
return true;
};
- auto findMonsterTargetAtPoint = [&](const Common::Point &point) -> MonsterRecord * {
- 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(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;
- };
- auto findOverlappingMonsterTarget = [&]() -> MonsterRecord * {
- if (!playerState.entity)
- return nullptr;
-
- for (MonsterRecord &monster : scene.state.roomMonsters) {
- if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
- continue;
-
- Entity *entity = findSceneRuntimeEntity(monster.monsterName);
- if (entity && areCombatantsWithinRoomCombatReach(scene.state,
- *playerState.entity, playerState.z, *entity, (float)monster.posZ, monster.engageDistance))
- return &monster;
- }
-
- return nullptr;
- };
- auto findOverlappingNpcTarget = [&]() -> NpcRecord * {
- if (!playerState.entity)
- return nullptr;
-
- for (NpcRecord &npc : scene.state.roomNpcs) {
- if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
- continue;
-
- Entity *entity = findSceneRuntimeEntity(npc.npcName);
- if (entity && areCombatantsWithinRoomCombatReach(scene.state,
- *playerState.entity, playerState.z, *entity, entity->getZ(), 0))
- return &npc;
- }
-
- return nullptr;
- };
- auto playerAttackCanReachTarget = [&](Entity *targetEntity, int engageDistance = 0) {
- 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);
- };
- enum NativeAttackSide {
- kNativeAttackSideNone,
- kNativeAttackSideLeft,
- kNativeAttackSideRight
- };
- enum NativeAttackBand {
- kNativeAttackBandMid,
- kNativeAttackBandUpper,
- kNativeAttackBandLower
- };
- auto 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;
- }
- };
- auto findProjectileContactMonsterTarget = [&]() -> MonsterRecord * {
- 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(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;
- };
- auto describeCombatTargetClass = [](int classId) -> const char * {
- switch (classId) {
- case kRuntimeEntityClassNpc:
- return "npc";
- case kRuntimeEntityClassMonster:
- return "monster";
- case kRuntimeEntityClassPlayer:
- return "player";
- default:
- return "none";
- }
- };
auto handleCombatInteraction = [&](const InteractionResult &interaction) -> Common::Error {
bool didTransition = false;
Common::Error interactionError =
@@ -1411,70 +1216,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
auto killDebugActiveMonster = [&]() -> Common::Error {
if (!_engine.isCombatDebugEnabled())
return Common::kNoError;
-
- auto isKillableMonster = [&](const MonsterRecord *monster) {
- return monster && monster->active && monster->visible && monster->currentHitPoints > 0;
- };
- auto selectMonsterEntity = [&](const MonsterRecord &monster) -> Entity * {
- Entity *entity = findSceneRuntimeEntity(monster.monsterName);
- return (entity && entity->isVisible()) ? entity : nullptr;
- };
- auto resolveKillTarget = [&]() -> MonsterRecord * {
- if (playerState.attackTargetClassId == kRuntimeEntityClassMonster) {
- MonsterRecord *monster = findRoomMonsterRecordByName(playerState.attackTargetName);
- if (isKillableMonster(monster) && selectMonsterEntity(*monster))
- return monster;
- }
-
- if (MonsterRecord *monster = findMonsterTargetAtPoint(_mousePos)) {
- if (isKillableMonster(monster) && selectMonsterEntity(*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 RoomMonsterCombatState &combatState = monsterCombatStates[i];
- if (!combatState.attackActive || !isKillableMonster(&monster) || !selectMonsterEntity(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(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;
- };
-
- MonsterRecord *monster = resolveKillTarget();
- Entity *monsterEntity = monster ? selectMonsterEntity(*monster) : nullptr;
- if (!monster || !monsterEntity) {
+ MonsterRecord *monster = combat.resolveKillTarget(scene.state, playerState, entityManager, _mousePos);
+ Entity *monsterEntity = monster ? findSceneRuntimeEntity(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);
@@ -1550,142 +1294,15 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return handleCombatInteraction(interaction);
};
auto capturePlayerAttackTarget = [&]() {
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
-
- const RoomHoverState hoverState = resolveRoomHoverState(
- _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
- _mousePos, flow._menuTextConfig, &flow._dialogue);
- if (hoverState.npc) {
- playerState.attackTargetName = hoverState.npc->npcName;
- playerState.attackTargetClassId = kRuntimeEntityClassNpc;
- 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)) {
- playerState.attackTargetName = monster->monsterName;
- playerState.attackTargetClassId = kRuntimeEntityClassMonster;
- 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);
- };
- auto capturePlayerAttackTargetForKeyboard = [&](bool attackLeft, bool attackRight, bool attackUp, bool attackDown) {
playerState.attackTargetName.clear();
playerState.attackTargetClassId = -1;
- 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;
- 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(npc.npcName), npc.npcName, kRuntimeEntityClassNpc);
- }
- for (MonsterRecord &monster : scene.state.roomMonsters) {
- if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
- continue;
- considerTarget(findSceneRuntimeEntity(monster.monsterName), monster.monsterName, kRuntimeEntityClassMonster);
- }
-
- 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;
- }
+ const RoomHoverState hoverState = resolveRoomHoverState(
+ _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
+ _mousePos, flow._menuTextConfig, &flow._dialogue);
+ combat.capturePlayerAttackTarget(scene.state, playerState, entityManager, _mousePos, hoverState.npc);
+ };
- playerState.attackTargetName = bestName;
- playerState.attackTargetClassId = 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);
- };
auto startMonsterHitReaction = [&](MonsterRecord &monster, Entity &monsterEntity,
RoomMonsterCombatState *combatState, int attackerAttackFirstFrame) {
if (combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
@@ -1754,18 +1371,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!script)
return Common::kNoError;
- if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
- if (NpcRecord *fallbackNpc = findOverlappingNpcTarget()) {
- playerState.attackTargetName = fallbackNpc->npcName;
- playerState.attackTargetClassId = kRuntimeEntityClassNpc;
- }
- }
+ combat.captureOverlappingNpcAttackTarget(scene.state, playerState, entityManager);
if (playerState.attackTargetClassId == kRuntimeEntityClassNpc) {
- NpcRecord *npc = findRoomNpcRecordByName(playerState.attackTargetName);
- Entity *npcEntity = npc ? findSceneRuntimeEntity(npc->npcName) : nullptr;
- if (!npc || !npcEntity || !playerAttackCanReachTarget(npcEntity))
- npc = Player::isProjectileCombatLoadout(playerState.combatLoadout) ? nullptr : findOverlappingNpcTarget();
- npcEntity = npc ? findSceneRuntimeEntity(npc->npcName) : nullptr;
+ Entity *npcEntity = nullptr;
+ NpcRecord *npc = combat.selectNpcAttackContactTarget(scene.state, playerState, entityManager, npcEntity);
if (!npc) {
debugC(1, kDebugCombat,
"Harvester: combat player attack missed class='npc' target='%s' reason='no reachable npc'",
@@ -1813,24 +1422,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return Common::kNoError;
}
- MonsterRecord *monster = nullptr;
- if (playerState.attackTargetClassId == kRuntimeEntityClassMonster)
- monster = findRoomMonsterRecordByName(playerState.attackTargetName);
- Entity *monsterEntity = monster ? findSceneRuntimeEntity(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) {
- playerState.attackTargetName = monster->monsterName;
- playerState.attackTargetClassId = kRuntimeEntityClassMonster;
- }
- } else {
- monster = nullptr;
- }
- monsterEntity = monster ? findSceneRuntimeEntity(monster->monsterName) : nullptr;
- }
+ Entity *monsterEntity = nullptr;
+ MonsterRecord *monster = combat.selectMonsterAttackContactTarget(scene.state, playerState, entityManager, monsterEntity);
if (!monster || !monsterEntity) {
debugC(1, kDebugCombat,
"Harvester: combat player attack missed class='monster' target='%s' reason='no reachable monster'",
@@ -3441,7 +3034,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
!idleState.active && !idleState.exiting) {
const uint32 now = Player::getRuntimeClockTicks();
if (nextKeyboardAttackAllowedTick == 0 || (int32)(now - nextKeyboardAttackAllowedTick) >= 0) {
- capturePlayerAttackTargetForKeyboard(moveLeft, moveRight, moveUp, moveDown);
+ combat.capturePlayerAttackTargetForKeyboard(scene.state, playerState, entityManager, moveLeft, moveRight, moveUp, moveDown);
if (Player::startKeyboardAttackAnimation(
scene.state, playerState, moveLeft, moveRight, moveUp, moveDown)) {
nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index 1b05c868c2a..4210bd4ab7e 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -62,6 +62,81 @@ 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 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;
@@ -488,6 +563,391 @@ int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster) {
: kDefaultMonsterAttackContactFrameOffset;
}
+const char *describeCombatTargetClass(int classId) {
+ switch (classId) {
+ case kRuntimeEntityClassNpc:
+ return "npc";
+ case kRuntimeEntityClassMonster:
+ return "monster";
+ case kRuntimeEntityClassPlayer:
+ return "player";
+ default:
+ return "none";
+ }
+}
+
+MonsterRecord *RoomCombat::findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const {
+ MonsterRecord *bestMonster = nullptr;
+ int bestZ = -0x7fffffff;
+ int bestTop = -0x7fffffff;
+ for (MonsterRecord &monster : 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 *RoomCombat::findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager) const {
+ if (!playerState.entity)
+ return nullptr;
+
+ for (MonsterRecord &monster : state.roomMonsters) {
+ if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
+ continue;
+
+ Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
+ if (entity && areCombatantsWithinRoomCombatReach(state,
+ *playerState.entity, playerState.z, *entity, (float)monster.posZ, monster.engageDistance))
+ return &monster;
+ }
+
+ return nullptr;
+}
+
+NpcRecord *RoomCombat::findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const {
+ if (!playerState.entity)
+ return nullptr;
+
+ for (NpcRecord &npc : state.roomNpcs) {
+ if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
+ continue;
+
+ Entity *entity = findSceneRuntimeEntity(entityManager, npc.npcName);
+ if (entity && areCombatantsWithinRoomCombatReach(state,
+ *playerState.entity, playerState.z, *entity, entity->getZ(), 0))
+ return &npc;
+ }
+
+ return nullptr;
+}
+
+bool RoomCombat::playerAttackCanReachTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ Entity *targetEntity, int engageDistance) const {
+ if (!playerState.entity || !targetEntity)
+ return false;
+ if (Player::isProjectileCombatLoadout(playerState.combatLoadout))
+ return true;
+
+ return areCombatantsWithinRoomCombatReach(state,
+ *playerState.entity, playerState.z, *targetEntity, targetEntity->getZ(), engageDistance);
+}
+
+MonsterRecord *RoomCombat::findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager) const {
+ 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 : 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 RoomCombat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ 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 : state.roomNpcs) {
+ if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
+ continue;
+ considerTarget(findSceneRuntimeEntity(entityManager, npc.npcName), npc.npcName, kRuntimeEntityClassNpc);
+ }
+ for (MonsterRecord &monster : state.roomMonsters) {
+ if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
+ continue;
+ considerTarget(findSceneRuntimeEntity(entityManager, monster.monsterName), monster.monsterName, kRuntimeEntityClassMonster);
+ }
+
+ 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;
+ }
+
+ playerState.attackTargetName = bestName;
+ playerState.attackTargetClassId = 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 *RoomCombat::resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager, const Common::Point &mousePos) const {
+ if (playerState.attackTargetClassId == kRuntimeEntityClassMonster) {
+ MonsterRecord *monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
+ if (isKillableMonster(monster) && selectMonsterEntity(entityManager, *monster))
+ return monster;
+ }
+
+ if (MonsterRecord *monster = findMonsterTargetAtPoint(state, entityManager, mousePos)) {
+ if (isKillableMonster(monster) && selectMonsterEntity(entityManager, *monster))
+ return monster;
+ }
+
+ if (playerState.entity) {
+ for (uint i = 0; i < state.roomMonsters.size() && i < _monsterCombatStates.size(); ++i) {
+ MonsterRecord &monster = state.roomMonsters[i];
+ const RoomMonsterCombatState &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 : 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 RoomCombat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
+ if (hoverNpc) {
+ playerState.attackTargetName = hoverNpc->npcName;
+ playerState.attackTargetClassId = kRuntimeEntityClassNpc;
+ 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(state, entityManager, mousePos)) {
+ playerState.attackTargetName = monster->monsterName;
+ playerState.attackTargetClassId = kRuntimeEntityClassMonster;
+ 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 RoomCombat::captureOverlappingNpcAttackTarget(RoomSetupState &state, RoomPlayerState &playerState, EntityManager *entityManager) const {
+ if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
+ if (NpcRecord *fallbackNpc = findOverlappingNpcTarget(state, playerState, entityManager)) {
+ playerState.attackTargetName = fallbackNpc->npcName;
+ playerState.attackTargetClassId = kRuntimeEntityClassNpc;
+ }
+ }
+}
+
+NpcRecord *RoomCombat::selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager, Entity *&npcEntity) const {
+ NpcRecord *npc = findRoomNpcRecordByName(state, playerState.attackTargetName);
+ npcEntity = npc ? findSceneRuntimeEntity(entityManager, npc->npcName) : nullptr;
+ if (!npc || !npcEntity || !playerAttackCanReachTarget(state, playerState, npcEntity))
+ npc = Player::isProjectileCombatLoadout(playerState.combatLoadout) ? nullptr : findOverlappingNpcTarget(state, playerState, entityManager);
+ npcEntity = npc ? findSceneRuntimeEntity(entityManager, npc->npcName) : nullptr;
+ return npc;
+}
+
+MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, Entity *&monsterEntity) const {
+ MonsterRecord *monster = nullptr;
+ if (playerState.attackTargetClassId == kRuntimeEntityClassMonster)
+ monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
+ monsterEntity = monster ? findSceneRuntimeEntity(entityManager, monster->monsterName) : nullptr;
+ if (!monster || !monsterEntity || !playerAttackCanReachTarget(state, playerState, monsterEntity, monster->engageDistance)) {
+ if (!Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
+ monster = findOverlappingMonsterTarget(state, playerState, entityManager);
+ } else if (playerState.attackTargetClassId < 0) {
+ monster = findProjectileContactMonsterTarget(state, playerState, entityManager);
+ if (monster) {
+ playerState.attackTargetName = monster->monsterName;
+ playerState.attackTargetClassId = kRuntimeEntityClassMonster;
+ }
+ } else {
+ monster = nullptr;
+ }
+ monsterEntity = monster ? findSceneRuntimeEntity(entityManager, monster->monsterName) : nullptr;
+ }
+ return monster;
+}
+
void RoomCombat::initializeCombatStates(const RoomSetupState &state) {
_monsterCombatStates.resize(state.roomMonsters.size());
_npcCombatStates.resize(state.roomNpcs.size());
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index 8a262f7f0db..9e73c6cf315 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -129,7 +129,27 @@ public:
void commitMonsterCombatStatesForSceneChange(Common::Array<RoomMonsterCombatState> &&states);
void resetForSceneRefresh(const RoomSetupState &state);
+ void capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const;
+ MonsterRecord *resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager, const Common::Point &mousePos) const;
+ // The caller clears the captured target before resolving the room hover state.
+ void capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const;
+ void captureOverlappingNpcAttackTarget(RoomSetupState &state, RoomPlayerState &playerState, EntityManager *entityManager) const;
+ NpcRecord *selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager, Entity *&npcEntity) const;
+ MonsterRecord *selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, Entity *&monsterEntity) const;
+
private:
+ MonsterRecord *findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const;
+ MonsterRecord *findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const;
+ NpcRecord *findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const;
+ bool playerAttackCanReachTarget(RoomSetupState &state, const RoomPlayerState &playerState, Entity *targetEntity, int engageDistance = 0) const;
+ MonsterRecord *findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+ EntityManager *entityManager) const;
+
Common::Array<RoomMonsterCombatState> _monsterCombatStates;
Common::Array<RoomNpcCombatState> _npcCombatStates;
};
@@ -192,6 +212,7 @@ struct ScopedDeferredLiveNpcDeathTransitions {
Script *_script;
};
+const char *describeCombatTargetClass(int classId);
bool shouldRetainNpcDeathEntityInCurrentRoom(const NpcRecord &npc);
bool isRetainedCurrentRoomNpcDeathRecord(const NpcRecord &npc);
bool resolveCombatLoadoutHudInfo(int loadout, CombatLoadoutHudInfo &info);
Commit: 60d34b8f08ef51bc3d689e73bd2bc06f0b6229e8
https://github.com/scummvm/scummvm/commit/60d34b8f08ef51bc3d689e73bd2bc06f0b6229e8
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor combat interaction handling and NPC death transitions
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
engines/harvester/room_interaction.cpp
engines/harvester/room_interaction.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 4aff9a22223..f3ebdbbd8ed 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -1061,22 +1061,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
_engine.getScript());
- auto findRoomNpcRecordByName = [&](const Common::String &npcName) -> NpcRecord * {
- for (NpcRecord &npc : scene.state.roomNpcs) {
- if (npc.npcName.equalsIgnoreCase(npcName))
- return &npc;
- }
- return nullptr;
- };
- auto findRoomNpcIndexByName = [&](const Common::String &npcName) -> int {
- for (uint i = 0; i < scene.state.roomNpcs.size(); ++i) {
- if (scene.state.roomNpcs[i].npcName.equalsIgnoreCase(npcName))
- return (int)i;
- }
-
- return -1;
- };
auto findRoomMonsterIndexByName = [&](const Common::String &monsterName) -> int {
for (uint i = 0; i < scene.state.roomMonsters.size(); ++i) {
if (scene.state.roomMonsters[i].monsterName.equalsIgnoreCase(monsterName))
@@ -1088,131 +1073,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
auto findSceneRuntimeEntity = [&](const Common::String &entityName) -> Entity * {
return entityManager ? entityManager->findSceneEntityByName(entityName) : nullptr;
};
- auto queueInnerSanctumHerrillDeathAfterMuck = [&](const MonsterRecord &monster) {
- if (!monster.roomName.equalsIgnoreCase(kInnerSanctumRoomName) ||
- !monster.monsterName.equalsIgnoreCase(kMuckeyMonsterName) ||
- !monster.onDeathActionTag.equalsIgnoreCase(kActivateInnerSanctumDoorActionTag)) {
- return false;
- }
-
- Script *script = _engine.getScript();
- NpcRecord *herrill = findRoomNpcRecordByName(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;
- };
-
- auto handleCombatInteraction = [&](const InteractionResult &interaction) -> Common::Error {
- bool didTransition = false;
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(interaction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (!flow.hasPendingMainMenuReturn() &&
- _engine.getScript() &&
- _engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
- session.stopPlayerRegionInteraction();
- startPlayerDefeatSequence("combat_interaction", Common::String(), 1);
- }
- needsRedraw = true;
- return Common::kNoError;
- };
- auto beginNpcDeathTransition = [&](NpcRecord &npc, Entity &npcEntity,
- RoomNpcCombatState &combatState, int deathDamageType) {
- RoomDeathAnimationRange 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;
- };
- auto finalizeNpcDeathTransition = [&](NpcRecord &npc,
- RoomNpcCombatState &combatState) -> Common::Error {
- Script *script = _engine.getScript();
- Entity *npcEntity = findSceneRuntimeEntity(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(kRuntimeEntityHitTestNone);
- 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, npc.roomName)) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
-
- return handleCombatInteraction(interaction);
- };
auto killDebugActiveMonster = [&]() -> Common::Error {
if (!_engine.isCombatDebugEnabled())
return Common::kNoError;
@@ -1291,7 +1152,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
}
- return handleCombatInteraction(interaction);
+ return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
};
auto capturePlayerAttackTarget = [&]() {
playerState.attackTargetName.clear();
@@ -1303,209 +1164,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combat.capturePlayerAttackTarget(scene.state, playerState, entityManager, _mousePos, hoverState.npc);
};
- auto startMonsterHitReaction = [&](MonsterRecord &monster, Entity &monsterEntity,
- RoomMonsterCombatState *combatState, int attackerAttackFirstFrame) {
- if (combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
- needsRedraw = true;
-
- if (!combatState) {
- (void)playRandomRoomAttackSound(_engine,
- monster.hitSound1, monster.hitSound2, monster.hitSound3);
- return;
- }
-
- RoomHitAnimationRange 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;
- };
- auto resolvePlayerAttackContact = [&]() -> Common::Error {
- if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
- playerState.attackContactFrame < 0 ||
- playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
- return Common::kNoError;
- }
-
- playerState.attackContactResolved = true;
- 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;
-
- combat.captureOverlappingNpcAttackTarget(scene.state, playerState, entityManager);
- if (playerState.attackTargetClassId == kRuntimeEntityClassNpc) {
- Entity *npcEntity = nullptr;
- NpcRecord *npc = combat.selectNpcAttackContactTarget(scene.state, playerState, entityManager, npcEntity);
- if (!npc) {
- debugC(1, kDebugCombat,
- "Harvester: combat player attack missed class='npc' target='%s' reason='no reachable npc'",
- playerState.attackTargetName.c_str());
- return Common::kNoError;
- }
-
- 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(npc->npcName);
- RoomNpcCombatState *combatState =
- npcIndex >= 0 && npcIndex < (int)npcCombatStates.size()
- ? &npcCombatStates[(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))
- 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());
- RoomNpcCombatState fallbackState;
- fallbackState.deathDamageType = damageType;
- return finalizeNpcDeathTransition(*npc, combatState ? *combatState : fallbackState);
- }
-
- return Common::kNoError;
- }
-
- Entity *monsterEntity = nullptr;
- MonsterRecord *monster = combat.selectMonsterAttackContactTarget(scene.state, playerState, entityManager, monsterEntity);
- if (!monster || !monsterEntity) {
- debugC(1, kDebugCombat,
- "Harvester: combat player attack missed class='monster' target='%s' reason='no reachable monster'",
- playerState.attackTargetName.c_str());
- return Common::kNoError;
- }
-
- const int hitPointsBefore = monster->currentHitPoints;
- monster->currentHitPoints = MAX(0,
- monster->currentHitPoints - damageAmount);
- const int damageLanded = hitPointsBefore - monster->currentHitPoints;
- const int monsterIndex = findRoomMonsterIndexByName(monster->monsterName);
- RoomMonsterCombatState *combatState =
- monsterIndex >= 0 && monsterIndex < (int)monsterCombatStates.size()
- ? &monsterCombatStates[(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);
- }
- if (monster->currentHitPoints > 0) {
- (void)script->syncRuntimeMonsterRecord(*monster);
- needsRedraw = true;
- return Common::kNoError;
- }
- if (combatState)
- clearRoomMonsterCombatState(*combatState);
- (void)script->syncRuntimeMonsterRecord(*monster);
-
- RoomDeathAnimationRange 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);
- 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, monster->roomName)) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
- if (herrillDeathQueued) {
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
-
- return handleCombatInteraction(interaction);
- };
auto updateRoomMonsterCombat = [&]() -> Common::Error {
if (!entityManager)
return Common::kNoError;
@@ -1529,7 +1187,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
continue;
}
if (!combatState.deathActive && npc.active && npc.deathDamageType != 0) {
- if (beginNpcDeathTransition(npc, *entity, combatState, npc.deathDamageType))
+ if (combat.beginNpcDeathTransition(_engine, npc, *entity, combatState, npc.deathDamageType, needsRedraw))
continue;
debugC(1, kDebugCombat,
@@ -1537,7 +1195,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
npc.npcName.c_str(), npc.deathDamageType, npc.onDeathActionTag.c_str());
combatState.deathDamageType = npc.deathDamageType;
combatState.deathLastFrame = entity->getCurrentFrame();
- return finalizeNpcDeathTransition(npc, combatState);
+ return combat.finalizeNpcDeathTransition(_engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
}
if (!combatState.deathActive)
continue;
@@ -1548,7 +1206,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
entity->setAnimationFrameRange(combatState.deathLastFrame, combatState.deathLastFrame, false);
entity->setCurrentFrame(combatState.deathLastFrame);
entity->setAnimationEnabled(false);
- return finalizeNpcDeathTransition(npc, combatState);
+ return combat.finalizeNpcDeathTransition(_engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
}
for (uint i = 0; i < scene.state.roomMonsters.size(); ++i) {
@@ -1577,7 +1235,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monster.runtimeState,
monster.onDeathActionTag.c_str());
clearRoomMonsterCombatState(combatState);
- const bool herrillDeathQueued = queueInnerSanctumHerrillDeathAfterMuck(monster);
+ const bool herrillDeathQueued = combat.queueInnerSanctumHerrillDeathAfterMuck(_engine, scene.state, monster);
InteractionResult interaction;
interaction.mutatedRuntimeState = true;
@@ -1596,7 +1254,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
interaction.visualRuntimeStateChanged = true;
}
- return handleCombatInteraction(interaction);
+ return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
}
continue;
}
@@ -3010,7 +2668,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
Common::Error combatError = Common::kNoError;
if (!playerControlPaused) {
- combatError = resolvePlayerAttackContact();
+ combatError = combat.resolvePlayerAttackContact(_engine, scene.state, playerState, entityManager,
+ sceneRuntime, combatEffects, interactionProcessor, needsRedraw);
if (combatError.getCode() != Common::kNoError)
return combatError;
if (flow.hasPendingMainMenuReturn())
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index 4210bd4ab7e..b2c5785ec21 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -28,6 +28,8 @@
#include "harvester/detection.h"
#include "harvester/harvester.h"
#include "harvester/player.h"
+#include "harvester/room_interaction.h"
+#include "harvester/room_scene_runtime.h"
#include "harvester/runtime_entity.h"
namespace Harvester {
@@ -82,6 +84,24 @@ static NpcRecord *findRoomNpcRecordByName(RoomSetupState &state, const Common::S
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))
+ return (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 (int)i;
+ }
+
+ return -1;
+}
+
static bool isKillableMonster(const MonsterRecord *monster) {
return monster && monster->active && monster->visible && monster->currentHitPoints > 0;
}
@@ -948,6 +968,328 @@ MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &stat
return monster;
}
+bool RoomCombat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, 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(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 RoomCombat::beginNpcDeathTransition(HarvesterEngine &engine,
+ NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const {
+ RoomDeathAnimationRange 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 RoomCombat::finalizeNpcDeathTransition(HarvesterEngine &engine,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
+ RoomInteractionProcessor &interactionProcessor, 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(kRuntimeEntityHitTestNone);
+ 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, npc.roomName)) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+
+ return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
+void RoomCombat::startMonsterHitReaction(HarvesterEngine &engine,
+ RoomCombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, RoomMonsterCombatState *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;
+ }
+
+ RoomHitAnimationRange 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 RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
+ RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ RoomCombatEffects &combatEffects,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
+ playerState.attackContactFrame < 0 ||
+ playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
+ return Common::kNoError;
+ }
+
+ playerState.attackContactResolved = true;
+ 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;
+
+ captureOverlappingNpcAttackTarget(state, playerState, entityManager);
+ if (playerState.attackTargetClassId == kRuntimeEntityClassNpc) {
+ Entity *npcEntity = nullptr;
+ NpcRecord *npc = selectNpcAttackContactTarget(state, playerState, entityManager, npcEntity);
+ if (!npc) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player attack missed class='npc' target='%s' reason='no reachable npc'",
+ playerState.attackTargetName.c_str());
+ return Common::kNoError;
+ }
+
+ 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(state, npc->npcName);
+ RoomNpcCombatState *combatState =
+ npcIndex >= 0 && npcIndex < (int)_npcCombatStates.size()
+ ? &_npcCombatStates[(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(engine, *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());
+ RoomNpcCombatState fallbackState;
+ fallbackState.deathDamageType = damageType;
+ return finalizeNpcDeathTransition(engine, entityManager, sceneRuntime, *npc, combatState ? *combatState : fallbackState, interactionProcessor, needsRedraw);
+ }
+
+ return Common::kNoError;
+ }
+
+ Entity *monsterEntity = nullptr;
+ MonsterRecord *monster = selectMonsterAttackContactTarget(state, playerState, entityManager, monsterEntity);
+ if (!monster || !monsterEntity) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player attack missed class='monster' target='%s' reason='no reachable monster'",
+ playerState.attackTargetName.c_str());
+ return Common::kNoError;
+ }
+
+ const int hitPointsBefore = monster->currentHitPoints;
+ monster->currentHitPoints = MAX(0,
+ monster->currentHitPoints - damageAmount);
+ const int damageLanded = hitPointsBefore - monster->currentHitPoints;
+ const int monsterIndex = findRoomMonsterIndexByName(state, monster->monsterName);
+ RoomMonsterCombatState *combatState =
+ monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
+ ? &_monsterCombatStates[(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(engine, combatEffects, *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);
+
+ RoomDeathAnimationRange 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);
+ 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(engine, state, *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, monster->roomName)) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+ if (herrillDeathQueued) {
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+
+ return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
void RoomCombat::initializeCombatStates(const RoomSetupState &state) {
_monsterCombatStates.resize(state.roomMonsters.size());
_npcCombatStates.resize(state.roomNpcs.size());
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index 9e73c6cf315..6847f891a28 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -22,6 +22,7 @@
#ifndef HARVESTER_ROOM_COMBAT_H
#define HARVESTER_ROOM_COMBAT_H
+#include "common/error.h"
#include "harvester/room_support.h"
namespace Harvester {
@@ -29,6 +30,9 @@ namespace Harvester {
class Entity;
class EntityManager;
class HarvesterEngine;
+class RoomCombatEffects;
+class RoomInteractionProcessor;
+class RoomSceneRuntime;
extern const int kRoomMonsterAnimationRate;
extern const int kNativeMonsterAttackAnimationRate;
@@ -142,7 +146,23 @@ public:
MonsterRecord *selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, Entity *&monsterEntity) const;
+ bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
+ bool beginNpcDeathTransition(HarvesterEngine &engine,
+ NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
+ Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const;
+ Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
+ RoomSetupState &state, RoomPlayerState &playerState,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ RoomCombatEffects &combatEffects,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+
private:
+ void startMonsterHitReaction(HarvesterEngine &engine,
+ RoomCombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, RoomMonsterCombatState *combatState,
+ int attackerAttackFirstFrame, bool &needsRedraw) const;
+
MonsterRecord *findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const;
MonsterRecord *findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const;
NpcRecord *findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const;
diff --git a/engines/harvester/room_interaction.cpp b/engines/harvester/room_interaction.cpp
index 464759091bf..b0991ace310 100644
--- a/engines/harvester/room_interaction.cpp
+++ b/engines/harvester/room_interaction.cpp
@@ -50,6 +50,23 @@ RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow
_canExitCloseupToParent(canExitCloseupToParent), _callbacks(callbacks) {
}
+Common::Error RoomInteractionProcessor::handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw) {
+ bool didTransition = false;
+ Common::Error interactionError =
+ handleInteractionResult(interaction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (!_flow.hasPendingMainMenuReturn() &&
+ _engine.getScript() &&
+ _engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
+ _callbacks.stopPlayerRegionInteraction();
+ _callbacks.startPlayerDefeatSequence("combat_interaction", Common::String(), 1);
+ }
+
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
Common::Error RoomInteractionProcessor::handleInteractionResult(const InteractionResult &interaction,
bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement) {
didTransition = false;
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/room_interaction.h
index 0fcdb578441..28142232c52 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/room_interaction.h
@@ -61,6 +61,7 @@ public:
bool &pendingRoomChangeUsesSavedRoomState, SaveRoomState &pendingRoomChangeSavedRoomState,
bool canExitCloseupToParent, RoomInteractionCallbacks &callbacks);
+ Common::Error handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw);
Common::Error handleInteractionResult(const InteractionResult &interaction,
bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement = true);
Common::Error runScriptedDialogue(const Common::String &npcName, const Common::String &usedItemName,
Commit: 79ef7090566f6e5f49525e99ecdadba50f4fa39e
https://github.com/scummvm/scummvm/commit/79ef7090566f6e5f49525e99ecdadba50f4fa39e
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor NPC combat state handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index f3ebdbbd8ed..eb51c57efc1 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -435,7 +435,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomCombat &combat = session.getCombat();
combat.initializeCombatStates(scene.state);
Common::Array<RoomMonsterCombatState> &monsterCombatStates = combat.getMonsterCombatStates();
- Common::Array<RoomNpcCombatState> &npcCombatStates = combat.getNpcCombatStates();
RoomSceneRuntime &sceneRuntime = session.getSceneRuntime();
RoomCombatEffects &combatEffects = session.getCombatEffects();
ResolvedText inspectText;
@@ -1178,36 +1177,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combat.resizeCombatStates(scene.state);
- for (uint i = 0; i < scene.state.roomNpcs.size(); ++i) {
- NpcRecord &npc = scene.state.roomNpcs[i];
- RoomNpcCombatState &combatState = npcCombatStates[i];
- Entity *entity = findSceneRuntimeEntity(npc.npcName);
- if (!entity || !npc.visible || npc.deathOrMonsterfyFlag) {
- clearRoomNpcCombatState(combatState);
- continue;
- }
- if (!combatState.deathActive && npc.active && npc.deathDamageType != 0) {
- if (combat.beginNpcDeathTransition(_engine, 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();
- return combat.finalizeNpcDeathTransition(_engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
- }
- 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);
- return combat.finalizeNpcDeathTransition(_engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
- }
+ const RoomNpcCombatUpdateResult npcResult = combat.updateRoomNpcCombat(_engine, scene.state,
+ entityManager, sceneRuntime, interactionProcessor, needsRedraw);
+ if (npcResult.finalizedDeath)
+ return npcResult.error;
for (uint i = 0; i < scene.state.roomMonsters.size(); ++i) {
MonsterRecord &monster = scene.state.roomMonsters[i];
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index b2c5785ec21..ecfb46ca11d 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -968,6 +968,47 @@ MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &stat
return monster;
}
+RoomNpcCombatUpdateResult RoomCombat::updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ RoomNpcCombatUpdateResult result;
+ for (uint i = 0; i < state.roomNpcs.size(); ++i) {
+ NpcRecord &npc = state.roomNpcs[i];
+ RoomNpcCombatState &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(engine, 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(engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, 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(engine, entityManager, sceneRuntime, npc, combatState, interactionProcessor, needsRedraw);
+ return result;
+ }
+ return result;
+}
+
bool RoomCombat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const {
if (!monster.roomName.equalsIgnoreCase(kInnerSanctumRoomName) ||
!monster.monsterName.equalsIgnoreCase(kMuckeyMonsterName) ||
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index 6847f891a28..ebc7286f763 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -120,10 +120,14 @@ struct RoomNpcCombatState {
int deathDamageType = 0;
};
+struct RoomNpcCombatUpdateResult {
+ Common::Error error = Common::kNoError;
+ bool finalizedDeath = false;
+};
+
class RoomCombat {
public:
Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
- Common::Array<RoomNpcCombatState> &getNpcCombatStates() { return _npcCombatStates; }
void initializeCombatStates(const RoomSetupState &state);
void resizeCombatStates(const RoomSetupState &state);
@@ -147,11 +151,10 @@ public:
EntityManager *entityManager, Entity *&monsterEntity) const;
bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
- bool beginNpcDeathTransition(HarvesterEngine &engine,
- NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
- Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const;
+ // Called after resizeCombatStates; NPC finalization ends the entire combat update.
+ RoomNpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
@@ -159,6 +162,12 @@ public:
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
private:
+ bool beginNpcDeathTransition(HarvesterEngine &engine,
+ NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
+ Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const;
+
void startMonsterHitReaction(HarvesterEngine &engine,
RoomCombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, RoomMonsterCombatState *combatState,
int attackerAttackFirstFrame, bool &needsRedraw) const;
Commit: 1f2fc6454ede8b87d4dd6c23af67662b479e6f11
https://github.com/scummvm/scummvm/commit/1f2fc6454ede8b87d4dd6c23af67662b479e6f11
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: more RoomCombat refactoring
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
engines/harvester/room_interaction.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index eb51c57efc1..6feeeb98ddc 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -434,7 +434,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.turnTargetFacing = -1;
RoomCombat &combat = session.getCombat();
combat.initializeCombatStates(scene.state);
- Common::Array<RoomMonsterCombatState> &monsterCombatStates = combat.getMonsterCombatStates();
RoomSceneRuntime &sceneRuntime = session.getSceneRuntime();
RoomCombatEffects &combatEffects = session.getCombatEffects();
ResolvedText inspectText;
@@ -980,6 +979,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
session.resetIdleState();
}
+ void notePlayerActivity() override {
+ session.notePlayerActivity();
+ }
+
void stopPlayerRegionInteraction() override {
session.stopPlayerRegionInteraction();
}
@@ -1061,98 +1064,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
_engine.getScript());
- auto findRoomMonsterIndexByName = [&](const Common::String &monsterName) -> int {
- for (uint i = 0; i < scene.state.roomMonsters.size(); ++i) {
- if (scene.state.roomMonsters[i].monsterName.equalsIgnoreCase(monsterName))
- return (int)i;
- }
-
- return -1;
- };
- auto findSceneRuntimeEntity = [&](const Common::String &entityName) -> Entity * {
- return entityManager ? entityManager->findSceneEntityByName(entityName) : nullptr;
- };
-
- auto killDebugActiveMonster = [&]() -> Common::Error {
- if (!_engine.isCombatDebugEnabled())
- return Common::kNoError;
- MonsterRecord *monster = combat.resolveKillTarget(scene.state, playerState, entityManager, _mousePos);
- Entity *monsterEntity = monster ? findSceneRuntimeEntity(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 == kRuntimeEntityClassMonster &&
- playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- }
-
- 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(monster->monsterName);
- RoomMonsterCombatState *combatState =
- monsterIndex >= 0 && monsterIndex < (int)monsterCombatStates.size()
- ? &monsterCombatStates[(uint)monsterIndex]
- : nullptr;
- if (combatState)
- clearRoomMonsterCombatState(*combatState);
- (void)script->syncRuntimeMonsterRecord(*monster);
-
- RoomDeathAnimationRange 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);
- 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, monster->roomName)) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
-
- return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
- };
auto capturePlayerAttackTarget = [&]() {
playerState.attackTargetName.clear();
playerState.attackTargetClassId = -1;
@@ -1163,329 +1074,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combat.capturePlayerAttackTarget(scene.state, playerState, entityManager, _mousePos, hoverState.npc);
};
- auto updateRoomMonsterCombat = [&]() -> Common::Error {
- if (!entityManager)
- return Common::kNoError;
-
- Script &script = *_engine.getScript();
- const uint32 now = Player::getRuntimeClockTicks();
- const uint32 moveInterval = MAX<uint32>(1, 100U / (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;
-
- combat.resizeCombatStates(scene.state);
-
- const RoomNpcCombatUpdateResult npcResult = combat.updateRoomNpcCombat(_engine, scene.state,
- entityManager, sceneRuntime, interactionProcessor, needsRedraw);
- if (npcResult.finalizedDeath)
- return npcResult.error;
-
- for (uint i = 0; i < scene.state.roomMonsters.size(); ++i) {
- MonsterRecord &monster = scene.state.roomMonsters[i];
- RoomMonsterCombatState &combatState = monsterCombatStates[i];
- Entity *entity = findSceneRuntimeEntity(monster.monsterName);
- if (!entity || !monster.visible) {
- clearRoomMonsterCombatState(combatState);
- 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 = combat.queueInnerSanctumHerrillDeathAfterMuck(_engine, scene.state, monster);
-
- InteractionResult interaction;
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- if (!monster.onDeathActionTag.empty()) {
- InteractionResult deathInteraction;
- if (script.executeActionTag(
- monster.onDeathActionTag, deathInteraction, true, monster.roomName)) {
- interaction = Common::move(deathInteraction);
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
- }
- if (herrillDeathQueued) {
- interaction.mutatedRuntimeState = true;
- interaction.visualRuntimeStateChanged = true;
- }
-
- return interactionProcessor.handleCombatInteraction(interaction, 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) {
- 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, (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;
- }
- 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);
- if (idleState.entity) {
- idleState.entity->setVisible(false);
- idleState.entity->setAnimationRate(0);
- }
- idleState.active = false;
- idleState.loopStarted = false;
- idleState.exiting = false;
- idleState.restoreFacing = -1;
- playerState.entity->setVisible(true);
- if (playerHitPointsAfter > 0 &&
- Player::startHitAnimation(_engine, playerState, combatState.attackFirstFrame)) {
- session.notePlayerActivity();
- }
- }
- if (changed && playerHitPointsAfter <= 0) {
- playerAlive = false;
- session.stopPlayerRegionInteraction();
- 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;
- // Native room-entry monster spawns seed a left/right pursue state immediately
- // from the player's relative X position. Our room-combat shim does not model
- // those actor-state bytes directly, so allow one immediate pursue evaluation
- // before the first animation-driven movement tick lands.
- 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 - (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);
- 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);
- } 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, (float)monster.posZ) * (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;
- 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, (float)monster.posZ);
- if (monster.posZ != previousZ)
- entityManager->reinsertSceneEntity(entity);
- (void)script.syncRuntimeMonsterRecord(monster);
- debugC(1, 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,
- previousX, previousY, previousZ, monster.posX, monster.posY, monster.posZ, monster.facing);
- needsRedraw = true;
- needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, true) || needsRedraw;
- continue;
- }
-
- if (attemptedMoveX || attemptedMoveZ) {
- debugC(1, 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.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);
- // Native class-6 monsters store `now + random(99)` when they arm an attack, then
- // apply the fixed 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;
-
- RoomAttackAnimationRange 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);
- 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, (double)zDelta,
- engageDistance, kNativeMonsterAttackAnimationRate, combatState.attackCooldownSeedTick,
- closeRangeBypassDistance);
- needsRedraw = true;
- }
-
- return Common::kNoError;
- };
auto moveRoomItemDirectlyToInventory = [&](const ObjectRecord &object) -> Common::Error {
Script *script = _engine.getScript();
if (!script)
@@ -2510,7 +2098,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
event.kbd.keycode == Common::KEYCODE_RCTRL) {
attackModifierHeld = true;
} else if (event.kbd.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
- Common::Error debugKillError = killDebugActiveMonster();
+ Common::Error debugKillError = combat.killDebugActiveMonster(_engine, scene.state, playerState, entityManager,
+ _mousePos, combatEffects, interactionProcessor, needsRedraw);
if (debugKillError.getCode() != Common::kNoError)
return debugKillError;
break;
@@ -2719,7 +2308,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
}
}
- combatError = updateRoomMonsterCombat();
+ combatError = combat.updateRoomMonsterCombat(_engine, scene.state, playerState, idleState,
+ entityManager, sceneRuntime, combatEffects, _cheats, interactionCallbacks, interactionProcessor, needsRedraw);
if (combatError.getCode() != Common::kNoError)
return combatError;
if (combatEffects.syncCombatHitEffects())
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index ecfb46ca11d..dfd27b7a156 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -25,6 +25,7 @@
#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"
@@ -1171,6 +1172,412 @@ void RoomCombat::startMonsterHitReaction(HarvesterEngine &engine,
needsRedraw = true;
}
+Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
+ RoomPlayerState &playerState, EntityManager *entityManager, const Common::Point &mousePos,
+ RoomCombatEffects &combatEffects, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ if (!engine.isCombatDebugEnabled())
+ return Common::kNoError;
+ MonsterRecord *monster = resolveKillTarget(state, playerState, entityManager, 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 == kRuntimeEntityClassMonster &&
+ playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ }
+
+ 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(state, monster->monsterName);
+ RoomMonsterCombatState *combatState =
+ monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
+ ? &_monsterCombatStates[(uint)monsterIndex]
+ : nullptr;
+ if (combatState)
+ clearRoomMonsterCombatState(*combatState);
+ (void)script->syncRuntimeMonsterRecord(*monster);
+
+ RoomDeathAnimationRange 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);
+ 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, monster->roomName)) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+
+ return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
+}
+
+Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
+ RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
+ RoomSceneRuntime &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
+ RoomInteractionCallbacks &callbacks, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ if (!entityManager)
+ return Common::kNoError;
+
+ Script &script = *engine.getScript();
+ const uint32 now = Player::getRuntimeClockTicks();
+ const uint32 moveInterval = MAX<uint32>(1, 100U / (uint32)kRoomMonsterAnimationRate);
+ const int horizontalStepBase = 8;
+ const int depthStep = MAX<int>(1, roundRoomCombatFloat(
+ state.roomZVelocityStep > 0.0f ? state.roomZVelocityStep : 1.0f));
+ bool playerAlive = script.getPlayerCurrentHitPoints() > 0;
+
+ resizeCombatStates(state);
+
+ const RoomNpcCombatUpdateResult npcResult = updateRoomNpcCombat(engine, state,
+ entityManager, sceneRuntime, interactionProcessor, needsRedraw);
+ if (npcResult.finalizedDeath)
+ return npcResult.error;
+
+ for (uint i = 0; i < state.roomMonsters.size(); ++i) {
+ MonsterRecord &monster = state.roomMonsters[i];
+ RoomMonsterCombatState &combatState = _monsterCombatStates[i];
+ Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
+ if (!entity || !monster.visible) {
+ clearRoomMonsterCombatState(combatState);
+ 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(engine, state, monster);
+
+ InteractionResult interaction;
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ if (!monster.onDeathActionTag.empty()) {
+ InteractionResult deathInteraction;
+ if (script.executeActionTag(
+ monster.onDeathActionTag, deathInteraction, true, monster.roomName)) {
+ interaction = Common::move(deathInteraction);
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+ }
+ if (herrillDeathQueued) {
+ interaction.mutatedRuntimeState = true;
+ interaction.visualRuntimeStateChanged = true;
+ }
+
+ return interactionProcessor.handleCombatInteraction(interaction, 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) {
+ 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(
+ state, *entity, monster.posX, monster.posY, (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;
+ }
+ 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);
+ if (idleState.entity) {
+ idleState.entity->setVisible(false);
+ idleState.entity->setAnimationRate(0);
+ }
+ idleState.active = false;
+ idleState.loopStarted = false;
+ idleState.exiting = false;
+ idleState.restoreFacing = -1;
+ playerState.entity->setVisible(true);
+ if (playerHitPointsAfter > 0 &&
+ Player::startHitAnimation(engine, playerState, 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 - (float)monster.posZ;
+ const float absZDelta = zDelta >= 0.0f ? zDelta : -zDelta;
+ const int engageDistance = MAX(0, monster.engageDistance);
+ const int liveWaypointTolerance = roundRoomCombatFloat(
+ Player::computeDepthScale(state,
+ MIN<float>(playerState.z, (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(state, (float)monster.posZ);
+ monster.posY = mapRoomDepthToScreenYForCombat(state, (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(state, (float)monster.posZ) * (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;
+ 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(state, *entity, monster.posX, monster.posY, (float)monster.posZ);
+ if (monster.posZ != previousZ)
+ entityManager->reinsertSceneEntity(entity);
+ (void)script.syncRuntimeMonsterRecord(monster);
+ debugC(1, 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,
+ previousX, previousY, previousZ, monster.posX, monster.posY, monster.posZ, monster.facing);
+ needsRedraw = true;
+ needsRedraw = setRoomMonsterAnimation(*entity, monster.facing, true) || needsRedraw;
+ continue;
+ }
+
+ if (attemptedMoveX || attemptedMoveZ) {
+ debugC(1, 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.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);
+ // 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;
+
+ RoomAttackAnimationRange 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);
+ 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, (double)zDelta,
+ engageDistance, kNativeMonsterAttackAnimationRate, combatState.attackCooldownSeedTick,
+ closeRangeBypassDistance);
+ needsRedraw = true;
+ }
+
+ return Common::kNoError;
+}
+
Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index ebc7286f763..95a3fac1b3a 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -27,10 +27,12 @@
namespace Harvester {
+class CheatSystem;
class Entity;
class EntityManager;
class HarvesterEngine;
class RoomCombatEffects;
+class RoomInteractionCallbacks;
class RoomInteractionProcessor;
class RoomSceneRuntime;
@@ -127,10 +129,7 @@ struct RoomNpcCombatUpdateResult {
class RoomCombat {
public:
- Common::Array<RoomMonsterCombatState> &getMonsterCombatStates() { return _monsterCombatStates; }
-
void initializeCombatStates(const RoomSetupState &state);
- void resizeCombatStates(const RoomSetupState &state);
// Keep prepared state separate until scene-entity mutations succeed.
Common::Array<RoomMonsterCombatState> prepareMonsterCombatStatesForSceneChange(
const Common::Array<MonsterRecord> &previousMonsters, const Common::Array<MonsterRecord> &updatedMonsters) const;
@@ -150,11 +149,14 @@ public:
MonsterRecord *selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, Entity *&monsterEntity) const;
- bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
- // Called after resizeCombatStates; NPC finalization ends the entire combat update.
- RoomNpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ Common::Error killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
+ RoomPlayerState &playerState, EntityManager *entityManager, const Common::Point &mousePos,
+ RoomCombatEffects &combatEffects, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+
+ Common::Error updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
+ RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
+ RoomSceneRuntime &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
+ RoomInteractionCallbacks &callbacks, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
@@ -162,6 +164,13 @@ public:
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
private:
+ void resizeCombatStates(const RoomSetupState &state);
+ bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
+ // Called after resizeCombatStates; NPC finalization ends the entire combat update.
+ RoomNpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
+ EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+
bool beginNpcDeathTransition(HarvesterEngine &engine,
NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/room_interaction.h
index 28142232c52..92e5d9217ba 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/room_interaction.h
@@ -48,6 +48,7 @@ public:
virtual void applyPlayerGotoXZ(int x, int z) = 0;
virtual Common::Error runModalShowText(const ResolvedText &modalText) = 0;
virtual void resetIdleState() = 0;
+ virtual void notePlayerActivity() = 0;
virtual void stopPlayerRegionInteraction() = 0;
virtual void startPlayerDefeatSequence(const char *reason, const Common::String &sourceName,
int damageType) = 0;
Commit: dd34002fcccda52d836fe9c5ac0d638a765631f3
https://github.com/scummvm/scummvm/commit/dd34002fcccda52d836fe9c5ac0d638a765631f3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor room item interactions some inventory handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_interaction.cpp
engines/harvester/room_interaction.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 6feeeb98ddc..3d6f35971b3 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -51,7 +51,6 @@ namespace Harvester {
static const uint32 kRoomExitFastClickWindowTicks = 20;
static const char *const kPlayerInventoryLabel = "your inventory";
-static const char *const kInventoryOwnerName = "INVENTORY";
static const byte kTransparentPaletteIndex = 0;
static const float kNormalPaletteBrightness = 1.0f;
static const float kDimPaletteBrightness = 0.6f;
@@ -159,12 +158,6 @@ static Common::String resolveStartupNpcLabel(const NpcRecord &npc) {
return label;
}
-static Common::String resolveCarriedObjectSpritePath(const ObjectRecord &object) {
- if (!object.altSpritePath.empty())
- return object.altSpritePath;
- return object.spritePath;
-}
-
static bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
Common::Array<byte> data;
if (!resources.loadFile(path, data) || data.size() < 12)
@@ -206,24 +199,6 @@ static void renderCdChangePromptScreen(HarvesterEngine &engine, const IndexedBit
screen->update();
}
-static bool shouldDispatchPickupActionOnCarryStart(const ObjectRecord &object) {
- if (object.actionTag.empty())
- return false;
-
- return !object.objectName.equalsIgnoreCase("SANDWICH") &&
- !object.objectName.equalsIgnoreCase("SANDWICH2") &&
- !object.objectName.equalsIgnoreCase("SYRINGE");
-}
-
-static bool shouldDispatchPickupActionOnDirectInventoryTransfer(const ObjectRecord &object) {
- if (object.actionTag.empty())
- return false;
-
- return !object.objectName.equalsIgnoreCase("ST_ASPRIN") &&
- !object.objectName.equalsIgnoreCase("ST_COUGHM") &&
- !object.objectName.equalsIgnoreCase("ST_VITAMN");
-}
-
static void drawRoomPrompt(Graphics::Screen &screen, const Graphics::Font &font,
const Common::String &promptText, bool useNativeFont) {
if (promptText.empty())
@@ -467,17 +442,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
uint32 nextKeyboardAttackAllowedTick = 0;
RoomIdleAnimationState &idleState = session.getIdleState();
bool needsRedraw = true;
- Common::String carriedRoomItemName;
- Common::String carriedRoomItemLabel;
- IndexedBitmap carriedRoomItemBitmap;
- auto hasCarriedRoomItem = [&]() {
- return !carriedRoomItemName.empty();
- };
- auto clearCarriedRoomItem = [&]() {
- carriedRoomItemName.clear();
- carriedRoomItemLabel.clear();
- carriedRoomItemBitmap = IndexedBitmap();
- };
+ RoomItemInteractions roomItems(_engine, scene, sceneRuntime, _inventory, pendingRoomChange);
+
auto queueCombatLoadoutStatusMessage = [&](const Common::String &message) {
if (message.empty()) {
const bool hadMessage = !combatLoadoutStatusText.value.empty();
@@ -494,16 +460,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
Player::getRuntimeClockTicks() + kCombatLoadoutStatusDisplayTicks;
needsRedraw = true;
};
- auto hideSceneObject = [&](const Common::String &objectName, const Common::String *ownerOrRoom) {
- ObjectRecord *sceneObject = findSceneObjectByName(scene.sceneObjects, objectName);
- if (!sceneObject)
- return;
-
- sceneObject->visible = false;
- sceneObject->runtimeVisible = false;
- if (ownerOrRoom)
- sceneObject->currentOwnerOrRoom = *ownerOrRoom;
- };
session.resetIdleState();
@@ -521,44 +477,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
monster.posX, monster.posY, (float)monster.posZ);
}
};
- auto stowCarriedRoomItemToInventory = [&]() {
- if (!hasCarriedRoomItem())
- return true;
-
- _engine.getScript()->addRuntimeObjectToInventory(carriedRoomItemName);
- clearCarriedRoomItem();
- return _inventory.refresh();
- };
- auto shouldCancelDeniedPickup = [&](const InteractionResult &interaction,
- bool didTransition) {
- return !didTransition &&
- !interaction.mutatedRuntimeState &&
- !interaction.requestPlayerGotoXZ &&
- !interaction.requestPlayerGotoZ &&
- interaction.lightingCommand == kStartupLightingCommandNone &&
- !interaction.requestMainMenu &&
- !interaction.requestDemoEnding &&
- interaction.dialogueNpcName.empty() &&
- interaction.dialogueContinuationTag.empty() &&
- interaction.continuationTag.empty() &&
- interaction.nextRoomName.empty() &&
- interaction.cutscenePath.empty() &&
- !interaction.modalText.value.empty();
- };
- auto restoreDeniedPickup = [&](Script &script,
- const ObjectRecord &savedObject, bool clearCarryState) -> Common::Error {
- if (!script.syncRuntimeObjectRecord(savedObject))
- return Common::kReadingFailed;
- if (clearCarryState)
- clearCarriedRoomItem();
- if (!session.applyCurrentRoomRuntimeMutationsInPlace(true) && !session.refreshCurrentScene(true))
- return Common::kReadingFailed;
- if (!_inventory.refresh())
- return Common::kReadingFailed;
- needsRedraw = true;
- return Common::kNoError;
- };
auto openInventoryOverlay = [&]() {
moveLeft = false;
moveRight = false;
@@ -569,7 +488,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
playerState.hasMoveTarget = false;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return false;
return _inventory.open();
};
@@ -1074,184 +993,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combat.capturePlayerAttackTarget(scene.state, playerState, entityManager, _mousePos, hoverState.npc);
};
- auto moveRoomItemDirectlyToInventory = [&](const ObjectRecord &object) -> Common::Error {
- Script *script = _engine.getScript();
- if (!script)
- return Common::kReadingFailed;
-
- const ObjectRecord savedObject = object;
-
- script->addRuntimeObjectToInventory(object.objectName);
- const Common::String inventoryOwner(kInventoryOwnerName);
- hideSceneObject(object.objectName, &inventoryOwner);
- sceneRuntime.removeSceneEntityByName(object.objectName);
-
- bool didTransition = false;
- if (shouldDispatchPickupActionOnDirectInventoryTransfer(object)) {
- InteractionResult pickupInteraction;
- if (script->executeActionTag(
- object.actionTag, pickupInteraction, true, scene.state.roomName)) {
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- pickupInteraction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (shouldCancelDeniedPickup(pickupInteraction, didTransition)) {
- debugC(1, kDebugRoom,
- "Harvester: cancelling denied direct pickup object='%s' action='%s' text='%s'",
- savedObject.objectName.c_str(), savedObject.actionTag.c_str(),
- pickupInteraction.modalText.value.c_str());
- return restoreDeniedPickup(*script, savedObject, false);
- }
- }
- }
-
- if (!_inventory.refresh())
- return Common::kReadingFailed;
- needsRedraw = true;
- return Common::kNoError;
- };
- auto beginRoomItemCarry = [&](const ObjectRecord &object) -> Common::Error {
- Script *script = _engine.getScript();
- ResourceManager *resources = _engine.getResources();
- if (!script || !resources)
- return Common::kReadingFailed;
-
- const ObjectRecord savedObject = object;
-
- clearCarriedRoomItem();
- carriedRoomItemName = object.objectName;
- carriedRoomItemLabel = script->resolveObjectLabel(object);
- const Common::String spritePath = resolveCarriedObjectSpritePath(object);
- if (!spritePath.empty())
- loadBitmapResource(*resources, spritePath, carriedRoomItemBitmap);
-
- script->setRuntimeObjectVisible(object.currentOwnerOrRoom, object.objectName, false);
- hideSceneObject(object.objectName, nullptr);
- sceneRuntime.removeSceneEntityByName(object.objectName);
-
- bool didTransition = false;
- if (shouldDispatchPickupActionOnCarryStart(object)) {
- InteractionResult pickupInteraction;
- if (script->executeActionTag(
- object.actionTag, pickupInteraction, true, scene.state.roomName)) {
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- pickupInteraction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (shouldCancelDeniedPickup(pickupInteraction, didTransition)) {
- debugC(1, kDebugRoom,
- "Harvester: cancelling denied carried pickup object='%s' action='%s' text='%s'",
- savedObject.objectName.c_str(), savedObject.actionTag.c_str(),
- pickupInteraction.modalText.value.c_str());
- return restoreDeniedPickup(*script, savedObject, true);
- }
- if (!didTransition &&
- !pickupInteraction.dialogueNpcName.empty() &&
- !script->isObjectInInventory(savedObject.objectName)) {
- debugC(1, kDebugRoom,
- "Harvester: restoring carried pickup after dialogue object='%s' action='%s' npc='%s'",
- savedObject.objectName.c_str(), savedObject.actionTag.c_str(),
- pickupInteraction.dialogueNpcName.c_str());
- return restoreDeniedPickup(*script, savedObject, true);
- }
- }
- }
-
- if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- needsRedraw = true;
- return Common::kNoError;
- }
-
- needsRedraw = true;
- return Common::kNoError;
- };
- auto handleInventoryTargetInteraction = [&](const ObjectRecord &target) -> Common::Error {
- if (!_inventory.hasSelection())
- return Common::kNoError;
-
- const Common::String selectedItemName = _inventory.getSelectedItemName();
- InteractionResult interaction;
- const bool handled = _engine.getScript()->resolveUseItemInteraction(
- selectedItemName, target, interaction, scene.state.roomName);
- if (!handled) {
- debugC(1, kDebugInventory,
- "Harvester: inventory target miss selected='%s' target='%s' owner='%s' action='%s'",
- selectedItemName.c_str(), target.objectName.c_str(),
- target.currentOwnerOrRoom.c_str(), target.actionTag.c_str());
- return Common::kNoError;
- }
-
- StartupRoomTransitionKind roomTransition = interaction.roomTransition;
- if (roomTransition == kStartupRoomTransitionNone && !interaction.nextRoomName.empty())
- roomTransition = kStartupRoomTransitionCloseup;
-
- // Flow shares one inventory system across nested room loops. Clear the held item
- // before entering a closeup so it is not still carried while the child room loop
- // is active.
- if (!interaction.nextRoomName.empty() &&
- roomTransition != kStartupRoomTransitionChangeRoom)
- _inventory.clearSelection();
-
- bool didTransition = false;
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(interaction, didTransition, selectedItemName);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- _inventory.clearSelection();
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!_inventory.refresh())
- return Common::kReadingFailed;
-
- needsRedraw = true;
- return Common::kNoError;
- };
- auto findSelectedInventoryRoomTarget = [&](const Common::Point &point) -> ObjectRecord * {
- if (!_inventory.hasSelection())
- return nullptr;
-
- EntityManager *entityManager = _engine.getRuntimeEntities();
- Script *script = _engine.getScript();
- if (!entityManager || !script)
- return nullptr;
-
- const Common::String selectedItemName = _inventory.getSelectedItemName();
- ObjectRecord *bestTarget = nullptr;
- int bestDrawIndex = -1;
- for (ObjectRecord &candidate : scene.sceneObjects) {
- if (candidate.objectName.empty() ||
- !script->hasUseItemInteraction(selectedItemName, candidate)) {
- continue;
- }
-
- const Entity *entity = entityManager->findSceneEntityByName(candidate.objectName);
- if (!entity || !entity->hitTest(point))
- continue;
- if (entity->getClassId() == kRuntimeEntityClassBackground ||
- entity->getClassId() == kRuntimeEntityClassPlayer ||
- entity->getClassId() == kRuntimeEntityClassRectHotspot19) {
- continue;
- }
-
- const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
- if (drawIndex < 0)
- continue;
- if (!bestTarget || drawIndex > bestDrawIndex) {
- bestTarget = &candidate;
- bestDrawIndex = drawIndex;
- }
- }
-
- return bestTarget;
- };
auto queueRegionInteraction = [&](const RegionRecord ®ion) {
pendingRegionName = region.regionName;
if (!playerState.entity)
@@ -1364,7 +1105,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
pendingRoomChangeIsRoomName = false;
pendingRoomChangeUsesSavedRoomState = false;
pendingRoomChangeSavedRoomState.clear();
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -1377,17 +1118,17 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
pendingRoomChange = _engine.getPendingLoadedSaveRoomState().roomName;
pendingRoomChangeIsRoomName = true;
}
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
if (flow.hasPendingDebugRoomChange()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
return Common::kNoError;
}
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -1403,7 +1144,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -1432,7 +1173,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
const bool inventorySelectionActive = _inventory.hasSelection();
- const bool roomCarryActive = hasCarriedRoomItem();
+ const bool roomCarryActive = roomItems.hasCarriedRoomItem();
const bool activeCarry = inventorySelectionActive || roomCarryActive;
const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_mousePos);
const bool suppressHover = showingInspectText || idleState.active || idleState.exiting ||
@@ -1443,7 +1184,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
: resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
scene.sceneRegions, _mousePos, flow._menuTextConfig, &flow._dialogue);
if (!suppressHover && inventorySelectionActive && !hoverState.npc) {
- if (ObjectRecord *selectedTarget = findSelectedInventoryRoomTarget(_mousePos))
+ if (ObjectRecord *selectedTarget = roomItems.findSelectedInventoryRoomTarget(_mousePos))
hoverState.object = selectedTarget;
}
Common::String promptText;
@@ -1476,7 +1217,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
_inventory.setPromptText(promptText);
} else {
promptText = buildUseItemPrompt(
- flow._menuTextConfig, carriedRoomItemLabel, targetLabel);
+ flow._menuTextConfig, roomItems.getCarriedRoomItemLabel(), targetLabel);
}
hoverState.cursorSequence = 7;
} else if (_inventory.isOpen()) {
@@ -1515,10 +1256,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
if (inventorySelectionActive) {
_inventory.drawSelectedDragItem(*activeScreen, _mousePos);
- } else if (roomCarryActive && carriedRoomItemBitmap.isValid()) {
- const int drawX = _mousePos.x - (int)carriedRoomItemBitmap.width / 2;
- const int drawY = _mousePos.y - (int)carriedRoomItemBitmap.height / 2;
- blitBitmap(*activeScreen, carriedRoomItemBitmap, drawX, drawY);
+ } else if (roomCarryActive && roomItems.getCarriedRoomItemBitmap().isValid()) {
+ const int drawX = _mousePos.x - (int)roomItems.getCarriedRoomItemBitmap().width / 2;
+ const int drawY = _mousePos.y - (int)roomItems.getCarriedRoomItemBitmap().height / 2;
+ blitBitmap(*activeScreen, roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
}
if (showingInspectText) {
@@ -1675,8 +1416,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
break;
}
- if (hasCarriedRoomItem()) {
- if (!stowCarriedRoomItemToInventory())
+ if (roomItems.hasCarriedRoomItem()) {
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
needsRedraw = true;
break;
@@ -1773,7 +1514,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
_inventory.getSelectedItemName().c_str(),
inventoryHover->object.objectName.c_str());
Common::Error interactionError =
- handleInventoryTargetInteraction(inventoryHover->object);
+ roomItems.handleInventoryTargetInteraction(inventoryHover->object, interactionProcessor, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
}
@@ -1796,7 +1537,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
_mousePos, flow._menuTextConfig, &flow._dialogue);
ObjectRecord *selectedRoomTarget = nullptr;
if (_inventory.hasSelection() && !hoverState.npc)
- selectedRoomTarget = findSelectedInventoryRoomTarget(_mousePos);
+ selectedRoomTarget = roomItems.findSelectedInventoryRoomTarget(_mousePos);
if (selectedRoomTarget &&
(!hoverState.object ||
!selectedRoomTarget->objectName.equalsIgnoreCase(hoverState.object->objectName))) {
@@ -1816,10 +1557,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
clickHoverState.npc ? clickHoverState.npc->npcName.c_str() : "",
clickHoverState.region ? clickHoverState.region->regionName.c_str() : "",
clickHoverState.cursorSequence, clickHoverState.promptText.c_str());
- if (hasCarriedRoomItem()) {
+ if (roomItems.hasCarriedRoomItem()) {
if (clickHoverState.playerEntity && playerState.entity &&
clickHoverState.playerEntity == playerState.entity) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
needsRedraw = true;
}
@@ -1845,7 +1586,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
: nullptr;
if (roomTarget) {
Common::Error interactionError =
- handleInventoryTargetInteraction(*roomTarget);
+ roomItems.handleInventoryTargetInteraction(*roomTarget, interactionProcessor, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
needsRedraw = true;
@@ -1966,22 +1707,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
break;
}
if (_engine.getScript()->isPickupObject(*clickedObject)) {
- InteractionResult blockedPickupInteraction;
- if (_engine.getScript()->isPickupBlockedByAction(
- *clickedObject, &blockedPickupInteraction)) {
- bool didTransition = false;
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- blockedPickupInteraction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- needsRedraw = true;
- break;
- }
-
- Common::Error pickupError = playerState.entity
- ? beginRoomItemCarry(*clickedObject)
- : moveRoomItemDirectlyToInventory(*clickedObject);
+ Common::Error pickupError = roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
+ interactionProcessor, interactionCallbacks, needsRedraw);
if (pickupError.getCode() != Common::kNoError)
return pickupError;
break;
@@ -2119,7 +1846,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
} else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (session.cancelPlayerAttackAnimation())
needsRedraw = true;
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
moveLeft = false;
moveRight = false;
@@ -2150,7 +1877,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (event.kbd.keycode == Common::KEYCODE_RETURN ||
event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
Common::Error exitError = runRoomExitCommands();
if (exitError.getCode() != Common::kNoError)
@@ -2176,7 +1903,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
}
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -2205,7 +1932,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -2216,7 +1943,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -2237,7 +1964,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
@@ -2317,7 +2044,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (flow.hasPendingMainMenuReturn())
return Common::kNoError;
if (!pendingRoomChange.empty()) {
- if (!stowCarriedRoomItemToInventory())
+ if (!roomItems.stowCarriedRoomItemToInventory())
return Common::kReadingFailed;
break;
}
diff --git a/engines/harvester/room_interaction.cpp b/engines/harvester/room_interaction.cpp
index b0991ace310..5ca7ff6613f 100644
--- a/engines/harvester/room_interaction.cpp
+++ b/engines/harvester/room_interaction.cpp
@@ -29,6 +29,8 @@
#include "harvester/flow.h"
#include "harvester/fst_player.h"
#include "harvester/harvester.h"
+#include "harvester/inventory.h"
+#include "harvester/room_scene_runtime.h"
#include "harvester/resources.h"
#include "harvester/runtime_entity.h"
@@ -36,6 +38,311 @@ namespace Harvester {
const char *const kExitCloseupPendingRoomChange = "__EXIT_CLOSEUP__";
+static const char *const kInventoryOwnerName = "INVENTORY";
+
+static Common::String resolveCarriedObjectSpritePath(const ObjectRecord &object) {
+ if (!object.altSpritePath.empty())
+ return object.altSpritePath;
+ return object.spritePath;
+}
+
+static bool shouldDispatchPickupActionOnCarryStart(const ObjectRecord &object) {
+ if (object.actionTag.empty())
+ return false;
+
+ return !object.objectName.equalsIgnoreCase("SANDWICH") &&
+ !object.objectName.equalsIgnoreCase("SANDWICH2") &&
+ !object.objectName.equalsIgnoreCase("SYRINGE");
+}
+
+static bool shouldDispatchPickupActionOnDirectInventoryTransfer(const ObjectRecord &object) {
+ if (object.actionTag.empty())
+ return false;
+
+ return !object.objectName.equalsIgnoreCase("ST_ASPRIN") &&
+ !object.objectName.equalsIgnoreCase("ST_COUGHM") &&
+ !object.objectName.equalsIgnoreCase("ST_VITAMN");
+}
+
+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;
+
+ bitmap = IndexedBitmap();
+ return decodeBitmap(data, bitmap);
+}
+
+RoomItemInteractions::RoomItemInteractions(HarvesterEngine &engine, RoomSceneResources &scene,
+ RoomSceneRuntime &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange)
+ : _engine(engine), _scene(scene), _sceneRuntime(sceneRuntime), _inventory(inventory),
+ _pendingRoomChange(pendingRoomChange) {
+}
+
+bool RoomItemInteractions::hasCarriedRoomItem() const {
+ return !_carriedRoomItemName.empty();
+}
+
+void RoomItemInteractions::clearCarriedRoomItem() {
+ _carriedRoomItemName.clear();
+ _carriedRoomItemLabel.clear();
+ _carriedRoomItemBitmap = IndexedBitmap();
+}
+
+void RoomItemInteractions::hideSceneObject(const Common::String &objectName, const Common::String *ownerOrRoom) {
+ ObjectRecord *sceneObject = findSceneObjectByName(_scene.sceneObjects, objectName);
+ if (!sceneObject)
+ return;
+
+ sceneObject->visible = false;
+ sceneObject->runtimeVisible = false;
+ if (ownerOrRoom)
+ sceneObject->currentOwnerOrRoom = *ownerOrRoom;
+}
+
+bool RoomItemInteractions::stowCarriedRoomItemToInventory() {
+ if (!hasCarriedRoomItem())
+ return true;
+
+ _engine.getScript()->addRuntimeObjectToInventory(_carriedRoomItemName);
+ clearCarriedRoomItem();
+ return _inventory.refresh();
+}
+
+bool RoomItemInteractions::shouldCancelDeniedPickup(const InteractionResult &interaction, bool didTransition) const {
+ return !didTransition &&
+ !interaction.mutatedRuntimeState &&
+ !interaction.requestPlayerGotoXZ &&
+ !interaction.requestPlayerGotoZ &&
+ interaction.lightingCommand == kStartupLightingCommandNone &&
+ !interaction.requestMainMenu &&
+ !interaction.requestDemoEnding &&
+ interaction.dialogueNpcName.empty() &&
+ interaction.dialogueContinuationTag.empty() &&
+ interaction.continuationTag.empty() &&
+ interaction.nextRoomName.empty() &&
+ interaction.cutscenePath.empty() &&
+ !interaction.modalText.value.empty();
+}
+
+Common::Error RoomItemInteractions::restoreDeniedPickup(Script &script, const ObjectRecord &savedObject, bool clearCarryState,
+ RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+ if (!script.syncRuntimeObjectRecord(savedObject))
+ return Common::kReadingFailed;
+ if (clearCarryState)
+ clearCarriedRoomItem();
+
+ if (!callbacks.applyCurrentRoomRuntimeMutationsInPlace(true) && !callbacks.refreshCurrentScene(true))
+ return Common::kReadingFailed;
+ if (!_inventory.refresh())
+ return Common::kReadingFailed;
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
+Common::Error RoomItemInteractions::moveRoomItemDirectlyToInventory(const ObjectRecord &object,
+ RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+ Script *script = _engine.getScript();
+ if (!script)
+ return Common::kReadingFailed;
+
+ const ObjectRecord savedObject = object;
+
+ script->addRuntimeObjectToInventory(object.objectName);
+ const Common::String inventoryOwner(kInventoryOwnerName);
+ hideSceneObject(object.objectName, &inventoryOwner);
+ _sceneRuntime.removeSceneEntityByName(object.objectName);
+
+ bool didTransition = false;
+ if (shouldDispatchPickupActionOnDirectInventoryTransfer(object)) {
+ InteractionResult pickupInteraction;
+ if (script->executeActionTag(
+ object.actionTag, pickupInteraction, true, _scene.state.roomName)) {
+ Common::Error interactionError =
+ interactionProcessor.handleInteractionResult(
+ pickupInteraction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (interactionProcessor.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ if (shouldCancelDeniedPickup(pickupInteraction, didTransition)) {
+ debugC(1, kDebugRoom,
+ "Harvester: cancelling denied direct pickup object='%s' action='%s' text='%s'",
+ savedObject.objectName.c_str(), savedObject.actionTag.c_str(),
+ pickupInteraction.modalText.value.c_str());
+ return restoreDeniedPickup(*script, savedObject, false, callbacks, needsRedraw);
+ }
+ }
+ }
+
+ if (!_inventory.refresh())
+ return Common::kReadingFailed;
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
+Common::Error RoomItemInteractions::beginRoomItemCarry(const ObjectRecord &object,
+ RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+ Script *script = _engine.getScript();
+ ResourceManager *resources = _engine.getResources();
+ if (!script || !resources)
+ return Common::kReadingFailed;
+
+ const ObjectRecord savedObject = object;
+
+ clearCarriedRoomItem();
+ _carriedRoomItemName = object.objectName;
+ _carriedRoomItemLabel = script->resolveObjectLabel(object);
+ const Common::String spritePath = resolveCarriedObjectSpritePath(object);
+ if (!spritePath.empty())
+ loadBitmapResource(*resources, spritePath, _carriedRoomItemBitmap);
+
+ script->setRuntimeObjectVisible(object.currentOwnerOrRoom, object.objectName, false);
+ hideSceneObject(object.objectName, nullptr);
+ _sceneRuntime.removeSceneEntityByName(object.objectName);
+
+ bool didTransition = false;
+ if (shouldDispatchPickupActionOnCarryStart(object)) {
+ InteractionResult pickupInteraction;
+ if (script->executeActionTag(
+ object.actionTag, pickupInteraction, true, _scene.state.roomName)) {
+ Common::Error interactionError =
+ interactionProcessor.handleInteractionResult(
+ pickupInteraction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (interactionProcessor.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ if (shouldCancelDeniedPickup(pickupInteraction, didTransition)) {
+ debugC(1, kDebugRoom,
+ "Harvester: cancelling denied carried pickup object='%s' action='%s' text='%s'",
+ savedObject.objectName.c_str(), savedObject.actionTag.c_str(),
+ pickupInteraction.modalText.value.c_str());
+ return restoreDeniedPickup(*script, savedObject, true, callbacks, needsRedraw);
+ }
+ if (!didTransition &&
+ !pickupInteraction.dialogueNpcName.empty() &&
+ !script->isObjectInInventory(savedObject.objectName)) {
+ debugC(1, kDebugRoom,
+ "Harvester: restoring carried pickup after dialogue object='%s' action='%s' npc='%s'",
+ savedObject.objectName.c_str(), savedObject.actionTag.c_str(),
+ pickupInteraction.dialogueNpcName.c_str());
+ return restoreDeniedPickup(*script, savedObject, true, callbacks, needsRedraw);
+ }
+ }
+ }
+
+ if (!_pendingRoomChange.empty()) {
+ if (!stowCarriedRoomItemToInventory())
+ return Common::kReadingFailed;
+ needsRedraw = true;
+ return Common::kNoError;
+ }
+
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
+Common::Error RoomItemInteractions::handleInventoryTargetInteraction(const ObjectRecord &target,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ if (!_inventory.hasSelection())
+ return Common::kNoError;
+
+ const Common::String selectedItemName = _inventory.getSelectedItemName();
+ InteractionResult interaction;
+ const bool handled = _engine.getScript()->resolveUseItemInteraction(
+ selectedItemName, target, interaction, _scene.state.roomName);
+ if (!handled) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory target miss selected='%s' target='%s' owner='%s' action='%s'",
+ selectedItemName.c_str(), target.objectName.c_str(),
+ target.currentOwnerOrRoom.c_str(), target.actionTag.c_str());
+ return Common::kNoError;
+ }
+
+ StartupRoomTransitionKind roomTransition = interaction.roomTransition;
+ if (roomTransition == kStartupRoomTransitionNone && !interaction.nextRoomName.empty())
+ roomTransition = kStartupRoomTransitionCloseup;
+
+ // Flow shares one inventory system across nested room loops. Clear the held item
+ // before entering a closeup so it is not still carried while the child room loop
+ // is active.
+ if (!interaction.nextRoomName.empty() &&
+ roomTransition != kStartupRoomTransitionChangeRoom)
+ _inventory.clearSelection();
+
+ bool didTransition = false;
+ Common::Error interactionError =
+ interactionProcessor.handleInteractionResult(interaction, didTransition, selectedItemName);
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ _inventory.clearSelection();
+ if (interactionProcessor.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ if (!_inventory.refresh())
+ return Common::kReadingFailed;
+
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
+ObjectRecord *RoomItemInteractions::findSelectedInventoryRoomTarget(const Common::Point &point) {
+ if (!_inventory.hasSelection())
+ return nullptr;
+
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ Script *script = _engine.getScript();
+ if (!entityManager || !script)
+ return nullptr;
+
+ const Common::String selectedItemName = _inventory.getSelectedItemName();
+ ObjectRecord *bestTarget = nullptr;
+ int bestDrawIndex = -1;
+ for (ObjectRecord &candidate : _scene.sceneObjects) {
+ if (candidate.objectName.empty() ||
+ !script->hasUseItemInteraction(selectedItemName, candidate)) {
+ continue;
+ }
+
+ const Entity *entity = entityManager->findSceneEntityByName(candidate.objectName);
+ if (!entity || !entity->hitTest(point))
+ continue;
+ if (entity->getClassId() == kRuntimeEntityClassBackground ||
+ entity->getClassId() == kRuntimeEntityClassPlayer ||
+ entity->getClassId() == kRuntimeEntityClassRectHotspot19) {
+ continue;
+ }
+
+ const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
+ if (drawIndex < 0)
+ continue;
+ if (!bestTarget || drawIndex > bestDrawIndex) {
+ bestTarget = &candidate;
+ bestDrawIndex = drawIndex;
+ }
+ }
+
+ return bestTarget;
+}
+
+Common::Error RoomItemInteractions::handleRoomItemPickup(const ObjectRecord &object, bool playerPresent,
+ RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+ InteractionResult blockedPickupInteraction;
+ if (_engine.getScript()->isPickupBlockedByAction(object, &blockedPickupInteraction)) {
+ bool didTransition = false;
+ Common::Error interactionError = interactionProcessor.handleInteractionResult(
+ blockedPickupInteraction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ needsRedraw = true;
+ return Common::kNoError;
+ }
+
+ return playerPresent
+ ? beginRoomItemCarry(object, interactionProcessor, callbacks, needsRedraw)
+ : moveRoomItemDirectlyToInventory(object, interactionProcessor, callbacks, needsRedraw);
+}
+
RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow,
RoomSceneResources &scene, RoomPlayerState &playerState,
Common::String &pendingRegionName, Common::String &pendingRoomChange,
@@ -50,6 +357,10 @@ RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow
_canExitCloseupToParent(canExitCloseupToParent), _callbacks(callbacks) {
}
+bool RoomInteractionProcessor::hasPendingMainMenuReturn() const {
+ return _flow.hasPendingMainMenuReturn();
+}
+
Common::Error RoomInteractionProcessor::handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw) {
bool didTransition = false;
Common::Error interactionError =
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/room_interaction.h
index 92e5d9217ba..d6e0f50cd21 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/room_interaction.h
@@ -23,6 +23,7 @@
#define HARVESTER_ROOM_INTERACTION_H
#include "common/error.h"
+#include "harvester/bitmap.h"
#include "harvester/room_support.h"
#include "harvester/saveload.h"
@@ -30,6 +31,9 @@ namespace Harvester {
class Flow;
class HarvesterEngine;
+class InventorySystem;
+class RoomInteractionProcessor;
+class RoomSceneRuntime;
struct IndexedBitmap;
extern const char *const kExitCloseupPendingRoomChange;
@@ -54,6 +58,42 @@ public:
int damageType) = 0;
};
+class RoomItemInteractions {
+public:
+ RoomItemInteractions(HarvesterEngine &engine, RoomSceneResources &scene,
+ RoomSceneRuntime &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange);
+
+ const Common::String &getCarriedRoomItemLabel() const { return _carriedRoomItemLabel; }
+ const IndexedBitmap &getCarriedRoomItemBitmap() const { return _carriedRoomItemBitmap; }
+ bool hasCarriedRoomItem() const;
+ bool stowCarriedRoomItemToInventory();
+ Common::Error handleInventoryTargetInteraction(const ObjectRecord &target,
+ RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ ObjectRecord *findSelectedInventoryRoomTarget(const Common::Point &point);
+ Common::Error handleRoomItemPickup(const ObjectRecord &object, bool playerPresent,
+ RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+
+private:
+ void clearCarriedRoomItem();
+ void hideSceneObject(const Common::String &objectName, const Common::String *ownerOrRoom);
+ bool shouldCancelDeniedPickup(const InteractionResult &interaction, bool didTransition) const;
+ Common::Error restoreDeniedPickup(Script &script, const ObjectRecord &savedObject, bool clearCarryState,
+ RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+ Common::Error moveRoomItemDirectlyToInventory(const ObjectRecord &object,
+ RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+ Common::Error beginRoomItemCarry(const ObjectRecord &object,
+ RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+
+ HarvesterEngine &_engine;
+ RoomSceneResources &_scene;
+ RoomSceneRuntime &_sceneRuntime;
+ InventorySystem &_inventory;
+ const Common::String &_pendingRoomChange;
+ Common::String _carriedRoomItemName;
+ Common::String _carriedRoomItemLabel;
+ IndexedBitmap _carriedRoomItemBitmap;
+};
+
class RoomInteractionProcessor {
public:
RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene,
@@ -62,6 +102,7 @@ public:
bool &pendingRoomChangeUsesSavedRoomState, SaveRoomState &pendingRoomChangeSavedRoomState,
bool canExitCloseupToParent, RoomInteractionCallbacks &callbacks);
+ bool hasPendingMainMenuReturn() const;
Common::Error handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw);
Common::Error handleInteractionResult(const InteractionResult &interaction,
bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement = true);
Commit: 47ccd38593428f6cc27ef55cd43b07bb0022e3e4
https://github.com/scummvm/scummvm/commit/47ccd38593428f6cc27ef55cd43b07bb0022e3e4
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor region interaction handling in RoomInteractionProcessor
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/room.cpp
engines/harvester/room_interaction.cpp
engines/harvester/room_interaction.h
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 3d6f35971b3..98916bb1446 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -993,101 +993,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combat.capturePlayerAttackTarget(scene.state, playerState, entityManager, _mousePos, hoverState.npc);
};
- auto queueRegionInteraction = [&](const RegionRecord ®ion) {
- pendingRegionName = region.regionName;
- if (!playerState.entity)
- return;
-
- const bool playerOverlapsRegion = doesPlayerOverlapRegion(*playerState.entity, region);
- if (_engine.isPathfindingDebugEnabled()) {
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- const Common::Rect regionBounds(region.left, region.top, region.right + 1, region.bottom + 1);
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding region click room='%s' mouse=(%d,%d) region='%s' bounds=(%d,%d)-(%d,%d) z=[%d,%d] start=%d cursor=%d desired_facing=%d player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d) overlaps=%d action='%s'",
- scene.state.roomName.c_str(), _mousePos.x, _mousePos.y,
- region.regionName.c_str(),
- regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
- region.minZ, region.maxZ, region.startEnabled, region.cursorEnabled,
- region.desiredFacing,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
- playerOverlapsRegion, region.actionTag.c_str());
- }
- if (playerOverlapsRegion && doesPlayerFacingMatchRegion(playerState.facing, region))
- return;
-
- Player::setRegionMoveTarget(scene.state, playerState,
- Player::resolveRegionTargetX(scene.state, region, playerState),
- Player::resolveRegionTargetZ(region));
- if (_engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding region target room='%s' region='%s' target=(%d,%d,z=%.2f)",
- scene.state.roomName.c_str(), region.regionName.c_str(),
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
- }
- };
- auto runRegionInteraction = [&](const RegionRecord ®ion) -> Common::Error {
- InteractionResult interaction;
- if (!_engine.getScript()->resolveRegionInteraction(region, interaction, scene.state.roomName))
- return Common::kNoError;
- debugC(2, kDebugRoom,
- "Harvester: region interaction room='%s' region='%s' action='%s' player=(%d,%d,z=%.2f) next_room='%s'",
- scene.state.roomName.c_str(), region.regionName.c_str(), region.actionTag.c_str(),
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- interaction.nextRoomName.c_str());
-
- bool didTransition = false;
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- interaction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
-
- if (!_inventory.refresh())
- return Common::kReadingFailed;
- needsRedraw = true;
- return Common::kNoError;
- };
- auto tryActivatePendingRegion = [&]() -> Common::Error {
- if (pendingRegionName.empty() || !playerState.entity)
- return Common::kNoError;
-
- const RegionRecord *region = findSceneRegionByName(scene.sceneRegions, pendingRegionName);
- if (!region || !region->startEnabled) {
- pendingRegionName.clear();
- return Common::kNoError;
- }
- if (!doesPlayerOverlapRegion(*playerState.entity, *region))
- return Common::kNoError;
- if (!doesPlayerFacingMatchRegion(playerState.facing, *region)) {
- if (!playerState.hasMoveTarget && !playerState.turnActive && region->desiredFacing >= 0)
- (void)Player::startTurnAnimation(playerState, region->desiredFacing);
- return Common::kNoError;
- }
-
- session.stopPlayerRegionInteraction();
- return runRegionInteraction(*region);
- };
- auto tryActivatePassiveRegion = [&]() -> Common::Error {
- if (!playerState.entity)
- return Common::kNoError;
-
- for (const RegionRecord ®ion : scene.sceneRegions) {
- if (!region.startEnabled)
- continue;
- if (!doesPlayerOverlapRegion(*playerState.entity, region))
- continue;
- if (!doesPlayerFacingMatchRegion(playerState.facing, region))
- continue;
-
- session.stopPlayerRegionInteraction();
- return runRegionInteraction(region);
- }
-
- return Common::kNoError;
- };
if (!_inventory.refresh())
return Common::kReadingFailed;
Graphics::FrameLimiter limiter(g_system, 60);
@@ -1630,7 +1535,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
hoverState.region->startEnabled &&
roomAllowsImmediateExitClick(scene.state.roomName)) {
session.stopPlayerRegionInteraction();
- Common::Error interactionError = runRegionInteraction(*hoverState.region);
+ Common::Error interactionError =
+ interactionProcessor.runRegionInteraction(*hoverState.region, _inventory, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
if (flow.hasPendingMainMenuReturn())
@@ -1638,8 +1544,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
break;
}
- queueRegionInteraction(*hoverState.region);
- Common::Error interactionError = tryActivatePendingRegion();
+ interactionProcessor.queueRegionInteraction(*hoverState.region, _mousePos);
+ Common::Error interactionError =
+ interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
return interactionError;
needsRedraw = true;
@@ -1926,7 +1833,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
Common::Error pendingRegionError =
session.isPlayerCombatLocked()
? Common::kNoError
- : tryActivatePendingRegion();
+ : interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
if (pendingRegionError.getCode() != Common::kNoError)
return pendingRegionError;
if (flow.hasPendingMainMenuReturn())
@@ -1937,7 +1844,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
break;
}
if (!session.isPlayerCombatLocked()) {
- pendingRegionError = tryActivatePassiveRegion();
+ pendingRegionError = interactionProcessor.tryActivatePassiveRegion(_inventory, needsRedraw);
if (pendingRegionError.getCode() != Common::kNoError)
return pendingRegionError;
if (flow.hasPendingMainMenuReturn())
@@ -2011,9 +1918,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (playerCanAct &&
!playerState.attackActive && !playerState.hitActive &&
!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
- const RegionRecord *pendingRegion = findSceneRegionByName(scene.sceneRegions, pendingRegionName);
- const int regionFacing = pendingRegion && pendingRegion->startEnabled
- ? pendingRegion->desiredFacing : -1;
+ const int regionFacing = interactionProcessor.getPendingRegionFacing();
if (Player::stepKeyboardMovement(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
playerState, moveLeft, moveRight, moveUp, moveDown)) {
session.notePlayerActivity();
diff --git a/engines/harvester/room_interaction.cpp b/engines/harvester/room_interaction.cpp
index 5ca7ff6613f..4110a8ae5aa 100644
--- a/engines/harvester/room_interaction.cpp
+++ b/engines/harvester/room_interaction.cpp
@@ -30,6 +30,7 @@
#include "harvester/fst_player.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
+#include "harvester/player.h"
#include "harvester/room_scene_runtime.h"
#include "harvester/resources.h"
#include "harvester/runtime_entity.h"
@@ -357,6 +358,110 @@ RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow
_canExitCloseupToParent(canExitCloseupToParent), _callbacks(callbacks) {
}
+void RoomInteractionProcessor::queueRegionInteraction(const RegionRecord ®ion, const Common::Point &mousePos) {
+ _pendingRegionName = region.regionName;
+ if (!_playerState.entity)
+ return;
+
+ const bool playerOverlapsRegion = doesPlayerOverlapRegion(*_playerState.entity, region);
+ if (_engine.isPathfindingDebugEnabled()) {
+ const Common::Rect playerRect = _playerState.entity->getScreenRect();
+ const Common::Rect regionBounds(region.left, region.top, region.right + 1, region.bottom + 1);
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding region click room='%s' mouse=(%d,%d) region='%s' bounds=(%d,%d)-(%d,%d) z=[%d,%d] start=%d cursor=%d desired_facing=%d player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d) overlaps=%d action='%s'",
+ _scene.state.roomName.c_str(), mousePos.x, mousePos.y,
+ region.regionName.c_str(),
+ regionBounds.left, regionBounds.top, regionBounds.right, regionBounds.bottom,
+ region.minZ, region.maxZ, region.startEnabled, region.cursorEnabled,
+ region.desiredFacing,
+ _playerState.centerX, _playerState.bottomY, (double)_playerState.z,
+ playerRect.left, playerRect.top, playerRect.right, playerRect.bottom,
+ playerOverlapsRegion, region.actionTag.c_str());
+ }
+ if (playerOverlapsRegion && doesPlayerFacingMatchRegion(_playerState.facing, region))
+ return;
+
+ Player::setRegionMoveTarget(_scene.state, _playerState,
+ Player::resolveRegionTargetX(_scene.state, region, _playerState),
+ Player::resolveRegionTargetZ(region));
+ if (_engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding region target room='%s' region='%s' target=(%d,%d,z=%.2f)",
+ _scene.state.roomName.c_str(), region.regionName.c_str(),
+ _playerState.targetX, _playerState.targetBottomY, (double)_playerState.targetZ);
+ }
+}
+
+Common::Error RoomInteractionProcessor::runRegionInteraction(const RegionRecord ®ion,
+ InventorySystem &inventory, bool &needsRedraw) {
+ InteractionResult interaction;
+ if (!_engine.getScript()->resolveRegionInteraction(region, interaction, _scene.state.roomName))
+ return Common::kNoError;
+ debugC(2, kDebugRoom,
+ "Harvester: region interaction room='%s' region='%s' action='%s' player=(%d,%d,z=%.2f) next_room='%s'",
+ _scene.state.roomName.c_str(), region.regionName.c_str(), region.actionTag.c_str(),
+ _playerState.centerX, _playerState.bottomY, (double)_playerState.z,
+ interaction.nextRoomName.c_str());
+
+ bool didTransition = false;
+ Common::Error interactionError =
+ handleInteractionResult(interaction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (_flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+
+ if (!inventory.refresh())
+ return Common::kReadingFailed;
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
+Common::Error RoomInteractionProcessor::tryActivatePendingRegion(InventorySystem &inventory, bool &needsRedraw) {
+ if (_pendingRegionName.empty() || !_playerState.entity)
+ return Common::kNoError;
+
+ const RegionRecord *region = findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
+ if (!region || !region->startEnabled) {
+ _pendingRegionName.clear();
+ return Common::kNoError;
+ }
+ if (!doesPlayerOverlapRegion(*_playerState.entity, *region))
+ return Common::kNoError;
+ if (!doesPlayerFacingMatchRegion(_playerState.facing, *region)) {
+ if (!_playerState.hasMoveTarget && !_playerState.turnActive && region->desiredFacing >= 0)
+ (void)Player::startTurnAnimation(_playerState, region->desiredFacing);
+ return Common::kNoError;
+ }
+
+ _callbacks.stopPlayerRegionInteraction();
+ return runRegionInteraction(*region, inventory, needsRedraw);
+}
+
+Common::Error RoomInteractionProcessor::tryActivatePassiveRegion(InventorySystem &inventory, bool &needsRedraw) {
+ if (!_playerState.entity)
+ return Common::kNoError;
+
+ for (const RegionRecord ®ion : _scene.sceneRegions) {
+ if (!region.startEnabled)
+ continue;
+ if (!doesPlayerOverlapRegion(*_playerState.entity, region))
+ continue;
+ if (!doesPlayerFacingMatchRegion(_playerState.facing, region))
+ continue;
+
+ _callbacks.stopPlayerRegionInteraction();
+ return runRegionInteraction(region, inventory, needsRedraw);
+ }
+
+ return Common::kNoError;
+}
+
+int RoomInteractionProcessor::getPendingRegionFacing() const {
+ const RegionRecord *pendingRegion = findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
+ return pendingRegion && pendingRegion->startEnabled ? pendingRegion->desiredFacing : -1;
+}
+
bool RoomInteractionProcessor::hasPendingMainMenuReturn() const {
return _flow.hasPendingMainMenuReturn();
}
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/room_interaction.h
index d6e0f50cd21..7ae7db80601 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/room_interaction.h
@@ -102,6 +102,13 @@ public:
bool &pendingRoomChangeUsesSavedRoomState, SaveRoomState &pendingRoomChangeSavedRoomState,
bool canExitCloseupToParent, RoomInteractionCallbacks &callbacks);
+ void queueRegionInteraction(const RegionRecord ®ion, const Common::Point &mousePos);
+ Common::Error runRegionInteraction(const RegionRecord ®ion,
+ InventorySystem &inventory, bool &needsRedraw);
+ Common::Error tryActivatePendingRegion(InventorySystem &inventory, bool &needsRedraw);
+ Common::Error tryActivatePassiveRegion(InventorySystem &inventory, bool &needsRedraw);
+ int getPendingRegionFacing() const;
+
bool hasPendingMainMenuReturn() const;
Common::Error handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw);
Common::Error handleInteractionResult(const InteractionResult &interaction,
Commit: a38bca9f3d7a3170f671a745dd8daf8da8adcabf
https://github.com/scummvm/scummvm/commit/a38bca9f3d7a3170f671a745dd8daf8da8adcabf
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor to separate presentation and modal operations
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/room_presentation.cpp
A engines/harvester/room_presentation.h
engines/harvester/flow.cpp
engines/harvester/flow.h
engines/harvester/menu.cpp
engines/harvester/menu.h
engines/harvester/module.mk
engines/harvester/room.cpp
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 6c3fc4fe55b..ae98fb4a091 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1747,6 +1747,40 @@ Common::Error Flow::runMainMenuStub() {
return _menu.runMainMenuStub(*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::runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, bool canSaveGame) {
return _menu.runRoomMenuStub(backdrop, palette, paletteBrightness, *this, canSaveGame);
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 92d9e15dc5d..776e9f650e8 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -52,6 +52,8 @@ public:
bool buildDialogueSaveStateBlob(Common::Array<byte> &blob, uint32 saveVersion);
bool loadDialogueSaveStateBlob(const Common::Array<byte> &blob, uint32 saveVersion);
Common::Error run();
+ Common::Error showCdChangePrompt(int discNumber);
+ bool handleSystemEvent(const Common::Event &event, Common::Error &result);
private:
friend class HarvesterEngine;
@@ -115,7 +117,6 @@ private:
void resetRoomNpcDialogueState();
void resetCursorAnimationSequence();
bool tickRuntimeEntities();
- bool handleSystemEvent(const Common::Event &event, Common::Error &result);
HarvesterEngine &_engine;
Common::Array<Common::String> _quickTips;
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index c9485fb58f8..ecf0537af55 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -146,6 +146,7 @@ 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;
class ScopedSceneTimerPause {
@@ -442,6 +443,22 @@ static void applyMenuPalette(Graphics::Screen &screen, const HarvesterEngine &en
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)
@@ -845,6 +862,78 @@ MenuSystem::MenuSystem(HarvesterEngine &engine, Common::Point &mousePos,
: _engine(engine), _mousePos(mousePos), _menuItems(menuItems) {
}
+Common::Error MenuSystem::showCdChangePrompt(int discNumber, ResourceManager &resources,
+ Flow &flow, bool &promptCompleted) {
+ promptCompleted = false;
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return Common::kReadingFailed;
+
+ const Common::String bitmapPath = Common::String::format("1:/GRAPHIC/OTHER/CD%d.BM", discNumber);
+ IndexedBitmap promptBitmap;
+ byte promptPalette[256 * 3];
+ if (!loadBitmapResource(resources, bitmapPath, promptBitmap) ||
+ !loadPaletteResource(resources, kCdChangePromptPalettePath, promptPalette)) {
+ warning("Harvester: unable to load CD change prompt assets for disc %d", discNumber);
+ return Common::kReadingFailed;
+ }
+
+ debugC(1, kDebugScene, "Harvester: showing CD change prompt for disc %d", discNumber);
+ _engine.stopMusic();
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (flow.handleSystemEvent(event, result))
+ return result;
+ if (event.type == Common::EVENT_MOUSEMOVE)
+ _mousePos = event.mouse;
+ }
+
+ bool waitingForRelease = false;
+ bool needsRedraw = true;
+ bool activateRequestedDisc = false;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit() && !activateRequestedDisc) {
+ if (needsRedraw) {
+ renderCdChangePromptScreen(_engine, promptBitmap, promptPalette);
+ needsRedraw = false;
+ }
+
+ 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:
+ _mousePos = event.mouse;
+ needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ waitingForRelease = true;
+ break;
+ case Common::EVENT_LBUTTONUP:
+ if (waitingForRelease)
+ activateRequestedDisc = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ if (entityManager && entityManager->syncCursorEntityPosition(_mousePos))
+ needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+ promptCompleted = true;
+ return Common::kNoError;
+}
+
Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
const Art *art = _engine.getArt();
if (!art)
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index e077f4af1c7..be59851e1ac 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -36,6 +36,7 @@ namespace Harvester {
class HarvesterEngine;
class Flow;
+class ResourceManager;
struct MenuTextConfig {
Common::Array<Common::String> optionItems;
@@ -91,6 +92,8 @@ public:
Common::Error runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, Flow &flow, bool canSaveGame);
Common::Error validateParentalPassword(Flow &flow);
+ Common::Error showCdChangePrompt(int discNumber, ResourceManager &resources,
+ Flow &flow, bool &promptCompleted);
private:
Common::Error runLoadGameMenu(const byte *palette, float paletteBrightness,
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index c3cc27e9b8c..de8ce1ecbb2 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -19,6 +19,7 @@ MODULE_OBJS = \
resources.o \
room_combat.o \
room_interaction.o \
+ room_presentation.o \
room_scene_runtime.o \
room_session.o \
room.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 98916bb1446..984f1bd4a55 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -34,7 +34,6 @@
#include "harvester/detection.h"
#include "harvester/fst_player.h"
#include "harvester/harvester.h"
-#include "harvester/palette_utils.h"
#include "harvester/resources.h"
#include "harvester/runtime_entity.h"
#include "harvester/art.h"
@@ -44,6 +43,7 @@
#include "harvester/player.h"
#include "harvester/room_combat.h"
#include "harvester/room_interaction.h"
+#include "harvester/room_presentation.h"
#include "harvester/room_session.h"
#include "harvester/room_support.h"
@@ -51,9 +51,7 @@ namespace Harvester {
static const uint32 kRoomExitFastClickWindowTicks = 20;
static const char *const kPlayerInventoryLabel = "your inventory";
-static const byte kTransparentPaletteIndex = 0;
-static const float kNormalPaletteBrightness = 1.0f;
-static const float kDimPaletteBrightness = 0.6f;
+
static const int kNativeInventoryDragCloseLeft = 0x45;
static const int kNativeInventoryDragCloseTop = 0x4b;
static const int kNativeInventoryDragCloseRight = 0x239;
@@ -67,7 +65,6 @@ static const uint32 kCombatLoadoutStatusDisplayTicks = 75;
static const int kNativeCombatResourceIconsX = 10;
static const int kNativeCombatResourceIconsY = 30;
static const int kNativeCombatResourceIconSpacing = 15;
-static const char *const kCdChangePromptPalettePath = "1:/GRAPHIC/PAL/CD1.PAL";
static bool roomAllowsImmediateExitClick(const Common::String &roomName) {
return !roomName.equalsIgnoreCase("LAVAPIT") &&
@@ -100,41 +97,6 @@ static void debugLogInventoryClick(const char *buttonLabel, const Common::Point
InventorySystem::isStatusObject(inventoryHover->object));
}
-static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
- if (!bitmap.isValid())
- return;
-
- int destX = x;
- int destY = y;
- int srcX = 0;
- int srcY = 0;
- int width = (int)bitmap.width;
- int height = (int)bitmap.height;
-
- if (destX < 0) {
- srcX = -destX;
- width += destX;
- destX = 0;
- }
- if (destY < 0) {
- srcY = -destY;
- height += destY;
- destY = 0;
- }
- if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
- return;
-
- width = MIN<int>(width, screen.w - destX);
- height = MIN<int>(height, screen.h - destY);
- if (width <= 0 || height <= 0)
- return;
-
- const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
- byte *dst = (byte *)screen.getBasePtr(destX, destY);
- Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
- screen.format.bytesPerPixel, kTransparentPaletteIndex);
-}
-
static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
const Text *text = engine.getText();
if (!text || !fontName)
@@ -158,47 +120,6 @@ static Common::String resolveStartupNpcLabel(const NpcRecord &npc) {
return label;
}
-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;
-
- bitmap = IndexedBitmap();
- 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)
- return false;
-
- memcpy(palette, data.data(), 256 * 3);
- return true;
-}
-
-static void applyRoomPalette(Graphics::Screen &screen, const HarvesterEngine &engine,
- const byte *palette, float brightness) {
- byte scaledPalette[256 * 3];
- buildHarvesterDisplayPalette(palette, brightness * engine.getGammaBrightnessScale(), scaledPalette);
- screen.setPalette(scaledPalette);
-}
-
-static void renderCdChangePromptScreen(HarvesterEngine &engine, const IndexedBitmap &bitmap,
- const byte *palette) {
- Graphics::Screen *screen = engine.getScreen();
- if (!screen)
- return;
-
- applyRoomPalette(*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 drawRoomPrompt(Graphics::Screen &screen, const Graphics::Font &font,
const Common::String &promptText, bool useNativeFont) {
if (promptText.empty())
@@ -247,17 +168,10 @@ static void drawCombatLoadoutResourceIcons(Graphics::Screen &screen, const Art &
const int iconCount = CLIP<int>(script.getPlayerCombatResourceCount(loadout), 0, info.maxVisibleCount);
for (int i = 0; i < iconCount; ++i)
- blitBitmap(screen, icon, kNativeCombatResourceIconsX + i * kNativeCombatResourceIconSpacing,
+ RoomPresentation::blitBitmap(screen, icon, kNativeCombatResourceIconsX + i * kNativeCombatResourceIconSpacing,
kNativeCombatResourceIconsY);
}
-static void setScaledRoomPalette(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);
-}
-
RoomSystem::RoomSystem(HarvesterEngine &engine, Common::Point &mousePos,
InventorySystem &inventory, CheatSystem &cheats)
: _engine(engine), _mousePos(mousePos), _inventory(inventory), _cheats(cheats) {
@@ -492,107 +406,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
return false;
return _inventory.open();
};
- auto captureDialogueBackdrop = [&](IndexedBitmap &dialogueBackdrop) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return false;
-
- drawRoomScene(_engine, *activeScreen, scene, scene.targetPaletteBrightness);
- return captureScreenBackdrop(*activeScreen, dialogueBackdrop);
- };
- auto showCdChangePrompt = [&](int discNumber) -> Common::Error {
- if (discNumber <= 0)
- return Common::kNoError;
-
- ResourceManager *resources = _engine.getResources();
- if (!resources)
- return Common::kReadingFailed;
- const int previousDisc = resources->getCurrentDisc();
-
- if (_engine.shouldShowCdChangePrompts()) {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen)
- return Common::kReadingFailed;
-
- const Common::String bitmapPath = Common::String::format("1:/GRAPHIC/OTHER/CD%d.BM", discNumber);
- IndexedBitmap promptBitmap;
- byte promptPalette[256 * 3];
- if (!loadBitmapResource(*resources, bitmapPath, promptBitmap) ||
- !loadPaletteResource(*resources, kCdChangePromptPalettePath, promptPalette)) {
- warning("Harvester: unable to load CD change prompt assets for disc %d", discNumber);
- return Common::kReadingFailed;
- }
-
- debugC(1, kDebugScene, "Harvester: showing CD change prompt for disc %d", discNumber);
- _engine.stopMusic();
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (flow.handleSystemEvent(event, result))
- return result;
- if (event.type == Common::EVENT_MOUSEMOVE)
- _mousePos = event.mouse;
- }
-
- bool waitingForRelease = false;
- bool needsRedraw = true;
- bool activateRequestedDisc = false;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit() && !activateRequestedDisc) {
- if (needsRedraw) {
- renderCdChangePromptScreen(_engine, promptBitmap, promptPalette);
- needsRedraw = false;
- }
-
- 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:
- _mousePos = event.mouse;
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN:
- waitingForRelease = true;
- break;
- case Common::EVENT_LBUTTONUP:
- if (waitingForRelease)
- activateRequestedDisc = true;
- break;
- default:
- break;
- }
- }
- EntityManager *entityManager = _engine.getRuntimeEntities();
- if (entityManager && entityManager->syncCursorEntityPosition(_mousePos))
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
- }
-
- 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;
- }
- }
+ RoomPresentation presentation(_engine, scene, art, inspectFont, useNativeInspectFont);
- return Common::kNoError;
- };
auto runRoomExitCommands = [&]() -> Common::Error {
InteractionResult exitInteraction;
if (!_engine.getScript()->executeRoomExitCommands(scene.state.roomName, exitInteraction))
@@ -600,7 +416,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
for (uint exitStep = 0; exitStep < 128; ++exitStep) {
if (exitInteraction.cdChangeDisc > 0) {
- Common::Error cdPromptError = showCdChangePrompt(exitInteraction.cdChangeDisc);
+ Common::Error cdPromptError = flow.showCdChangePrompt(exitInteraction.cdChangeDisc);
if (cdPromptError.getCode() != Common::kNoError)
return cdPromptError;
}
@@ -644,51 +460,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
scene.state.roomName.c_str());
return Common::kNoError;
};
- auto applyLightingCommand = [&](StartupLightingCommand lightingCommand) -> Common::Error {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return Common::kNoError;
-
- switch (lightingCommand) {
- case kStartupLightingCommandNone:
- return Common::kNoError;
- case kStartupLightingCommandDim:
- scene.targetPaletteBrightness = kDimPaletteBrightness;
- drawRoomScene(_engine, *activeScreen, scene, scene.targetPaletteBrightness);
- setScaledRoomPalette(*activeScreen, scene.palette, scene.targetPaletteBrightness);
- activeScreen->makeAllDirty();
- activeScreen->update();
- return Common::kNoError;
- case kStartupLightingCommandNormal:
- if (ResourceManager *resources = _engine.getResources()) {
- if (!scene.state.palettePath.empty() &&
- !loadPaletteResource(*resources, scene.state.palettePath, scene.palette)) {
- warning("Harvester: unable to reload room palette '%s' for CHANGE_LIGHTING NORMAL",
- scene.state.palettePath.c_str());
- }
- }
- scene.targetPaletteBrightness = kNormalPaletteBrightness;
- drawRoomScene(_engine, *activeScreen, scene, scene.targetPaletteBrightness);
- setScaledRoomPalette(*activeScreen, scene.palette, scene.targetPaletteBrightness);
- activeScreen->makeAllDirty();
- activeScreen->update();
- return Common::kNoError;
- case kStartupLightingCommandBlack: {
- byte blackPalette[256 * 3] = { 0 };
- activeScreen->fillRect(activeScreen->getBounds(), 0);
- activeScreen->setPalette(blackPalette);
- activeScreen->makeAllDirty();
- activeScreen->update();
- return Common::kNoError;
- }
- case kStartupLightingCommandFadeIn:
- debugC(1, kDebugRoom,
- "Harvester: CHANGE_LIGHTING FADE_IN has no direct room-side equivalent yet; preserving control flow");
- return Common::kNoError;
- }
- return Common::kNoError;
- };
auto applyPlayerGotoXZ = [&](int x, int z) {
playerState.hasMoveTarget = false;
playerState.turnActive = false;
@@ -718,72 +490,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
session.resetIdleState();
session.captureCurrentSaveState();
};
- auto runModalShowText = [&](const ResolvedText &modalText) -> Common::Error {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen || !art || !inspectFont)
- return Common::kNoError;
- if (!resolveInspectTextboxBitmap(*art, modalText)) {
- debug(1, "Harvester: unsupported SHOW_TEXT textbox '%s' text='%s'",
- modalText.boxName.c_str(), modalText.value.c_str());
- return Common::kNoError;
- }
-
- IndexedBitmap backdrop;
- if (!captureDialogueBackdrop(backdrop))
- return Common::kReadingFailed;
- Graphics::FrameLimiter limiter(g_system, 60);
- bool initialInputReleased = false;
- bool dismissPressed = false;
- uint frameCount = 0;
-
- while (!_engine.shouldQuit()) {
- Common::Event event;
- Common::Error result = Common::kNoError;
- while (g_system->getEventManager()->pollEvent(event)) {
- if (flow.handleSystemEvent(event, result))
- return result;
-
- if (!initialInputReleased) {
- if (event.type == Common::EVENT_LBUTTONUP ||
- event.type == Common::EVENT_RBUTTONUP ||
- event.type == Common::EVENT_KEYUP) {
- initialInputReleased = true;
- }
- continue;
- }
-
- if (!dismissPressed) {
- if (event.type == Common::EVENT_LBUTTONDOWN ||
- event.type == Common::EVENT_RBUTTONDOWN) {
- dismissPressed = true;
- } else if (event.type == Common::EVENT_KEYDOWN) {
- return Common::kNoError;
- }
- continue;
- }
-
- if (event.type == Common::EVENT_LBUTTONUP ||
- event.type == Common::EVENT_RBUTTONUP) {
- return Common::kNoError;
- }
- }
-
- if (!initialInputReleased && frameCount++ > 0)
- initialInputReleased = true;
-
- blitBitmap(*activeScreen, backdrop, 0, 0);
- setScaledRoomPalette(*activeScreen, scene.palette, scene.targetPaletteBrightness);
- drawRoomInspectText(*activeScreen, *art, *inspectFont, modalText, useNativeInspectFont);
- activeScreen->makeAllDirty();
- activeScreen->update();
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
- };
auto requestPlayerGameOver = [&](const char *reason, const Common::String &sourceName) {
if (flow.hasPendingMainMenuReturn())
return;
@@ -830,31 +537,19 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
? _engine.getScript()->getPlayerCurrentHitPoints()
: 0);
};
- typedef decltype(captureDialogueBackdrop) CaptureDialogueBackdropFn;
- typedef decltype(showCdChangePrompt) ShowCdChangePromptFn;
typedef decltype(runRoomExitCommands) RunRoomExitCommandsFn;
- typedef decltype(applyLightingCommand) ApplyLightingCommandFn;
typedef decltype(applyPlayerGotoXZ) ApplyPlayerGotoXZFn;
- typedef decltype(runModalShowText) RunModalShowTextFn;
typedef decltype(startPlayerDefeatSequence) StartPlayerDefeatSequenceFn;
struct RoomInteractionCallbacksImpl : RoomInteractionCallbacks {
RoomInteractionCallbacksImpl(
- RoomSession &session_,
- CaptureDialogueBackdropFn &captureDialogueBackdropFn_,
- ShowCdChangePromptFn &showCdChangePromptFn_,
+ RoomSession &session_, RoomPresentation &presentation_, Flow &flow_,
RunRoomExitCommandsFn &runRoomExitCommandsFn_,
- ApplyLightingCommandFn &applyLightingCommandFn_,
ApplyPlayerGotoXZFn &applyPlayerGotoXZFn_,
- RunModalShowTextFn &runModalShowTextFn_,
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn_)
- : session(session_),
- captureDialogueBackdropFn(captureDialogueBackdropFn_),
- showCdChangePromptFn(showCdChangePromptFn_),
+ : session(session_), presentation(presentation_), flow(flow_),
runRoomExitCommandsFn(runRoomExitCommandsFn_),
- applyLightingCommandFn(applyLightingCommandFn_),
applyPlayerGotoXZFn(applyPlayerGotoXZFn_),
- runModalShowTextFn(runModalShowTextFn_),
startPlayerDefeatSequenceFn(startPlayerDefeatSequenceFn_) {
}
@@ -871,11 +566,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop) override {
- return captureDialogueBackdropFn(dialogueBackdrop);
+ return presentation.captureDialogueBackdrop(dialogueBackdrop);
}
Common::Error showCdChangePrompt(int discNumber) override {
- return showCdChangePromptFn(discNumber);
+ return flow.showCdChangePrompt(discNumber);
}
Common::Error runRoomExitCommands() override {
@@ -883,7 +578,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
Common::Error applyLightingCommand(StartupLightingCommand lightingCommand) override {
- return applyLightingCommandFn(lightingCommand);
+ return presentation.applyLightingCommand(lightingCommand);
}
void applyPlayerGotoXZ(int x, int z) override {
@@ -891,7 +586,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
Common::Error runModalShowText(const ResolvedText &modalText) override {
- return runModalShowTextFn(modalText);
+ return presentation.runModalShowText(modalText, flow);
}
void resetIdleState() override {
@@ -912,18 +607,14 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
RoomSession &session;
- CaptureDialogueBackdropFn &captureDialogueBackdropFn;
- ShowCdChangePromptFn &showCdChangePromptFn;
+ RoomPresentation &presentation;
+ Flow &flow;
RunRoomExitCommandsFn &runRoomExitCommandsFn;
- ApplyLightingCommandFn &applyLightingCommandFn;
ApplyPlayerGotoXZFn &applyPlayerGotoXZFn;
- RunModalShowTextFn &runModalShowTextFn;
StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn;
};
RoomInteractionCallbacksImpl interactionCallbacks(
- session,
- captureDialogueBackdrop, showCdChangePrompt,
- runRoomExitCommands, applyLightingCommand, applyPlayerGotoXZ, runModalShowText,
+ session, presentation, flow, runRoomExitCommands, applyPlayerGotoXZ,
startPlayerDefeatSequence);
RoomInteractionProcessor interactionProcessor(
_engine, flow, scene, playerState, pendingRegionName, pendingRoomChange,
@@ -1164,7 +855,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
} else if (roomCarryActive && roomItems.getCarriedRoomItemBitmap().isValid()) {
const int drawX = _mousePos.x - (int)roomItems.getCarriedRoomItemBitmap().width / 2;
const int drawY = _mousePos.y - (int)roomItems.getCarriedRoomItemBitmap().height / 2;
- blitBitmap(*activeScreen, roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
+ RoomPresentation::blitBitmap(*activeScreen, roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
}
if (showingInspectText) {
diff --git a/engines/harvester/room_presentation.cpp b/engines/harvester/room_presentation.cpp
new file mode 100644
index 00000000000..ea63ac33e0d
--- /dev/null
+++ b/engines/harvester/room_presentation.cpp
@@ -0,0 +1,224 @@
+/* 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/room_presentation.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+#include "graphics/blit.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
+#include "harvester/detection.h"
+#include "harvester/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/palette_utils.h"
+#include "harvester/resources.h"
+#include "harvester/room_support.h"
+
+namespace Harvester {
+
+static const byte kTransparentPaletteIndex = 0;
+static const float kNormalPaletteBrightness = 1.0f;
+static const float kDimPaletteBrightness = 0.6f;
+
+static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *palette) {
+ Common::Array<byte> data;
+ if (!resources.loadFile(path, data) || data.size() < 256 * 3)
+ return false;
+
+ memcpy(palette, data.data(), 256 * 3);
+ return true;
+}
+
+static void setScaledRoomPalette(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);
+}
+
+void RoomPresentation::blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
+ if (!bitmap.isValid())
+ return;
+
+ int destX = x;
+ int destY = y;
+ int srcX = 0;
+ int srcY = 0;
+ int width = (int)bitmap.width;
+ int height = (int)bitmap.height;
+
+ if (destX < 0) {
+ srcX = -destX;
+ width += destX;
+ destX = 0;
+ }
+ if (destY < 0) {
+ srcY = -destY;
+ height += destY;
+ destY = 0;
+ }
+ if (destX >= screen.w || destY >= screen.h || width <= 0 || height <= 0)
+ return;
+
+ width = MIN<int>(width, screen.w - destX);
+ height = MIN<int>(height, screen.h - destY);
+ if (width <= 0 || height <= 0)
+ return;
+
+ const byte *src = bitmap.pixels.data() + srcY * bitmap.width + srcX;
+ byte *dst = (byte *)screen.getBasePtr(destX, destY);
+ Graphics::keyBlit(dst, src, screen.pitch, bitmap.width, width, height,
+ screen.format.bytesPerPixel, kTransparentPaletteIndex);
+}
+
+RoomPresentation::RoomPresentation(HarvesterEngine &engine, RoomSceneResources &scene,
+ const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont)
+ : _engine(engine), _scene(scene), _art(art), _inspectFont(inspectFont),
+ _useNativeInspectFont(useNativeInspectFont) {
+}
+
+bool RoomPresentation::captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop) {
+ Graphics::Screen *activeScreen = _engine.getScreen();
+ if (!activeScreen)
+ return false;
+
+ drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+ return captureScreenBackdrop(*activeScreen, dialogueBackdrop);
+}
+
+Common::Error RoomPresentation::applyLightingCommand(StartupLightingCommand lightingCommand) {
+ Graphics::Screen *activeScreen = _engine.getScreen();
+ if (!activeScreen)
+ return Common::kNoError;
+
+ switch (lightingCommand) {
+ case kStartupLightingCommandNone:
+ return Common::kNoError;
+ case kStartupLightingCommandDim:
+ _scene.targetPaletteBrightness = kDimPaletteBrightness;
+ drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+ setScaledRoomPalette(*activeScreen, _scene.palette, _scene.targetPaletteBrightness);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+ return Common::kNoError;
+ case kStartupLightingCommandNormal:
+ if (ResourceManager *resources = _engine.getResources()) {
+ if (!_scene.state.palettePath.empty() &&
+ !loadPaletteResource(*resources, _scene.state.palettePath, _scene.palette)) {
+ warning("Harvester: unable to reload room palette '%s' for CHANGE_LIGHTING NORMAL",
+ _scene.state.palettePath.c_str());
+ }
+ }
+ _scene.targetPaletteBrightness = kNormalPaletteBrightness;
+ drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+ setScaledRoomPalette(*activeScreen, _scene.palette, _scene.targetPaletteBrightness);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+ return Common::kNoError;
+ case kStartupLightingCommandBlack: {
+ byte blackPalette[256 * 3] = { 0 };
+ activeScreen->fillRect(activeScreen->getBounds(), 0);
+ activeScreen->setPalette(blackPalette);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+ return Common::kNoError;
+ }
+ case kStartupLightingCommandFadeIn:
+ debugC(1, kDebugRoom,
+ "Harvester: CHANGE_LIGHTING FADE_IN has no direct room-side equivalent yet; preserving control flow");
+ return Common::kNoError;
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomPresentation::runModalShowText(const ResolvedText &modalText, Flow &flow) {
+ Graphics::Screen *activeScreen = _engine.getScreen();
+ if (!activeScreen || !_art || !_inspectFont)
+ return Common::kNoError;
+ if (!resolveInspectTextboxBitmap(*_art, modalText)) {
+ debug(1, "Harvester: unsupported SHOW_TEXT textbox '%s' text='%s'",
+ modalText.boxName.c_str(), modalText.value.c_str());
+ return Common::kNoError;
+ }
+
+ IndexedBitmap backdrop;
+ if (!captureDialogueBackdrop(backdrop))
+ return Common::kReadingFailed;
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ bool initialInputReleased = false;
+ bool dismissPressed = false;
+ uint frameCount = 0;
+
+ while (!_engine.shouldQuit()) {
+ Common::Event event;
+ Common::Error result = Common::kNoError;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (flow.handleSystemEvent(event, result))
+ return result;
+
+ if (!initialInputReleased) {
+ if (event.type == Common::EVENT_LBUTTONUP ||
+ event.type == Common::EVENT_RBUTTONUP ||
+ event.type == Common::EVENT_KEYUP) {
+ initialInputReleased = true;
+ }
+ continue;
+ }
+
+ if (!dismissPressed) {
+ if (event.type == Common::EVENT_LBUTTONDOWN ||
+ event.type == Common::EVENT_RBUTTONDOWN) {
+ dismissPressed = true;
+ } else if (event.type == Common::EVENT_KEYDOWN) {
+ return Common::kNoError;
+ }
+ continue;
+ }
+
+ if (event.type == Common::EVENT_LBUTTONUP ||
+ event.type == Common::EVENT_RBUTTONUP) {
+ return Common::kNoError;
+ }
+ }
+
+ if (!initialInputReleased && frameCount++ > 0)
+ initialInputReleased = true;
+
+ blitBitmap(*activeScreen, backdrop, 0, 0);
+ setScaledRoomPalette(*activeScreen, _scene.palette, _scene.targetPaletteBrightness);
+ drawRoomInspectText(*activeScreen, *_art, *_inspectFont, modalText, _useNativeInspectFont);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
new file mode 100644
index 00000000000..1c97dbeabf2
--- /dev/null
+++ b/engines/harvester/room_presentation.h
@@ -0,0 +1,62 @@
+/* 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_ROOM_PRESENTATION_H
+#define HARVESTER_ROOM_PRESENTATION_H
+
+#include "common/error.h"
+#include "harvester/script.h"
+
+namespace Graphics {
+class Font;
+class Screen;
+}
+
+namespace Harvester {
+
+class Art;
+class Flow;
+class HarvesterEngine;
+struct IndexedBitmap;
+struct ResolvedText;
+struct RoomSceneResources;
+
+class RoomPresentation {
+public:
+ RoomPresentation(HarvesterEngine &engine, RoomSceneResources &scene,
+ const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont);
+
+ bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop);
+ Common::Error applyLightingCommand(StartupLightingCommand lightingCommand);
+ Common::Error runModalShowText(const ResolvedText &modalText, Flow &flow);
+ static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y);
+
+private:
+ HarvesterEngine &_engine;
+ RoomSceneResources &_scene;
+ const Art *_art;
+ const Graphics::Font *_inspectFont;
+ bool _useNativeInspectFont;
+};
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_ROOM_PRESENTATION_H
Commit: 1d3c209f66d4596e9a77c024bb13d627f20ae8e6
https://github.com/scummvm/scummvm/commit/1d3c209f66d4596e9a77c024bb13d627f20ae8e6
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Extract input handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/room_input.cpp
A engines/harvester/room_input.h
engines/harvester/flow.h
engines/harvester/module.mk
engines/harvester/room.cpp
engines/harvester/room_presentation.h
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 776e9f650e8..c7328121d5e 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -63,6 +63,7 @@ private:
friend class MenuSystem;
friend class RoomInteractionProcessor;
friend class RoomSystem;
+ friend class RoomInput;
friend class RoomSession;
bool loadQuickTips();
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index de8ce1ecbb2..3487d10e819 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -19,6 +19,7 @@ MODULE_OBJS = \
resources.o \
room_combat.o \
room_interaction.o \
+ room_input.o \
room_presentation.o \
room_scene_runtime.o \
room_session.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 984f1bd4a55..f8dd76ee4c6 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -43,60 +43,25 @@
#include "harvester/player.h"
#include "harvester/room_combat.h"
#include "harvester/room_interaction.h"
+#include "harvester/room_input.h"
#include "harvester/room_presentation.h"
#include "harvester/room_session.h"
#include "harvester/room_support.h"
namespace Harvester {
-static const uint32 kRoomExitFastClickWindowTicks = 20;
static const char *const kPlayerInventoryLabel = "your inventory";
-static const int kNativeInventoryDragCloseLeft = 0x45;
-static const int kNativeInventoryDragCloseTop = 0x4b;
-static const int kNativeInventoryDragCloseRight = 0x239;
-static const int kNativeInventoryDragCloseBottom = 0x1b4;
static const int kNativeInventoryTooltipX = 0xbc;
static const int kNativeInventoryTooltipY = 0x19e;
static const int kNativeInventoryWeekdayX = 0x1e0;
static const int kNativeInventoryWeekdayY = 0x18c;
static const byte kNativeInventoryTooltipColor = 0xf4;
-static const uint32 kCombatLoadoutStatusDisplayTicks = 75;
+
static const int kNativeCombatResourceIconsX = 10;
static const int kNativeCombatResourceIconsY = 30;
static const int kNativeCombatResourceIconSpacing = 15;
-static bool roomAllowsImmediateExitClick(const Common::String &roomName) {
- return !roomName.equalsIgnoreCase("LAVAPIT") &&
- !roomName.equalsIgnoreCase("BOWLSNTRY1");
-}
-
-static bool isOutsideNativeInventoryDragCloseBounds(const Common::Point &point) {
- return point.x < kNativeInventoryDragCloseLeft ||
- point.x >= kNativeInventoryDragCloseRight ||
- point.y < kNativeInventoryDragCloseTop ||
- point.y >= kNativeInventoryDragCloseBottom;
-}
-
-static void debugLogInventoryClick(const char *buttonLabel, const Common::Point &point,
- const InventoryVisual *inventoryHover) {
- if (!inventoryHover) {
- debugC(1, kDebugInventory,
- "Harvester: inventory %s click at (%d,%d) hit no item", buttonLabel, point.x, point.y);
- return;
- }
-
- debugC(1, kDebugInventory,
- "Harvester: inventory %s click at (%d,%d) object='%s' sprite='%s' alt='%s' action='%s' owner='%s' text='%s' bounds=(%d,%d)-(%d,%d) exit=%d status=%d",
- buttonLabel, point.x, point.y, inventoryHover->object.objectName.c_str(),
- inventoryHover->object.spritePath.c_str(), inventoryHover->object.altSpritePath.c_str(),
- inventoryHover->object.actionTag.c_str(), inventoryHover->object.currentOwnerOrRoom.c_str(),
- inventoryHover->object.inventoryTextKey.c_str(), inventoryHover->bounds.left,
- inventoryHover->bounds.top, inventoryHover->bounds.right, inventoryHover->bounds.bottom,
- InventorySystem::isExitObject(inventoryHover->object),
- InventorySystem::isStatusObject(inventoryHover->object));
-}
-
static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
const Text *text = engine.getText();
if (!text || !fontName)
@@ -325,56 +290,21 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
combat.initializeCombatStates(scene.state);
RoomSceneRuntime &sceneRuntime = session.getSceneRuntime();
RoomCombatEffects &combatEffects = session.getCombatEffects();
- ResolvedText inspectText;
- Common::String currentInteractionPromptText;
- bool showingInspectText = false;
- bool inspectCanDismiss = false;
- auto beginInspectText = [&](const ObjectRecord &object, const ResolvedText &text) {
- inspectText = text;
- showingInspectText = true;
- inspectCanDismiss = false;
- debugC(2, kDebugRoom,
- "Harvester: showing IDENT object='%s' prompt='%s' box='%s' text='%s'",
- object.objectName.c_str(), currentInteractionPromptText.c_str(),
- inspectText.boxName.c_str(), inspectText.value.c_str());
- };
- ResolvedText combatLoadoutStatusText;
- uint32 combatLoadoutStatusHideTick = 0;
- bool closeInventoryAfterCombatLoadoutStatus = false;
- bool &moveLeft = session.getMovementInput().moveLeft;
- bool &moveRight = session.getMovementInput().moveRight;
- bool &moveUp = session.getMovementInput().moveUp;
- bool &moveDown = session.getMovementInput().moveDown;
- bool attackModifierHeld = false;
+ RoomPresentationState presentationState;
+
+ RoomInputState &inputState = session.getInputState();
+
Common::String &pendingRegionName = session.getPendingRegionName();
Common::String pendingRoomChange;
bool pendingRoomChangeIsRoomName = false;
bool pendingRoomChangeUsesSavedRoomState = false;
SaveRoomState pendingRoomChangeSavedRoomState;
const bool canExitCloseupToParent = flow._roomLoopDepth > 1;
- uint32 lastLeftButtonReleaseTick = 0;
- uint32 nextKeyboardAttackAllowedTick = 0;
+
RoomIdleAnimationState &idleState = session.getIdleState();
bool needsRedraw = true;
RoomItemInteractions roomItems(_engine, scene, sceneRuntime, _inventory, pendingRoomChange);
- auto queueCombatLoadoutStatusMessage = [&](const Common::String &message) {
- if (message.empty()) {
- const bool hadMessage = !combatLoadoutStatusText.value.empty();
- combatLoadoutStatusText = ResolvedText();
- combatLoadoutStatusHideTick = 0;
- if (hadMessage)
- needsRedraw = true;
- return;
- }
-
- combatLoadoutStatusText.boxName = "BOX1";
- combatLoadoutStatusText.value = message;
- combatLoadoutStatusHideTick =
- Player::getRuntimeClockTicks() + kCombatLoadoutStatusDisplayTicks;
- needsRedraw = true;
- };
-
session.resetIdleState();
auto syncAnimatedRoomActorPlacement = [&]() {
@@ -392,21 +322,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
};
- auto openInventoryOverlay = [&]() {
- moveLeft = false;
- moveRight = false;
- moveUp = false;
- moveDown = false;
- pendingRegionName.clear();
- (void)session.cancelPlayerAttackAnimation();
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- if (!roomItems.stowCarriedRoomItemToInventory())
- return false;
- return _inventory.open();
- };
-
RoomPresentation presentation(_engine, scene, art, inspectFont, useNativeInspectFont);
auto runRoomExitCommands = [&]() -> Common::Error {
@@ -674,15 +589,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
_engine.getScript());
- auto capturePlayerAttackTarget = [&]() {
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
-
- const RoomHoverState hoverState = resolveRoomHoverState(
- _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
- _mousePos, flow._menuTextConfig, &flow._dialogue);
- combat.capturePlayerAttackTarget(scene.state, playerState, entityManager, _mousePos, hoverState.npc);
- };
+ RoomInput input(_engine, flow, session, scene, entityManager, _inventory, _cheats,
+ roomItems, interactionProcessor, interactionCallbacks, presentationState, *art, canExitCloseupToParent);
if (!_inventory.refresh())
return Common::kReadingFailed;
@@ -749,13 +657,13 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
session.captureCurrentSaveState();
if (_inventory.isOpen() && _inventory.refreshIfRuntimeStateChanged())
needsRedraw = true;
- if (!combatLoadoutStatusText.value.empty() &&
- combatLoadoutStatusHideTick != 0 &&
- (int32)(Player::getRuntimeClockTicks() - combatLoadoutStatusHideTick) >= 0) {
- combatLoadoutStatusText = ResolvedText();
- combatLoadoutStatusHideTick = 0;
- if (closeInventoryAfterCombatLoadoutStatus) {
- closeInventoryAfterCombatLoadoutStatus = false;
+ if (!presentationState.combatLoadoutStatusText.value.empty() &&
+ presentationState.combatLoadoutStatusHideTick != 0 &&
+ (int32)(Player::getRuntimeClockTicks() - presentationState.combatLoadoutStatusHideTick) >= 0) {
+ presentationState.combatLoadoutStatusText = ResolvedText();
+ presentationState.combatLoadoutStatusHideTick = 0;
+ if (presentationState.closeInventoryAfterCombatLoadoutStatus) {
+ presentationState.closeInventoryAfterCombatLoadoutStatus = false;
if (_inventory.close())
needsRedraw = true;
}
@@ -772,7 +680,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const bool roomCarryActive = roomItems.hasCarriedRoomItem();
const bool activeCarry = inventorySelectionActive || roomCarryActive;
const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_mousePos);
- const bool suppressHover = showingInspectText || idleState.active || idleState.exiting ||
+ const bool suppressHover = presentationState.showingInspectText || idleState.active || idleState.exiting ||
session.isPlayerCombatLocked() ||
(_inventory.isOpen() && (inventoryPanelContainsMouse || !inventorySelectionActive));
RoomHoverState hoverState = suppressHover
@@ -828,11 +736,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
} else {
promptText = hoverState.promptText;
}
- if (!showingInspectText)
- currentInteractionPromptText = promptText;
+ if (!presentationState.showingInspectText)
+ presentationState.currentInteractionPromptText = promptText;
if (Entity *cursor = entityManager ? entityManager->getCursorEntity() : nullptr) {
cursor->setAnimationSequence(
- (showingInspectText || idleState.active || idleState.exiting ||
+ (presentationState.showingInspectText || idleState.active || idleState.exiting ||
session.isPlayerCombatLocked() ||
_inventory.isOpen() || activeCarry)
? 7
@@ -858,13 +766,13 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
RoomPresentation::blitBitmap(*activeScreen, roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
}
- if (showingInspectText) {
- drawRoomInspectText(*activeScreen, *art, *inspectFont, inspectText, useNativeInspectFont);
- if (!currentInteractionPromptText.empty())
+ if (presentationState.showingInspectText) {
+ drawRoomInspectText(*activeScreen, *art, *inspectFont, presentationState.inspectText, useNativeInspectFont);
+ if (!presentationState.currentInteractionPromptText.empty())
drawRoomPrompt(*activeScreen, *promptFont,
- currentInteractionPromptText, useNativePromptFont);
- } else if (!combatLoadoutStatusText.value.empty()) {
- drawRoomInspectText(*activeScreen, *art, *inspectFont, combatLoadoutStatusText,
+ presentationState.currentInteractionPromptText, useNativePromptFont);
+ } else if (!presentationState.combatLoadoutStatusText.value.empty()) {
+ drawRoomInspectText(*activeScreen, *art, *inspectFont, presentationState.combatLoadoutStatusText,
useNativeInspectFont);
} else if (!inventoryTooltipText.empty()) {
drawInventoryTooltip(*activeScreen, *inventoryTooltipFont, inventoryTooltipText);
@@ -881,624 +789,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
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:
- if (_inventory.isOpen() && _inventory.hasSelection()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_mousePos);
- // Native inventory handoff is driven by the latched selected item, not by
- // a second click after the selection has already been made.
- if (!inventoryHover && isOutsideNativeInventoryDragCloseBounds(_mousePos)) {
- if (_inventory.close())
- needsRedraw = true;
- }
- }
- needsRedraw = true;
- break;
- case Common::EVENT_RBUTTONDOWN: {
- session.notePlayerActivity();
- if (idleState.active || idleState.exiting) {
- if (Player::requestIdleAnimationExit(scene.state, playerState, idleState))
- needsRedraw = true;
- break;
- }
- if (session.isPlayerCombatLocked())
- break;
- pendingRegionName.clear();
- if (_inventory.isOpen()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_mousePos);
- debugLogInventoryClick("right", _mousePos, inventoryHover);
- if (_inventory.hasSelection()) {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click cleared active selection='%s'",
- _inventory.getSelectedItemName().c_str());
- if (_inventory.clearSelection())
- needsRedraw = true;
- break;
- }
- if (inventoryHover &&
- !InventorySystem::isExitObject(inventoryHover->object) &&
- !InventorySystem::isStatusObject(inventoryHover->object)) {
- Script *script = _engine.getScript();
- const int previousLoadout = playerState.combatLoadout >= 0
- ? playerState.combatLoadout
- : (script ? script->getPlayerCombatLoadout() : 0);
- bool loadoutChanged = false;
- if (_inventory.toggleCombatLoadout(inventoryHover->object,
- playerState.combatLoadout, loadoutChanged)) {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click handled as combat toggle object='%s' changed=%d",
- inventoryHover->object.objectName.c_str(), loadoutChanged);
- if (loadoutChanged) {
- if (script) {
- const Common::String statusMessage = buildCombatLoadoutStatusMessage(
- *script, inventoryHover->object, previousLoadout,
- script->getPlayerCombatLoadout());
- queueCombatLoadoutStatusMessage(statusMessage);
- closeInventoryAfterCombatLoadoutStatus = true;
- }
- session.captureCurrentSaveState();
- if (playerState.entity) {
- (void)Player::syncCombatLoadoutVisual(_engine, scene.state, playerState,
- _engine.getScript()->getPlayerCombatLoadout());
- } else if (_engine.getScript()) {
- playerState.combatLoadout =
- _engine.getScript()->getPlayerCombatLoadout();
- }
- if (!_inventory.refresh())
- return Common::kReadingFailed;
- }
- needsRedraw = true;
- break;
- }
-
- InventorySecondaryAction secondaryAction;
- if (_inventory.resolveSecondaryAction(inventoryHover->object, secondaryAction)) {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click executing secondary action object='%s' action='%s' closeInventory=%d",
- inventoryHover->object.objectName.c_str(),
- secondaryAction.actionTag.c_str(), secondaryAction.closeInventory);
- if (secondaryAction.closeInventory)
- (void)_inventory.close();
-
- InteractionResult interaction;
- bool didTransition = false;
- const bool executedActionTag = _engine.getScript()->executeActionTag(
- secondaryAction.actionTag, interaction, true, scene.state.roomName);
- debugC(1, kDebugInventory,
- "Harvester: inventory right click executeActionTag('%s') -> %d music='%s' cutscene='%s' nextRoom='%s'",
- secondaryAction.actionTag.c_str(), executedActionTag,
- interaction.musicPath.c_str(), interaction.cutscenePath.c_str(),
- interaction.nextRoomName.c_str());
- if (executedActionTag) {
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- interaction, didTransition, Common::String());
- debugC(1, kDebugInventory,
- "Harvester: inventory right click action='%s' interaction result error=%d transitioned=%d pendingMainMenu=%d",
- secondaryAction.actionTag.c_str(), interactionError.getCode(),
- didTransition, flow.hasPendingMainMenuReturn());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- }
-
- if (_inventory.isOpen()) {
- if (!_inventory.refresh())
- return Common::kReadingFailed;
- } else {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click skipped overlay refresh after action='%s' transitioned=%d closeInventory=%d",
- secondaryAction.actionTag.c_str(), didTransition, secondaryAction.closeInventory);
- }
- needsRedraw = true;
- break;
- }
- debugC(1, kDebugInventory,
- "Harvester: inventory right click object='%s' produced no combat toggle or secondary action",
- inventoryHover->object.objectName.c_str());
- }
- debugC(1, kDebugInventory, "Harvester: inventory right click closing overlay");
- if (_inventory.close())
- needsRedraw = true;
- break;
- }
- if (_inventory.hasSelection()) {
- if (_inventory.clearSelection())
- needsRedraw = true;
- break;
- }
- if (roomItems.hasCarriedRoomItem()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- needsRedraw = true;
- break;
- }
- Script *script = _engine.getScript();
- if (!showingInspectText && !session.isPlayerCombatLocked() && script && playerState.entity &&
- script->getPlayerCurrentHitPoints() > 0 && !script->isPlayerControlPaused()) {
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- playerState.turnFirstFrame = -1;
- playerState.turnLastFrame = -1;
- playerState.turnEndFrame = -1;
- playerState.turnPlayBackwards = false;
- (void)Player::syncCombatLoadoutVisual(_engine, scene.state, playerState,
- script->getPlayerCombatLoadout());
- capturePlayerAttackTarget();
- if (Player::startAttackAnimation(scene.state, playerState, _mousePos)) {
- debugC(1, kDebugCombat,
- "Harvester: combat player attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' cursor=(%d,%d)",
- playerState.combatLoadout,
- Player::describeCombatLoadout(playerState.combatLoadout),
- Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
- Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
- describeCombatTargetClass(playerState.attackTargetClassId),
- playerState.attackTargetName.c_str(), _mousePos.x, _mousePos.y);
- needsRedraw = true;
- } else {
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- }
- }
- break;
- }
- case Common::EVENT_LBUTTONUP:
- lastLeftButtonReleaseTick = Player::getRuntimeClockTicks();
- if (showingInspectText)
- inspectCanDismiss = true;
- break;
- case Common::EVENT_LBUTTONDOWN: {
- const uint32 now = Player::getRuntimeClockTicks();
- const bool isFastExitClick =
- lastLeftButtonReleaseTick != 0 &&
- now - lastLeftButtonReleaseTick < kRoomExitFastClickWindowTicks;
- session.notePlayerActivity();
- if (idleState.active || idleState.exiting) {
- if (Player::requestIdleAnimationExit(scene.state, playerState, idleState))
- needsRedraw = true;
- break;
- }
-
- if (showingInspectText) {
- if (inspectCanDismiss) {
- showingInspectText = false;
- inspectCanDismiss = false;
- inspectText = ResolvedText();
- needsRedraw = true;
- }
- break;
- }
- if (session.isPlayerCombatLocked())
- break;
-
- const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
- if (_inventory.isOpen()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_mousePos);
- debugLogInventoryClick("left", _mousePos, inventoryHover);
- if (inventoryHover) {
- if (InventorySystem::isExitObject(inventoryHover->object)) {
- debugC(1, kDebugInventory, "Harvester: inventory left click exit button");
- const bool clearedSelection = _inventory.clearSelection();
- const bool closedInventory = _inventory.close();
- if (clearedSelection || closedInventory)
- needsRedraw = true;
- break;
- }
- if (InventorySystem::isStatusObject(inventoryHover->object))
- break;
-
- if (!_inventory.hasSelection()) {
- debugC(1, kDebugInventory,
- "Harvester: inventory left click selecting object='%s'",
- inventoryHover->object.objectName.c_str());
- _inventory.selectItem(inventoryHover->object.objectName);
- _inventory.setPromptText(_inventory.buildSelectedPrompt(
- Common::String(), flow._menuTextConfig));
- needsRedraw = true;
- break;
- }
-
- if (!inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
- debugC(1, kDebugInventory,
- "Harvester: inventory left click using selected='%s' on target='%s'",
- _inventory.getSelectedItemName().c_str(),
- inventoryHover->object.objectName.c_str());
- Common::Error interactionError =
- roomItems.handleInventoryTargetInteraction(inventoryHover->object, interactionProcessor, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- }
- needsRedraw = true;
- break;
- }
-
- if (_inventory.hasSelection()) {
- if (!inventoryPanelBounds.contains(_mousePos) && _inventory.close())
- needsRedraw = true;
- break;
- }
- if (!inventoryPanelBounds.contains(_mousePos) && _inventory.close())
- needsRedraw = true;
- break;
- }
-
- const RoomHoverState hoverState = resolveRoomHoverState(
- _engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
- _mousePos, flow._menuTextConfig, &flow._dialogue);
- ObjectRecord *selectedRoomTarget = nullptr;
- if (_inventory.hasSelection() && !hoverState.npc)
- selectedRoomTarget = roomItems.findSelectedInventoryRoomTarget(_mousePos);
- if (selectedRoomTarget &&
- (!hoverState.object ||
- !selectedRoomTarget->objectName.equalsIgnoreCase(hoverState.object->objectName))) {
- debugC(1, kDebugInventory,
- "Harvester: inventory target remap selected='%s' hovered='%s' remapped='%s'",
- _inventory.getSelectedItemName().c_str(),
- hoverState.object ? hoverState.object->objectName.c_str() : "",
- selectedRoomTarget->objectName.c_str());
- }
- RoomHoverState clickHoverState = hoverState;
- if (selectedRoomTarget)
- clickHoverState.object = selectedRoomTarget;
- debugC(1, kDebugRoom,
- "Harvester: room click room='%s' mouse=(%d,%d) object='%s' npc='%s' region='%s' cursor_sequence=%d prompt='%s'",
- scene.state.roomName.c_str(), _mousePos.x, _mousePos.y,
- clickHoverState.object ? clickHoverState.object->objectName.c_str() : "",
- clickHoverState.npc ? clickHoverState.npc->npcName.c_str() : "",
- clickHoverState.region ? clickHoverState.region->regionName.c_str() : "",
- clickHoverState.cursorSequence, clickHoverState.promptText.c_str());
- if (roomItems.hasCarriedRoomItem()) {
- if (clickHoverState.playerEntity && playerState.entity &&
- clickHoverState.playerEntity == playerState.entity) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- needsRedraw = true;
- }
- break;
- }
- if (_inventory.hasSelection()) {
- const Common::String selectedItemName = _inventory.getSelectedItemName();
- if (clickHoverState.npc) {
- bool didTransition = false;
- Common::Error dialogueError = interactionProcessor.runScriptedDialogue(
- clickHoverState.npc->npcName, selectedItemName, Common::String(), didTransition);
- if (dialogueError.getCode() != Common::kNoError)
- return dialogueError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (_inventory.clearSelection())
- needsRedraw = true;
- break;
- }
-
- ObjectRecord *roomTarget = clickHoverState.object
- ? findSceneObjectByName(scene.sceneObjects, clickHoverState.object->objectName)
- : nullptr;
- if (roomTarget) {
- Common::Error interactionError =
- roomItems.handleInventoryTargetInteraction(*roomTarget, interactionProcessor, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- needsRedraw = true;
- }
- break;
- }
- if (hoverState.playerEntity) {
- if (!playerState.entity || hoverState.playerEntity != playerState.entity ||
- idleState.active || idleState.exiting ||
- playerState.hasMoveTarget || playerState.turnActive ||
- playerState.entity->getAnimationRate() != 0) {
- break;
- }
- if (!openInventoryOverlay())
- return Common::kReadingFailed;
- needsRedraw = true;
- break;
- }
- if (hoverState.npc) {
- pendingRegionName.clear();
- moveLeft = false;
- moveRight = false;
- moveUp = false;
- moveDown = false;
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- bool didTransition = false;
- Common::Error dialogueError = interactionProcessor.runScriptedDialogue(
- hoverState.npc->npcName, Common::String(), Common::String(), didTransition);
- if (dialogueError.getCode() != Common::kNoError)
- return dialogueError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- needsRedraw = true;
- break;
- }
- // select one target in reverse render order; a selected object prevents exit-region selection.
- if (hoverState.region && !hoverState.object && playerState.entity) {
- if (isFastExitClick &&
- hoverState.region->startEnabled &&
- roomAllowsImmediateExitClick(scene.state.roomName)) {
- session.stopPlayerRegionInteraction();
- Common::Error interactionError =
- interactionProcessor.runRegionInteraction(*hoverState.region, _inventory, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- needsRedraw = true;
- break;
- }
- interactionProcessor.queueRegionInteraction(*hoverState.region, _mousePos);
- Common::Error interactionError =
- interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- needsRedraw = true;
- break;
- }
- ObjectRecord *clickedObject = hoverState.object
- ? findSceneObjectByName(scene.sceneObjects, hoverState.object->objectName)
- : nullptr;
- if (!clickedObject) {
- pendingRegionName.clear();
- if (hoverState.cursorSequence == 0 && playerState.entity) {
- if (_engine.isPathfindingDebugEnabled()) {
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding click accepted room='%s' mouse=(%d,%d) movement_band=(y=%d..%d,z=%d..%d) player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d)",
- scene.state.roomName.c_str(), _mousePos.x, _mousePos.y,
- scene.state.roomMaxZScreenY, scene.state.roomMinZScreenY,
- scene.state.roomMinZ, scene.state.roomMaxZ,
- playerState.centerX, playerState.bottomY, (double)playerState.z,
- playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
- }
- Player::setMoveTargetFromScreenPoint(scene.state, playerState, _mousePos.x, _mousePos.y);
- if (_engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
- scene.state.roomName.c_str(), _mousePos.x, _mousePos.y,
- playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
- }
- needsRedraw = true;
- }
- break;
- }
- pendingRegionName.clear();
- if (clickedObject->objectName.equalsIgnoreCase("EXIT_BM") ||
- clickedObject->objectName.equalsIgnoreCase("EXIT_HS")) {
- Common::Error exitError = runRoomExitCommands();
- if (exitError.getCode() != Common::kNoError)
- return exitError;
- // Native closeup exits return through room_setup so the parent on-enter command runs.
- if (canExitCloseupToParent)
- flow.requestCloseupParentRestart();
- return Common::kNoError;
- }
-
- ResolvedText resolvedInspectText;
- const bool hasInspectText =
- _engine.getScript()->resolveObjectInspectText(*clickedObject, resolvedInspectText);
- const bool unlocksAfterInitialExamine =
- unlocksRoomObjectInteractionAfterInitialExamine(*clickedObject, *_engine.getScript());
- const bool canShowInspectText =
- hasInspectText && resolveInspectTextboxBitmap(*art, resolvedInspectText);
- if (!clickedObject->identShown && unlocksAfterInitialExamine) {
- clickedObject->identShown = true;
- _engine.getScript()->markObjectIdentShown(*clickedObject);
- if (canShowInspectText) {
- beginInspectText(*clickedObject, resolvedInspectText);
- } else if (hasInspectText) {
- debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
- resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
- }
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- needsRedraw = true;
- break;
- }
- if (_engine.getScript()->isPickupObject(*clickedObject)) {
- Common::Error pickupError = roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
- interactionProcessor, interactionCallbacks, needsRedraw);
- if (pickupError.getCode() != Common::kNoError)
- return pickupError;
- break;
- }
-
- InteractionResult interaction;
- if (!_engine.getScript()->resolveObjectInteraction(
- *clickedObject, interaction, scene.state.roomName)) {
- if (canShowInspectText) {
- beginInspectText(*clickedObject, resolvedInspectText);
- } else if (hasInspectText) {
- debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
- resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
- }
- needsRedraw = true;
- break;
- }
-
- bool didTransition = false;
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- interaction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- needsRedraw = true;
- break;
- }
- case Common::EVENT_KEYDOWN:
- if (showingInspectText) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- showingInspectText = false;
- inspectCanDismiss = false;
- inspectText = ResolvedText();
- needsRedraw = true;
- }
- break;
- }
-
- session.notePlayerActivity();
- if (event.kbd.hasFlags(Common::KBD_CTRL))
- attackModifierHeld = true;
- if (idleState.active || idleState.exiting) {
- if (Player::requestIdleAnimationExit(scene.state, playerState, idleState))
- needsRedraw = true;
- break;
- }
-
- if (Script *script = _engine.getScript()) {
- InteractionResult cheatInteraction;
- CheatActivation cheatActivation;
- const CheatInputResult cheatInputResult = _cheats.processKey(
- event.kbd, *script, scene.state.roomName,
- cheatInteraction, cheatActivation);
- if (cheatInputResult != kCheatInputIgnored) {
- if (cheatInputResult == kCheatInputComplete && cheatActivation.activated) {
- if (cheatActivation.clearInteractionState && _inventory.clearSelection())
- needsRedraw = true;
- if (cheatActivation.inventoryChanged && _inventory.isOpen() &&
- !_inventory.refresh()) {
- return Common::kReadingFailed;
- }
- if (cheatActivation.hasInteraction) {
- if (_inventory.isOpen())
- (void)_inventory.close();
- bool didTransition = false;
- Common::Error cheatError = interactionProcessor.handleInteractionResult(
- cheatInteraction, didTransition, Common::String());
- if (cheatError.getCode() != Common::kNoError)
- return cheatError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- }
- needsRedraw = true;
- }
- break;
- }
- }
-
- if (_inventory.isOpen()) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- const bool clearedSelection = _inventory.clearSelection();
- const bool closedInventory = _inventory.close();
- if (clearedSelection || closedInventory)
- needsRedraw = true;
- } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER ||
- event.kbd.keycode == Common::KEYCODE_i) {
- if (_inventory.close())
- needsRedraw = true;
- }
- break;
- }
- if (_inventory.hasSelection()) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- if (_inventory.clearSelection())
- needsRedraw = true;
- break;
- }
- if (event.kbd.keycode == Common::KEYCODE_i) {
- if (_inventory.clearSelection())
- needsRedraw = true;
- if (!openInventoryOverlay())
- return Common::kReadingFailed;
- needsRedraw = true;
- break;
- }
- }
- if (session.isPlayerCombatLocked())
- break;
-
- if (event.kbd.keycode == Common::KEYCODE_LCTRL ||
- event.kbd.keycode == Common::KEYCODE_RCTRL) {
- attackModifierHeld = true;
- } else if (event.kbd.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
- Common::Error debugKillError = combat.killDebugActiveMonster(_engine, scene.state, playerState, entityManager,
- _mousePos, combatEffects, interactionProcessor, needsRedraw);
- if (debugKillError.getCode() != Common::kNoError)
- return debugKillError;
- break;
- } else if (event.kbd.keycode == Common::KEYCODE_LEFT)
- moveLeft = true;
- else if (event.kbd.keycode == Common::KEYCODE_RIGHT)
- moveRight = true;
- else if (event.kbd.keycode == Common::KEYCODE_UP)
- moveUp = true;
- else if (event.kbd.keycode == Common::KEYCODE_DOWN)
- moveDown = true;
- else if (event.kbd.keycode == Common::KEYCODE_i) {
- if (!openInventoryOverlay())
- return Common::kReadingFailed;
- needsRedraw = true;
- break;
- } else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- if (session.cancelPlayerAttackAnimation())
- needsRedraw = true;
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- moveLeft = false;
- moveRight = false;
- moveUp = false;
- moveDown = false;
- pendingRegionName.clear();
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- session.captureCurrentSaveState();
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return Common::kNoError;
- IndexedBitmap roomMenuBackdrop;
- drawRoomScene(_engine, *activeScreen, scene, scene.targetPaletteBrightness);
- if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
- return Common::kReadingFailed;
- Common::Error menuError = flow.runRoomMenuStub(
- roomMenuBackdrop, scene.palette, scene.targetPaletteBrightness,
- scene.state.hasEntrance);
- if (menuError.getCode() != Common::kNoError)
- return menuError;
- flow.resetCursorAnimationSequence();
- session.resetIdleState();
- needsRedraw = true;
- break;
- }
-
- if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- Common::Error exitError = runRoomExitCommands();
- if (exitError.getCode() != Common::kNoError)
- return exitError;
- return Common::kNoError;
- }
- break;
- case Common::EVENT_KEYUP:
- if (event.kbd.keycode == Common::KEYCODE_LCTRL ||
- event.kbd.keycode == Common::KEYCODE_RCTRL)
- attackModifierHeld = false;
- else if (event.kbd.keycode == Common::KEYCODE_LEFT)
- moveLeft = false;
- else if (event.kbd.keycode == Common::KEYCODE_RIGHT)
- moveRight = false;
- else if (event.kbd.keycode == Common::KEYCODE_UP)
- moveUp = false;
- else if (event.kbd.keycode == Common::KEYCODE_DOWN)
- moveDown = false;
- break;
- default:
- break;
- }
+ const RoomInputResult inputResult = input.handleEvent(event, needsRedraw);
+ if (inputResult.returnFromRoom)
+ return inputResult.error;
}
if (!pendingRoomChange.empty()) {
if (!roomItems.stowCarriedRoomItemToInventory())
@@ -1510,10 +803,10 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
const bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
const bool playerControlPaused = script && script->isPlayerControlPaused();
const bool playerCanAct = playerAlive && !playerControlPaused;
- if (!playerAlive && (moveLeft || moveRight || moveUp || moveDown ||
+ if (!playerAlive && (inputState.moveLeft || inputState.moveRight || inputState.moveUp || inputState.moveDown ||
playerState.hasMoveTarget || playerState.turnActive ||
playerState.attackActive || playerState.hitActive)) {
- attackModifierHeld = false;
+ inputState.attackModifierHeld = false;
session.stopPlayerRegionInteraction();
}
@@ -1574,16 +867,16 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
}
}
const bool keyboardAttackRequested =
- attackModifierHeld && (moveLeft || moveRight || moveUp || moveDown);
+ inputState.attackModifierHeld && (inputState.moveLeft || inputState.moveRight || inputState.moveUp || inputState.moveDown);
if (playerCanAct &&
!playerState.attackActive && !playerState.hitActive && keyboardAttackRequested &&
!idleState.active && !idleState.exiting) {
const uint32 now = Player::getRuntimeClockTicks();
- if (nextKeyboardAttackAllowedTick == 0 || (int32)(now - nextKeyboardAttackAllowedTick) >= 0) {
- combat.capturePlayerAttackTargetForKeyboard(scene.state, playerState, entityManager, moveLeft, moveRight, moveUp, moveDown);
+ if (inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - inputState.nextKeyboardAttackAllowedTick) >= 0) {
+ combat.capturePlayerAttackTargetForKeyboard(scene.state, playerState, entityManager, inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown);
if (Player::startKeyboardAttackAnimation(
- scene.state, playerState, moveLeft, moveRight, moveUp, moveDown)) {
- nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
+ scene.state, playerState, inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown)) {
+ inputState.nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
session.notePlayerActivity();
debugC(1, kDebugCombat,
"Harvester: combat player keyboard attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' input=(L=%d R=%d U=%d D=%d) facing=%d",
@@ -1593,7 +886,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
describeCombatTargetClass(playerState.attackTargetClassId),
playerState.attackTargetName.c_str(),
- moveLeft, moveRight, moveUp, moveDown, playerState.facing);
+ inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown, playerState.facing);
needsRedraw = true;
} else {
playerState.attackTargetName.clear();
@@ -1611,7 +904,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
!keyboardAttackRequested && !idleState.active && !idleState.exiting) {
const int regionFacing = interactionProcessor.getPendingRegionFacing();
if (Player::stepKeyboardMovement(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
- playerState, moveLeft, moveRight, moveUp, moveDown)) {
+ playerState, inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown)) {
session.notePlayerActivity();
needsRedraw = true;
} else if (Player::stepMoveTarget(
@@ -1621,8 +914,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
needsRedraw = true;
}
- if (!showingInspectText && !keyboardAttackRequested &&
- !moveLeft && !moveRight && !moveUp && !moveDown &&
+ if (!presentationState.showingInspectText && !keyboardAttackRequested &&
+ !inputState.moveLeft && !inputState.moveRight && !inputState.moveUp && !inputState.moveDown &&
!playerState.hasMoveTarget && !playerState.turnActive && !playerState.hitActive &&
playerState.entity && playerState.facing >= 0 &&
!Player::isIdleAnimationExcludedRoom(scene.state.roomName) &&
diff --git a/engines/harvester/room_input.cpp b/engines/harvester/room_input.cpp
new file mode 100644
index 00000000000..7ca357be3ab
--- /dev/null
+++ b/engines/harvester/room_input.cpp
@@ -0,0 +1,809 @@
+/* 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/room_input.h"
+
+#include "common/debug.h"
+#include "graphics/screen.h"
+#include "harvester/art.h"
+#include "harvester/cheats.h"
+#include "harvester/detection.h"
+#include "harvester/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/inventory.h"
+#include "harvester/player.h"
+#include "harvester/room_interaction.h"
+#include "harvester/room_presentation.h"
+#include "harvester/room_session.h"
+#include "harvester/room_support.h"
+
+namespace Harvester {
+
+static const uint32 kRoomExitFastClickWindowTicks = 20;
+static const int kNativeInventoryDragCloseLeft = 0x45;
+static const int kNativeInventoryDragCloseTop = 0x4b;
+static const int kNativeInventoryDragCloseRight = 0x239;
+static const int kNativeInventoryDragCloseBottom = 0x1b4;
+static const uint32 kCombatLoadoutStatusDisplayTicks = 75;
+
+static bool roomAllowsImmediateExitClick(const Common::String &roomName) {
+ return !roomName.equalsIgnoreCase("LAVAPIT") &&
+ !roomName.equalsIgnoreCase("BOWLSNTRY1");
+}
+
+static bool isOutsideNativeInventoryDragCloseBounds(const Common::Point &point) {
+ return point.x < kNativeInventoryDragCloseLeft ||
+ point.x >= kNativeInventoryDragCloseRight ||
+ point.y < kNativeInventoryDragCloseTop ||
+ point.y >= kNativeInventoryDragCloseBottom;
+}
+
+static void debugLogInventoryClick(const char *buttonLabel, const Common::Point &point,
+ const InventoryVisual *inventoryHover) {
+ if (!inventoryHover) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory %s click at (%d,%d) hit no item", buttonLabel, point.x, point.y);
+ return;
+ }
+
+ debugC(1, kDebugInventory,
+ "Harvester: inventory %s click at (%d,%d) object='%s' sprite='%s' alt='%s' action='%s' owner='%s' text='%s' bounds=(%d,%d)-(%d,%d) exit=%d status=%d",
+ buttonLabel, point.x, point.y, inventoryHover->object.objectName.c_str(),
+ inventoryHover->object.spritePath.c_str(), inventoryHover->object.altSpritePath.c_str(),
+ inventoryHover->object.actionTag.c_str(), inventoryHover->object.currentOwnerOrRoom.c_str(),
+ inventoryHover->object.inventoryTextKey.c_str(), inventoryHover->bounds.left,
+ inventoryHover->bounds.top, inventoryHover->bounds.right, inventoryHover->bounds.bottom,
+ InventorySystem::isExitObject(inventoryHover->object),
+ InventorySystem::isStatusObject(inventoryHover->object));
+}
+
+RoomInput::RoomInput(HarvesterEngine &engine, Flow &flow, RoomSession &session, RoomSceneResources &scene,
+ EntityManager *entityManager, InventorySystem &inventory, CheatSystem &cheats,
+ RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
+ RoomInteractionCallbacks &interactionCallbacks, RoomPresentationState &presentationState,
+ const Art &art, bool canExitCloseupToParent)
+ : _engine(engine), _flow(flow), _session(session), _scene(scene), _entityManager(entityManager),
+ _inventory(inventory), _cheats(cheats), _roomItems(roomItems), _interactionProcessor(interactionProcessor),
+ _interactionCallbacks(interactionCallbacks), _presentationState(presentationState), _art(art),
+ _canExitCloseupToParent(canExitCloseupToParent) {
+}
+
+void RoomInput::beginInspectText(const ObjectRecord &object, const ResolvedText &text) {
+ _presentationState.inspectText = text;
+ _presentationState.showingInspectText = true;
+ _presentationState.inspectCanDismiss = false;
+ debugC(2, kDebugRoom,
+ "Harvester: showing IDENT object='%s' prompt='%s' box='%s' text='%s'",
+ object.objectName.c_str(), _presentationState.currentInteractionPromptText.c_str(),
+ _presentationState.inspectText.boxName.c_str(), _presentationState.inspectText.value.c_str());
+}
+
+void RoomInput::queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw) {
+ if (message.empty()) {
+ const bool hadMessage = !_presentationState.combatLoadoutStatusText.value.empty();
+ _presentationState.combatLoadoutStatusText = ResolvedText();
+ _presentationState.combatLoadoutStatusHideTick = 0;
+ if (hadMessage)
+ needsRedraw = true;
+ return;
+ }
+
+ _presentationState.combatLoadoutStatusText.boxName = "BOX1";
+ _presentationState.combatLoadoutStatusText.value = message;
+ _presentationState.combatLoadoutStatusHideTick =
+ Player::getRuntimeClockTicks() + kCombatLoadoutStatusDisplayTicks;
+ needsRedraw = true;
+}
+
+bool RoomInput::openInventoryOverlay() {
+ RoomPlayerState &playerState = _session.getPlayerState();
+ RoomInputState &inputState = _session.getInputState();
+ Common::String &pendingRegionName = _session.getPendingRegionName();
+
+ inputState.moveLeft = false;
+ inputState.moveRight = false;
+ inputState.moveUp = false;
+ inputState.moveDown = false;
+ pendingRegionName.clear();
+ (void)_session.cancelPlayerAttackAnimation();
+ playerState.hasMoveTarget = false;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return false;
+ return _inventory.open();
+}
+
+void RoomInput::capturePlayerAttackTarget() {
+ RoomPlayerState &playerState = _session.getPlayerState();
+
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+
+ const RoomHoverState hoverState = resolveRoomHoverState(
+ _engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
+ _flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
+ _session.getCombat().capturePlayerAttackTarget(_scene.state, playerState, _entityManager, _flow._mousePos, hoverState.npc);
+}
+
+void RoomInput::handleMouseMove(bool &needsRedraw) {
+ if (_inventory.isOpen() && _inventory.hasSelection()) {
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+ // Native inventory handoff is driven by the latched selected item, not by
+ // a second click after the selection has already been made.
+ if (!inventoryHover && isOutsideNativeInventoryDragCloseBounds(_flow._mousePos)) {
+ if (_inventory.close())
+ needsRedraw = true;
+ }
+ }
+ needsRedraw = true;
+}
+
+RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
+ RoomPlayerState &playerState = _session.getPlayerState();
+ RoomIdleAnimationState &idleState = _session.getIdleState();
+ Common::String &pendingRegionName = _session.getPendingRegionName();
+
+ _session.notePlayerActivity();
+ if (idleState.active || idleState.exiting) {
+ if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (_session.isPlayerCombatLocked())
+ return RoomInputResult();
+ pendingRegionName.clear();
+ if (_inventory.isOpen()) {
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+ debugLogInventoryClick("right", _flow._mousePos, inventoryHover);
+ if (_inventory.hasSelection()) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click cleared active selection='%s'",
+ _inventory.getSelectedItemName().c_str());
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (inventoryHover &&
+ !InventorySystem::isExitObject(inventoryHover->object) &&
+ !InventorySystem::isStatusObject(inventoryHover->object)) {
+ Script *script = _engine.getScript();
+ const int previousLoadout = playerState.combatLoadout >= 0
+ ? playerState.combatLoadout
+ : (script ? script->getPlayerCombatLoadout() : 0);
+ bool loadoutChanged = false;
+ if (_inventory.toggleCombatLoadout(inventoryHover->object,
+ playerState.combatLoadout, loadoutChanged)) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click handled as combat toggle object='%s' changed=%d",
+ inventoryHover->object.objectName.c_str(), loadoutChanged);
+ if (loadoutChanged) {
+ if (script) {
+ const Common::String statusMessage = buildCombatLoadoutStatusMessage(
+ *script, inventoryHover->object, previousLoadout,
+ script->getPlayerCombatLoadout());
+ queueCombatLoadoutStatusMessage(statusMessage, needsRedraw);
+ _presentationState.closeInventoryAfterCombatLoadoutStatus = true;
+ }
+ _session.captureCurrentSaveState();
+ if (playerState.entity) {
+ (void)Player::syncCombatLoadoutVisual(_engine, _scene.state, playerState,
+ _engine.getScript()->getPlayerCombatLoadout());
+ } else if (_engine.getScript()) {
+ playerState.combatLoadout =
+ _engine.getScript()->getPlayerCombatLoadout();
+ }
+ if (!_inventory.refresh())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ }
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ InventorySecondaryAction secondaryAction;
+ if (_inventory.resolveSecondaryAction(inventoryHover->object, secondaryAction)) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click executing secondary action object='%s' action='%s' closeInventory=%d",
+ inventoryHover->object.objectName.c_str(),
+ secondaryAction.actionTag.c_str(), secondaryAction.closeInventory);
+ if (secondaryAction.closeInventory)
+ (void)_inventory.close();
+
+ InteractionResult interaction;
+ bool didTransition = false;
+ const bool executedActionTag = _engine.getScript()->executeActionTag(
+ secondaryAction.actionTag, interaction, true, _scene.state.roomName);
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click executeActionTag('%s') -> %d music='%s' cutscene='%s' nextRoom='%s'",
+ secondaryAction.actionTag.c_str(), executedActionTag,
+ interaction.musicPath.c_str(), interaction.cutscenePath.c_str(),
+ interaction.nextRoomName.c_str());
+ if (executedActionTag) {
+ Common::Error interactionError =
+ _interactionProcessor.handleInteractionResult(
+ interaction, didTransition, Common::String());
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click action='%s' interaction result error=%d transitioned=%d pendingMainMenu=%d",
+ secondaryAction.actionTag.c_str(), interactionError.getCode(),
+ didTransition, _flow.hasPendingMainMenuReturn());
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ }
+
+ if (_inventory.isOpen()) {
+ if (!_inventory.refresh())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ } else {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click skipped overlay refresh after action='%s' transitioned=%d closeInventory=%d",
+ secondaryAction.actionTag.c_str(), didTransition, secondaryAction.closeInventory);
+ }
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click object='%s' produced no combat toggle or secondary action",
+ inventoryHover->object.objectName.c_str());
+ }
+ debugC(1, kDebugInventory, "Harvester: inventory right click closing overlay");
+ if (_inventory.close())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (_inventory.hasSelection()) {
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (_roomItems.hasCarriedRoomItem()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ Script *script = _engine.getScript();
+ if (!_presentationState.showingInspectText && !_session.isPlayerCombatLocked() && script && playerState.entity &&
+ script->getPlayerCurrentHitPoints() > 0 && !script->isPlayerControlPaused()) {
+ playerState.hasMoveTarget = false;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ playerState.turnFirstFrame = -1;
+ playerState.turnLastFrame = -1;
+ playerState.turnEndFrame = -1;
+ playerState.turnPlayBackwards = false;
+ (void)Player::syncCombatLoadoutVisual(_engine, _scene.state, playerState,
+ script->getPlayerCombatLoadout());
+ capturePlayerAttackTarget();
+ if (Player::startAttackAnimation(_scene.state, playerState, _flow._mousePos)) {
+ debugC(1, kDebugCombat,
+ "Harvester: combat player attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' cursor=(%d,%d)",
+ playerState.combatLoadout,
+ Player::describeCombatLoadout(playerState.combatLoadout),
+ Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
+ Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
+ describeCombatTargetClass(playerState.attackTargetClassId),
+ playerState.attackTargetName.c_str(), _flow._mousePos.x, _flow._mousePos.y);
+ needsRedraw = true;
+ } else {
+ playerState.attackTargetName.clear();
+ playerState.attackTargetClassId = -1;
+ }
+ }
+ return RoomInputResult();
+}
+
+void RoomInput::handleLeftButtonUp() {
+ RoomInputState &inputState = _session.getInputState();
+
+ inputState.lastLeftButtonReleaseTick = Player::getRuntimeClockTicks();
+ if (_presentationState.showingInspectText)
+ _presentationState.inspectCanDismiss = true;
+}
+
+RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
+ RoomPlayerState &playerState = _session.getPlayerState();
+ RoomIdleAnimationState &idleState = _session.getIdleState();
+ RoomInputState &inputState = _session.getInputState();
+ Common::String &pendingRegionName = _session.getPendingRegionName();
+
+ const uint32 now = Player::getRuntimeClockTicks();
+ const bool isFastExitClick =
+ inputState.lastLeftButtonReleaseTick != 0 &&
+ now - inputState.lastLeftButtonReleaseTick < kRoomExitFastClickWindowTicks;
+ _session.notePlayerActivity();
+ if (idleState.active || idleState.exiting) {
+ if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ if (_presentationState.showingInspectText) {
+ if (_presentationState.inspectCanDismiss) {
+ _presentationState.showingInspectText = false;
+ _presentationState.inspectCanDismiss = false;
+ _presentationState.inspectText = ResolvedText();
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+ if (_session.isPlayerCombatLocked())
+ return RoomInputResult();
+
+ const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
+ if (_inventory.isOpen()) {
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+ debugLogInventoryClick("left", _flow._mousePos, inventoryHover);
+ if (inventoryHover) {
+ if (InventorySystem::isExitObject(inventoryHover->object)) {
+ debugC(1, kDebugInventory, "Harvester: inventory left click exit button");
+ const bool clearedSelection = _inventory.clearSelection();
+ const bool closedInventory = _inventory.close();
+ if (clearedSelection || closedInventory)
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (InventorySystem::isStatusObject(inventoryHover->object))
+ return RoomInputResult();
+
+ if (!_inventory.hasSelection()) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory left click selecting object='%s'",
+ inventoryHover->object.objectName.c_str());
+ _inventory.selectItem(inventoryHover->object.objectName);
+ _inventory.setPromptText(_inventory.buildSelectedPrompt(
+ Common::String(), _flow._menuTextConfig));
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ if (!inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory left click using selected='%s' on target='%s'",
+ _inventory.getSelectedItemName().c_str(),
+ inventoryHover->object.objectName.c_str());
+ Common::Error interactionError =
+ _roomItems.handleInventoryTargetInteraction(inventoryHover->object, _interactionProcessor, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ }
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ if (_inventory.hasSelection()) {
+ if (!inventoryPanelBounds.contains(_flow._mousePos) && _inventory.close())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (!inventoryPanelBounds.contains(_flow._mousePos) && _inventory.close())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ const RoomHoverState hoverState = resolveRoomHoverState(
+ _engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
+ _flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
+ ObjectRecord *selectedRoomTarget = nullptr;
+ if (_inventory.hasSelection() && !hoverState.npc)
+ selectedRoomTarget = _roomItems.findSelectedInventoryRoomTarget(_flow._mousePos);
+ if (selectedRoomTarget &&
+ (!hoverState.object ||
+ !selectedRoomTarget->objectName.equalsIgnoreCase(hoverState.object->objectName))) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory target remap selected='%s' hovered='%s' remapped='%s'",
+ _inventory.getSelectedItemName().c_str(),
+ hoverState.object ? hoverState.object->objectName.c_str() : "",
+ selectedRoomTarget->objectName.c_str());
+ }
+ RoomHoverState clickHoverState = hoverState;
+ if (selectedRoomTarget)
+ clickHoverState.object = selectedRoomTarget;
+ debugC(1, kDebugRoom,
+ "Harvester: room click room='%s' mouse=(%d,%d) object='%s' npc='%s' region='%s' cursor_sequence=%d prompt='%s'",
+ _scene.state.roomName.c_str(), _flow._mousePos.x, _flow._mousePos.y,
+ clickHoverState.object ? clickHoverState.object->objectName.c_str() : "",
+ clickHoverState.npc ? clickHoverState.npc->npcName.c_str() : "",
+ clickHoverState.region ? clickHoverState.region->regionName.c_str() : "",
+ clickHoverState.cursorSequence, clickHoverState.promptText.c_str());
+ if (_roomItems.hasCarriedRoomItem()) {
+ if (clickHoverState.playerEntity && playerState.entity &&
+ clickHoverState.playerEntity == playerState.entity) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+ if (_inventory.hasSelection()) {
+ const Common::String selectedItemName = _inventory.getSelectedItemName();
+ if (clickHoverState.npc) {
+ bool didTransition = false;
+ Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
+ clickHoverState.npc->npcName, selectedItemName, Common::String(), didTransition);
+ if (dialogueError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(dialogueError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ ObjectRecord *roomTarget = clickHoverState.object
+ ? findSceneObjectByName(_scene.sceneObjects, clickHoverState.object->objectName)
+ : nullptr;
+ if (roomTarget) {
+ Common::Error interactionError =
+ _roomItems.handleInventoryTargetInteraction(*roomTarget, _interactionProcessor, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+ if (hoverState.playerEntity) {
+ if (!playerState.entity || hoverState.playerEntity != playerState.entity ||
+ idleState.active || idleState.exiting ||
+ playerState.hasMoveTarget || playerState.turnActive ||
+ playerState.entity->getAnimationRate() != 0) {
+ return RoomInputResult();
+ }
+ if (!openInventoryOverlay())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (hoverState.npc) {
+ pendingRegionName.clear();
+ inputState.moveLeft = false;
+ inputState.moveRight = false;
+ inputState.moveUp = false;
+ inputState.moveDown = false;
+ playerState.hasMoveTarget = false;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ bool didTransition = false;
+ Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
+ hoverState.npc->npcName, Common::String(), Common::String(), didTransition);
+ if (dialogueError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(dialogueError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ // select one target in reverse render order; a selected object prevents exit-region selection.
+ if (hoverState.region && !hoverState.object && playerState.entity) {
+ if (isFastExitClick &&
+ hoverState.region->startEnabled &&
+ roomAllowsImmediateExitClick(_scene.state.roomName)) {
+ _session.stopPlayerRegionInteraction();
+ Common::Error interactionError =
+ _interactionProcessor.runRegionInteraction(*hoverState.region, _inventory, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ _interactionProcessor.queueRegionInteraction(*hoverState.region, _flow._mousePos);
+ Common::Error interactionError =
+ _interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ ObjectRecord *clickedObject = hoverState.object
+ ? findSceneObjectByName(_scene.sceneObjects, hoverState.object->objectName)
+ : nullptr;
+ if (!clickedObject) {
+ pendingRegionName.clear();
+ if (hoverState.cursorSequence == 0 && playerState.entity) {
+ if (_engine.isPathfindingDebugEnabled()) {
+ const Common::Rect playerRect = playerState.entity->getScreenRect();
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding click accepted room='%s' mouse=(%d,%d) movement_band=(y=%d..%d,z=%d..%d) player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d)",
+ _scene.state.roomName.c_str(), _flow._mousePos.x, _flow._mousePos.y,
+ _scene.state.roomMaxZScreenY, _scene.state.roomMinZScreenY,
+ _scene.state.roomMinZ, _scene.state.roomMaxZ,
+ playerState.centerX, playerState.bottomY, (double)playerState.z,
+ playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
+ }
+ Player::setMoveTargetFromScreenPoint(_scene.state, playerState, _flow._mousePos.x, _flow._mousePos.y);
+ if (_engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
+ _scene.state.roomName.c_str(), _flow._mousePos.x, _flow._mousePos.y,
+ playerState.targetX, playerState.targetBottomY, (double)playerState.targetZ);
+ }
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+ pendingRegionName.clear();
+ if (clickedObject->objectName.equalsIgnoreCase("EXIT_BM") ||
+ clickedObject->objectName.equalsIgnoreCase("EXIT_HS")) {
+ Common::Error exitError = _interactionCallbacks.runRoomExitCommands();
+ if (exitError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(exitError);
+ // Native closeup exits return through room_setup so the parent on-enter command runs.
+ if (_canExitCloseupToParent)
+ _flow.requestCloseupParentRestart();
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ }
+
+ ResolvedText resolvedInspectText;
+ const bool hasInspectText =
+ _engine.getScript()->resolveObjectInspectText(*clickedObject, resolvedInspectText);
+ const bool unlocksAfterInitialExamine =
+ unlocksRoomObjectInteractionAfterInitialExamine(*clickedObject, *_engine.getScript());
+ const bool canShowInspectText =
+ hasInspectText && resolveInspectTextboxBitmap(_art, resolvedInspectText);
+ if (!clickedObject->identShown && unlocksAfterInitialExamine) {
+ clickedObject->identShown = true;
+ _engine.getScript()->markObjectIdentShown(*clickedObject);
+ if (canShowInspectText) {
+ beginInspectText(*clickedObject, resolvedInspectText);
+ } else if (hasInspectText) {
+ debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
+ resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
+ }
+ playerState.hasMoveTarget = false;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (_engine.getScript()->isPickupObject(*clickedObject)) {
+ Common::Error pickupError = _roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
+ _interactionProcessor, _interactionCallbacks, needsRedraw);
+ if (pickupError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(pickupError);
+ return RoomInputResult();
+ }
+
+ InteractionResult interaction;
+ if (!_engine.getScript()->resolveObjectInteraction(
+ *clickedObject, interaction, _scene.state.roomName)) {
+ if (canShowInspectText) {
+ beginInspectText(*clickedObject, resolvedInspectText);
+ } else if (hasInspectText) {
+ debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
+ resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
+ }
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ bool didTransition = false;
+ Common::Error interactionError =
+ _interactionProcessor.handleInteractionResult(
+ interaction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ needsRedraw = true;
+ return RoomInputResult();
+}
+
+RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &needsRedraw) {
+ RoomPlayerState &playerState = _session.getPlayerState();
+ RoomIdleAnimationState &idleState = _session.getIdleState();
+ RoomInputState &inputState = _session.getInputState();
+ Common::String &pendingRegionName = _session.getPendingRegionName();
+
+ if (_presentationState.showingInspectText) {
+ if (key.keycode == Common::KEYCODE_ESCAPE) {
+ _presentationState.showingInspectText = false;
+ _presentationState.inspectCanDismiss = false;
+ _presentationState.inspectText = ResolvedText();
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+
+ _session.notePlayerActivity();
+ if (key.hasFlags(Common::KBD_CTRL))
+ inputState.attackModifierHeld = true;
+ if (idleState.active || idleState.exiting) {
+ if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ if (Script *script = _engine.getScript()) {
+ InteractionResult cheatInteraction;
+ CheatActivation cheatActivation;
+ const CheatInputResult cheatInputResult = _cheats.processKey(
+ key, *script, _scene.state.roomName,
+ cheatInteraction, cheatActivation);
+ if (cheatInputResult != kCheatInputIgnored) {
+ if (cheatInputResult == kCheatInputComplete && cheatActivation.activated) {
+ if (cheatActivation.clearInteractionState && _inventory.clearSelection())
+ needsRedraw = true;
+ if (cheatActivation.inventoryChanged && _inventory.isOpen() &&
+ !_inventory.refresh()) {
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ }
+ if (cheatActivation.hasInteraction) {
+ if (_inventory.isOpen())
+ (void)_inventory.close();
+ bool didTransition = false;
+ Common::Error cheatError = _interactionProcessor.handleInteractionResult(
+ cheatInteraction, didTransition, Common::String());
+ if (cheatError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(cheatError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ }
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+ }
+
+ if (_inventory.isOpen()) {
+ if (key.keycode == Common::KEYCODE_ESCAPE) {
+ const bool clearedSelection = _inventory.clearSelection();
+ const bool closedInventory = _inventory.close();
+ if (clearedSelection || closedInventory)
+ needsRedraw = true;
+ } else if (key.keycode == Common::KEYCODE_RETURN ||
+ key.keycode == Common::KEYCODE_KP_ENTER ||
+ key.keycode == Common::KEYCODE_i) {
+ if (_inventory.close())
+ needsRedraw = true;
+ }
+ return RoomInputResult();
+ }
+ if (_inventory.hasSelection()) {
+ if (key.keycode == Common::KEYCODE_ESCAPE) {
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ if (key.keycode == Common::KEYCODE_i) {
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ if (!openInventoryOverlay())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+ }
+ if (_session.isPlayerCombatLocked())
+ return RoomInputResult();
+
+ if (key.keycode == Common::KEYCODE_LCTRL ||
+ key.keycode == Common::KEYCODE_RCTRL) {
+ inputState.attackModifierHeld = true;
+ } else if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
+ Common::Error debugKillError = _session.getCombat().killDebugActiveMonster(_engine, _scene.state, playerState, _entityManager,
+ _flow._mousePos, _session.getCombatEffects(), _interactionProcessor, needsRedraw);
+ if (debugKillError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(debugKillError);
+ return RoomInputResult();
+ } 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) {
+ if (!openInventoryOverlay())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ return RoomInputResult();
+ } else if (key.keycode == Common::KEYCODE_ESCAPE) {
+ if (_session.cancelPlayerAttackAnimation())
+ needsRedraw = true;
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ inputState.moveLeft = false;
+ inputState.moveRight = false;
+ inputState.moveUp = false;
+ inputState.moveDown = false;
+ pendingRegionName.clear();
+ playerState.hasMoveTarget = false;
+ playerState.turnActive = false;
+ playerState.turnTargetFacing = -1;
+ _session.captureCurrentSaveState();
+ Graphics::Screen *activeScreen = _engine.getScreen();
+ if (!activeScreen)
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ IndexedBitmap roomMenuBackdrop;
+ drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+ if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ Common::Error menuError = _flow.runRoomMenuStub(
+ roomMenuBackdrop, _scene.palette, _scene.targetPaletteBrightness,
+ _scene.state.hasEntrance);
+ if (menuError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(menuError);
+ _flow.resetCursorAnimationSequence();
+ _session.resetIdleState();
+ needsRedraw = true;
+ return RoomInputResult();
+ }
+
+ if (key.keycode == Common::KEYCODE_RETURN ||
+ key.keycode == Common::KEYCODE_KP_ENTER) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ Common::Error exitError = _interactionCallbacks.runRoomExitCommands();
+ if (exitError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(exitError);
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ }
+ return RoomInputResult();
+}
+
+void RoomInput::handleKeyUp(const Common::KeyState &key) {
+ RoomInputState &inputState = _session.getInputState();
+
+ if (key.keycode == Common::KEYCODE_LCTRL ||
+ key.keycode == Common::KEYCODE_RCTRL)
+ inputState.attackModifierHeld = false;
+ else if (key.keycode == Common::KEYCODE_LEFT)
+ inputState.moveLeft = false;
+ else if (key.keycode == Common::KEYCODE_RIGHT)
+ inputState.moveRight = false;
+ else if (key.keycode == Common::KEYCODE_UP)
+ inputState.moveUp = false;
+ else if (key.keycode == Common::KEYCODE_DOWN)
+ inputState.moveDown = false;
+}
+
+RoomInputResult RoomInput::handleEvent(const Common::Event &event, bool &needsRedraw) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return RoomInputResult::leaveRoom(result);
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ handleMouseMove(needsRedraw);
+ break;
+ case Common::EVENT_RBUTTONDOWN:
+ return handleRightButtonDown(needsRedraw);
+ case Common::EVENT_LBUTTONUP:
+ handleLeftButtonUp();
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ return handleLeftButtonDown(needsRedraw);
+ case Common::EVENT_KEYDOWN:
+ return handleKeyDown(event.kbd, needsRedraw);
+ case Common::EVENT_KEYUP:
+ handleKeyUp(event.kbd);
+ break;
+ default:
+ break;
+ }
+ return RoomInputResult();
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/room_input.h b/engines/harvester/room_input.h
new file mode 100644
index 00000000000..0cdbe9653a8
--- /dev/null
+++ b/engines/harvester/room_input.h
@@ -0,0 +1,107 @@
+/* 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_ROOM_INPUT_H
+#define HARVESTER_ROOM_INPUT_H
+
+#include "common/error.h"
+#include "common/events.h"
+
+namespace Harvester {
+
+class Art;
+class CheatSystem;
+class EntityManager;
+class Flow;
+class HarvesterEngine;
+class InventorySystem;
+class RoomInteractionCallbacks;
+class RoomInteractionProcessor;
+class RoomItemInteractions;
+class RoomSession;
+struct ObjectRecord;
+struct ResolvedText;
+struct RoomPresentationState;
+struct RoomSceneResources;
+
+struct RoomInputState {
+ bool moveLeft = false;
+ bool moveRight = false;
+ bool moveUp = false;
+ bool moveDown = false;
+ bool attackModifierHeld = false;
+ uint32 lastLeftButtonReleaseTick = 0;
+ uint32 nextKeyboardAttackAllowedTick = 0;
+};
+
+// Enter and closeup exits return from runRoomLoop even when the error is kNoError.
+struct RoomInputResult {
+ Common::Error error;
+ bool returnFromRoom = false;
+
+ static RoomInputResult leaveRoom(const Common::Error &error) {
+ RoomInputResult result;
+ result.error = error;
+ result.returnFromRoom = true;
+ return result;
+ }
+};
+
+class RoomInput {
+public:
+ RoomInput(HarvesterEngine &engine, Flow &flow, RoomSession &session, RoomSceneResources &scene,
+ EntityManager *entityManager, InventorySystem &inventory, CheatSystem &cheats,
+ RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
+ RoomInteractionCallbacks &interactionCallbacks, RoomPresentationState &presentationState,
+ const Art &art, bool canExitCloseupToParent);
+
+ RoomInputResult handleEvent(const Common::Event &event, bool &needsRedraw);
+
+private:
+ void handleMouseMove(bool &needsRedraw);
+ RoomInputResult handleRightButtonDown(bool &needsRedraw);
+ void handleLeftButtonUp();
+ RoomInputResult handleLeftButtonDown(bool &needsRedraw);
+ RoomInputResult handleKeyDown(const Common::KeyState &key, bool &needsRedraw);
+ void handleKeyUp(const Common::KeyState &key);
+ void beginInspectText(const ObjectRecord &object, const ResolvedText &text);
+ void queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw);
+ bool openInventoryOverlay();
+ void capturePlayerAttackTarget();
+
+ HarvesterEngine &_engine;
+ Flow &_flow;
+ RoomSession &_session;
+ RoomSceneResources &_scene;
+ EntityManager *_entityManager;
+ InventorySystem &_inventory;
+ CheatSystem &_cheats;
+ RoomItemInteractions &_roomItems;
+ RoomInteractionProcessor &_interactionProcessor;
+ RoomInteractionCallbacks &_interactionCallbacks;
+ RoomPresentationState &_presentationState;
+ const Art &_art;
+ bool _canExitCloseupToParent;
+};
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_ROOM_INPUT_H
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
index 1c97dbeabf2..10aa4443041 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/room_presentation.h
@@ -39,6 +39,16 @@ struct IndexedBitmap;
struct ResolvedText;
struct RoomSceneResources;
+struct RoomPresentationState {
+ ResolvedText inspectText;
+ Common::String currentInteractionPromptText;
+ bool showingInspectText = false;
+ bool inspectCanDismiss = false;
+ ResolvedText combatLoadoutStatusText;
+ uint32 combatLoadoutStatusHideTick = 0;
+ bool closeInventoryAfterCombatLoadoutStatus = false;
+};
+
class RoomPresentation {
public:
RoomPresentation(HarvesterEngine &engine, RoomSceneResources &scene,
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index fa83ff4c9c0..dbbff282ca9 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -76,10 +76,10 @@ bool RoomSession::cancelPlayerAttackAnimation() {
}
void RoomSession::stopPlayerRegionInteraction() {
- _movementInput.moveLeft = false;
- _movementInput.moveRight = false;
- _movementInput.moveUp = false;
- _movementInput.moveDown = false;
+ _inputState.moveLeft = false;
+ _inputState.moveRight = false;
+ _inputState.moveUp = false;
+ _inputState.moveDown = false;
_pendingRegionName.clear();
(void)cancelPlayerAttackAnimation();
_playerState.hasMoveTarget = false;
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 1a132068bbb..3642623f263 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -23,6 +23,7 @@
#define HARVESTER_ROOM_SESSION_H
#include "harvester/room_combat.h"
+#include "harvester/room_input.h"
#include "harvester/room_scene_runtime.h"
#include "harvester/room_support.h"
#include "harvester/runtime_entity.h"
@@ -33,13 +34,6 @@ class EntityManager;
class Flow;
class InventorySystem;
-struct RoomMovementInputState {
- bool moveLeft = false;
- bool moveRight = false;
- bool moveUp = false;
- bool moveDown = false;
-};
-
// Scoped to one runRoomLoop iteration, including time spent in nested closeups.
// Entity pointers remain owned by EntityManager.
class RoomSession {
@@ -52,7 +46,7 @@ public:
RoomSceneRuntime &getSceneRuntime() { return _sceneRuntime; }
RoomCombatEffects &getCombatEffects() { return _combatEffects; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
- RoomMovementInputState &getMovementInput() { return _movementInput; }
+ RoomInputState &getInputState() { return _inputState; }
Common::String &getPendingRegionName() { return _pendingRegionName; }
void resetIdleState();
@@ -76,7 +70,7 @@ private:
RoomCombat _combat;
RoomCombatEffects _combatEffects;
RoomIdleAnimationState _idleState;
- RoomMovementInputState _movementInput;
+ RoomInputState _inputState;
Common::String _pendingRegionName;
};
Commit: 3238191c58f0321de1a28572ef43e612e72dbf8b
https://github.com/scummvm/scummvm/commit/3238191c58f0321de1a28572ef43e612e72dbf8b
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Refactor session and input classes for improved interaction handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/flow.cpp
engines/harvester/room.cpp
engines/harvester/room.h
engines/harvester/room_input.cpp
engines/harvester/room_input.h
engines/harvester/room_presentation.h
engines/harvester/room_session.cpp
engines/harvester/room_session.h
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index ae98fb4a091..2c7ae9ef5b1 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1357,7 +1357,7 @@ static void renderQuickTipsScreen(HarvesterEngine &engine, const RoomSceneResour
Flow::Flow(HarvesterEngine &engine)
: _engine(engine), _mousePos(320, 200), _dialogue(engine, _mousePos), _inventory(engine),
_menu(engine, _mousePos, _menuItems), _cheats(engine),
- _room(engine, _mousePos, _inventory, _cheats) {
+ _room(engine, _inventory, _cheats) {
}
bool Flow::load() {
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index f8dd76ee4c6..e67ef6539b2 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -50,18 +50,6 @@
namespace Harvester {
-static const char *const kPlayerInventoryLabel = "your inventory";
-
-static const int kNativeInventoryTooltipX = 0xbc;
-static const int kNativeInventoryTooltipY = 0x19e;
-static const int kNativeInventoryWeekdayX = 0x1e0;
-static const int kNativeInventoryWeekdayY = 0x18c;
-static const byte kNativeInventoryTooltipColor = 0xf4;
-
-static const int kNativeCombatResourceIconsX = 10;
-static const int kNativeCombatResourceIconsY = 30;
-static const int kNativeCombatResourceIconSpacing = 15;
-
static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
const Text *text = engine.getText();
if (!text || !fontName)
@@ -75,71 +63,9 @@ static const CftFontResource *findStartupFontByName(const HarvesterEngine &engin
return nullptr;
}
-static Common::String resolveStartupNpcLabel(const NpcRecord &npc) {
- Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
- for (uint i = 0; i < label.size(); ++i) {
- if (label[i] == '_')
- label.setChar(' ', i);
- }
-
- return label;
-}
-
-static void drawRoomPrompt(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::String &promptText, bool useNativeFont) {
- if (promptText.empty())
- return;
-
- if (useNativeFont) {
- font.drawString(&screen, promptText, 0, 462, 640, 0, Graphics::kTextAlignCenter);
- return;
- }
-
- font.drawString(&screen, promptText, 1, 463, 640, 0, Graphics::kTextAlignCenter);
- font.drawString(&screen, promptText, 0, 462, 640, 0xce, Graphics::kTextAlignCenter);
-}
-
-static void drawInventoryTooltip(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::String &tooltipText) {
- if (tooltipText.empty())
- return;
-
- font.drawString(&screen, tooltipText, kNativeInventoryTooltipX, kNativeInventoryTooltipY,
- font.getStringWidth(tooltipText), kNativeInventoryTooltipColor);
-}
-
-static void drawInventoryWeekday(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::String &weekdayText) {
- if (weekdayText.empty())
- return;
-
- font.drawString(&screen, weekdayText, kNativeInventoryWeekdayX, kNativeInventoryWeekdayY,
- font.getStringWidth(weekdayText), kNativeInventoryTooltipColor);
-}
-
-static void drawCombatLoadoutResourceIcons(Graphics::Screen &screen, const Art &art,
- const Script &script, int loadout) {
- CombatLoadoutHudInfo info;
- if (!resolveCombatLoadoutHudInfo(loadout, info))
- return;
-
- const Common::Array<IndexedBitmap> &ammoIcons = art.getAmmoIcons();
- if (info.iconIndex >= ammoIcons.size())
- return;
-
- const IndexedBitmap &icon = ammoIcons[info.iconIndex];
- if (!icon.isValid())
- return;
-
- const int iconCount = CLIP<int>(script.getPlayerCombatResourceCount(loadout), 0, info.maxVisibleCount);
- for (int i = 0; i < iconCount; ++i)
- RoomPresentation::blitBitmap(screen, icon, kNativeCombatResourceIconsX + i * kNativeCombatResourceIconSpacing,
- kNativeCombatResourceIconsY);
-}
-
-RoomSystem::RoomSystem(HarvesterEngine &engine, Common::Point &mousePos,
+RoomSystem::RoomSystem(HarvesterEngine &engine,
InventorySystem &inventory, CheatSystem &cheats)
- : _engine(engine), _mousePos(mousePos), _inventory(inventory), _cheats(cheats) {
+ : _engine(engine), _inventory(inventory), _cheats(cheats) {
}
Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetName,
@@ -225,10 +151,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (!loadRoomSceneResources(state, *_engine.getResources(), scene))
return Common::kReadingFailed;
- auto getActiveScreen = [&]() -> Graphics::Screen * {
- return _engine.getScreen();
- };
- Graphics::Screen *screen = getActiveScreen();
+ Graphics::Screen *screen = _engine.getScreen();
const Art *art = _engine.getArt();
const Graphics::Font *bodyFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
const CftFontResource *promptFontResource = findStartupFontByName(_engine, "MEDFONT1");
@@ -274,267 +197,15 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
flow.executeStartupAudioCommands(scene.state.audioCommands);
if (!scene.state.musicPath.empty())
(void)_engine.playMusic(scene.state.musicPath);
- RoomSession session(_engine, flow, scene, entityManager, _inventory);
+ const RoomViewResources view = { art, promptFont, inspectFont, inventoryTooltipFont,
+ useNativePromptFont, useNativeInspectFont };
+ RoomSession session(_engine, flow, scene, entityManager, _inventory, _cheats, view,
+ flow._roomLoopDepth > 1);
debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
- RoomPlayerState &playerState = session.getPlayerState();
- playerState.entity = entityManager ? entityManager->findSceneEntityByName("PLAYER") : nullptr;
- playerState.centerX = scene.state.playerSpawnX;
- playerState.bottomY = scene.state.playerSpawnY;
- playerState.z = (float)scene.state.playerSpawnZ;
- playerState.facing = scene.state.playerFacing;
- playerState.combatLoadout =
- _engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- RoomCombat &combat = session.getCombat();
- combat.initializeCombatStates(scene.state);
- RoomSceneRuntime &sceneRuntime = session.getSceneRuntime();
- RoomCombatEffects &combatEffects = session.getCombatEffects();
- RoomPresentationState presentationState;
-
- RoomInputState &inputState = session.getInputState();
-
- Common::String &pendingRegionName = session.getPendingRegionName();
- Common::String pendingRoomChange;
- bool pendingRoomChangeIsRoomName = false;
- bool pendingRoomChangeUsesSavedRoomState = false;
- SaveRoomState pendingRoomChangeSavedRoomState;
- const bool canExitCloseupToParent = flow._roomLoopDepth > 1;
-
- RoomIdleAnimationState &idleState = session.getIdleState();
- bool needsRedraw = true;
- RoomItemInteractions roomItems(_engine, scene, sceneRuntime, _inventory, pendingRoomChange);
+ session.initializePlayerState();
session.resetIdleState();
- auto syncAnimatedRoomActorPlacement = [&]() {
- Player::syncAnimationPlacement(playerState);
- if (!entityManager)
- return;
-
- // Class-4 NPCs retain their spawn base through ambient and death animation banks.
- for (const MonsterRecord &monster : scene.state.roomMonsters) {
- Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
- if (!entity)
- continue;
- (void)applyRoomActorPlacement(scene.state, *entity,
- monster.posX, monster.posY, (float)monster.posZ);
- }
- };
-
- RoomPresentation presentation(_engine, scene, art, inspectFont, useNativeInspectFont);
-
- auto runRoomExitCommands = [&]() -> Common::Error {
- InteractionResult exitInteraction;
- if (!_engine.getScript()->executeRoomExitCommands(scene.state.roomName, exitInteraction))
- return Common::kReadingFailed;
-
- for (uint exitStep = 0; exitStep < 128; ++exitStep) {
- if (exitInteraction.cdChangeDisc > 0) {
- Common::Error cdPromptError = flow.showCdChangePrompt(exitInteraction.cdChangeDisc);
- if (cdPromptError.getCode() != Common::kNoError)
- return cdPromptError;
- }
- if (!exitInteraction.musicPath.empty())
- (void)_engine.playMusic(exitInteraction.musicPath);
- flow.executeStartupAudioCommands(exitInteraction.audioCommands);
- if (exitInteraction.mutatedRuntimeState)
- sceneRuntime.syncGlobalTimerEntities(exitInteraction.previousTimerRecords, true);
-
- if (!exitInteraction.cutscenePath.empty()) {
- FstPlayer fstPlayer(_engine);
- if (!fstPlayer.play(exitInteraction.cutscenePath))
- return Common::kReadingFailed;
- }
-
- if (!exitInteraction.dialogueNpcName.empty() ||
- !exitInteraction.dialogueContinuationTag.empty() ||
- !exitInteraction.modalText.value.empty() ||
- exitInteraction.lightingCommand != kStartupLightingCommandNone ||
- exitInteraction.requestPlayerGotoXZ ||
- exitInteraction.requestPlayerGotoZ) {
- debugC(1, kDebugRoom,
- "Harvester: room exit command for '%s' produced unsupported deferred output; preserving accumulated state",
- scene.state.roomName.c_str());
- }
-
- if (exitInteraction.continuationTag.empty())
- return Common::kNoError;
-
- InteractionResult continuationInteraction;
- if (!_engine.getScript()->executeActionTag(
- exitInteraction.continuationTag, continuationInteraction, false,
- scene.state.roomName)) {
- return Common::kNoError;
- }
-
- exitInteraction = Common::move(continuationInteraction);
- }
-
- warning("Harvester: room exit command chain for '%s' exceeded continuation safety limit",
- scene.state.roomName.c_str());
- return Common::kNoError;
- };
-
- auto applyPlayerGotoXZ = [&](int x, int z) {
- playerState.hasMoveTarget = false;
- playerState.turnActive = false;
- playerState.turnTargetFacing = -1;
- playerState.centerX = x;
- playerState.z = (float)z;
- playerState.bottomY = mapRoomDepthToScreenYForCombat(
- scene.state, playerState.z, playerState.bottomY);
- Common::Rect screenRect;
- bool hasScreenRect = false;
- float depthScale = 0.0f;
- if (playerState.entity) {
- (void)applyRoomActorPlacement(scene.state, *playerState.entity,
- playerState.centerX, playerState.bottomY, playerState.z);
- if (entityManager)
- entityManager->reinsertSceneEntity(playerState.entity);
- screenRect = playerState.entity->getScreenRect();
- hasScreenRect = true;
- depthScale = playerState.entity->getDepthScale();
- }
- debugC(1, kDebugPlayer,
- "Harvester: PC_GOTO_XZ applied x=%d z=%d pos=(%d,%d,z=%.2f) screen_rect=(%d,%d)-(%d,%d) depth_scale=%.3f",
- x, z, playerState.centerX, playerState.bottomY, (double)playerState.z,
- hasScreenRect ? screenRect.left : 0, hasScreenRect ? screenRect.top : 0,
- hasScreenRect ? screenRect.right : 0, hasScreenRect ? screenRect.bottom : 0,
- (double)depthScale);
- session.resetIdleState();
- session.captureCurrentSaveState();
- };
-
- auto requestPlayerGameOver = [&](const char *reason, const Common::String &sourceName) {
- if (flow.hasPendingMainMenuReturn())
- return;
-
- debugC(1, kDebugCombat,
- "Harvester: player game over queued reason='%s' source='%s' hp=%d",
- reason ? reason : "", sourceName.c_str(),
- _engine.getScript()
- ? _engine.getScript()->getPlayerCurrentHitPoints()
- : 0);
- flow.requestGameOverReturn();
- };
- auto startPlayerDefeatSequence = [&](const char *reason, const Common::String &sourceName,
- int damageType) {
- if (flow.hasPendingMainMenuReturn() || playerState.deathActive)
- return;
-
- 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);
-
- if (!Player::startDeathAnimation(playerState, damageType, _engine.isGoreEnabled())) {
- debugC(1, kDebugCombat,
- "Harvester: player defeat fallback reason='%s' source='%s' damage_type=%d hp=%d",
- reason ? reason : "", sourceName.c_str(), damageType,
- _engine.getScript()
- ? _engine.getScript()->getPlayerCurrentHitPoints()
- : 0);
- requestPlayerGameOver(reason, sourceName);
- return;
- }
-
- debugC(1, kDebugCombat,
- "Harvester: player defeat started reason='%s' source='%s' damage_type=%d hp=%d",
- reason ? reason : "", sourceName.c_str(), damageType,
- _engine.getScript()
- ? _engine.getScript()->getPlayerCurrentHitPoints()
- : 0);
- };
- typedef decltype(runRoomExitCommands) RunRoomExitCommandsFn;
- typedef decltype(applyPlayerGotoXZ) ApplyPlayerGotoXZFn;
- typedef decltype(startPlayerDefeatSequence) StartPlayerDefeatSequenceFn;
-
- struct RoomInteractionCallbacksImpl : RoomInteractionCallbacks {
- RoomInteractionCallbacksImpl(
- RoomSession &session_, RoomPresentation &presentation_, Flow &flow_,
- RunRoomExitCommandsFn &runRoomExitCommandsFn_,
- ApplyPlayerGotoXZFn &applyPlayerGotoXZFn_,
- StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn_)
- : session(session_), presentation(presentation_), flow(flow_),
- runRoomExitCommandsFn(runRoomExitCommandsFn_),
- applyPlayerGotoXZFn(applyPlayerGotoXZFn_),
- startPlayerDefeatSequenceFn(startPlayerDefeatSequenceFn_) {
- }
-
- bool refreshCurrentScene(bool preservePlayerPlacement) override {
- return session.refreshCurrentScene(preservePlayerPlacement);
- }
-
- bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) override {
- return session.applyCurrentRoomRuntimeMutationsInPlace(preservePaletteState);
- }
-
- void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) override {
- session.getSceneRuntime().syncGlobalTimerEntities(previousTimerRecords);
- }
-
- bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop) override {
- return presentation.captureDialogueBackdrop(dialogueBackdrop);
- }
-
- Common::Error showCdChangePrompt(int discNumber) override {
- return flow.showCdChangePrompt(discNumber);
- }
-
- Common::Error runRoomExitCommands() override {
- return runRoomExitCommandsFn();
- }
-
- Common::Error applyLightingCommand(StartupLightingCommand lightingCommand) override {
- return presentation.applyLightingCommand(lightingCommand);
- }
-
- void applyPlayerGotoXZ(int x, int z) override {
- applyPlayerGotoXZFn(x, z);
- }
-
- Common::Error runModalShowText(const ResolvedText &modalText) override {
- return presentation.runModalShowText(modalText, flow);
- }
-
- void resetIdleState() override {
- session.resetIdleState();
- }
-
- void notePlayerActivity() override {
- session.notePlayerActivity();
- }
-
- void stopPlayerRegionInteraction() override {
- session.stopPlayerRegionInteraction();
- }
-
- void startPlayerDefeatSequence(const char *reason, const Common::String &sourceName,
- int damageType) override {
- startPlayerDefeatSequenceFn(reason, sourceName, damageType);
- }
-
- RoomSession &session;
- RoomPresentation &presentation;
- Flow &flow;
- RunRoomExitCommandsFn &runRoomExitCommandsFn;
- ApplyPlayerGotoXZFn &applyPlayerGotoXZFn;
- StartPlayerDefeatSequenceFn &startPlayerDefeatSequenceFn;
- };
- RoomInteractionCallbacksImpl interactionCallbacks(
- session, presentation, flow, runRoomExitCommands, applyPlayerGotoXZ,
- startPlayerDefeatSequence);
- RoomInteractionProcessor interactionProcessor(
- _engine, flow, scene, playerState, pendingRegionName, pendingRoomChange,
- pendingRoomChangeIsRoomName, pendingRoomChangeUsesSavedRoomState,
- pendingRoomChangeSavedRoomState, canExitCloseupToParent, interactionCallbacks);
InteractionResult roomEntryInteraction;
bool hasPendingRoomEntryInteraction = false;
if (shouldRunRoomEntryCommands) {
@@ -545,7 +216,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
// Native room_setup applies entry-time state changes before the first visible room frame.
if (roomEntryInteraction.mutatedRuntimeState) {
- sceneRuntime.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
+ session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
if (!session.applyCurrentRoomRuntimeMutationsInPlace(false) &&
!session.refreshCurrentScene(true)) {
return Common::kReadingFailed;
@@ -574,13 +245,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
if (shouldRunRoomEntryCommands) {
if (hasPendingRoomEntryInteraction) {
- bool didEntryTransition = false;
- Common::Error entryInteractionError = interactionProcessor.handleInteractionResult(
- roomEntryInteraction, didEntryTransition, Common::String());
- if (entryInteractionError.getCode() != Common::kNoError)
- return entryInteractionError;
- if (flow.hasPendingMainMenuReturn() || didEntryTransition)
- return Common::kNoError;
+ const RoomInputResult entryResult = session.runRoomEntryInteraction(roomEntryInteraction);
+ if (entryResult.returnFromRoom)
+ return entryResult.error;
}
flow.resetCursorAnimationSequence();
@@ -589,411 +256,15 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
_engine.getScript());
- RoomInput input(_engine, flow, session, scene, entityManager, _inventory, _cheats,
- roomItems, interactionProcessor, interactionCallbacks, presentationState, *art, canExitCloseupToParent);
-
- if (!_inventory.refresh())
- return Common::kReadingFailed;
- Graphics::FrameLimiter limiter(g_system, 60);
- session.captureCurrentSaveState();
-
- if (shouldRunStartupRoomProbe())
- logStartupRoomProbe(_engine, scene, currentRoomTarget, _mousePos, flow._menuTextConfig);
-
- while (!_engine.shouldQuit()) {
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (flow.takePendingNewGameRestart()) {
- flow.prepareForNewGame();
- pendingRoomChange = "START";
- pendingRoomChangeIsRoomName = false;
- pendingRoomChangeUsesSavedRoomState = false;
- pendingRoomChangeSavedRoomState.clear();
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- if (_engine.hasPendingLoadedSaveRoomState()) {
- pendingRoomChange = _engine.getPendingLoadedSaveRoomState().entranceName;
- pendingRoomChangeIsRoomName = false;
- pendingRoomChangeUsesSavedRoomState = false;
- pendingRoomChangeSavedRoomState.clear();
- if (pendingRoomChange.empty()) {
- pendingRoomChange = _engine.getPendingLoadedSaveRoomState().roomName;
- pendingRoomChangeIsRoomName = true;
- }
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- if (flow.hasPendingDebugRoomChange()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- return Common::kNoError;
- }
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- InteractionResult queuedDebugInteraction;
- if (flow.takeQueuedDebugInteraction(queuedDebugInteraction)) {
- // Run console-injected commands through the normal room interaction handler so
- // CHANGE_ROOM, modal text, dialogue, and chained tags behave exactly like script-driven actions.
- bool didDebugTransition = false;
- Common::Error debugInteractionError = interactionProcessor.handleInteractionResult(
- queuedDebugInteraction, didDebugTransition, Common::String());
- if (debugInteractionError.getCode() != Common::kNoError)
- return debugInteractionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- needsRedraw = true;
- }
- session.captureCurrentSaveState();
- if (_inventory.isOpen() && _inventory.refreshIfRuntimeStateChanged())
- needsRedraw = true;
- if (!presentationState.combatLoadoutStatusText.value.empty() &&
- presentationState.combatLoadoutStatusHideTick != 0 &&
- (int32)(Player::getRuntimeClockTicks() - presentationState.combatLoadoutStatusHideTick) >= 0) {
- presentationState.combatLoadoutStatusText = ResolvedText();
- presentationState.combatLoadoutStatusHideTick = 0;
- if (presentationState.closeInventoryAfterCombatLoadoutStatus) {
- presentationState.closeInventoryAfterCombatLoadoutStatus = false;
- if (_inventory.close())
- needsRedraw = true;
- }
- needsRedraw = true;
- }
-
- if (needsRedraw) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return Common::kNoError;
-
- const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
- const bool inventorySelectionActive = _inventory.hasSelection();
- const bool roomCarryActive = roomItems.hasCarriedRoomItem();
- const bool activeCarry = inventorySelectionActive || roomCarryActive;
- const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_mousePos);
- const bool suppressHover = presentationState.showingInspectText || idleState.active || idleState.exiting ||
- session.isPlayerCombatLocked() ||
- (_inventory.isOpen() && (inventoryPanelContainsMouse || !inventorySelectionActive));
- RoomHoverState hoverState = suppressHover
- ? RoomHoverState()
- : resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
- scene.sceneRegions, _mousePos, flow._menuTextConfig, &flow._dialogue);
- if (!suppressHover && inventorySelectionActive && !hoverState.npc) {
- if (ObjectRecord *selectedTarget = roomItems.findSelectedInventoryRoomTarget(_mousePos))
- hoverState.object = selectedTarget;
- }
- Common::String promptText;
- Common::String inventoryTooltipText;
- auto resolveCarryTargetLabel = [&]() {
- if (hoverState.playerEntity && playerState.entity && hoverState.playerEntity == playerState.entity)
- return Common::String(kPlayerInventoryLabel);
- if (hoverState.object)
- return _engine.getScript()->resolveObjectLabel(*hoverState.object);
- if (hoverState.npc)
- return resolveStartupNpcLabel(*hoverState.npc);
- return Common::String();
- };
- if (activeCarry) {
- Common::String targetLabel;
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_mousePos);
- if (_inventory.isOpen()) {
- if (inventoryHover && !InventorySystem::isExitObject(inventoryHover->object) &&
- !InventorySystem::isStatusObject(inventoryHover->object) &&
- !inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
- targetLabel = _engine.getScript()->resolveObjectLabel(inventoryHover->object);
- } else if (!inventoryPanelContainsMouse) {
- targetLabel = resolveCarryTargetLabel();
- }
- } else {
- targetLabel = resolveCarryTargetLabel();
- }
- if (inventorySelectionActive) {
- promptText = _inventory.buildSelectedPrompt(targetLabel, flow._menuTextConfig);
- _inventory.setPromptText(promptText);
- } else {
- promptText = buildUseItemPrompt(
- flow._menuTextConfig, roomItems.getCarriedRoomItemLabel(), targetLabel);
- }
- hoverState.cursorSequence = 7;
- } else if (_inventory.isOpen()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_mousePos);
- if (inventoryHover &&
- !InventorySystem::isExitObject(inventoryHover->object)) {
- inventoryTooltipText =
- _engine.getScript()->resolveInventoryTooltipText(inventoryHover->object);
- }
- _inventory.setPromptText(Common::String());
- hoverState.cursorSequence = 7;
- } else {
- promptText = hoverState.promptText;
- }
- if (!presentationState.showingInspectText)
- presentationState.currentInteractionPromptText = promptText;
- if (Entity *cursor = entityManager ? entityManager->getCursorEntity() : nullptr) {
- cursor->setAnimationSequence(
- (presentationState.showingInspectText || idleState.active || idleState.exiting ||
- session.isPlayerCombatLocked() ||
- _inventory.isOpen() || activeCarry)
- ? 7
- : hoverState.cursorSequence);
- }
-
- drawRoomScene(_engine, *activeScreen, scene, scene.targetPaletteBrightness);
- combatEffects.drawCombatDamagePopups(*activeScreen, _engine.isCombatDebugEnabled());
- if (_engine.getScript() && playerState.entity) {
- drawCombatLoadoutResourceIcons(*activeScreen, *art, *_engine.getScript(),
- _engine.getScript()->getPlayerCombatLoadout());
- }
- if (_inventory.isOpen()) {
- _inventory.drawOverlay(*activeScreen);
- drawInventoryWeekday(*activeScreen, *inventoryTooltipFont,
- _inventory.resolveWeekdayLabel(flow._menuTextConfig));
- }
- if (inventorySelectionActive) {
- _inventory.drawSelectedDragItem(*activeScreen, _mousePos);
- } else if (roomCarryActive && roomItems.getCarriedRoomItemBitmap().isValid()) {
- const int drawX = _mousePos.x - (int)roomItems.getCarriedRoomItemBitmap().width / 2;
- const int drawY = _mousePos.y - (int)roomItems.getCarriedRoomItemBitmap().height / 2;
- RoomPresentation::blitBitmap(*activeScreen, roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
- }
-
- if (presentationState.showingInspectText) {
- drawRoomInspectText(*activeScreen, *art, *inspectFont, presentationState.inspectText, useNativeInspectFont);
- if (!presentationState.currentInteractionPromptText.empty())
- drawRoomPrompt(*activeScreen, *promptFont,
- presentationState.currentInteractionPromptText, useNativePromptFont);
- } else if (!presentationState.combatLoadoutStatusText.value.empty()) {
- drawRoomInspectText(*activeScreen, *art, *inspectFont, presentationState.combatLoadoutStatusText,
- useNativeInspectFont);
- } else if (!inventoryTooltipText.empty()) {
- drawInventoryTooltip(*activeScreen, *inventoryTooltipFont, inventoryTooltipText);
- } else if (!promptText.empty()) {
- drawRoomPrompt(*activeScreen, *promptFont, promptText, useNativePromptFont);
- }
-
- if (entityManager)
- entityManager->drawCursor(*activeScreen);
- activeScreen->makeAllDirty();
- activeScreen->update();
- needsRedraw = false;
- }
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- const RoomInputResult inputResult = input.handleEvent(event, needsRedraw);
- if (inputResult.returnFromRoom)
- return inputResult.error;
- }
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
-
- Script *script = _engine.getScript();
- const bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
- const bool playerControlPaused = script && script->isPlayerControlPaused();
- const bool playerCanAct = playerAlive && !playerControlPaused;
- if (!playerAlive && (inputState.moveLeft || inputState.moveRight || inputState.moveUp || inputState.moveDown ||
- playerState.hasMoveTarget || playerState.turnActive ||
- playerState.attackActive || playerState.hitActive)) {
- inputState.attackModifierHeld = false;
- session.stopPlayerRegionInteraction();
- }
-
- // Native run_harvester_main_loop checks player/region overlap before
- // update_actor_runtime_state. This is significant when a wide walk frame
- // reaches a narrow exit: the actor update may otherwise satisfy the X
- // target and replace that frame with a narrower idle frame first.
- Common::Error pendingRegionError =
- session.isPlayerCombatLocked()
- ? Common::kNoError
- : interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
- if (pendingRegionError.getCode() != Common::kNoError)
- return pendingRegionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- if (!session.isPlayerCombatLocked()) {
- pendingRegionError = interactionProcessor.tryActivatePassiveRegion(_inventory, needsRedraw);
- if (pendingRegionError.getCode() != Common::kNoError)
- return pendingRegionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- }
-
- // At 60 Hz, consume animation advances before the next screen update so
- // a walk frame and its movement step are presented together.
- if (flow.tickRuntimeEntities())
- needsRedraw = true;
- syncAnimatedRoomActorPlacement();
-
- Common::Error combatError = Common::kNoError;
- if (!playerControlPaused) {
- combatError = combat.resolvePlayerAttackContact(_engine, scene.state, playerState, entityManager,
- sceneRuntime, combatEffects, interactionProcessor, needsRedraw);
- if (combatError.getCode() != Common::kNoError)
- return combatError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- if (Player::updateAttackAnimationState(_engine, playerState))
- needsRedraw = true;
- if (Player::updateHitAnimationState(
- _engine, scene.state, scene.sceneObjects, scene.sceneAnimations, playerState)) {
- needsRedraw = true;
- }
- }
- const bool keyboardAttackRequested =
- inputState.attackModifierHeld && (inputState.moveLeft || inputState.moveRight || inputState.moveUp || inputState.moveDown);
- if (playerCanAct &&
- !playerState.attackActive && !playerState.hitActive && keyboardAttackRequested &&
- !idleState.active && !idleState.exiting) {
- const uint32 now = Player::getRuntimeClockTicks();
- if (inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - inputState.nextKeyboardAttackAllowedTick) >= 0) {
- combat.capturePlayerAttackTargetForKeyboard(scene.state, playerState, entityManager, inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown);
- if (Player::startKeyboardAttackAnimation(
- scene.state, playerState, inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown)) {
- inputState.nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
- session.notePlayerActivity();
- debugC(1, kDebugCombat,
- "Harvester: combat player keyboard attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' input=(L=%d R=%d U=%d D=%d) facing=%d",
- playerState.combatLoadout,
- Player::describeCombatLoadout(playerState.combatLoadout),
- Player::resolveCombatLoadoutDamageAmount(playerState.combatLoadout),
- Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(playerState.combatLoadout)),
- describeCombatTargetClass(playerState.attackTargetClassId),
- playerState.attackTargetName.c_str(),
- inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown, playerState.facing);
- needsRedraw = true;
- } else {
- playerState.attackTargetName.clear();
- playerState.attackTargetClassId = -1;
- }
- }
- }
- if (playerCanAct && !playerState.attackActive && !playerState.hitActive &&
- Player::updateTurnAnimationState(playerState)) {
- needsRedraw = true;
- }
-
- if (playerCanAct &&
- !playerState.attackActive && !playerState.hitActive &&
- !keyboardAttackRequested && !idleState.active && !idleState.exiting) {
- const int regionFacing = interactionProcessor.getPendingRegionFacing();
- if (Player::stepKeyboardMovement(_engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
- playerState, inputState.moveLeft, inputState.moveRight, inputState.moveUp, inputState.moveDown)) {
- session.notePlayerActivity();
- needsRedraw = true;
- } else if (Player::stepMoveTarget(
- _engine, scene.state, scene.sceneObjects, scene.sceneAnimations,
- playerState, regionFacing)) {
- session.notePlayerActivity();
- needsRedraw = true;
- }
-
- if (!presentationState.showingInspectText && !keyboardAttackRequested &&
- !inputState.moveLeft && !inputState.moveRight && !inputState.moveUp && !inputState.moveDown &&
- !playerState.hasMoveTarget && !playerState.turnActive && !playerState.hitActive &&
- playerState.entity && playerState.facing >= 0 &&
- !Player::isIdleAnimationExcludedRoom(scene.state.roomName) &&
- Player::getRuntimeClockTicks() > idleState.triggerTick &&
- Player::startIdleAnimation(_engine, scene.state, playerState, idleState)) {
- needsRedraw = true;
- }
- }
- combatError = combat.updateRoomMonsterCombat(_engine, scene.state, playerState, idleState,
- entityManager, sceneRuntime, combatEffects, _cheats, interactionCallbacks, interactionProcessor, needsRedraw);
- if (combatError.getCode() != Common::kNoError)
- return combatError;
- if (combatEffects.syncCombatHitEffects())
- needsRedraw = true;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (!pendingRoomChange.empty()) {
- if (!roomItems.stowCarriedRoomItemToInventory())
- return Common::kReadingFailed;
- break;
- }
- if (Player::updateDeathAnimationState(playerState)) {
- requestPlayerGameOver("combat_player_death_complete", Common::String());
- needsRedraw = true;
- }
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (combatEffects.pruneCombatHitEffects())
- needsRedraw = true;
- if (combatEffects.pruneCombatDamagePopups())
- needsRedraw = true;
- if (entityManager) {
- Common::Array<Common::String> expiredTimerNames;
- if (entityManager->takeExpiredTimerNames(expiredTimerNames)) {
- sceneRuntime.syncCurrentRoomRuntimeState(scene.sceneRegions);
- for (const Common::String &timerName : expiredTimerNames) {
- InteractionResult timerInteraction;
- if (!_engine.getScript()->executeTimerAction(
- timerName, timerInteraction, true, scene.state.roomName))
- continue;
-
- bool didTransition = false;
- Common::Error interactionError =
- interactionProcessor.handleInteractionResult(
- timerInteraction, didTransition, Common::String(), false);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- needsRedraw = true;
- if (!pendingRoomChange.empty())
- break;
- }
- }
- }
- if (Player::updateIdleAnimation(scene.state, playerState, idleState))
- needsRedraw = true;
- if (_engine.isCombatDebugEnabled() && combatEffects.hasDamagePopups())
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
- if (_engine.shouldQuit())
- return Common::kNoError;
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (pendingRoomChange.empty())
- return Common::kNoError;
- if (pendingRoomChange.equals(kExitCloseupPendingRoomChange)) {
- flow.requestCloseupParentRestart();
- return Common::kNoError;
- }
+ const RoomInputResult frameResult = session.runRoomFrames(currentRoomTarget);
+ if (frameResult.returnFromRoom)
+ return frameResult.error;
+ const RoomPendingTransition &pendingTransition = session.getPendingTransition();
- currentRoomTarget = pendingRoomChange;
- currentTargetIsRoomName = pendingRoomChangeIsRoomName;
- currentTargetUsesSavedRoomState = pendingRoomChangeUsesSavedRoomState;
- currentTargetSavedRoomState = pendingRoomChangeSavedRoomState;
+ currentRoomTarget = pendingTransition.roomName;
+ currentTargetIsRoomName = pendingTransition.isRoomName;
+ currentTargetUsesSavedRoomState = pendingTransition.usesSavedRoomState;
+ currentTargetSavedRoomState = pendingTransition.savedRoomState;
}
return Common::kNoError;
diff --git a/engines/harvester/room.h b/engines/harvester/room.h
index 0f3cab9fc48..2c0f15e864d 100644
--- a/engines/harvester/room.h
+++ b/engines/harvester/room.h
@@ -23,7 +23,6 @@
#define HARVESTER_ROOM_H
#include "common/error.h"
-#include "common/rect.h"
#include "common/str.h"
namespace Harvester {
@@ -35,14 +34,13 @@ class InventorySystem;
class RoomSystem {
public:
- RoomSystem(HarvesterEngine &engine, Common::Point &mousePos, InventorySystem &inventory,
+ RoomSystem(HarvesterEngine &engine, InventorySystem &inventory,
CheatSystem &cheats);
Common::Error runRoomLoop(Flow &flow, const Common::String &targetName, bool targetIsRoomName);
private:
HarvesterEngine &_engine;
- Common::Point &_mousePos;
InventorySystem &_inventory;
CheatSystem &_cheats;
};
diff --git a/engines/harvester/room_input.cpp b/engines/harvester/room_input.cpp
index 7ca357be3ab..55346caf9d5 100644
--- a/engines/harvester/room_input.cpp
+++ b/engines/harvester/room_input.cpp
@@ -76,13 +76,13 @@ static void debugLogInventoryClick(const char *buttonLabel, const Common::Point
}
RoomInput::RoomInput(HarvesterEngine &engine, Flow &flow, RoomSession &session, RoomSceneResources &scene,
- EntityManager *entityManager, InventorySystem &inventory, CheatSystem &cheats,
+ InventorySystem &inventory, CheatSystem &cheats,
RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
- RoomInteractionCallbacks &interactionCallbacks, RoomPresentationState &presentationState,
+ RoomPresentationState &presentationState,
const Art &art, bool canExitCloseupToParent)
- : _engine(engine), _flow(flow), _session(session), _scene(scene), _entityManager(entityManager),
+ : _engine(engine), _flow(flow), _session(session), _scene(scene),
_inventory(inventory), _cheats(cheats), _roomItems(roomItems), _interactionProcessor(interactionProcessor),
- _interactionCallbacks(interactionCallbacks), _presentationState(presentationState), _art(art),
+ _presentationState(presentationState), _art(art),
_canExitCloseupToParent(canExitCloseupToParent) {
}
@@ -116,13 +116,12 @@ void RoomInput::queueCombatLoadoutStatusMessage(const Common::String &message, b
bool RoomInput::openInventoryOverlay() {
RoomPlayerState &playerState = _session.getPlayerState();
RoomInputState &inputState = _session.getInputState();
- Common::String &pendingRegionName = _session.getPendingRegionName();
inputState.moveLeft = false;
inputState.moveRight = false;
inputState.moveUp = false;
inputState.moveDown = false;
- pendingRegionName.clear();
+ _session.clearPendingRegionInteraction();
(void)_session.cancelPlayerAttackAnimation();
playerState.hasMoveTarget = false;
playerState.turnActive = false;
@@ -141,7 +140,7 @@ void RoomInput::capturePlayerAttackTarget() {
const RoomHoverState hoverState = resolveRoomHoverState(
_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs, _scene.sceneRegions,
_flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
- _session.getCombat().capturePlayerAttackTarget(_scene.state, playerState, _entityManager, _flow._mousePos, hoverState.npc);
+ _session.capturePlayerAttackTarget(_flow._mousePos, hoverState.npc);
}
void RoomInput::handleMouseMove(bool &needsRedraw) {
@@ -160,7 +159,6 @@ void RoomInput::handleMouseMove(bool &needsRedraw) {
RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
RoomPlayerState &playerState = _session.getPlayerState();
RoomIdleAnimationState &idleState = _session.getIdleState();
- Common::String &pendingRegionName = _session.getPendingRegionName();
_session.notePlayerActivity();
if (idleState.active || idleState.exiting) {
@@ -170,7 +168,7 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
}
if (_session.isPlayerCombatLocked())
return RoomInputResult();
- pendingRegionName.clear();
+ _session.clearPendingRegionInteraction();
if (_inventory.isOpen()) {
const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
debugLogInventoryClick("right", _flow._mousePos, inventoryHover);
@@ -324,7 +322,6 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
RoomPlayerState &playerState = _session.getPlayerState();
RoomIdleAnimationState &idleState = _session.getIdleState();
RoomInputState &inputState = _session.getInputState();
- Common::String &pendingRegionName = _session.getPendingRegionName();
const uint32 now = Player::getRuntimeClockTicks();
const bool isFastExitClick =
@@ -474,7 +471,7 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
return RoomInputResult();
}
if (hoverState.npc) {
- pendingRegionName.clear();
+ _session.clearPendingRegionInteraction();
inputState.moveLeft = false;
inputState.moveRight = false;
inputState.moveUp = false;
@@ -519,7 +516,7 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
? findSceneObjectByName(_scene.sceneObjects, hoverState.object->objectName)
: nullptr;
if (!clickedObject) {
- pendingRegionName.clear();
+ _session.clearPendingRegionInteraction();
if (hoverState.cursorSequence == 0 && playerState.entity) {
if (_engine.isPathfindingDebugEnabled()) {
const Common::Rect playerRect = playerState.entity->getScreenRect();
@@ -542,10 +539,10 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
}
return RoomInputResult();
}
- pendingRegionName.clear();
+ _session.clearPendingRegionInteraction();
if (clickedObject->objectName.equalsIgnoreCase("EXIT_BM") ||
clickedObject->objectName.equalsIgnoreCase("EXIT_HS")) {
- Common::Error exitError = _interactionCallbacks.runRoomExitCommands();
+ Common::Error exitError = _session.runRoomExitCommands();
if (exitError.getCode() != Common::kNoError)
return RoomInputResult::leaveRoom(exitError);
// Native closeup exits return through room_setup so the parent on-enter command runs.
@@ -578,7 +575,7 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
}
if (_engine.getScript()->isPickupObject(*clickedObject)) {
Common::Error pickupError = _roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
- _interactionProcessor, _interactionCallbacks, needsRedraw);
+ _interactionProcessor, _session, needsRedraw);
if (pickupError.getCode() != Common::kNoError)
return RoomInputResult::leaveRoom(pickupError);
return RoomInputResult();
@@ -613,7 +610,6 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
RoomPlayerState &playerState = _session.getPlayerState();
RoomIdleAnimationState &idleState = _session.getIdleState();
RoomInputState &inputState = _session.getInputState();
- Common::String &pendingRegionName = _session.getPendingRegionName();
if (_presentationState.showingInspectText) {
if (key.keycode == Common::KEYCODE_ESCAPE) {
@@ -701,8 +697,7 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
key.keycode == Common::KEYCODE_RCTRL) {
inputState.attackModifierHeld = true;
} else if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
- Common::Error debugKillError = _session.getCombat().killDebugActiveMonster(_engine, _scene.state, playerState, _entityManager,
- _flow._mousePos, _session.getCombatEffects(), _interactionProcessor, needsRedraw);
+ Common::Error debugKillError = _session.killDebugActiveMonster(_flow._mousePos, needsRedraw);
if (debugKillError.getCode() != Common::kNoError)
return RoomInputResult::leaveRoom(debugKillError);
return RoomInputResult();
@@ -728,7 +723,7 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
inputState.moveRight = false;
inputState.moveUp = false;
inputState.moveDown = false;
- pendingRegionName.clear();
+ _session.clearPendingRegionInteraction();
playerState.hasMoveTarget = false;
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
@@ -755,7 +750,7 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
key.keycode == Common::KEYCODE_KP_ENTER) {
if (!_roomItems.stowCarriedRoomItemToInventory())
return RoomInputResult::leaveRoom(Common::kReadingFailed);
- Common::Error exitError = _interactionCallbacks.runRoomExitCommands();
+ Common::Error exitError = _session.runRoomExitCommands();
if (exitError.getCode() != Common::kNoError)
return RoomInputResult::leaveRoom(exitError);
return RoomInputResult::leaveRoom(Common::kNoError);
diff --git a/engines/harvester/room_input.h b/engines/harvester/room_input.h
index 0cdbe9653a8..5986475b58c 100644
--- a/engines/harvester/room_input.h
+++ b/engines/harvester/room_input.h
@@ -29,11 +29,9 @@ namespace Harvester {
class Art;
class CheatSystem;
-class EntityManager;
class Flow;
class HarvesterEngine;
class InventorySystem;
-class RoomInteractionCallbacks;
class RoomInteractionProcessor;
class RoomItemInteractions;
class RoomSession;
@@ -68,9 +66,9 @@ struct RoomInputResult {
class RoomInput {
public:
RoomInput(HarvesterEngine &engine, Flow &flow, RoomSession &session, RoomSceneResources &scene,
- EntityManager *entityManager, InventorySystem &inventory, CheatSystem &cheats,
+ InventorySystem &inventory, CheatSystem &cheats,
RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
- RoomInteractionCallbacks &interactionCallbacks, RoomPresentationState &presentationState,
+ RoomPresentationState &presentationState,
const Art &art, bool canExitCloseupToParent);
RoomInputResult handleEvent(const Common::Event &event, bool &needsRedraw);
@@ -91,12 +89,10 @@ private:
Flow &_flow;
RoomSession &_session;
RoomSceneResources &_scene;
- EntityManager *_entityManager;
InventorySystem &_inventory;
CheatSystem &_cheats;
RoomItemInteractions &_roomItems;
RoomInteractionProcessor &_interactionProcessor;
- RoomInteractionCallbacks &_interactionCallbacks;
RoomPresentationState &_presentationState;
const Art &_art;
bool _canExitCloseupToParent;
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
index 10aa4443041..2339d791b79 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/room_presentation.h
@@ -39,6 +39,15 @@ struct IndexedBitmap;
struct ResolvedText;
struct RoomSceneResources;
+struct RoomViewResources {
+ const Art *art;
+ const Graphics::Font *promptFont;
+ const Graphics::Font *inspectFont;
+ const Graphics::Font *inventoryTooltipFont;
+ bool useNativePromptFont;
+ bool useNativeInspectFont;
+};
+
struct RoomPresentationState {
ResolvedText inspectText;
Common::String currentInteractionPromptText;
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/room_session.cpp
index dbbff282ca9..6406f03009d 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/room_session.cpp
@@ -21,7 +21,14 @@
#include "harvester/room_session.h"
+#include "common/algorithm.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/font.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
#include "harvester/detection.h"
+#include "harvester/fst_player.h"
#include "harvester/flow.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
@@ -31,10 +38,87 @@
namespace Harvester {
+static const char *const kPlayerInventoryLabel = "your inventory";
+static const int kNativeInventoryTooltipX = 0xbc;
+static const int kNativeInventoryTooltipY = 0x19e;
+static const int kNativeInventoryWeekdayX = 0x1e0;
+static const int kNativeInventoryWeekdayY = 0x18c;
+static const byte kNativeInventoryTooltipColor = 0xf4;
+static const int kNativeCombatResourceIconsX = 10;
+static const int kNativeCombatResourceIconsY = 30;
+static const int kNativeCombatResourceIconSpacing = 15;
+static Common::String resolveStartupNpcLabel(const NpcRecord &npc) {
+ Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
+ for (uint i = 0; i < label.size(); ++i) {
+ if (label[i] == '_')
+ label.setChar(' ', i);
+ }
+
+ return label;
+}
+
+static void drawRoomPrompt(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &promptText, bool useNativeFont) {
+ if (promptText.empty())
+ return;
+
+ if (useNativeFont) {
+ font.drawString(&screen, promptText, 0, 462, 640, 0, Graphics::kTextAlignCenter);
+ return;
+ }
+
+ font.drawString(&screen, promptText, 1, 463, 640, 0, Graphics::kTextAlignCenter);
+ font.drawString(&screen, promptText, 0, 462, 640, 0xce, Graphics::kTextAlignCenter);
+}
+
+static void drawInventoryTooltip(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &tooltipText) {
+ if (tooltipText.empty())
+ return;
+
+ font.drawString(&screen, tooltipText, kNativeInventoryTooltipX, kNativeInventoryTooltipY,
+ font.getStringWidth(tooltipText), kNativeInventoryTooltipColor);
+}
+
+static void drawInventoryWeekday(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::String &weekdayText) {
+ if (weekdayText.empty())
+ return;
+
+ font.drawString(&screen, weekdayText, kNativeInventoryWeekdayX, kNativeInventoryWeekdayY,
+ font.getStringWidth(weekdayText), kNativeInventoryTooltipColor);
+}
+
+static void drawCombatLoadoutResourceIcons(Graphics::Screen &screen, const Art &art,
+ const Script &script, int loadout) {
+ CombatLoadoutHudInfo info;
+ if (!resolveCombatLoadoutHudInfo(loadout, info))
+ return;
+
+ const Common::Array<IndexedBitmap> &ammoIcons = art.getAmmoIcons();
+ if (info.iconIndex >= ammoIcons.size())
+ return;
+
+ const IndexedBitmap &icon = ammoIcons[info.iconIndex];
+ if (!icon.isValid())
+ return;
+
+ const int iconCount = CLIP<int>(script.getPlayerCombatResourceCount(loadout), 0, info.maxVisibleCount);
+ for (int i = 0; i < iconCount; ++i)
+ RoomPresentation::blitBitmap(screen, icon, kNativeCombatResourceIconsX + i * kNativeCombatResourceIconSpacing,
+ kNativeCombatResourceIconsY);
+}
+
RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
- InventorySystem &inventory)
+ InventorySystem &inventory, CheatSystem &cheats, const RoomViewResources &view, bool canExitCloseupToParent)
: _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory),
- _sceneRuntime(scene.state, engine.getScript(), engine.getResources(), entityManager), _combatEffects(entityManager) {
+ _sceneRuntime(scene.state, engine.getScript(), engine.getResources(), entityManager), _combatEffects(entityManager),
+ _cheats(cheats), _view(view), _canExitCloseupToParent(canExitCloseupToParent),
+ _roomItems(engine, scene, _sceneRuntime, inventory, _pendingTransition.roomName),
+ _presentation(engine, scene, view.art, view.inspectFont, view.useNativeInspectFont),
+ _interactionProcessor(engine, flow, scene, _playerState, _pendingRegionName, _pendingTransition.roomName,
+ _pendingTransition.isRoomName, _pendingTransition.usesSavedRoomState, _pendingTransition.savedRoomState,
+ canExitCloseupToParent, *this) {
}
void RoomSession::resetIdleState() {
@@ -181,4 +265,682 @@ bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
return _inventory.refresh();
}
+void RoomSession::initializePlayerState() {
+ _playerState.entity = _entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr;
+ _playerState.centerX = _scene.state.playerSpawnX;
+ _playerState.bottomY = _scene.state.playerSpawnY;
+ _playerState.z = (float)_scene.state.playerSpawnZ;
+ _playerState.facing = _scene.state.playerFacing;
+ _playerState.combatLoadout =
+ _engine.getScript() ? _engine.getScript()->getPlayerCombatLoadout() : 0;
+ _playerState.turnActive = false;
+ _playerState.turnTargetFacing = -1;
+ _combat.initializeCombatStates(_scene.state);
+}
+
+void RoomSession::syncAnimatedRoomActorPlacement() {
+ Player::syncAnimationPlacement(_playerState);
+ if (!_entityManager)
+ return;
+
+ // Class-4 NPCs retain their spawn base through ambient and death animation banks.
+ for (const MonsterRecord &monster : _scene.state.roomMonsters) {
+ Entity *entity = _entityManager->findSceneEntityByName(monster.monsterName);
+ if (!entity)
+ continue;
+ (void)applyRoomActorPlacement(_scene.state, *entity,
+ monster.posX, monster.posY, (float)monster.posZ);
+ }
+}
+
+Common::Error RoomSession::runRoomExitCommands() {
+ InteractionResult exitInteraction;
+ if (!_engine.getScript()->executeRoomExitCommands(_scene.state.roomName, exitInteraction))
+ return Common::kReadingFailed;
+
+ for (uint exitStep = 0; exitStep < 128; ++exitStep) {
+ if (exitInteraction.cdChangeDisc > 0) {
+ Common::Error cdPromptError = _flow.showCdChangePrompt(exitInteraction.cdChangeDisc);
+ if (cdPromptError.getCode() != Common::kNoError)
+ return cdPromptError;
+ }
+ if (!exitInteraction.musicPath.empty())
+ (void)_engine.playMusic(exitInteraction.musicPath);
+ _flow.executeStartupAudioCommands(exitInteraction.audioCommands);
+ if (exitInteraction.mutatedRuntimeState)
+ _sceneRuntime.syncGlobalTimerEntities(exitInteraction.previousTimerRecords, true);
+
+ if (!exitInteraction.cutscenePath.empty()) {
+ FstPlayer fstPlayer(_engine);
+ if (!fstPlayer.play(exitInteraction.cutscenePath))
+ return Common::kReadingFailed;
+ }
+
+ if (!exitInteraction.dialogueNpcName.empty() ||
+ !exitInteraction.dialogueContinuationTag.empty() ||
+ !exitInteraction.modalText.value.empty() ||
+ exitInteraction.lightingCommand != kStartupLightingCommandNone ||
+ exitInteraction.requestPlayerGotoXZ ||
+ exitInteraction.requestPlayerGotoZ) {
+ debugC(1, kDebugRoom,
+ "Harvester: room exit command for '%s' produced unsupported deferred output; preserving accumulated state",
+ _scene.state.roomName.c_str());
+ }
+
+ if (exitInteraction.continuationTag.empty())
+ return Common::kNoError;
+
+ InteractionResult continuationInteraction;
+ if (!_engine.getScript()->executeActionTag(
+ exitInteraction.continuationTag, continuationInteraction, false,
+ _scene.state.roomName)) {
+ return Common::kNoError;
+ }
+
+ exitInteraction = Common::move(continuationInteraction);
+ }
+
+ warning("Harvester: room exit command chain for '%s' exceeded continuation safety limit",
+ _scene.state.roomName.c_str());
+ return Common::kNoError;
+}
+
+void RoomSession::applyPlayerGotoXZ(int x, int z) {
+ _playerState.hasMoveTarget = false;
+ _playerState.turnActive = false;
+ _playerState.turnTargetFacing = -1;
+ _playerState.centerX = x;
+ _playerState.z = (float)z;
+ _playerState.bottomY = mapRoomDepthToScreenYForCombat(
+ _scene.state, _playerState.z, _playerState.bottomY);
+ Common::Rect screenRect;
+ bool hasScreenRect = false;
+ float depthScale = 0.0f;
+ if (_playerState.entity) {
+ (void)applyRoomActorPlacement(_scene.state, *_playerState.entity,
+ _playerState.centerX, _playerState.bottomY, _playerState.z);
+ if (_entityManager)
+ _entityManager->reinsertSceneEntity(_playerState.entity);
+ screenRect = _playerState.entity->getScreenRect();
+ hasScreenRect = true;
+ depthScale = _playerState.entity->getDepthScale();
+ }
+ debugC(1, kDebugPlayer,
+ "Harvester: PC_GOTO_XZ applied x=%d z=%d pos=(%d,%d,z=%.2f) screen_rect=(%d,%d)-(%d,%d) depth_scale=%.3f",
+ x, z, _playerState.centerX, _playerState.bottomY, (double)_playerState.z,
+ hasScreenRect ? screenRect.left : 0, hasScreenRect ? screenRect.top : 0,
+ hasScreenRect ? screenRect.right : 0, hasScreenRect ? screenRect.bottom : 0,
+ (double)depthScale);
+ resetIdleState();
+ captureCurrentSaveState();
+}
+
+void RoomSession::requestPlayerGameOver(const char *reason, const Common::String &sourceName) {
+ if (_flow.hasPendingMainMenuReturn())
+ return;
+
+ debugC(1, kDebugCombat,
+ "Harvester: player game over queued reason='%s' source='%s' hp=%d",
+ reason ? reason : "", sourceName.c_str(),
+ _engine.getScript()
+ ? _engine.getScript()->getPlayerCurrentHitPoints()
+ : 0);
+ _flow.requestGameOverReturn();
+}
+
+void RoomSession::startPlayerDefeatSequence(const char *reason, const Common::String &sourceName, int damageType) {
+ if (_flow.hasPendingMainMenuReturn() || _playerState.deathActive)
+ return;
+
+ 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);
+
+ if (!Player::startDeathAnimation(_playerState, damageType, _engine.isGoreEnabled())) {
+ debugC(1, kDebugCombat,
+ "Harvester: player defeat fallback reason='%s' source='%s' damage_type=%d hp=%d",
+ reason ? reason : "", sourceName.c_str(), damageType,
+ _engine.getScript()
+ ? _engine.getScript()->getPlayerCurrentHitPoints()
+ : 0);
+ requestPlayerGameOver(reason, sourceName);
+ return;
+ }
+
+ debugC(1, kDebugCombat,
+ "Harvester: player defeat started reason='%s' source='%s' damage_type=%d hp=%d",
+ reason ? reason : "", sourceName.c_str(), damageType,
+ _engine.getScript()
+ ? _engine.getScript()->getPlayerCurrentHitPoints()
+ : 0);
+}
+
+RoomInputResult RoomSession::runRoomEntryInteraction(const InteractionResult &roomEntryInteraction) {
+ bool didEntryTransition = false;
+ Common::Error entryInteractionError = _interactionProcessor.handleInteractionResult(
+ roomEntryInteraction, didEntryTransition, Common::String());
+ if (entryInteractionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(entryInteractionError);
+ if (_flow.hasPendingMainMenuReturn() || didEntryTransition)
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ return RoomInputResult();
+}
+
+void RoomSession::updateRoomInterface(bool &needsRedraw) {
+ captureCurrentSaveState();
+ if (_inventory.isOpen() && _inventory.refreshIfRuntimeStateChanged())
+ needsRedraw = true;
+ if (!_presentationState.combatLoadoutStatusText.value.empty() &&
+ _presentationState.combatLoadoutStatusHideTick != 0 &&
+ (int32)(Player::getRuntimeClockTicks() - _presentationState.combatLoadoutStatusHideTick) >= 0) {
+ _presentationState.combatLoadoutStatusText = ResolvedText();
+ _presentationState.combatLoadoutStatusHideTick = 0;
+ if (_presentationState.closeInventoryAfterCombatLoadoutStatus) {
+ _presentationState.closeInventoryAfterCombatLoadoutStatus = false;
+ if (_inventory.close())
+ needsRedraw = true;
+ }
+ needsRedraw = true;
+ }
+}
+
+bool RoomSession::drawRoomFrame(bool &needsRedraw) {
+ if (!needsRedraw)
+ return true;
+
+ Graphics::Screen *activeScreen = _engine.getScreen();
+ if (!activeScreen)
+ return false;
+
+ const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
+ const bool inventorySelectionActive = _inventory.hasSelection();
+ const bool roomCarryActive = _roomItems.hasCarriedRoomItem();
+ const bool activeCarry = inventorySelectionActive || roomCarryActive;
+ const bool inventoryPanelContainsMouse = _inventory.isOpen() && inventoryPanelBounds.contains(_flow._mousePos);
+ const bool suppressHover = _presentationState.showingInspectText || _idleState.active || _idleState.exiting ||
+ isPlayerCombatLocked() ||
+ (_inventory.isOpen() && (inventoryPanelContainsMouse || !inventorySelectionActive));
+ RoomHoverState hoverState = suppressHover
+ ? RoomHoverState()
+ : resolveRoomHoverState(_engine, _scene.state, _scene.sceneObjects, _scene.state.roomNpcs,
+ _scene.sceneRegions, _flow._mousePos, _flow._menuTextConfig, &_flow._dialogue);
+ if (!suppressHover && inventorySelectionActive && !hoverState.npc) {
+ if (ObjectRecord *selectedTarget = _roomItems.findSelectedInventoryRoomTarget(_flow._mousePos))
+ hoverState.object = selectedTarget;
+ }
+ Common::String promptText;
+ Common::String inventoryTooltipText;
+ auto resolveCarryTargetLabel = [&]() {
+ if (hoverState.playerEntity && _playerState.entity && hoverState.playerEntity == _playerState.entity)
+ return Common::String(kPlayerInventoryLabel);
+ if (hoverState.object)
+ return _engine.getScript()->resolveObjectLabel(*hoverState.object);
+ if (hoverState.npc)
+ return resolveStartupNpcLabel(*hoverState.npc);
+ return Common::String();
+ };
+ if (activeCarry) {
+ Common::String targetLabel;
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+ if (_inventory.isOpen()) {
+ if (inventoryHover && !InventorySystem::isExitObject(inventoryHover->object) &&
+ !InventorySystem::isStatusObject(inventoryHover->object) &&
+ !inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
+ targetLabel = _engine.getScript()->resolveObjectLabel(inventoryHover->object);
+ } else if (!inventoryPanelContainsMouse) {
+ targetLabel = resolveCarryTargetLabel();
+ }
+ } else {
+ targetLabel = resolveCarryTargetLabel();
+ }
+ if (inventorySelectionActive) {
+ promptText = _inventory.buildSelectedPrompt(targetLabel, _flow._menuTextConfig);
+ _inventory.setPromptText(promptText);
+ } else {
+ promptText = buildUseItemPrompt(
+ _flow._menuTextConfig, _roomItems.getCarriedRoomItemLabel(), targetLabel);
+ }
+ hoverState.cursorSequence = 7;
+ } else if (_inventory.isOpen()) {
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
+ if (inventoryHover &&
+ !InventorySystem::isExitObject(inventoryHover->object)) {
+ inventoryTooltipText =
+ _engine.getScript()->resolveInventoryTooltipText(inventoryHover->object);
+ }
+ _inventory.setPromptText(Common::String());
+ hoverState.cursorSequence = 7;
+ } else {
+ promptText = hoverState.promptText;
+ }
+ if (!_presentationState.showingInspectText)
+ _presentationState.currentInteractionPromptText = promptText;
+ if (Entity *cursor = _entityManager ? _entityManager->getCursorEntity() : nullptr) {
+ cursor->setAnimationSequence(
+ (_presentationState.showingInspectText || _idleState.active || _idleState.exiting ||
+ isPlayerCombatLocked() ||
+ _inventory.isOpen() || activeCarry)
+ ? 7
+ : hoverState.cursorSequence);
+ }
+
+ drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+ _combatEffects.drawCombatDamagePopups(*activeScreen, _engine.isCombatDebugEnabled());
+ if (_engine.getScript() && _playerState.entity) {
+ drawCombatLoadoutResourceIcons(*activeScreen, *_view.art, *_engine.getScript(),
+ _engine.getScript()->getPlayerCombatLoadout());
+ }
+ if (_inventory.isOpen()) {
+ _inventory.drawOverlay(*activeScreen);
+ drawInventoryWeekday(*activeScreen, *_view.inventoryTooltipFont,
+ _inventory.resolveWeekdayLabel(_flow._menuTextConfig));
+ }
+ if (inventorySelectionActive) {
+ _inventory.drawSelectedDragItem(*activeScreen, _flow._mousePos);
+ } else if (roomCarryActive && _roomItems.getCarriedRoomItemBitmap().isValid()) {
+ const int drawX = _flow._mousePos.x - (int)_roomItems.getCarriedRoomItemBitmap().width / 2;
+ const int drawY = _flow._mousePos.y - (int)_roomItems.getCarriedRoomItemBitmap().height / 2;
+ RoomPresentation::blitBitmap(*activeScreen, _roomItems.getCarriedRoomItemBitmap(), drawX, drawY);
+ }
+
+ if (_presentationState.showingInspectText) {
+ drawRoomInspectText(*activeScreen, *_view.art, *_view.inspectFont, _presentationState.inspectText, _view.useNativeInspectFont);
+ if (!_presentationState.currentInteractionPromptText.empty())
+ drawRoomPrompt(*activeScreen, *_view.promptFont,
+ _presentationState.currentInteractionPromptText, _view.useNativePromptFont);
+ } else if (!_presentationState.combatLoadoutStatusText.value.empty()) {
+ drawRoomInspectText(*activeScreen, *_view.art, *_view.inspectFont, _presentationState.combatLoadoutStatusText,
+ _view.useNativeInspectFont);
+ } else if (!inventoryTooltipText.empty()) {
+ drawInventoryTooltip(*activeScreen, *_view.inventoryTooltipFont, inventoryTooltipText);
+ } else if (!promptText.empty()) {
+ drawRoomPrompt(*activeScreen, *_view.promptFont, promptText, _view.useNativePromptFont);
+ }
+
+ if (_entityManager)
+ _entityManager->drawCursor(*activeScreen);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+ needsRedraw = false;
+
+ return true;
+}
+
+RoomInputResult RoomSession::pollRoomEvents(RoomInput &input, bool &needsRedraw) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ const RoomInputResult inputResult = input.handleEvent(event, needsRedraw);
+ if (inputResult.returnFromRoom)
+ return inputResult;
+ }
+ return RoomInputResult();
+}
+
+RoomSession::PlayerControlState RoomSession::preparePlayerControlState() {
+ Script *script = _engine.getScript();
+ const bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
+ const bool playerControlPaused = script && script->isPlayerControlPaused();
+ const bool playerCanAct = playerAlive && !playerControlPaused;
+ if (!playerAlive && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown ||
+ _playerState.hasMoveTarget || _playerState.turnActive ||
+ _playerState.attackActive || _playerState.hitActive)) {
+ _inputState.attackModifierHeld = false;
+ stopPlayerRegionInteraction();
+ }
+
+ PlayerControlState control;
+ control.alive = playerAlive;
+ control.paused = playerControlPaused;
+ control.canAct = playerCanAct;
+ return control;
+}
+
+void RoomSession::advanceRuntimeEntities(bool &needsRedraw) {
+ if (_flow.tickRuntimeEntities())
+ needsRedraw = true;
+ syncAnimatedRoomActorPlacement();
+}
+
+void RoomSession::updatePlayerCombatAnimations(bool &needsRedraw) {
+ if (Player::updateAttackAnimationState(_engine, _playerState))
+ needsRedraw = true;
+ if (Player::updateHitAnimationState(
+ _engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations, _playerState)) {
+ needsRedraw = true;
+ }
+}
+
+void RoomSession::updatePlayerControls(const PlayerControlState &control, bool &needsRedraw) {
+ const bool keyboardAttackRequested =
+ _inputState.attackModifierHeld && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown);
+ if (control.canAct &&
+ !_playerState.attackActive && !_playerState.hitActive && keyboardAttackRequested &&
+ !_idleState.active && !_idleState.exiting) {
+ const uint32 now = Player::getRuntimeClockTicks();
+ if (_inputState.nextKeyboardAttackAllowedTick == 0 || (int32)(now - _inputState.nextKeyboardAttackAllowedTick) >= 0) {
+ _combat.capturePlayerAttackTargetForKeyboard(_scene.state, _playerState, _entityManager, _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown);
+ if (Player::startKeyboardAttackAnimation(
+ _scene.state, _playerState, _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
+ _inputState.nextKeyboardAttackAllowedTick = now + kNativeKeyboardAttackRepeatTicks;
+ notePlayerActivity();
+ debugC(1, kDebugCombat,
+ "Harvester: _combat player keyboard attack start loadout=%d weapon='%s' damage=%d damage_type='%s' target_class='%s' target='%s' input=(L=%d R=%d U=%d D=%d) facing=%d",
+ _playerState.combatLoadout,
+ Player::describeCombatLoadout(_playerState.combatLoadout),
+ Player::resolveCombatLoadoutDamageAmount(_playerState.combatLoadout),
+ Player::describeCombatDamageType(Player::resolveCombatLoadoutDamageType(_playerState.combatLoadout)),
+ describeCombatTargetClass(_playerState.attackTargetClassId),
+ _playerState.attackTargetName.c_str(),
+ _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown, _playerState.facing);
+ needsRedraw = true;
+ } else {
+ _playerState.attackTargetName.clear();
+ _playerState.attackTargetClassId = -1;
+ }
+ }
+ }
+ if (control.canAct && !_playerState.attackActive && !_playerState.hitActive &&
+ Player::updateTurnAnimationState(_playerState)) {
+ needsRedraw = true;
+ }
+
+ if (control.canAct &&
+ !_playerState.attackActive && !_playerState.hitActive &&
+ !keyboardAttackRequested && !_idleState.active && !_idleState.exiting) {
+ const int regionFacing = _interactionProcessor.getPendingRegionFacing();
+ if (Player::stepKeyboardMovement(_engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
+ _playerState, _inputState.moveLeft, _inputState.moveRight, _inputState.moveUp, _inputState.moveDown)) {
+ notePlayerActivity();
+ needsRedraw = true;
+ } else if (Player::stepMoveTarget(
+ _engine, _scene.state, _scene.sceneObjects, _scene.sceneAnimations,
+ _playerState, regionFacing)) {
+ notePlayerActivity();
+ needsRedraw = true;
+ }
+
+ if (!_presentationState.showingInspectText && !keyboardAttackRequested &&
+ !_inputState.moveLeft && !_inputState.moveRight && !_inputState.moveUp && !_inputState.moveDown &&
+ !_playerState.hasMoveTarget && !_playerState.turnActive && !_playerState.hitActive &&
+ _playerState.entity && _playerState.facing >= 0 &&
+ !Player::isIdleAnimationExcludedRoom(_scene.state.roomName) &&
+ Player::getRuntimeClockTicks() > _idleState.triggerTick &&
+ Player::startIdleAnimation(_engine, _scene.state, _playerState, _idleState)) {
+ needsRedraw = true;
+ }
+ }
+}
+
+void RoomSession::updatePlayerDeath(bool &needsRedraw) {
+ if (Player::updateDeathAnimationState(_playerState)) {
+ requestPlayerGameOver("combat_player_death_complete", Common::String());
+ needsRedraw = true;
+ }
+}
+
+void RoomSession::pruneCombatEffects(bool &needsRedraw) {
+ if (_combatEffects.pruneCombatHitEffects())
+ needsRedraw = true;
+ if (_combatEffects.pruneCombatDamagePopups())
+ needsRedraw = true;
+}
+
+RoomInputResult RoomSession::dispatchExpiredTimers(bool &needsRedraw) {
+ if (_entityManager) {
+ Common::Array<Common::String> expiredTimerNames;
+ if (_entityManager->takeExpiredTimerNames(expiredTimerNames)) {
+ _sceneRuntime.syncCurrentRoomRuntimeState(_scene.sceneRegions);
+ for (const Common::String &timerName : expiredTimerNames) {
+ InteractionResult timerInteraction;
+ if (!_engine.getScript()->executeTimerAction(
+ timerName, timerInteraction, true, _scene.state.roomName))
+ continue;
+
+ bool didTransition = false;
+ Common::Error interactionError =
+ _interactionProcessor.handleInteractionResult(
+ timerInteraction, didTransition, Common::String(), false);
+ if (interactionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(interactionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ needsRedraw = true;
+ if (!_pendingTransition.roomName.empty())
+ break;
+ }
+ }
+ }
+ return RoomInputResult();
+}
+
+RoomInputResult RoomSession::runRoomFrames(const Common::String ¤tRoomTarget) {
+ RoomInput input(_engine, _flow, *this, _scene, _inventory, _cheats,
+ _roomItems, _interactionProcessor, _presentationState, *_view.art, _canExitCloseupToParent);
+ bool needsRedraw = true;
+
+ if (!_inventory.refresh())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ Graphics::FrameLimiter limiter(g_system, 60);
+ captureCurrentSaveState();
+
+ if (shouldRunStartupRoomProbe())
+ logStartupRoomProbe(_engine, _scene, currentRoomTarget, _flow._mousePos, _flow._menuTextConfig);
+
+ while (!_engine.shouldQuit()) {
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (_flow.takePendingNewGameRestart()) {
+ _flow.prepareForNewGame();
+ _pendingTransition.roomName = "START";
+ _pendingTransition.isRoomName = false;
+ _pendingTransition.usesSavedRoomState = false;
+ _pendingTransition.savedRoomState.clear();
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ if (_engine.hasPendingLoadedSaveRoomState()) {
+ _pendingTransition.roomName = _engine.getPendingLoadedSaveRoomState().entranceName;
+ _pendingTransition.isRoomName = false;
+ _pendingTransition.usesSavedRoomState = false;
+ _pendingTransition.savedRoomState.clear();
+ if (_pendingTransition.roomName.empty()) {
+ _pendingTransition.roomName = _engine.getPendingLoadedSaveRoomState().roomName;
+ _pendingTransition.isRoomName = true;
+ }
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ if (_flow.hasPendingDebugRoomChange()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ }
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ InteractionResult queuedDebugInteraction;
+ if (_flow.takeQueuedDebugInteraction(queuedDebugInteraction)) {
+ // Run console-injected commands through the normal room interaction handler so
+ // CHANGE_ROOM, modal text, dialogue, and chained tags behave exactly like script-driven actions.
+ bool didDebugTransition = false;
+ Common::Error debugInteractionError = _interactionProcessor.handleInteractionResult(
+ queuedDebugInteraction, didDebugTransition, Common::String());
+ if (debugInteractionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(debugInteractionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ needsRedraw = true;
+ }
+ updateRoomInterface(needsRedraw);
+ if (!drawRoomFrame(needsRedraw))
+ return RoomInputResult::leaveRoom(Common::kNoError);
+
+ const RoomInputResult inputResult = pollRoomEvents(input, needsRedraw);
+ if (inputResult.returnFromRoom)
+ return inputResult;
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+
+ const PlayerControlState control = preparePlayerControlState();
+ // Native run_harvester_main_loop checks player/region overlap before
+ // update_actor_runtime_state. This is significant when a wide walk frame
+ // reaches a narrow exit: the actor update may otherwise satisfy the X
+ // target and replace that frame with a narrower idle frame first.
+ Common::Error pendingRegionError =
+ tryActivatePendingRegion(needsRedraw);
+ if (pendingRegionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(pendingRegionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ if (!isPlayerCombatLocked()) {
+ pendingRegionError = tryActivatePassiveRegion(needsRedraw);
+ if (pendingRegionError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(pendingRegionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ }
+
+ // At 60 Hz, consume animation advances before the next screen update so
+ // a walk frame and its movement step are presented together.
+ advanceRuntimeEntities(needsRedraw);
+ Common::Error combatError = Common::kNoError;
+ if (!control.paused) {
+ combatError = resolvePlayerAttackContact(needsRedraw);
+ if (combatError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(combatError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ updatePlayerCombatAnimations(needsRedraw);
+ }
+ updatePlayerControls(control, needsRedraw);
+ combatError = updateRoomMonsterCombat(needsRedraw);
+ if (combatError.getCode() != Common::kNoError)
+ return RoomInputResult::leaveRoom(combatError);
+ syncCombatHitEffects(needsRedraw);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (!_pendingTransition.roomName.empty()) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ break;
+ }
+ updatePlayerDeath(needsRedraw);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ pruneCombatEffects(needsRedraw);
+ const RoomInputResult timerResult = dispatchExpiredTimers(needsRedraw);
+ if (timerResult.returnFromRoom)
+ return timerResult;
+ updateIdleAnimation(needsRedraw);
+ if (_engine.isCombatDebugEnabled() && _combatEffects.hasDamagePopups())
+ needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+ if (_engine.shouldQuit())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (_flow.hasPendingMainMenuReturn())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (_pendingTransition.roomName.empty())
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ if (_pendingTransition.roomName.equals(kExitCloseupPendingRoomChange)) {
+ _flow.requestCloseupParentRestart();
+ return RoomInputResult::leaveRoom(Common::kNoError);
+ }
+
+ return RoomInputResult();
+}
+
+void RoomSession::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) {
+ _sceneRuntime.syncGlobalTimerEntities(previousTimerRecords);
+}
+
+bool RoomSession::captureDialogueBackdrop(IndexedBitmap &backdrop) {
+ return _presentation.captureDialogueBackdrop(backdrop);
+}
+
+Common::Error RoomSession::showCdChangePrompt(int discNumber) {
+ return _flow.showCdChangePrompt(discNumber);
+}
+
+Common::Error RoomSession::applyLightingCommand(StartupLightingCommand command) {
+ return _presentation.applyLightingCommand(command);
+}
+
+Common::Error RoomSession::runModalShowText(const ResolvedText &text) {
+ return _presentation.runModalShowText(text, _flow);
+}
+
+Common::Error RoomSession::tryActivatePendingRegion(bool &needsRedraw) {
+ return isPlayerCombatLocked() ? Common::kNoError : _interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
+}
+
+Common::Error RoomSession::tryActivatePassiveRegion(bool &needsRedraw) {
+ return _interactionProcessor.tryActivatePassiveRegion(_inventory, needsRedraw);
+}
+
+Common::Error RoomSession::resolvePlayerAttackContact(bool &needsRedraw) {
+ return _combat.resolvePlayerAttackContact(_engine, _scene.state, _playerState, _entityManager,
+ _sceneRuntime, _combatEffects, _interactionProcessor, needsRedraw);
+}
+
+Common::Error RoomSession::updateRoomMonsterCombat(bool &needsRedraw) {
+ return _combat.updateRoomMonsterCombat(_engine, _scene.state, _playerState, _idleState,
+ _entityManager, _sceneRuntime, _combatEffects, _cheats, *this, _interactionProcessor, needsRedraw);
+}
+
+void RoomSession::syncCombatHitEffects(bool &needsRedraw) {
+ if (_combatEffects.syncCombatHitEffects())
+ needsRedraw = true;
+}
+
+void RoomSession::updateIdleAnimation(bool &needsRedraw) {
+ if (Player::updateIdleAnimation(_scene.state, _playerState, _idleState))
+ needsRedraw = true;
+}
+
+void RoomSession::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc) {
+ _combat.capturePlayerAttackTarget(_scene.state, _playerState, _entityManager, mousePos, npc);
+}
+
+Common::Error RoomSession::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
+ return _combat.killDebugActiveMonster(_engine, _scene.state, _playerState, _entityManager,
+ mousePos, _combatEffects, _interactionProcessor, needsRedraw);
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/room_session.h
index 3642623f263..55f3e7e52d4 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/room_session.h
@@ -24,6 +24,8 @@
#include "harvester/room_combat.h"
#include "harvester/room_input.h"
+#include "harvester/room_interaction.h"
+#include "harvester/room_presentation.h"
#include "harvester/room_scene_runtime.h"
#include "harvester/room_support.h"
#include "harvester/runtime_entity.h"
@@ -34,32 +36,76 @@ class EntityManager;
class Flow;
class InventorySystem;
+struct RoomPendingTransition {
+ Common::String roomName;
+ bool isRoomName = false;
+ bool usesSavedRoomState = false;
+ SaveRoomState savedRoomState;
+};
+
// Scoped to one runRoomLoop iteration, including time spent in nested closeups.
// Entity pointers remain owned by EntityManager.
-class RoomSession {
+class RoomSession : public RoomInteractionCallbacks {
public:
RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
- InventorySystem &inventory);
+ InventorySystem &inventory, CheatSystem &cheats, const RoomViewResources &view, bool canExitCloseupToParent);
RoomPlayerState &getPlayerState() { return _playerState; }
- RoomCombat &getCombat() { return _combat; }
- RoomSceneRuntime &getSceneRuntime() { return _sceneRuntime; }
- RoomCombatEffects &getCombatEffects() { return _combatEffects; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
RoomInputState &getInputState() { return _inputState; }
- Common::String &getPendingRegionName() { return _pendingRegionName; }
- void resetIdleState();
- void notePlayerActivity();
+ void initializePlayerState();
+ RoomInputResult runRoomEntryInteraction(const InteractionResult &interaction);
+ RoomInputResult runRoomFrames(const Common::String ¤tRoomTarget);
+ const RoomPendingTransition &getPendingTransition() const { return _pendingTransition; }
+ void clearPendingRegionInteraction() { _pendingRegionName.clear(); }
+ void capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc);
+ Common::Error killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw);
+
+ void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) override;
+ bool captureDialogueBackdrop(IndexedBitmap &backdrop) override;
+ Common::Error showCdChangePrompt(int discNumber) override;
+ Common::Error runRoomExitCommands() override;
+ Common::Error applyLightingCommand(StartupLightingCommand command) override;
+ void applyPlayerGotoXZ(int x, int z) override;
+ Common::Error runModalShowText(const ResolvedText &text) override;
+ void startPlayerDefeatSequence(const char *reason, const Common::String &sourceName, int damageType) override;
+ void resetIdleState() override;
+ void notePlayerActivity() override;
bool isPlayerCombatLocked() const;
bool cancelPlayerAttackAnimation();
- void stopPlayerRegionInteraction();
+ void stopPlayerRegionInteraction() override;
void syncPlayerPlacement();
void captureCurrentSaveState();
- bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState);
- bool refreshCurrentScene(bool preservePlayerPlacement);
+ bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) override;
+ bool refreshCurrentScene(bool preservePlayerPlacement) override;
private:
+ struct PlayerControlState {
+ bool alive;
+ bool paused;
+ bool canAct;
+ };
+
+ void syncAnimatedRoomActorPlacement();
+ void requestPlayerGameOver(const char *reason, const Common::String &sourceName);
+ void updateRoomInterface(bool &needsRedraw);
+ bool drawRoomFrame(bool &needsRedraw);
+ RoomInputResult pollRoomEvents(RoomInput &input, bool &needsRedraw);
+ PlayerControlState preparePlayerControlState();
+ Common::Error tryActivatePendingRegion(bool &needsRedraw);
+ Common::Error tryActivatePassiveRegion(bool &needsRedraw);
+ void advanceRuntimeEntities(bool &needsRedraw);
+ Common::Error resolvePlayerAttackContact(bool &needsRedraw);
+ void updatePlayerCombatAnimations(bool &needsRedraw);
+ void updatePlayerControls(const PlayerControlState &control, bool &needsRedraw);
+ Common::Error updateRoomMonsterCombat(bool &needsRedraw);
+ void syncCombatHitEffects(bool &needsRedraw);
+ void updatePlayerDeath(bool &needsRedraw);
+ void pruneCombatEffects(bool &needsRedraw);
+ RoomInputResult dispatchExpiredTimers(bool &needsRedraw);
+ void updateIdleAnimation(bool &needsRedraw);
+
HarvesterEngine &_engine;
Flow &_flow;
RoomSceneResources &_scene;
@@ -72,6 +118,14 @@ private:
RoomIdleAnimationState _idleState;
RoomInputState _inputState;
Common::String _pendingRegionName;
+ CheatSystem &_cheats;
+ const RoomViewResources _view;
+ bool _canExitCloseupToParent;
+ RoomPresentationState _presentationState;
+ RoomPendingTransition _pendingTransition;
+ RoomItemInteractions _roomItems;
+ RoomPresentation _presentation;
+ RoomInteractionProcessor _interactionProcessor;
};
} // End of namespace Harvester
Commit: ccfabd608beec7dccf1bdb9ab1e94168c1b98fac
https://github.com/scummvm/scummvm/commit/ccfabd608beec7dccf1bdb9ab1e94168c1b98fac
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: begin splitting room/scene handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/scene.cpp
A engines/harvester/scene.h
A engines/harvester/scene_entities.cpp
A engines/harvester/scene_entities.h
R engines/harvester/room.cpp
R engines/harvester/room.h
R engines/harvester/room_scene_runtime.cpp
R engines/harvester/room_scene_runtime.h
R engines/harvester/room_session.cpp
R engines/harvester/room_session.h
engines/harvester/flow.cpp
engines/harvester/flow.h
engines/harvester/module.mk
engines/harvester/room_combat.cpp
engines/harvester/room_combat.h
engines/harvester/room_input.cpp
engines/harvester/room_input.h
engines/harvester/room_interaction.cpp
engines/harvester/room_interaction.h
engines/harvester/room_presentation.cpp
engines/harvester/room_presentation.h
engines/harvester/room_support.h
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 2c7ae9ef5b1..c3fbb62dbda 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -43,7 +43,7 @@
#include "harvester/palette_utils.h"
#include "harvester/resources.h"
#include "harvester/runtime_entity.h"
-#include "harvester/room_scene_runtime.h"
+#include "harvester/scene_entities.h"
#include "harvester/art.h"
#include "harvester/monster.h"
#include "harvester/player.h"
@@ -205,7 +205,7 @@ static void drawRoomDebugLabel(Graphics::Screen &screen, const Graphics::Font &f
}
static void drawRoomDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
- const RoomSceneResources &scene) {
+ const SceneState &scene) {
if (!engine.isRoomDebugEnabled())
return;
@@ -254,7 +254,7 @@ static void drawRoomDebugOverlay(HarvesterEngine &engine, Graphics::Screen &scre
}
static void drawCombatDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
- const RoomSceneResources &scene) {
+ const SceneState &scene) {
if (!engine.isCombatDebugEnabled())
return;
@@ -348,7 +348,7 @@ static void drawPathfindingBlockerOutline(Graphics::Screen &screen, EntityManage
}
static void drawPathfindingDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
- const RoomSceneResources &scene) {
+ const SceneState &scene) {
if (!engine.isPathfindingDebugEnabled())
return;
@@ -384,7 +384,7 @@ static Common::String resolveTimerDebugLabel(const TimerRecord &timer, const Ent
}
static void drawTimerDebugOverlay(HarvesterEngine &engine, Graphics::Screen &screen,
- const RoomSceneResources &) {
+ const SceneState &) {
if (!engine.isTimerDebugEnabled())
return;
@@ -574,7 +574,7 @@ static uint32 hashPalette(const byte *palette) {
return hash;
}
-void logScenePaletteSummary(const char *label, const RoomSceneResources &scene, float brightness) {
+void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness) {
const byte *palette = scene.palette;
byte minValue = 0;
byte maxValue = 0;
@@ -768,8 +768,8 @@ static void queueSceneObject(const char *source, const ObjectRecord &object,
logSceneObjectSelection("queued", source, object);
}
-bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, RoomSceneResources &scene) {
- scene = RoomSceneResources();
+bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene) {
+ scene = SceneState();
scene.state = state;
scene.targetPaletteBrightness = state.paletteBrightness;
if (!loadPaletteResource(resources, state.palettePath, scene.palette)) {
@@ -804,7 +804,7 @@ bool shouldRunStartupRoomProbe() {
ConfMan.getBool("harvester_debug_probe_startup_room");
}
-void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const RoomSceneResources &scene,
+void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const SceneState &scene,
float brightness) {
setScaledPalette(screen, scene.palette, brightness);
screen.fillRect(screen.getBounds(), 0);
@@ -1201,7 +1201,7 @@ static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Arra
return false;
}
-void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scene,
+void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
const Common::String &entranceName, Common::Point &mousePos,
const MenuTextConfig &menuTextConfig) {
EntityManager *entityManager = engine.getRuntimeEntities();
@@ -1329,7 +1329,7 @@ static Common::String trimAsciiLine(const Common::String &value) {
return value.substr(start, end - start);
}
-static bool loadQuickTipsScene(HarvesterEngine &engine, RoomSceneResources &scene) {
+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()))
@@ -1338,7 +1338,7 @@ static bool loadQuickTipsScene(HarvesterEngine &engine, RoomSceneResources &scen
return loadRoomSceneResources(state, *engine.getResources(), scene);
}
-static void renderQuickTipsScreen(HarvesterEngine &engine, const RoomSceneResources &scene,
+static void renderQuickTipsScreen(HarvesterEngine &engine, const SceneState &scene,
const MenuTextConfig &config, const QuickTipsLayout &layout,
const Common::String &tipText) {
Graphics::Screen *screen = engine.getScreen();
@@ -1510,7 +1510,7 @@ Common::Error Flow::runQuickTips() {
if (transitionError.getCode() != Common::kNoError)
return transitionError;
- RoomSceneResources scene;
+ SceneState scene;
if (!loadQuickTipsScene(_engine, scene))
return Common::kReadingFailed;
@@ -2142,7 +2142,7 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
if (!entityManager)
return false;
- RoomSceneRuntime sceneRuntime(state, _engine.getScript(), _engine.getResources(), entityManager);
+ SceneEntities sceneRuntime(state, _engine.getScript(), _engine.getResources(), entityManager);
Entity *preservedPlayer = entityManager->detachSceneEntityByName(kPlayerActorEntityName);
entityManager->clearSceneEntities(true);
sceneRuntime.populateRoomRegions();
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index c7328121d5e..78a204857a0 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -30,7 +30,7 @@
#include "harvester/dialogue.h"
#include "harvester/inventory.h"
#include "harvester/menu.h"
-#include "harvester/room.h"
+#include "harvester/scene.h"
#include "harvester/script.h"
namespace Common {
@@ -62,9 +62,9 @@ private:
friend class InventorySystem;
friend class MenuSystem;
friend class RoomInteractionProcessor;
- friend class RoomSystem;
+ friend class SceneRunner;
friend class RoomInput;
- friend class RoomSession;
+ friend class Scene;
bool loadQuickTips();
bool loadMenuItems();
@@ -128,7 +128,7 @@ private:
InventorySystem _inventory;
MenuSystem _menu;
CheatSystem _cheats;
- RoomSystem _room;
+ SceneRunner _room;
InteractionResult _queuedDialogueInteraction;
bool _hasQueuedDialogueInteraction = false;
InteractionResult _queuedDebugInteraction;
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 3487d10e819..1955e9fe591 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -21,9 +21,8 @@ MODULE_OBJS = \
room_interaction.o \
room_input.o \
room_presentation.o \
- room_scene_runtime.o \
- room_session.o \
- room.o \
+ scene_entities.o \
+ scene.o \
runtime_entity.o \
saveload.o \
script.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
deleted file mode 100644
index e67ef6539b2..00000000000
--- a/engines/harvester/room.cpp
+++ /dev/null
@@ -1,273 +0,0 @@
-/* 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/room.h"
-
-#include "common/algorithm.h"
-#include "common/events.h"
-#include "common/ptr.h"
-#include "common/system.h"
-#include "graphics/blit.h"
-#include "graphics/font.h"
-#include "graphics/fontman.h"
-#include "graphics/framelimiter.h"
-#include "harvester/cft_font.h"
-#include "harvester/cheats.h"
-#include "harvester/detection.h"
-#include "harvester/fst_player.h"
-#include "harvester/harvester.h"
-#include "harvester/resources.h"
-#include "harvester/runtime_entity.h"
-#include "harvester/art.h"
-#include "harvester/flow.h"
-#include "harvester/inventory.h"
-#include "harvester/monster.h"
-#include "harvester/player.h"
-#include "harvester/room_combat.h"
-#include "harvester/room_interaction.h"
-#include "harvester/room_input.h"
-#include "harvester/room_presentation.h"
-#include "harvester/room_session.h"
-#include "harvester/room_support.h"
-
-namespace Harvester {
-
-static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
- const Text *text = engine.getText();
- if (!text || !fontName)
- return nullptr;
-
- for (const CftFontResource &font : text->getFonts()) {
- if (font.name.equalsIgnoreCase(fontName))
- return &font;
- }
-
- return nullptr;
-}
-
-RoomSystem::RoomSystem(HarvesterEngine &engine,
- InventorySystem &inventory, CheatSystem &cheats)
- : _engine(engine), _inventory(inventory), _cheats(cheats) {
-}
-
-Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetName,
- bool targetIsRoomName) {
- if (flow.hasPendingMainMenuReturn())
- return Common::kNoError;
-
- Common::String currentRoomTarget = targetName;
- bool currentTargetIsRoomName = targetIsRoomName;
- bool currentTargetUsesSavedRoomState = false;
- SaveRoomState currentTargetSavedRoomState;
- while (!currentRoomTarget.empty()) {
- RoomSetupState state;
- bool shouldRunRoomEntryCommands = false;
- if (_engine.hasPendingLoadedSaveRoomState()) {
- // Native load destroys live render entities and frees preserved global timers
- // before room_setup rebuilds the loaded room from restored TIMER records.
- if (EntityManager *entityManager = _engine.getRuntimeEntities())
- entityManager->clearSceneEntities(false);
- flow.resetRoomNpcDialogueState();
- if (_engine.hasPendingLoadedDialogueStateBlob()) {
- if (!flow.loadDialogueSaveStateBlob(
- _engine.getPendingLoadedDialogueStateBlob(),
- _engine.getPendingLoadedDialogueStateBlobVersion()))
- return Common::kReadingFailed;
- _engine.clearPendingLoadedDialogueStateBlob();
- }
- if (_engine.getPendingLoadedDisc() > 0 &&
- !_engine.activateDisc(_engine.getPendingLoadedDisc())) {
- return Common::kReadingFailed;
- }
- const SaveRoomState &loadedState = _engine.getPendingLoadedSaveRoomState();
- debugC(1, kDebugRoom,
- "Harvester: applying pending loaded room state entrance='%s' room='%s' spawn=(%d,%d,%d) facing=%d music='%s'",
- loadedState.entranceName.c_str(), loadedState.roomName.c_str(),
- loadedState.playerX, loadedState.playerY, loadedState.playerZ,
- loadedState.playerFacing, loadedState.musicPath.c_str());
- if (!_engine.getScript()->materializeRoomState(
- loadedState.entranceName, loadedState.roomName, state, *_engine.getResources())) {
- return Common::kReadingFailed;
- }
- state.entranceName = loadedState.entranceName;
- state.playerSpawnX = loadedState.playerX;
- state.playerSpawnY = loadedState.playerY;
- state.playerSpawnZ = loadedState.playerZ;
- state.playerFacing = loadedState.playerFacing;
- state.musicPath = loadedState.musicPath;
- currentRoomTarget = !loadedState.entranceName.empty() ? loadedState.entranceName : loadedState.roomName;
- _engine.clearPendingLoadedSaveRoomState();
- currentTargetIsRoomName = false;
- shouldRunRoomEntryCommands = false;
- } else if ((currentTargetIsRoomName
- ? !_engine.getScript()->resolveRoomSetupStateByRoomName(
- currentRoomTarget, state, *_engine.getResources())
- : !_engine.getScript()->resolveRoomSetupState(
- currentRoomTarget, state, *_engine.getResources()))) {
- return Common::kReadingFailed;
- } else {
- shouldRunRoomEntryCommands = true;
- }
- if (shouldRunRoomEntryCommands && currentTargetUsesSavedRoomState &&
- currentTargetSavedRoomState.valid) {
- const SaveRoomState &savedState = currentTargetSavedRoomState;
- state.entranceName = savedState.entranceName;
- state.playerSpawnX = savedState.playerX;
- state.playerSpawnY = savedState.playerY;
- state.playerSpawnZ = savedState.playerZ;
- state.playerFacing = savedState.playerFacing;
- if (!savedState.musicPath.empty())
- state.musicPath = savedState.musicPath;
- debugC(1, kDebugRoom,
- "Harvester: applying saved room restart state entrance='%s' room='%s' spawn=(%d,%d,%d) facing=%d music='%s'",
- savedState.entranceName.c_str(), savedState.roomName.c_str(),
- savedState.playerX, savedState.playerY, savedState.playerZ,
- savedState.playerFacing, savedState.musicPath.c_str());
- }
-
- Common::Error transitionError = flow.beginRoomSetupTransition();
- if (transitionError.getCode() != Common::kNoError)
- return transitionError;
-
- RoomSceneResources scene;
- if (!loadRoomSceneResources(state, *_engine.getResources(), scene))
- return Common::kReadingFailed;
-
- Graphics::Screen *screen = _engine.getScreen();
- const Art *art = _engine.getArt();
- const Graphics::Font *bodyFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
- const CftFontResource *promptFontResource = findStartupFontByName(_engine, "MEDFONT1");
- const CftFontResource *inventoryTooltipFontResource = findStartupFontByName(_engine, "TEXTFONT");
- Common::ScopedPtr<HarvesterCftFont> promptCftFont;
- Common::ScopedPtr<HarvesterCftFont> inventoryTooltipCftFont;
- const Graphics::Font *promptFont = bodyFont;
- const Graphics::Font *inspectFont = bodyFont;
- const Graphics::Font *inventoryTooltipFont = bodyFont;
- bool useNativePromptFont = false;
- bool useNativeInspectFont = false;
- if (promptFontResource) {
- promptCftFont.reset(new HarvesterCftFont(*promptFontResource));
- if (promptCftFont->isValid()) {
- promptFont = promptCftFont.get();
- useNativePromptFont = true;
- }
- }
- if (inventoryTooltipFontResource) {
- inventoryTooltipCftFont.reset(new HarvesterCftFont(*inventoryTooltipFontResource));
- if (inventoryTooltipCftFont->isValid()) {
- inventoryTooltipFont = inventoryTooltipCftFont.get();
- inspectFont = inventoryTooltipCftFont.get();
- useNativeInspectFont = true;
- }
- }
- if (!screen || !art || !bodyFont || !promptFont || !inventoryTooltipFont || !inspectFont)
- return Common::kNoError;
- EntityManager *entityManager = _engine.getRuntimeEntities();
-
- if (!flow.populateRoomSceneEntities(scene.state, scene.sceneObjects, scene.sceneAnimations))
- return Common::kReadingFailed;
- if (entityManager) {
- for (const MonsterRecord &monster : scene.state.roomMonsters) {
- Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
- if (entity)
- Monster::applyAnimation(*entity, monster);
- }
- entityManager->pauseTimerCountdowns();
- }
-
- flow.resetCursorAnimationSequence();
- flow.executeStartupAudioCommands(scene.state.audioCommands);
- if (!scene.state.musicPath.empty())
- (void)_engine.playMusic(scene.state.musicPath);
- const RoomViewResources view = { art, promptFont, inspectFont, inventoryTooltipFont,
- useNativePromptFont, useNativeInspectFont };
- RoomSession session(_engine, flow, scene, entityManager, _inventory, _cheats, view,
- flow._roomLoopDepth > 1);
- debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
- session.initializePlayerState();
-
- session.resetIdleState();
-
- InteractionResult roomEntryInteraction;
- bool hasPendingRoomEntryInteraction = false;
- if (shouldRunRoomEntryCommands) {
- if (!_engine.getScript()->executeRoomEnterCommands(
- scene.state.roomName, roomEntryInteraction)) {
- return Common::kReadingFailed;
- }
-
- // Native room_setup applies entry-time state changes before the first visible room frame.
- if (roomEntryInteraction.mutatedRuntimeState) {
- session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
- if (!session.applyCurrentRoomRuntimeMutationsInPlace(false) &&
- !session.refreshCurrentScene(true)) {
- return Common::kReadingFailed;
- }
- roomEntryInteraction.mutatedRuntimeState = false;
- roomEntryInteraction.visualRuntimeStateChanged = false;
- }
- hasPendingRoomEntryInteraction = hasRoomEntryInteraction(roomEntryInteraction);
- }
-
- transitionError = flow.waitForRoomSetupTransitionHold();
- if (transitionError.getCode() != Common::kNoError)
- return transitionError;
-
- logScenePaletteSummary("room setup stub palette", scene, 0.0f);
- drawRoomScene(_engine, *screen, scene, 0.0f);
- screen->makeAllDirty();
- screen->update();
-
- logScenePaletteSummary("room setup fade target", scene, scene.targetPaletteBrightness);
- transitionError = flow.fadeInRoomScene(scene.palette, scene.targetPaletteBrightness);
- if (transitionError.getCode() != Common::kNoError)
- return transitionError;
- if (entityManager)
- entityManager->resumeTimerCountdowns();
-
- if (shouldRunRoomEntryCommands) {
- if (hasPendingRoomEntryInteraction) {
- const RoomInputResult entryResult = session.runRoomEntryInteraction(roomEntryInteraction);
- if (entryResult.returnFromRoom)
- return entryResult.error;
- }
-
- flow.resetCursorAnimationSequence();
- session.resetIdleState();
- }
- ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
- _engine.getScript());
-
- const RoomInputResult frameResult = session.runRoomFrames(currentRoomTarget);
- if (frameResult.returnFromRoom)
- return frameResult.error;
- const RoomPendingTransition &pendingTransition = session.getPendingTransition();
-
- currentRoomTarget = pendingTransition.roomName;
- currentTargetIsRoomName = pendingTransition.isRoomName;
- currentTargetUsesSavedRoomState = pendingTransition.usesSavedRoomState;
- currentTargetSavedRoomState = pendingTransition.savedRoomState;
- }
-
- return Common::kNoError;
-}
-
-} // End of namespace Harvester
diff --git a/engines/harvester/room.h b/engines/harvester/room.h
deleted file mode 100644
index 2c0f15e864d..00000000000
--- a/engines/harvester/room.h
+++ /dev/null
@@ -1,50 +0,0 @@
-/* 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_ROOM_H
-#define HARVESTER_ROOM_H
-
-#include "common/error.h"
-#include "common/str.h"
-
-namespace Harvester {
-
-class HarvesterEngine;
-class CheatSystem;
-class Flow;
-class InventorySystem;
-
-class RoomSystem {
-public:
- RoomSystem(HarvesterEngine &engine, InventorySystem &inventory,
- CheatSystem &cheats);
-
- Common::Error runRoomLoop(Flow &flow, const Common::String &targetName, bool targetIsRoomName);
-
-private:
- HarvesterEngine &_engine;
- InventorySystem &_inventory;
- CheatSystem &_cheats;
-};
-
-} // End of namespace Harvester
-
-#endif // HARVESTER_ROOM_H
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/room_combat.cpp
index dfd27b7a156..53eb6a5ffb1 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/room_combat.cpp
@@ -30,7 +30,7 @@
#include "harvester/harvester.h"
#include "harvester/player.h"
#include "harvester/room_interaction.h"
-#include "harvester/room_scene_runtime.h"
+#include "harvester/scene_entities.h"
#include "harvester/runtime_entity.h"
namespace Harvester {
@@ -970,7 +970,7 @@ MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &stat
}
RoomNpcCombatUpdateResult RoomCombat::updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ EntityManager *entityManager, SceneEntities &sceneRuntime,
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
RoomNpcCombatUpdateResult result;
for (uint i = 0; i < state.roomNpcs.size(); ++i) {
@@ -1066,7 +1066,7 @@ bool RoomCombat::beginNpcDeathTransition(HarvesterEngine &engine,
}
Common::Error RoomCombat::finalizeNpcDeathTransition(HarvesterEngine &engine,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
+ EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const {
Script *script = engine.getScript();
Entity *npcEntity = findSceneRuntimeEntity(entityManager, npc.npcName);
@@ -1257,7 +1257,7 @@ Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSe
Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
- RoomSceneRuntime &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
+ SceneEntities &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
RoomInteractionCallbacks &callbacks, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
if (!entityManager)
return Common::kNoError;
@@ -1580,7 +1580,7 @@ Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomS
Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ EntityManager *entityManager, SceneEntities &sceneRuntime,
RoomCombatEffects &combatEffects,
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
diff --git a/engines/harvester/room_combat.h b/engines/harvester/room_combat.h
index 95a3fac1b3a..86bf6a61d29 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/room_combat.h
@@ -34,7 +34,7 @@ class HarvesterEngine;
class RoomCombatEffects;
class RoomInteractionCallbacks;
class RoomInteractionProcessor;
-class RoomSceneRuntime;
+class SceneEntities;
extern const int kRoomMonsterAnimationRate;
extern const int kNativeMonsterAttackAnimationRate;
@@ -155,11 +155,11 @@ public:
Common::Error updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
- RoomSceneRuntime &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
+ SceneEntities &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
RoomInteractionCallbacks &callbacks, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ EntityManager *entityManager, SceneEntities &sceneRuntime,
RoomCombatEffects &combatEffects,
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
@@ -168,13 +168,13 @@ private:
bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
// Called after resizeCombatStates; NPC finalization ends the entire combat update.
RoomNpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime,
+ EntityManager *entityManager, SceneEntities &sceneRuntime,
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
bool beginNpcDeathTransition(HarvesterEngine &engine,
NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
- EntityManager *entityManager, RoomSceneRuntime &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
+ EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const;
void startMonsterHitReaction(HarvesterEngine &engine,
diff --git a/engines/harvester/room_input.cpp b/engines/harvester/room_input.cpp
index 55346caf9d5..976719fb2b2 100644
--- a/engines/harvester/room_input.cpp
+++ b/engines/harvester/room_input.cpp
@@ -32,7 +32,7 @@
#include "harvester/player.h"
#include "harvester/room_interaction.h"
#include "harvester/room_presentation.h"
-#include "harvester/room_session.h"
+#include "harvester/scene.h"
#include "harvester/room_support.h"
namespace Harvester {
@@ -75,7 +75,7 @@ static void debugLogInventoryClick(const char *buttonLabel, const Common::Point
InventorySystem::isStatusObject(inventoryHover->object));
}
-RoomInput::RoomInput(HarvesterEngine &engine, Flow &flow, RoomSession &session, RoomSceneResources &scene,
+RoomInput::RoomInput(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
InventorySystem &inventory, CheatSystem &cheats,
RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
RoomPresentationState &presentationState,
diff --git a/engines/harvester/room_input.h b/engines/harvester/room_input.h
index 5986475b58c..9da9b566ef8 100644
--- a/engines/harvester/room_input.h
+++ b/engines/harvester/room_input.h
@@ -34,11 +34,11 @@ class HarvesterEngine;
class InventorySystem;
class RoomInteractionProcessor;
class RoomItemInteractions;
-class RoomSession;
+class Scene;
struct ObjectRecord;
struct ResolvedText;
struct RoomPresentationState;
-struct RoomSceneResources;
+struct SceneState;
struct RoomInputState {
bool moveLeft = false;
@@ -65,7 +65,7 @@ struct RoomInputResult {
class RoomInput {
public:
- RoomInput(HarvesterEngine &engine, Flow &flow, RoomSession &session, RoomSceneResources &scene,
+ RoomInput(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
InventorySystem &inventory, CheatSystem &cheats,
RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
RoomPresentationState &presentationState,
@@ -87,8 +87,8 @@ private:
HarvesterEngine &_engine;
Flow &_flow;
- RoomSession &_session;
- RoomSceneResources &_scene;
+ Scene &_session;
+ SceneState &_scene;
InventorySystem &_inventory;
CheatSystem &_cheats;
RoomItemInteractions &_roomItems;
diff --git a/engines/harvester/room_interaction.cpp b/engines/harvester/room_interaction.cpp
index 4110a8ae5aa..1ece1ed9e87 100644
--- a/engines/harvester/room_interaction.cpp
+++ b/engines/harvester/room_interaction.cpp
@@ -31,7 +31,7 @@
#include "harvester/harvester.h"
#include "harvester/inventory.h"
#include "harvester/player.h"
-#include "harvester/room_scene_runtime.h"
+#include "harvester/scene_entities.h"
#include "harvester/resources.h"
#include "harvester/runtime_entity.h"
@@ -74,8 +74,8 @@ static bool loadBitmapResource(ResourceManager &resources, const Common::String
return decodeBitmap(data, bitmap);
}
-RoomItemInteractions::RoomItemInteractions(HarvesterEngine &engine, RoomSceneResources &scene,
- RoomSceneRuntime &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange)
+RoomItemInteractions::RoomItemInteractions(HarvesterEngine &engine, SceneState &scene,
+ SceneEntities &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange)
: _engine(engine), _scene(scene), _sceneRuntime(sceneRuntime), _inventory(inventory),
_pendingRoomChange(pendingRoomChange) {
}
@@ -345,7 +345,7 @@ Common::Error RoomItemInteractions::handleRoomItemPickup(const ObjectRecord &obj
}
RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow,
- RoomSceneResources &scene, RoomPlayerState &playerState,
+ SceneState &scene, RoomPlayerState &playerState,
Common::String &pendingRegionName, Common::String &pendingRoomChange,
bool &pendingRoomChangeIsRoomName, bool &pendingRoomChangeUsesSavedRoomState,
SaveRoomState &pendingRoomChangeSavedRoomState, bool canExitCloseupToParent,
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/room_interaction.h
index 7ae7db80601..052bd309dfe 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/room_interaction.h
@@ -33,7 +33,7 @@ class Flow;
class HarvesterEngine;
class InventorySystem;
class RoomInteractionProcessor;
-class RoomSceneRuntime;
+class SceneEntities;
struct IndexedBitmap;
extern const char *const kExitCloseupPendingRoomChange;
@@ -60,8 +60,8 @@ public:
class RoomItemInteractions {
public:
- RoomItemInteractions(HarvesterEngine &engine, RoomSceneResources &scene,
- RoomSceneRuntime &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange);
+ RoomItemInteractions(HarvesterEngine &engine, SceneState &scene,
+ SceneEntities &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange);
const Common::String &getCarriedRoomItemLabel() const { return _carriedRoomItemLabel; }
const IndexedBitmap &getCarriedRoomItemBitmap() const { return _carriedRoomItemBitmap; }
@@ -85,8 +85,8 @@ private:
RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
HarvesterEngine &_engine;
- RoomSceneResources &_scene;
- RoomSceneRuntime &_sceneRuntime;
+ SceneState &_scene;
+ SceneEntities &_sceneRuntime;
InventorySystem &_inventory;
const Common::String &_pendingRoomChange;
Common::String _carriedRoomItemName;
@@ -96,7 +96,7 @@ private:
class RoomInteractionProcessor {
public:
- RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene,
+ RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow, SceneState &scene,
RoomPlayerState &playerState, Common::String &pendingRegionName,
Common::String &pendingRoomChange, bool &pendingRoomChangeIsRoomName,
bool &pendingRoomChangeUsesSavedRoomState, SaveRoomState &pendingRoomChangeSavedRoomState,
@@ -119,7 +119,7 @@ public:
private:
HarvesterEngine &_engine;
Flow &_flow;
- RoomSceneResources &_scene;
+ SceneState &_scene;
RoomPlayerState &_playerState;
Common::String &_pendingRegionName;
Common::String &_pendingRoomChange;
diff --git a/engines/harvester/room_presentation.cpp b/engines/harvester/room_presentation.cpp
index ea63ac33e0d..7cfc3505581 100644
--- a/engines/harvester/room_presentation.cpp
+++ b/engines/harvester/room_presentation.cpp
@@ -93,7 +93,7 @@ void RoomPresentation::blitBitmap(Graphics::Screen &screen, const IndexedBitmap
screen.format.bytesPerPixel, kTransparentPaletteIndex);
}
-RoomPresentation::RoomPresentation(HarvesterEngine &engine, RoomSceneResources &scene,
+RoomPresentation::RoomPresentation(HarvesterEngine &engine, SceneState &scene,
const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont)
: _engine(engine), _scene(scene), _art(art), _inspectFont(inspectFont),
_useNativeInspectFont(useNativeInspectFont) {
diff --git a/engines/harvester/room_presentation.h b/engines/harvester/room_presentation.h
index 2339d791b79..93ecb8985f8 100644
--- a/engines/harvester/room_presentation.h
+++ b/engines/harvester/room_presentation.h
@@ -37,7 +37,7 @@ class Flow;
class HarvesterEngine;
struct IndexedBitmap;
struct ResolvedText;
-struct RoomSceneResources;
+struct SceneState;
struct RoomViewResources {
const Art *art;
@@ -60,7 +60,7 @@ struct RoomPresentationState {
class RoomPresentation {
public:
- RoomPresentation(HarvesterEngine &engine, RoomSceneResources &scene,
+ RoomPresentation(HarvesterEngine &engine, SceneState &scene,
const Art *art, const Graphics::Font *inspectFont, bool useNativeInspectFont);
bool captureDialogueBackdrop(IndexedBitmap &dialogueBackdrop);
@@ -70,7 +70,7 @@ public:
private:
HarvesterEngine &_engine;
- RoomSceneResources &_scene;
+ SceneState &_scene;
const Art *_art;
const Graphics::Font *_inspectFont;
bool _useNativeInspectFont;
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 463efbda54e..1908dc848e5 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -43,7 +43,7 @@ class DialogueSystem;
struct IndexedBitmap;
struct MenuTextConfig;
-struct RoomSceneResources {
+struct SceneState {
RoomSetupState state;
byte palette[256 * 3] = { 0 };
Common::Array<ObjectRecord> sceneObjects;
@@ -130,13 +130,13 @@ struct RoomIdleAnimationState {
bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs);
bool applyRoomActorPlacement(const RoomSetupState &state, Entity &entity,
int centerX, int bottomY, float z, const Common::String *entranceName = nullptr);
-void logScenePaletteSummary(const char *label, const RoomSceneResources &scene, float brightness);
+void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness);
bool captureScreenBackdrop(const Graphics::Screen &screen, IndexedBitmap &bitmap);
Common::Rect getRoomObjectHotspotBounds(const ObjectRecord &object);
bool shouldDisplaySceneAnimation(const AnimRecord &anim);
-bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, RoomSceneResources &scene);
+bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resources, SceneState &scene);
bool shouldRunStartupRoomProbe();
-void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const RoomSceneResources &scene,
+void drawRoomScene(HarvesterEngine &engine, Graphics::Screen &screen, const SceneState &scene,
float brightness);
const ObjectRecord *findSceneObjectByName(const Common::Array<ObjectRecord> &objects,
const Common::String &objectName);
@@ -157,7 +157,7 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
const Common::Array<RegionRecord> ®ions,
const Common::Point &mousePos, const MenuTextConfig &menuTextConfig,
const DialogueSystem *dialogue = nullptr);
-void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scene,
+void logStartupRoomProbe(HarvesterEngine &engine, const SceneState &scene,
const Common::String &entranceName, Common::Point &mousePos,
const MenuTextConfig &menuTextConfig);
diff --git a/engines/harvester/room_session.cpp b/engines/harvester/scene.cpp
similarity index 74%
rename from engines/harvester/room_session.cpp
rename to engines/harvester/scene.cpp
index 6406f03009d..d08d12df167 100644
--- a/engines/harvester/room_session.cpp
+++ b/engines/harvester/scene.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/room_session.h"
+#include "harvester/scene.h"
#include "common/algorithm.h"
#include "common/events.h"
@@ -36,6 +36,18 @@
#include "harvester/room_combat.h"
#include "harvester/runtime_entity.h"
+#include "common/ptr.h"
+#include "graphics/blit.h"
+#include "graphics/fontman.h"
+#include "harvester/cft_font.h"
+#include "harvester/cheats.h"
+#include "harvester/resources.h"
+#include "harvester/monster.h"
+#include "harvester/room_interaction.h"
+#include "harvester/room_input.h"
+#include "harvester/room_presentation.h"
+#include "harvester/room_support.h"
+
namespace Harvester {
static const char *const kPlayerInventoryLabel = "your inventory";
@@ -109,7 +121,7 @@ static void drawCombatLoadoutResourceIcons(Graphics::Screen &screen, const Art &
kNativeCombatResourceIconsY);
}
-RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
+Scene::Scene(HarvesterEngine &engine, Flow &flow, SceneState &scene, EntityManager *entityManager,
InventorySystem &inventory, CheatSystem &cheats, const RoomViewResources &view, bool canExitCloseupToParent)
: _engine(engine), _flow(flow), _scene(scene), _entityManager(entityManager), _inventory(inventory),
_sceneRuntime(scene.state, engine.getScript(), engine.getResources(), entityManager), _combatEffects(entityManager),
@@ -121,23 +133,23 @@ RoomSession::RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources
canExitCloseupToParent, *this) {
}
-void RoomSession::resetIdleState() {
+void Scene::resetIdleState() {
_idleState = RoomIdleAnimationState();
_idleState.activityTick = Player::getRuntimeClockTicks();
_idleState.resetTick = _idleState.activityTick;
Player::updateIdleTrigger(_idleState);
}
-void RoomSession::notePlayerActivity() {
+void Scene::notePlayerActivity() {
_idleState.activityTick = Player::getRuntimeClockTicks();
Player::updateIdleTrigger(_idleState);
}
-bool RoomSession::isPlayerCombatLocked() const {
+bool Scene::isPlayerCombatLocked() const {
return _playerState.attackActive || _playerState.hitActive || _playerState.deathActive;
}
-bool RoomSession::cancelPlayerAttackAnimation() {
+bool Scene::cancelPlayerAttackAnimation() {
if (!_playerState.attackActive)
return false;
@@ -159,7 +171,7 @@ bool RoomSession::cancelPlayerAttackAnimation() {
return Player::setIdleAnimation(_playerState, resumeFacing);
}
-void RoomSession::stopPlayerRegionInteraction() {
+void Scene::stopPlayerRegionInteraction() {
_inputState.moveLeft = false;
_inputState.moveRight = false;
_inputState.moveUp = false;
@@ -174,11 +186,11 @@ void RoomSession::stopPlayerRegionInteraction() {
syncPlayerPlacement();
}
-void RoomSession::syncPlayerPlacement() {
+void Scene::syncPlayerPlacement() {
Player::syncAnimationPlacement(_playerState);
}
-void RoomSession::captureCurrentSaveState() {
+void Scene::captureCurrentSaveState() {
syncPlayerPlacement();
_sceneRuntime.syncCurrentRoomRuntimeState(_scene.sceneRegions);
const int facing = _playerState.facing >= 0 ? _playerState.facing : _scene.state.playerFacing;
@@ -187,7 +199,7 @@ void RoomSession::captureCurrentSaveState() {
_scene.state.discNumber, _engine.getMusicPath());
}
-bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
+bool Scene::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
Script *script = _engine.getScript();
ResourceManager *resources = _engine.getResources();
if (!script || !resources)
@@ -206,7 +218,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
}
updatedState.audioCommands = Common::move(entryAudioCommands);
- RoomSceneResources updatedScene;
+ SceneState updatedScene;
if (!_sceneRuntime.loadSceneResources(updatedState, updatedScene))
return false;
Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates =
@@ -237,7 +249,7 @@ bool RoomSession::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteSt
return _inventory.refresh();
}
-bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
+bool Scene::refreshCurrentScene(bool preservePlayerPlacement) {
debugC(2, kDebugRoom, "Harvester: refreshing room scene room='%s' preserve_player_placement=%d",
_scene.state.roomName.c_str(), preservePlayerPlacement);
Common::Array<AudioCommand> entryAudioCommands = _scene.state.audioCommands;
@@ -265,7 +277,7 @@ bool RoomSession::refreshCurrentScene(bool preservePlayerPlacement) {
return _inventory.refresh();
}
-void RoomSession::initializePlayerState() {
+void Scene::initializePlayerState() {
_playerState.entity = _entityManager ? _entityManager->findSceneEntityByName("PLAYER") : nullptr;
_playerState.centerX = _scene.state.playerSpawnX;
_playerState.bottomY = _scene.state.playerSpawnY;
@@ -278,7 +290,7 @@ void RoomSession::initializePlayerState() {
_combat.initializeCombatStates(_scene.state);
}
-void RoomSession::syncAnimatedRoomActorPlacement() {
+void Scene::syncAnimatedRoomActorPlacement() {
Player::syncAnimationPlacement(_playerState);
if (!_entityManager)
return;
@@ -293,7 +305,7 @@ void RoomSession::syncAnimatedRoomActorPlacement() {
}
}
-Common::Error RoomSession::runRoomExitCommands() {
+Common::Error Scene::runRoomExitCommands() {
InteractionResult exitInteraction;
if (!_engine.getScript()->executeRoomExitCommands(_scene.state.roomName, exitInteraction))
return Common::kReadingFailed;
@@ -345,7 +357,7 @@ Common::Error RoomSession::runRoomExitCommands() {
return Common::kNoError;
}
-void RoomSession::applyPlayerGotoXZ(int x, int z) {
+void Scene::applyPlayerGotoXZ(int x, int z) {
_playerState.hasMoveTarget = false;
_playerState.turnActive = false;
_playerState.turnTargetFacing = -1;
@@ -375,7 +387,7 @@ void RoomSession::applyPlayerGotoXZ(int x, int z) {
captureCurrentSaveState();
}
-void RoomSession::requestPlayerGameOver(const char *reason, const Common::String &sourceName) {
+void Scene::requestPlayerGameOver(const char *reason, const Common::String &sourceName) {
if (_flow.hasPendingMainMenuReturn())
return;
@@ -388,7 +400,7 @@ void RoomSession::requestPlayerGameOver(const char *reason, const Common::String
_flow.requestGameOverReturn();
}
-void RoomSession::startPlayerDefeatSequence(const char *reason, const Common::String &sourceName, int damageType) {
+void Scene::startPlayerDefeatSequence(const char *reason, const Common::String &sourceName, int damageType) {
if (_flow.hasPendingMainMenuReturn() || _playerState.deathActive)
return;
@@ -422,7 +434,7 @@ void RoomSession::startPlayerDefeatSequence(const char *reason, const Common::St
: 0);
}
-RoomInputResult RoomSession::runRoomEntryInteraction(const InteractionResult &roomEntryInteraction) {
+RoomInputResult Scene::runRoomEntryInteraction(const InteractionResult &roomEntryInteraction) {
bool didEntryTransition = false;
Common::Error entryInteractionError = _interactionProcessor.handleInteractionResult(
roomEntryInteraction, didEntryTransition, Common::String());
@@ -433,7 +445,7 @@ RoomInputResult RoomSession::runRoomEntryInteraction(const InteractionResult &ro
return RoomInputResult();
}
-void RoomSession::updateRoomInterface(bool &needsRedraw) {
+void Scene::updateRoomInterface(bool &needsRedraw) {
captureCurrentSaveState();
if (_inventory.isOpen() && _inventory.refreshIfRuntimeStateChanged())
needsRedraw = true;
@@ -451,7 +463,7 @@ void RoomSession::updateRoomInterface(bool &needsRedraw) {
}
}
-bool RoomSession::drawRoomFrame(bool &needsRedraw) {
+bool Scene::drawRoomFrame(bool &needsRedraw) {
if (!needsRedraw)
return true;
@@ -573,7 +585,7 @@ bool RoomSession::drawRoomFrame(bool &needsRedraw) {
return true;
}
-RoomInputResult RoomSession::pollRoomEvents(RoomInput &input, bool &needsRedraw) {
+RoomInputResult Scene::pollRoomEvents(RoomInput &input, bool &needsRedraw) {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
const RoomInputResult inputResult = input.handleEvent(event, needsRedraw);
@@ -583,7 +595,7 @@ RoomInputResult RoomSession::pollRoomEvents(RoomInput &input, bool &needsRedraw)
return RoomInputResult();
}
-RoomSession::PlayerControlState RoomSession::preparePlayerControlState() {
+Scene::PlayerControlState Scene::preparePlayerControlState() {
Script *script = _engine.getScript();
const bool playerAlive = script && script->getPlayerCurrentHitPoints() > 0;
const bool playerControlPaused = script && script->isPlayerControlPaused();
@@ -602,13 +614,13 @@ RoomSession::PlayerControlState RoomSession::preparePlayerControlState() {
return control;
}
-void RoomSession::advanceRuntimeEntities(bool &needsRedraw) {
+void Scene::advanceRuntimeEntities(bool &needsRedraw) {
if (_flow.tickRuntimeEntities())
needsRedraw = true;
syncAnimatedRoomActorPlacement();
}
-void RoomSession::updatePlayerCombatAnimations(bool &needsRedraw) {
+void Scene::updatePlayerCombatAnimations(bool &needsRedraw) {
if (Player::updateAttackAnimationState(_engine, _playerState))
needsRedraw = true;
if (Player::updateHitAnimationState(
@@ -617,7 +629,7 @@ void RoomSession::updatePlayerCombatAnimations(bool &needsRedraw) {
}
}
-void RoomSession::updatePlayerControls(const PlayerControlState &control, bool &needsRedraw) {
+void Scene::updatePlayerControls(const PlayerControlState &control, bool &needsRedraw) {
const bool keyboardAttackRequested =
_inputState.attackModifierHeld && (_inputState.moveLeft || _inputState.moveRight || _inputState.moveUp || _inputState.moveDown);
if (control.canAct &&
@@ -678,21 +690,21 @@ void RoomSession::updatePlayerControls(const PlayerControlState &control, bool &
}
}
-void RoomSession::updatePlayerDeath(bool &needsRedraw) {
+void Scene::updatePlayerDeath(bool &needsRedraw) {
if (Player::updateDeathAnimationState(_playerState)) {
requestPlayerGameOver("combat_player_death_complete", Common::String());
needsRedraw = true;
}
}
-void RoomSession::pruneCombatEffects(bool &needsRedraw) {
+void Scene::pruneCombatEffects(bool &needsRedraw) {
if (_combatEffects.pruneCombatHitEffects())
needsRedraw = true;
if (_combatEffects.pruneCombatDamagePopups())
needsRedraw = true;
}
-RoomInputResult RoomSession::dispatchExpiredTimers(bool &needsRedraw) {
+RoomInputResult Scene::dispatchExpiredTimers(bool &needsRedraw) {
if (_entityManager) {
Common::Array<Common::String> expiredTimerNames;
if (_entityManager->takeExpiredTimerNames(expiredTimerNames)) {
@@ -720,7 +732,7 @@ RoomInputResult RoomSession::dispatchExpiredTimers(bool &needsRedraw) {
return RoomInputResult();
}
-RoomInputResult RoomSession::runRoomFrames(const Common::String ¤tRoomTarget) {
+RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
RoomInput input(_engine, _flow, *this, _scene, _inventory, _cheats,
_roomItems, _interactionProcessor, _presentationState, *_view.art, _canExitCloseupToParent);
bool needsRedraw = true;
@@ -886,61 +898,281 @@ RoomInputResult RoomSession::runRoomFrames(const Common::String ¤tRoomTarg
return RoomInputResult();
}
-void RoomSession::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) {
+void Scene::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) {
_sceneRuntime.syncGlobalTimerEntities(previousTimerRecords);
}
-bool RoomSession::captureDialogueBackdrop(IndexedBitmap &backdrop) {
+bool Scene::captureDialogueBackdrop(IndexedBitmap &backdrop) {
return _presentation.captureDialogueBackdrop(backdrop);
}
-Common::Error RoomSession::showCdChangePrompt(int discNumber) {
+Common::Error Scene::showCdChangePrompt(int discNumber) {
return _flow.showCdChangePrompt(discNumber);
}
-Common::Error RoomSession::applyLightingCommand(StartupLightingCommand command) {
+Common::Error Scene::applyLightingCommand(StartupLightingCommand command) {
return _presentation.applyLightingCommand(command);
}
-Common::Error RoomSession::runModalShowText(const ResolvedText &text) {
+Common::Error Scene::runModalShowText(const ResolvedText &text) {
return _presentation.runModalShowText(text, _flow);
}
-Common::Error RoomSession::tryActivatePendingRegion(bool &needsRedraw) {
+Common::Error Scene::tryActivatePendingRegion(bool &needsRedraw) {
return isPlayerCombatLocked() ? Common::kNoError : _interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
}
-Common::Error RoomSession::tryActivatePassiveRegion(bool &needsRedraw) {
+Common::Error Scene::tryActivatePassiveRegion(bool &needsRedraw) {
return _interactionProcessor.tryActivatePassiveRegion(_inventory, needsRedraw);
}
-Common::Error RoomSession::resolvePlayerAttackContact(bool &needsRedraw) {
+Common::Error Scene::resolvePlayerAttackContact(bool &needsRedraw) {
return _combat.resolvePlayerAttackContact(_engine, _scene.state, _playerState, _entityManager,
_sceneRuntime, _combatEffects, _interactionProcessor, needsRedraw);
}
-Common::Error RoomSession::updateRoomMonsterCombat(bool &needsRedraw) {
+Common::Error Scene::updateRoomMonsterCombat(bool &needsRedraw) {
return _combat.updateRoomMonsterCombat(_engine, _scene.state, _playerState, _idleState,
_entityManager, _sceneRuntime, _combatEffects, _cheats, *this, _interactionProcessor, needsRedraw);
}
-void RoomSession::syncCombatHitEffects(bool &needsRedraw) {
+void Scene::syncCombatHitEffects(bool &needsRedraw) {
if (_combatEffects.syncCombatHitEffects())
needsRedraw = true;
}
-void RoomSession::updateIdleAnimation(bool &needsRedraw) {
+void Scene::updateIdleAnimation(bool &needsRedraw) {
if (Player::updateIdleAnimation(_scene.state, _playerState, _idleState))
needsRedraw = true;
}
-void RoomSession::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc) {
+void Scene::capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc) {
_combat.capturePlayerAttackTarget(_scene.state, _playerState, _entityManager, mousePos, npc);
}
-Common::Error RoomSession::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
+Common::Error Scene::killDebugActiveMonster(const Common::Point &mousePos, bool &needsRedraw) {
return _combat.killDebugActiveMonster(_engine, _scene.state, _playerState, _entityManager,
mousePos, _combatEffects, _interactionProcessor, needsRedraw);
}
+static const CftFontResource *findStartupFontByName(const HarvesterEngine &engine, const char *fontName) {
+ const Text *text = engine.getText();
+ if (!text || !fontName)
+ return nullptr;
+
+ for (const CftFontResource &font : text->getFonts()) {
+ if (font.name.equalsIgnoreCase(fontName))
+ return &font;
+ }
+
+ return nullptr;
+}
+
+SceneRunner::SceneRunner(HarvesterEngine &engine,
+ InventorySystem &inventory, CheatSystem &cheats)
+ : _engine(engine), _inventory(inventory), _cheats(cheats) {
+}
+
+Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetName,
+ bool targetIsRoomName) {
+ if (flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+
+ Common::String currentRoomTarget = targetName;
+ bool currentTargetIsRoomName = targetIsRoomName;
+ bool currentTargetUsesSavedRoomState = false;
+ SaveRoomState currentTargetSavedRoomState;
+ while (!currentRoomTarget.empty()) {
+ RoomSetupState state;
+ bool shouldRunRoomEntryCommands = false;
+ if (_engine.hasPendingLoadedSaveRoomState()) {
+ // Native load destroys live render entities and frees preserved global timers
+ // before room_setup rebuilds the loaded room from restored TIMER records.
+ if (EntityManager *entityManager = _engine.getRuntimeEntities())
+ entityManager->clearSceneEntities(false);
+ flow.resetRoomNpcDialogueState();
+ if (_engine.hasPendingLoadedDialogueStateBlob()) {
+ if (!flow.loadDialogueSaveStateBlob(
+ _engine.getPendingLoadedDialogueStateBlob(),
+ _engine.getPendingLoadedDialogueStateBlobVersion()))
+ return Common::kReadingFailed;
+ _engine.clearPendingLoadedDialogueStateBlob();
+ }
+ if (_engine.getPendingLoadedDisc() > 0 &&
+ !_engine.activateDisc(_engine.getPendingLoadedDisc())) {
+ return Common::kReadingFailed;
+ }
+ const SaveRoomState &loadedState = _engine.getPendingLoadedSaveRoomState();
+ debugC(1, kDebugRoom,
+ "Harvester: applying pending loaded room state entrance='%s' room='%s' spawn=(%d,%d,%d) facing=%d music='%s'",
+ loadedState.entranceName.c_str(), loadedState.roomName.c_str(),
+ loadedState.playerX, loadedState.playerY, loadedState.playerZ,
+ loadedState.playerFacing, loadedState.musicPath.c_str());
+ if (!_engine.getScript()->materializeRoomState(
+ loadedState.entranceName, loadedState.roomName, state, *_engine.getResources())) {
+ return Common::kReadingFailed;
+ }
+ state.entranceName = loadedState.entranceName;
+ state.playerSpawnX = loadedState.playerX;
+ state.playerSpawnY = loadedState.playerY;
+ state.playerSpawnZ = loadedState.playerZ;
+ state.playerFacing = loadedState.playerFacing;
+ state.musicPath = loadedState.musicPath;
+ currentRoomTarget = !loadedState.entranceName.empty() ? loadedState.entranceName : loadedState.roomName;
+ _engine.clearPendingLoadedSaveRoomState();
+ currentTargetIsRoomName = false;
+ shouldRunRoomEntryCommands = false;
+ } else if ((currentTargetIsRoomName
+ ? !_engine.getScript()->resolveRoomSetupStateByRoomName(
+ currentRoomTarget, state, *_engine.getResources())
+ : !_engine.getScript()->resolveRoomSetupState(
+ currentRoomTarget, state, *_engine.getResources()))) {
+ return Common::kReadingFailed;
+ } else {
+ shouldRunRoomEntryCommands = true;
+ }
+ if (shouldRunRoomEntryCommands && currentTargetUsesSavedRoomState &&
+ currentTargetSavedRoomState.valid) {
+ const SaveRoomState &savedState = currentTargetSavedRoomState;
+ state.entranceName = savedState.entranceName;
+ state.playerSpawnX = savedState.playerX;
+ state.playerSpawnY = savedState.playerY;
+ state.playerSpawnZ = savedState.playerZ;
+ state.playerFacing = savedState.playerFacing;
+ if (!savedState.musicPath.empty())
+ state.musicPath = savedState.musicPath;
+ debugC(1, kDebugRoom,
+ "Harvester: applying saved room restart state entrance='%s' room='%s' spawn=(%d,%d,%d) facing=%d music='%s'",
+ savedState.entranceName.c_str(), savedState.roomName.c_str(),
+ savedState.playerX, savedState.playerY, savedState.playerZ,
+ savedState.playerFacing, savedState.musicPath.c_str());
+ }
+
+ Common::Error transitionError = flow.beginRoomSetupTransition();
+ if (transitionError.getCode() != Common::kNoError)
+ return transitionError;
+
+ SceneState scene;
+ if (!loadRoomSceneResources(state, *_engine.getResources(), scene))
+ return Common::kReadingFailed;
+
+ Graphics::Screen *screen = _engine.getScreen();
+ const Art *art = _engine.getArt();
+ const Graphics::Font *bodyFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+ const CftFontResource *promptFontResource = findStartupFontByName(_engine, "MEDFONT1");
+ const CftFontResource *inventoryTooltipFontResource = findStartupFontByName(_engine, "TEXTFONT");
+ Common::ScopedPtr<HarvesterCftFont> promptCftFont;
+ Common::ScopedPtr<HarvesterCftFont> inventoryTooltipCftFont;
+ const Graphics::Font *promptFont = bodyFont;
+ const Graphics::Font *inspectFont = bodyFont;
+ const Graphics::Font *inventoryTooltipFont = bodyFont;
+ bool useNativePromptFont = false;
+ bool useNativeInspectFont = false;
+ if (promptFontResource) {
+ promptCftFont.reset(new HarvesterCftFont(*promptFontResource));
+ if (promptCftFont->isValid()) {
+ promptFont = promptCftFont.get();
+ useNativePromptFont = true;
+ }
+ }
+ if (inventoryTooltipFontResource) {
+ inventoryTooltipCftFont.reset(new HarvesterCftFont(*inventoryTooltipFontResource));
+ if (inventoryTooltipCftFont->isValid()) {
+ inventoryTooltipFont = inventoryTooltipCftFont.get();
+ inspectFont = inventoryTooltipCftFont.get();
+ useNativeInspectFont = true;
+ }
+ }
+ if (!screen || !art || !bodyFont || !promptFont || !inventoryTooltipFont || !inspectFont)
+ return Common::kNoError;
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+
+ if (!flow.populateRoomSceneEntities(scene.state, scene.sceneObjects, scene.sceneAnimations))
+ return Common::kReadingFailed;
+ if (entityManager) {
+ for (const MonsterRecord &monster : scene.state.roomMonsters) {
+ Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
+ if (entity)
+ Monster::applyAnimation(*entity, monster);
+ }
+ entityManager->pauseTimerCountdowns();
+ }
+
+ flow.resetCursorAnimationSequence();
+ flow.executeStartupAudioCommands(scene.state.audioCommands);
+ if (!scene.state.musicPath.empty())
+ (void)_engine.playMusic(scene.state.musicPath);
+ const RoomViewResources view = { art, promptFont, inspectFont, inventoryTooltipFont,
+ useNativePromptFont, useNativeInspectFont };
+ Scene session(_engine, flow, scene, entityManager, _inventory, _cheats, view,
+ flow._roomLoopDepth > 1);
+ debugC(1, kDebugRoom, "Harvester: created room session room='%s'", scene.state.roomName.c_str());
+ session.initializePlayerState();
+
+ session.resetIdleState();
+
+ InteractionResult roomEntryInteraction;
+ bool hasPendingRoomEntryInteraction = false;
+ if (shouldRunRoomEntryCommands) {
+ if (!_engine.getScript()->executeRoomEnterCommands(
+ scene.state.roomName, roomEntryInteraction)) {
+ return Common::kReadingFailed;
+ }
+
+ // Native room_setup applies entry-time state changes before the first visible room frame.
+ if (roomEntryInteraction.mutatedRuntimeState) {
+ session.syncGlobalTimerEntities(roomEntryInteraction.previousTimerRecords);
+ if (!session.applyCurrentRoomRuntimeMutationsInPlace(false) &&
+ !session.refreshCurrentScene(true)) {
+ return Common::kReadingFailed;
+ }
+ roomEntryInteraction.mutatedRuntimeState = false;
+ roomEntryInteraction.visualRuntimeStateChanged = false;
+ }
+ hasPendingRoomEntryInteraction = hasRoomEntryInteraction(roomEntryInteraction);
+ }
+
+ transitionError = flow.waitForRoomSetupTransitionHold();
+ if (transitionError.getCode() != Common::kNoError)
+ return transitionError;
+
+ logScenePaletteSummary("room setup stub palette", scene, 0.0f);
+ drawRoomScene(_engine, *screen, scene, 0.0f);
+ screen->makeAllDirty();
+ screen->update();
+
+ logScenePaletteSummary("room setup fade target", scene, scene.targetPaletteBrightness);
+ transitionError = flow.fadeInRoomScene(scene.palette, scene.targetPaletteBrightness);
+ if (transitionError.getCode() != Common::kNoError)
+ return transitionError;
+ if (entityManager)
+ entityManager->resumeTimerCountdowns();
+
+ if (shouldRunRoomEntryCommands) {
+ if (hasPendingRoomEntryInteraction) {
+ const RoomInputResult entryResult = session.runRoomEntryInteraction(roomEntryInteraction);
+ if (entryResult.returnFromRoom)
+ return entryResult.error;
+ }
+
+ flow.resetCursorAnimationSequence();
+ session.resetIdleState();
+ }
+ ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
+ _engine.getScript());
+
+ const RoomInputResult frameResult = session.runRoomFrames(currentRoomTarget);
+ if (frameResult.returnFromRoom)
+ return frameResult.error;
+ const RoomPendingTransition &pendingTransition = session.getPendingTransition();
+
+ currentRoomTarget = pendingTransition.roomName;
+ currentTargetIsRoomName = pendingTransition.isRoomName;
+ currentTargetUsesSavedRoomState = pendingTransition.usesSavedRoomState;
+ currentTargetSavedRoomState = pendingTransition.savedRoomState;
+ }
+
+ return Common::kNoError;
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/room_session.h b/engines/harvester/scene.h
similarity index 87%
rename from engines/harvester/room_session.h
rename to engines/harvester/scene.h
index 55f3e7e52d4..da19ea16e3f 100644
--- a/engines/harvester/room_session.h
+++ b/engines/harvester/scene.h
@@ -19,14 +19,16 @@
*
*/
-#ifndef HARVESTER_ROOM_SESSION_H
-#define HARVESTER_ROOM_SESSION_H
+#ifndef HARVESTER_SCENE_H
+#define HARVESTER_SCENE_H
+#include "common/error.h"
+#include "common/str.h"
#include "harvester/room_combat.h"
#include "harvester/room_input.h"
#include "harvester/room_interaction.h"
#include "harvester/room_presentation.h"
-#include "harvester/room_scene_runtime.h"
+#include "harvester/scene_entities.h"
#include "harvester/room_support.h"
#include "harvester/runtime_entity.h"
@@ -36,6 +38,19 @@ class EntityManager;
class Flow;
class InventorySystem;
+class SceneRunner {
+public:
+ SceneRunner(HarvesterEngine &engine, InventorySystem &inventory,
+ CheatSystem &cheats);
+
+ Common::Error runRoomLoop(Flow &flow, const Common::String &targetName, bool targetIsRoomName);
+
+private:
+ HarvesterEngine &_engine;
+ InventorySystem &_inventory;
+ CheatSystem &_cheats;
+};
+
struct RoomPendingTransition {
Common::String roomName;
bool isRoomName = false;
@@ -43,11 +58,9 @@ struct RoomPendingTransition {
SaveRoomState savedRoomState;
};
-// Scoped to one runRoomLoop iteration, including time spent in nested closeups.
-// Entity pointers remain owned by EntityManager.
-class RoomSession : public RoomInteractionCallbacks {
+class Scene : public RoomInteractionCallbacks {
public:
- RoomSession(HarvesterEngine &engine, Flow &flow, RoomSceneResources &scene, EntityManager *entityManager,
+ Scene(HarvesterEngine &engine, Flow &flow, SceneState &scene, EntityManager *entityManager,
InventorySystem &inventory, CheatSystem &cheats, const RoomViewResources &view, bool canExitCloseupToParent);
RoomPlayerState &getPlayerState() { return _playerState; }
@@ -108,10 +121,10 @@ private:
HarvesterEngine &_engine;
Flow &_flow;
- RoomSceneResources &_scene;
+ SceneState &_scene;
EntityManager *_entityManager;
InventorySystem &_inventory;
- RoomSceneRuntime _sceneRuntime;
+ SceneEntities _sceneRuntime;
RoomPlayerState _playerState;
RoomCombat _combat;
RoomCombatEffects _combatEffects;
@@ -130,4 +143,4 @@ private:
} // End of namespace Harvester
-#endif // HARVESTER_ROOM_SESSION_H
+#endif // HARVESTER_SCENE_H
diff --git a/engines/harvester/room_scene_runtime.cpp b/engines/harvester/scene_entities.cpp
similarity index 91%
rename from engines/harvester/room_scene_runtime.cpp
rename to engines/harvester/scene_entities.cpp
index c4ab1749e8e..1ba485826cc 100644
--- a/engines/harvester/room_scene_runtime.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/room_scene_runtime.h"
+#include "harvester/scene_entities.h"
#include "harvester/detection.h"
#include "harvester/monster.h"
@@ -140,8 +140,8 @@ static bool sameSceneObjectState(const ObjectRecord &lhs, const ObjectRecord &rh
lhs.identTextKey.equalsIgnoreCase(rhs.identTextKey) &&
lhs.interactionLabel.equalsIgnoreCase(rhs.interactionLabel) &&
lhs.currentOwnerOrRoom.equalsIgnoreCase(rhs.currentOwnerOrRoom) &&
- RoomSceneRuntime::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(RoomSceneRuntime::resolveSceneObjectSpritePathLocal(rhs)) &&
- sameRect(RoomSceneRuntime::getSceneObjectBounds(lhs), RoomSceneRuntime::getSceneObjectBounds(rhs));
+ SceneEntities::resolveSceneObjectSpritePathLocal(lhs).equalsIgnoreCase(SceneEntities::resolveSceneObjectSpritePathLocal(rhs)) &&
+ sameRect(SceneEntities::getSceneObjectBounds(lhs), SceneEntities::getSceneObjectBounds(rhs));
}
static bool sameSceneRegionState(const RegionRecord &lhs, const RegionRecord &rhs) {
@@ -169,18 +169,18 @@ bool sameMonsterEntityState(const MonsterRecord &lhs, const MonsterRecord &rhs)
lhs.modelPath.equalsIgnoreCase(rhs.modelPath);
}
-RoomSceneRuntime::RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager)
+SceneEntities::SceneEntities(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager)
: _state(state), _script(script), _resources(resources), _entityManager(entityManager) {
debugC(1, kDebugRoom, "Harvester: created room scene runtime room='%s' entities=%d",
_state.roomName.c_str(), _entityManager != nullptr);
}
-bool RoomSceneRuntime::loadSceneResources(const RoomSetupState &state, RoomSceneResources &scene) const {
+bool SceneEntities::loadSceneResources(const RoomSetupState &state, SceneState &scene) const {
return _resources && loadRoomSceneResources(state, *_resources, scene);
}
-bool RoomSceneRuntime::reconcileSceneEntitiesInPlace(const RoomSceneResources &previousScene,
- RoomSetupState &updatedState, const RoomSceneResources &updatedScene) {
+bool SceneEntities::reconcileSceneEntitiesInPlace(const SceneState &previousScene,
+ RoomSetupState &updatedState, const SceneState &updatedScene) {
for (const ObjectRecord &object : previousScene.sceneObjects) {
if (!findSceneObjectByName(updatedScene.sceneObjects, object.objectName))
removeSceneEntityByName(object.objectName);
@@ -388,14 +388,11 @@ bool RoomSceneRuntime::reconcileSceneEntitiesInPlace(const RoomSceneResources &p
return true;
}
-void RoomSceneRuntime::syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions) {
+void SceneEntities::syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions) {
Script *script = _script;
if (!script)
return;
-
- debugC(3, kDebugRoom, "Harvester: syncing room runtime state room='%s' entities=%d",
- _state.roomName.c_str(), _entityManager != nullptr);
-
+
for (const RegionRecord &baseRegion : script->getRegions()) {
if (!baseRegion.roomName.equalsIgnoreCase(_state.roomName))
continue;
@@ -434,7 +431,7 @@ void RoomSceneRuntime::syncCurrentRoomRuntimeState(const Common::Array<RegionRec
}
}
-bool RoomSceneRuntime::isTimerMaterializedInCurrentRoom(const Common::String &timerName) const {
+bool SceneEntities::isTimerMaterializedInCurrentRoom(const Common::String &timerName) const {
for (const TimerRecord &timer : _state.roomTimers) {
if (timer.timerName.equalsIgnoreCase(timerName))
return true;
@@ -443,7 +440,7 @@ bool RoomSceneRuntime::isTimerMaterializedInCurrentRoom(const Common::String &ti
return false;
}
-void RoomSceneRuntime::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
+void SceneEntities::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers) {
Script *script = _script;
if (!script || !_entityManager || previousTimerRecords.empty())
@@ -490,7 +487,7 @@ void RoomSceneRuntime::syncGlobalTimerEntities(const Common::Array<TimerRecord>
}
}
-void RoomSceneRuntime::removeSceneEntityByName(const Common::String &entityName) {
+void SceneEntities::removeSceneEntityByName(const Common::String &entityName) {
if (!_entityManager)
return;
@@ -498,13 +495,13 @@ void RoomSceneRuntime::removeSceneEntityByName(const Common::String &entityName)
delete entity;
}
-Common::Rect RoomSceneRuntime::getSceneObjectBounds(const ObjectRecord &object) {
+Common::Rect SceneEntities::getSceneObjectBounds(const ObjectRecord &object) {
if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
return Common::Rect();
}
-Common::String RoomSceneRuntime::resolveSceneObjectSpritePathLocal(const ObjectRecord &object) {
+Common::String SceneEntities::resolveSceneObjectSpritePathLocal(const ObjectRecord &object) {
const bool atInitialPlacement = object.currentX == object.initialX &&
object.currentY == object.initialY &&
object.currentOwnerOrRoom.equalsIgnoreCase(object.initialOwnerOrRoom);
@@ -514,7 +511,7 @@ Common::String RoomSceneRuntime::resolveSceneObjectSpritePathLocal(const ObjectR
return object.spritePath;
}
-bool RoomSceneRuntime::isInteractiveSceneHotspot(const ObjectRecord &object) const {
+bool SceneEntities::isInteractiveSceneHotspot(const ObjectRecord &object) const {
if (object.operatable || !object.actionTag.empty())
return true;
@@ -528,7 +525,7 @@ bool RoomSceneRuntime::isInteractiveSceneHotspot(const ObjectRecord &object) con
return script->resolveObjectInspectText(object, inspectText);
}
-RuntimeEntityClass RoomSceneRuntime::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
+RuntimeEntityClass SceneEntities::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
if (entity) {
return ((!_state.backgroundObjectName.empty() &&
object.objectName.equalsIgnoreCase(_state.backgroundObjectName)) ||
@@ -543,7 +540,7 @@ RuntimeEntityClass RoomSceneRuntime::resolveSceneObjectClassLocal(const ObjectRe
: kRuntimeEntityClassDisabledHotspot;
}
-void RoomSceneRuntime::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
+void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
if (!_entityManager)
return;
@@ -568,13 +565,13 @@ void RoomSceneRuntime::spawnSceneObjectEntityFromRecord(const ObjectRecord &obje
object.objectName.c_str(), spritePath.c_str(), entity->getClassId());
}
-Common::Rect RoomSceneRuntime::getSceneRegionBounds(const RegionRecord ®ion) {
+Common::Rect SceneEntities::getSceneRegionBounds(const RegionRecord ®ion) {
if (region.right > region.left && region.bottom > region.top)
return Common::Rect(region.left, region.top, region.right + 1, region.bottom + 1);
return Common::Rect();
}
-void RoomSceneRuntime::spawnSceneRegionEntityFromRecord(const RegionRecord ®ion) {
+void SceneEntities::spawnSceneRegionEntityFromRecord(const RegionRecord ®ion) {
if (!_entityManager)
return;
@@ -599,15 +596,15 @@ void RoomSceneRuntime::spawnSceneRegionEntityFromRecord(const RegionRecord ®i
region.minZ, region.maxZ, region.desiredFacing, region.cursorEnabled, region.actionTag.c_str());
}
-bool RoomSceneRuntime::shouldSpawnRoomNpcEntity(const NpcRecord &npc) {
+bool SceneEntities::shouldSpawnRoomNpcEntity(const NpcRecord &npc) {
return npc.visible && !npc.deathOrMonsterfyFlag;
}
-bool RoomSceneRuntime::shouldSpawnRoomMonsterEntity(const MonsterRecord &monster) {
+bool SceneEntities::shouldSpawnRoomMonsterEntity(const MonsterRecord &monster) {
return monster.visible;
}
-bool RoomSceneRuntime::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc) {
+bool SceneEntities::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc) {
int width = 0;
int height = 0;
int xOffset = 0;
@@ -622,7 +619,7 @@ bool RoomSceneRuntime::applyRoomNpcPlacement(Entity &entity, const NpcRecord &np
return true;
}
-Entity *RoomSceneRuntime::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
+Entity *SceneEntities::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
const bool shouldSpawnNpc = shouldSpawnRoomNpcEntity(npc);
const bool preservedCorpse = !npc.active &&
npc.runtimeSpawned &&
@@ -658,7 +655,7 @@ Entity *RoomSceneRuntime::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
return entity;
}
-Entity *RoomSceneRuntime::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
+Entity *SceneEntities::spawnSceneMonsterEntityFromRecord(const RoomSetupState &roomState, const MonsterRecord &monster) {
if (!_entityManager || !shouldSpawnRoomMonsterEntity(monster))
return nullptr;
@@ -682,7 +679,7 @@ Entity *RoomSceneRuntime::spawnSceneMonsterEntityFromRecord(const RoomSetupState
return entity;
}
-void RoomSceneRuntime::restoreRoomMonsterAnimations() {
+void SceneEntities::restoreRoomMonsterAnimations() {
if (_entityManager) {
for (const MonsterRecord &monster : _state.roomMonsters) {
Entity *entity = _entityManager->findSceneEntityByName(monster.monsterName);
@@ -692,7 +689,7 @@ void RoomSceneRuntime::restoreRoomMonsterAnimations() {
}
}
-void RoomSceneRuntime::populateRoomRegions() {
+void SceneEntities::populateRoomRegions() {
if (!_entityManager)
return;
@@ -725,7 +722,7 @@ void RoomSceneRuntime::populateRoomRegions() {
}
}
-void RoomSceneRuntime::populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects) {
+void SceneEntities::populateRoomObjects(const Common::Array<ObjectRecord> &drawableObjects) {
if (!_entityManager)
return;
@@ -769,7 +766,7 @@ void RoomSceneRuntime::populateRoomObjects(const Common::Array<ObjectRecord> &dr
}
}
-void RoomSceneRuntime::populateRoomNpcs() {
+void SceneEntities::populateRoomNpcs() {
if (!_entityManager)
return;
@@ -815,7 +812,7 @@ void RoomSceneRuntime::populateRoomNpcs() {
}
}
-void RoomSceneRuntime::populateRoomMonsters() {
+void SceneEntities::populateRoomMonsters() {
if (!_entityManager)
return;
diff --git a/engines/harvester/room_scene_runtime.h b/engines/harvester/scene_entities.h
similarity index 84%
rename from engines/harvester/room_scene_runtime.h
rename to engines/harvester/scene_entities.h
index 15febc6fbae..fb451a6c12a 100644
--- a/engines/harvester/room_scene_runtime.h
+++ b/engines/harvester/scene_entities.h
@@ -19,8 +19,8 @@
*
*/
-#ifndef HARVESTER_ROOM_SCENE_RUNTIME_H
-#define HARVESTER_ROOM_SCENE_RUNTIME_H
+#ifndef HARVESTER_SCENE_ENTITIES_H
+#define HARVESTER_SCENE_ENTITIES_H
#include "harvester/room_support.h"
#include "harvester/runtime_entity.h"
@@ -28,13 +28,13 @@
namespace Harvester {
// Borrows the materialized room state; live entities remain owned by EntityManager.
-class RoomSceneRuntime {
+class SceneEntities {
public:
- RoomSceneRuntime(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager);
+ SceneEntities(RoomSetupState &state, Script *script, ResourceManager *resources, EntityManager *entityManager);
- bool loadSceneResources(const RoomSetupState &state, RoomSceneResources &scene) const;
- bool reconcileSceneEntitiesInPlace(const RoomSceneResources &previousScene,
- RoomSetupState &updatedState, const RoomSceneResources &updatedScene);
+ bool loadSceneResources(const RoomSetupState &state, SceneState &scene) const;
+ bool reconcileSceneEntitiesInPlace(const SceneState &previousScene,
+ RoomSetupState &updatedState, const SceneState &updatedScene);
void syncCurrentRoomRuntimeState(const Common::Array<RegionRecord> &sceneRegions);
void syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords,
bool includeCurrentRoomTimers = false);
@@ -69,4 +69,4 @@ private:
} // End of namespace Harvester
-#endif // HARVESTER_ROOM_SCENE_RUNTIME_H
+#endif // HARVESTER_SCENE_ENTITIES_H
Commit: d8ab1acf31a44aa1ced4dd4890af257f25138ec2
https://github.com/scummvm/scummvm/commit/d8ab1acf31a44aa1ced4dd4890af257f25138ec2
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: refactor/rename Entity, Interaction, Combat and Input
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/combat.cpp
A engines/harvester/combat.h
A engines/harvester/entity.cpp
A engines/harvester/entity.h
A engines/harvester/input.cpp
A engines/harvester/input.h
A engines/harvester/interaction.cpp
A engines/harvester/interaction.h
R engines/harvester/room_combat.cpp
R engines/harvester/room_combat.h
R engines/harvester/room_input.cpp
R engines/harvester/room_input.h
R engines/harvester/room_interaction.cpp
R engines/harvester/room_interaction.h
R engines/harvester/runtime_entity.cpp
R engines/harvester/runtime_entity.h
engines/harvester/dialogue.cpp
engines/harvester/flow.cpp
engines/harvester/flow.h
engines/harvester/media_manager.cpp
engines/harvester/menu.cpp
engines/harvester/module.mk
engines/harvester/monster.cpp
engines/harvester/player.cpp
engines/harvester/scene.cpp
engines/harvester/scene.h
engines/harvester/scene_entities.cpp
engines/harvester/scene_entities.h
diff --git a/engines/harvester/room_combat.cpp b/engines/harvester/combat.cpp
similarity index 89%
rename from engines/harvester/room_combat.cpp
rename to engines/harvester/combat.cpp
index 53eb6a5ffb1..45cbbbf6bb2 100644
--- a/engines/harvester/room_combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/room_combat.h"
+#include "harvester/combat.h"
#include "common/util.h"
#include "graphics/font.h"
@@ -29,9 +29,9 @@
#include "harvester/detection.h"
#include "harvester/harvester.h"
#include "harvester/player.h"
-#include "harvester/room_interaction.h"
+#include "harvester/interaction.h"
#include "harvester/scene_entities.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
namespace Harvester {
@@ -364,21 +364,21 @@ bool runtimeEntityHasDrawableFrameRange(const Entity &entity, int firstFrame, in
bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
const Entity &entity, int actorCenterX, int targetCenterX,
- RoomAttackAnimationRange &range) {
+ 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 RoomAttackAnimationRange kLeftAttackRanges[3] = {
- RoomAttackAnimationRange(0x64, 0x6d, 1),
- RoomAttackAnimationRange(0x5a, 0x63, 1),
- RoomAttackAnimationRange(0x50, 0x59, 1)
+ static const AttackAnimationRange kLeftAttackRanges[3] = {
+ AttackAnimationRange(0x64, 0x6d, 1),
+ AttackAnimationRange(0x5a, 0x63, 1),
+ AttackAnimationRange(0x50, 0x59, 1)
};
- static const RoomAttackAnimationRange kRightAttackRanges[3] = {
- RoomAttackAnimationRange(0x82, 0x8b, 2),
- RoomAttackAnimationRange(0x78, 0x81, 2),
- RoomAttackAnimationRange(0x6e, 0x77, 2)
+ static const AttackAnimationRange kRightAttackRanges[3] = {
+ AttackAnimationRange(0x82, 0x8b, 2),
+ AttackAnimationRange(0x78, 0x81, 2),
+ AttackAnimationRange(0x6e, 0x77, 2)
};
- const RoomAttackAnimationRange *candidates =
+ const AttackAnimationRange *candidates =
targetCenterX < actorCenterX ? kLeftAttackRanges : kRightAttackRanges;
int availableCandidateIndices[3];
int availableCandidateCount = 0;
@@ -398,23 +398,23 @@ bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
}
bool resolveMonsterDeathAnimationRange(const Entity &entity, int facing,
- int deathDamageType, bool goreEnabled, RoomDeathAnimationRange &range) {
+ int deathDamageType, bool goreEnabled, DeathAnimationRange &range) {
const bool preferLeftBank = facing == 1;
- const RoomDeathAnimationRange rightBludge(0xb0, 0xb9);
- const RoomDeathAnimationRange leftBludge(0xba, 0xc3);
- const RoomDeathAnimationRange rightSlash(0x100, 0x109);
- const RoomDeathAnimationRange leftSlash(0x10a, 0x113);
- const RoomDeathAnimationRange rightProjectile(0x114, 0x11d);
- const RoomDeathAnimationRange leftProjectile(0x11e, 0x127);
-
- const RoomDeathAnimationRange primaryBludge = preferLeftBank ? leftBludge : rightBludge;
- const RoomDeathAnimationRange primarySlash = preferLeftBank ? leftSlash : rightSlash;
- const RoomDeathAnimationRange primaryProjectile = preferLeftBank ? leftProjectile : rightProjectile;
- const RoomDeathAnimationRange fallbackBludge = preferLeftBank ? rightBludge : leftBludge;
- const RoomDeathAnimationRange fallbackSlash = preferLeftBank ? rightSlash : leftSlash;
- const RoomDeathAnimationRange fallbackProjectile = preferLeftBank ? rightProjectile : leftProjectile;
-
- auto chooseIfAvailable = [&](const RoomDeathAnimationRange &candidate) {
+ 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;
@@ -437,12 +437,12 @@ bool resolveMonsterDeathAnimationRange(const Entity &entity, int facing,
}
bool resolveNpcDeathAnimationRange(const Entity &entity, bool hasMonsterfyTarget,
- int deathDamageType, bool goreEnabled, RoomDeathAnimationRange &range) {
- const RoomDeathAnimationRange goreBank(0x3c, 0x45);
- const RoomDeathAnimationRange bludgeBank(0x46, 0x4f);
+ 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 RoomDeathAnimationRange &candidate) {
+ auto chooseIfAvailable = [&](const DeathAnimationRange &candidate) {
if (!runtimeEntityHasFrameRange(entity, candidate.firstFrame, candidate.lastFrame))
return false;
range = candidate;
@@ -528,26 +528,26 @@ bool playRandomRoomAttackSound(HarvesterEngine &engine, const Common::String &so
}
bool resolveRoomMonsterHitAnimationRange(const Entity &entity,
- int attackerAttackFirstFrame, RoomHitAnimationRange &range) {
- RoomHitAnimationRange candidate;
+ int attackerAttackFirstFrame, HitAnimationRange &range) {
+ HitAnimationRange candidate;
switch (attackerAttackFirstFrame) {
case 0x64:
- candidate = RoomHitAnimationRange(0x92, 0x94, 2, -kNativeCombatHitKnockbackDistance);
+ candidate = HitAnimationRange(0x92, 0x94, 2, -kNativeCombatHitKnockbackDistance);
break;
case 0x5a:
- candidate = RoomHitAnimationRange(0x8f, 0x91, 2, -kNativeCombatHitKnockbackDistance);
+ candidate = HitAnimationRange(0x8f, 0x91, 2, -kNativeCombatHitKnockbackDistance);
break;
case 0x50:
- candidate = RoomHitAnimationRange(0x8c, 0x8e, 2, -kNativeCombatHitKnockbackDistance);
+ candidate = HitAnimationRange(0x8c, 0x8e, 2, -kNativeCombatHitKnockbackDistance);
break;
case 0x82:
- candidate = RoomHitAnimationRange(0x9b, 0x9d, 1, kNativeCombatHitKnockbackDistance);
+ candidate = HitAnimationRange(0x9b, 0x9d, 1, kNativeCombatHitKnockbackDistance);
break;
case 0x78:
- candidate = RoomHitAnimationRange(0x98, 0x9a, 1, kNativeCombatHitKnockbackDistance);
+ candidate = HitAnimationRange(0x98, 0x9a, 1, kNativeCombatHitKnockbackDistance);
break;
case 0x6e:
- candidate = RoomHitAnimationRange(0x95, 0x97, 1, kNativeCombatHitKnockbackDistance);
+ candidate = HitAnimationRange(0x95, 0x97, 1, kNativeCombatHitKnockbackDistance);
break;
default:
return false;
@@ -586,18 +586,18 @@ int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster) {
const char *describeCombatTargetClass(int classId) {
switch (classId) {
- case kRuntimeEntityClassNpc:
+ case kEntityClassNpc:
return "npc";
- case kRuntimeEntityClassMonster:
+ case kEntityClassMonster:
return "monster";
- case kRuntimeEntityClassPlayer:
+ case kEntityClassPlayer:
return "player";
default:
return "none";
}
}
-MonsterRecord *RoomCombat::findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const {
+MonsterRecord *Combat::findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const {
MonsterRecord *bestMonster = nullptr;
int bestZ = -0x7fffffff;
int bestTop = -0x7fffffff;
@@ -624,7 +624,7 @@ MonsterRecord *RoomCombat::findMonsterTargetAtPoint(RoomSetupState &state, Entit
return bestMonster;
}
-MonsterRecord *RoomCombat::findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+MonsterRecord *Combat::findOverlappingMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
EntityManager *entityManager) const {
if (!playerState.entity)
return nullptr;
@@ -642,7 +642,7 @@ MonsterRecord *RoomCombat::findOverlappingMonsterTarget(RoomSetupState &state, c
return nullptr;
}
-NpcRecord *RoomCombat::findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const {
+NpcRecord *Combat::findOverlappingNpcTarget(RoomSetupState &state, const RoomPlayerState &playerState, EntityManager *entityManager) const {
if (!playerState.entity)
return nullptr;
@@ -659,7 +659,7 @@ NpcRecord *RoomCombat::findOverlappingNpcTarget(RoomSetupState &state, const Roo
return nullptr;
}
-bool RoomCombat::playerAttackCanReachTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+bool Combat::playerAttackCanReachTarget(RoomSetupState &state, const RoomPlayerState &playerState,
Entity *targetEntity, int engageDistance) const {
if (!playerState.entity || !targetEntity)
return false;
@@ -670,7 +670,7 @@ bool RoomCombat::playerAttackCanReachTarget(RoomSetupState &state, const RoomPla
*playerState.entity, playerState.z, *targetEntity, targetEntity->getZ(), engageDistance);
}
-MonsterRecord *RoomCombat::findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+MonsterRecord *Combat::findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
EntityManager *entityManager) const {
if (!playerState.entity)
return nullptr;
@@ -737,7 +737,7 @@ MonsterRecord *RoomCombat::findProjectileContactMonsterTarget(RoomSetupState &st
return bestMonster;
}
-void RoomCombat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
+void Combat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, bool attackLeft, bool attackRight, bool attackUp, bool attackDown) const {
playerState.attackTargetName.clear();
playerState.attackTargetClassId = -1;
@@ -823,12 +823,12 @@ void RoomCombat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Roo
for (NpcRecord &npc : state.roomNpcs) {
if (!npc.visible || npc.deathOrMonsterfyFlag || npc.deathDamageType != 0)
continue;
- considerTarget(findSceneRuntimeEntity(entityManager, npc.npcName), npc.npcName, kRuntimeEntityClassNpc);
+ considerTarget(findSceneRuntimeEntity(entityManager, npc.npcName), npc.npcName, kEntityClassNpc);
}
for (MonsterRecord &monster : state.roomMonsters) {
if (!monster.active || !monster.visible || monster.currentHitPoints <= 0)
continue;
- considerTarget(findSceneRuntimeEntity(entityManager, monster.monsterName), monster.monsterName, kRuntimeEntityClassMonster);
+ considerTarget(findSceneRuntimeEntity(entityManager, monster.monsterName), monster.monsterName, kEntityClassMonster);
}
if (!bestEntity) {
@@ -848,9 +848,9 @@ void RoomCombat::capturePlayerAttackTargetForKeyboard(RoomSetupState &state, Roo
bestEntity->getScreenRect().right, bestEntity->getScreenRect().bottom);
}
-MonsterRecord *RoomCombat::resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+MonsterRecord *Combat::resolveKillTarget(RoomSetupState &state, const RoomPlayerState &playerState,
EntityManager *entityManager, const Common::Point &mousePos) const {
- if (playerState.attackTargetClassId == kRuntimeEntityClassMonster) {
+ if (playerState.attackTargetClassId == kEntityClassMonster) {
MonsterRecord *monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
if (isKillableMonster(monster) && selectMonsterEntity(entityManager, *monster))
return monster;
@@ -864,7 +864,7 @@ MonsterRecord *RoomCombat::resolveKillTarget(RoomSetupState &state, const RoomPl
if (playerState.entity) {
for (uint i = 0; i < state.roomMonsters.size() && i < _monsterCombatStates.size(); ++i) {
MonsterRecord &monster = state.roomMonsters[i];
- const RoomMonsterCombatState &combatState = _monsterCombatStates[i];
+ const MonsterCombatState &combatState = _monsterCombatStates[i];
if (!combatState.attackActive || !isKillableMonster(&monster) || !selectMonsterEntity(entityManager, monster))
continue;
if (!combatState.attackTargetName.empty() &&
@@ -902,11 +902,11 @@ MonsterRecord *RoomCombat::resolveKillTarget(RoomSetupState &state, const RoomPl
return bestMonster;
}
-void RoomCombat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &playerState,
+void Combat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, const Common::Point &mousePos, const NpcRecord *hoverNpc) const {
if (hoverNpc) {
playerState.attackTargetName = hoverNpc->npcName;
- playerState.attackTargetClassId = kRuntimeEntityClassNpc;
+ playerState.attackTargetClassId = kEntityClassNpc;
debugC(1, kDebugCombat,
"Harvester: combat player target capture class='npc' name='%s' cursor=(%d,%d)",
playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
@@ -915,7 +915,7 @@ void RoomCombat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerStat
if (MonsterRecord *monster = findMonsterTargetAtPoint(state, entityManager, mousePos)) {
playerState.attackTargetName = monster->monsterName;
- playerState.attackTargetClassId = kRuntimeEntityClassMonster;
+ playerState.attackTargetClassId = kEntityClassMonster;
debugC(1, kDebugCombat,
"Harvester: combat player target capture class='monster' name='%s' cursor=(%d,%d)",
playerState.attackTargetName.c_str(), mousePos.x, mousePos.y);
@@ -927,16 +927,16 @@ void RoomCombat::capturePlayerAttackTarget(RoomSetupState &state, RoomPlayerStat
mousePos.x, mousePos.y);
}
-void RoomCombat::captureOverlappingNpcAttackTarget(RoomSetupState &state, RoomPlayerState &playerState, EntityManager *entityManager) const {
+void Combat::captureOverlappingNpcAttackTarget(RoomSetupState &state, RoomPlayerState &playerState, EntityManager *entityManager) const {
if (playerState.attackTargetClassId < 0 && !Player::isProjectileCombatLoadout(playerState.combatLoadout)) {
if (NpcRecord *fallbackNpc = findOverlappingNpcTarget(state, playerState, entityManager)) {
playerState.attackTargetName = fallbackNpc->npcName;
- playerState.attackTargetClassId = kRuntimeEntityClassNpc;
+ playerState.attackTargetClassId = kEntityClassNpc;
}
}
}
-NpcRecord *RoomCombat::selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
+NpcRecord *Combat::selectNpcAttackContactTarget(RoomSetupState &state, const RoomPlayerState &playerState,
EntityManager *entityManager, Entity *&npcEntity) const {
NpcRecord *npc = findRoomNpcRecordByName(state, playerState.attackTargetName);
npcEntity = npc ? findSceneRuntimeEntity(entityManager, npc->npcName) : nullptr;
@@ -946,10 +946,10 @@ NpcRecord *RoomCombat::selectNpcAttackContactTarget(RoomSetupState &state, const
return npc;
}
-MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
+MonsterRecord *Combat::selectMonsterAttackContactTarget(RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, Entity *&monsterEntity) const {
MonsterRecord *monster = nullptr;
- if (playerState.attackTargetClassId == kRuntimeEntityClassMonster)
+ if (playerState.attackTargetClassId == kEntityClassMonster)
monster = findRoomMonsterRecordByName(state, playerState.attackTargetName);
monsterEntity = monster ? findSceneRuntimeEntity(entityManager, monster->monsterName) : nullptr;
if (!monster || !monsterEntity || !playerAttackCanReachTarget(state, playerState, monsterEntity, monster->engageDistance)) {
@@ -959,7 +959,7 @@ MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &stat
monster = findProjectileContactMonsterTarget(state, playerState, entityManager);
if (monster) {
playerState.attackTargetName = monster->monsterName;
- playerState.attackTargetClassId = kRuntimeEntityClassMonster;
+ playerState.attackTargetClassId = kEntityClassMonster;
}
} else {
monster = nullptr;
@@ -969,13 +969,13 @@ MonsterRecord *RoomCombat::selectMonsterAttackContactTarget(RoomSetupState &stat
return monster;
}
-RoomNpcCombatUpdateResult RoomCombat::updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
+NpcCombatUpdateResult Combat::updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
EntityManager *entityManager, SceneEntities &sceneRuntime,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
- RoomNpcCombatUpdateResult result;
+ Interaction &interactionProcessor, bool &needsRedraw) {
+ NpcCombatUpdateResult result;
for (uint i = 0; i < state.roomNpcs.size(); ++i) {
NpcRecord &npc = state.roomNpcs[i];
- RoomNpcCombatState &combatState = _npcCombatStates[i];
+ NpcCombatState &combatState = _npcCombatStates[i];
Entity *entity = findSceneRuntimeEntity(entityManager, npc.npcName);
if (!entity || !npc.visible || npc.deathOrMonsterfyFlag) {
clearRoomNpcCombatState(combatState);
@@ -1010,7 +1010,7 @@ RoomNpcCombatUpdateResult RoomCombat::updateRoomNpcCombat(HarvesterEngine &engin
return result;
}
-bool RoomCombat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const {
+bool Combat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const {
if (!monster.roomName.equalsIgnoreCase(kInnerSanctumRoomName) ||
!monster.monsterName.equalsIgnoreCase(kMuckeyMonsterName) ||
!monster.onDeathActionTag.equalsIgnoreCase(kActivateInnerSanctumDoorActionTag)) {
@@ -1039,9 +1039,9 @@ bool RoomCombat::queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine,
return true;
}
-bool RoomCombat::beginNpcDeathTransition(HarvesterEngine &engine,
- NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const {
- RoomDeathAnimationRange deathRange;
+bool Combat::beginNpcDeathTransition(HarvesterEngine &engine,
+ 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;
@@ -1065,9 +1065,9 @@ bool RoomCombat::beginNpcDeathTransition(HarvesterEngine &engine,
return true;
}
-Common::Error RoomCombat::finalizeNpcDeathTransition(HarvesterEngine &engine,
- EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const {
+Common::Error Combat::finalizeNpcDeathTransition(HarvesterEngine &engine,
+ EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, NpcCombatState &combatState,
+ Interaction &interactionProcessor, 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
@@ -1084,7 +1084,7 @@ Common::Error RoomCombat::finalizeNpcDeathTransition(HarvesterEngine &engine,
npcEntity->setAnimationFrameRange(combatState.deathLastFrame, combatState.deathLastFrame, false);
npcEntity->setCurrentFrame(combatState.deathLastFrame);
npcEntity->setAnimationEnabled(false);
- npcEntity->setHitTestMode(kRuntimeEntityHitTestNone);
+ npcEntity->setHitTestMode(kEntityHitTestNone);
npcEntity->setVisible(true);
} else {
sceneRuntime.removeSceneEntityByName(npc.npcName);
@@ -1122,8 +1122,8 @@ Common::Error RoomCombat::finalizeNpcDeathTransition(HarvesterEngine &engine,
return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
}
-void RoomCombat::startMonsterHitReaction(HarvesterEngine &engine,
- RoomCombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, RoomMonsterCombatState *combatState,
+void Combat::startMonsterHitReaction(HarvesterEngine &engine,
+ CombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
int attackerAttackFirstFrame, bool &needsRedraw) const {
if (combatEffects.spawnCombatHitEffect(monsterEntity, monster.monsterName))
needsRedraw = true;
@@ -1134,7 +1134,7 @@ void RoomCombat::startMonsterHitReaction(HarvesterEngine &engine,
return;
}
- RoomHitAnimationRange range;
+ HitAnimationRange range;
if (!resolveRoomMonsterHitAnimationRange(monsterEntity, attackerAttackFirstFrame, range)) {
(void)playRandomRoomAttackSound(engine,
monster.hitSound1, monster.hitSound2, monster.hitSound3);
@@ -1172,9 +1172,9 @@ void RoomCombat::startMonsterHitReaction(HarvesterEngine &engine,
needsRedraw = true;
}
-Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
+Common::Error Combat::killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
RoomPlayerState &playerState, EntityManager *entityManager, const Common::Point &mousePos,
- RoomCombatEffects &combatEffects, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ CombatEffects &combatEffects, Interaction &interactionProcessor, bool &needsRedraw) {
if (!engine.isCombatDebugEnabled())
return Common::kNoError;
MonsterRecord *monster = resolveKillTarget(state, playerState, entityManager, mousePos);
@@ -1200,7 +1200,7 @@ Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSe
if (damageLanded > 0)
combatEffects.spawnCombatDamagePopup(*monsterEntity, monster->monsterName, damageLanded);
- if (playerState.attackTargetClassId == kRuntimeEntityClassMonster &&
+ if (playerState.attackTargetClassId == kEntityClassMonster &&
playerState.attackTargetName.equalsIgnoreCase(monster->monsterName)) {
playerState.attackTargetName.clear();
playerState.attackTargetClassId = -1;
@@ -1211,7 +1211,7 @@ Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSe
monster->monsterName.c_str(), damageLanded, debugDamageType, mousePos.x, mousePos.y);
const int monsterIndex = findRoomMonsterIndexByName(state, monster->monsterName);
- RoomMonsterCombatState *combatState =
+ MonsterCombatState *combatState =
monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
? &_monsterCombatStates[(uint)monsterIndex]
: nullptr;
@@ -1219,7 +1219,7 @@ Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSe
clearRoomMonsterCombatState(*combatState);
(void)script->syncRuntimeMonsterRecord(*monster);
- RoomDeathAnimationRange deathRange;
+ DeathAnimationRange deathRange;
if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
debugDamageType, engine.isGoreEnabled(), deathRange)) {
combatState->deathActive = true;
@@ -1255,10 +1255,10 @@ Common::Error RoomCombat::killDebugActiveMonster(HarvesterEngine &engine, RoomSe
return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
}
-Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
+Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
- SceneEntities &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
- RoomInteractionCallbacks &callbacks, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ SceneEntities &sceneRuntime, CombatEffects &combatEffects, const CheatSystem &cheats,
+ InteractionCallbacks &callbacks, Interaction &interactionProcessor, bool &needsRedraw) {
if (!entityManager)
return Common::kNoError;
@@ -1272,14 +1272,14 @@ Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomS
resizeCombatStates(state);
- const RoomNpcCombatUpdateResult npcResult = updateRoomNpcCombat(engine, state,
+ const NpcCombatUpdateResult npcResult = updateRoomNpcCombat(engine, state,
entityManager, sceneRuntime, interactionProcessor, needsRedraw);
if (npcResult.finalizedDeath)
return npcResult.error;
for (uint i = 0; i < state.roomMonsters.size(); ++i) {
MonsterRecord &monster = state.roomMonsters[i];
- RoomMonsterCombatState &combatState = _monsterCombatStates[i];
+ MonsterCombatState &combatState = _monsterCombatStates[i];
Entity *entity = findSceneRuntimeEntity(entityManager, monster.monsterName);
if (!entity || !monster.visible) {
clearRoomMonsterCombatState(combatState);
@@ -1540,7 +1540,7 @@ Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomS
if (!attackCooldownExpired && absLiveCenterDx > closeRangeBypassDistance)
continue;
- RoomAttackAnimationRange range;
+ 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",
@@ -1578,11 +1578,11 @@ Common::Error RoomCombat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomS
return Common::kNoError;
}
-Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
+Common::Error Combat::resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, SceneEntities &sceneRuntime,
- RoomCombatEffects &combatEffects,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+ CombatEffects &combatEffects,
+ Interaction &interactionProcessor, bool &needsRedraw) {
if (!playerState.attackActive || playerState.attackContactResolved || !playerState.entity ||
playerState.attackContactFrame < 0 ||
playerState.entity->getCurrentFrame() < playerState.attackContactFrame) {
@@ -1603,7 +1603,7 @@ Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
return Common::kNoError;
captureOverlappingNpcAttackTarget(state, playerState, entityManager);
- if (playerState.attackTargetClassId == kRuntimeEntityClassNpc) {
+ if (playerState.attackTargetClassId == kEntityClassNpc) {
Entity *npcEntity = nullptr;
NpcRecord *npc = selectNpcAttackContactTarget(state, playerState, entityManager, npcEntity);
if (!npc) {
@@ -1619,7 +1619,7 @@ Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
? script->setRuntimeFlagValue("JIMMY_ATTACKED", true)
: false;
const int npcIndex = findRoomNpcIndexByName(state, npc->npcName);
- RoomNpcCombatState *combatState =
+ NpcCombatState *combatState =
npcIndex >= 0 && npcIndex < (int)_npcCombatStates.size()
? &_npcCombatStates[(uint)npcIndex]
: nullptr;
@@ -1645,7 +1645,7 @@ Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
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());
- RoomNpcCombatState fallbackState;
+ NpcCombatState fallbackState;
fallbackState.deathDamageType = damageType;
return finalizeNpcDeathTransition(engine, entityManager, sceneRuntime, *npc, combatState ? *combatState : fallbackState, interactionProcessor, needsRedraw);
}
@@ -1667,7 +1667,7 @@ Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
monster->currentHitPoints - damageAmount);
const int damageLanded = hitPointsBefore - monster->currentHitPoints;
const int monsterIndex = findRoomMonsterIndexByName(state, monster->monsterName);
- RoomMonsterCombatState *combatState =
+ MonsterCombatState *combatState =
monsterIndex >= 0 && monsterIndex < (int)_monsterCombatStates.size()
? &_monsterCombatStates[(uint)monsterIndex]
: nullptr;
@@ -1689,7 +1689,7 @@ Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
clearRoomMonsterCombatState(*combatState);
(void)script->syncRuntimeMonsterRecord(*monster);
- RoomDeathAnimationRange deathRange;
+ DeathAnimationRange deathRange;
if (combatState && resolveMonsterDeathAnimationRange(*monsterEntity, monster->facing,
damageType, engine.isGoreEnabled(), deathRange)) {
combatState->deathActive = true;
@@ -1738,23 +1738,23 @@ Common::Error RoomCombat::resolvePlayerAttackContact(HarvesterEngine &engine,
return interactionProcessor.handleCombatInteraction(interaction, needsRedraw);
}
-void RoomCombat::initializeCombatStates(const RoomSetupState &state) {
+void Combat::initializeCombatStates(const RoomSetupState &state) {
_monsterCombatStates.resize(state.roomMonsters.size());
_npcCombatStates.resize(state.roomNpcs.size());
debugC(1, kDebugCombat, "Harvester: initialized room combat states monsters=%u npcs=%u",
_monsterCombatStates.size(), _npcCombatStates.size());
}
-void RoomCombat::resizeCombatStates(const RoomSetupState &state) {
+void Combat::resizeCombatStates(const RoomSetupState &state) {
if (_npcCombatStates.size() != state.roomNpcs.size())
_npcCombatStates.resize(state.roomNpcs.size());
if (_monsterCombatStates.size() != state.roomMonsters.size())
_monsterCombatStates.resize(state.roomMonsters.size());
}
-Common::Array<RoomMonsterCombatState> RoomCombat::prepareMonsterCombatStatesForSceneChange(
+Common::Array<MonsterCombatState> Combat::prepareMonsterCombatStatesForSceneChange(
const Common::Array<MonsterRecord> &previousMonsters, const Common::Array<MonsterRecord> &updatedMonsters) const {
- Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates;
+ Common::Array<MonsterCombatState> updatedMonsterCombatStates;
updatedMonsterCombatStates.resize(updatedMonsters.size());
for (uint i = 0; i < updatedMonsters.size(); ++i) {
const MonsterRecord &updatedMonster = updatedMonsters[i];
@@ -1772,12 +1772,12 @@ Common::Array<RoomMonsterCombatState> RoomCombat::prepareMonsterCombatStatesForS
return updatedMonsterCombatStates;
}
-void RoomCombat::commitMonsterCombatStatesForSceneChange(Common::Array<RoomMonsterCombatState> &&states) {
+void Combat::commitMonsterCombatStatesForSceneChange(Common::Array<MonsterCombatState> &&states) {
_monsterCombatStates = Common::move(states);
debugC(2, kDebugCombat, "Harvester: committed room monster combat states count=%u", _monsterCombatStates.size());
}
-void RoomCombat::resetForSceneRefresh(const RoomSetupState &state) {
+void Combat::resetForSceneRefresh(const RoomSetupState &state) {
// NPC death state survives refresh and is resized by index at the next combat update.
_monsterCombatStates.clear();
_monsterCombatStates.resize(state.roomMonsters.size());
@@ -1785,15 +1785,15 @@ void RoomCombat::resetForSceneRefresh(const RoomSetupState &state) {
_monsterCombatStates.size(), _npcCombatStates.size());
}
-void clearRoomMonsterCombatState(RoomMonsterCombatState &combatState) {
- combatState = RoomMonsterCombatState();
+void clearRoomMonsterCombatState(MonsterCombatState &combatState) {
+ combatState = MonsterCombatState();
}
-void clearRoomNpcCombatState(RoomNpcCombatState &combatState) {
- combatState = RoomNpcCombatState();
+void clearRoomNpcCombatState(NpcCombatState &combatState) {
+ combatState = NpcCombatState();
}
-void clearRoomMonsterAttackState(RoomMonsterCombatState &combatState) {
+void clearRoomMonsterAttackState(MonsterCombatState &combatState) {
combatState.attackActive = false;
combatState.attackFirstFrame = -1;
combatState.attackLastFrame = -1;
@@ -1804,7 +1804,7 @@ void clearRoomMonsterAttackState(RoomMonsterCombatState &combatState) {
combatState.attackTargetName.clear();
}
-void clearRoomMonsterHitState(RoomMonsterCombatState &combatState) {
+void clearRoomMonsterHitState(MonsterCombatState &combatState) {
combatState.hitActive = false;
combatState.hitFirstFrame = -1;
combatState.hitLastFrame = -1;
@@ -1814,35 +1814,35 @@ void clearRoomMonsterHitState(RoomMonsterCombatState &combatState) {
combatState.hitSoundPlayed = false;
}
-RoomCombatEffects::RoomCombatEffects(EntityManager *entityManager) : _entityManager(entityManager) {
+CombatEffects::CombatEffects(EntityManager *entityManager) : _entityManager(entityManager) {
debugC(1, kDebugCombat, "Harvester: created room combat effects entities=%d", _entityManager != nullptr);
}
-void RoomCombatEffects::clearHitEffects() {
+void CombatEffects::clearHitEffects() {
debugC(2, kDebugCombat, "Harvester: resetting room blood effects records=%u next_id=%u",
(uint)_hitEffectStates.size(), _nextCombatEffectId);
_hitEffectStates.clear();
_nextCombatEffectId = 0;
}
-void RoomCombatEffects::resolveHitEffectAnchor(const Entity &targetEntity, Common::Point &anchorPoint, float &renderZ) {
+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 RoomCombatEffects::resolveCombatDamagePopupAnchor(const Entity &targetEntity, Common::Point &anchorPoint) {
+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 RoomCombatEffects::spawnCombatHitEffect(Entity &sourceEntity, const Common::String &followTargetName) {
+bool CombatEffects::spawnCombatHitEffect(Entity &sourceEntity, const Common::String &followTargetName) {
if (!_entityManager)
return false;
- RoomHitEffectState effectState;
+ HitEffectState effectState;
effectState.entityName = Common::String::format("__combat_blood_%u", _nextCombatEffectId++);
effectState.followTargetName = followTargetName;
resolveHitEffectAnchor(sourceEntity, effectState.anchorPoint, effectState.renderZ);
@@ -1861,11 +1861,11 @@ bool RoomCombatEffects::spawnCombatHitEffect(Entity &sourceEntity, const Common:
return true;
}
-void RoomCombatEffects::spawnCombatDamagePopup(Entity &targetEntity, const Common::String &followTargetName, int damageAmount) {
+void CombatEffects::spawnCombatDamagePopup(Entity &targetEntity, const Common::String &followTargetName, int damageAmount) {
if (damageAmount <= 0)
return;
- RoomCombatDamagePopupState popupState;
+ CombatDamagePopupState popupState;
popupState.followTargetName = followTargetName;
popupState.startTick = Player::getRuntimeClockTicks();
popupState.damageAmount = damageAmount;
@@ -1873,12 +1873,12 @@ void RoomCombatEffects::spawnCombatDamagePopup(Entity &targetEntity, const Commo
_damagePopupStates.push_back(Common::move(popupState));
}
-bool RoomCombatEffects::syncCombatHitEffects() {
+bool CombatEffects::syncCombatHitEffects() {
if (!_entityManager)
return false;
bool changed = false;
- for (RoomHitEffectState &effectState : _hitEffectStates) {
+ for (HitEffectState &effectState : _hitEffectStates) {
Entity *effectEntity = _entityManager->findSceneEntityByName(effectState.entityName);
if (!effectEntity)
continue;
@@ -1904,7 +1904,7 @@ bool RoomCombatEffects::syncCombatHitEffects() {
return changed;
}
-void RoomCombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool combatDebugEnabled) {
+void CombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool combatDebugEnabled) {
if (!combatDebugEnabled || _damagePopupStates.empty())
return;
@@ -1917,7 +1917,7 @@ void RoomCombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool co
const byte white = findNearestPaletteColor(displayPalette, 0xff, 0xff, 0xff);
const byte black = findNearestPaletteColor(displayPalette, 0x00, 0x00, 0x00);
const uint32 now = Player::getRuntimeClockTicks();
- for (RoomCombatDamagePopupState &popupState : _damagePopupStates) {
+ for (CombatDamagePopupState &popupState : _damagePopupStates) {
if (_entityManager && !popupState.followTargetName.empty()) {
Entity *targetEntity =
_entityManager->findSceneEntityByName(popupState.followTargetName);
@@ -1941,7 +1941,7 @@ void RoomCombatEffects::drawCombatDamagePopups(Graphics::Screen &screen, bool co
}
}
-bool RoomCombatEffects::pruneCombatHitEffects() {
+bool CombatEffects::pruneCombatHitEffects() {
bool removedAny = false;
for (uint i = 0; i < _hitEffectStates.size();) {
const Common::String effectName = _hitEffectStates[i].entityName;
@@ -1971,7 +1971,7 @@ bool RoomCombatEffects::pruneCombatHitEffects() {
return removedAny;
}
-bool RoomCombatEffects::pruneCombatDamagePopups() {
+bool CombatEffects::pruneCombatDamagePopups() {
const uint32 now = Player::getRuntimeClockTicks();
bool removedAny = false;
for (uint i = 0; i < _damagePopupStates.size();) {
diff --git a/engines/harvester/room_combat.h b/engines/harvester/combat.h
similarity index 79%
rename from engines/harvester/room_combat.h
rename to engines/harvester/combat.h
index 86bf6a61d29..84a2e9bf6f3 100644
--- a/engines/harvester/room_combat.h
+++ b/engines/harvester/combat.h
@@ -19,8 +19,8 @@
*
*/
-#ifndef HARVESTER_ROOM_COMBAT_H
-#define HARVESTER_ROOM_COMBAT_H
+#ifndef HARVESTER_COMBAT_H
+#define HARVESTER_COMBAT_H
#include "common/error.h"
#include "harvester/room_support.h"
@@ -31,9 +31,9 @@ class CheatSystem;
class Entity;
class EntityManager;
class HarvesterEngine;
-class RoomCombatEffects;
-class RoomInteractionCallbacks;
-class RoomInteractionProcessor;
+class CombatEffects;
+class InteractionCallbacks;
+class Interaction;
class SceneEntities;
extern const int kRoomMonsterAnimationRate;
@@ -59,9 +59,9 @@ extern const char *const kMuckeyMonsterName;
extern const char *const kActivateInnerSanctumDoorActionTag;
extern const int kNativeDefaultNpcDeathDamageType;
-struct RoomAttackAnimationRange {
- RoomAttackAnimationRange() {}
- RoomAttackAnimationRange(int firstFrame, int lastFrame, int resumeFacing)
+struct AttackAnimationRange {
+ AttackAnimationRange() {}
+ AttackAnimationRange(int firstFrame, int lastFrame, int resumeFacing)
: firstFrame(firstFrame), lastFrame(lastFrame), resumeFacing(resumeFacing) {}
int firstFrame = 0;
@@ -69,18 +69,18 @@ struct RoomAttackAnimationRange {
int resumeFacing = 0;
};
-struct RoomDeathAnimationRange {
- RoomDeathAnimationRange() {}
- RoomDeathAnimationRange(int firstFrame, int lastFrame)
+struct DeathAnimationRange {
+ DeathAnimationRange() {}
+ DeathAnimationRange(int firstFrame, int lastFrame)
: firstFrame(firstFrame), lastFrame(lastFrame) {}
int firstFrame = -1;
int lastFrame = -1;
};
-struct RoomHitAnimationRange {
- RoomHitAnimationRange() {}
- RoomHitAnimationRange(int firstFrame, int lastFrame, int resumeFacing, int knockbackX)
+struct HitAnimationRange {
+ HitAnimationRange() {}
+ HitAnimationRange(int firstFrame, int lastFrame, int resumeFacing, int knockbackX)
: firstFrame(firstFrame), lastFrame(lastFrame), resumeFacing(resumeFacing),
knockbackX(knockbackX) {}
@@ -90,7 +90,7 @@ struct RoomHitAnimationRange {
int knockbackX = 0;
};
-struct RoomMonsterCombatState {
+struct MonsterCombatState {
bool attackActive = false;
int attackFirstFrame = -1;
int attackLastFrame = -1;
@@ -115,25 +115,25 @@ struct RoomMonsterCombatState {
uint32 attackCooldownSeedTick = 0;
};
-struct RoomNpcCombatState {
+struct NpcCombatState {
bool deathActive = false;
int deathFirstFrame = -1;
int deathLastFrame = -1;
int deathDamageType = 0;
};
-struct RoomNpcCombatUpdateResult {
+struct NpcCombatUpdateResult {
Common::Error error = Common::kNoError;
bool finalizedDeath = false;
};
-class RoomCombat {
+class Combat {
public:
void initializeCombatStates(const RoomSetupState &state);
// Keep prepared state separate until scene-entity mutations succeed.
- Common::Array<RoomMonsterCombatState> prepareMonsterCombatStatesForSceneChange(
+ Common::Array<MonsterCombatState> prepareMonsterCombatStatesForSceneChange(
const Common::Array<MonsterRecord> &previousMonsters, const Common::Array<MonsterRecord> &updatedMonsters) const;
- void commitMonsterCombatStatesForSceneChange(Common::Array<RoomMonsterCombatState> &&states);
+ void commitMonsterCombatStatesForSceneChange(Common::Array<MonsterCombatState> &&states);
void resetForSceneRefresh(const RoomSetupState &state);
void capturePlayerAttackTargetForKeyboard(RoomSetupState &state, RoomPlayerState &playerState,
@@ -151,34 +151,34 @@ public:
Common::Error killDebugActiveMonster(HarvesterEngine &engine, RoomSetupState &state,
RoomPlayerState &playerState, EntityManager *entityManager, const Common::Point &mousePos,
- RoomCombatEffects &combatEffects, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ CombatEffects &combatEffects, Interaction &interactionProcessor, bool &needsRedraw);
Common::Error updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetupState &state,
RoomPlayerState &playerState, RoomIdleAnimationState &idleState, EntityManager *entityManager,
- SceneEntities &sceneRuntime, RoomCombatEffects &combatEffects, const CheatSystem &cheats,
- RoomInteractionCallbacks &callbacks, RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ SceneEntities &sceneRuntime, CombatEffects &combatEffects, const CheatSystem &cheats,
+ InteractionCallbacks &callbacks, Interaction &interactionProcessor, bool &needsRedraw);
Common::Error resolvePlayerAttackContact(HarvesterEngine &engine,
RoomSetupState &state, RoomPlayerState &playerState,
EntityManager *entityManager, SceneEntities &sceneRuntime,
- RoomCombatEffects &combatEffects,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ CombatEffects &combatEffects,
+ Interaction &interactionProcessor, bool &needsRedraw);
private:
void resizeCombatStates(const RoomSetupState &state);
bool queueInnerSanctumHerrillDeathAfterMuck(HarvesterEngine &engine, RoomSetupState &state, const MonsterRecord &monster) const;
// Called after resizeCombatStates; NPC finalization ends the entire combat update.
- RoomNpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
+ NpcCombatUpdateResult updateRoomNpcCombat(HarvesterEngine &engine, RoomSetupState &state,
EntityManager *entityManager, SceneEntities &sceneRuntime,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ Interaction &interactionProcessor, bool &needsRedraw);
bool beginNpcDeathTransition(HarvesterEngine &engine,
- NpcRecord &npc, Entity &npcEntity, RoomNpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
+ NpcRecord &npc, Entity &npcEntity, NpcCombatState &combatState, int deathDamageType, bool &needsRedraw) const;
Common::Error finalizeNpcDeathTransition(HarvesterEngine &engine,
- EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, RoomNpcCombatState &combatState,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) const;
+ EntityManager *entityManager, SceneEntities &sceneRuntime, NpcRecord &npc, NpcCombatState &combatState,
+ Interaction &interactionProcessor, bool &needsRedraw) const;
void startMonsterHitReaction(HarvesterEngine &engine,
- RoomCombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, RoomMonsterCombatState *combatState,
+ CombatEffects &combatEffects, MonsterRecord &monster, Entity &monsterEntity, MonsterCombatState *combatState,
int attackerAttackFirstFrame, bool &needsRedraw) const;
MonsterRecord *findMonsterTargetAtPoint(RoomSetupState &state, EntityManager *entityManager, const Common::Point &point) const;
@@ -188,11 +188,11 @@ private:
MonsterRecord *findProjectileContactMonsterTarget(RoomSetupState &state, const RoomPlayerState &playerState,
EntityManager *entityManager) const;
- Common::Array<RoomMonsterCombatState> _monsterCombatStates;
- Common::Array<RoomNpcCombatState> _npcCombatStates;
+ Common::Array<MonsterCombatState> _monsterCombatStates;
+ Common::Array<NpcCombatState> _npcCombatStates;
};
-struct RoomHitEffectState {
+struct HitEffectState {
Common::String entityName;
Common::String followTargetName;
Common::Point anchorPoint;
@@ -200,16 +200,16 @@ struct RoomHitEffectState {
bool finished = false;
};
-struct RoomCombatDamagePopupState {
+struct CombatDamagePopupState {
Common::String followTargetName;
Common::Point anchorPoint;
uint32 startTick = 0;
int damageAmount = 0;
};
-class RoomCombatEffects {
+class CombatEffects {
public:
- explicit RoomCombatEffects(EntityManager *entityManager);
+ explicit CombatEffects(EntityManager *entityManager);
void clearHitEffects();
bool hasDamagePopups() const { return !_damagePopupStates.empty(); }
@@ -225,8 +225,8 @@ private:
static void resolveCombatDamagePopupAnchor(const Entity &targetEntity, Common::Point &anchorPoint);
EntityManager *_entityManager;
- Common::Array<RoomHitEffectState> _hitEffectStates;
- Common::Array<RoomCombatDamagePopupState> _damagePopupStates;
+ Common::Array<HitEffectState> _hitEffectStates;
+ Common::Array<CombatDamagePopupState> _damagePopupStates;
uint _nextCombatEffectId = 0;
};
@@ -261,11 +261,11 @@ bool runtimeEntityHasFrameRange(const Entity &entity, int firstFrame, int lastFr
bool runtimeEntityHasDrawableFrameRange(const Entity &entity, int firstFrame, int lastFrame);
bool resolveMonsterAttackAnimationRange(HarvesterEngine &engine,
const Entity &entity, int actorCenterX, int targetCenterX,
- RoomAttackAnimationRange &range);
+ AttackAnimationRange &range);
bool resolveMonsterDeathAnimationRange(const Entity &entity, int facing,
- int deathDamageType, bool goreEnabled, RoomDeathAnimationRange &range);
+ int deathDamageType, bool goreEnabled, DeathAnimationRange &range);
bool resolveNpcDeathAnimationRange(const Entity &entity, bool hasMonsterfyTarget,
- int deathDamageType, bool goreEnabled, RoomDeathAnimationRange &range);
+ int deathDamageType, bool goreEnabled, DeathAnimationRange &range);
int stepTowardsRoomCombatInt(int current, int target, int step);
int computeRuntimeEntityHorizontalGap(const Entity &left, const Entity &right);
bool doRuntimeEntityDepthExtentsOverlap(const Entity &first, const Entity &second);
@@ -277,16 +277,16 @@ bool isWithinNativeMonsterAttackEntryRange(const Entity &monster, float monsterZ
bool playRandomRoomAttackSound(HarvesterEngine &engine, const Common::String &sound1,
const Common::String &sound2, const Common::String &sound3);
bool resolveRoomMonsterHitAnimationRange(const Entity &entity,
- int attackerAttackFirstFrame, RoomHitAnimationRange &range);
+ int attackerAttackFirstFrame, HitAnimationRange &range);
float clampRoomDepthForEvent(const RoomSetupState &state, float z);
int roundRoomCombatFloat(float value);
int mapRoomDepthToScreenYForCombat(const RoomSetupState &state, float z, int fallbackY);
int resolveMonsterAttackContactFrameOffset(const MonsterRecord &monster);
-void clearRoomMonsterCombatState(RoomMonsterCombatState &combatState);
-void clearRoomNpcCombatState(RoomNpcCombatState &combatState);
-void clearRoomMonsterAttackState(RoomMonsterCombatState &combatState);
-void clearRoomMonsterHitState(RoomMonsterCombatState &combatState);
+void clearRoomMonsterCombatState(MonsterCombatState &combatState);
+void clearRoomNpcCombatState(NpcCombatState &combatState);
+void clearRoomMonsterAttackState(MonsterCombatState &combatState);
+void clearRoomMonsterHitState(MonsterCombatState &combatState);
} // End of namespace Harvester
-#endif // HARVESTER_ROOM_COMBAT_H
+#endif // HARVESTER_COMBAT_H
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 830861b16c4..259240bc17f 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -86,7 +86,7 @@
#include "harvester/npc/whaley_dialogue.h"
#include "harvester/npc/wasp_woman_dialogue.h"
#include "harvester/resources.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
#include "harvester/art.h"
#include "harvester/flow.h"
#include "harvester/text.h"
diff --git a/engines/harvester/runtime_entity.cpp b/engines/harvester/entity.cpp
similarity index 91%
rename from engines/harvester/runtime_entity.cpp
rename to engines/harvester/entity.cpp
index 509c8031693..52ef2385650 100644
--- a/engines/harvester/runtime_entity.cpp
+++ b/engines/harvester/entity.cpp
@@ -21,7 +21,7 @@
#include <math.h>
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
#include "common/algorithm.h"
#include "common/debug.h"
@@ -120,9 +120,9 @@ uint32 getRuntimeClockTicks() {
}
static bool isRectangleOnlyHitClass(int classId) {
- return classId == kRuntimeEntityClassRectHotspot ||
- classId == kRuntimeEntityClassRectHotspot18 ||
- classId == kRuntimeEntityClassRectHotspot19;
+ return classId == kEntityClassRectHotspot ||
+ classId == kEntityClassRectHotspot18 ||
+ classId == kEntityClassRectHotspot19;
}
bool Entity::loadBitmapResource(ResourceManager &resources, const Common::String &path) {
@@ -151,7 +151,7 @@ bool Entity::loadBitmapResource(ResourceManager &resources, const Common::String
_drawEnabled = true;
_depthScale = 1.0f;
updateBoundsFromCurrentFrame();
- _hitTestMode = kRuntimeEntityHitTestOpaquePixels;
+ _hitTestMode = kEntityHitTestOpaquePixels;
return true;
}
@@ -178,7 +178,7 @@ bool Entity::loadAbmResource(ResourceManager &resources, const Common::String &p
_drawEnabled = true;
_depthScale = 1.0f;
updateBoundsFromCurrentFrame();
- _hitTestMode = kRuntimeEntityHitTestOpaquePixels;
+ _hitTestMode = kEntityHitTestOpaquePixels;
return true;
}
@@ -194,7 +194,7 @@ void Entity::setPosition(int x, int y, float z) {
if (anchorChanged)
updateScreenBaseFromCurrentFrame();
- if ((anchorChanged || previousZ != z) && _classId == kRuntimeEntityClassNpc) {
+ if ((anchorChanged || previousZ != z) && _classId == kEntityClassNpc) {
int width = 0;
int height = 0;
int xOffset = 0;
@@ -210,7 +210,7 @@ void Entity::setPosition(int x, int y, float z) {
}
}
-void Entity::setAnchorMode(RuntimeEntityAnchorMode anchorMode) {
+void Entity::setAnchorMode(EntityAnchorMode anchorMode) {
_anchorMode = anchorMode;
updateScreenBaseFromCurrentFrame();
}
@@ -225,7 +225,7 @@ void Entity::setAnimationRate(int rate) {
if (rate != _animationRate) {
_nextAnimationTick = 0;
_animationRate = rate;
- if (_classId == kRuntimeEntityClassNpc) {
+ if (_classId == kEntityClassNpc) {
debugC(2, kDebugPlayer,
"Harvester: npc animation rate npc='%s' rate=%d->%d interval_ticks=%u next_tick=%u",
_name.c_str(), previousRate, _animationRate,
@@ -237,7 +237,7 @@ void Entity::setAnimationRate(int rate) {
void Entity::setAnimationEnabled(bool enabled) {
const bool wasEnabled = _animationEnabled;
_animationEnabled = enabled && !_frames.empty() && _currentFrame >= 0;
- if (wasEnabled != _animationEnabled && _classId == kRuntimeEntityClassNpc) {
+ if (wasEnabled != _animationEnabled && _classId == kEntityClassNpc) {
debugC(2, kDebugPlayer,
"Harvester: npc animation enabled npc='%s' enabled=%d->%d frame=%d range=%d..%d",
_name.c_str(), wasEnabled, _animationEnabled,
@@ -276,7 +276,7 @@ void Entity::setAnimationFrameRange(int firstFrame, int lastFrame, bool looping)
else
updateBoundsFromCurrentFrame();
- if (_classId == kRuntimeEntityClassNpc) {
+ if (_classId == kEntityClassNpc) {
debugC(2, kDebugPlayer,
"Harvester: npc animation range npc='%s' frames=%d..%d->%d..%d current=%d looping=%d->%d enabled=%d",
_name.c_str(), previousFirstFrame, previousLastFrame,
@@ -297,7 +297,7 @@ void Entity::setAnimationSequence(int sequence) {
_lastFrame = MIN<int>(_firstFrame + kFramesPerSequence - 1, (int)_frames.size() - 1);
advanceAnimationFrame(_firstFrame);
- if (_classId == kRuntimeEntityClassCursor) {
+ if (_classId == kEntityClassCursor) {
debugC(1, kDebugCursor,
"Harvester: cursor animation sequence=%d frames=%d..%d current=%d",
_animationSequence, _firstFrame, _lastFrame, _currentFrame);
@@ -315,7 +315,7 @@ void Entity::configureHotspotBounds(int width, int height) {
_drawEnabled = false;
_boundsWidth = MAX(width, 1);
_boundsHeight = MAX(height, 1);
- _hitTestMode = kRuntimeEntityHitTestBounds;
+ _hitTestMode = kEntityHitTestBounds;
_depthScale = 1.0f;
updateScreenBaseFromCurrentFrame();
}
@@ -389,14 +389,14 @@ bool Entity::tickVisualState(uint32 now) {
_nextAnimationTick = now + _animationTickInterval;
_animationAdvancedLastTick = true;
- if (_classId == kRuntimeEntityClassPlayer && previousFrameIndex != _currentFrame) {
+ if (_classId == kEntityClassPlayer && previousFrameIndex != _currentFrame) {
debugC(4, kDebugPlayer,
"Harvester: player animation advance frame=%d->%d range=%d..%d rate=%d tick=%u interval=%u next_tick=%u",
previousFrameIndex, _currentFrame, _firstFrame, _lastFrame,
_animationRate, now, _animationTickInterval, _nextAnimationTick);
}
- if (_classId == kRuntimeEntityClassNpc) {
+ if (_classId == kEntityClassNpc) {
const AbmFrame ¤tFrame = _frames[(uint)_currentFrame];
const Common::Point drawOrigin = getDrawOrigin();
const bool loopReset = _looping && !_pingPong &&
@@ -443,12 +443,12 @@ void Entity::configureTimerCountdown(int initialValue, int currentValue,
_visible = false;
_drawEnabled = false;
_animationEnabled = false;
- _hitTestMode = kRuntimeEntityHitTestNone;
+ _hitTestMode = kEntityHitTestNone;
setTimerEnabled(enabled);
}
void Entity::setTimerEnabled(bool enabled) {
- if (_classId != kRuntimeEntityClassTimer)
+ if (_classId != kEntityClassTimer)
return;
if (!enabled) {
@@ -471,7 +471,7 @@ void Entity::setTimerEnabled(bool enabled) {
}
bool Entity::tickTimerState(uint32 now, Common::Array<Common::String> &expiredTimerNames) {
- if (_classId != kRuntimeEntityClassTimer || !_timerEnabled || _timerPaused)
+ if (_classId != kEntityClassTimer || !_timerEnabled || _timerPaused)
return false;
if (_timerNextFireTick != 0 && (int32)(now - _timerNextFireTick) < 0) {
@@ -496,7 +496,7 @@ bool Entity::tickTimerState(uint32 now, Common::Array<Common::String> &expiredTi
}
void Entity::pauseTimerCountdown(uint32 now) {
- if (_classId != kRuntimeEntityClassTimer || !_timerEnabled || _timerPaused)
+ if (_classId != kEntityClassTimer || !_timerEnabled || _timerPaused)
return;
if (_timerNextFireTick != 0 && (int32)(now - _timerNextFireTick) < 0) {
@@ -511,7 +511,7 @@ void Entity::pauseTimerCountdown(uint32 now) {
}
void Entity::resumeTimerCountdown(uint32 now) {
- if (_classId != kRuntimeEntityClassTimer || !_timerEnabled || !_timerPaused)
+ if (_classId != kEntityClassTimer || !_timerEnabled || !_timerPaused)
return;
if (_timerNextFireTick != 0)
@@ -525,7 +525,7 @@ void Entity::draw(Graphics::Screen &screen) const {
return;
const Common::Point drawOrigin = getDrawOrigin();
- if (_classId == kRuntimeEntityClassPlayer) {
+ if (_classId == kEntityClassPlayer) {
debugC(4, kDebugPlayer,
"Harvester: player animation draw frame=%d pos=(%d,%d,z=%.2f) origin=(%d,%d) tick=%u",
_currentFrame, _x, _y, _z, drawOrigin.x, drawOrigin.y, getRuntimeClockTicks());
@@ -568,17 +568,17 @@ bool Entity::isOpaqueAt(const Common::Point &point) const {
bool Entity::hitTest(const Common::Point &point) const {
if (!_visible)
return false;
- if (_classId == kRuntimeEntityClassDisabledHotspot)
+ if (_classId == kEntityClassDisabledHotspot)
return false;
- if (_classId == kRuntimeEntityClassBackground)
+ if (_classId == kEntityClassBackground)
return true;
- if (_hitTestMode == kRuntimeEntityHitTestNone)
+ if (_hitTestMode == kEntityHitTestNone)
return false;
const Common::Rect bounds = getFrameRect();
if (!bounds.contains(point))
return false;
- if (_hitTestMode != kRuntimeEntityHitTestOpaquePixels || isRectangleOnlyHitClass(_classId))
+ if (_hitTestMode != kEntityHitTestOpaquePixels || isRectangleOnlyHitClass(_classId))
return true;
return isOpaqueAt(point);
@@ -587,10 +587,10 @@ bool Entity::hitTest(const Common::Point &point) const {
bool Entity::overlapsEntity(const Entity &other) const {
if (this == &other || !_visible || !other._visible)
return false;
- if (_classId == kRuntimeEntityClassCursor || _classId == kRuntimeEntityClassBackground ||
- _classId == kRuntimeEntityClassRectHotspot || _classId == kRuntimeEntityClassRectHotspot19 ||
- other._classId == kRuntimeEntityClassCursor || other._classId == kRuntimeEntityClassBackground ||
- other._classId == kRuntimeEntityClassRectHotspot || other._classId == kRuntimeEntityClassRectHotspot19) {
+ if (_classId == kEntityClassCursor || _classId == kEntityClassBackground ||
+ _classId == kEntityClassRectHotspot || _classId == kEntityClassRectHotspot19 ||
+ other._classId == kEntityClassCursor || other._classId == kEntityClassBackground ||
+ other._classId == kEntityClassRectHotspot || other._classId == kEntityClassRectHotspot19) {
return false;
}
@@ -607,8 +607,8 @@ bool Entity::overlapsEntity(const Entity &other) const {
const int bottom = MIN(thisRect.bottom, otherRect.bottom);
if (left >= right || top >= bottom)
return false;
- if (_classId == kRuntimeEntityClassDisabledHotspot ||
- other._classId == kRuntimeEntityClassDisabledHotspot) {
+ if (_classId == kEntityClassDisabledHotspot ||
+ other._classId == kEntityClassDisabledHotspot) {
return true;
}
if (!hasOpaqueFrame() || !other.hasOpaqueFrame())
@@ -668,7 +668,7 @@ void Entity::advanceAnimationFrame(int directive) {
if (!_looping) {
_currentFrame = _firstFrame;
- if (_classId == kRuntimeEntityClassAnimation)
+ if (_classId == kEntityClassAnimation)
_animationEnabled = false;
goto done;
}
@@ -690,7 +690,7 @@ void Entity::advanceAnimationFrame(int directive) {
if (!_looping) {
_currentFrame = _lastFrame;
- if (_classId == kRuntimeEntityClassAnimation)
+ if (_classId == kEntityClassAnimation)
_animationEnabled = false;
goto done;
}
@@ -728,7 +728,7 @@ void Entity::updateScreenBaseFromCurrentFrame() {
int baseX = _x;
int baseY = _y;
- if (_anchorMode == kRuntimeEntityAnchorCentered) {
+ if (_anchorMode == kEntityAnchorCentered) {
baseX -= _boundsWidth / 2;
baseY -= _boundsHeight / 2;
}
@@ -781,7 +781,7 @@ void EntityManager::clearSceneEntities(bool preserveGlobalTimers) {
Common::Array<Entity *> preservedEntities;
for (Entity *entity : _sceneEntities) {
if (preserveGlobalTimers &&
- entity->getClassId() == kRuntimeEntityClassTimer &&
+ entity->getClassId() == kEntityClassTimer &&
entity->isTimerGlobal()) {
preservedEntities.push_back(entity);
continue;
@@ -831,11 +831,11 @@ Entity *EntityManager::spawnCursorEntity(const Common::Point &position) {
return _cursorEntity;
_cursorEntity = spawnAbmEntityFromResource(kCursorEntityName, kCursorResourcePath,
- kRuntimeEntityClassCursor, position, kCursorEntityZ, kCursorAnimationRate, true, false);
+ kEntityClassCursor, position, kCursorEntityZ, kCursorAnimationRate, true, false);
if (_cursorEntity)
_cursorEntity->setAnimationSequence(0);
if (_cursorEntity)
- _cursorEntity->setHitTestMode(kRuntimeEntityHitTestNone);
+ _cursorEntity->setHitTestMode(kEntityHitTestNone);
if (_cursorEntity) {
const uint32 animationInterval = _cursorEntity->getAnimationRate() == 0 ? 0 :
(100U / (uint32)_cursorEntity->getAnimationRate());
@@ -851,7 +851,7 @@ Entity *EntityManager::spawnCursorEntity(const Common::Point &position) {
Entity *EntityManager::spawnSceneBitmapEntity(const Common::String &name,
const Common::String &resourcePath, const Common::Point &position, float z) {
- Entity *entity = spawnBitmapEntityFromResource(name, resourcePath, kRuntimeEntityClassObject,
+ Entity *entity = spawnBitmapEntityFromResource(name, resourcePath, kEntityClassObject,
position, z);
if (entity)
insertSceneEntity(entity);
@@ -862,7 +862,7 @@ Entity *EntityManager::spawnSceneHotspotEntity(const Common::String &name,
const Common::Rect &bounds, float z) {
Entity *entity = new Entity();
entity->setName(name);
- entity->setClassId(kRuntimeEntityClassObject);
+ entity->setClassId(kEntityClassObject);
entity->setPosition(bounds.left, bounds.top, z);
entity->configureHotspotBounds(bounds.width(), bounds.height());
insertSceneEntity(entity);
@@ -872,7 +872,7 @@ Entity *EntityManager::spawnSceneHotspotEntity(const Common::String &name,
Entity *EntityManager::spawnSceneAnimationEntity(const Common::String &name,
const Common::String &resourcePath, const Common::Point &position, float z, int animationRate,
bool active, bool visible, bool looping, bool playBackwards, bool pingPong, int initialFrame) {
- Entity *entity = spawnAbmEntityFromResource(name, resourcePath, kRuntimeEntityClassAnimation,
+ Entity *entity = spawnAbmEntityFromResource(name, resourcePath, kEntityClassAnimation,
position, z, animationRate, looping, pingPong);
if (!entity)
return nullptr;
@@ -881,12 +881,12 @@ Entity *EntityManager::spawnSceneAnimationEntity(const Common::String &name,
// x/y pivot, lower the render-list depth anchor by half z_extent, and then
// keep that cached screen base while later frame ABM x/y offsets shift the
// final draw rect.
- entity->setAnchorMode(kRuntimeEntityAnchorCentered);
+ entity->setAnchorMode(kEntityAnchorCentered);
entity->setZExtent(3.0f);
entity->setPosition(position.x, position.y, z - floorf(MAX<float>(entity->getZExtent(), 0.0f) * 0.5f));
entity->setVisible(visible);
entity->setPlayBackwards(playBackwards);
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setHitTestMode(kEntityHitTestNone);
int startFrame = playBackwards ? entity->getLastFrame() : 0;
if (!playBackwards && initialFrame >= 0)
startFrame = CLIP<int>(initialFrame, 0, entity->getLastFrame());
@@ -903,14 +903,14 @@ Entity *EntityManager::spawnSceneAnimationEntity(const Common::String &name,
Entity *EntityManager::spawnSceneActorEntity(const Common::String &name,
const Common::String &resourcePath, const Common::Point &position, float z, int initialFrame) {
- Entity *entity = spawnAbmEntityFromResource(name, resourcePath, kRuntimeEntityClassObject,
+ Entity *entity = spawnAbmEntityFromResource(name, resourcePath, kEntityClassObject,
position, z, 0, false, false);
if (!entity)
return nullptr;
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setAnchorMode(kEntityAnchorTopLeft);
entity->setZExtent(3.0f);
- entity->setHitTestMode(kRuntimeEntityHitTestOpaquePixels);
+ entity->setHitTestMode(kEntityHitTestOpaquePixels);
entity->setCurrentFrame(initialFrame);
debugC(1, kDebugScene,
"Harvester: spawned scene actor '%s' resource='%s' frames=0..%d initial_frame=%d animation_rate=%d sequence=%d pos=(%d,%d,z=%.2f)",
@@ -925,7 +925,7 @@ Entity *EntityManager::spawnSceneTimerEntity(const Common::String &name,
int initialValue, int currentValue, bool enabled, bool looping, bool global) {
Entity *entity = new Entity();
entity->setName(name);
- entity->setClassId(kRuntimeEntityClassTimer);
+ entity->setClassId(kEntityClassTimer);
configureSceneTimerEntity(*entity, initialValue, currentValue, enabled, looping, global);
insertSceneEntity(entity);
return entity;
@@ -982,7 +982,7 @@ bool EntityManager::tickSceneEntities() {
bool changed = false;
for (Entity *entity : _sceneEntities) {
- if (entity->getClassId() == kRuntimeEntityClassTimer) {
+ if (entity->getClassId() == kEntityClassTimer) {
if (_timerPauseDepth == 0)
(void)entity->tickTimerState(now, _expiredTimerNames);
continue;
diff --git a/engines/harvester/runtime_entity.h b/engines/harvester/entity.h
similarity index 88%
rename from engines/harvester/runtime_entity.h
rename to engines/harvester/entity.h
index 5a7f0c2186c..8fbff46d5de 100644
--- a/engines/harvester/runtime_entity.h
+++ b/engines/harvester/entity.h
@@ -19,8 +19,8 @@
*
*/
-#ifndef HARVESTER_RUNTIME_ENTITY_H
-#define HARVESTER_RUNTIME_ENTITY_H
+#ifndef HARVESTER_ENTITY_H
+#define HARVESTER_ENTITY_H
#include "common/array.h"
#include "common/rect.h"
@@ -37,30 +37,30 @@ class ResourceManager;
uint32 getRuntimeClockTicks();
-enum RuntimeEntityClass {
- kRuntimeEntityClassObject = 0,
- kRuntimeEntityClassAnimation = 1,
- kRuntimeEntityClassCursor = 2,
- kRuntimeEntityClassBackground = 3,
- kRuntimeEntityClassNpc = 4,
- kRuntimeEntityClassPlayer = 5,
- kRuntimeEntityClassMonster = 6,
- kRuntimeEntityClassRectHotspot = 0x15,
- kRuntimeEntityClassDisabledHotspot = 0x16,
- kRuntimeEntityClassTimer = 0x17,
- kRuntimeEntityClassRectHotspot18 = 0x18,
- kRuntimeEntityClassRectHotspot19 = 0x19
+enum EntityClass {
+ kEntityClassObject = 0,
+ kEntityClassAnimation = 1,
+ kEntityClassCursor = 2,
+ kEntityClassBackground = 3,
+ kEntityClassNpc = 4,
+ kEntityClassPlayer = 5,
+ kEntityClassMonster = 6,
+ kEntityClassRectHotspot = 0x15,
+ kEntityClassDisabledHotspot = 0x16,
+ kEntityClassTimer = 0x17,
+ kEntityClassRectHotspot18 = 0x18,
+ kEntityClassRectHotspot19 = 0x19
};
-enum RuntimeEntityHitTestMode {
- kRuntimeEntityHitTestNone = 0,
- kRuntimeEntityHitTestBounds = 1,
- kRuntimeEntityHitTestOpaquePixels = 2
+enum EntityHitTestMode {
+ kEntityHitTestNone = 0,
+ kEntityHitTestBounds = 1,
+ kEntityHitTestOpaquePixels = 2
};
-enum RuntimeEntityAnchorMode {
- kRuntimeEntityAnchorTopLeft = 0,
- kRuntimeEntityAnchorCentered = 1
+enum EntityAnchorMode {
+ kEntityAnchorTopLeft = 0,
+ kEntityAnchorCentered = 1
};
class Entity {
@@ -73,7 +73,7 @@ public:
void setClassId(int classId) { _classId = classId; }
int getClassId() const { return _classId; }
- void setAnchorMode(RuntimeEntityAnchorMode anchorMode);
+ void setAnchorMode(EntityAnchorMode anchorMode);
void setZExtent(float zExtent) { _zExtent = zExtent; }
float getZExtent() const { return _zExtent; }
@@ -101,7 +101,7 @@ public:
void setAnimationSequence(int sequence);
int getAnimationSequence() const { return _animationSequence; }
void configureHotspotBounds(int width, int height);
- void setHitTestMode(RuntimeEntityHitTestMode mode) { _hitTestMode = mode; }
+ void setHitTestMode(EntityHitTestMode mode) { _hitTestMode = mode; }
int getBoundsWidth() const { return _boundsWidth; }
int getBoundsHeight() const { return _boundsHeight; }
bool getCurrentFrameMetrics(int &width, int &height, int &xOffset, int &yOffset) const;
@@ -165,8 +165,8 @@ private:
bool _drawEnabled = true;
int _boundsWidth = 0;
int _boundsHeight = 0;
- RuntimeEntityHitTestMode _hitTestMode = kRuntimeEntityHitTestNone;
- RuntimeEntityAnchorMode _anchorMode = kRuntimeEntityAnchorTopLeft;
+ EntityHitTestMode _hitTestMode = kEntityHitTestNone;
+ EntityAnchorMode _anchorMode = kEntityAnchorTopLeft;
float _zExtent = 0.0f;
float _depthScale = 1.0f;
int _timerInitialValue = 0;
@@ -234,4 +234,4 @@ private:
} // End of namespace Harvester
-#endif // HARVESTER_RUNTIME_ENTITY_H
+#endif // HARVESTER_ENTITY_H
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index c3fbb62dbda..a90f0af6792 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -42,7 +42,7 @@
#include "harvester/harvester.h"
#include "harvester/palette_utils.h"
#include "harvester/resources.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
#include "harvester/scene_entities.h"
#include "harvester/art.h"
#include "harvester/monster.h"
@@ -233,7 +233,7 @@ static void drawRoomDebugOverlay(HarvesterEngine &engine, Graphics::Screen &scre
const Entity *entity = entityManager
? entityManager->findSceneEntityByName(object.objectName)
: nullptr;
- if (entity && entity->getClassId() == kRuntimeEntityClassBackground)
+ if (entity && entity->getClassId() == kEntityClassBackground)
continue;
drawRoomDebugLabel(screen, *font, resolveRoomDebugObjectLabel(engine, object),
@@ -244,7 +244,7 @@ static void drawRoomDebugOverlay(HarvesterEngine &engine, Graphics::Screen &scre
const Entity *entity = entityManager
? entityManager->findSceneEntityByName(npc.npcName)
: nullptr;
- if (!entity || entity->getClassId() != kRuntimeEntityClassNpc || !entity->isVisible())
+ if (!entity || entity->getClassId() != kEntityClassNpc || !entity->isVisible())
continue;
const Common::Rect npcRect = entity->getScreenRect();
@@ -272,7 +272,7 @@ static void drawCombatDebugOverlay(HarvesterEngine &engine, Graphics::Screen &sc
const Entity *playerEntity = entityManager->findSceneEntityByName(kPlayerActorEntityName);
if (script && playerEntity &&
- playerEntity->getClassId() == kRuntimeEntityClassPlayer &&
+ playerEntity->getClassId() == kEntityClassPlayer &&
playerEntity->isVisible()) {
const Common::Rect playerRect = playerEntity->getScreenRect();
drawRoomDebugLabel(screen, *font,
@@ -283,7 +283,7 @@ static void drawCombatDebugOverlay(HarvesterEngine &engine, Graphics::Screen &sc
for (const MonsterRecord &monster : scene.state.roomMonsters) {
const Entity *entity = entityManager->findSceneEntityByName(monster.monsterName);
- if (!entity || entity->getClassId() != kRuntimeEntityClassMonster || !entity->isVisible())
+ if (!entity || entity->getClassId() != kEntityClassMonster || !entity->isVisible())
continue;
const Common::Rect monsterRect = entity->getScreenRect();
@@ -315,18 +315,18 @@ static bool canEntityAffectPlayerPathfinding(const Entity &entity) {
return false;
switch (entity.getClassId()) {
- case kRuntimeEntityClassCursor:
- case kRuntimeEntityClassBackground:
- case kRuntimeEntityClassPlayer:
- case kRuntimeEntityClassRectHotspot:
- case kRuntimeEntityClassRectHotspot19:
- case kRuntimeEntityClassTimer:
+ case kEntityClassCursor:
+ case kEntityClassBackground:
+ case kEntityClassPlayer:
+ case kEntityClassRectHotspot:
+ case kEntityClassRectHotspot19:
+ case kEntityClassTimer:
return false;
default:
break;
}
- return entity.getClassId() == kRuntimeEntityClassDisabledHotspot || entity.hasFrames();
+ return entity.getClassId() == kEntityClassDisabledHotspot || entity.hasFrames();
}
static void drawPathfindingBlockerOutline(Graphics::Screen &screen, EntityManager &entityManager,
@@ -397,7 +397,7 @@ static void drawTimerDebugOverlay(HarvesterEngine &engine, Graphics::Screen &scr
Common::Array<Common::String> labels;
for (const TimerRecord &timer : script->getTimers()) {
const Entity *entity = entityManager->findSceneEntityByName(timer.timerName);
- if (!entity || entity->getClassId() != kRuntimeEntityClassTimer || !entity->isTimerEnabled())
+ if (!entity || entity->getClassId() != kEntityClassTimer || !entity->isTimerEnabled())
continue;
const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(timer.timerName);
@@ -529,7 +529,7 @@ static bool applyRoomActorPlacementInternal(const RoomSetupState &state, Entity
return false;
const float renderZ = computeRoomActorRenderZ(z, entity);
- entity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity.setAnchorMode(kEntityAnchorTopLeft);
setRoomActorScreenPosition(entity, centerX, bottomY, renderZ, width, height, xOffset, yOffset);
float depthScale = 1.0f;
@@ -881,9 +881,9 @@ static const ObjectRecord *findRoomObjectAtPoint(HarvesterEngine &engine,
const Entity *entity = entityManager->findSceneEntityByName(object.objectName);
if (!entity || !entity->hitTest(point))
continue;
- if (entity->getClassId() == kRuntimeEntityClassBackground ||
- entity->getClassId() == kRuntimeEntityClassPlayer ||
- entity->getClassId() == kRuntimeEntityClassRectHotspot19) {
+ if (entity->getClassId() == kEntityClassBackground ||
+ entity->getClassId() == kEntityClassPlayer ||
+ entity->getClassId() == kEntityClassRectHotspot19) {
continue;
}
const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
@@ -917,7 +917,7 @@ static const RegionRecord *findRoomRegionAtPoint(HarvesterEngine &engine,
const Entity *entity = entityManager->findSceneEntityByName(region.regionName);
if (!entity || !entity->hitTest(point))
continue;
- if (entity->getClassId() != kRuntimeEntityClassRectHotspot19)
+ if (entity->getClassId() != kEntityClassRectHotspot19)
continue;
const int drawIndex = entityManager->findSceneEntityDrawIndexByName(entity->getName());
if (drawIndex < 0)
@@ -939,7 +939,7 @@ static const Entity *findRoomPlayerAtPoint(HarvesterEngine &engine, const Common
return nullptr;
const Entity *entity = entityManager->findTopSceneEntityAt(point);
- if (!entity || entity->getClassId() != kRuntimeEntityClassPlayer)
+ if (!entity || entity->getClassId() != kEntityClassPlayer)
return nullptr;
return entity;
@@ -953,7 +953,7 @@ static const NpcRecord *findRoomNpcAtPoint(HarvesterEngine &engine,
return nullptr;
const Entity *entity = entityManager->findTopSceneEntityAt(point);
- if (!entity || entity->getClassId() != kRuntimeEntityClassNpc)
+ if (!entity || entity->getClassId() != kEntityClassNpc)
return nullptr;
const NpcRecord *npc = findSceneNpcByName(sceneNpcs, entity->getName());
@@ -2148,7 +2148,7 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
sceneRuntime.populateRoomRegions();
for (const TimerRecord &timer : state.roomTimers) {
Entity *existingTimer = entityManager->findSceneEntityByName(timer.timerName);
- if (existingTimer && existingTimer->getClassId() == kRuntimeEntityClassTimer) {
+ 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(),
@@ -2197,9 +2197,9 @@ bool Flow::populateRoomSceneEntities(RoomSetupState &state,
if (!player) {
debug(1, "Harvester: unable to spawn startup player actor from '%s'", playerResourcePath.c_str());
} else {
- player->setClassId(kRuntimeEntityClassPlayer);
- player->setAnchorMode(kRuntimeEntityAnchorTopLeft);
- player->setHitTestMode(kRuntimeEntityHitTestOpaquePixels);
+ player->setClassId(kEntityClassPlayer);
+ player->setAnchorMode(kEntityAnchorTopLeft);
+ player->setHitTestMode(kEntityHitTestOpaquePixels);
player->setVisible(true);
player->setAnimationRate(0);
player->setAnimationFrameRange(playerFrame, playerFrame, false);
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index 78a204857a0..f5d2157cfc8 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -41,7 +41,7 @@ namespace Harvester {
class HarvesterEngine;
class DialogueFlowAccess;
-class RoomInteractionProcessor;
+class Interaction;
struct IndexedBitmap;
class Flow {
@@ -61,9 +61,9 @@ private:
friend class DialogueSystem;
friend class InventorySystem;
friend class MenuSystem;
- friend class RoomInteractionProcessor;
+ friend class Interaction;
friend class SceneRunner;
- friend class RoomInput;
+ friend class Input;
friend class Scene;
bool loadQuickTips();
diff --git a/engines/harvester/room_input.cpp b/engines/harvester/input.cpp
similarity index 86%
rename from engines/harvester/room_input.cpp
rename to engines/harvester/input.cpp
index 976719fb2b2..f55c8c14b3b 100644
--- a/engines/harvester/room_input.cpp
+++ b/engines/harvester/input.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/room_input.h"
+#include "harvester/input.h"
#include "common/debug.h"
#include "graphics/screen.h"
@@ -30,7 +30,7 @@
#include "harvester/harvester.h"
#include "harvester/inventory.h"
#include "harvester/player.h"
-#include "harvester/room_interaction.h"
+#include "harvester/interaction.h"
#include "harvester/room_presentation.h"
#include "harvester/scene.h"
#include "harvester/room_support.h"
@@ -75,9 +75,9 @@ static void debugLogInventoryClick(const char *buttonLabel, const Common::Point
InventorySystem::isStatusObject(inventoryHover->object));
}
-RoomInput::RoomInput(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
+Input::Input(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
InventorySystem &inventory, CheatSystem &cheats,
- RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
+ ItemInteractions &roomItems, Interaction &interactionProcessor,
RoomPresentationState &presentationState,
const Art &art, bool canExitCloseupToParent)
: _engine(engine), _flow(flow), _session(session), _scene(scene),
@@ -86,7 +86,7 @@ RoomInput::RoomInput(HarvesterEngine &engine, Flow &flow, Scene &session, SceneS
_canExitCloseupToParent(canExitCloseupToParent) {
}
-void RoomInput::beginInspectText(const ObjectRecord &object, const ResolvedText &text) {
+void Input::beginInspectText(const ObjectRecord &object, const ResolvedText &text) {
_presentationState.inspectText = text;
_presentationState.showingInspectText = true;
_presentationState.inspectCanDismiss = false;
@@ -96,7 +96,7 @@ void RoomInput::beginInspectText(const ObjectRecord &object, const ResolvedText
_presentationState.inspectText.boxName.c_str(), _presentationState.inspectText.value.c_str());
}
-void RoomInput::queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw) {
+void Input::queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw) {
if (message.empty()) {
const bool hadMessage = !_presentationState.combatLoadoutStatusText.value.empty();
_presentationState.combatLoadoutStatusText = ResolvedText();
@@ -113,9 +113,9 @@ void RoomInput::queueCombatLoadoutStatusMessage(const Common::String &message, b
needsRedraw = true;
}
-bool RoomInput::openInventoryOverlay() {
+bool Input::openInventoryOverlay() {
RoomPlayerState &playerState = _session.getPlayerState();
- RoomInputState &inputState = _session.getInputState();
+ InputState &inputState = _session.getInputState();
inputState.moveLeft = false;
inputState.moveRight = false;
@@ -131,7 +131,7 @@ bool RoomInput::openInventoryOverlay() {
return _inventory.open();
}
-void RoomInput::capturePlayerAttackTarget() {
+void Input::capturePlayerAttackTarget() {
RoomPlayerState &playerState = _session.getPlayerState();
playerState.attackTargetName.clear();
@@ -143,7 +143,7 @@ void RoomInput::capturePlayerAttackTarget() {
_session.capturePlayerAttackTarget(_flow._mousePos, hoverState.npc);
}
-void RoomInput::handleMouseMove(bool &needsRedraw) {
+void Input::handleMouseMove(bool &needsRedraw) {
if (_inventory.isOpen() && _inventory.hasSelection()) {
const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
// Native inventory handoff is driven by the latched selected item, not by
@@ -156,7 +156,7 @@ void RoomInput::handleMouseMove(bool &needsRedraw) {
needsRedraw = true;
}
-RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
+InputResult Input::handleRightButtonDown(bool &needsRedraw) {
RoomPlayerState &playerState = _session.getPlayerState();
RoomIdleAnimationState &idleState = _session.getIdleState();
@@ -164,10 +164,10 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
if (idleState.active || idleState.exiting) {
if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (_session.isPlayerCombatLocked())
- return RoomInputResult();
+ return InputResult();
_session.clearPendingRegionInteraction();
if (_inventory.isOpen()) {
const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow._mousePos);
@@ -178,7 +178,7 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
_inventory.getSelectedItemName().c_str());
if (_inventory.clearSelection())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (inventoryHover &&
!InventorySystem::isExitObject(inventoryHover->object) &&
@@ -210,10 +210,10 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
_engine.getScript()->getPlayerCombatLoadout();
}
if (!_inventory.refresh())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
}
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
InventorySecondaryAction secondaryAction;
@@ -243,21 +243,21 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
secondaryAction.actionTag.c_str(), interactionError.getCode(),
didTransition, _flow.hasPendingMainMenuReturn());
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
}
if (_inventory.isOpen()) {
if (!_inventory.refresh())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
} else {
debugC(1, kDebugInventory,
"Harvester: inventory right click skipped overlay refresh after action='%s' transitioned=%d closeInventory=%d",
secondaryAction.actionTag.c_str(), didTransition, secondaryAction.closeInventory);
}
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
debugC(1, kDebugInventory,
"Harvester: inventory right click object='%s' produced no combat toggle or secondary action",
@@ -266,18 +266,18 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
debugC(1, kDebugInventory, "Harvester: inventory right click closing overlay");
if (_inventory.close())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (_inventory.hasSelection()) {
if (_inventory.clearSelection())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (_roomItems.hasCarriedRoomItem()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
Script *script = _engine.getScript();
if (!_presentationState.showingInspectText && !_session.isPlayerCombatLocked() && script && playerState.entity &&
@@ -307,21 +307,21 @@ RoomInputResult RoomInput::handleRightButtonDown(bool &needsRedraw) {
playerState.attackTargetClassId = -1;
}
}
- return RoomInputResult();
+ return InputResult();
}
-void RoomInput::handleLeftButtonUp() {
- RoomInputState &inputState = _session.getInputState();
+void Input::handleLeftButtonUp() {
+ InputState &inputState = _session.getInputState();
inputState.lastLeftButtonReleaseTick = Player::getRuntimeClockTicks();
if (_presentationState.showingInspectText)
_presentationState.inspectCanDismiss = true;
}
-RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
+InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
RoomPlayerState &playerState = _session.getPlayerState();
RoomIdleAnimationState &idleState = _session.getIdleState();
- RoomInputState &inputState = _session.getInputState();
+ InputState &inputState = _session.getInputState();
const uint32 now = Player::getRuntimeClockTicks();
const bool isFastExitClick =
@@ -331,7 +331,7 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
if (idleState.active || idleState.exiting) {
if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (_presentationState.showingInspectText) {
@@ -341,10 +341,10 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
_presentationState.inspectText = ResolvedText();
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
if (_session.isPlayerCombatLocked())
- return RoomInputResult();
+ return InputResult();
const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
if (_inventory.isOpen()) {
@@ -357,10 +357,10 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
const bool closedInventory = _inventory.close();
if (clearedSelection || closedInventory)
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (InventorySystem::isStatusObject(inventoryHover->object))
- return RoomInputResult();
+ return InputResult();
if (!_inventory.hasSelection()) {
debugC(1, kDebugInventory,
@@ -370,7 +370,7 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
_inventory.setPromptText(_inventory.buildSelectedPrompt(
Common::String(), _flow._menuTextConfig));
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (!inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
@@ -381,20 +381,20 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
Common::Error interactionError =
_roomItems.handleInventoryTargetInteraction(inventoryHover->object, _interactionProcessor, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
}
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (_inventory.hasSelection()) {
if (!inventoryPanelBounds.contains(_flow._mousePos) && _inventory.close())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (!inventoryPanelBounds.contains(_flow._mousePos) && _inventory.close())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
const RoomHoverState hoverState = resolveRoomHoverState(
@@ -426,10 +426,10 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
if (clickHoverState.playerEntity && playerState.entity &&
clickHoverState.playerEntity == playerState.entity) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
if (_inventory.hasSelection()) {
const Common::String selectedItemName = _inventory.getSelectedItemName();
@@ -438,12 +438,12 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
clickHoverState.npc->npcName, selectedItemName, Common::String(), didTransition);
if (dialogueError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(dialogueError);
+ return InputResult::leaveRoom(dialogueError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (_inventory.clearSelection())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
ObjectRecord *roomTarget = clickHoverState.object
@@ -453,22 +453,22 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
Common::Error interactionError =
_roomItems.handleInventoryTargetInteraction(*roomTarget, _interactionProcessor, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
if (hoverState.playerEntity) {
if (!playerState.entity || hoverState.playerEntity != playerState.entity ||
idleState.active || idleState.exiting ||
playerState.hasMoveTarget || playerState.turnActive ||
playerState.entity->getAnimationRate() != 0) {
- return RoomInputResult();
+ return InputResult();
}
if (!openInventoryOverlay())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (hoverState.npc) {
_session.clearPendingRegionInteraction();
@@ -483,11 +483,11 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
hoverState.npc->npcName, Common::String(), Common::String(), didTransition);
if (dialogueError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(dialogueError);
+ return InputResult::leaveRoom(dialogueError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
// select one target in reverse render order; a selected object prevents exit-region selection.
if (hoverState.region && !hoverState.object && playerState.entity) {
@@ -498,19 +498,19 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
Common::Error interactionError =
_interactionProcessor.runRegionInteraction(*hoverState.region, _inventory, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
_interactionProcessor.queueRegionInteraction(*hoverState.region, _flow._mousePos);
Common::Error interactionError =
_interactionProcessor.tryActivatePendingRegion(_inventory, needsRedraw);
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
ObjectRecord *clickedObject = hoverState.object
? findSceneObjectByName(_scene.sceneObjects, hoverState.object->objectName)
@@ -537,18 +537,18 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
}
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
_session.clearPendingRegionInteraction();
if (clickedObject->objectName.equalsIgnoreCase("EXIT_BM") ||
clickedObject->objectName.equalsIgnoreCase("EXIT_HS")) {
Common::Error exitError = _session.runRoomExitCommands();
if (exitError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(exitError);
+ return InputResult::leaveRoom(exitError);
// Native closeup exits return through room_setup so the parent on-enter command runs.
if (_canExitCloseupToParent)
_flow.requestCloseupParentRestart();
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
}
ResolvedText resolvedInspectText;
@@ -571,14 +571,14 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
playerState.turnActive = false;
playerState.turnTargetFacing = -1;
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (_engine.getScript()->isPickupObject(*clickedObject)) {
Common::Error pickupError = _roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
_interactionProcessor, _session, needsRedraw);
if (pickupError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(pickupError);
- return RoomInputResult();
+ return InputResult::leaveRoom(pickupError);
+ return InputResult();
}
InteractionResult interaction;
@@ -591,7 +591,7 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
}
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
bool didTransition = false;
@@ -599,17 +599,17 @@ RoomInputResult RoomInput::handleLeftButtonDown(bool &needsRedraw) {
_interactionProcessor.handleInteractionResult(
interaction, didTransition, Common::String());
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
-RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &needsRedraw) {
+InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw) {
RoomPlayerState &playerState = _session.getPlayerState();
RoomIdleAnimationState &idleState = _session.getIdleState();
- RoomInputState &inputState = _session.getInputState();
+ InputState &inputState = _session.getInputState();
if (_presentationState.showingInspectText) {
if (key.keycode == Common::KEYCODE_ESCAPE) {
@@ -618,7 +618,7 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
_presentationState.inspectText = ResolvedText();
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
_session.notePlayerActivity();
@@ -627,7 +627,7 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
if (idleState.active || idleState.exiting) {
if (Player::requestIdleAnimationExit(_scene.state, playerState, idleState))
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (Script *script = _engine.getScript()) {
@@ -642,7 +642,7 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
needsRedraw = true;
if (cheatActivation.inventoryChanged && _inventory.isOpen() &&
!_inventory.refresh()) {
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
}
if (cheatActivation.hasInteraction) {
if (_inventory.isOpen())
@@ -651,13 +651,13 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
Common::Error cheatError = _interactionProcessor.handleInteractionResult(
cheatInteraction, didTransition, Common::String());
if (cheatError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(cheatError);
+ return InputResult::leaveRoom(cheatError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
}
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
}
@@ -673,25 +673,25 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
if (_inventory.close())
needsRedraw = true;
}
- return RoomInputResult();
+ return InputResult();
}
if (_inventory.hasSelection()) {
if (key.keycode == Common::KEYCODE_ESCAPE) {
if (_inventory.clearSelection())
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (key.keycode == Common::KEYCODE_i) {
if (_inventory.clearSelection())
needsRedraw = true;
if (!openInventoryOverlay())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
}
if (_session.isPlayerCombatLocked())
- return RoomInputResult();
+ return InputResult();
if (key.keycode == Common::KEYCODE_LCTRL ||
key.keycode == Common::KEYCODE_RCTRL) {
@@ -699,8 +699,8 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
} else if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
Common::Error debugKillError = _session.killDebugActiveMonster(_flow._mousePos, needsRedraw);
if (debugKillError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(debugKillError);
- return RoomInputResult();
+ return InputResult::leaveRoom(debugKillError);
+ return InputResult();
} else if (key.keycode == Common::KEYCODE_LEFT)
inputState.moveLeft = true;
else if (key.keycode == Common::KEYCODE_RIGHT)
@@ -711,14 +711,14 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
inputState.moveDown = true;
else if (key.keycode == Common::KEYCODE_i) {
if (!openInventoryOverlay())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
} else if (key.keycode == Common::KEYCODE_ESCAPE) {
if (_session.cancelPlayerAttackAnimation())
needsRedraw = true;
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
inputState.moveLeft = false;
inputState.moveRight = false;
inputState.moveUp = false;
@@ -730,36 +730,36 @@ RoomInputResult RoomInput::handleKeyDown(const Common::KeyState &key, bool &need
_session.captureCurrentSaveState();
Graphics::Screen *activeScreen = _engine.getScreen();
if (!activeScreen)
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
IndexedBitmap roomMenuBackdrop;
drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
Common::Error menuError = _flow.runRoomMenuStub(
roomMenuBackdrop, _scene.palette, _scene.targetPaletteBrightness,
_scene.state.hasEntrance);
if (menuError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(menuError);
+ return InputResult::leaveRoom(menuError);
_flow.resetCursorAnimationSequence();
_session.resetIdleState();
needsRedraw = true;
- return RoomInputResult();
+ return InputResult();
}
if (key.keycode == Common::KEYCODE_RETURN ||
key.keycode == Common::KEYCODE_KP_ENTER) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
Common::Error exitError = _session.runRoomExitCommands();
if (exitError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(exitError);
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(exitError);
+ return InputResult::leaveRoom(Common::kNoError);
}
- return RoomInputResult();
+ return InputResult();
}
-void RoomInput::handleKeyUp(const Common::KeyState &key) {
- RoomInputState &inputState = _session.getInputState();
+void Input::handleKeyUp(const Common::KeyState &key) {
+ InputState &inputState = _session.getInputState();
if (key.keycode == Common::KEYCODE_LCTRL ||
key.keycode == Common::KEYCODE_RCTRL)
@@ -774,10 +774,10 @@ void RoomInput::handleKeyUp(const Common::KeyState &key) {
inputState.moveDown = false;
}
-RoomInputResult RoomInput::handleEvent(const Common::Event &event, bool &needsRedraw) {
+InputResult Input::handleEvent(const Common::Event &event, bool &needsRedraw) {
Common::Error result = Common::kNoError;
if (_flow.handleSystemEvent(event, result))
- return RoomInputResult::leaveRoom(result);
+ return InputResult::leaveRoom(result);
switch (event.type) {
case Common::EVENT_MOUSEMOVE:
@@ -798,7 +798,7 @@ RoomInputResult RoomInput::handleEvent(const Common::Event &event, bool &needsRe
default:
break;
}
- return RoomInputResult();
+ return InputResult();
}
} // End of namespace Harvester
diff --git a/engines/harvester/room_input.h b/engines/harvester/input.h
similarity index 73%
rename from engines/harvester/room_input.h
rename to engines/harvester/input.h
index 9da9b566ef8..f4eeddfcd52 100644
--- a/engines/harvester/room_input.h
+++ b/engines/harvester/input.h
@@ -19,8 +19,8 @@
*
*/
-#ifndef HARVESTER_ROOM_INPUT_H
-#define HARVESTER_ROOM_INPUT_H
+#ifndef HARVESTER_INPUT_H
+#define HARVESTER_INPUT_H
#include "common/error.h"
#include "common/events.h"
@@ -32,15 +32,15 @@ class CheatSystem;
class Flow;
class HarvesterEngine;
class InventorySystem;
-class RoomInteractionProcessor;
-class RoomItemInteractions;
+class Interaction;
+class ItemInteractions;
class Scene;
struct ObjectRecord;
struct ResolvedText;
struct RoomPresentationState;
struct SceneState;
-struct RoomInputState {
+struct InputState {
bool moveLeft = false;
bool moveRight = false;
bool moveUp = false;
@@ -51,34 +51,34 @@ struct RoomInputState {
};
// Enter and closeup exits return from runRoomLoop even when the error is kNoError.
-struct RoomInputResult {
+struct InputResult {
Common::Error error;
bool returnFromRoom = false;
- static RoomInputResult leaveRoom(const Common::Error &error) {
- RoomInputResult result;
+ static InputResult leaveRoom(const Common::Error &error) {
+ InputResult result;
result.error = error;
result.returnFromRoom = true;
return result;
}
};
-class RoomInput {
+class Input {
public:
- RoomInput(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
+ Input(HarvesterEngine &engine, Flow &flow, Scene &session, SceneState &scene,
InventorySystem &inventory, CheatSystem &cheats,
- RoomItemInteractions &roomItems, RoomInteractionProcessor &interactionProcessor,
+ ItemInteractions &roomItems, Interaction &interactionProcessor,
RoomPresentationState &presentationState,
const Art &art, bool canExitCloseupToParent);
- RoomInputResult handleEvent(const Common::Event &event, bool &needsRedraw);
+ InputResult handleEvent(const Common::Event &event, bool &needsRedraw);
private:
void handleMouseMove(bool &needsRedraw);
- RoomInputResult handleRightButtonDown(bool &needsRedraw);
+ InputResult handleRightButtonDown(bool &needsRedraw);
void handleLeftButtonUp();
- RoomInputResult handleLeftButtonDown(bool &needsRedraw);
- RoomInputResult handleKeyDown(const Common::KeyState &key, bool &needsRedraw);
+ InputResult handleLeftButtonDown(bool &needsRedraw);
+ InputResult handleKeyDown(const Common::KeyState &key, bool &needsRedraw);
void handleKeyUp(const Common::KeyState &key);
void beginInspectText(const ObjectRecord &object, const ResolvedText &text);
void queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw);
@@ -91,8 +91,8 @@ private:
SceneState &_scene;
InventorySystem &_inventory;
CheatSystem &_cheats;
- RoomItemInteractions &_roomItems;
- RoomInteractionProcessor &_interactionProcessor;
+ ItemInteractions &_roomItems;
+ Interaction &_interactionProcessor;
RoomPresentationState &_presentationState;
const Art &_art;
bool _canExitCloseupToParent;
@@ -100,4 +100,4 @@ private:
} // End of namespace Harvester
-#endif // HARVESTER_ROOM_INPUT_H
+#endif // HARVESTER_INPUT_H
diff --git a/engines/harvester/room_interaction.cpp b/engines/harvester/interaction.cpp
similarity index 91%
rename from engines/harvester/room_interaction.cpp
rename to engines/harvester/interaction.cpp
index 1ece1ed9e87..8a6500efe65 100644
--- a/engines/harvester/room_interaction.cpp
+++ b/engines/harvester/interaction.cpp
@@ -19,7 +19,7 @@
*
*/
-#include "harvester/room_interaction.h"
+#include "harvester/interaction.h"
#include "common/algorithm.h"
#include "common/debug.h"
@@ -33,7 +33,7 @@
#include "harvester/player.h"
#include "harvester/scene_entities.h"
#include "harvester/resources.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
namespace Harvester {
@@ -74,23 +74,23 @@ static bool loadBitmapResource(ResourceManager &resources, const Common::String
return decodeBitmap(data, bitmap);
}
-RoomItemInteractions::RoomItemInteractions(HarvesterEngine &engine, SceneState &scene,
+ItemInteractions::ItemInteractions(HarvesterEngine &engine, SceneState &scene,
SceneEntities &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange)
: _engine(engine), _scene(scene), _sceneRuntime(sceneRuntime), _inventory(inventory),
_pendingRoomChange(pendingRoomChange) {
}
-bool RoomItemInteractions::hasCarriedRoomItem() const {
+bool ItemInteractions::hasCarriedRoomItem() const {
return !_carriedRoomItemName.empty();
}
-void RoomItemInteractions::clearCarriedRoomItem() {
+void ItemInteractions::clearCarriedRoomItem() {
_carriedRoomItemName.clear();
_carriedRoomItemLabel.clear();
_carriedRoomItemBitmap = IndexedBitmap();
}
-void RoomItemInteractions::hideSceneObject(const Common::String &objectName, const Common::String *ownerOrRoom) {
+void ItemInteractions::hideSceneObject(const Common::String &objectName, const Common::String *ownerOrRoom) {
ObjectRecord *sceneObject = findSceneObjectByName(_scene.sceneObjects, objectName);
if (!sceneObject)
return;
@@ -101,7 +101,7 @@ void RoomItemInteractions::hideSceneObject(const Common::String &objectName, con
sceneObject->currentOwnerOrRoom = *ownerOrRoom;
}
-bool RoomItemInteractions::stowCarriedRoomItemToInventory() {
+bool ItemInteractions::stowCarriedRoomItemToInventory() {
if (!hasCarriedRoomItem())
return true;
@@ -110,7 +110,7 @@ bool RoomItemInteractions::stowCarriedRoomItemToInventory() {
return _inventory.refresh();
}
-bool RoomItemInteractions::shouldCancelDeniedPickup(const InteractionResult &interaction, bool didTransition) const {
+bool ItemInteractions::shouldCancelDeniedPickup(const InteractionResult &interaction, bool didTransition) const {
return !didTransition &&
!interaction.mutatedRuntimeState &&
!interaction.requestPlayerGotoXZ &&
@@ -126,8 +126,8 @@ bool RoomItemInteractions::shouldCancelDeniedPickup(const InteractionResult &int
!interaction.modalText.value.empty();
}
-Common::Error RoomItemInteractions::restoreDeniedPickup(Script &script, const ObjectRecord &savedObject, bool clearCarryState,
- RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+Common::Error ItemInteractions::restoreDeniedPickup(Script &script, const ObjectRecord &savedObject, bool clearCarryState,
+ InteractionCallbacks &callbacks, bool &needsRedraw) {
if (!script.syncRuntimeObjectRecord(savedObject))
return Common::kReadingFailed;
if (clearCarryState)
@@ -141,8 +141,8 @@ Common::Error RoomItemInteractions::restoreDeniedPickup(Script &script, const Ob
return Common::kNoError;
}
-Common::Error RoomItemInteractions::moveRoomItemDirectlyToInventory(const ObjectRecord &object,
- RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+Common::Error ItemInteractions::moveRoomItemDirectlyToInventory(const ObjectRecord &object,
+ Interaction &interactionProcessor, InteractionCallbacks &callbacks, bool &needsRedraw) {
Script *script = _engine.getScript();
if (!script)
return Common::kReadingFailed;
@@ -182,8 +182,8 @@ Common::Error RoomItemInteractions::moveRoomItemDirectlyToInventory(const Object
return Common::kNoError;
}
-Common::Error RoomItemInteractions::beginRoomItemCarry(const ObjectRecord &object,
- RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+Common::Error ItemInteractions::beginRoomItemCarry(const ObjectRecord &object,
+ Interaction &interactionProcessor, InteractionCallbacks &callbacks, bool &needsRedraw) {
Script *script = _engine.getScript();
ResourceManager *resources = _engine.getResources();
if (!script || !resources)
@@ -244,8 +244,8 @@ Common::Error RoomItemInteractions::beginRoomItemCarry(const ObjectRecord &objec
return Common::kNoError;
}
-Common::Error RoomItemInteractions::handleInventoryTargetInteraction(const ObjectRecord &target,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw) {
+Common::Error ItemInteractions::handleInventoryTargetInteraction(const ObjectRecord &target,
+ Interaction &interactionProcessor, bool &needsRedraw) {
if (!_inventory.hasSelection())
return Common::kNoError;
@@ -287,7 +287,7 @@ Common::Error RoomItemInteractions::handleInventoryTargetInteraction(const Objec
return Common::kNoError;
}
-ObjectRecord *RoomItemInteractions::findSelectedInventoryRoomTarget(const Common::Point &point) {
+ObjectRecord *ItemInteractions::findSelectedInventoryRoomTarget(const Common::Point &point) {
if (!_inventory.hasSelection())
return nullptr;
@@ -308,9 +308,9 @@ ObjectRecord *RoomItemInteractions::findSelectedInventoryRoomTarget(const Common
const Entity *entity = entityManager->findSceneEntityByName(candidate.objectName);
if (!entity || !entity->hitTest(point))
continue;
- if (entity->getClassId() == kRuntimeEntityClassBackground ||
- entity->getClassId() == kRuntimeEntityClassPlayer ||
- entity->getClassId() == kRuntimeEntityClassRectHotspot19) {
+ if (entity->getClassId() == kEntityClassBackground ||
+ entity->getClassId() == kEntityClassPlayer ||
+ entity->getClassId() == kEntityClassRectHotspot19) {
continue;
}
@@ -326,8 +326,8 @@ ObjectRecord *RoomItemInteractions::findSelectedInventoryRoomTarget(const Common
return bestTarget;
}
-Common::Error RoomItemInteractions::handleRoomItemPickup(const ObjectRecord &object, bool playerPresent,
- RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw) {
+Common::Error ItemInteractions::handleRoomItemPickup(const ObjectRecord &object, bool playerPresent,
+ Interaction &interactionProcessor, InteractionCallbacks &callbacks, bool &needsRedraw) {
InteractionResult blockedPickupInteraction;
if (_engine.getScript()->isPickupBlockedByAction(object, &blockedPickupInteraction)) {
bool didTransition = false;
@@ -344,12 +344,12 @@ Common::Error RoomItemInteractions::handleRoomItemPickup(const ObjectRecord &obj
: moveRoomItemDirectlyToInventory(object, interactionProcessor, callbacks, needsRedraw);
}
-RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow,
+Interaction::Interaction(HarvesterEngine &engine, Flow &flow,
SceneState &scene, RoomPlayerState &playerState,
Common::String &pendingRegionName, Common::String &pendingRoomChange,
bool &pendingRoomChangeIsRoomName, bool &pendingRoomChangeUsesSavedRoomState,
SaveRoomState &pendingRoomChangeSavedRoomState, bool canExitCloseupToParent,
- RoomInteractionCallbacks &callbacks)
+ InteractionCallbacks &callbacks)
: _engine(engine), _flow(flow), _scene(scene), _playerState(playerState),
_pendingRegionName(pendingRegionName), _pendingRoomChange(pendingRoomChange),
_pendingRoomChangeIsRoomName(pendingRoomChangeIsRoomName),
@@ -358,7 +358,7 @@ RoomInteractionProcessor::RoomInteractionProcessor(HarvesterEngine &engine, Flow
_canExitCloseupToParent(canExitCloseupToParent), _callbacks(callbacks) {
}
-void RoomInteractionProcessor::queueRegionInteraction(const RegionRecord ®ion, const Common::Point &mousePos) {
+void Interaction::queueRegionInteraction(const RegionRecord ®ion, const Common::Point &mousePos) {
_pendingRegionName = region.regionName;
if (!_playerState.entity)
return;
@@ -392,7 +392,7 @@ void RoomInteractionProcessor::queueRegionInteraction(const RegionRecord ®ion
}
}
-Common::Error RoomInteractionProcessor::runRegionInteraction(const RegionRecord ®ion,
+Common::Error Interaction::runRegionInteraction(const RegionRecord ®ion,
InventorySystem &inventory, bool &needsRedraw) {
InteractionResult interaction;
if (!_engine.getScript()->resolveRegionInteraction(region, interaction, _scene.state.roomName))
@@ -417,7 +417,7 @@ Common::Error RoomInteractionProcessor::runRegionInteraction(const RegionRecord
return Common::kNoError;
}
-Common::Error RoomInteractionProcessor::tryActivatePendingRegion(InventorySystem &inventory, bool &needsRedraw) {
+Common::Error Interaction::tryActivatePendingRegion(InventorySystem &inventory, bool &needsRedraw) {
if (_pendingRegionName.empty() || !_playerState.entity)
return Common::kNoError;
@@ -438,7 +438,7 @@ Common::Error RoomInteractionProcessor::tryActivatePendingRegion(InventorySystem
return runRegionInteraction(*region, inventory, needsRedraw);
}
-Common::Error RoomInteractionProcessor::tryActivatePassiveRegion(InventorySystem &inventory, bool &needsRedraw) {
+Common::Error Interaction::tryActivatePassiveRegion(InventorySystem &inventory, bool &needsRedraw) {
if (!_playerState.entity)
return Common::kNoError;
@@ -457,16 +457,16 @@ Common::Error RoomInteractionProcessor::tryActivatePassiveRegion(InventorySystem
return Common::kNoError;
}
-int RoomInteractionProcessor::getPendingRegionFacing() const {
+int Interaction::getPendingRegionFacing() const {
const RegionRecord *pendingRegion = findSceneRegionByName(_scene.sceneRegions, _pendingRegionName);
return pendingRegion && pendingRegion->startEnabled ? pendingRegion->desiredFacing : -1;
}
-bool RoomInteractionProcessor::hasPendingMainMenuReturn() const {
+bool Interaction::hasPendingMainMenuReturn() const {
return _flow.hasPendingMainMenuReturn();
}
-Common::Error RoomInteractionProcessor::handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw) {
+Common::Error Interaction::handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw) {
bool didTransition = false;
Common::Error interactionError =
handleInteractionResult(interaction, didTransition, Common::String());
@@ -483,7 +483,7 @@ Common::Error RoomInteractionProcessor::handleCombatInteraction(const Interactio
return Common::kNoError;
}
-Common::Error RoomInteractionProcessor::handleInteractionResult(const InteractionResult &interaction,
+Common::Error Interaction::handleInteractionResult(const InteractionResult &interaction,
bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement) {
didTransition = false;
@@ -745,7 +745,7 @@ Common::Error RoomInteractionProcessor::handleInteractionResult(const Interactio
return Common::kNoError;
}
-Common::Error RoomInteractionProcessor::runScriptedDialogue(const Common::String &npcName,
+Common::Error Interaction::runScriptedDialogue(const Common::String &npcName,
const Common::String &usedItemName, const Common::String &continuationTag,
bool &didTransition) {
didTransition = false;
diff --git a/engines/harvester/room_interaction.h b/engines/harvester/interaction.h
similarity index 83%
rename from engines/harvester/room_interaction.h
rename to engines/harvester/interaction.h
index 052bd309dfe..1e80ef1cb38 100644
--- a/engines/harvester/room_interaction.h
+++ b/engines/harvester/interaction.h
@@ -19,8 +19,8 @@
*
*/
-#ifndef HARVESTER_ROOM_INTERACTION_H
-#define HARVESTER_ROOM_INTERACTION_H
+#ifndef HARVESTER_INTERACTION_H
+#define HARVESTER_INTERACTION_H
#include "common/error.h"
#include "harvester/bitmap.h"
@@ -32,15 +32,15 @@ namespace Harvester {
class Flow;
class HarvesterEngine;
class InventorySystem;
-class RoomInteractionProcessor;
+class Interaction;
class SceneEntities;
struct IndexedBitmap;
extern const char *const kExitCloseupPendingRoomChange;
-class RoomInteractionCallbacks {
+class InteractionCallbacks {
public:
- virtual ~RoomInteractionCallbacks() {}
+ virtual ~InteractionCallbacks() {}
virtual bool refreshCurrentScene(bool preservePlayerPlacement) = 0;
virtual bool applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) = 0;
@@ -58,9 +58,9 @@ public:
int damageType) = 0;
};
-class RoomItemInteractions {
+class ItemInteractions {
public:
- RoomItemInteractions(HarvesterEngine &engine, SceneState &scene,
+ ItemInteractions(HarvesterEngine &engine, SceneState &scene,
SceneEntities &sceneRuntime, InventorySystem &inventory, const Common::String &pendingRoomChange);
const Common::String &getCarriedRoomItemLabel() const { return _carriedRoomItemLabel; }
@@ -68,21 +68,21 @@ public:
bool hasCarriedRoomItem() const;
bool stowCarriedRoomItemToInventory();
Common::Error handleInventoryTargetInteraction(const ObjectRecord &target,
- RoomInteractionProcessor &interactionProcessor, bool &needsRedraw);
+ Interaction &interactionProcessor, bool &needsRedraw);
ObjectRecord *findSelectedInventoryRoomTarget(const Common::Point &point);
Common::Error handleRoomItemPickup(const ObjectRecord &object, bool playerPresent,
- RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+ Interaction &interactionProcessor, InteractionCallbacks &callbacks, bool &needsRedraw);
private:
void clearCarriedRoomItem();
void hideSceneObject(const Common::String &objectName, const Common::String *ownerOrRoom);
bool shouldCancelDeniedPickup(const InteractionResult &interaction, bool didTransition) const;
Common::Error restoreDeniedPickup(Script &script, const ObjectRecord &savedObject, bool clearCarryState,
- RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+ InteractionCallbacks &callbacks, bool &needsRedraw);
Common::Error moveRoomItemDirectlyToInventory(const ObjectRecord &object,
- RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+ Interaction &interactionProcessor, InteractionCallbacks &callbacks, bool &needsRedraw);
Common::Error beginRoomItemCarry(const ObjectRecord &object,
- RoomInteractionProcessor &interactionProcessor, RoomInteractionCallbacks &callbacks, bool &needsRedraw);
+ Interaction &interactionProcessor, InteractionCallbacks &callbacks, bool &needsRedraw);
HarvesterEngine &_engine;
SceneState &_scene;
@@ -94,13 +94,13 @@ private:
IndexedBitmap _carriedRoomItemBitmap;
};
-class RoomInteractionProcessor {
+class Interaction {
public:
- RoomInteractionProcessor(HarvesterEngine &engine, Flow &flow, SceneState &scene,
+ Interaction(HarvesterEngine &engine, Flow &flow, SceneState &scene,
RoomPlayerState &playerState, Common::String &pendingRegionName,
Common::String &pendingRoomChange, bool &pendingRoomChangeIsRoomName,
bool &pendingRoomChangeUsesSavedRoomState, SaveRoomState &pendingRoomChangeSavedRoomState,
- bool canExitCloseupToParent, RoomInteractionCallbacks &callbacks);
+ bool canExitCloseupToParent, InteractionCallbacks &callbacks);
void queueRegionInteraction(const RegionRecord ®ion, const Common::Point &mousePos);
Common::Error runRegionInteraction(const RegionRecord ®ion,
@@ -127,11 +127,11 @@ private:
bool &_pendingRoomChangeUsesSavedRoomState;
SaveRoomState &_pendingRoomChangeSavedRoomState;
bool _canExitCloseupToParent;
- RoomInteractionCallbacks &_callbacks;
+ InteractionCallbacks &_callbacks;
};
bool hasRoomEntryInteraction(const InteractionResult &interaction);
} // End of namespace Harvester
-#endif // HARVESTER_ROOM_INTERACTION_H
+#endif // HARVESTER_INTERACTION_H
diff --git a/engines/harvester/media_manager.cpp b/engines/harvester/media_manager.cpp
index 02e4a522859..da0b4335fec 100644
--- a/engines/harvester/media_manager.cpp
+++ b/engines/harvester/media_manager.cpp
@@ -34,7 +34,7 @@
#include "graphics/screen.h"
#include "harvester/art.h"
#include "harvester/resources.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
#include "harvester/script.h"
#include "harvester/text.h"
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index ecf0537af55..9cbcd4b90b9 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -38,7 +38,7 @@
#include "harvester/palette_utils.h"
#include "harvester/resources.h"
#include "harvester/room_support.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
#include "harvester/art.h"
#include "harvester/flow.h"
#include "harvester/script.h"
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 1955e9fe591..ddcde655bc6 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -17,13 +17,13 @@ MODULE_OBJS = \
monster.o \
player.o \
resources.o \
- room_combat.o \
- room_interaction.o \
- room_input.o \
+ combat.o \
+ interaction.o \
+ input.o \
room_presentation.o \
scene_entities.o \
scene.o \
- runtime_entity.o \
+ entity.o \
saveload.o \
script.o \
text.o \
diff --git a/engines/harvester/monster.cpp b/engines/harvester/monster.cpp
index 50be328beaf..82198abd7be 100644
--- a/engines/harvester/monster.cpp
+++ b/engines/harvester/monster.cpp
@@ -21,7 +21,7 @@
#include "harvester/monster.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
namespace Harvester {
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index a57b986cba5..229a080de6d 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -28,7 +28,7 @@
#include "harvester/detection.h"
#include "harvester/harvester.h"
#include "harvester/resources.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
namespace Harvester {
@@ -1052,7 +1052,7 @@ static void positionPlayerIdleAnimationEntity(const RoomSetupState &state,
if (!playerState.entity)
return;
- idleEntity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ idleEntity.setAnchorMode(kEntityAnchorTopLeft);
idleEntity.setDepthScale(Player::computeDepthScale(state, playerState.z));
idleEntity.setPosition(playerState.entity->getX(),
playerState.entity->getY() + kRoomPlayerIdleYOffset, playerState.z);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index d08d12df167..7829e3da046 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -33,8 +33,8 @@
#include "harvester/harvester.h"
#include "harvester/inventory.h"
#include "harvester/player.h"
-#include "harvester/room_combat.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/combat.h"
+#include "harvester/entity.h"
#include "common/ptr.h"
#include "graphics/blit.h"
@@ -43,8 +43,8 @@
#include "harvester/cheats.h"
#include "harvester/resources.h"
#include "harvester/monster.h"
-#include "harvester/room_interaction.h"
-#include "harvester/room_input.h"
+#include "harvester/interaction.h"
+#include "harvester/input.h"
#include "harvester/room_presentation.h"
#include "harvester/room_support.h"
@@ -221,7 +221,7 @@ bool Scene::applyCurrentRoomRuntimeMutationsInPlace(bool preservePaletteState) {
SceneState updatedScene;
if (!_sceneRuntime.loadSceneResources(updatedState, updatedScene))
return false;
- Common::Array<RoomMonsterCombatState> updatedMonsterCombatStates =
+ Common::Array<MonsterCombatState> updatedMonsterCombatStates =
_combat.prepareMonsterCombatStatesForSceneChange(_scene.state.roomMonsters, updatedState.roomMonsters);
if (!_sceneRuntime.reconcileSceneEntitiesInPlace(_scene, updatedState, updatedScene))
@@ -434,15 +434,15 @@ void Scene::startPlayerDefeatSequence(const char *reason, const Common::String &
: 0);
}
-RoomInputResult Scene::runRoomEntryInteraction(const InteractionResult &roomEntryInteraction) {
+InputResult Scene::runRoomEntryInteraction(const InteractionResult &roomEntryInteraction) {
bool didEntryTransition = false;
Common::Error entryInteractionError = _interactionProcessor.handleInteractionResult(
roomEntryInteraction, didEntryTransition, Common::String());
if (entryInteractionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(entryInteractionError);
+ return InputResult::leaveRoom(entryInteractionError);
if (_flow.hasPendingMainMenuReturn() || didEntryTransition)
- return RoomInputResult::leaveRoom(Common::kNoError);
- return RoomInputResult();
+ return InputResult::leaveRoom(Common::kNoError);
+ return InputResult();
}
void Scene::updateRoomInterface(bool &needsRedraw) {
@@ -585,14 +585,14 @@ bool Scene::drawRoomFrame(bool &needsRedraw) {
return true;
}
-RoomInputResult Scene::pollRoomEvents(RoomInput &input, bool &needsRedraw) {
+InputResult Scene::pollRoomEvents(Input &input, bool &needsRedraw) {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
- const RoomInputResult inputResult = input.handleEvent(event, needsRedraw);
+ const InputResult inputResult = input.handleEvent(event, needsRedraw);
if (inputResult.returnFromRoom)
return inputResult;
}
- return RoomInputResult();
+ return InputResult();
}
Scene::PlayerControlState Scene::preparePlayerControlState() {
@@ -704,7 +704,7 @@ void Scene::pruneCombatEffects(bool &needsRedraw) {
needsRedraw = true;
}
-RoomInputResult Scene::dispatchExpiredTimers(bool &needsRedraw) {
+InputResult Scene::dispatchExpiredTimers(bool &needsRedraw) {
if (_entityManager) {
Common::Array<Common::String> expiredTimerNames;
if (_entityManager->takeExpiredTimerNames(expiredTimerNames)) {
@@ -720,25 +720,25 @@ RoomInputResult Scene::dispatchExpiredTimers(bool &needsRedraw) {
_interactionProcessor.handleInteractionResult(
timerInteraction, didTransition, Common::String(), false);
if (interactionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(interactionError);
+ return InputResult::leaveRoom(interactionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
needsRedraw = true;
if (!_pendingTransition.roomName.empty())
break;
}
}
}
- return RoomInputResult();
+ return InputResult();
}
-RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
- RoomInput input(_engine, _flow, *this, _scene, _inventory, _cheats,
+InputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
+ Input input(_engine, _flow, *this, _scene, _inventory, _cheats,
_roomItems, _interactionProcessor, _presentationState, *_view.art, _canExitCloseupToParent);
bool needsRedraw = true;
if (!_inventory.refresh())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
Graphics::FrameLimiter limiter(g_system, 60);
captureCurrentSaveState();
@@ -747,7 +747,7 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
while (!_engine.shouldQuit()) {
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (_flow.takePendingNewGameRestart()) {
_flow.prepareForNewGame();
_pendingTransition.roomName = "START";
@@ -755,7 +755,7 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
_pendingTransition.usesSavedRoomState = false;
_pendingTransition.savedRoomState.clear();
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
if (_engine.hasPendingLoadedSaveRoomState()) {
@@ -768,17 +768,17 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
_pendingTransition.isRoomName = true;
}
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
if (_flow.hasPendingDebugRoomChange()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kNoError);
}
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
InteractionResult queuedDebugInteraction;
@@ -789,26 +789,26 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
Common::Error debugInteractionError = _interactionProcessor.handleInteractionResult(
queuedDebugInteraction, didDebugTransition, Common::String());
if (debugInteractionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(debugInteractionError);
+ return InputResult::leaveRoom(debugInteractionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
needsRedraw = true;
}
updateRoomInterface(needsRedraw);
if (!drawRoomFrame(needsRedraw))
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
- const RoomInputResult inputResult = pollRoomEvents(input, needsRedraw);
+ const InputResult inputResult = pollRoomEvents(input, needsRedraw);
if (inputResult.returnFromRoom)
return inputResult;
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
@@ -820,23 +820,23 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
Common::Error pendingRegionError =
tryActivatePendingRegion(needsRedraw);
if (pendingRegionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(pendingRegionError);
+ return InputResult::leaveRoom(pendingRegionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
if (!isPlayerCombatLocked()) {
pendingRegionError = tryActivatePassiveRegion(needsRedraw);
if (pendingRegionError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(pendingRegionError);
+ return InputResult::leaveRoom(pendingRegionError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
}
@@ -848,12 +848,12 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
if (!control.paused) {
combatError = resolvePlayerAttackContact(needsRedraw);
if (combatError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(combatError);
+ return InputResult::leaveRoom(combatError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
updatePlayerCombatAnimations(needsRedraw);
@@ -861,20 +861,20 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
updatePlayerControls(control, needsRedraw);
combatError = updateRoomMonsterCombat(needsRedraw);
if (combatError.getCode() != Common::kNoError)
- return RoomInputResult::leaveRoom(combatError);
+ return InputResult::leaveRoom(combatError);
syncCombatHitEffects(needsRedraw);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (!_pendingTransition.roomName.empty()) {
if (!_roomItems.stowCarriedRoomItemToInventory())
- return RoomInputResult::leaveRoom(Common::kReadingFailed);
+ return InputResult::leaveRoom(Common::kReadingFailed);
break;
}
updatePlayerDeath(needsRedraw);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
pruneCombatEffects(needsRedraw);
- const RoomInputResult timerResult = dispatchExpiredTimers(needsRedraw);
+ const InputResult timerResult = dispatchExpiredTimers(needsRedraw);
if (timerResult.returnFromRoom)
return timerResult;
updateIdleAnimation(needsRedraw);
@@ -885,17 +885,17 @@ RoomInputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
limiter.startFrame();
}
if (_engine.shouldQuit())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (_flow.hasPendingMainMenuReturn())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (_pendingTransition.roomName.empty())
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
if (_pendingTransition.roomName.equals(kExitCloseupPendingRoomChange)) {
_flow.requestCloseupParentRestart();
- return RoomInputResult::leaveRoom(Common::kNoError);
+ return InputResult::leaveRoom(Common::kNoError);
}
- return RoomInputResult();
+ return InputResult();
}
void Scene::syncGlobalTimerEntities(const Common::Array<TimerRecord> &previousTimerRecords) {
@@ -1150,7 +1150,7 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
if (shouldRunRoomEntryCommands) {
if (hasPendingRoomEntryInteraction) {
- const RoomInputResult entryResult = session.runRoomEntryInteraction(roomEntryInteraction);
+ const InputResult entryResult = session.runRoomEntryInteraction(roomEntryInteraction);
if (entryResult.returnFromRoom)
return entryResult.error;
}
@@ -1161,7 +1161,7 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
ScopedDeferredLiveNpcDeathTransitions deferredLiveNpcDeathTransitions(
_engine.getScript());
- const RoomInputResult frameResult = session.runRoomFrames(currentRoomTarget);
+ const InputResult frameResult = session.runRoomFrames(currentRoomTarget);
if (frameResult.returnFromRoom)
return frameResult.error;
const RoomPendingTransition &pendingTransition = session.getPendingTransition();
diff --git a/engines/harvester/scene.h b/engines/harvester/scene.h
index da19ea16e3f..8c896f14bf7 100644
--- a/engines/harvester/scene.h
+++ b/engines/harvester/scene.h
@@ -24,13 +24,13 @@
#include "common/error.h"
#include "common/str.h"
-#include "harvester/room_combat.h"
-#include "harvester/room_input.h"
-#include "harvester/room_interaction.h"
+#include "harvester/combat.h"
+#include "harvester/input.h"
+#include "harvester/interaction.h"
#include "harvester/room_presentation.h"
#include "harvester/scene_entities.h"
#include "harvester/room_support.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
namespace Harvester {
@@ -58,18 +58,18 @@ struct RoomPendingTransition {
SaveRoomState savedRoomState;
};
-class Scene : public RoomInteractionCallbacks {
+class Scene : public InteractionCallbacks {
public:
Scene(HarvesterEngine &engine, Flow &flow, SceneState &scene, EntityManager *entityManager,
InventorySystem &inventory, CheatSystem &cheats, const RoomViewResources &view, bool canExitCloseupToParent);
RoomPlayerState &getPlayerState() { return _playerState; }
RoomIdleAnimationState &getIdleState() { return _idleState; }
- RoomInputState &getInputState() { return _inputState; }
+ InputState &getInputState() { return _inputState; }
void initializePlayerState();
- RoomInputResult runRoomEntryInteraction(const InteractionResult &interaction);
- RoomInputResult runRoomFrames(const Common::String ¤tRoomTarget);
+ InputResult runRoomEntryInteraction(const InteractionResult &interaction);
+ InputResult runRoomFrames(const Common::String ¤tRoomTarget);
const RoomPendingTransition &getPendingTransition() const { return _pendingTransition; }
void clearPendingRegionInteraction() { _pendingRegionName.clear(); }
void capturePlayerAttackTarget(const Common::Point &mousePos, const NpcRecord *npc);
@@ -104,7 +104,7 @@ private:
void requestPlayerGameOver(const char *reason, const Common::String &sourceName);
void updateRoomInterface(bool &needsRedraw);
bool drawRoomFrame(bool &needsRedraw);
- RoomInputResult pollRoomEvents(RoomInput &input, bool &needsRedraw);
+ InputResult pollRoomEvents(Input &input, bool &needsRedraw);
PlayerControlState preparePlayerControlState();
Common::Error tryActivatePendingRegion(bool &needsRedraw);
Common::Error tryActivatePassiveRegion(bool &needsRedraw);
@@ -116,7 +116,7 @@ private:
void syncCombatHitEffects(bool &needsRedraw);
void updatePlayerDeath(bool &needsRedraw);
void pruneCombatEffects(bool &needsRedraw);
- RoomInputResult dispatchExpiredTimers(bool &needsRedraw);
+ InputResult dispatchExpiredTimers(bool &needsRedraw);
void updateIdleAnimation(bool &needsRedraw);
HarvesterEngine &_engine;
@@ -126,19 +126,19 @@ private:
InventorySystem &_inventory;
SceneEntities _sceneRuntime;
RoomPlayerState _playerState;
- RoomCombat _combat;
- RoomCombatEffects _combatEffects;
+ Combat _combat;
+ CombatEffects _combatEffects;
RoomIdleAnimationState _idleState;
- RoomInputState _inputState;
+ InputState _inputState;
Common::String _pendingRegionName;
CheatSystem &_cheats;
const RoomViewResources _view;
bool _canExitCloseupToParent;
RoomPresentationState _presentationState;
RoomPendingTransition _pendingTransition;
- RoomItemInteractions _roomItems;
+ ItemInteractions _roomItems;
RoomPresentation _presentation;
- RoomInteractionProcessor _interactionProcessor;
+ Interaction _interactionProcessor;
};
} // End of namespace Harvester
diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index 1ba485826cc..0c39ba1fdd8 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -24,7 +24,7 @@
#include "harvester/detection.h"
#include "harvester/monster.h"
#include "harvester/player.h"
-#include "harvester/room_combat.h"
+#include "harvester/combat.h"
namespace Harvester {
@@ -57,12 +57,12 @@ static int resolveSceneObjectClass(const RoomSetupState &state,
const ObjectRecord &object, const Entity *entity, Script *script) {
if (entity)
return isBackgroundSceneObject(state, object, *entity)
- ? kRuntimeEntityClassBackground
- : kRuntimeEntityClassObject;
+ ? kEntityClassBackground
+ : kEntityClassObject;
return isInteractiveSceneHotspot(object, script)
- ? kRuntimeEntityClassRectHotspot
- : kRuntimeEntityClassDisabledHotspot;
+ ? kEntityClassRectHotspot
+ : kEntityClassDisabledHotspot;
}
static bool sameRect(const Common::Rect &lhs, const Common::Rect &rhs) {
@@ -418,7 +418,7 @@ void SceneEntities::syncCurrentRoomRuntimeState(const Common::Array<RegionRecord
for (const TimerRecord &baseTimer : script->getTimers()) {
Entity *entity = _entityManager->findSceneEntityByName(baseTimer.timerName);
- if (!entity || entity->getClassId() != kRuntimeEntityClassTimer)
+ if (!entity || entity->getClassId() != kEntityClassTimer)
continue;
const TimerRecord *runtimeTimer = script->findRuntimeTimerRecord(baseTimer.timerName);
@@ -456,7 +456,7 @@ void SceneEntities::syncGlobalTimerEntities(const Common::Array<TimerRecord> &pr
}
Entity *entity = _entityManager->findSceneEntityByName(runtimeTimer->timerName);
- if (entity && entity->getClassId() != kRuntimeEntityClassTimer)
+ if (entity && entity->getClassId() != kEntityClassTimer)
continue;
if (!entity) {
@@ -525,19 +525,19 @@ bool SceneEntities::isInteractiveSceneHotspot(const ObjectRecord &object) const
return script->resolveObjectInspectText(object, inspectText);
}
-RuntimeEntityClass SceneEntities::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
+EntityClass SceneEntities::resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const {
if (entity) {
return ((!_state.backgroundObjectName.empty() &&
object.objectName.equalsIgnoreCase(_state.backgroundObjectName)) ||
(object.initialX == 0 && object.initialY == 0 &&
entity->getBoundsWidth() == 640 && entity->getBoundsHeight() == 480))
- ? kRuntimeEntityClassBackground
- : kRuntimeEntityClassObject;
+ ? kEntityClassBackground
+ : kEntityClassObject;
}
return isInteractiveSceneHotspot(object)
- ? kRuntimeEntityClassRectHotspot
- : kRuntimeEntityClassDisabledHotspot;
+ ? kEntityClassRectHotspot
+ : kEntityClassDisabledHotspot;
}
void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object) {
@@ -559,7 +559,7 @@ void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object)
entity->setClassId(resolveSceneObjectClassLocal(
object, entity->hasFrames() ? entity : nullptr));
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setAnchorMode(kEntityAnchorTopLeft);
entity->setZExtent((float)object.zExtent);
debugC(3, kDebugRoom, "Harvester: scene object spawned object='%s' sprite='%s' class=0x%x",
object.objectName.c_str(), spritePath.c_str(), entity->getClassId());
@@ -584,11 +584,11 @@ void SceneEntities::spawnSceneRegionEntityFromRecord(const RegionRecord ®ion)
if (!entity)
return;
- entity->setClassId(kRuntimeEntityClassRectHotspot19);
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setClassId(kEntityClassRectHotspot19);
+ entity->setAnchorMode(kEntityAnchorTopLeft);
entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
if (!region.cursorEnabled)
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setHitTestMode(kEntityHitTestNone);
debugC(1, kDebugRoom,
"Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
region.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
@@ -614,7 +614,7 @@ bool SceneEntities::applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc)
const float renderZ =
(float)npc.posZ - floorf(MAX<float>(entity.getZExtent(), 0.0f) * 0.5f);
- entity.setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity.setAnchorMode(kEntityAnchorTopLeft);
entity.setPosition(npc.posX - xOffset - width / 2, npc.posY - height - yOffset, renderZ);
return true;
}
@@ -633,9 +633,9 @@ Entity *SceneEntities::spawnSceneNpcEntityFromRecord(const NpcRecord &npc) {
if (!entity)
return nullptr;
- entity->setClassId(kRuntimeEntityClassNpc);
+ entity->setClassId(kEntityClassNpc);
entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(preservedCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+ entity->setHitTestMode(preservedCorpse ? kEntityHitTestNone : kEntityHitTestOpaquePixels);
entity->setVisible(true);
if (!applyRoomNpcPlacement(*entity, npc)) {
removeSceneEntityByName(npc.npcName);
@@ -666,9 +666,9 @@ Entity *SceneEntities::spawnSceneMonsterEntityFromRecord(const RoomSetupState &r
if (!entity)
return nullptr;
- entity->setClassId(kRuntimeEntityClassMonster);
+ entity->setClassId(kEntityClassMonster);
entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setHitTestMode(kEntityHitTestNone);
entity->setVisible(monster.visible);
Monster::applyAnimation(*entity, monster);
if (!applyRoomActorPlacement(roomState, *entity, monster.posX, monster.posY, (float)monster.posZ)) {
@@ -709,11 +709,11 @@ void SceneEntities::populateRoomRegions() {
continue;
}
- entity->setClassId(kRuntimeEntityClassRectHotspot19);
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setClassId(kEntityClassRectHotspot19);
+ entity->setAnchorMode(kEntityAnchorTopLeft);
entity->setZExtent((float)MAX(0, region.maxZ - region.minZ));
if (!region.cursorEnabled)
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setHitTestMode(kEntityHitTestNone);
debugC(1, kDebugRoom,
"Harvester: scene region spawned room='%s' region='%s' class=0x%x bounds=(%d,%d)-(%d,%d) z=[%d,%d] facing=%d cursor=%d action='%s'",
_state.roomName.c_str(), region.regionName.c_str(), entity->getClassId(),
@@ -753,7 +753,7 @@ void SceneEntities::populateRoomObjects(const Common::Array<ObjectRecord> &drawa
entity->setClassId(resolveSceneObjectClass(
_state, object, entity->hasFrames() ? entity : nullptr, _script));
- entity->setAnchorMode(kRuntimeEntityAnchorTopLeft);
+ entity->setAnchorMode(kEntityAnchorTopLeft);
entity->setZExtent((float)object.zExtent);
const Common::Rect entityRect = entity->getScreenRect();
debugC(1, kDebugRoom,
@@ -779,13 +779,13 @@ void SceneEntities::populateRoomNpcs() {
continue;
}
- entity->setClassId(kRuntimeEntityClassNpc);
+ entity->setClassId(kEntityClassNpc);
entity->setZExtent(kNativeNpcMonsterZExtent);
const bool isCorpse = !npc.active &&
npc.runtimeSpawned &&
npc.deathDamageType != 0 &&
npc.runtimeState >= 0;
- entity->setHitTestMode(isCorpse ? kRuntimeEntityHitTestNone : kRuntimeEntityHitTestOpaquePixels);
+ entity->setHitTestMode(isCorpse ? kEntityHitTestNone : kEntityHitTestOpaquePixels);
// Native spawn positions frame 0 before selecting ambient or corpse frame banks.
if (!applyRoomNpcPlacement(*entity, npc)) {
debug(1, "Harvester: unable to apply room npc placement for '%s'",
@@ -826,9 +826,9 @@ void SceneEntities::populateRoomMonsters() {
continue;
}
- entity->setClassId(kRuntimeEntityClassMonster);
+ entity->setClassId(kEntityClassMonster);
entity->setZExtent(kNativeNpcMonsterZExtent);
- entity->setHitTestMode(kRuntimeEntityHitTestNone);
+ entity->setHitTestMode(kEntityHitTestNone);
entity->setVisible(monster.visible);
// Native restores a saved monster corpse frame before set_entity_screen_position().
Monster::applyAnimation(*entity, monster);
diff --git a/engines/harvester/scene_entities.h b/engines/harvester/scene_entities.h
index fb451a6c12a..7265a47da3a 100644
--- a/engines/harvester/scene_entities.h
+++ b/engines/harvester/scene_entities.h
@@ -23,7 +23,7 @@
#define HARVESTER_SCENE_ENTITIES_H
#include "harvester/room_support.h"
-#include "harvester/runtime_entity.h"
+#include "harvester/entity.h"
namespace Harvester {
@@ -57,7 +57,7 @@ public:
private:
bool isTimerMaterializedInCurrentRoom(const Common::String &timerName) const;
bool isInteractiveSceneHotspot(const ObjectRecord &object) const;
- RuntimeEntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
+ EntityClass resolveSceneObjectClassLocal(const ObjectRecord &object, const Entity *entity) const;
static Common::Rect getSceneRegionBounds(const RegionRecord ®ion);
static bool applyRoomNpcPlacement(Entity &entity, const NpcRecord &npc);
Commit: 939d4f10ea0751dba6f1992c4efdd0f8cf4719dd
https://github.com/scummvm/scummvm/commit/939d4f10ea0751dba6f1992c4efdd0f8cf4719dd
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: Update walk direction/depth after animation
Fixes #17328
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/player.cpp
engines/harvester/room_support.h
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 229a080de6d..66043207e8f 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -596,6 +596,10 @@ 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 void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
playerState.moveTargetXReached = false;
playerState.moveTargetZReached = false;
@@ -603,7 +607,7 @@ static void resetPlayerMoveTargetProgress(RoomPlayerState &playerState) {
static int resolveMoveTargetHorizontalDirection(const RoomSetupState &state,
const RoomPlayerState &playerState, int regionFacing) {
- const float slack = Player::computeDepthScale(state, playerState.z) *
+ const float slack = Player::computeDepthScale(state, computePlayerRenderZ(playerState)) *
kRoomPlayerHorizontalTargetSlackBase;
const float frameLeftX = (float)computePlayerFrameLeftX(playerState);
if (frameLeftX < (float)playerState.targetX - slack || regionFacing == 2)
@@ -615,9 +619,10 @@ static int resolveMoveTargetHorizontalDirection(const RoomSetupState &state,
}
static int resolveMoveTargetDepthDirection(const RoomPlayerState &playerState, int regionFacing) {
- if (playerState.targetZ < playerState.z - kRoomPlayerDepthTargetSlack || regionFacing == 0)
+ const float z = computePlayerRenderZ(playerState);
+ if (playerState.targetZ < z - kRoomPlayerDepthTargetSlack || regionFacing == 0)
return 1;
- if (playerState.z + kRoomPlayerDepthTargetSlack < playerState.targetZ || regionFacing == 3)
+ if (z + kRoomPlayerDepthTargetSlack < playerState.targetZ || regionFacing == 3)
return -1;
return 0;
@@ -684,14 +689,6 @@ static bool consumePlayerMovementTick(RoomPlayerState &playerState) {
return true;
}
-static float stepTowardsFloat(float current, float target, float step) {
- if (step <= 0.0f || fabsf(target - current) <= kRoomDepthCompareEpsilon)
- return target;
- if (current < target)
- return MIN(current + step, target);
- return MAX(current - step, target);
-}
-
static bool shouldIgnoreMovementBlocker(const Entity &entity, const Common::String *ignoredBlockerName) {
return ignoredBlockerName && !ignoredBlockerName->empty() &&
entity.getName().equalsIgnoreCase(*ignoredBlockerName);
@@ -1353,7 +1350,7 @@ int Player::resolveRegionTargetX(const RoomSetupState &state, const RegionRecord
targetX -= kRoomRegionTargetXBias;
const int targetSlack = MAX<int>(1,
- roundToInt(Player::computeDepthScale(state, playerState.z) *
+ roundToInt(Player::computeDepthScale(state, computePlayerRenderZ(playerState)) *
kRoomPlayerHorizontalTargetSlackBase));
if (region.desiredFacing == 1)
targetX = MIN<int>(targetX, region.right - targetSlack);
@@ -1431,6 +1428,8 @@ bool Player::setIdleAnimation(RoomPlayerState &playerState, int facing) {
playerState.facing = facing;
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);
@@ -1742,7 +1741,15 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
// run_harvester_main_loop (0x6dc70) latches the clicked region facing at
// 0xc3f0c. update_actor_runtime_state (0x4d750) uses it to keep the matching
// target component active inside the usual X/Z stopping tolerance.
- updatePlayerMoveTargetProgress(state, playerState, regionFacing);
+ const PlayerAnimationRange range = resolvePlayerAnimationRange(playerState.facing);
+ const int currentFrame = playerState.entity->getCurrentFrame();
+ const bool walking = playerState.entity->getAnimationRate() == kRoomPlayerWalkAnimationRate &&
+ 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);
if (hasPlayerReachedMoveTarget(playerState)) {
playerState.hasMoveTarget = false;
return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
@@ -1750,9 +1757,9 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
const int previousCenterX = playerState.centerX;
const int previousBottomY = playerState.bottomY;
- int horizontalDirection = playerState.moveTargetXReached
- ? 0
- : resolveMoveTargetHorizontalDirection(state, playerState, regionFacing);
+ int horizontalDirection = updateTarget
+ ? (playerState.moveTargetXReached ? 0 : resolveMoveTargetHorizontalDirection(state, playerState, regionFacing))
+ : playerState.walkHorizontalDirection;
if (horizontalDirection != 0 && isHorizontalDirectionBlockedByHistory(playerState, horizontalDirection)) {
if (engine.isPathfindingDebugEnabled()) {
debugC(1, kDebugPathfinding,
@@ -1765,9 +1772,9 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
}
horizontalDirection = 0;
}
- int depthDirection = !playerState.moveTargetZReached
- ? resolveMoveTargetDepthDirection(playerState, regionFacing)
- : 0;
+ int depthDirection = updateTarget
+ ? (playerState.moveTargetZReached ? 0 : resolveMoveTargetDepthDirection(playerState, regionFacing))
+ : playerState.walkDepthDirection;
if (depthDirection != 0 && isDepthDirectionBlockedByHistory(playerState, depthDirection)) {
if (engine.isPathfindingDebugEnabled()) {
debugC(1, kDebugPathfinding,
@@ -1801,7 +1808,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
int candidateBottomY = playerState.bottomY;
float candidateZ = playerState.z;
if (horizontalDirection != 0) {
- const int horizontalStep = computeRoomPlayerHorizontalStep(state, playerState.z);
+ const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
candidateCenterX = clampPlayerCenterXToNativeBounds(
playerState, playerState.centerX + horizontalDirection * horizontalStep);
candidateZ = clampRoomDepth(state, playerState.z - depthDirection);
@@ -1813,9 +1820,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
: playerState.z <= (float)state.roomMinZ;
if (!depthBoundaryReached) {
candidateBottomY = playerState.bottomY + depthDirection * verticalStep;
- candidateZ = regionFacing == 0 || regionFacing == 3
- ? clampRoomDepth(state, playerState.z - depthDirection * depthStep)
- : stepTowardsFloat(playerState.z, playerState.targetZ, depthStep);
+ candidateZ = clampRoomDepth(state, playerState.z - depthDirection * depthStep);
}
}
@@ -1865,7 +1870,16 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
}
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(playerState);
refreshPlayerCurrentBlocker(engine, sceneObjects, sceneAnimations, playerState);
if (engine.isPathfindingDebugEnabled()) {
@@ -1875,11 +1889,6 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
playerState.centerX, playerState.bottomY, (double)playerState.z,
actualFacing);
}
- updatePlayerMoveTargetProgress(state, playerState, regionFacing);
- if (hasPlayerReachedMoveTarget(playerState)) {
- playerState.hasMoveTarget = false;
- (void)setIdleAnimation(playerState, 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,
@@ -1929,7 +1938,7 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
const int previousCenterX = playerState.centerX;
const int previousBottomY = playerState.bottomY;
const float previousZ = playerState.z;
- const int horizontalStep = computeRoomPlayerHorizontalStep(state, playerState.z);
+ const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
const int verticalStep = computeRoomPlayerVerticalScreenStep();
const float depthStep = computeRoomPlayerDepthStep(state);
int candidateCenterX = clampPlayerCenterXToNativeBounds(
@@ -1985,6 +1994,8 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
const int actualFacing = resolveFacingFromRoomMovement(
previousBaseX, previousBaseY, playerState.entity->getX(), playerState.entity->getY());
+ 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;
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 1908dc848e5..8a3cb38e265 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -58,11 +58,11 @@ struct RoomPlayerState {
int bottomY = 0;
float z = 0.0f;
int facing = -1;
- // Mirrors native RenderEntityRuntime.player_combat_loadout_id (+0x11bc).
int combatLoadout = 0;
uint32 nextMovementTick = 0;
- // Held input queues the next walk bank; release preserves that request.
int keyboardWalkRequest = -1;
+ int walkHorizontalDirection = 0;
+ int walkDepthDirection = 0;
bool hasMoveTarget = false;
int targetX = 0;
int targetBottomY = 0;
@@ -70,9 +70,7 @@ struct RoomPlayerState {
bool moveTargetXReached = false;
bool moveTargetZReached = false;
Common::String currentBlockerName;
- // Native RenderEntityRuntime keeps these directional blocker-history
- // pointers at +0x108c..+0x1098 and clears them when no current
- // opaque blocker overlaps the actor.
+
Common::String upwardBlockerHistory;
Common::String downwardBlockerHistory;
Common::String leftwardBlockerHistory;
@@ -91,8 +89,7 @@ struct RoomPlayerState {
bool attackSoundPlayed = false;
int attackSoundPlaybackFrame = -1;
bool attackContactResolved = false;
- // Native room combat tracks the live chase/attack focus through
- // RenderEntityRuntime.combat_target_entity (+0x11a4).
+
Common::String attackTargetName;
int attackTargetClassId = -1;
bool hitActive = false;
Commit: 11452159f5c120846b919a2da4109f2b7970c30f
https://github.com/scummvm/scummvm/commit/11452159f5c120846b919a2da4109f2b7970c30f
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:14-04:00
Commit Message:
HARVESTER: fix player bounds checking with room exits
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/entity.cpp
engines/harvester/flow.cpp
engines/harvester/player.cpp
engines/harvester/scene.cpp
engines/harvester/scene_entities.cpp
diff --git a/engines/harvester/entity.cpp b/engines/harvester/entity.cpp
index 52ef2385650..bd471b1d939 100644
--- a/engines/harvester/entity.cpp
+++ b/engines/harvester/entity.cpp
@@ -44,12 +44,8 @@ static const uint32 kDosPitInputFrequency = 1193180;
static const uint32 kDosPitTimerDivisor = 65536;
static const byte kTransparentPaletteIndex = 0;
-static int roundToInt(float value) {
- return value >= 0.0f ? (int)floorf(value + 0.5f) : (int)ceilf(value - 0.5f);
-}
-
static int scaleDimension(uint32 value, float scale) {
- return MAX<int>(roundToInt((float)value * scale), 1);
+ return MAX<int>((int)((float)value * scale), 1);
}
static void scaleIndexedBitmapNearest(const IndexedBitmap &source, IndexedBitmap &dest,
@@ -359,7 +355,7 @@ void Entity::setDepthScale(float scale) {
if (_frames.empty())
return;
- const float newScale = scale > 0.0f ? scale : 1.0f;
+ const float newScale = scale >= 0.0f ? scale : 1.0f;
if (fabsf(_depthScale - newScale) < 0.0001f)
return;
@@ -594,9 +590,9 @@ bool Entity::overlapsEntity(const Entity &other) const {
return false;
}
- const float thisZMax = _z + _zExtent;
- const float otherZMax = other._z + other._zExtent;
- if (thisZMax < other._z || otherZMax < _z)
+ const int thisZMax = (int)(_z + _zExtent);
+ const int otherZMax = (int)(other._z + other._zExtent);
+ if (thisZMax < (int)other._z || otherZMax < (int)_z)
return false;
const Common::Rect thisRect = getFrameRect();
@@ -756,7 +752,7 @@ void Entity::scaleCurrentFrame() {
scaleDimension(source.height, _depthScale));
// Native depth scaling preserves the authored horizontal frame offset.
scaled.xOffset = source.xOffset;
- scaled.yOffset = roundToInt((float)source.yOffset * _depthScale);
+ scaled.yOffset = (int)((float)source.yOffset * _depthScale);
}
_frameDepthScales[frameIndex] = _depthScale;
debugC(3, kDebugPlayer,
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index a90f0af6792..b43355f7873 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -702,7 +702,7 @@ Common::Rect getRoomObjectHotspotBounds(const ObjectRecord &object) {
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 + 1, region.bottom + 1);
+ return Common::Rect(region.left, region.top, region.right, region.bottom);
return Common::Rect();
}
@@ -1103,8 +1103,8 @@ bool doesPlayerOverlapRegion(const Entity &playerEntity, const RegionRecord ®
return false;
}
- const float playerMaxZ = playerEntity.getZ() + playerEntity.getZExtent();
- return playerMaxZ >= (float)region.minZ && (float)region.maxZ >= playerEntity.getZ();
+ const int playerMaxZ = (int)(playerEntity.getZ() + playerEntity.getZExtent());
+ return playerMaxZ >= region.minZ && region.maxZ >= (int)playerEntity.getZ();
}
RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupState &state,
diff --git a/engines/harvester/player.cpp b/engines/harvester/player.cpp
index 66043207e8f..8e83244f67f 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -578,7 +578,7 @@ static int resolveFacingFromRoomMovement(
}
static int computeRoomPlayerHorizontalStep(const RoomSetupState &state, float z) {
- return MAX<int>(1, roundToInt(Player::computeDepthScale(state, z) * kRoomPlayerHorizontalMoveBase));
+ return MAX<int>(1, (int)(Player::computeDepthScale(state, z) * kRoomPlayerHorizontalMoveBase));
}
static float computeRoomPlayerDepthStep(const RoomSetupState &state) {
@@ -836,40 +836,6 @@ static bool tryApplyPlayerMovement(HarvesterEngine &engine, const RoomSetupState
return true;
}
-static bool tryApplyPlayerMovementWithFallbacks(HarvesterEngine &engine, const RoomSetupState &state,
- const Common::Array<ObjectRecord> &sceneObjects,
- const Common::Array<AnimRecord> &sceneAnimations,
- RoomPlayerState &playerState,
- int previousCenterX, int previousBottomY, float previousZ,
- int candidateCenterX, int candidateBottomY, float candidateZ) {
- if (tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, candidateBottomY, candidateZ)) {
- return true;
- }
- if (tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, previousBottomY, candidateZ)) {
- return true;
- }
- if (tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, candidateBottomY, previousZ)) {
- return true;
- }
- if (tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, candidateCenterX, previousBottomY, previousZ)) {
- return true;
- }
- if (tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, previousCenterX, candidateBottomY, candidateZ)) {
- return true;
- }
- if (tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, previousCenterX, candidateBottomY, previousZ)) {
- return true;
- }
- return tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
- playerState, previousCenterX, previousBottomY, candidateZ);
-}
-
static bool isPathfindingDebugEnabled() {
return g_engine && g_engine->isPathfindingDebugEnabled();
}
@@ -996,6 +962,37 @@ static bool recordNativePrimaryWalkBlockerHistory(const RoomSetupState &state,
return false;
}
+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)) {
+ if (blockerOut)
+ *blockerOut = blocker;
+ return false;
+ }
+
+ if (candidateCenterX == playerState.centerX &&
+ candidateBottomY == playerState.bottomY &&
+ 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;
+ Player::syncAnimationPlacement(playerState);
+ reinsertPlayerSceneEntity(engine, playerState);
+ return true;
+}
+
static void setMoveTargetInternal(const RoomSetupState &state, RoomPlayerState &playerState,
int targetX, float targetZ, int targetBottomY) {
playerState.hasMoveTarget = true;
@@ -1172,7 +1169,7 @@ float Player::computeDepthScale(const RoomSetupState &state, float z) {
float scale = 1.0f;
if (state.roomPerspectiveScale != 0.0f) {
scale -= (z - (float)state.roomFullScaleZ) * state.roomPerspectiveScale;
- if (scale <= 0.0f)
+ if (scale < 0.0f)
scale = 1.0f;
}
@@ -1340,7 +1337,7 @@ bool Player::resolveBlockedStartupSpawn(HarvesterEngine &engine, const RoomSetup
int Player::resolveRegionTargetX(const RoomSetupState &state, const RegionRecord ®ion,
const RoomPlayerState &playerState) {
- const Common::Rect bounds(region.left, region.top, region.right + 1, region.bottom + 1);
+ 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);
@@ -1349,13 +1346,6 @@ int Player::resolveRegionTargetX(const RoomSetupState &state, const RegionRecord
if (targetX + kRoomRegionTargetXBias < frameLeftX)
targetX -= kRoomRegionTargetXBias;
- const int targetSlack = MAX<int>(1,
- roundToInt(Player::computeDepthScale(state, computePlayerRenderZ(playerState)) *
- kRoomPlayerHorizontalTargetSlackBase));
- if (region.desiredFacing == 1)
- targetX = MIN<int>(targetX, region.right - targetSlack);
- else if (region.desiredFacing == 2)
- targetX = MAX<int>(targetX, region.left + targetSlack);
return CLIP<int>(targetX, 0, 639);
}
@@ -1835,9 +1825,9 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
}
const Entity *blocker = nullptr;
- bool moved = tryApplyPlayerMovement(engine, state, sceneObjects, sceneAnimations,
+ bool moved = tryApplyPlayerWalkMovement(engine, state, sceneObjects, sceneAnimations,
playerState, candidateCenterX, candidateBottomY, candidateZ,
- nullptr, &blocker);
+ horizontalDirection, depthDirection, &blocker);
if (!moved) {
if (engine.isPathfindingDebugEnabled()) {
const Common::Rect blockerRect = blocker ? blocker->getScreenRect() : Common::Rect();
@@ -1851,8 +1841,7 @@ bool Player::stepMoveTarget(HarvesterEngine &engine, const RoomSetupState &state
blocker ? (double)blocker->getZExtent() : 0.0);
}
setPlayerCurrentBlocker(playerState, blocker);
- if (blocker && recordNativePrimaryWalkBlockerHistory(state, playerState, *blocker,
- horizontalDirection, depthDirection)) {
+ if (blocker) {
return setIdleAnimation(playerState,
resolveFacingFromMoveDirection(horizontalDirection, depthDirection,
playerState.facing));
@@ -1935,9 +1924,6 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
const int previousBaseX = playerState.entity->getX();
const int previousBaseY = playerState.entity->getY();
- const int previousCenterX = playerState.centerX;
- const int previousBottomY = playerState.bottomY;
- const float previousZ = playerState.z;
const int horizontalStep = computeRoomPlayerHorizontalStep(state, computePlayerRenderZ(playerState));
const int verticalStep = computeRoomPlayerVerticalScreenStep();
const float depthStep = computeRoomPlayerDepthStep(state);
@@ -1986,9 +1972,9 @@ bool Player::stepKeyboardMovement(HarvesterEngine &engine, const RoomSetupState
return false;
playerState.hasMoveTarget = false;
- bool moved = tryApplyPlayerMovementWithFallbacks(engine, state, sceneObjects, sceneAnimations,
- playerState, previousCenterX, previousBottomY, previousZ,
- candidateCenterX, candidateBottomY, candidateZ);
+ bool moved = tryApplyPlayerWalkMovement(engine, state, sceneObjects, sceneAnimations,
+ playerState, candidateCenterX, candidateBottomY, candidateZ,
+ horizontalInput, verticalInput);
if (!moved)
return setIdleAnimation(playerState, playerState.facing >= 0 ? playerState.facing : 0);
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 7829e3da046..96e14515427 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -813,10 +813,7 @@ InputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
}
const PlayerControlState control = preparePlayerControlState();
- // Native run_harvester_main_loop checks player/region overlap before
- // update_actor_runtime_state. This is significant when a wide walk frame
- // reaches a narrow exit: the actor update may otherwise satisfy the X
- // target and replace that frame with a narrower idle frame first.
+ advanceRuntimeEntities(needsRedraw);
Common::Error pendingRegionError =
tryActivatePendingRegion(needsRedraw);
if (pendingRegionError.getCode() != Common::kNoError)
@@ -841,9 +838,6 @@ InputResult Scene::runRoomFrames(const Common::String ¤tRoomTarget) {
}
}
- // At 60 Hz, consume animation advances before the next screen update so
- // a walk frame and its movement step are presented together.
- advanceRuntimeEntities(needsRedraw);
Common::Error combatError = Common::kNoError;
if (!control.paused) {
combatError = resolvePlayerAttackContact(needsRedraw);
diff --git a/engines/harvester/scene_entities.cpp b/engines/harvester/scene_entities.cpp
index 0c39ba1fdd8..fba500d6c03 100644
--- a/engines/harvester/scene_entities.cpp
+++ b/engines/harvester/scene_entities.cpp
@@ -567,7 +567,7 @@ void SceneEntities::spawnSceneObjectEntityFromRecord(const ObjectRecord &object)
Common::Rect SceneEntities::getSceneRegionBounds(const RegionRecord ®ion) {
if (region.right > region.left && region.bottom > region.top)
- return Common::Rect(region.left, region.top, region.right + 1, region.bottom + 1);
+ return Common::Rect(region.left, region.top, region.right, region.bottom);
return Common::Rect();
}
Commit: 0cca8a4c2cb38b1b028d66560f6f70c0a1edf103
https://github.com/scummvm/scummvm/commit/0cca8a4c2cb38b1b028d66560f6f70c0a1edf103
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:15-04:00
Commit Message:
HARVESTER: play walking blocked sounds
Fixes #17331
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 8e83244f67f..8d838aa2f48 100644
--- a/engines/harvester/player.cpp
+++ b/engines/harvester/player.cpp
@@ -962,6 +962,28 @@ static bool recordNativePrimaryWalkBlockerHistory(const RoomSetupState &state,
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() != kRoomPlayerWalkAnimationRate ||
+ 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,
@@ -974,6 +996,7 @@ static bool tryApplyPlayerWalkMovement(HarvesterEngine &engine, const RoomSetupS
*blockerOut = nullptr;
if (blocker && recordNativePrimaryWalkBlockerHistory(state, playerState, *blocker,
horizontalDirection, depthDirection)) {
+ playPlayerBlockedWalkSounds(engine, state, playerState, *blocker);
if (blockerOut)
*blockerOut = blocker;
return false;
Commit: 0b24b9b5b0caa395a68bde42e7508a3544d34533
https://github.com/scummvm/scummvm/commit/0b24b9b5b0caa395a68bde42e7508a3544d34533
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-13T17:39:15-04:00
Commit Message:
HARVESTER: cleanup and log verbosity adjustments
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/art.cpp
engines/harvester/combat.cpp
engines/harvester/combat.h
engines/harvester/dialogue.cpp
engines/harvester/flow.cpp
engines/harvester/flow.h
engines/harvester/harvester.cpp
engines/harvester/harvester.h
engines/harvester/input.cpp
engines/harvester/menu.cpp
engines/harvester/menu.h
engines/harvester/scene.cpp
engines/harvester/script.cpp
diff --git a/engines/harvester/art.cpp b/engines/harvester/art.cpp
index 1ed1a09eeef..aab5aed1950 100644
--- a/engines/harvester/art.cpp
+++ b/engines/harvester/art.cpp
@@ -60,7 +60,7 @@ static void logPaletteSummary(const char *label, const Common::String &path, con
byte maxValue = 0;
getPaletteByteRange(palette, minValue, maxValue);
- debugC(1, kDebugGeneral,
+ debugC(3, kDebugGeneral,
"Harvester: %s '%s' bytes=%u range=[%u,%u] hash=%08x idx0=(%u,%u,%u) idx1=(%u,%u,%u) idx127=(%u,%u,%u) idx255=(%u,%u,%u)",
label, path.c_str(), 256U * 3U, minValue, maxValue, hashPalette(palette),
palette[0], palette[1], palette[2],
diff --git a/engines/harvester/combat.cpp b/engines/harvester/combat.cpp
index 45cbbbf6bb2..c3b20c1ad67 100644
--- a/engines/harvester/combat.cpp
+++ b/engines/harvester/combat.cpp
@@ -1497,6 +1497,9 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
}
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;
@@ -1508,7 +1511,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
if (monster.posZ != previousZ)
entityManager->reinsertSceneEntity(entity);
(void)script.syncRuntimeMonsterRecord(monster);
- debugC(1, kDebugCombat,
+ 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,
previousX, previousY, previousZ, monster.posX, monster.posY, monster.posZ, monster.facing);
@@ -1517,8 +1520,8 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
continue;
}
- if (attemptedMoveX || attemptedMoveZ) {
- debugC(1, kDebugCombat,
+ 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.posX, monster.posY, monster.posZ, attemptedMoveX, attemptedMoveZ);
@@ -1558,6 +1561,7 @@ Common::Error Combat::updateRoomMonsterCombat(HarvesterEngine &engine, RoomSetup
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);
diff --git a/engines/harvester/combat.h b/engines/harvester/combat.h
index 84a2e9bf6f3..cb04d5f37ea 100644
--- a/engines/harvester/combat.h
+++ b/engines/harvester/combat.h
@@ -110,6 +110,7 @@ struct MonsterCombatState {
int deathFirstFrame = -1;
int deathLastFrame = -1;
int deathDamageType = 0;
+ int loggedPursuitStallDirections = 0;
bool allowInitialPursuitStep = true;
uint32 nextMovementTick = 0;
uint32 attackCooldownSeedTick = 0;
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 259240bc17f..92e7c579581 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -384,10 +384,10 @@ static void splitDialogueMenuLine(const Common::String &line, Common::Array<Comm
static void logDialogueMenuItems(const char *label, int sourceLineIndex,
const Common::String &rawLine, const Common::Array<Common::String> &items) {
if (sourceLineIndex >= 0) {
- debugC(1, kDebugDialogue, "Harvester: %s source line 0x%x (%d) raw='%s'",
+ debugC(3, kDebugDialogue, "Harvester: %s source line 0x%x (%d) raw='%s'",
label, sourceLineIndex, sourceLineIndex + 1, rawLine.c_str());
} else {
- debugC(1, kDebugDialogue, "Harvester: %s source raw='%s'", label, rawLine.c_str());
+ debugC(3, kDebugDialogue, "Harvester: %s source raw='%s'", label, rawLine.c_str());
}
if (items.empty()) {
@@ -396,7 +396,7 @@ static void logDialogueMenuItems(const char *label, int sourceLineIndex,
}
for (uint i = 0; i < items.size(); ++i)
- debugC(1, kDebugDialogue, "Harvester: %s item[%u]='%s'", label, i + 1, items[i].c_str());
+ debugC(3, kDebugDialogue, "Harvester: %s item[%u]='%s'", label, i + 1, items[i].c_str());
}
static Common::String buildDialogueHeadId(const Common::String &speakerId, int headVariant) {
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index b43355f7873..2b8b985c9b6 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -575,19 +575,8 @@ static uint32 hashPalette(const byte *palette) {
}
void logScenePaletteSummary(const char *label, const SceneState &scene, float brightness) {
- const byte *palette = scene.palette;
- byte minValue = 0;
- byte maxValue = 0;
- getPaletteByteRange(palette, minValue, maxValue);
-
- debugC(1, kDebugRoom,
- "Harvester: %s room='%s' palette='%s' bytes=%u range=[%u,%u] brightness=%.2f hash=%08x idx0=(%u,%u,%u) idx1=(%u,%u,%u) idx127=(%u,%u,%u) idx255=(%u,%u,%u)",
- label, scene.state.roomName.c_str(), scene.state.palettePath.c_str(), 256U * 3U, minValue, maxValue,
- (double)brightness, hashPalette(palette),
- palette[0], palette[1], palette[2],
- palette[3], palette[4], palette[5],
- palette[127 * 3], palette[127 * 3 + 1], palette[127 * 3 + 2],
- palette[255 * 3], palette[255 * 3 + 1], palette[255 * 3 + 2]);
+ debugC(2, kDebugRoom, "Harvester: %s room='%s' palette='%s' brightness=%.2f",
+ label, scene.state.roomName.c_str(), scene.state.palettePath.c_str(), (double)brightness);
}
static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
@@ -684,10 +673,12 @@ static bool loadPaletteResource(ResourceManager &resources, const Common::String
byte minValue = 0;
byte maxValue = 0;
getPaletteByteRange(dest, minValue, maxValue);
- debugC(1, kDebugRoom,
- "Harvester: decoded room palette '%s' bytes=%u range=[%u,%u] idx0=(%u,%u,%u) idx255=(%u,%u,%u)",
- path.c_str(), (uint)data.size(), minValue, maxValue,
+ debugC(3, kDebugRoom,
+ "Harvester: decoded room palette '%s' bytes=%u range=[%u,%u] hash=%08x idx0=(%u,%u,%u) idx1=(%u,%u,%u) idx127=(%u,%u,%u) idx255=(%u,%u,%u)",
+ path.c_str(), (uint)data.size(), minValue, maxValue, hashPalette(dest),
dest[0], dest[1], dest[2],
+ dest[3], dest[4], dest[5],
+ dest[127 * 3], dest[127 * 3 + 1], dest[127 * 3 + 2],
dest[255 * 3], dest[255 * 3 + 1], dest[255 * 3 + 2]);
return true;
@@ -731,7 +722,7 @@ static void logSceneObjectSelection(const char *decision, const char *source, co
const Common::String &detail = Common::String()) {
const Common::Rect hotspotBounds = getRoomObjectHotspotBounds(object);
const Common::String resolvedSpritePath = resolveSceneObjectSpritePath(object);
- debugC(1, kDebugScene,
+ debugC(3, kDebugScene,
"Harvester: scene object %s source='%s' object='%s' owner='%s' visible=%d runtimeVisible=%d sprite='%s' alt='%s' resolved='%s' pos=(%d,%d,%d) bounds=(%d,%d)-(%d,%d) action='%s' detail='%s'",
decision, source, object.objectName.c_str(), object.currentOwnerOrRoom.c_str(),
object.visible, object.runtimeVisible, object.spritePath.c_str(), object.altSpritePath.c_str(),
@@ -789,12 +780,11 @@ bool loadRoomSceneResources(const RoomSetupState &state, ResourceManager &resour
}
scene.sceneRegions = state.roomRegions;
- debugC(1, kDebugRoom,
- "Harvester: loadRoomSceneResources room='%s' palette='%s' background='%s' brightness=%.2f objects=%u activeObjects=%u sceneObjects=%u anims=%u visibleAnims=%u npcs=%u monsters=%u regions=%u",
- state.roomName.c_str(), state.palettePath.c_str(), state.backgroundPath.c_str(), (double)state.paletteBrightness,
- (uint)state.roomObjects.size(), (uint)state.activeObjects.size(), (uint)scene.sceneObjects.size(),
- (uint)state.roomAnimations.size(), (uint)scene.sceneAnimations.size(),
- (uint)state.roomNpcs.size(), (uint)state.roomMonsters.size(), (uint)scene.sceneRegions.size());
+ debugC(2, kDebugRoom,
+ "Harvester: room scene loaded room='%s' objects=%u anims=%u npcs=%u monsters=%u timers=%u regions=%u",
+ state.roomName.c_str(), (uint)scene.sceneObjects.size(), (uint)scene.sceneAnimations.size(),
+ (uint)state.roomNpcs.size(), (uint)state.roomMonsters.size(),
+ (uint)state.roomTimers.size(), (uint)scene.sceneRegions.size());
return true;
}
@@ -1432,7 +1422,7 @@ Common::Error Flow::run() {
_engine.stopMusic();
_engine.stopSound();
- return runMainMenuStub();
+ return runMainMenu();
}
bool Flow::loadQuickTips() {
@@ -1743,8 +1733,8 @@ Common::Error Flow::runDemoIntroduction() {
return Common::kNoError;
}
-Common::Error Flow::runMainMenuStub() {
- return _menu.runMainMenuStub(*this);
+Common::Error Flow::runMainMenu() {
+ return _menu.runMainMenu(*this);
}
Common::Error Flow::showCdChangePrompt(int discNumber) {
@@ -1781,9 +1771,9 @@ Common::Error Flow::showCdChangePrompt(int discNumber) {
return Common::kNoError;
}
-Common::Error Flow::runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
+Common::Error Flow::runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, bool canSaveGame) {
- return _menu.runRoomMenuStub(backdrop, palette, paletteBrightness, *this, canSaveGame);
+ return _menu.runRoomMenu(backdrop, palette, paletteBrightness, *this, canSaveGame);
}
Common::Error Flow::runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index f5d2157cfc8..445e629dd45 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -71,8 +71,8 @@ private:
Common::Error runQuickTips();
Common::Error runDemoIntroduction();
Common::Error runDemoEnding();
- Common::Error runMainMenuStub();
- Common::Error runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
+ Common::Error runMainMenu();
+ Common::Error runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, bool canSaveGame);
Common::Error runRoomNpcDialogue(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, const NpcRecord &npc, const Common::String &usedItemName);
diff --git a/engines/harvester/harvester.cpp b/engines/harvester/harvester.cpp
index d8373fad21b..9896cfeecaa 100644
--- a/engines/harvester/harvester.cpp
+++ b/engines/harvester/harvester.cpp
@@ -328,7 +328,6 @@ Common::Error HarvesterEngine::run() {
// The intro FST files play on the narrower startup movie surface.
setDisplayMode(320, 200);
- // Set the engine's debugger console
setDebugger(new Console());
if (shouldSkipIntroMoviesForDebug()) {
diff --git a/engines/harvester/harvester.h b/engines/harvester/harvester.h
index 7ffd3941cdc..805a718994a 100644
--- a/engines/harvester/harvester.h
+++ b/engines/harvester/harvester.h
@@ -52,16 +52,10 @@ public:
Common::Error run() override;
bool hasFeature(EngineFeature f) const override;
- /**
- * Returns the game Id
- */
Common::String getGameId() const;
Common::Language getLanguage() const;
bool isDemo() const;
- /**
- * Gets a random number
- */
uint32 getRandomNumber(uint maxNum) {
return _randomSource.getRandomNumber(maxNum);
}
@@ -69,10 +63,6 @@ public:
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
- /**
- * Uses a serializer to allow implementing savegame
- * loading and saving using a single method
- */
Common::Error syncGame(Common::Serializer &s);
Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override;
Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
diff --git a/engines/harvester/input.cpp b/engines/harvester/input.cpp
index f55c8c14b3b..c4a93f1e195 100644
--- a/engines/harvester/input.cpp
+++ b/engines/harvester/input.cpp
@@ -735,7 +735,7 @@ InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw)
drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
return InputResult::leaveRoom(Common::kReadingFailed);
- Common::Error menuError = _flow.runRoomMenuStub(
+ Common::Error menuError = _flow.runRoomMenu(
roomMenuBackdrop, _scene.palette, _scene.targetPaletteBrightness,
_scene.state.hasEntrance);
if (menuError.getCode() != Common::kNoError)
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 9cbcd4b90b9..0a60adc48f4 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -368,11 +368,6 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
loadMenuDisplayValue(menu, "keyword", config.dialogueKeywordLabel);
for (uint i = 0; i < config.weekdayLabels.size(); ++i)
loadMenuDisplayValue(menu, kMenuWeekdays[i].key, config.weekdayLabels[i]);
- debugC(2, kDebugGeneral,
- "Harvester: inventory weekdays monday='%s' tuesday='%s' wednesday='%s' thursday='%s' friday='%s' saturday='%s'",
- config.weekdayLabels[0].c_str(), config.weekdayLabels[1].c_str(),
- config.weekdayLabels[2].c_str(), config.weekdayLabels[3].c_str(),
- config.weekdayLabels[4].c_str(), config.weekdayLabels[5].c_str());
loadMenuDisplayValue(menu, "Exit", config.quickTipsExitLabel);
loadMenuDisplayValue(menu, "next", config.quickTipsNextLabel);
loadMenuDisplayValue(menu, "show_tips_on", config.quickTipsOnLabel);
@@ -934,7 +929,7 @@ Common::Error MenuSystem::showCdChangePrompt(int discNumber, ResourceManager &re
return Common::kNoError;
}
-Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
+Common::Error MenuSystem::runMainMenu(Flow &flow) {
const Art *art = _engine.getArt();
if (!art)
return Common::kReadingFailed;
@@ -1090,7 +1085,7 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
if (_hasMainMenuBackdrop)
renderBackdropMenuScreen(_mainMenuBackdrop, _mainMenuBackdropPalette, 1.0f, mainMenuItems, selectedItem);
else
- renderMainMenuStub(mainMenuItems, selectedItem, statusMessage);
+ renderMainMenuScreen(selectedItem, statusMessage, true, mainMenuItems);
needsRedraw = false;
}
@@ -1231,7 +1226,7 @@ void MenuSystem::clearMainMenuBackdrop() {
_hasMainMenuBackdrop = false;
}
-Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
+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;
@@ -2481,11 +2476,6 @@ Common::Error MenuSystem::runHelpScreen(const byte *palette, float paletteBright
return Common::kNoError;
}
-void MenuSystem::renderMainMenuStub(const Common::Array<Common::String> &menuItems, int selectedItem,
- const Common::String &statusMessage) const {
- renderMainMenuScreen(selectedItem, statusMessage, true, menuItems);
-}
-
void MenuSystem::renderMainMenuScreen(int selectedItem, const Common::String &statusMessage,
bool drawCursor, const Common::Array<Common::String> &menuItems) const {
Graphics::Screen *screen = _engine.getScreen();
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index be59851e1ac..58dd3782ca9 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -88,8 +88,8 @@ public:
MenuSystem(HarvesterEngine &engine, Common::Point &mousePos,
const Common::Array<Common::String> &menuItems);
- Common::Error runMainMenuStub(Flow &flow);
- Common::Error runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
+ Common::Error runMainMenu(Flow &flow);
+ Common::Error runRoomMenu(const IndexedBitmap &backdrop, const byte *palette,
float paletteBrightness, Flow &flow, bool canSaveGame);
Common::Error validateParentalPassword(Flow &flow);
Common::Error showCdChangePrompt(int discNumber, ResourceManager &resources,
@@ -115,8 +115,6 @@ private:
bool &accepted) const;
Common::Error showGameOverBackdrop(Flow &flow);
void clearMainMenuBackdrop();
- void renderMainMenuStub(const Common::Array<Common::String> &menuItems, int selectedItem,
- const Common::String &statusMessage) const;
void renderMainMenuScreen(int selectedItem, const Common::String &statusMessage,
bool drawCursor, const Common::Array<Common::String> &menuItems) const;
void renderBackdropMenuScreen(const IndexedBitmap &backdrop, const byte *palette,
diff --git a/engines/harvester/scene.cpp b/engines/harvester/scene.cpp
index 96e14515427..7f92daf4227 100644
--- a/engines/harvester/scene.cpp
+++ b/engines/harvester/scene.cpp
@@ -1130,7 +1130,7 @@ Common::Error SceneRunner::runRoomLoop(Flow &flow, const Common::String &targetN
if (transitionError.getCode() != Common::kNoError)
return transitionError;
- logScenePaletteSummary("room setup stub palette", scene, 0.0f);
+ logScenePaletteSummary("room setup initial palette", scene, 0.0f);
drawRoomScene(_engine, *screen, scene, 0.0f);
screen->makeAllDirty();
screen->update();
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index 6cf913b48c4..de71ae5f6e5 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -1384,7 +1384,7 @@ bool Script::resolveRoomSetupState(const Common::String &entranceName, RoomSetup
if (!buildRuntimeRoomState(*room, entrance, resources, state))
return false;
- debugC(1, kDebugRoom,
+ 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,
@@ -1419,7 +1419,7 @@ bool Script::resolveRoomSetupStateByRoomName(const Common::String &roomName, Roo
if (!buildRuntimeRoomState(*room, entrance, resources, state))
return false;
- debugC(1, kDebugRoom,
+ 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,
@@ -1502,7 +1502,7 @@ void Script::logRuntimeSaveState(const char *operation) const {
}
}
if (!baseFlag || flag.value != baseFlag->value) {
- debugC(1, kDebugGeneral,
+ 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);
@@ -1533,7 +1533,7 @@ void Script::logRuntimeSaveState(const char *operation) const {
object.runtimeVisible != defaultRuntimeVisible ||
object.visible != defaultVisible ||
object.identShown != defaultIdentShown) {
- debugC(1, kDebugGeneral,
+ 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,
@@ -1566,7 +1566,7 @@ void Script::logRuntimeSaveState(const char *operation) const {
npc.deathDamageType != defaultDeathDamageType ||
npc.runtimeSpawned ||
npc.runtimeState != defaultRuntimeState) {
- debugC(1, kDebugGeneral,
+ 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,
@@ -1595,7 +1595,7 @@ void Script::logRuntimeSaveState(const char *operation) const {
monster.runtimeState != -1 ||
monster.screenMinXBound != defaultScreenMinXBound ||
monster.screenMaxXBound != defaultScreenMaxXBound) {
- debugC(1, kDebugGeneral,
+ 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,
@@ -1627,7 +1627,7 @@ void Script::logRuntimeSaveState(const char *operation) const {
timer.enabled != defaultEnabled ||
timer.looping != defaultLooping ||
timer.global != defaultGlobal) {
- debugC(1, kDebugGeneral,
+ 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,
@@ -2358,12 +2358,8 @@ bool Script::consumePlayerCombatResourceUnit(int loadout) {
if (maxCount <= 0)
return true;
- if (getPlayerCombatResourceCount(loadout) < 1) {
- debugC(1, kDebugGeneral,
- "Harvester: PLAYER_ATTACK player combat resource loadout=%d('%s') delta=0 count=0->0 max=%d changed=0",
- loadout, Player::describeCombatLoadout(loadout), maxCount);
+ if (getPlayerCombatResourceCount(loadout) < 1)
return false;
- }
return adjustPlayerCombatResourceCount(loadout, -1, maxCount, "PLAYER_ATTACK");
}
@@ -2689,7 +2685,7 @@ bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord
: kDefaultPaletteBrightness;
for (const ObjectRecord &object : state.roomObjects) {
- debugC(1, kDebugRoom,
+ 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(),
@@ -2697,7 +2693,7 @@ bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord
object.currentX, object.currentY, object.boundsX2, object.boundsY2, object.actionTag.c_str());
}
for (const NpcRecord &npc : state.roomNpcs) {
- debugC(1, kDebugRoom,
+ 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,
@@ -2705,7 +2701,7 @@ bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord
npc.modelPath.c_str(), npc.onDeathActionTag.c_str(), npc.audioPath.c_str());
}
for (const MonsterRecord &monster : state.roomMonsters) {
- debugC(1, kDebugRoom,
+ 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,
@@ -2715,13 +2711,13 @@ bool Script::buildRuntimeRoomState(const RoomRecord &room, const EntranceRecord
monster.modelPath.c_str(), monster.onDeathActionTag.c_str());
}
for (const TimerRecord &timer : state.roomTimers) {
- debugC(1, kDebugRoom,
+ 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(1, kDebugRoom,
+ 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(),
@@ -2783,7 +2779,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const Command
break;
}
- debugC(1, kDebugScene,
+ 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());
@@ -2791,7 +2787,7 @@ void Script::executeCommandChain(const Common::String &initialTag, const Command
if (command->opcodeName.equalsIgnoreCase("CHECK_FLAG")) {
const FlagRecord *flag = findRuntimeFlag(command->arg1);
const bool flagValue = flag && flag->value;
- debugC(1, kDebugScene, "Harvester: %s '%s' flag '%s' -> %d",
+ 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;
More information about the Scummvm-git-logs
mailing list