WASD: direction movement steers the walk path directly
The keys rewrite the hero's path every cCreature::Update: one segment along the held direction, the facing set to it, sliding along blocked cells. Starting from idle runs the game's own Think steps at once (StartPendingWalk, WalkAction) instead of waiting up to 110 ms, so weapon switches no longer stop the walk. Stops wait 50 ms after the keys are released, and a stop in progress is taken back when keys are held again. Names for movement, stats and text-key functions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d296e2850b
commit
84c50d8c33
6 files changed
+422
-104
No files matched your search
+217
-76
@@ -1,13 +1,29 @@
|
||||
// WASD movement for the player character.
|
||||
// WASD movement for the player character: the keys steer the hero directly
|
||||
// (request of 2026-10-18: direction movement instead of walk orders).
|
||||
//
|
||||
// While W, A, S or D is held, the controlled creature gets a plain walk order
|
||||
// (cEvent_creature 4 to a ground point) every engine tick, aimed a cursor's
|
||||
// distance from it in the pressed screen direction. That is what the mouse
|
||||
// does while its button is held: every mouse event re-sends a walk to the point
|
||||
// under the cursor (cEngine::HandleMouseButton), which keeps the creature
|
||||
// walking smoothly. (The "held" re-aim orders cEngine::Tick sends as well stop
|
||||
// it when sent on their own.) Releasing the keys sends the target-less walk
|
||||
// order a released mouse button sends, which stops it in place.
|
||||
// The game walks a creature along a path of waypoints in cCreature::Update
|
||||
// (0x594110, about 40 times per second; path layout in game/engine/orders.h).
|
||||
// While W, A, S or D is held, each update of the controlled creature first
|
||||
// replaces its path with one straight segment from where it stands, far into
|
||||
// the pressed screen direction: the game's own step then moves it at its own
|
||||
// speed, with its walk animation, and its step test refuses blocked cells. No
|
||||
// pathfinding, no arrival, and a new direction counts from the next update.
|
||||
//
|
||||
// Obstacles: the cell one step ahead is probed with cCreature::IsCellWalkable
|
||||
// (measured: walking straight into a fence, 27 of 28 steps were refused).
|
||||
// When it is blocked, the direction turned by up to 90 degrees is tried,
|
||||
// the side the hero last slid to first, so it slides along walls instead of
|
||||
// running on the spot; with everything blocked it stands.
|
||||
//
|
||||
// The facing is set to the direction too: the game turns it towards the path
|
||||
// over about 300 ms, which made reversals look like walking backwards.
|
||||
//
|
||||
// The path only drives a creature that walks. Standing idle (at the start, or
|
||||
// after an attack, a cast, a weapon switch, which resets the hero to idle) it
|
||||
// gets a walk order, and the two steps of cCreature::Think that would start it
|
||||
// up to 110 ms later run at once: the walk goes on in the same update.
|
||||
// Releasing the keys sends the target-less walk order a released mouse button
|
||||
// sends, which stops it in place.
|
||||
//
|
||||
// W, A and S are game hotkeys; the key bindings (src/mods/input.cpp) keep
|
||||
// W/A/S/D away from the game and move those actions to other keys. Typing into
|
||||
@@ -34,9 +50,25 @@ using game::ControlledCreature;
|
||||
using game::CreatureOrder;
|
||||
using game::kEngineFlags;
|
||||
|
||||
// Walk orders aim this far (world units) from the creature, about where the
|
||||
// cursor would be.
|
||||
constexpr double kTargetDistance = 300.0;
|
||||
// The walk order that starts walking aims this far (world units).
|
||||
constexpr double kStartDistance = 300.0;
|
||||
// Between two start orders while the walk has not begun yet.
|
||||
constexpr DWORD kStartOrderMs = 150;
|
||||
// Keys released this long before the hero stops: changing keys often leaves
|
||||
// a moment without any (measured: a stop order there cost about 125 ms
|
||||
// standing).
|
||||
constexpr DWORD kStopGraceMs = 50;
|
||||
// The steered segment's length: farther than any step, so it never arrives.
|
||||
constexpr double kSegmentLength = 1000.0;
|
||||
// How far ahead a direction is probed: a bit more than one map cell.
|
||||
constexpr double kProbeDistance = game::kMapCellSize * 1.25;
|
||||
constexpr double kProbeStep = game::kMapCellSize * 0.25;
|
||||
// Directions tried around a blocked one (degrees, both sides).
|
||||
constexpr double kSlideAngles[] = {30.0, 60.0, 90.0};
|
||||
|
||||
// Engine flags (+0x54): a mouse button is held / input is blocked.
|
||||
constexpr uint32_t kEngineMouseHeld = 0x10;
|
||||
constexpr uint32_t kEngineInputBlocked = 0x4000;
|
||||
|
||||
bool KeyDown(int key) {
|
||||
return (GetAsyncKeyState(key) & 0x8000) != 0;
|
||||
@@ -59,10 +91,125 @@ void SendStop(uint8_t* engine, void* creature) {
|
||||
game::SendCreatureOrder(engine, creature, order);
|
||||
}
|
||||
|
||||
struct MoveState {
|
||||
bool moving = false;
|
||||
std::atomic<uint8_t*> g_engine{nullptr};
|
||||
bool g_moving = false;
|
||||
DWORD g_releasedAt = 0; // when the keys were released (0: held)
|
||||
DWORD g_startOrderAt = 0;
|
||||
int g_slideSide = 1; // +1 / -1: the side the hero last turned to
|
||||
|
||||
struct Direction {
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
MoveState g_move;
|
||||
|
||||
// The held movement keys as a unit screen direction in world axes: (-1,-1)
|
||||
// up, (1,1) down, (-1,1) left, (1,-1) right. False when none (or the keys
|
||||
// belong to the mouse, blocked input or a text field).
|
||||
bool HeldDirection(const uint8_t* engine, Direction& dir) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t*>(engine + kEngineFlags);
|
||||
if ((flags & (kEngineMouseHeld | kEngineInputBlocked)) == 0 && !mods::input::TypingIntoTextField()) {
|
||||
if (KeyDown('W')) { x -= 1; y -= 1; }
|
||||
if (KeyDown('S')) { x += 1; y += 1; }
|
||||
if (KeyDown('A')) { x -= 1; y += 1; }
|
||||
if (KeyDown('D')) { x += 1; y -= 1; }
|
||||
}
|
||||
if (x == 0 && y == 0) {
|
||||
return false;
|
||||
}
|
||||
const double length = std::sqrt(static_cast<double>(x * x + y * y));
|
||||
dir = {x / length, y / length};
|
||||
return true;
|
||||
}
|
||||
|
||||
Direction Rotated(Direction dir, double degrees) {
|
||||
const double a = degrees * 3.14159265358979 / 180.0;
|
||||
return {dir.x * std::cos(a) - dir.y * std::sin(a), dir.x * std::sin(a) + dir.y * std::cos(a)};
|
||||
}
|
||||
|
||||
// Whether the creature can go a probe distance along `dir`: every cell the
|
||||
// way crosses (sampled at quarter cells, so none is skipped) is walkable.
|
||||
bool Open(uint8_t* creature, Direction dir) {
|
||||
const int x = game::CreatureField(creature, game::kCreatureX);
|
||||
const int y = game::CreatureField(creature, game::kCreatureY);
|
||||
game::MapPosition last = game::CellAt(creature, x, y);
|
||||
for (double d = kProbeStep; d <= kProbeDistance; d += kProbeStep) {
|
||||
game::MapPosition cell =
|
||||
game::CellAt(creature, x + static_cast<int>(dir.x * d), y + static_cast<int>(dir.y * d));
|
||||
if (cell.x == last.x && cell.y == last.y) {
|
||||
continue;
|
||||
}
|
||||
if (!game::IsCellWalkable(creature, cell)) {
|
||||
return false;
|
||||
}
|
||||
last = cell;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The direction to go for keys pointing along `dir`: itself, or the nearest
|
||||
// open one within 90 degrees (the last slide side first). False if blocked.
|
||||
bool OpenDirection(uint8_t* creature, Direction dir, Direction& out) {
|
||||
if (Open(creature, dir)) {
|
||||
out = dir;
|
||||
return true;
|
||||
}
|
||||
const int sides[] = {g_slideSide, -g_slideSide};
|
||||
for (const double angle : kSlideAngles) {
|
||||
for (const int side : sides) {
|
||||
const Direction turned = Rotated(dir, side * angle);
|
||||
if (Open(creature, turned)) {
|
||||
g_slideSide = side;
|
||||
out = turned;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// One straight segment from the creature's position along `dir`.
|
||||
void Steer(uint8_t* creature, Direction dir) {
|
||||
const int x = game::CreatureField(creature, game::kCreatureX);
|
||||
const int y = game::CreatureField(creature, game::kCreatureY);
|
||||
auto* path = reinterpret_cast<int32_t*>(creature + game::kCreaturePath);
|
||||
path[0] = x;
|
||||
path[1] = y;
|
||||
path[2] = x + static_cast<int>(dir.x * kSegmentLength);
|
||||
path[3] = y + static_cast<int>(dir.y * kSegmentLength);
|
||||
*reinterpret_cast<int32_t*>(creature + game::kCreaturePathIndex) = 0;
|
||||
*reinterpret_cast<int32_t*>(creature + game::kCreaturePathCount) = 2;
|
||||
auto* facing = reinterpret_cast<float*>(creature + game::kCreatureFacing);
|
||||
facing[0] = static_cast<float>(dir.x);
|
||||
facing[1] = static_cast<float>(dir.y);
|
||||
}
|
||||
|
||||
// Starts walking along `dir` now: the walk order, then the two Think steps
|
||||
// that would otherwise follow up to 110 ms later (start the pending walk, the
|
||||
// walk action's first phase). True if the creature walks.
|
||||
bool StartWalk(uint8_t* engine, uint8_t* creature, Direction dir) {
|
||||
SendWalk(engine, creature, game::CreatureField(creature, game::kCreatureX) + static_cast<int>(dir.x * kStartDistance),
|
||||
game::CreatureField(creature, game::kCreatureY) + static_cast<int>(dir.y * kStartDistance));
|
||||
if (*reinterpret_cast<uint16_t*>(creature + game::kCreaturePendingAction) == game::kPendingWalk) {
|
||||
game::StartPendingWalk(creature);
|
||||
}
|
||||
if (game::CurrentAction(creature) != game::kActionWalk) {
|
||||
return false; // held by a status effect: Think decides later
|
||||
}
|
||||
if (game::CreatureField(creature, game::kCreatureAction) >> 16 == 0) {
|
||||
game::WalkAction(creature);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnTick(uint8_t* engine) {
|
||||
g_engine = engine;
|
||||
}
|
||||
|
||||
struct Registration {
|
||||
Registration() { game::OnEngineTick(&OnTick); }
|
||||
} g_registration;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -70,78 +217,72 @@ bool IsMovementKey(int vk) {
|
||||
return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D');
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The held movement keys as a screen direction in world axes: (-1,-1) up,
|
||||
// (1,1) down, (-1,1) left, (1,-1) right; 0,0 when none (or the keys belong to
|
||||
// the mouse, blocked input or a text field).
|
||||
void HeldDirection(uint8_t* engine, int& dx, int& dy) {
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
|
||||
if ((flags & 0x10) == 0 && (flags & 0x4000) == 0 && !mods::input::TypingIntoTextField()) {
|
||||
if (KeyDown('W')) { dx -= 1; dy -= 1; }
|
||||
if (KeyDown('S')) { dx += 1; dy += 1; }
|
||||
if (KeyDown('A')) { dx -= 1; dy += 1; }
|
||||
if (KeyDown('D')) { dx += 1; dy -= 1; }
|
||||
// Before each update of a creature: steer the controlled one.
|
||||
void BeforeCreatureUpdate(uint8_t* creature) {
|
||||
uint8_t* engine = g_engine.load();
|
||||
if (!engine || creature != ControlledCreature(engine)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Walk(uint8_t* engine, void* creature, int dx, int dy) {
|
||||
const double length = std::sqrt(static_cast<double>(dx * dx + dy * dy));
|
||||
const int x = game::CreatureField(creature, game::kCreatureX) + static_cast<int>(dx / length * kTargetDistance);
|
||||
const int y = game::CreatureField(creature, game::kCreatureY) + static_cast<int>(dy / length * kTargetDistance);
|
||||
SendWalk(engine, creature, x, y);
|
||||
}
|
||||
|
||||
std::atomic<uint8_t*> g_engine{nullptr};
|
||||
|
||||
// Called once per engine tick (10 Hz), after the game's own input handling.
|
||||
void Update(uint8_t* engine) {
|
||||
g_engine = engine;
|
||||
int dx;
|
||||
int dy;
|
||||
HeldDirection(engine, dx, dy);
|
||||
const bool mouseMoving = (*reinterpret_cast<uint32_t*>(engine + kEngineFlags) & 0x10) != 0;
|
||||
if (dx == 0 && dy == 0) {
|
||||
if (g_move.moving && !mouseMoving) {
|
||||
if (void* creature = ControlledCreature(engine)) {
|
||||
Direction keys;
|
||||
if (!HeldDirection(engine, keys)) {
|
||||
const DWORD now = GetTickCount();
|
||||
if (g_moving && !g_releasedAt) {
|
||||
g_releasedAt = now;
|
||||
}
|
||||
if (g_moving && now - g_releasedAt >= kStopGraceMs) {
|
||||
if ((*reinterpret_cast<uint32_t*>(engine + kEngineFlags) & kEngineMouseHeld) == 0) {
|
||||
SendStop(engine, creature);
|
||||
}
|
||||
g_moving = false;
|
||||
}
|
||||
g_move.moving = false;
|
||||
return;
|
||||
}
|
||||
void* creature = ControlledCreature(engine);
|
||||
if (!creature) {
|
||||
g_moving = true;
|
||||
g_releasedAt = 0;
|
||||
int32_t action = game::CurrentAction(creature);
|
||||
if (action == game::kActionStop) {
|
||||
action = game::kActionIdle; // stopping: start again like from standing
|
||||
}
|
||||
if (action != game::kActionWalk && action != game::kActionIdle) {
|
||||
return; // attacking, casting, dying: the action finishes first
|
||||
}
|
||||
Direction dir;
|
||||
const bool open = OpenDirection(creature, keys, dir);
|
||||
if (action == game::kActionWalk) {
|
||||
auto* phase = reinterpret_cast<uint16_t*>(creature + game::kCreatureActionPhase);
|
||||
if (open) {
|
||||
if (*phase == game::kPhaseStopping) {
|
||||
*phase = 1; // a stop requested but not begun: walk on
|
||||
}
|
||||
Steer(creature, dir);
|
||||
} else if (*phase != game::kPhaseStopping) {
|
||||
SendStop(engine, creature); // stand instead of running on the spot
|
||||
}
|
||||
return;
|
||||
}
|
||||
Walk(engine, creature, dx, dy);
|
||||
g_move.moving = true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ResumeWalk() {
|
||||
uint8_t* engine = g_engine.load();
|
||||
void* creature = engine && g_move.moving ? ControlledCreature(engine) : nullptr;
|
||||
int dx;
|
||||
int dy;
|
||||
if (creature) {
|
||||
HeldDirection(engine, dx, dy);
|
||||
if (dx != 0 || dy != 0) {
|
||||
Walk(engine, creature, dx, dy);
|
||||
const DWORD now = GetTickCount();
|
||||
if (open && now - g_startOrderAt >= kStartOrderMs) {
|
||||
g_startOrderAt = now;
|
||||
if (StartWalk(engine, creature, dir)) {
|
||||
g_startOrderAt = 0;
|
||||
Steer(creature, dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct Registration {
|
||||
Registration() {
|
||||
game::OnEngineTick(&Update);
|
||||
}
|
||||
} g_registration;
|
||||
} // namespace
|
||||
|
||||
} // namespace mods::wasd
|
||||
|
||||
namespace game {
|
||||
|
||||
// cCreature::Update (0x594110, thiscall (u8), ret 4): moves the creature
|
||||
// along its path, among others.
|
||||
void __fastcall Creature_Update(void* self, void* edx, uint32_t arg);
|
||||
RE_HOOK(0x00594110, Creature_Update);
|
||||
void __fastcall Creature_Update(void* self, void* edx, uint32_t arg) {
|
||||
if (self) {
|
||||
mods::wasd::BeforeCreatureUpdate(static_cast<uint8_t*>(self));
|
||||
}
|
||||
Creature_Update_original(self, edx, arg);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user