3D entities
This commit is contained in:
parent
63008bdefd
commit
f4c9db8436
9 changed files with 1964 additions and 0 deletions
865
src/3d/Entity3D.cpp
Normal file
865
src/3d/Entity3D.cpp
Normal 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
325
src/3d/Entity3D.h
Normal 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
|
||||
259
src/3d/EntityCollection3D.cpp
Normal file
259
src/3d/EntityCollection3D.cpp
Normal 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
127
src/3d/EntityCollection3D.h
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<std::list<std::shared_ptr<Entity3D>>>())
|
||||
{
|
||||
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<std::list<std::shared_ptr<Entity3D>>>())
|
||||
{
|
||||
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<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[] = {
|
||||
// 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),
|
||||
|
|
|
|||
|
|
@ -14,10 +14,16 @@
|
|||
#include "VoxelPoint.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <libtcod.h>
|
||||
|
||||
// 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<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
|
||||
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<std::list<std::shared_ptr<Entity3D>>> entities_;
|
||||
|
||||
// Shader for PS1-style rendering
|
||||
std::unique_ptr<Shader3D> shader_;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue