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
|
// Check if shader is valid
|
||||||
bool isValid() const { return program_ != 0; }
|
bool isValid() const { return program_ != 0; }
|
||||||
|
|
||||||
|
// Get the raw shader program ID (for glGetUniformLocation in Entity3D)
|
||||||
|
unsigned int getProgram() const { return program_; }
|
||||||
|
|
||||||
// Uniform setters (cached location lookup)
|
// Uniform setters (cached location lookup)
|
||||||
void setUniform(const std::string& name, float value);
|
void setUniform(const std::string& name, float value);
|
||||||
void setUniform(const std::string& name, int value);
|
void setUniform(const std::string& name, int value);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#include "Viewport3D.h"
|
#include "Viewport3D.h"
|
||||||
#include "Shader3D.h"
|
#include "Shader3D.h"
|
||||||
#include "MeshLayer.h"
|
#include "MeshLayer.h"
|
||||||
|
#include "Entity3D.h"
|
||||||
|
#include "EntityCollection3D.h"
|
||||||
#include "../platform/GLContext.h"
|
#include "../platform/GLContext.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
|
|
@ -39,6 +41,7 @@ namespace mcrf {
|
||||||
|
|
||||||
Viewport3D::Viewport3D()
|
Viewport3D::Viewport3D()
|
||||||
: size_(320.0f, 240.0f)
|
: size_(320.0f, 240.0f)
|
||||||
|
, entities_(std::make_shared<std::list<std::shared_ptr<Entity3D>>>())
|
||||||
{
|
{
|
||||||
position = sf::Vector2f(0, 0);
|
position = sf::Vector2f(0, 0);
|
||||||
camera_.setAspect(size_.x / size_.y);
|
camera_.setAspect(size_.x / size_.y);
|
||||||
|
|
@ -46,6 +49,7 @@ Viewport3D::Viewport3D()
|
||||||
|
|
||||||
Viewport3D::Viewport3D(float x, float y, float width, float height)
|
Viewport3D::Viewport3D(float x, float y, float width, float height)
|
||||||
: size_(width, height)
|
: size_(width, height)
|
||||||
|
, entities_(std::make_shared<std::list<std::shared_ptr<Entity3D>>>())
|
||||||
{
|
{
|
||||||
position = sf::Vector2f(x, y);
|
position = sf::Vector2f(x, y);
|
||||||
camera_.setAspect(size_.x / size_.y);
|
camera_.setAspect(size_.x / size_.y);
|
||||||
|
|
@ -423,6 +427,37 @@ bool Viewport3D::isInFOV(int x, int z) const {
|
||||||
return tcodMap_->isInFov(x, z);
|
return tcodMap_->isInFov(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Entity3D Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::updateEntities(float dt) {
|
||||||
|
if (!entities_) return;
|
||||||
|
|
||||||
|
for (auto& entity : *entities_) {
|
||||||
|
if (entity) {
|
||||||
|
entity->update(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::renderEntities(const mat4& view, const mat4& proj) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (!entities_ || !shader_ || !shader_->isValid()) return;
|
||||||
|
|
||||||
|
// Entity rendering uses the same shader as terrain
|
||||||
|
shader_->bind();
|
||||||
|
|
||||||
|
for (auto& entity : *entities_) {
|
||||||
|
if (entity && entity->isVisible()) {
|
||||||
|
entity->render(view, proj, shader_->getProgram());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shader_->unbind();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// FBO Management
|
// FBO Management
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -629,6 +664,11 @@ void Viewport3D::render3DContent() {
|
||||||
// Render mesh layers first (terrain, etc.) - sorted by z_index
|
// Render mesh layers first (terrain, etc.) - sorted by z_index
|
||||||
renderMeshLayers();
|
renderMeshLayers();
|
||||||
|
|
||||||
|
// Render entities
|
||||||
|
mat4 view = camera_.getViewMatrix();
|
||||||
|
mat4 projection = camera_.getProjectionMatrix();
|
||||||
|
renderEntities(view, projection);
|
||||||
|
|
||||||
// Render test cube if enabled (disabled when layers are added)
|
// Render test cube if enabled (disabled when layers are added)
|
||||||
if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) {
|
if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) {
|
||||||
shader_->bind();
|
shader_->bind();
|
||||||
|
|
@ -1125,6 +1165,20 @@ static int Viewport3D_set_cell_size_prop(PyViewport3DObject* self, PyObject* val
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Entities collection property
|
||||||
|
static PyObject* Viewport3D_get_entities(PyViewport3DObject* self, void* closure) {
|
||||||
|
// Create an EntityCollection3D wrapper for this viewport's entity list
|
||||||
|
auto type = &mcrfpydef::PyEntityCollection3DType;
|
||||||
|
auto obj = (PyEntityCollection3DObject*)type->tp_alloc(type, 0);
|
||||||
|
if (!obj) return NULL;
|
||||||
|
|
||||||
|
// Use placement new for shared_ptr members
|
||||||
|
new (&obj->data) std::shared_ptr<std::list<std::shared_ptr<mcrf::Entity3D>>>(self->data->getEntities());
|
||||||
|
new (&obj->viewport) std::shared_ptr<mcrf::Viewport3D>(self->data);
|
||||||
|
|
||||||
|
return (PyObject*)obj;
|
||||||
|
}
|
||||||
|
|
||||||
PyGetSetDef Viewport3D::getsetters[] = {
|
PyGetSetDef Viewport3D::getsetters[] = {
|
||||||
// Position and size
|
// Position and size
|
||||||
{"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x,
|
{"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x,
|
||||||
|
|
@ -1178,6 +1232,10 @@ PyGetSetDef Viewport3D::getsetters[] = {
|
||||||
{"cell_size", (getter)Viewport3D_get_cell_size_prop, (setter)Viewport3D_set_cell_size_prop,
|
{"cell_size", (getter)Viewport3D_get_cell_size_prop, (setter)Viewport3D_set_cell_size_prop,
|
||||||
MCRF_PROPERTY(cell_size, "World units per navigation grid cell."), NULL},
|
MCRF_PROPERTY(cell_size, "World units per navigation grid cell."), NULL},
|
||||||
|
|
||||||
|
// Entity collection
|
||||||
|
{"entities", (getter)Viewport3D_get_entities, NULL,
|
||||||
|
MCRF_PROPERTY(entities, "Collection of Entity3D objects (read-only). Use append/remove to modify."), NULL},
|
||||||
|
|
||||||
// Common UIDrawable properties
|
// Common UIDrawable properties
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D),
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,16 @@
|
||||||
#include "VoxelPoint.h"
|
#include "VoxelPoint.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <list>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <libtcod.h>
|
#include <libtcod.h>
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
namespace mcrf {
|
||||||
|
class Entity3D;
|
||||||
|
}
|
||||||
|
|
||||||
namespace mcrf {
|
namespace mcrf {
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
|
|
@ -171,6 +177,19 @@ public:
|
||||||
/// Get TCODMap pointer (for advanced usage)
|
/// Get TCODMap pointer (for advanced usage)
|
||||||
TCODMap* getTCODMap() const { return tcodMap_; }
|
TCODMap* getTCODMap() const { return tcodMap_; }
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Entity3D Management
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Get the entity list (for EntityCollection3D)
|
||||||
|
std::shared_ptr<std::list<std::shared_ptr<Entity3D>>> getEntities() { return entities_; }
|
||||||
|
|
||||||
|
/// Update all entities (call once per frame)
|
||||||
|
void updateEntities(float dt);
|
||||||
|
|
||||||
|
/// Render all entities
|
||||||
|
void renderEntities(const mat4& view, const mat4& proj);
|
||||||
|
|
||||||
// Background color
|
// Background color
|
||||||
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||||
sf::Color getBackgroundColor() const { return bgColor_; }
|
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||||
|
|
@ -254,6 +273,9 @@ private:
|
||||||
TCODMap* tcodMap_ = nullptr;
|
TCODMap* tcodMap_ = nullptr;
|
||||||
mutable std::mutex fovMutex_;
|
mutable std::mutex fovMutex_;
|
||||||
|
|
||||||
|
// Entity3D storage
|
||||||
|
std::shared_ptr<std::list<std::shared_ptr<Entity3D>>> entities_;
|
||||||
|
|
||||||
// Shader for PS1-style rendering
|
// Shader for PS1-style rendering
|
||||||
std::unique_ptr<Shader3D> shader_;
|
std::unique_ptr<Shader3D> shader_;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@
|
||||||
#include "PyUniformBinding.h" // Shader uniform bindings (#106)
|
#include "PyUniformBinding.h" // Shader uniform bindings (#106)
|
||||||
#include "PyUniformCollection.h" // Shader uniform collection (#106)
|
#include "PyUniformCollection.h" // Shader uniform collection (#106)
|
||||||
#include "3d/Viewport3D.h" // 3D rendering viewport
|
#include "3d/Viewport3D.h" // 3D rendering viewport
|
||||||
|
#include "3d/Entity3D.h" // 3D game entities
|
||||||
|
#include "3d/EntityCollection3D.h" // Entity3D collection
|
||||||
#include "McRogueFaceVersion.h"
|
#include "McRogueFaceVersion.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
// ImGui is only available for SFML builds
|
// ImGui is only available for SFML builds
|
||||||
|
|
@ -435,6 +437,10 @@ PyObject* PyInit_mcrfpy()
|
||||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
||||||
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||||
|
|
||||||
|
/*3D entities*/
|
||||||
|
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
|
||||||
|
&mcrfpydef::PyEntityCollection3DIterType,
|
||||||
|
|
||||||
/*grid layers (#147)*/
|
/*grid layers (#147)*/
|
||||||
&PyColorLayerType, &PyTileLayerType,
|
&PyColorLayerType, &PyTileLayerType,
|
||||||
|
|
||||||
|
|
@ -552,6 +558,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
||||||
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
||||||
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
|
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
|
||||||
|
mcrfpydef::PyEntity3DType.tp_weaklistoffset = offsetof(PyEntity3DObject, weakreflist);
|
||||||
|
|
||||||
// #219 - Initialize PyLock context manager type
|
// #219 - Initialize PyLock context manager type
|
||||||
if (PyLock::init() < 0) {
|
if (PyLock::init() < 0) {
|
||||||
|
|
|
||||||
298
tests/demo/screens/entity3d_demo.py
Normal file
298
tests/demo/screens/entity3d_demo.py
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
# entity3d_demo.py - Visual demo of Entity3D 3D game entities
|
||||||
|
# Shows entities moving on a terrain grid with pathfinding and FOV
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Create demo scene
|
||||||
|
scene = mcrfpy.Scene("entity3d_demo")
|
||||||
|
|
||||||
|
# Dark background frame
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Entity3D Demo - 3D Entities on Navigation Grid", pos=(20, 10))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
# Create the 3D viewport
|
||||||
|
viewport = mcrfpy.Viewport3D(
|
||||||
|
pos=(50, 60),
|
||||||
|
size=(600, 450),
|
||||||
|
render_resolution=(320, 240), # PS1 resolution
|
||||||
|
fov=60.0,
|
||||||
|
camera_pos=(16.0, 12.0, 24.0),
|
||||||
|
camera_target=(8.0, 0.0, 8.0),
|
||||||
|
bg_color=mcrfpy.Color(50, 70, 100) # Twilight background
|
||||||
|
)
|
||||||
|
scene.children.append(viewport)
|
||||||
|
|
||||||
|
# Set up the navigation grid (16x16 for this demo)
|
||||||
|
GRID_SIZE = 16
|
||||||
|
viewport.set_grid_size(GRID_SIZE, GRID_SIZE)
|
||||||
|
|
||||||
|
# Generate simple terrain using HeightMap
|
||||||
|
print("Generating terrain...")
|
||||||
|
hm = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
|
||||||
|
# Create a gentle rolling terrain
|
||||||
|
hm.mid_point_displacement(0.3, seed=123) # Low roughness for gentle hills
|
||||||
|
hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
# Apply heightmap to navigation grid
|
||||||
|
viewport.apply_heightmap(hm, 3.0) # y_scale = 3.0 for moderate elevation changes
|
||||||
|
|
||||||
|
# Build terrain mesh
|
||||||
|
vertex_count = viewport.build_terrain(
|
||||||
|
layer_name="terrain",
|
||||||
|
heightmap=hm,
|
||||||
|
y_scale=3.0,
|
||||||
|
cell_size=1.0
|
||||||
|
)
|
||||||
|
print(f"Terrain built with {vertex_count} vertices")
|
||||||
|
|
||||||
|
# Create terrain colors (grass-like green with some variation)
|
||||||
|
r_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
g_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
b_map = mcrfpy.HeightMap((GRID_SIZE, GRID_SIZE))
|
||||||
|
|
||||||
|
for y in range(GRID_SIZE):
|
||||||
|
for x in range(GRID_SIZE):
|
||||||
|
h = hm[x, y]
|
||||||
|
# Green grass with height-based variation
|
||||||
|
r_map[x, y] = 0.2 + h * 0.2
|
||||||
|
g_map[x, y] = 0.4 + h * 0.3 # More green on higher ground
|
||||||
|
b_map[x, y] = 0.15 + h * 0.1
|
||||||
|
|
||||||
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||||
|
|
||||||
|
# Create entities
|
||||||
|
print("Creating entities...")
|
||||||
|
|
||||||
|
# Player entity (bright yellow/orange)
|
||||||
|
player = mcrfpy.Entity3D(
|
||||||
|
pos=(8, 8),
|
||||||
|
rotation=0.0,
|
||||||
|
scale=0.8,
|
||||||
|
color=mcrfpy.Color(255, 200, 50)
|
||||||
|
)
|
||||||
|
viewport.entities.append(player)
|
||||||
|
|
||||||
|
# NPC entities (different colors)
|
||||||
|
npc_colors = [
|
||||||
|
mcrfpy.Color(50, 150, 255), # Blue
|
||||||
|
mcrfpy.Color(255, 80, 80), # Red
|
||||||
|
mcrfpy.Color(80, 255, 80), # Green
|
||||||
|
mcrfpy.Color(200, 80, 200), # Purple
|
||||||
|
]
|
||||||
|
|
||||||
|
npcs = []
|
||||||
|
npc_positions = [(2, 2), (14, 2), (2, 14), (14, 14)]
|
||||||
|
for i, (x, z) in enumerate(npc_positions):
|
||||||
|
npc = mcrfpy.Entity3D(
|
||||||
|
pos=(x, z),
|
||||||
|
rotation=45.0 * i,
|
||||||
|
scale=0.6,
|
||||||
|
color=npc_colors[i]
|
||||||
|
)
|
||||||
|
viewport.entities.append(npc)
|
||||||
|
npcs.append(npc)
|
||||||
|
|
||||||
|
print(f"Created {len(viewport.entities)} entities")
|
||||||
|
|
||||||
|
# Info panel on the right
|
||||||
|
info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450),
|
||||||
|
fill_color=mcrfpy.Color(30, 30, 40),
|
||||||
|
outline_color=mcrfpy.Color(80, 80, 100),
|
||||||
|
outline=2.0)
|
||||||
|
scene.children.append(info_panel)
|
||||||
|
|
||||||
|
# Panel title
|
||||||
|
panel_title = mcrfpy.Caption(text="Entity3D Properties", pos=(690, 70))
|
||||||
|
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
|
||||||
|
scene.children.append(panel_title)
|
||||||
|
|
||||||
|
# Dynamic property displays
|
||||||
|
pos_label = mcrfpy.Caption(text="Player Pos: (8, 8)", pos=(690, 100))
|
||||||
|
pos_label.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
scene.children.append(pos_label)
|
||||||
|
|
||||||
|
world_pos_label = mcrfpy.Caption(text="World Pos: (8.5, ?, 8.5)", pos=(690, 125))
|
||||||
|
world_pos_label.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
scene.children.append(world_pos_label)
|
||||||
|
|
||||||
|
entity_count_label = mcrfpy.Caption(text=f"Entities: {len(viewport.entities)}", pos=(690, 150))
|
||||||
|
entity_count_label.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
scene.children.append(entity_count_label)
|
||||||
|
|
||||||
|
# Static properties
|
||||||
|
props = [
|
||||||
|
("", ""),
|
||||||
|
("Grid Size:", f"{GRID_SIZE}x{GRID_SIZE}"),
|
||||||
|
("Cell Size:", "1.0"),
|
||||||
|
("Y Scale:", "3.0"),
|
||||||
|
("", ""),
|
||||||
|
("Entity Features:", ""),
|
||||||
|
(" - Grid position (x, z)", ""),
|
||||||
|
(" - Smooth movement", ""),
|
||||||
|
(" - Height from terrain", ""),
|
||||||
|
(" - Per-entity color", ""),
|
||||||
|
]
|
||||||
|
|
||||||
|
y_offset = 180
|
||||||
|
for label, value in props:
|
||||||
|
if label:
|
||||||
|
cap = mcrfpy.Caption(text=f"{label} {value}", pos=(690, y_offset))
|
||||||
|
cap.fill_color = mcrfpy.Color(150, 150, 170)
|
||||||
|
scene.children.append(cap)
|
||||||
|
y_offset += 22
|
||||||
|
|
||||||
|
# Instructions at bottom
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
text="[WASD] Move player | [Q/E] Rotate | [Space] Orbit | [N] NPC wander | [ESC] Quit",
|
||||||
|
pos=(20, 530)
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
# Status line
|
||||||
|
status = mcrfpy.Caption(text="Status: Use WASD to move the yellow player entity", pos=(20, 555))
|
||||||
|
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
scene.children.append(status)
|
||||||
|
|
||||||
|
# Animation state
|
||||||
|
animation_time = [0.0]
|
||||||
|
camera_orbit = [False]
|
||||||
|
npc_wander = [False]
|
||||||
|
|
||||||
|
# Update function - called each frame
|
||||||
|
def update(timer, runtime):
|
||||||
|
animation_time[0] += runtime / 1000.0
|
||||||
|
|
||||||
|
# Update position labels
|
||||||
|
px, pz = player.pos
|
||||||
|
pos_label.text = f"Player Pos: ({px}, {pz})"
|
||||||
|
|
||||||
|
wp = player.world_pos
|
||||||
|
world_pos_label.text = f"World Pos: ({wp[0]:.1f}, {wp[1]:.1f}, {wp[2]:.1f})"
|
||||||
|
|
||||||
|
# Camera orbit
|
||||||
|
if camera_orbit[0]:
|
||||||
|
angle = animation_time[0] * 0.5
|
||||||
|
radius = 20.0
|
||||||
|
center_x = 8.0
|
||||||
|
center_z = 8.0
|
||||||
|
height = 12.0 + math.sin(animation_time[0] * 0.3) * 3.0
|
||||||
|
|
||||||
|
x = center_x + math.cos(angle) * radius
|
||||||
|
z = center_z + math.sin(angle) * radius
|
||||||
|
|
||||||
|
viewport.camera_pos = (x, height, z)
|
||||||
|
viewport.camera_target = (center_x, 2.0, center_z)
|
||||||
|
else:
|
||||||
|
# Follow player (smoothly)
|
||||||
|
px, pz = player.pos
|
||||||
|
target_x = px + 0.5 # Center of cell
|
||||||
|
target_z = pz + 0.5
|
||||||
|
|
||||||
|
# Look at player from behind and above
|
||||||
|
cam_x = target_x
|
||||||
|
cam_z = target_z + 12.0
|
||||||
|
cam_y = 10.0
|
||||||
|
|
||||||
|
viewport.camera_pos = (cam_x, cam_y, cam_z)
|
||||||
|
viewport.camera_target = (target_x, 1.0, target_z)
|
||||||
|
|
||||||
|
# NPC wandering
|
||||||
|
if npc_wander[0]:
|
||||||
|
for i, npc in enumerate(npcs):
|
||||||
|
# Each NPC rotates slowly
|
||||||
|
npc.rotation = (npc.rotation + 1.0 + i * 0.5) % 360.0
|
||||||
|
|
||||||
|
# Key handler
|
||||||
|
def on_key(key, state):
|
||||||
|
if state != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
|
||||||
|
px, pz = player.pos
|
||||||
|
|
||||||
|
# Player movement with WASD
|
||||||
|
if key == mcrfpy.Key.W:
|
||||||
|
new_z = max(0, pz - 1)
|
||||||
|
player.teleport(px, new_z)
|
||||||
|
player.rotation = 0.0
|
||||||
|
status.text = f"Moved north to ({px}, {new_z})"
|
||||||
|
elif key == mcrfpy.Key.S:
|
||||||
|
new_z = min(GRID_SIZE - 1, pz + 1)
|
||||||
|
player.teleport(px, new_z)
|
||||||
|
player.rotation = 180.0
|
||||||
|
status.text = f"Moved south to ({px}, {new_z})"
|
||||||
|
elif key == mcrfpy.Key.A:
|
||||||
|
new_x = max(0, px - 1)
|
||||||
|
player.teleport(new_x, pz)
|
||||||
|
player.rotation = 270.0
|
||||||
|
status.text = f"Moved west to ({new_x}, {pz})"
|
||||||
|
elif key == mcrfpy.Key.D:
|
||||||
|
new_x = min(GRID_SIZE - 1, px + 1)
|
||||||
|
player.teleport(new_x, pz)
|
||||||
|
player.rotation = 90.0
|
||||||
|
status.text = f"Moved east to ({new_x}, {pz})"
|
||||||
|
|
||||||
|
# Rotation with Q/E
|
||||||
|
elif key == mcrfpy.Key.Q:
|
||||||
|
player.rotation = (player.rotation - 15.0) % 360.0
|
||||||
|
status.text = f"Rotated to {player.rotation:.1f} degrees"
|
||||||
|
elif key == mcrfpy.Key.E:
|
||||||
|
player.rotation = (player.rotation + 15.0) % 360.0
|
||||||
|
status.text = f"Rotated to {player.rotation:.1f} degrees"
|
||||||
|
|
||||||
|
# Toggle camera orbit
|
||||||
|
elif key == mcrfpy.Key.SPACE:
|
||||||
|
camera_orbit[0] = not camera_orbit[0]
|
||||||
|
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF (following player)'}"
|
||||||
|
|
||||||
|
# Toggle NPC wandering
|
||||||
|
elif key == mcrfpy.Key.N:
|
||||||
|
npc_wander[0] = not npc_wander[0]
|
||||||
|
status.text = f"NPC wandering: {'ON' if npc_wander[0] else 'OFF'}"
|
||||||
|
|
||||||
|
# Entity visibility toggle
|
||||||
|
elif key == mcrfpy.Key.V:
|
||||||
|
for npc in npcs:
|
||||||
|
npc.visible = not npc.visible
|
||||||
|
status.text = f"NPCs visible: {npcs[0].visible}"
|
||||||
|
|
||||||
|
# Scale adjustment
|
||||||
|
elif key == mcrfpy.Key.EQUAL: # +
|
||||||
|
player.scale = min(2.0, player.scale + 0.1)
|
||||||
|
status.text = f"Player scale: {player.scale:.1f}"
|
||||||
|
elif key == mcrfpy.Key.HYPHEN: # -
|
||||||
|
player.scale = max(0.3, player.scale - 0.1)
|
||||||
|
status.text = f"Player scale: {player.scale:.1f}"
|
||||||
|
|
||||||
|
elif key == mcrfpy.Key.ESCAPE:
|
||||||
|
mcrfpy.exit()
|
||||||
|
|
||||||
|
# Set up scene
|
||||||
|
scene.on_key = on_key
|
||||||
|
|
||||||
|
# Create timer for updates
|
||||||
|
timer = mcrfpy.Timer("entity_update", update, 16) # ~60fps
|
||||||
|
|
||||||
|
# Activate scene
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Entity3D Demo loaded!")
|
||||||
|
print(f"Created {len(viewport.entities)} entities on a {GRID_SIZE}x{GRID_SIZE} grid.")
|
||||||
|
print()
|
||||||
|
print("Controls:")
|
||||||
|
print(" [WASD] Move player")
|
||||||
|
print(" [Q/E] Rotate player")
|
||||||
|
print(" [Space] Toggle camera orbit")
|
||||||
|
print(" [N] Toggle NPC rotation")
|
||||||
|
print(" [V] Toggle NPC visibility")
|
||||||
|
print(" [+/-] Scale player")
|
||||||
|
print(" [ESC] Quit")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue