diff --git a/src/3d/Entity3D.cpp b/src/3d/Entity3D.cpp new file mode 100644 index 0000000..633b690 --- /dev/null +++ b/src/3d/Entity3D.cpp @@ -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 + +// Include appropriate GL headers based on backend +#if defined(MCRF_SDL2) + #ifdef __EMSCRIPTEN__ + #include + #else + #include + #include + #endif + #define MCRF_HAS_GL 1 +#elif !defined(MCRF_HEADLESS) + // SFML backend - use GLAD + #include + #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 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(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(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> 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>& 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(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(""); + } + + char buffer[128]; + snprintf(buffer, sizeof(buffer), + "", + 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(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(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(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 +}; diff --git a/src/3d/Entity3D.h b/src/3d/Entity3D.h new file mode 100644 index 0000000..c317f5b --- /dev/null +++ b/src/3d/Entity3D.h @@ -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 +#include +#include +#include + +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 { +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 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 getViewport() const { return viewport_.lock(); } + + /// Set owning viewport (called when added to viewport) + void setViewport(std::shared_ptr 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> pathTo(int target_x, int target_z); + + /// Follow a path (queue movement steps) + void followPath(const std::vector>& 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 viewport_; + + // Visibility state per cell (lazy initialized) + mutable std::vector voxel_state_; + mutable bool voxel_state_initialized_ = false; + + // Movement animation + std::queue> 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 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(); + self->weakreflist = nullptr; + } + return (PyObject*)self; + } +}; + +} // namespace mcrfpydef diff --git a/src/3d/EntityCollection3D.cpp b/src/3d/EntityCollection3D.cpp new file mode 100644 index 0000000..08eef9d --- /dev/null +++ b/src/3d/EntityCollection3D.cpp @@ -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(""); + } + return PyUnicode_FromFormat("", 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>>(self->data); + new (&iter_obj->current) std::list>::iterator(self->data->begin()); + new (&iter_obj->end) std::list>::iterator(self->data->end()); + iter_obj->start_size = static_cast(self->data->size()); + + return (PyObject*)iter_obj; +} + +Py_ssize_t EntityCollection3D::len(PyEntityCollection3DObject* self) +{ + if (!self->data) return 0; + return static_cast(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(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(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(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(entity); + obj->weakreflist = nullptr; + + return (PyObject*)obj; +} + +PyObject* EntityCollection3DIter::repr(PyEntityCollection3DIterObject* self) +{ + return PyUnicode_FromString(""); +} diff --git a/src/3d/EntityCollection3D.h b/src/3d/EntityCollection3D.h new file mode 100644 index 0000000..f236de2 --- /dev/null +++ b/src/3d/EntityCollection3D.h @@ -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 +#include + +namespace mcrf { + +// Forward declarations +class Entity3D; +class Viewport3D; + +} // namespace mcrf + +// Python object for EntityCollection3D +typedef struct { + PyObject_HEAD + std::shared_ptr>> data; + std::shared_ptr viewport; +} PyEntityCollection3DObject; + +// Python object for EntityCollection3D iterator +typedef struct { + PyObject_HEAD + std::shared_ptr>> data; + std::list>::iterator current; + std::list>::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 diff --git a/src/3d/Shader3D.h b/src/3d/Shader3D.h index 72c60b3..ffb6b2d 100644 --- a/src/3d/Shader3D.h +++ b/src/3d/Shader3D.h @@ -28,6 +28,9 @@ public: // Check if shader is valid 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) void setUniform(const std::string& name, float value); void setUniform(const std::string& name, int value); diff --git a/src/3d/Viewport3D.cpp b/src/3d/Viewport3D.cpp index 72ab105..d73acd7 100644 --- a/src/3d/Viewport3D.cpp +++ b/src/3d/Viewport3D.cpp @@ -3,6 +3,8 @@ #include "Viewport3D.h" #include "Shader3D.h" #include "MeshLayer.h" +#include "Entity3D.h" +#include "EntityCollection3D.h" #include "../platform/GLContext.h" #include "PyVector.h" #include "PyColor.h" @@ -39,6 +41,7 @@ namespace mcrf { Viewport3D::Viewport3D() : size_(320.0f, 240.0f) + , entities_(std::make_shared>>()) { position = sf::Vector2f(0, 0); camera_.setAspect(size_.x / size_.y); @@ -46,6 +49,7 @@ Viewport3D::Viewport3D() Viewport3D::Viewport3D(float x, float y, float width, float height) : size_(width, height) + , entities_(std::make_shared>>()) { position = sf::Vector2f(x, y); camera_.setAspect(size_.x / size_.y); @@ -423,6 +427,37 @@ bool Viewport3D::isInFOV(int x, int z) const { 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 // ============================================================================= @@ -629,6 +664,11 @@ void Viewport3D::render3DContent() { // Render mesh layers first (terrain, etc.) - sorted by z_index renderMeshLayers(); + // Render entities + mat4 view = camera_.getViewMatrix(); + mat4 projection = camera_.getProjectionMatrix(); + renderEntities(view, projection); + // Render test cube if enabled (disabled when layers are added) if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) { shader_->bind(); @@ -1125,6 +1165,20 @@ static int Viewport3D_set_cell_size_prop(PyViewport3DObject* self, PyObject* val 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>>(self->data->getEntities()); + new (&obj->viewport) std::shared_ptr(self->data); + + return (PyObject*)obj; +} + PyGetSetDef Viewport3D::getsetters[] = { // Position and size {"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, 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 UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D), diff --git a/src/3d/Viewport3D.h b/src/3d/Viewport3D.h index 273557e..ab7d5b2 100644 --- a/src/3d/Viewport3D.h +++ b/src/3d/Viewport3D.h @@ -14,10 +14,16 @@ #include "VoxelPoint.h" #include #include +#include #include #include #include +// Forward declaration +namespace mcrf { +class Entity3D; +} + namespace mcrf { // Forward declarations @@ -171,6 +177,19 @@ public: /// Get TCODMap pointer (for advanced usage) TCODMap* getTCODMap() const { return tcodMap_; } + // ========================================================================= + // Entity3D Management + // ========================================================================= + + /// Get the entity list (for EntityCollection3D) + std::shared_ptr>> 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 void setBackgroundColor(const sf::Color& color) { bgColor_ = color; } sf::Color getBackgroundColor() const { return bgColor_; } @@ -254,6 +273,9 @@ private: TCODMap* tcodMap_ = nullptr; mutable std::mutex fovMutex_; + // Entity3D storage + std::shared_ptr>> entities_; + // Shader for PS1-style rendering std::unique_ptr shader_; diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 1a25f19..a5940dc 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -32,6 +32,8 @@ #include "PyUniformBinding.h" // Shader uniform bindings (#106) #include "PyUniformCollection.h" // Shader uniform collection (#106) #include "3d/Viewport3D.h" // 3D rendering viewport +#include "3d/Entity3D.h" // 3D game entities +#include "3d/EntityCollection3D.h" // Entity3D collection #include "McRogueFaceVersion.h" #include "GameEngine.h" // ImGui is only available for SFML builds @@ -435,6 +437,10 @@ PyObject* PyInit_mcrfpy() &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, &PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType, + /*3D entities*/ + &mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType, + &mcrfpydef::PyEntityCollection3DIterType, + /*grid layers (#147)*/ &PyColorLayerType, &PyTileLayerType, @@ -552,6 +558,7 @@ PyObject* PyInit_mcrfpy() PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist); PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist); PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist); + mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist); // #219 - Initialize PyLock context manager type if (PyLock::init() < 0) { diff --git a/tests/demo/screens/entity3d_demo.py b/tests/demo/screens/entity3d_demo.py new file mode 100644 index 0000000..9a73807 --- /dev/null +++ b/tests/demo/screens/entity3d_demo.py @@ -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")