Potion counts on the bottom bar and potion stacks in the inventory

- potioncounts.cpp: each potion slot shows how many potions of its kind the
  hero carries (stacks added up)
- potionstack.cpp: potions of one type stack up to 20 per cell; every
  potion entering the hero's grid pours into stacks with room, the sort
  button stacks first, drinking takes one off a stack, counts drawn on the
  cells and saved in the JSON inventories
- game/engine/items.h: potion kinds and grid helpers; ObjectManager and
  DestroyObject in objects.h
- auto-attack: the projectile-weapon target radius is a design value
- docs/potion-stacks.md: measured paths and the design

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-17 01:31:01 +02:00
1 parent b2e886c7f5
commit 13b91cdd3d
13 files changed
+612 -10

No files matched your search

+2 -1
View File
@@ -2,6 +2,7 @@
#include <vector>
#include "game/engine/objects.h"
#include "re/hook.h"
namespace game {
@@ -29,7 +30,7 @@ void BeforeFxEvent(FxEventFn callback) {
uint32_t CreateObjectAt(uint32_t type, const void* position, uint32_t link) {
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, const void*, uint32_t, uint32_t, uint32_t);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
void* manager = ObjectManager();
return manager ? reinterpret_cast<Fn>(0x005fbaa0)(manager, nullptr, type, position, link, 0, 0) : 0;
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// Items and the inventory grid (cInventoryEntry, docs/skill-slots.md).
#include <cstddef>
#include <cstdint>
#include "game/engine/objects.h"
namespace game {
// Item objects (cItemBase and subclasses such as cPotion3D).
constexpr size_t kItemType = 0x10;
constexpr uint32_t kObjectUsedUp = 0x8000000; // kObjectFlags: set by cPotion3D::vf18 when drunk
// The item type table at *0xaa9564 (ecx of ItemPotionKind).
constexpr uint32_t kItemTypeTable = 0x00aa9564;
// ItemPotionKind (0x43f630): the potion kind (1..5, the bottom bar's potion
// slots; 1 = healing) of item type `type`, 0 if it is no potion.
constexpr int kPotionKinds = 5;
inline int ItemPotionKind(uint32_t type) {
using Fn = int(__fastcall*)(void* table, void* edx, uint32_t type);
void* table = *reinterpret_cast<void**>(kItemTypeTable);
return table ? reinterpret_cast<Fn>(0x0043f630)(table, nullptr, type) : 0;
}
inline uint32_t ItemType(const void* item) {
return *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemType);
}
// The potion kind of object `id`, 0 if it is no potion (or gone).
inline int PotionKindOf(uint32_t id) {
const void* item = id ? GetObjectById(id) : nullptr;
return item ? ItemPotionKind(ItemType(item)) : 0;
}
// cInventoryEntry item grid: cells of {u32 item id, u8 flags, u8, u8, u8,
// u16 type, u16}. An item's id is in its first cell only; the other cells it
// covers have id 0, flags bit 0 and the first cell's x, y at +5 / +6
// (cInventoryEntry::GetItemAt 0x5da290 resolves them).
constexpr size_t kInventoryGrid = 0x18;
constexpr size_t kGridCellSize = 0xc;
constexpr size_t kGridWidth = 0x115c; // u16, 0x14
constexpr size_t kGridHeight = 0x115e; // u16, 8
constexpr size_t kInventoryOwner = 0x1160; // u16: player index (heroes < 0x12)
inline unsigned GridCells(const uint8_t* inventory) {
return *reinterpret_cast<const uint16_t*>(inventory + kGridWidth) *
*reinterpret_cast<const uint16_t*>(inventory + kGridHeight);
}
inline uint32_t GridItem(const uint8_t* inventory, unsigned cell) {
return *reinterpret_cast<const uint32_t*>(inventory + kInventoryGrid + cell * kGridCellSize);
}
// Calls `fn(id)` for each item in `inventory`'s grid.
template <typename Fn>
void ForEachGridItem(const uint8_t* inventory, Fn fn) {
const unsigned cells = GridCells(inventory);
for (unsigned cell = 0; cell < cells; ++cell) {
if (const uint32_t id = GridItem(inventory, cell)) {
fn(id);
}
}
}
} // namespace game
+17 -3
View File
@@ -38,13 +38,27 @@ inline void* DynamicCast(void* object, uint32_t sourceType, uint32_t targetType)
return object ? reinterpret_cast<Fn>(0x00849701)(object, 0, sourceType, targetType, 0) : nullptr;
}
// cObjectManager::GetObject (0x5fe060) on the manager at *0xad3bc0.
// The object manager (cObjectManager at *0xad3bc0), or null.
inline void* ObjectManager() {
return *reinterpret_cast<void**>(0x00ad3bc0);
}
// cObjectManager::GetObject (0x5fe060).
inline void* GetObjectById(uint32_t id) {
using Fn = void*(__fastcall*)(void* manager, void* edx, uint32_t id);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
void* manager = ObjectManager();
return manager ? reinterpret_cast<Fn>(0x005fe060)(manager, nullptr, id) : nullptr;
}
// cObjectManager::DestroyObject (0x5fbe10), with the arguments cCreature::UseItem
// destroys a drunk potion with (id, 1, 0, 0).
inline void DestroyObject(uint32_t id) {
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, uint32_t, uint32_t, uint32_t);
if (void* manager = ObjectManager()) {
reinterpret_cast<Fn>(0x005fbe10)(manager, nullptr, id, 1, 0, 0);
}
}
// The object `id` as a cCreature, or null.
inline void* GetCreature(uint32_t id) {
return DynamicCast(GetObjectById(id), kTypeObject, kTypeCreature);
@@ -54,7 +68,7 @@ inline void* GetCreature(uint32_t id) {
// cCreature), or null.
inline void* GetPlayerHero() {
using Fn = void*(__fastcall*)(void* manager, void* edx);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
void* manager = ObjectManager();
return manager ? reinterpret_cast<Fn>(0x00603e90)(manager, nullptr) : nullptr;
}