3D entities

This commit is contained in:
John McCardle 2026-02-04 17:45:12 -05:00
commit f4c9db8436
9 changed files with 1964 additions and 0 deletions

865
src/3d/Entity3D.cpp Normal file
View file

@ -0,0 +1,865 @@
// Entity3D.cpp - 3D game entity implementation
#include "Entity3D.h"
#include "Viewport3D.h"
#include "VoxelPoint.h"
#include "PyVector.h"
#include "PyColor.h"
#include "PythonObjectCache.h"
#include <cstdio>
// Include appropriate GL headers based on backend
#if defined(MCRF_SDL2)
#ifdef __EMSCRIPTEN__
#include <GLES2/gl2.h>
#else
#include <GL/gl.h>
#include <GL/glext.h>
#endif
#define MCRF_HAS_GL 1
#elif !defined(MCRF_HEADLESS)
// SFML backend - use GLAD
#include <glad/glad.h>
#define MCRF_HAS_GL 1
#endif
namespace mcrf {
// Static members for placeholder cube
unsigned int Entity3D::cubeVBO_ = 0;
unsigned int Entity3D::cubeVertexCount_ = 0;
bool Entity3D::cubeInitialized_ = false;
// =============================================================================
// Constructor / Destructor
// =============================================================================
Entity3D::Entity3D()
: grid_x_(0)
, grid_z_(0)
, world_pos_(0, 0, 0)
, target_world_pos_(0, 0, 0)
{
}
Entity3D::Entity3D(int grid_x, int grid_z)
: grid_x_(grid_x)
, grid_z_(grid_z)
{
updateWorldPosFromGrid();
target_world_pos_ = world_pos_;
}
Entity3D::~Entity3D()
{
// Cleanup cube geometry when last entity is destroyed?
// For now, leave it - it's shared static data
}
// =============================================================================
// Position
// =============================================================================
void Entity3D::setGridPos(int x, int z, bool animate)
{
if (x == grid_x_ && z == grid_z_) return;
if (animate && !is_animating_) {
// Queue the move for animation
move_queue_.push({x, z});
if (!is_animating_) {
processNextMove();
}
} else if (!animate) {
teleportTo(x, z);
} else {
// Already animating, queue this move
move_queue_.push({x, z});
}
}
void Entity3D::teleportTo(int x, int z)
{
// Clear any pending moves
clearPath();
is_animating_ = false;
grid_x_ = x;
grid_z_ = z;
updateCellRegistration();
updateWorldPosFromGrid();
target_world_pos_ = world_pos_;
}
float Entity3D::getTerrainHeight() const
{
auto vp = viewport_.lock();
if (!vp) return 0.0f;
if (vp->isValidCell(grid_x_, grid_z_)) {
return vp->at(grid_x_, grid_z_).height;
}
return 0.0f;
}
void Entity3D::updateWorldPosFromGrid()
{
auto vp = viewport_.lock();
float cellSize = vp ? vp->getCellSize() : 1.0f;
world_pos_.x = grid_x_ * cellSize + cellSize * 0.5f; // Center of cell
world_pos_.z = grid_z_ * cellSize + cellSize * 0.5f;
world_pos_.y = getTerrainHeight() + 0.5f; // Slightly above terrain
}
// =============================================================================
// Viewport Integration
// =============================================================================
void Entity3D::setViewport(std::shared_ptr<Viewport3D> vp)
{
viewport_ = vp;
if (vp) {
updateWorldPosFromGrid();
target_world_pos_ = world_pos_;
updateCellRegistration();
}
}
void Entity3D::updateCellRegistration()
{
// For now, just track the old position
// VoxelPoint.entities list support will be added later
old_grid_x_ = grid_x_;
old_grid_z_ = grid_z_;
}
// =============================================================================
// Visibility / FOV
// =============================================================================
void Entity3D::initVoxelState() const
{
auto vp = viewport_.lock();
if (!vp) {
voxel_state_.clear();
voxel_state_initialized_ = false;
return;
}
int w = vp->getGridWidth();
int d = vp->getGridDepth();
if (w <= 0 || d <= 0) {
voxel_state_.clear();
voxel_state_initialized_ = false;
return;
}
voxel_state_.resize(w * d);
for (auto& state : voxel_state_) {
state.visible = false;
state.discovered = false;
}
voxel_state_initialized_ = true;
}
void Entity3D::updateVisibility()
{
auto vp = viewport_.lock();
if (!vp) return;
if (!voxel_state_initialized_) {
initVoxelState();
}
int w = vp->getGridWidth();
int d = vp->getGridDepth();
// Reset visibility (keep discovered)
for (auto& state : voxel_state_) {
state.visible = false;
}
// Compute FOV from entity position
auto visible_cells = vp->computeFOV(grid_x_, grid_z_, 10); // Default radius 10
// Mark visible cells
for (const auto& cell : visible_cells) {
int idx = cell.second * w + cell.first;
if (idx >= 0 && idx < static_cast<int>(voxel_state_.size())) {
voxel_state_[idx].visible = true;
voxel_state_[idx].discovered = true;
}
}
}
const VoxelPointState& Entity3D::getVoxelState(int x, int z) const
{
static VoxelPointState empty;
auto vp = viewport_.lock();
if (!vp) return empty;
if (!voxel_state_initialized_) {
initVoxelState();
}
int w = vp->getGridWidth();
int idx = z * w + x;
if (idx >= 0 && idx < static_cast<int>(voxel_state_.size())) {
return voxel_state_[idx];
}
return empty;
}
bool Entity3D::canSee(int x, int z) const
{
return getVoxelState(x, z).visible;
}
bool Entity3D::hasDiscovered(int x, int z) const
{
return getVoxelState(x, z).discovered;
}
// =============================================================================
// Pathfinding
// =============================================================================
std::vector<std::pair<int, int>> Entity3D::pathTo(int target_x, int target_z)
{
auto vp = viewport_.lock();
if (!vp) return {};
return vp->findPath(grid_x_, grid_z_, target_x, target_z);
}
void Entity3D::followPath(const std::vector<std::pair<int, int>>& path)
{
for (const auto& step : path) {
move_queue_.push(step);
}
if (!is_animating_ && !move_queue_.empty()) {
processNextMove();
}
}
void Entity3D::processNextMove()
{
if (move_queue_.empty()) {
is_animating_ = false;
return;
}
auto next = move_queue_.front();
move_queue_.pop();
// Update grid position immediately (game logic)
grid_x_ = next.first;
grid_z_ = next.second;
updateCellRegistration();
// Set up animation
move_start_pos_ = world_pos_;
// Calculate target world position
auto vp = viewport_.lock();
float cellSize = vp ? vp->getCellSize() : 1.0f;
float terrainHeight = getTerrainHeight();
target_world_pos_.x = grid_x_ * cellSize + cellSize * 0.5f;
target_world_pos_.z = grid_z_ * cellSize + cellSize * 0.5f;
target_world_pos_.y = terrainHeight + 0.5f;
is_animating_ = true;
move_progress_ = 0.0f;
}
// =============================================================================
// Animation / Update
// =============================================================================
void Entity3D::update(float dt)
{
if (!is_animating_) return;
move_progress_ += dt * move_speed_;
if (move_progress_ >= 1.0f) {
// Animation complete
world_pos_ = target_world_pos_;
is_animating_ = false;
// Process next move in queue
if (!move_queue_.empty()) {
processNextMove();
}
} else {
// Interpolate position
world_pos_ = vec3::lerp(move_start_pos_, target_world_pos_, move_progress_);
}
}
bool Entity3D::setProperty(const std::string& name, float value)
{
if (name == "x" || name == "world_x") {
world_pos_.x = value;
return true;
}
if (name == "y" || name == "world_y") {
world_pos_.y = value;
return true;
}
if (name == "z" || name == "world_z") {
world_pos_.z = value;
return true;
}
if (name == "rotation" || name == "rot_y") {
rotation_ = value;
return true;
}
if (name == "scale") {
scale_ = vec3(value, value, value);
return true;
}
if (name == "scale_x") {
scale_.x = value;
return true;
}
if (name == "scale_y") {
scale_.y = value;
return true;
}
if (name == "scale_z") {
scale_.z = value;
return true;
}
return false;
}
bool Entity3D::setProperty(const std::string& name, int value)
{
if (name == "sprite_index") {
sprite_index_ = value;
return true;
}
if (name == "visible") {
visible_ = value != 0;
return true;
}
return false;
}
bool Entity3D::getProperty(const std::string& name, float& value) const
{
if (name == "x" || name == "world_x") {
value = world_pos_.x;
return true;
}
if (name == "y" || name == "world_y") {
value = world_pos_.y;
return true;
}
if (name == "z" || name == "world_z") {
value = world_pos_.z;
return true;
}
if (name == "rotation" || name == "rot_y") {
value = rotation_;
return true;
}
if (name == "scale") {
value = scale_.x; // Return uniform scale
return true;
}
return false;
}
bool Entity3D::hasProperty(const std::string& name) const
{
return name == "x" || name == "y" || name == "z" ||
name == "world_x" || name == "world_y" || name == "world_z" ||
name == "rotation" || name == "rot_y" ||
name == "scale" || name == "scale_x" || name == "scale_y" || name == "scale_z" ||
name == "sprite_index" || name == "visible";
}
// =============================================================================
// Rendering
// =============================================================================
mat4 Entity3D::getModelMatrix() const
{
mat4 model = mat4::identity();
model = mat4::translate(world_pos_) * model;
model = mat4::rotateY(rotation_ * DEG_TO_RAD) * model;
model = mat4::scale(scale_) * model;
return model;
}
void Entity3D::initCubeGeometry()
{
if (cubeInitialized_) return;
// Unit cube vertices (position + normal + color placeholder)
// Each vertex: x, y, z, nx, ny, nz, r, g, b
float vertices[] = {
// Front face
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.25f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.25f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.25f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.25f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.25f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f, 0.25f,
// Back face
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.8f, 0.4f, 0.2f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.8f, 0.4f, 0.2f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.8f, 0.4f, 0.2f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.8f, 0.4f, 0.2f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.8f, 0.4f, 0.2f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.8f, 0.4f, 0.2f,
// Right face
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.9f, 0.45f, 0.22f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.9f, 0.45f, 0.22f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.9f, 0.45f, 0.22f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.9f, 0.45f, 0.22f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.9f, 0.45f, 0.22f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.9f, 0.45f, 0.22f,
// Left face
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.7f, 0.35f, 0.17f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.7f, 0.35f, 0.17f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.7f, 0.35f, 0.17f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.7f, 0.35f, 0.17f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.7f, 0.35f, 0.17f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.7f, 0.35f, 0.17f,
// Top face
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.6f, 0.3f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.6f, 0.3f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.6f, 0.3f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.6f, 0.3f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.6f, 0.3f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.6f, 0.3f,
// Bottom face
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.6f, 0.3f, 0.15f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.6f, 0.3f, 0.15f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.6f, 0.3f, 0.15f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.6f, 0.3f, 0.15f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.6f, 0.3f, 0.15f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.6f, 0.3f, 0.15f,
};
cubeVertexCount_ = 36;
glGenBuffers(1, &cubeVBO_);
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
cubeInitialized_ = true;
}
void Entity3D::render(const mat4& view, const mat4& proj, unsigned int shader)
{
if (!visible_) return;
// Initialize cube geometry if needed
if (!cubeInitialized_) {
initCubeGeometry();
}
// Set model matrix uniform
mat4 model = getModelMatrix();
mat4 mvp = proj * view * model;
// Get uniform locations (assuming shader is already bound)
int mvpLoc = glGetUniformLocation(shader, "u_mvp");
int modelLoc = glGetUniformLocation(shader, "u_model");
int colorLoc = glGetUniformLocation(shader, "u_entityColor");
if (mvpLoc >= 0) glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, mvp.data());
if (modelLoc >= 0) glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.data());
if (colorLoc >= 0) {
glUniform4f(colorLoc,
color_.r / 255.0f,
color_.g / 255.0f,
color_.b / 255.0f,
color_.a / 255.0f);
}
// Bind VBO and set up attributes
glBindBuffer(GL_ARRAY_BUFFER, cubeVBO_);
// Position attribute (location 0)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
// Normal attribute (location 1)
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float)));
// Color attribute (location 2)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6 * sizeof(float)));
// Draw
glDrawArrays(GL_TRIANGLES, 0, cubeVertexCount_);
// Cleanup
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
// =============================================================================
// Python API Implementation
// =============================================================================
int Entity3D::init(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"pos", "viewport", "rotation", "scale", "visible", "color", NULL};
PyObject* pos_obj = nullptr;
PyObject* viewport_obj = nullptr;
float rotation = 0.0f;
PyObject* scale_obj = nullptr;
int visible = 1;
PyObject* color_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOpO", const_cast<char**>(kwlist),
&pos_obj, &viewport_obj, &rotation, &scale_obj, &visible, &color_obj)) {
return -1;
}
// Parse position
int grid_x = 0, grid_z = 0;
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) >= 2) {
grid_x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
grid_z = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
if (PyErr_Occurred()) return -1;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, z)");
return -1;
}
}
// Initialize entity
self->data->grid_x_ = grid_x;
self->data->grid_z_ = grid_z;
self->data->rotation_ = rotation;
self->data->visible_ = visible != 0;
// Parse scale
if (scale_obj && scale_obj != Py_None) {
if (PyFloat_Check(scale_obj) || PyLong_Check(scale_obj)) {
float s = (float)PyFloat_AsDouble(scale_obj);
self->data->scale_ = vec3(s, s, s);
} else if (PyTuple_Check(scale_obj) && PyTuple_Size(scale_obj) >= 3) {
float sx = (float)PyFloat_AsDouble(PyTuple_GetItem(scale_obj, 0));
float sy = (float)PyFloat_AsDouble(PyTuple_GetItem(scale_obj, 1));
float sz = (float)PyFloat_AsDouble(PyTuple_GetItem(scale_obj, 2));
self->data->scale_ = vec3(sx, sy, sz);
}
}
// Parse color
if (color_obj && color_obj != Py_None) {
self->data->color_ = PyColor::fromPy(color_obj);
if (PyErr_Occurred()) return -1;
}
// Attach to viewport if provided
if (viewport_obj && viewport_obj != Py_None) {
// Will be handled by EntityCollection3D when appending
// For now, just validate it's the right type
if (!PyObject_IsInstance(viewport_obj, (PyObject*)&mcrfpydef::PyViewport3DType)) {
PyErr_SetString(PyExc_TypeError, "viewport must be a Viewport3D");
return -1;
}
}
// Register in object cache
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
self->data->self = (PyObject*)self;
return 0;
}
PyObject* Entity3D::repr(PyEntity3DObject* self)
{
if (!self->data) {
return PyUnicode_FromString("<Entity3D (null)>");
}
char buffer[128];
snprintf(buffer, sizeof(buffer),
"<Entity3D at (%d, %d) world=(%.1f, %.1f, %.1f) rot=%.1f>",
self->data->grid_x_, self->data->grid_z_,
self->data->world_pos_.x, self->data->world_pos_.y, self->data->world_pos_.z,
self->data->rotation_);
return PyUnicode_FromString(buffer);
}
// Property getters/setters
PyObject* Entity3D::get_pos(PyEntity3DObject* self, void* closure)
{
return Py_BuildValue("(ii)", self->data->grid_x_, self->data->grid_z_);
}
int Entity3D::set_pos(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyTuple_Check(value) || PyTuple_Size(value) < 2) {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, z)");
return -1;
}
int x = PyLong_AsLong(PyTuple_GetItem(value, 0));
int z = PyLong_AsLong(PyTuple_GetItem(value, 1));
if (PyErr_Occurred()) return -1;
self->data->setGridPos(x, z, true); // Animate by default
return 0;
}
PyObject* Entity3D::get_world_pos(PyEntity3DObject* self, void* closure)
{
vec3 wp = self->data->world_pos_;
return Py_BuildValue("(fff)", wp.x, wp.y, wp.z);
}
PyObject* Entity3D::get_grid_pos(PyEntity3DObject* self, void* closure)
{
return Py_BuildValue("(ii)", self->data->grid_x_, self->data->grid_z_);
}
int Entity3D::set_grid_pos(PyEntity3DObject* self, PyObject* value, void* closure)
{
return set_pos(self, value, closure);
}
PyObject* Entity3D::get_rotation(PyEntity3DObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->rotation_);
}
int Entity3D::set_rotation(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (!PyNumber_Check(value)) {
PyErr_SetString(PyExc_TypeError, "rotation must be a number");
return -1;
}
self->data->rotation_ = (float)PyFloat_AsDouble(value);
return 0;
}
PyObject* Entity3D::get_scale(PyEntity3DObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->scale_.x); // Return uniform scale
}
int Entity3D::set_scale(PyEntity3DObject* self, PyObject* value, void* closure)
{
if (PyFloat_Check(value) || PyLong_Check(value)) {
float s = (float)PyFloat_AsDouble(value);
self->data->scale_ = vec3(s, s, s);
return 0;
} else if (PyTuple_Check(value) && PyTuple_Size(value) >= 3) {
float sx = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0));
float sy = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1));
float sz = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 2));
self->data->scale_ = vec3(sx, sy, sz);
return 0;
}
PyErr_SetString(PyExc_TypeError, "scale must be a number or (x, y, z) tuple");
return -1;
}
PyObject* Entity3D::get_visible(PyEntity3DObject* self, void* closure)
{
return PyBool_FromLong(self->data->visible_ ? 1 : 0);
}
int Entity3D::set_visible(PyEntity3DObject* self, PyObject* value, void* closure)
{
self->data->visible_ = PyObject_IsTrue(value);
return 0;
}
PyObject* Entity3D::get_color(PyEntity3DObject* self, void* closure)
{
return PyColor(self->data->color_).pyObject();
}
int Entity3D::set_color(PyEntity3DObject* self, PyObject* value, void* closure)
{
self->data->color_ = PyColor::fromPy(value);
if (PyErr_Occurred()) return -1;
return 0;
}
PyObject* Entity3D::get_viewport(PyEntity3DObject* self, void* closure)
{
auto vp = self->data->viewport_.lock();
if (!vp) {
Py_RETURN_NONE;
}
// TODO: Return actual viewport Python object
// For now, return None
Py_RETURN_NONE;
}
// Methods
PyObject* Entity3D::py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"x", "z", "pos", NULL};
int x = -1, z = -1;
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiO", const_cast<char**>(kwlist),
&x, &z, &pos_obj)) {
return NULL;
}
// Parse position from tuple if provided
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) >= 2) {
x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
z = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
if (PyErr_Occurred()) return NULL;
}
}
if (x < 0 || z < 0) {
PyErr_SetString(PyExc_ValueError, "Target position required");
return NULL;
}
auto path = self->data->pathTo(x, z);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); ++i) {
PyObject* tuple = PyTuple_Pack(2,
PyLong_FromLong(path[i].first),
PyLong_FromLong(path[i].second));
PyList_SET_ITEM(path_list, i, tuple);
}
return path_list;
}
PyObject* Entity3D::py_teleport(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"x", "z", "pos", NULL};
int x = -1, z = -1;
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiO", const_cast<char**>(kwlist),
&x, &z, &pos_obj)) {
return NULL;
}
// Parse position from tuple if provided
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) >= 2) {
x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
z = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
if (PyErr_Occurred()) return NULL;
}
}
if (x < 0 || z < 0) {
PyErr_SetString(PyExc_ValueError, "Target position required");
return NULL;
}
self->data->teleportTo(x, z);
Py_RETURN_NONE;
}
PyObject* Entity3D::py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"x", "z", NULL};
int x, z;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(kwlist), &x, &z)) {
return NULL;
}
const auto& state = self->data->getVoxelState(x, z);
return Py_BuildValue("{s:O,s:O}",
"visible", state.visible ? Py_True : Py_False,
"discovered", state.discovered ? Py_True : Py_False);
}
PyObject* Entity3D::py_update_visibility(PyEntity3DObject* self, PyObject* args)
{
self->data->updateVisibility();
Py_RETURN_NONE;
}
PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
{
// TODO: Implement animation shorthand similar to UIEntity
// For now, return None
PyErr_SetString(PyExc_NotImplementedError, "Entity3D.animate() not yet implemented");
return NULL;
}
// Method and GetSet tables
PyMethodDef Entity3D::methods[] = {
{"path_to", (PyCFunction)Entity3D::py_path_to, METH_VARARGS | METH_KEYWORDS,
"path_to(x, z) or path_to(pos=(x, z)) -> list\n\n"
"Compute A* path to target position.\n"
"Returns list of (x, z) tuples, or empty list if no path exists."},
{"teleport", (PyCFunction)Entity3D::py_teleport, METH_VARARGS | METH_KEYWORDS,
"teleport(x, z) or teleport(pos=(x, z))\n\n"
"Instantly move to target position without animation."},
{"at", (PyCFunction)Entity3D::py_at, METH_VARARGS | METH_KEYWORDS,
"at(x, z) -> dict\n\n"
"Get visibility state for a cell from this entity's perspective.\n"
"Returns dict with 'visible' and 'discovered' boolean keys."},
{"update_visibility", (PyCFunction)Entity3D::py_update_visibility, METH_NOARGS,
"update_visibility()\n\n"
"Recompute field of view from current position."},
{"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS,
"animate(property, target, duration, easing=None, callback=None)\n\n"
"Animate a property over time. (Not yet implemented)"},
{NULL} // Sentinel
};
PyGetSetDef Entity3D::getsetters[] = {
{"pos", (getter)Entity3D::get_pos, (setter)Entity3D::set_pos,
"Grid position (x, z). Setting triggers smooth movement.", NULL},
{"grid_pos", (getter)Entity3D::get_grid_pos, (setter)Entity3D::set_grid_pos,
"Grid position (x, z). Same as pos.", NULL},
{"world_pos", (getter)Entity3D::get_world_pos, NULL,
"Current world position (x, y, z) (read-only). Includes animation interpolation.", NULL},
{"rotation", (getter)Entity3D::get_rotation, (setter)Entity3D::set_rotation,
"Y-axis rotation in degrees.", NULL},
{"scale", (getter)Entity3D::get_scale, (setter)Entity3D::set_scale,
"Uniform scale factor. Can also set as (x, y, z) tuple.", NULL},
{"visible", (getter)Entity3D::get_visible, (setter)Entity3D::set_visible,
"Visibility state.", NULL},
{"color", (getter)Entity3D::get_color, (setter)Entity3D::set_color,
"Entity render color.", NULL},
{"viewport", (getter)Entity3D::get_viewport, NULL,
"Owning Viewport3D (read-only).", NULL},
{NULL} // Sentinel
};
} // namespace mcrf
// Methods array for PyTypeObject
PyMethodDef Entity3D_methods[] = {
{NULL} // Will be populated from Entity3D::methods
};

325
src/3d/Entity3D.h Normal file
View file

@ -0,0 +1,325 @@
// Entity3D.h - 3D game entity for McRogueFace
// Represents a game object that exists on the VoxelPoint navigation grid
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include "Math3D.h"
#include <memory>
#include <queue>
#include <vector>
#include <string>
namespace mcrf {
// Forward declarations
class Viewport3D;
} // namespace mcrf
// Python object struct forward declaration
typedef struct PyEntity3DObject PyEntity3DObject;
namespace mcrf {
// =============================================================================
// VoxelPointState - Per-entity visibility state for a grid cell
// =============================================================================
struct VoxelPointState {
bool visible = false; // Currently in FOV
bool discovered = false; // Ever seen
};
// =============================================================================
// Entity3D - 3D game entity on the navigation grid
// =============================================================================
class Entity3D : public std::enable_shared_from_this<Entity3D> {
public:
// Python integration
PyObject* self = nullptr; // Reference to Python object
uint64_t serial_number = 0; // For object cache
Entity3D();
Entity3D(int grid_x, int grid_z);
~Entity3D();
// =========================================================================
// Position
// =========================================================================
/// Get grid position (logical game coordinates)
int getGridX() const { return grid_x_; }
int getGridZ() const { return grid_z_; }
std::pair<int, int> getGridPos() const { return {grid_x_, grid_z_}; }
/// Set grid position (triggers movement if animated)
void setGridPos(int x, int z, bool animate = true);
/// Teleport to grid position (instant, no animation)
void teleportTo(int x, int z);
/// Get world position (render coordinates, includes animation interpolation)
vec3 getWorldPos() const { return world_pos_; }
/// Get terrain height at current grid position
float getTerrainHeight() const;
// =========================================================================
// Rotation and Scale
// =========================================================================
float getRotation() const { return rotation_; } // Y-axis rotation in degrees
void setRotation(float degrees) { rotation_ = degrees; }
vec3 getScale() const { return scale_; }
void setScale(const vec3& s) { scale_ = s; }
void setScale(float uniform) { scale_ = vec3(uniform, uniform, uniform); }
// =========================================================================
// Appearance
// =========================================================================
bool isVisible() const { return visible_; }
void setVisible(bool v) { visible_ = v; }
// Color for placeholder cube rendering
sf::Color getColor() const { return color_; }
void setColor(const sf::Color& c) { color_ = c; }
// Sprite index (for future texture atlas support)
int getSpriteIndex() const { return sprite_index_; }
void setSpriteIndex(int idx) { sprite_index_ = idx; }
// =========================================================================
// Viewport Integration
// =========================================================================
/// Get owning viewport (may be null)
std::shared_ptr<Viewport3D> getViewport() const { return viewport_.lock(); }
/// Set owning viewport (called when added to viewport)
void setViewport(std::shared_ptr<Viewport3D> vp);
/// Update cell registration (call when grid position changes)
void updateCellRegistration();
// =========================================================================
// Visibility / FOV
// =========================================================================
/// Update visibility state from current FOV
void updateVisibility();
/// Get visibility state for a cell from this entity's perspective
const VoxelPointState& getVoxelState(int x, int z) const;
/// Check if a cell is currently visible to this entity
bool canSee(int x, int z) const;
/// Check if a cell has been discovered by this entity
bool hasDiscovered(int x, int z) const;
// =========================================================================
// Pathfinding
// =========================================================================
/// Compute path to target position
std::vector<std::pair<int, int>> pathTo(int target_x, int target_z);
/// Follow a path (queue movement steps)
void followPath(const std::vector<std::pair<int, int>>& path);
/// Check if entity is currently moving
bool isMoving() const { return !move_queue_.empty() || is_animating_; }
/// Clear movement queue
void clearPath() { while (!move_queue_.empty()) move_queue_.pop(); }
// =========================================================================
// Animation / Update
// =========================================================================
/// Update entity state (called each frame)
/// @param dt Delta time in seconds
void update(float dt);
/// Property system for animation
bool setProperty(const std::string& name, float value);
bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const;
bool hasProperty(const std::string& name) const;
// =========================================================================
// Rendering
// =========================================================================
/// Get model matrix for rendering
mat4 getModelMatrix() const;
/// Render the entity (called by Viewport3D)
void render(const mat4& view, const mat4& proj, unsigned int shader);
// =========================================================================
// Python API
// =========================================================================
static int init(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* repr(PyEntity3DObject* self);
// Property getters/setters
static PyObject* get_pos(PyEntity3DObject* self, void* closure);
static int set_pos(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_world_pos(PyEntity3DObject* self, void* closure);
static PyObject* get_grid_pos(PyEntity3DObject* self, void* closure);
static int set_grid_pos(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_rotation(PyEntity3DObject* self, void* closure);
static int set_rotation(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_scale(PyEntity3DObject* self, void* closure);
static int set_scale(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_visible(PyEntity3DObject* self, void* closure);
static int set_visible(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_color(PyEntity3DObject* self, void* closure);
static int set_color(PyEntity3DObject* self, PyObject* value, void* closure);
static PyObject* get_viewport(PyEntity3DObject* self, void* closure);
// Methods
static PyObject* py_path_to(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_teleport(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_update_visibility(PyEntity3DObject* self, PyObject* args);
static PyObject* py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
private:
// Grid position (logical game coordinates)
int grid_x_ = 0;
int grid_z_ = 0;
int old_grid_x_ = -1; // For cell registration tracking
int old_grid_z_ = -1;
// World position (render coordinates, smoothly interpolated)
vec3 world_pos_;
vec3 target_world_pos_; // Animation target
// Rotation (Y-axis, in degrees)
float rotation_ = 0.0f;
// Scale
vec3 scale_ = vec3(1.0f, 1.0f, 1.0f);
// Appearance
bool visible_ = true;
sf::Color color_ = sf::Color(200, 100, 50); // Default orange
int sprite_index_ = 0;
// Viewport (weak reference to avoid cycles)
std::weak_ptr<Viewport3D> viewport_;
// Visibility state per cell (lazy initialized)
mutable std::vector<VoxelPointState> voxel_state_;
mutable bool voxel_state_initialized_ = false;
// Movement animation
std::queue<std::pair<int, int>> move_queue_;
bool is_animating_ = false;
float move_progress_ = 0.0f;
float move_speed_ = 5.0f; // Cells per second
vec3 move_start_pos_;
// Helper to initialize voxel state
void initVoxelState() const;
// Helper to update world position from grid position
void updateWorldPosFromGrid();
// Process next move in queue
void processNextMove();
// Static VBO for placeholder cube
static unsigned int cubeVBO_;
static unsigned int cubeVertexCount_;
static bool cubeInitialized_;
static void initCubeGeometry();
};
} // namespace mcrf
// =============================================================================
// Python type definition
// =============================================================================
typedef struct PyEntity3DObject {
PyObject_HEAD
std::shared_ptr<mcrf::Entity3D> data;
PyObject* weakreflist;
} PyEntity3DObject;
// Forward declaration of methods array
extern PyMethodDef Entity3D_methods[];
namespace mcrfpydef {
inline PyTypeObject PyEntity3DType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Entity3D",
.tp_basicsize = sizeof(PyEntity3DObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyEntity3DObject* obj = (PyEntity3DObject*)self;
PyObject_GC_UnTrack(self);
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)mcrf::Entity3D::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_doc = PyDoc_STR("Entity3D(pos=None, **kwargs)\n\n"
"A 3D game entity that exists on a Viewport3D's navigation grid.\n\n"
"Args:\n"
" pos (tuple, optional): Grid position as (x, z). Default: (0, 0)\n\n"
"Keyword Args:\n"
" viewport (Viewport3D): Viewport to attach entity to. Default: None\n"
" rotation (float): Y-axis rotation in degrees. Default: 0\n"
" scale (float or tuple): Scale factor. Default: 1.0\n"
" visible (bool): Visibility state. Default: True\n"
" color (Color): Entity color. Default: orange\n\n"
"Attributes:\n"
" pos (tuple): Grid position (x, z) - setting triggers movement\n"
" grid_pos (tuple): Same as pos (read-only)\n"
" world_pos (tuple): Current world coordinates (x, y, z) (read-only)\n"
" rotation (float): Y-axis rotation in degrees\n"
" scale (float): Uniform scale factor\n"
" visible (bool): Visibility state\n"
" color (Color): Entity render color\n"
" viewport (Viewport3D): Owning viewport (read-only)"),
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
// No Python objects to visit currently
return 0;
},
.tp_clear = [](PyObject* self) -> int {
return 0;
},
.tp_methods = mcrf::Entity3D::methods,
.tp_getset = mcrf::Entity3D::getsetters,
.tp_init = (initproc)mcrf::Entity3D::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyEntity3DObject* self = (PyEntity3DObject*)type->tp_alloc(type, 0);
if (self) {
self->data = std::make_shared<mcrf::Entity3D>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};
} // namespace mcrfpydef

View file

@ -0,0 +1,259 @@
// EntityCollection3D.cpp - Python collection for Entity3D objects
#include "EntityCollection3D.h"
#include "Entity3D.h"
#include "Viewport3D.h"
// =============================================================================
// Sequence Methods
// =============================================================================
PySequenceMethods EntityCollection3D::sqmethods = {
.sq_length = (lenfunc)EntityCollection3D::len,
.sq_item = (ssizeargfunc)EntityCollection3D::getitem,
.sq_contains = (objobjproc)EntityCollection3D::contains,
};
// =============================================================================
// EntityCollection3D Implementation
// =============================================================================
PyObject* EntityCollection3D::repr(PyEntityCollection3DObject* self)
{
if (!self->data) {
return PyUnicode_FromString("<EntityCollection3D (null)>");
}
return PyUnicode_FromFormat("<EntityCollection3D with %zd entities>", self->data->size());
}
int EntityCollection3D::init(PyEntityCollection3DObject* self, PyObject* args, PyObject* kwds)
{
PyErr_SetString(PyExc_TypeError, "EntityCollection3D cannot be instantiated directly");
return -1;
}
PyObject* EntityCollection3D::iter(PyEntityCollection3DObject* self)
{
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Collection has no data");
return NULL;
}
// Create iterator
auto iter_type = &mcrfpydef::PyEntityCollection3DIterType;
auto iter_obj = (PyEntityCollection3DIterObject*)iter_type->tp_alloc(iter_type, 0);
if (!iter_obj) return NULL;
// Initialize with placement new for iterator members
new (&iter_obj->data) std::shared_ptr<std::list<std::shared_ptr<mcrf::Entity3D>>>(self->data);
new (&iter_obj->current) std::list<std::shared_ptr<mcrf::Entity3D>>::iterator(self->data->begin());
new (&iter_obj->end) std::list<std::shared_ptr<mcrf::Entity3D>>::iterator(self->data->end());
iter_obj->start_size = static_cast<int>(self->data->size());
return (PyObject*)iter_obj;
}
Py_ssize_t EntityCollection3D::len(PyEntityCollection3DObject* self)
{
if (!self->data) return 0;
return static_cast<Py_ssize_t>(self->data->size());
}
PyObject* EntityCollection3D::getitem(PyEntityCollection3DObject* self, Py_ssize_t index)
{
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Collection has no data");
return NULL;
}
// Handle negative indices
Py_ssize_t size = static_cast<Py_ssize_t>(self->data->size());
if (index < 0) index += size;
if (index < 0 || index >= size) {
PyErr_SetString(PyExc_IndexError, "EntityCollection3D index out of range");
return NULL;
}
// Iterate to the index (std::list doesn't have random access)
auto it = self->data->begin();
std::advance(it, index);
// Create Python wrapper for the Entity3D
auto entity = *it;
auto type = &mcrfpydef::PyEntity3DType;
auto obj = (PyEntity3DObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
// Use placement new for shared_ptr
new (&obj->data) std::shared_ptr<mcrf::Entity3D>(entity);
obj->weakreflist = nullptr;
return (PyObject*)obj;
}
int EntityCollection3D::contains(PyEntityCollection3DObject* self, PyObject* value)
{
if (!self->data) return 0;
// Check if value is an Entity3D
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyEntity3DType)) {
return 0;
}
auto entity_obj = (PyEntity3DObject*)value;
if (!entity_obj->data) return 0;
// Search for the entity
for (const auto& e : *self->data) {
if (e.get() == entity_obj->data.get()) {
return 1;
}
}
return 0;
}
PyObject* EntityCollection3D::append(PyEntityCollection3DObject* self, PyObject* o)
{
if (!self->data || !self->viewport) {
PyErr_SetString(PyExc_RuntimeError, "Collection has no data");
return NULL;
}
// Check if argument is an Entity3D
if (!PyObject_IsInstance(o, (PyObject*)&mcrfpydef::PyEntity3DType)) {
PyErr_SetString(PyExc_TypeError, "Can only append Entity3D objects");
return NULL;
}
auto entity_obj = (PyEntity3DObject*)o;
if (!entity_obj->data) {
PyErr_SetString(PyExc_ValueError, "Entity3D has no data");
return NULL;
}
// Remove from old viewport if any
auto old_vp = entity_obj->data->getViewport();
if (old_vp && old_vp != self->viewport) {
// TODO: Implement removal from old viewport
// For now, just warn
}
// Add to this viewport's collection
self->data->push_back(entity_obj->data);
// Set the entity's viewport
entity_obj->data->setViewport(self->viewport);
Py_RETURN_NONE;
}
PyObject* EntityCollection3D::remove(PyEntityCollection3DObject* self, PyObject* o)
{
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Collection has no data");
return NULL;
}
// Check if argument is an Entity3D
if (!PyObject_IsInstance(o, (PyObject*)&mcrfpydef::PyEntity3DType)) {
PyErr_SetString(PyExc_TypeError, "Can only remove Entity3D objects");
return NULL;
}
auto entity_obj = (PyEntity3DObject*)o;
if (!entity_obj->data) {
PyErr_SetString(PyExc_ValueError, "Entity3D has no data");
return NULL;
}
// Search and remove
for (auto it = self->data->begin(); it != self->data->end(); ++it) {
if (it->get() == entity_obj->data.get()) {
// Clear viewport reference
entity_obj->data->setViewport(nullptr);
self->data->erase(it);
Py_RETURN_NONE;
}
}
PyErr_SetString(PyExc_ValueError, "Entity3D not in collection");
return NULL;
}
PyObject* EntityCollection3D::clear(PyEntityCollection3DObject* self, PyObject* args)
{
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Collection has no data");
return NULL;
}
// Clear viewport references
for (auto& entity : *self->data) {
entity->setViewport(nullptr);
}
self->data->clear();
Py_RETURN_NONE;
}
PyMethodDef EntityCollection3D::methods[] = {
{"append", (PyCFunction)EntityCollection3D::append, METH_O,
"append(entity)\n\n"
"Add an Entity3D to the collection."},
{"remove", (PyCFunction)EntityCollection3D::remove, METH_O,
"remove(entity)\n\n"
"Remove an Entity3D from the collection."},
{"clear", (PyCFunction)EntityCollection3D::clear, METH_NOARGS,
"clear()\n\n"
"Remove all entities from the collection."},
{NULL} // Sentinel
};
// =============================================================================
// EntityCollection3DIter Implementation
// =============================================================================
int EntityCollection3DIter::init(PyEntityCollection3DIterObject* self, PyObject* args, PyObject* kwds)
{
PyErr_SetString(PyExc_TypeError, "EntityCollection3DIter cannot be instantiated directly");
return -1;
}
PyObject* EntityCollection3DIter::next(PyEntityCollection3DIterObject* self)
{
if (!self->data) {
PyErr_SetNone(PyExc_StopIteration);
return NULL;
}
// Check for modification during iteration
if (static_cast<int>(self->data->size()) != self->start_size) {
PyErr_SetString(PyExc_RuntimeError, "Collection modified during iteration");
return NULL;
}
// Check if we've reached the end
if (self->current == self->end) {
PyErr_SetNone(PyExc_StopIteration);
return NULL;
}
// Get current entity and advance
auto entity = *(self->current);
++(self->current);
// Create Python wrapper
auto type = &mcrfpydef::PyEntity3DType;
auto obj = (PyEntity3DObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
new (&obj->data) std::shared_ptr<mcrf::Entity3D>(entity);
obj->weakreflist = nullptr;
return (PyObject*)obj;
}
PyObject* EntityCollection3DIter::repr(PyEntityCollection3DIterObject* self)
{
return PyUnicode_FromString("<EntityCollection3DIter>");
}

127
src/3d/EntityCollection3D.h Normal file
View file

@ -0,0 +1,127 @@
// EntityCollection3D.h - Python collection type for Entity3D objects
// Manages entities belonging to a Viewport3D
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include <list>
#include <memory>
namespace mcrf {
// Forward declarations
class Entity3D;
class Viewport3D;
} // namespace mcrf
// Python object for EntityCollection3D
typedef struct {
PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<mcrf::Entity3D>>> data;
std::shared_ptr<mcrf::Viewport3D> viewport;
} PyEntityCollection3DObject;
// Python object for EntityCollection3D iterator
typedef struct {
PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<mcrf::Entity3D>>> data;
std::list<std::shared_ptr<mcrf::Entity3D>>::iterator current;
std::list<std::shared_ptr<mcrf::Entity3D>>::iterator end;
int start_size;
} PyEntityCollection3DIterObject;
// EntityCollection3D - Python collection wrapper
class EntityCollection3D {
public:
// Python sequence protocol
static PySequenceMethods sqmethods;
// Collection methods
static PyObject* append(PyEntityCollection3DObject* self, PyObject* o);
static PyObject* remove(PyEntityCollection3DObject* self, PyObject* o);
static PyObject* clear(PyEntityCollection3DObject* self, PyObject* args);
static PyMethodDef methods[];
// Python type slots
static PyObject* repr(PyEntityCollection3DObject* self);
static int init(PyEntityCollection3DObject* self, PyObject* args, PyObject* kwds);
static PyObject* iter(PyEntityCollection3DObject* self);
// Sequence methods
static Py_ssize_t len(PyEntityCollection3DObject* self);
static PyObject* getitem(PyEntityCollection3DObject* self, Py_ssize_t index);
static int contains(PyEntityCollection3DObject* self, PyObject* value);
};
// EntityCollection3DIter - Iterator
class EntityCollection3DIter {
public:
static int init(PyEntityCollection3DIterObject* self, PyObject* args, PyObject* kwds);
static PyObject* next(PyEntityCollection3DIterObject* self);
static PyObject* repr(PyEntityCollection3DIterObject* self);
};
namespace mcrfpydef {
// Iterator type
inline PyTypeObject PyEntityCollection3DIterType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.EntityCollection3DIter",
.tp_basicsize = sizeof(PyEntityCollection3DIterObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyEntityCollection3DIterObject* obj = (PyEntityCollection3DIterObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)EntityCollection3DIter::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterator for EntityCollection3D"),
.tp_iter = PyObject_SelfIter,
.tp_iternext = (iternextfunc)EntityCollection3DIter::next,
.tp_init = (initproc)EntityCollection3DIter::init,
.tp_alloc = PyType_GenericAlloc,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyErr_SetString(PyExc_TypeError, "EntityCollection3DIter cannot be instantiated directly");
return NULL;
}
};
// Collection type
inline PyTypeObject PyEntityCollection3DType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.EntityCollection3D",
.tp_basicsize = sizeof(PyEntityCollection3DObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyEntityCollection3DObject* obj = (PyEntityCollection3DObject*)self;
obj->data.reset();
obj->viewport.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)EntityCollection3D::repr,
.tp_as_sequence = &EntityCollection3D::sqmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Collection of Entity3D objects belonging to a Viewport3D.\n\n"
"Supports list-like operations: indexing, iteration, append, remove.\n\n"
"Example:\n"
" viewport.entities.append(entity)\n"
" for entity in viewport.entities:\n"
" print(entity.pos)"),
.tp_iter = (getiterfunc)EntityCollection3D::iter,
.tp_methods = EntityCollection3D::methods,
.tp_init = (initproc)EntityCollection3D::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyErr_SetString(PyExc_TypeError, "EntityCollection3D cannot be instantiated directly");
return NULL;
}
};
} // namespace mcrfpydef

View file

@ -28,6 +28,9 @@ public:
// Check if shader is valid // Check if shader is valid
bool isValid() const { return program_ != 0; } bool isValid() const { return program_ != 0; }
// Get the raw shader program ID (for glGetUniformLocation in Entity3D)
unsigned int getProgram() const { return program_; }
// Uniform setters (cached location lookup) // Uniform setters (cached location lookup)
void setUniform(const std::string& name, float value); void setUniform(const std::string& name, float value);
void setUniform(const std::string& name, int value); void setUniform(const std::string& name, int value);

View file

@ -3,6 +3,8 @@
#include "Viewport3D.h" #include "Viewport3D.h"
#include "Shader3D.h" #include "Shader3D.h"
#include "MeshLayer.h" #include "MeshLayer.h"
#include "Entity3D.h"
#include "EntityCollection3D.h"
#include "../platform/GLContext.h" #include "../platform/GLContext.h"
#include "PyVector.h" #include "PyVector.h"
#include "PyColor.h" #include "PyColor.h"
@ -39,6 +41,7 @@ namespace mcrf {
Viewport3D::Viewport3D() Viewport3D::Viewport3D()
: size_(320.0f, 240.0f) : size_(320.0f, 240.0f)
, entities_(std::make_shared<std::list<std::shared_ptr<Entity3D>>>())
{ {
position = sf::Vector2f(0, 0); position = sf::Vector2f(0, 0);
camera_.setAspect(size_.x / size_.y); camera_.setAspect(size_.x / size_.y);
@ -46,6 +49,7 @@ Viewport3D::Viewport3D()
Viewport3D::Viewport3D(float x, float y, float width, float height) Viewport3D::Viewport3D(float x, float y, float width, float height)
: size_(width, height) : size_(width, height)
, entities_(std::make_shared<std::list<std::shared_ptr<Entity3D>>>())
{ {
position = sf::Vector2f(x, y); position = sf::Vector2f(x, y);
camera_.setAspect(size_.x / size_.y); camera_.setAspect(size_.x / size_.y);
@ -423,6 +427,37 @@ bool Viewport3D::isInFOV(int x, int z) const {
return tcodMap_->isInFov(x, z); return tcodMap_->isInFov(x, z);
} }
// =============================================================================
// Entity3D Management
// =============================================================================
void Viewport3D::updateEntities(float dt) {
if (!entities_) return;
for (auto& entity : *entities_) {
if (entity) {
entity->update(dt);
}
}
}
void Viewport3D::renderEntities(const mat4& view, const mat4& proj) {
#ifdef MCRF_HAS_GL
if (!entities_ || !shader_ || !shader_->isValid()) return;
// Entity rendering uses the same shader as terrain
shader_->bind();
for (auto& entity : *entities_) {
if (entity && entity->isVisible()) {
entity->render(view, proj, shader_->getProgram());
}
}
shader_->unbind();
#endif
}
// ============================================================================= // =============================================================================
// FBO Management // FBO Management
// ============================================================================= // =============================================================================
@ -629,6 +664,11 @@ void Viewport3D::render3DContent() {
// Render mesh layers first (terrain, etc.) - sorted by z_index // Render mesh layers first (terrain, etc.) - sorted by z_index
renderMeshLayers(); renderMeshLayers();
// Render entities
mat4 view = camera_.getViewMatrix();
mat4 projection = camera_.getProjectionMatrix();
renderEntities(view, projection);
// Render test cube if enabled (disabled when layers are added) // Render test cube if enabled (disabled when layers are added)
if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) { if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) {
shader_->bind(); shader_->bind();
@ -1125,6 +1165,20 @@ static int Viewport3D_set_cell_size_prop(PyViewport3DObject* self, PyObject* val
return 0; return 0;
} }
// Entities collection property
static PyObject* Viewport3D_get_entities(PyViewport3DObject* self, void* closure) {
// Create an EntityCollection3D wrapper for this viewport's entity list
auto type = &mcrfpydef::PyEntityCollection3DType;
auto obj = (PyEntityCollection3DObject*)type->tp_alloc(type, 0);
if (!obj) return NULL;
// Use placement new for shared_ptr members
new (&obj->data) std::shared_ptr<std::list<std::shared_ptr<mcrf::Entity3D>>>(self->data->getEntities());
new (&obj->viewport) std::shared_ptr<mcrf::Viewport3D>(self->data);
return (PyObject*)obj;
}
PyGetSetDef Viewport3D::getsetters[] = { PyGetSetDef Viewport3D::getsetters[] = {
// Position and size // Position and size
{"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x, {"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x,
@ -1178,6 +1232,10 @@ PyGetSetDef Viewport3D::getsetters[] = {
{"cell_size", (getter)Viewport3D_get_cell_size_prop, (setter)Viewport3D_set_cell_size_prop, {"cell_size", (getter)Viewport3D_get_cell_size_prop, (setter)Viewport3D_set_cell_size_prop,
MCRF_PROPERTY(cell_size, "World units per navigation grid cell."), NULL}, MCRF_PROPERTY(cell_size, "World units per navigation grid cell."), NULL},
// Entity collection
{"entities", (getter)Viewport3D_get_entities, NULL,
MCRF_PROPERTY(entities, "Collection of Entity3D objects (read-only). Use append/remove to modify."), NULL},
// Common UIDrawable properties // Common UIDrawable properties
UIDRAWABLE_GETSETTERS, UIDRAWABLE_GETSETTERS,
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D), UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D),

View file

@ -14,10 +14,16 @@
#include "VoxelPoint.h" #include "VoxelPoint.h"
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <list>
#include <algorithm> #include <algorithm>
#include <mutex> #include <mutex>
#include <libtcod.h> #include <libtcod.h>
// Forward declaration
namespace mcrf {
class Entity3D;
}
namespace mcrf { namespace mcrf {
// Forward declarations // Forward declarations
@ -171,6 +177,19 @@ public:
/// Get TCODMap pointer (for advanced usage) /// Get TCODMap pointer (for advanced usage)
TCODMap* getTCODMap() const { return tcodMap_; } TCODMap* getTCODMap() const { return tcodMap_; }
// =========================================================================
// Entity3D Management
// =========================================================================
/// Get the entity list (for EntityCollection3D)
std::shared_ptr<std::list<std::shared_ptr<Entity3D>>> getEntities() { return entities_; }
/// Update all entities (call once per frame)
void updateEntities(float dt);
/// Render all entities
void renderEntities(const mat4& view, const mat4& proj);
// Background color // Background color
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; } void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
sf::Color getBackgroundColor() const { return bgColor_; } sf::Color getBackgroundColor() const { return bgColor_; }
@ -254,6 +273,9 @@ private:
TCODMap* tcodMap_ = nullptr; TCODMap* tcodMap_ = nullptr;
mutable std::mutex fovMutex_; mutable std::mutex fovMutex_;
// Entity3D storage
std::shared_ptr<std::list<std::shared_ptr<Entity3D>>> entities_;
// Shader for PS1-style rendering // Shader for PS1-style rendering
std::unique_ptr<Shader3D> shader_; std::unique_ptr<Shader3D> shader_;

View file

@ -32,6 +32,8 @@
#include "PyUniformBinding.h" // Shader uniform bindings (#106) #include "PyUniformBinding.h" // Shader uniform bindings (#106)
#include "PyUniformCollection.h" // Shader uniform collection (#106) #include "PyUniformCollection.h" // Shader uniform collection (#106)
#include "3d/Viewport3D.h" // 3D rendering viewport #include "3d/Viewport3D.h" // 3D rendering viewport
#include "3d/Entity3D.h" // 3D game entities
#include "3d/EntityCollection3D.h" // Entity3D collection
#include "McRogueFaceVersion.h" #include "McRogueFaceVersion.h"
#include "GameEngine.h" #include "GameEngine.h"
// ImGui is only available for SFML builds // ImGui is only available for SFML builds
@ -435,6 +437,10 @@ PyObject* PyInit_mcrfpy()
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType, &PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
/*3D entities*/
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
&mcrfpydef::PyEntityCollection3DIterType,
/*grid layers (#147)*/ /*grid layers (#147)*/
&PyColorLayerType, &PyTileLayerType, &PyColorLayerType, &PyTileLayerType,
@ -552,6 +558,7 @@ PyObject* PyInit_mcrfpy()
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist); PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist); PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist); PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist);
// #219 - Initialize PyLock context manager type // #219 - Initialize PyLock context manager type
if (PyLock::init() < 0) { if (PyLock::init() < 0) {

View file

@ -0,0 +1,298 @@
# entity3d_demo.py - Visual demo of Entity3D 3D game entities
# Shows entities moving on a terrain grid with pathfinding and FOV
import mcrfpy
import sys
import math
# Create demo scene
scene = mcrfpy.Scene("entity3d_demo")
# Dark background frame
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="Entity3D Demo - 3D Entities on Navigation Grid", pos=(20, 10))
title.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(title)
# Create the 3D viewport
viewport = mcrfpy.Viewport3D(
pos=(50, 60),
size=(600, 450),
render_resolution=(320, 240), # PS1 resolution
fov=60.0,
camera_pos=(16.0, 12.0, 24.0),
camera_target=(8.0, 0.0, 8.0),
bg_color=mcrfpy.Color(50, 70, 100) # Twilight background
)
scene.children.append(viewport)
# Set up the navigation grid (16x16 for this demo)
GRID_SIZE = 16
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
# Generate simple terrain using HeightMap
print("Generating terrain...")
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
# Create a gentle rolling terrain
hm.mid_point_displacement(0.3, seed=123) # Low roughness for gentle hills
hm.normalize(0.0, 1.0)
# Apply heightmap to navigation grid
viewport.apply_heightmap(hm, 3.0) # y_scale = 3.0 for moderate elevation changes
# Build terrain mesh
vertex_count = viewport.build_terrain(
layer_name="terrain",
heightmap=hm,
y_scale=3.0,
cell_size=1.0
)
print(f"Terrain built with {vertex_count} vertices")
# Create terrain colors (grass-like green with some variation)
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
h = hm[x, y]
# Green grass with height-based variation
r_map[x, y] = 0.2 + h * 0.2
g_map[x, y] = 0.4 + h * 0.3 # More green on higher ground
b_map[x, y] = 0.15 + h * 0.1
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
# Create entities
print("Creating entities...")
# Player entity (bright yellow/orange)
player = mcrfpy.Entity3D(
pos=(8, 8),
rotation=0.0,
scale=0.8,
color=mcrfpy.Color(255, 200, 50)
)
viewport.entities.append(player)
# NPC entities (different colors)
npc_colors = [
mcrfpy.Color(50, 150, 255), # Blue
mcrfpy.Color(255, 80, 80), # Red
mcrfpy.Color(80, 255, 80), # Green
mcrfpy.Color(200, 80, 200), # Purple
]
npcs = []
npc_positions = [(2, 2), (14, 2), (2, 14), (14, 14)]
for i, (x, z) in enumerate(npc_positions):
npc = mcrfpy.Entity3D(
pos=(x, z),
rotation=45.0 * i,
scale=0.6,
color=npc_colors[i]
)
viewport.entities.append(npc)
npcs.append(npc)
print(f"Created {len(viewport.entities)} entities")
# Info panel on the right
info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450),
fill_color=mcrfpy.Color(30, 30, 40),
outline_color=mcrfpy.Color(80, 80, 100),
outline=2.0)
scene.children.append(info_panel)
# Panel title
panel_title = mcrfpy.Caption(text="Entity3D Properties", pos=(690, 70))
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
scene.children.append(panel_title)
# Dynamic property displays
pos_label = mcrfpy.Caption(text="Player Pos: (8, 8)", pos=(690, 100))
pos_label.fill_color = mcrfpy.Color(180, 180, 200)
scene.children.append(pos_label)
world_pos_label = mcrfpy.Caption(text="World Pos: (8.5, ?, 8.5)", pos=(690, 125))
world_pos_label.fill_color = mcrfpy.Color(180, 180, 200)
scene.children.append(world_pos_label)
entity_count_label = mcrfpy.Caption(text=f"Entities: {len(viewport.entities)}", pos=(690, 150))
entity_count_label.fill_color = mcrfpy.Color(180, 180, 200)
scene.children.append(entity_count_label)
# Static properties
props = [
("", ""),
("Grid Size:", f"{GRID_SIZE}x{GRID_SIZE}"),
("Cell Size:", "1.0"),
("Y Scale:", "3.0"),
("", ""),
("Entity Features:", ""),
(" - Grid position (x, z)", ""),
(" - Smooth movement", ""),
(" - Height from terrain", ""),
(" - Per-entity color", ""),
]
y_offset = 180
for label, value in props:
if label:
cap = mcrfpy.Caption(text=f"{label} {value}", pos=(690, y_offset))
cap.fill_color = mcrfpy.Color(150, 150, 170)
scene.children.append(cap)
y_offset += 22
# Instructions at bottom
instructions = mcrfpy.Caption(
text="[WASD] Move player | [Q/E] Rotate | [Space] Orbit | [N] NPC wander | [ESC] Quit",
pos=(20, 530)
)
instructions.fill_color = mcrfpy.Color(150, 150, 150)
scene.children.append(instructions)
# Status line
status = mcrfpy.Caption(text="Status: Use WASD to move the yellow player entity", pos=(20, 555))
status.fill_color = mcrfpy.Color(100, 200, 100)
scene.children.append(status)
# Animation state
animation_time = [0.0]
camera_orbit = [False]
npc_wander = [False]
# Update function - called each frame
def update(timer, runtime):
animation_time[0] += runtime / 1000.0
# Update position labels
px, pz = player.pos
pos_label.text = f"Player Pos: ({px}, {pz})"
wp = player.world_pos
world_pos_label.text = f"World Pos: ({wp[0]:.1f}, {wp[1]:.1f}, {wp[2]:.1f})"
# Camera orbit
if camera_orbit[0]:
angle = animation_time[0] * 0.5
radius = 20.0
center_x = 8.0
center_z = 8.0
height = 12.0 + math.sin(animation_time[0] * 0.3) * 3.0
x = center_x + math.cos(angle) * radius
z = center_z + math.sin(angle) * radius
viewport.camera_pos = (x, height, z)
viewport.camera_target = (center_x, 2.0, center_z)
else:
# Follow player (smoothly)
px, pz = player.pos
target_x = px + 0.5 # Center of cell
target_z = pz + 0.5
# Look at player from behind and above
cam_x = target_x
cam_z = target_z + 12.0
cam_y = 10.0
viewport.camera_pos = (cam_x, cam_y, cam_z)
viewport.camera_target = (target_x, 1.0, target_z)
# NPC wandering
if npc_wander[0]:
for i, npc in enumerate(npcs):
# Each NPC rotates slowly
npc.rotation = (npc.rotation + 1.0 + i * 0.5) % 360.0
# Key handler
def on_key(key, state):
if state != mcrfpy.InputState.PRESSED:
return
px, pz = player.pos
# Player movement with WASD
if key == mcrfpy.Key.W:
new_z = max(0, pz - 1)
player.teleport(px, new_z)
player.rotation = 0.0
status.text = f"Moved north to ({px}, {new_z})"
elif key == mcrfpy.Key.S:
new_z = min(GRID_SIZE - 1, pz + 1)
player.teleport(px, new_z)
player.rotation = 180.0
status.text = f"Moved south to ({px}, {new_z})"
elif key == mcrfpy.Key.A:
new_x = max(0, px - 1)
player.teleport(new_x, pz)
player.rotation = 270.0
status.text = f"Moved west to ({new_x}, {pz})"
elif key == mcrfpy.Key.D:
new_x = min(GRID_SIZE - 1, px + 1)
player.teleport(new_x, pz)
player.rotation = 90.0
status.text = f"Moved east to ({new_x}, {pz})"
# Rotation with Q/E
elif key == mcrfpy.Key.Q:
player.rotation = (player.rotation - 15.0) % 360.0
status.text = f"Rotated to {player.rotation:.1f} degrees"
elif key == mcrfpy.Key.E:
player.rotation = (player.rotation + 15.0) % 360.0
status.text = f"Rotated to {player.rotation:.1f} degrees"
# Toggle camera orbit
elif key == mcrfpy.Key.SPACE:
camera_orbit[0] = not camera_orbit[0]
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF (following player)'}"
# Toggle NPC wandering
elif key == mcrfpy.Key.N:
npc_wander[0] = not npc_wander[0]
status.text = f"NPC wandering: {'ON' if npc_wander[0] else 'OFF'}"
# Entity visibility toggle
elif key == mcrfpy.Key.V:
for npc in npcs:
npc.visible = not npc.visible
status.text = f"NPCs visible: {npcs[0].visible}"
# Scale adjustment
elif key == mcrfpy.Key.EQUAL: # +
player.scale = min(2.0, player.scale + 0.1)
status.text = f"Player scale: {player.scale:.1f}"
elif key == mcrfpy.Key.HYPHEN: # -
player.scale = max(0.3, player.scale - 0.1)
status.text = f"Player scale: {player.scale:.1f}"
elif key == mcrfpy.Key.ESCAPE:
mcrfpy.exit()
# Set up scene
scene.on_key = on_key
# Create timer for updates
timer = mcrfpy.Timer("entity_update", update, 16) # ~60fps
# Activate scene
mcrfpy.current_scene = scene
print()
print("Entity3D Demo loaded!")
print(f"Created {len(viewport.entities)} entities on a {GRID_SIZE}x{GRID_SIZE} grid.")
print()
print("Controls:")
print(" [WASD] Move player")
print(" [Q/E] Rotate player")
print(" [Space] Toggle camera orbit")
print(" [N] Toggle NPC rotation")
print(" [V] Toggle NPC visibility")
print(" [+/-] Scale player")
print(" [ESC] Quit")