feat(engine): implement perspective FOV, pathfinding, and GUI text widgets
Major Engine Enhancements: - Complete FOV (Field of View) system with perspective rendering - UIGrid.perspective property for entity-based visibility - Three-layer overlay colors (unexplored, explored, visible) - Per-entity visibility state tracking - Perfect knowledge updates only for explored areas - Advanced Pathfinding Integration - A* pathfinding implementation in UIGrid - Entity.path_to() method for direct pathfinding - Dijkstra maps for multi-target pathfinding - Path caching for performance optimization - GUI Text Input Widgets - TextInputWidget class with cursor, selection, scrolling - Improved widget with proper text rendering and input handling - Example showcase of multiple text input fields - Foundation for in-game console and chat systems - Performance & Architecture Improvements - PyTexture copy operations optimized - GameEngine update cycle refined - UIEntity property handling enhanced - UITestScene modernized Test Suite: - Interactive visibility demos showing FOV in action - Pathfinding comparison (A* vs Dijkstra) - Debug utilities for visibility and empty path handling - Sizzle reel demo combining pathfinding and vision - Multiple text input test scenarios This commit brings McRogueFace closer to a complete roguelike engine with essential features like line-of-sight, intelligent pathfinding, and interactive text input capabilities. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
051a2ca951
commit
d13153ddb4
|
@ -5,6 +5,7 @@
|
|||
#include "UITestScene.h"
|
||||
#include "Resources.h"
|
||||
#include "Animation.h"
|
||||
#include <cmath>
|
||||
|
||||
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
|
||||
{
|
||||
|
@ -35,7 +36,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
|
||||
// Initialize the game view
|
||||
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y));
|
||||
gameView.setCenter(gameResolution.x / 2.0f, gameResolution.y / 2.0f);
|
||||
// Use integer center coordinates for pixel-perfect rendering
|
||||
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
|
||||
updateViewport();
|
||||
scene = "uitest";
|
||||
scenes["uitest"] = new UITestScene(this);
|
||||
|
@ -417,7 +419,8 @@ void GameEngine::setFramerateLimit(unsigned int limit)
|
|||
void GameEngine::setGameResolution(unsigned int width, unsigned int height) {
|
||||
gameResolution = sf::Vector2u(width, height);
|
||||
gameView.setSize(static_cast<float>(width), static_cast<float>(height));
|
||||
gameView.setCenter(width / 2.0f, height / 2.0f);
|
||||
// Use integer center coordinates for pixel-perfect rendering
|
||||
gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f));
|
||||
updateViewport();
|
||||
}
|
||||
|
||||
|
@ -446,8 +449,9 @@ void GameEngine::updateViewport() {
|
|||
float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x));
|
||||
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y));
|
||||
|
||||
float offsetX = (windowSize.x - viewportWidth) / 2.0f;
|
||||
float offsetY = (windowSize.y - viewportHeight) / 2.0f;
|
||||
// Floor offsets to ensure integer pixel alignment
|
||||
float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f);
|
||||
float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f);
|
||||
|
||||
gameView.setViewport(sf::FloatRect(
|
||||
offsetX / windowSize.x,
|
||||
|
@ -474,13 +478,21 @@ void GameEngine::updateViewport() {
|
|||
|
||||
if (windowAspect > gameAspect) {
|
||||
// Window is wider - black bars on sides
|
||||
// Calculate viewport size in pixels and floor for pixel-perfect scaling
|
||||
float pixelHeight = static_cast<float>(windowSize.y);
|
||||
float pixelWidth = std::floor(pixelHeight * gameAspect);
|
||||
|
||||
viewportHeight = 1.0f;
|
||||
viewportWidth = gameAspect / windowAspect;
|
||||
viewportWidth = pixelWidth / windowSize.x;
|
||||
offsetX = (1.0f - viewportWidth) / 2.0f;
|
||||
} else {
|
||||
// Window is taller - black bars on top/bottom
|
||||
// Calculate viewport size in pixels and floor for pixel-perfect scaling
|
||||
float pixelWidth = static_cast<float>(windowSize.x);
|
||||
float pixelHeight = std::floor(pixelWidth / gameAspect);
|
||||
|
||||
viewportWidth = 1.0f;
|
||||
viewportHeight = windowAspect / gameAspect;
|
||||
viewportHeight = pixelHeight / windowSize.y;
|
||||
offsetY = (1.0f - viewportHeight) / 2.0f;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
#include "McRFPy_API.h"
|
||||
|
||||
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
|
||||
{
|
||||
texture = sf::Texture();
|
||||
texture.loadFromFile(source);
|
||||
if (!texture.loadFromFile(source)) {
|
||||
// Failed to load texture - leave sheet dimensions as 0
|
||||
// This will be checked in init()
|
||||
return;
|
||||
}
|
||||
texture.setSmooth(false); // Disable smoothing for pixel art
|
||||
auto size = texture.getSize();
|
||||
sheet_width = (size.x / sprite_width);
|
||||
sheet_height = (size.y / sprite_height);
|
||||
|
@ -18,6 +23,12 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
|||
|
||||
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
|
||||
{
|
||||
// Protect against division by zero if texture failed to load
|
||||
if (sheet_width == 0 || sheet_height == 0) {
|
||||
// Return an empty sprite
|
||||
return sf::Sprite();
|
||||
}
|
||||
|
||||
int tx = index % sheet_width, ty = index / sheet_width;
|
||||
auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height);
|
||||
auto sprite = sf::Sprite(texture, ir);
|
||||
|
@ -71,7 +82,16 @@ int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds)
|
|||
int sprite_width, sprite_height;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast<char**>(keywords), &filename, &sprite_width, &sprite_height))
|
||||
return -1;
|
||||
|
||||
// Create the texture object
|
||||
self->data = std::make_shared<PyTexture>(filename, sprite_width, sprite_height);
|
||||
|
||||
// Check if the texture failed to load (sheet dimensions will be 0)
|
||||
if (self->data->sheet_width == 0 || self->data->sheet_height == 0) {
|
||||
PyErr_Format(PyExc_IOError, "Failed to load texture from file: %s", filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
111
src/UIEntity.cpp
111
src/UIEntity.cpp
|
@ -9,16 +9,52 @@
|
|||
#include "UIEntityPyMethods.h"
|
||||
|
||||
|
||||
|
||||
UIEntity::UIEntity()
|
||||
: self(nullptr), grid(nullptr), position(0.0f, 0.0f)
|
||||
{
|
||||
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
|
||||
// gridstate vector starts empty since we don't know grid dimensions
|
||||
// gridstate vector starts empty - will be lazily initialized when needed
|
||||
}
|
||||
|
||||
UIEntity::UIEntity(UIGrid& grid)
|
||||
: gridstate(grid.grid_x * grid.grid_y)
|
||||
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
|
||||
|
||||
void UIEntity::updateVisibility()
|
||||
{
|
||||
if (!grid) return;
|
||||
|
||||
// Lazy initialize gridstate if needed
|
||||
if (gridstate.size() == 0) {
|
||||
gridstate.resize(grid->grid_x * grid->grid_y);
|
||||
// Initialize all cells as not visible/discovered
|
||||
for (auto& state : gridstate) {
|
||||
state.visible = false;
|
||||
state.discovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// First, mark all cells as not visible
|
||||
for (auto& state : gridstate) {
|
||||
state.visible = false;
|
||||
}
|
||||
|
||||
// Compute FOV from entity's position
|
||||
int x = static_cast<int>(position.x);
|
||||
int y = static_cast<int>(position.y);
|
||||
|
||||
// Use default FOV radius of 10 (can be made configurable later)
|
||||
grid->computeFOV(x, y, 10);
|
||||
|
||||
// Update visible cells based on FOV computation
|
||||
for (int gy = 0; gy < grid->grid_y; gy++) {
|
||||
for (int gx = 0; gx < grid->grid_x; gx++) {
|
||||
int idx = gy * grid->grid_x + gx;
|
||||
if (grid->isInFOV(gx, gy)) {
|
||||
gridstate[idx].visible = true;
|
||||
gridstate[idx].discovered = true; // Once seen, always discovered
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
|
||||
|
@ -32,17 +68,29 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
|
|||
PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid");
|
||||
return NULL;
|
||||
}
|
||||
/*
|
||||
PyUIGridPointStateObject* obj = (PyUIGridPointStateObject*)((&mcrfpydef::PyUIGridPointStateType)->tp_alloc(&mcrfpydef::PyUIGridPointStateType, 0));
|
||||
*/
|
||||
|
||||
// Lazy initialize gridstate if needed
|
||||
if (self->data->gridstate.size() == 0) {
|
||||
self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y);
|
||||
// Initialize all cells as not visible/discovered
|
||||
for (auto& state : self->data->gridstate) {
|
||||
state.visible = false;
|
||||
state.discovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) {
|
||||
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
|
||||
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
|
||||
//auto target = std::static_pointer_cast<UIEntity>(target);
|
||||
obj->data = &(self->data->gridstate[y + self->data->grid->grid_x * x]);
|
||||
obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]);
|
||||
obj->grid = self->data->grid;
|
||||
obj->entity = self->data;
|
||||
return (PyObject*)obj;
|
||||
|
||||
}
|
||||
|
||||
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
|
@ -166,10 +214,8 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (grid_obj == NULL)
|
||||
// Always use default constructor for lazy initialization
|
||||
self->data = std::make_shared<UIEntity>();
|
||||
else
|
||||
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid_obj)->data);
|
||||
|
||||
// Store reference to Python object
|
||||
self->data->self = (PyObject*)self;
|
||||
|
@ -191,6 +237,9 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
self->data->grid = pygrid->data;
|
||||
// todone - on creation of Entity with Grid assignment, also append it to the entity list
|
||||
pygrid->data->entities->push_back(self->data);
|
||||
|
||||
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
|
||||
// gridstate will be initialized when visibility is updated or accessed
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -237,11 +286,26 @@ sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
|
|||
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
|
||||
}
|
||||
|
||||
// TODO - deprecate / remove this helper
|
||||
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) {
|
||||
// This function is incomplete - it creates an empty object without setting state data
|
||||
// Should use PyObjectUtils::createGridPointState() instead
|
||||
return PyObjectUtils::createPyObjectGeneric("GridPointState");
|
||||
// Create a new GridPointState Python object
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
|
||||
if (!type) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
|
||||
if (!obj) {
|
||||
Py_DECREF(type);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Allocate new data and copy values
|
||||
obj->data = new UIGridPointState();
|
||||
obj->data->visible = state.visible;
|
||||
obj->data->discovered = state.discovered;
|
||||
|
||||
Py_DECREF(type);
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec) {
|
||||
|
@ -434,11 +498,18 @@ PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kw
|
|||
return path_list;
|
||||
}
|
||||
|
||||
PyObject* UIEntity::update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
||||
{
|
||||
self->data->updateVisibility();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyMethodDef UIEntity::methods[] = {
|
||||
{"at", (PyCFunction)UIEntity::at, METH_O},
|
||||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"},
|
||||
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
@ -452,6 +523,7 @@ PyMethodDef UIEntity_all_methods[] = {
|
|||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"},
|
||||
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
|
@ -485,15 +557,12 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
|
|||
bool UIEntity::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
position.x = value;
|
||||
// Update sprite position based on grid position
|
||||
// Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties
|
||||
sprite.setPosition(sf::Vector2f(position.x, position.y));
|
||||
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
position.y = value;
|
||||
// Update sprite position based on grid position
|
||||
sprite.setPosition(sf::Vector2f(position.x, position.y));
|
||||
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
||||
return true;
|
||||
}
|
||||
else if (name == "sprite_scale") {
|
||||
|
|
|
@ -27,10 +27,10 @@ class UIGrid;
|
|||
//} PyUIEntityObject;
|
||||
|
||||
// helper methods with no namespace requirement
|
||||
static PyObject* sfVector2f_to_PyObject(sf::Vector2f vector);
|
||||
static sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
|
||||
static PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
|
||||
static PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
|
||||
PyObject* sfVector2f_to_PyObject(sf::Vector2f vector);
|
||||
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
|
||||
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
|
||||
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
|
||||
|
||||
// TODO: make UIEntity a drawable
|
||||
class UIEntity//: public UIDrawable
|
||||
|
@ -44,7 +44,9 @@ public:
|
|||
//void render(sf::Vector2f); //override final;
|
||||
|
||||
UIEntity();
|
||||
UIEntity(UIGrid&);
|
||||
|
||||
// Visibility methods
|
||||
void updateVisibility(); // Update gridstate from current FOV
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value);
|
||||
|
@ -60,6 +62,7 @@ public:
|
|||
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
static PyObject* get_position(PyUIEntityObject* self, void* closure);
|
||||
|
|
178
src/UIGrid.cpp
178
src/UIGrid.cpp
|
@ -7,7 +7,8 @@
|
|||
|
||||
UIGrid::UIGrid()
|
||||
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr) // Default dark gray background
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||
perspective(-1) // Default to omniscient view
|
||||
{
|
||||
// Initialize entities list
|
||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||
|
@ -34,7 +35,8 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
: grid_x(gx), grid_y(gy),
|
||||
zoom(1.0f),
|
||||
ptex(_ptex), points(gx * gy),
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr) // Default dark gray background
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||
perspective(-1) // Default to omniscient view
|
||||
{
|
||||
// Use texture dimensions if available, otherwise use defaults
|
||||
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
|
@ -70,6 +72,9 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
// Create TCOD dijkstra pathfinder
|
||||
tcod_dijkstra = new TCODDijkstra(tcod_map);
|
||||
|
||||
// Create TCOD A* pathfinder
|
||||
tcod_path = new TCODPath(tcod_map);
|
||||
|
||||
// Initialize grid points with parent reference
|
||||
for (int y = 0; y < gy; y++) {
|
||||
for (int x = 0; x < gx; x++) {
|
||||
|
@ -183,43 +188,55 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
}
|
||||
|
||||
|
||||
// top layer - opacity for discovered / visible status (debug, basically)
|
||||
/* // Disabled until I attach a "perspective"
|
||||
// top layer - opacity for discovered / visible status based on perspective
|
||||
// Only render visibility overlay if perspective is set (not omniscient)
|
||||
if (perspective >= 0 && perspective < static_cast<int>(entities->size())) {
|
||||
// Get the entity whose perspective we're using
|
||||
auto it = entities->begin();
|
||||
std::advance(it, perspective);
|
||||
auto& entity = *it;
|
||||
|
||||
// Create rectangle for overlays
|
||||
sf::RectangleShape overlay;
|
||||
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||
x < x_limit; //x < view_width;
|
||||
x < x_limit;
|
||||
x+=1)
|
||||
{
|
||||
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||
y < y_limit; //y < view_height;
|
||||
y < y_limit;
|
||||
y+=1)
|
||||
{
|
||||
// Skip out-of-bounds cells
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x*itex->grid_size - left_spritepixels) * zoom,
|
||||
(y*itex->grid_size - top_spritepixels) * zoom );
|
||||
(x*cell_width - left_spritepixels) * zoom,
|
||||
(y*cell_height - top_spritepixels) * zoom );
|
||||
|
||||
auto gridpoint = at(std::floor(x), std::floor(y));
|
||||
// Get visibility state from entity's perspective
|
||||
int idx = y * grid_x + x;
|
||||
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
||||
const auto& state = entity->gridstate[idx];
|
||||
|
||||
sprite.setPosition(pixel_pos);
|
||||
overlay.setPosition(pixel_pos);
|
||||
|
||||
r.setPosition(pixel_pos);
|
||||
|
||||
// visible & discovered layers for testing purposes
|
||||
if (!gridpoint.discovered) {
|
||||
r.setFillColor(sf::Color(16, 16, 20, 192)); // 255 opacity for actual blackout
|
||||
renderTexture.draw(r);
|
||||
} else if (!gridpoint.visible) {
|
||||
r.setFillColor(sf::Color(32, 32, 40, 128));
|
||||
renderTexture.draw(r);
|
||||
// Three overlay colors as specified:
|
||||
if (!state.discovered) {
|
||||
// Never seen - black
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
} else if (!state.visible) {
|
||||
// Discovered but not currently visible - dark gray
|
||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||
renderTexture.draw(overlay);
|
||||
}
|
||||
// If visible and discovered, no overlay (fully visible)
|
||||
}
|
||||
}
|
||||
|
||||
// overlay
|
||||
|
||||
// uisprite
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// grid lines for testing & validation
|
||||
/*
|
||||
|
@ -255,6 +272,10 @@ UIGridPoint& UIGrid::at(int x, int y)
|
|||
|
||||
UIGrid::~UIGrid()
|
||||
{
|
||||
if (tcod_path) {
|
||||
delete tcod_path;
|
||||
tcod_path = nullptr;
|
||||
}
|
||||
if (tcod_dijkstra) {
|
||||
delete tcod_dijkstra;
|
||||
tcod_dijkstra = nullptr;
|
||||
|
@ -363,6 +384,41 @@ std::vector<std::pair<int, int>> UIGrid::getDijkstraPath(int x, int y) const
|
|||
return path;
|
||||
}
|
||||
|
||||
// A* pathfinding implementation
|
||||
std::vector<std::pair<int, int>> UIGrid::computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost)
|
||||
{
|
||||
std::vector<std::pair<int, int>> path;
|
||||
|
||||
// Validate inputs
|
||||
if (!tcod_map || !tcod_path ||
|
||||
x1 < 0 || x1 >= grid_x || y1 < 0 || y1 >= grid_y ||
|
||||
x2 < 0 || x2 >= grid_x || y2 < 0 || y2 >= grid_y) {
|
||||
return path; // Return empty path
|
||||
}
|
||||
|
||||
// Set diagonal cost (TCODPath doesn't take it as parameter to compute)
|
||||
// Instead, diagonal cost is set during TCODPath construction
|
||||
// For now, we'll use the default diagonal cost from the constructor
|
||||
|
||||
// Compute the path
|
||||
bool success = tcod_path->compute(x1, y1, x2, y2);
|
||||
|
||||
if (success) {
|
||||
// Get the computed path
|
||||
int pathSize = tcod_path->size();
|
||||
path.reserve(pathSize);
|
||||
|
||||
// TCOD path includes the starting position, so we start from index 0
|
||||
for (int i = 0; i < pathSize; i++) {
|
||||
int px, py;
|
||||
tcod_path->get(i, &px, &py);
|
||||
path.push_back(std::make_pair(px, py));
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// Phase 1 implementations
|
||||
sf::FloatRect UIGrid::get_bounds() const
|
||||
{
|
||||
|
@ -876,6 +932,38 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->perspective);
|
||||
}
|
||||
|
||||
int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
long perspective = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate perspective (-1 for omniscient, or valid entity index)
|
||||
if (perspective < -1) {
|
||||
PyErr_SetString(PyExc_ValueError, "perspective must be -1 (omniscient) or a valid entity index");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if entity index is valid (if not omniscient)
|
||||
if (perspective >= 0 && self->data->entities) {
|
||||
int entity_count = self->data->entities->size();
|
||||
if (perspective >= entity_count) {
|
||||
PyErr_Format(PyExc_IndexError, "perspective index %ld out of range (grid has %d entities)",
|
||||
perspective, entity_count);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
self->data->perspective = perspective;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Python API implementations for TCOD functionality
|
||||
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
|
@ -980,6 +1068,31 @@ PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args)
|
|||
return path_list;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
int x1, y1, x2, y2;
|
||||
float diagonal_cost = 1.41f;
|
||||
|
||||
static char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", kwlist,
|
||||
&x1, &y1, &x2, &y2, &diagonal_cost)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Compute A* path
|
||||
std::vector<std::pair<int, int>> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost);
|
||||
|
||||
// Convert to Python list
|
||||
PyObject* path_list = PyList_New(path.size());
|
||||
for (size_t i = 0; i < path.size(); i++) {
|
||||
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
|
||||
PyList_SetItem(path_list, i, pos); // Steals reference
|
||||
}
|
||||
|
||||
return path_list;
|
||||
}
|
||||
|
||||
PyMethodDef UIGrid::methods[] = {
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
|
@ -994,6 +1107,8 @@ PyMethodDef UIGrid::methods[] = {
|
|||
"Get distance from Dijkstra root to position. Args: x, y. Returns float or None if invalid."},
|
||||
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
|
||||
"Get path from position to Dijkstra root. Args: x, y. Returns list of (x,y) tuples."},
|
||||
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
|
||||
"Compute A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41. Returns list of (x,y) tuples. Note: diagonal_cost is currently ignored (uses default 1.41)."},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
@ -1016,6 +1131,8 @@ PyMethodDef UIGrid_all_methods[] = {
|
|||
"Get distance from Dijkstra root to position. Args: x, y. Returns float or None if invalid."},
|
||||
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
|
||||
"Get path from position to Dijkstra root. Args: x, y. Returns list of (x,y) tuples."},
|
||||
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
|
||||
"Compute A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41. Returns list of (x,y) tuples. Note: diagonal_cost is currently ignored (uses default 1.41)."},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
|
@ -1044,6 +1161,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
|
||||
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
|
||||
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
|
||||
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective, "Entity perspective index (-1 for omniscient view)", NULL},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
|
@ -1387,6 +1505,16 @@ PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject*
|
|||
self->data->push_back(entity->data);
|
||||
entity->data->grid = self->grid;
|
||||
|
||||
// Initialize gridstate if not already done
|
||||
if (entity->data->gridstate.size() == 0 && self->grid) {
|
||||
entity->data->gridstate.resize(self->grid->grid_x * self->grid->grid_y);
|
||||
// Initialize all cells as not visible/discovered
|
||||
for (auto& state : entity->data->gridstate) {
|
||||
state.visible = false;
|
||||
state.discovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
|
10
src/UIGrid.h
10
src/UIGrid.h
|
@ -28,6 +28,7 @@ private:
|
|||
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
||||
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
|
||||
TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding
|
||||
TCODPath* tcod_path; // A* pathfinding
|
||||
|
||||
public:
|
||||
UIGrid();
|
||||
|
@ -53,6 +54,9 @@ public:
|
|||
float getDijkstraDistance(int x, int y) const;
|
||||
std::vector<std::pair<int, int>> getDijkstraPath(int x, int y) const;
|
||||
|
||||
// A* pathfinding methods
|
||||
std::vector<std::pair<int, int>> computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
|
||||
|
||||
// Phase 1 virtual method implementations
|
||||
sf::FloatRect get_bounds() const override;
|
||||
void move(float dx, float dy) override;
|
||||
|
@ -73,6 +77,9 @@ public:
|
|||
// Background rendering
|
||||
sf::Color fill_color;
|
||||
|
||||
// Perspective system - which entity's view to render (-1 = omniscient/default)
|
||||
int perspective;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||
|
@ -94,6 +101,8 @@ public:
|
|||
static PyObject* get_texture(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_fill_color(PyUIGridObject* self, void* closure);
|
||||
static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_perspective(PyUIGridObject* self, void* closure);
|
||||
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
|
||||
|
@ -101,6 +110,7 @@ public:
|
|||
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* get_children(PyUIGridObject* self, void* closure);
|
||||
|
|
|
@ -121,7 +121,7 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
|
|||
//UIEntity test:
|
||||
// asdf
|
||||
// TODO - reimplement UISprite style rendering within UIEntity class. Entities don't have a screen pixel position, they have a grid position, and grid sets zoom when rendering them.
|
||||
auto e5a = std::make_shared<UIEntity>(*e5); // this basic constructor sucks: sprite position + zoom are irrelevant for UIEntity.
|
||||
auto e5a = std::make_shared<UIEntity>(); // Default constructor - lazy initialization
|
||||
e5a->grid = e5;
|
||||
//auto e5as = UISprite(indextex, 85, sf::Vector2f(0, 0), 1.0);
|
||||
//e5a->sprite = e5as; // will copy constructor even exist for UISprite...?
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
from text_input_widget_improved import FocusManager, TextInput
|
||||
|
||||
# Create focus manager
|
||||
focus_mgr = FocusManager()
|
||||
|
||||
# Create input field
|
||||
name_input = TextInput(
|
||||
x=50, y=100,
|
||||
width=300,
|
||||
label="Name:",
|
||||
placeholder="Enter your name",
|
||||
on_change=lambda text: print(f"Name changed to: {text}")
|
||||
)
|
||||
|
||||
tags_input = TextInput(
|
||||
x=50, y=160,
|
||||
width=300,
|
||||
label="Tags:",
|
||||
placeholder="door,chest,floor,wall",
|
||||
on_change=lambda text: print(f"Text: {text}")
|
||||
)
|
||||
|
||||
# Register with focus manager
|
||||
name_input._focus_manager = focus_mgr
|
||||
focus_mgr.register(name_input)
|
||||
|
||||
|
||||
# Create demo scene
|
||||
import mcrfpy
|
||||
|
||||
mcrfpy.createScene("text_example")
|
||||
mcrfpy.setScene("text_example")
|
||||
|
||||
ui = mcrfpy.sceneUI("text_example")
|
||||
# Add to scene
|
||||
#ui.append(name_input) # don't do this, only the internal Frame class can go into the UI; have to manage derived objects "carefully" (McRogueFace alpha anti-feature)
|
||||
name_input.add_to_scene(ui)
|
||||
tags_input.add_to_scene(ui)
|
||||
|
||||
# Handle keyboard events
|
||||
def handle_keys(key, state):
|
||||
if not focus_mgr.handle_key(key, state):
|
||||
if key == "Tab" and state == "start":
|
||||
focus_mgr.focus_next()
|
||||
|
||||
# McRogueFace alpha anti-feature: only the active scene can be given a keypress callback
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
"""
|
||||
Text Input Widget System for McRogueFace
|
||||
A reusable module for text input fields with focus management
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
|
||||
class FocusManager:
|
||||
"""Manages focus across multiple widgets"""
|
||||
def __init__(self):
|
||||
self.widgets = []
|
||||
self.focused_widget = None
|
||||
self.focus_index = -1
|
||||
|
||||
def register(self, widget):
|
||||
"""Register a widget"""
|
||||
self.widgets.append(widget)
|
||||
if self.focused_widget is None:
|
||||
self.focus(widget)
|
||||
|
||||
def focus(self, widget):
|
||||
"""Set focus to widget"""
|
||||
if self.focused_widget:
|
||||
self.focused_widget.on_blur()
|
||||
|
||||
self.focused_widget = widget
|
||||
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
|
||||
|
||||
if widget:
|
||||
widget.on_focus()
|
||||
|
||||
def focus_next(self):
|
||||
"""Focus next widget"""
|
||||
if not self.widgets:
|
||||
return
|
||||
self.focus_index = (self.focus_index + 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def focus_prev(self):
|
||||
"""Focus previous widget"""
|
||||
if not self.widgets:
|
||||
return
|
||||
self.focus_index = (self.focus_index - 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Send key to focused widget"""
|
||||
if self.focused_widget:
|
||||
return self.focused_widget.handle_key(key)
|
||||
return False
|
||||
|
||||
|
||||
class TextInput:
|
||||
"""Text input field widget"""
|
||||
def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.label = label
|
||||
self.placeholder = placeholder
|
||||
self.on_change = on_change
|
||||
|
||||
# Text state
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
self.focused = False
|
||||
|
||||
# Visual elements
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create UI components"""
|
||||
# Background frame
|
||||
self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.height)
|
||||
self.frame.fill_color = (255, 255, 255, 255)
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
|
||||
# Label (above input)
|
||||
if self.label:
|
||||
self.label_text = mcrfpy.Caption(self.label, self.x, self.y - 20)
|
||||
self.label_text.fill_color = (255, 255, 255, 255)
|
||||
|
||||
# Text content
|
||||
self.text_display = mcrfpy.Caption("", self.x + 4, self.y + 4)
|
||||
self.text_display.fill_color = (0, 0, 0, 255)
|
||||
|
||||
# Placeholder text
|
||||
if self.placeholder:
|
||||
self.placeholder_text = mcrfpy.Caption(self.placeholder, self.x + 4, self.y + 4)
|
||||
self.placeholder_text.fill_color = (180, 180, 180, 255)
|
||||
|
||||
# Cursor
|
||||
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, self.height - 8)
|
||||
self.cursor.fill_color = (0, 0, 0, 255)
|
||||
self.cursor.visible = False
|
||||
|
||||
# Click handler
|
||||
self.frame.click = self._on_click
|
||||
|
||||
def _on_click(self, x, y, button, state):
|
||||
"""Handle mouse clicks"""
|
||||
print(self, x, y, button, state)
|
||||
if button == "left" and hasattr(self, '_focus_manager'):
|
||||
self._focus_manager.focus(self)
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when focused"""
|
||||
self.focused = True
|
||||
self.frame.outline_color = (0, 120, 255, 255)
|
||||
self.frame.outline = 3
|
||||
self.cursor.visible = True
|
||||
self._update_display()
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when focus lost"""
|
||||
self.focused = False
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
self.cursor.visible = False
|
||||
self._update_display()
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Process keyboard input"""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
old_text = self.text
|
||||
handled = True
|
||||
|
||||
# Navigation and editing keys
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif key in ("Tab", "Return"):
|
||||
handled = False # Let parent handle
|
||||
elif len(key) == 1 and key.isprintable():
|
||||
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
else:
|
||||
handled = False
|
||||
|
||||
# Update if changed
|
||||
if old_text != self.text:
|
||||
self._update_display()
|
||||
if self.on_change:
|
||||
self.on_change(self.text)
|
||||
elif handled:
|
||||
self._update_cursor()
|
||||
|
||||
return handled
|
||||
|
||||
def _update_display(self):
|
||||
"""Update visual state"""
|
||||
# Show/hide placeholder
|
||||
if hasattr(self, 'placeholder_text'):
|
||||
self.placeholder_text.visible = (self.text == "" and not self.focused)
|
||||
|
||||
# Update text
|
||||
self.text_display.text = self.text
|
||||
self._update_cursor()
|
||||
|
||||
def _update_cursor(self):
|
||||
"""Update cursor position"""
|
||||
if self.focused:
|
||||
# Estimate position (10 pixels per character)
|
||||
self.cursor.x = self.x + 4 + (self.cursor_pos * 10)
|
||||
|
||||
def set_text(self, text):
|
||||
"""Set text programmatically"""
|
||||
self.text = text
|
||||
self.cursor_pos = len(text)
|
||||
self._update_display()
|
||||
|
||||
def get_text(self):
|
||||
"""Get current text"""
|
||||
return self.text
|
||||
|
||||
def add_to_scene(self, scene):
|
||||
"""Add all components to scene"""
|
||||
scene.append(self.frame)
|
||||
if hasattr(self, 'label_text'):
|
||||
scene.append(self.label_text)
|
||||
if hasattr(self, 'placeholder_text'):
|
||||
scene.append(self.placeholder_text)
|
||||
scene.append(self.text_display)
|
||||
scene.append(self.cursor)
|
|
@ -0,0 +1,265 @@
|
|||
"""
|
||||
Improved Text Input Widget System for McRogueFace
|
||||
Uses proper parent-child frame structure and handles keyboard input correctly
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
|
||||
class FocusManager:
|
||||
"""Manages focus across multiple widgets"""
|
||||
def __init__(self):
|
||||
self.widgets = []
|
||||
self.focused_widget = None
|
||||
self.focus_index = -1
|
||||
# Global keyboard state
|
||||
self.shift_pressed = False
|
||||
self.caps_lock = False
|
||||
|
||||
def register(self, widget):
|
||||
"""Register a widget"""
|
||||
self.widgets.append(widget)
|
||||
if self.focused_widget is None:
|
||||
self.focus(widget)
|
||||
|
||||
def focus(self, widget):
|
||||
"""Set focus to widget"""
|
||||
if self.focused_widget:
|
||||
self.focused_widget.on_blur()
|
||||
|
||||
self.focused_widget = widget
|
||||
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
|
||||
|
||||
if widget:
|
||||
widget.on_focus()
|
||||
|
||||
def focus_next(self):
|
||||
"""Focus next widget"""
|
||||
if not self.widgets:
|
||||
return
|
||||
self.focus_index = (self.focus_index + 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def focus_prev(self):
|
||||
"""Focus previous widget"""
|
||||
if not self.widgets:
|
||||
return
|
||||
self.focus_index = (self.focus_index - 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def handle_key(self, key, state):
|
||||
"""Send key to focused widget"""
|
||||
# Track shift state
|
||||
if key == "LShift" or key == "RShift":
|
||||
self.shift_pressed = True
|
||||
return True
|
||||
elif key == "start": # Key release for shift
|
||||
self.shift_pressed = False
|
||||
return True
|
||||
elif key == "CapsLock":
|
||||
self.caps_lock = not self.caps_lock
|
||||
return True
|
||||
|
||||
if self.focused_widget:
|
||||
return self.focused_widget.handle_key(key, self.shift_pressed, self.caps_lock)
|
||||
return False
|
||||
|
||||
|
||||
class TextInput:
|
||||
"""Text input field widget with proper parent-child structure"""
|
||||
def __init__(self, x, y, width, height=24, label="", placeholder="", on_change=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.label = label
|
||||
self.placeholder = placeholder
|
||||
self.on_change = on_change
|
||||
|
||||
# Text state
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
self.focused = False
|
||||
|
||||
# Create the widget structure
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create UI components with proper parent-child structure"""
|
||||
# Parent frame that contains everything
|
||||
self.parent_frame = mcrfpy.Frame(self.x, self.y - (20 if self.label else 0),
|
||||
self.width, self.height + (20 if self.label else 0))
|
||||
self.parent_frame.fill_color = (0, 0, 0, 0) # Transparent parent
|
||||
|
||||
# Input frame (relative to parent)
|
||||
self.frame = mcrfpy.Frame(0, 20 if self.label else 0, self.width, self.height)
|
||||
self.frame.fill_color = (255, 255, 255, 255)
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
|
||||
# Label (relative to parent)
|
||||
if self.label:
|
||||
self.label_text = mcrfpy.Caption(self.label, 0, 0)
|
||||
self.label_text.fill_color = (255, 255, 255, 255)
|
||||
self.parent_frame.children.append(self.label_text)
|
||||
|
||||
# Text content (relative to input frame)
|
||||
self.text_display = mcrfpy.Caption("", 4, 4)
|
||||
self.text_display.fill_color = (0, 0, 0, 255)
|
||||
|
||||
# Placeholder text (relative to input frame)
|
||||
if self.placeholder:
|
||||
self.placeholder_text = mcrfpy.Caption(self.placeholder, 4, 4)
|
||||
self.placeholder_text.fill_color = (180, 180, 180, 255)
|
||||
self.frame.children.append(self.placeholder_text)
|
||||
|
||||
# Cursor (relative to input frame)
|
||||
# Experiment: replacing cursor frame with an inline text character
|
||||
#self.cursor = mcrfpy.Frame(4, 4, 2, self.height - 8)
|
||||
#self.cursor.fill_color = (0, 0, 0, 255)
|
||||
#self.cursor.visible = False
|
||||
|
||||
# Add children to input frame
|
||||
self.frame.children.append(self.text_display)
|
||||
#self.frame.children.append(self.cursor)
|
||||
|
||||
# Add input frame to parent
|
||||
self.parent_frame.children.append(self.frame)
|
||||
|
||||
# Click handler on the input frame
|
||||
self.frame.click = self._on_click
|
||||
|
||||
def _on_click(self, x, y, button, state):
|
||||
"""Handle mouse clicks"""
|
||||
print(f"{x=} {y=} {button=} {state=}")
|
||||
if button == "left" and hasattr(self, '_focus_manager'):
|
||||
self._focus_manager.focus(self)
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when focused"""
|
||||
self.focused = True
|
||||
self.frame.outline_color = (0, 120, 255, 255)
|
||||
self.frame.outline = 3
|
||||
#self.cursor.visible = True
|
||||
self._update_display()
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when focus lost"""
|
||||
self.focused = False
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
#self.cursor.visible = False
|
||||
self._update_display()
|
||||
|
||||
def handle_key(self, key, shift_pressed, caps_lock):
|
||||
"""Process keyboard input with shift state"""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
old_text = self.text
|
||||
handled = True
|
||||
|
||||
# Special key mappings for shifted characters
|
||||
shift_map = {
|
||||
"1": "!", "2": "@", "3": "#", "4": "$", "5": "%",
|
||||
"6": "^", "7": "&", "8": "*", "9": "(", "0": ")",
|
||||
"-": "_", "=": "+", "[": "{", "]": "}", "\\": "|",
|
||||
";": ":", "'": '"', ",": "<", ".": ">", "/": "?",
|
||||
"`": "~"
|
||||
}
|
||||
|
||||
# Navigation and editing keys
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif key == "Space":
|
||||
self._insert_at_cursor(" ")
|
||||
elif key in ("Tab", "Return"):
|
||||
handled = False # Let parent handle
|
||||
# Handle number keys with "Num" prefix
|
||||
elif key.startswith("Num") and len(key) == 4:
|
||||
num = key[3] # Get the digit after "Num"
|
||||
if shift_pressed and num in shift_map:
|
||||
self._insert_at_cursor(shift_map[num])
|
||||
else:
|
||||
self._insert_at_cursor(num)
|
||||
# Handle single character keys
|
||||
elif len(key) == 1:
|
||||
char = key
|
||||
# Apply shift transformations
|
||||
if shift_pressed:
|
||||
if char in shift_map:
|
||||
char = shift_map[char]
|
||||
elif char.isalpha():
|
||||
char = char.upper()
|
||||
else:
|
||||
# Apply caps lock for letters
|
||||
if char.isalpha():
|
||||
if caps_lock:
|
||||
char = char.upper()
|
||||
else:
|
||||
char = char.lower()
|
||||
self._insert_at_cursor(char)
|
||||
else:
|
||||
# Unhandled key - print for debugging
|
||||
print(f"[TextInput] Unhandled key: '{key}' (shift={shift_pressed}, caps={caps_lock})")
|
||||
handled = False
|
||||
|
||||
# Update if changed
|
||||
if old_text != self.text:
|
||||
self._update_display()
|
||||
if self.on_change:
|
||||
self.on_change(self.text)
|
||||
elif handled:
|
||||
self._update_cursor()
|
||||
|
||||
return handled
|
||||
|
||||
def _insert_at_cursor(self, char):
|
||||
"""Insert a character at the cursor position"""
|
||||
self.text = self.text[:self.cursor_pos] + char + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
|
||||
def _update_display(self):
|
||||
"""Update visual state"""
|
||||
# Show/hide placeholder
|
||||
if hasattr(self, 'placeholder_text'):
|
||||
self.placeholder_text.visible = (self.text == "" and not self.focused)
|
||||
|
||||
# Update text
|
||||
self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:]
|
||||
self._update_cursor()
|
||||
|
||||
def _update_cursor(self):
|
||||
"""Update cursor position"""
|
||||
if self.focused:
|
||||
# Estimate position (10 pixels per character)
|
||||
#self.cursor.x = 4 + (self.cursor_pos * 10)
|
||||
self.text_display.text = self.text[:self.cursor_pos] + "|" + self.text[self.cursor_pos:]
|
||||
pass
|
||||
|
||||
def set_text(self, text):
|
||||
"""Set text programmatically"""
|
||||
self.text = text
|
||||
self.cursor_pos = len(text)
|
||||
self._update_display()
|
||||
|
||||
def get_text(self):
|
||||
"""Get current text"""
|
||||
return self.text
|
||||
|
||||
def add_to_scene(self, scene):
|
||||
"""Add only the parent frame to scene"""
|
||||
scene.append(self.parent_frame)
|
|
@ -1,165 +1,208 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Animation System Demo - Shows all animation capabilities"""
|
||||
"""
|
||||
Animation Demo: Grid Center & Entity Movement
|
||||
=============================================
|
||||
|
||||
Demonstrates:
|
||||
- Animated grid centering following entity
|
||||
- Smooth entity movement along paths
|
||||
- Perspective shifts with zoom transitions
|
||||
- Field of view updates
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import math
|
||||
import sys
|
||||
|
||||
# Create main scene
|
||||
mcrfpy.createScene("animation_demo")
|
||||
ui = mcrfpy.sceneUI("animation_demo")
|
||||
mcrfpy.setScene("animation_demo")
|
||||
# Setup scene
|
||||
mcrfpy.createScene("anim_demo")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font)
|
||||
title.size = 24
|
||||
title.fill_color = (255, 255, 255)
|
||||
# Note: centered property doesn't exist for Caption
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Simple map
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
cell = grid.at(x, y)
|
||||
# Create walls around edges and some obstacles
|
||||
if x == 0 or x == 29 or y == 0 or y == 19:
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = mcrfpy.Color(40, 30, 30)
|
||||
elif (x == 10 and 5 <= y <= 15) or (y == 10 and 5 <= x <= 25):
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = mcrfpy.Color(60, 40, 40)
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(80, 80, 100)
|
||||
|
||||
# Create entities
|
||||
player = mcrfpy.Entity(5, 5, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
|
||||
enemy = mcrfpy.Entity(25, 15, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Update visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# UI setup
|
||||
ui = mcrfpy.sceneUI("anim_demo")
|
||||
ui.append(grid)
|
||||
grid.position = (100, 100)
|
||||
grid.size = (600, 400)
|
||||
|
||||
title = mcrfpy.Caption("Animation Demo - Grid Center & Entity Movement", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# 1. Position Animation Demo
|
||||
pos_frame = mcrfpy.Frame(50, 100, 80, 80)
|
||||
pos_frame.fill_color = (255, 100, 100)
|
||||
pos_frame.outline = 2
|
||||
ui.append(pos_frame)
|
||||
status = mcrfpy.Caption("Press 1: Move Player | 2: Move Enemy | 3: Perspective Shift | Q: Quit", 100, 50)
|
||||
status.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status)
|
||||
|
||||
pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font)
|
||||
pos_label.fill_color = (200, 200, 200)
|
||||
ui.append(pos_label)
|
||||
|
||||
# 2. Size Animation Demo
|
||||
size_frame = mcrfpy.Frame(200, 100, 50, 50)
|
||||
size_frame.fill_color = (100, 255, 100)
|
||||
size_frame.outline = 2
|
||||
ui.append(size_frame)
|
||||
|
||||
size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font)
|
||||
size_label.fill_color = (200, 200, 200)
|
||||
ui.append(size_label)
|
||||
|
||||
# 3. Color Animation Demo
|
||||
color_frame = mcrfpy.Frame(350, 100, 80, 80)
|
||||
color_frame.fill_color = (255, 0, 0)
|
||||
ui.append(color_frame)
|
||||
|
||||
color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font)
|
||||
color_label.fill_color = (200, 200, 200)
|
||||
ui.append(color_label)
|
||||
|
||||
# 4. Easing Functions Demo
|
||||
easing_y = 250
|
||||
easing_frames = []
|
||||
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"]
|
||||
|
||||
for i, easing in enumerate(easings):
|
||||
x = 50 + i * 120
|
||||
|
||||
frame = mcrfpy.Frame(x, easing_y, 20, 20)
|
||||
frame.fill_color = (100, 150, 255)
|
||||
ui.append(frame)
|
||||
easing_frames.append((frame, easing))
|
||||
|
||||
label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font)
|
||||
label.size = 12
|
||||
label.fill_color = (200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# 5. Complex Animation Demo
|
||||
complex_frame = mcrfpy.Frame(300, 350, 100, 100)
|
||||
complex_frame.fill_color = (128, 128, 255)
|
||||
complex_frame.outline = 3
|
||||
ui.append(complex_frame)
|
||||
|
||||
complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font)
|
||||
complex_label.fill_color = (200, 200, 200)
|
||||
ui.append(complex_label)
|
||||
|
||||
# Start animations
|
||||
def start_animations(runtime):
|
||||
# 1. Position animation - back and forth
|
||||
x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut")
|
||||
x_anim.start(pos_frame)
|
||||
|
||||
# 2. Size animation - pulsing
|
||||
w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut")
|
||||
h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut")
|
||||
w_anim.start(size_frame)
|
||||
h_anim.start(size_frame)
|
||||
|
||||
# 3. Color animation - rainbow cycle
|
||||
color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear")
|
||||
color_anim.start(color_frame)
|
||||
|
||||
# 4. Easing demos - all move up with different easings
|
||||
for frame, easing in easing_frames:
|
||||
y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing)
|
||||
y_anim.start(frame)
|
||||
|
||||
# 5. Complex animation - multiple properties
|
||||
cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut")
|
||||
cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut")
|
||||
cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic")
|
||||
ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic")
|
||||
outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear")
|
||||
|
||||
cx_anim.start(complex_frame)
|
||||
cy_anim.start(complex_frame)
|
||||
cw_anim.start(complex_frame)
|
||||
ch_anim.start(complex_frame)
|
||||
outline_anim.start(complex_frame)
|
||||
|
||||
# Individual color component animations
|
||||
r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut")
|
||||
g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut")
|
||||
b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut")
|
||||
|
||||
r_anim.start(complex_frame)
|
||||
g_anim.start(complex_frame)
|
||||
b_anim.start(complex_frame)
|
||||
|
||||
print("All animations started!")
|
||||
|
||||
# Reverse some animations
|
||||
def reverse_animations(runtime):
|
||||
# Position back
|
||||
x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut")
|
||||
x_anim.start(pos_frame)
|
||||
|
||||
# Size back
|
||||
w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut")
|
||||
h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut")
|
||||
w_anim.start(size_frame)
|
||||
h_anim.start(size_frame)
|
||||
|
||||
# Color cycle continues
|
||||
color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear")
|
||||
color_anim.start(color_frame)
|
||||
|
||||
# Easing frames back down
|
||||
for frame, easing in easing_frames:
|
||||
y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Continue color cycle
|
||||
def cycle_colors(runtime):
|
||||
color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear")
|
||||
color_anim.start(color_frame)
|
||||
|
||||
# Info text
|
||||
info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font)
|
||||
info.fill_color = (255, 255, 200)
|
||||
# Note: centered property doesn't exist for Caption
|
||||
info = mcrfpy.Caption("Perspective: Player", 500, 70)
|
||||
info.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(info)
|
||||
|
||||
# Schedule animations
|
||||
mcrfpy.setTimer("start", start_animations, 500)
|
||||
mcrfpy.setTimer("reverse", reverse_animations, 4000)
|
||||
mcrfpy.setTimer("cycle", cycle_colors, 2500)
|
||||
# Movement functions
|
||||
def move_player_demo():
|
||||
"""Demo player movement with camera follow"""
|
||||
# Calculate path to a destination
|
||||
path = player.path_to(20, 10)
|
||||
if not path:
|
||||
status.text = "No path available!"
|
||||
return
|
||||
|
||||
# Exit handler
|
||||
def on_key(key):
|
||||
if key == "Escape":
|
||||
mcrfpy.exit()
|
||||
status.text = f"Moving player along {len(path)} steps..."
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
# Animate along path
|
||||
for i, (x, y) in enumerate(path[:5]): # First 5 steps
|
||||
delay = i * 500 # 500ms between steps
|
||||
|
||||
print("Animation demo started! Press Escape to exit.")
|
||||
# Schedule movement
|
||||
def move_step(dt, px=x, py=y):
|
||||
# Animate entity position
|
||||
anim_x = mcrfpy.Animation("x", float(px), 0.4, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(py), 0.4, "easeInOut")
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
# Update visibility
|
||||
player.update_visibility()
|
||||
|
||||
# Animate camera to follow
|
||||
center_x = px * 16 # Assuming 16x16 tiles
|
||||
center_y = py * 16
|
||||
cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut")
|
||||
cam_anim.start(grid)
|
||||
|
||||
mcrfpy.setTimer(f"player_move_{i}", move_step, delay)
|
||||
|
||||
def move_enemy_demo():
|
||||
"""Demo enemy movement"""
|
||||
# Calculate path
|
||||
path = enemy.path_to(10, 5)
|
||||
if not path:
|
||||
status.text = "Enemy has no path!"
|
||||
return
|
||||
|
||||
status.text = f"Moving enemy along {len(path)} steps..."
|
||||
|
||||
# Animate along path
|
||||
for i, (x, y) in enumerate(path[:5]): # First 5 steps
|
||||
delay = i * 500
|
||||
|
||||
def move_step(dt, ex=x, ey=y):
|
||||
anim_x = mcrfpy.Animation("x", float(ex), 0.4, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(ey), 0.4, "easeInOut")
|
||||
anim_x.start(enemy)
|
||||
anim_y.start(enemy)
|
||||
enemy.update_visibility()
|
||||
|
||||
# If following enemy, update camera
|
||||
if grid.perspective == 1:
|
||||
center_x = ex * 16
|
||||
center_y = ey * 16
|
||||
cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut")
|
||||
cam_anim.start(grid)
|
||||
|
||||
mcrfpy.setTimer(f"enemy_move_{i}", move_step, delay)
|
||||
|
||||
def perspective_shift_demo():
|
||||
"""Demo dramatic perspective shift"""
|
||||
status.text = "Perspective shift in progress..."
|
||||
|
||||
# Phase 1: Zoom out
|
||||
zoom_out = mcrfpy.Animation("zoom", 0.5, 1.5, "easeInExpo")
|
||||
zoom_out.start(grid)
|
||||
|
||||
# Phase 2: Switch perspective at peak
|
||||
def switch_perspective(dt):
|
||||
if grid.perspective == 0:
|
||||
grid.perspective = 1
|
||||
info.text = "Perspective: Enemy"
|
||||
info.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
target = enemy
|
||||
else:
|
||||
grid.perspective = 0
|
||||
info.text = "Perspective: Player"
|
||||
info.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
target = player
|
||||
|
||||
# Update camera to new target
|
||||
center_x = target.x * 16
|
||||
center_y = target.y * 16
|
||||
cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.5, "linear")
|
||||
cam_anim.start(grid)
|
||||
|
||||
mcrfpy.setTimer("switch_persp", switch_perspective, 1600)
|
||||
|
||||
# Phase 3: Zoom back in
|
||||
def zoom_in(dt):
|
||||
zoom_in_anim = mcrfpy.Animation("zoom", 1.0, 1.5, "easeOutExpo")
|
||||
zoom_in_anim.start(grid)
|
||||
status.text = "Perspective shift complete!"
|
||||
|
||||
mcrfpy.setTimer("zoom_in", zoom_in, 2100)
|
||||
|
||||
# Input handler
|
||||
def handle_input(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "q":
|
||||
print("Exiting demo...")
|
||||
sys.exit(0)
|
||||
elif key == "1":
|
||||
move_player_demo()
|
||||
elif key == "2":
|
||||
move_enemy_demo()
|
||||
elif key == "3":
|
||||
perspective_shift_demo()
|
||||
|
||||
# Set scene
|
||||
mcrfpy.setScene("anim_demo")
|
||||
mcrfpy.keypressScene(handle_input)
|
||||
|
||||
# Initial setup
|
||||
grid.perspective = 0
|
||||
grid.zoom = 1.0
|
||||
|
||||
# Center on player initially
|
||||
center_x = player.x * 16
|
||||
center_y = player.y * 16
|
||||
initial_cam = mcrfpy.Animation("center", (center_x, center_y), 0.5, "easeOut")
|
||||
initial_cam.start(grid)
|
||||
|
||||
print("Animation Demo Started!")
|
||||
print("======================")
|
||||
print("Press 1: Animate player movement with camera follow")
|
||||
print("Press 2: Animate enemy movement")
|
||||
print("Press 3: Dramatic perspective shift with zoom")
|
||||
print("Press Q: Quit")
|
||||
print()
|
||||
print("Watch how the grid center smoothly follows entities")
|
||||
print("and how perspective shifts create cinematic effects!")
|
|
@ -0,0 +1,235 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
A* vs Dijkstra Visual Comparison
|
||||
=================================
|
||||
|
||||
Shows the difference between A* (single target) and Dijkstra (multi-target).
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20)
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80)
|
||||
ASTAR_COLOR = mcrfpy.Color(0, 255, 0) # Green for A*
|
||||
DIJKSTRA_COLOR = mcrfpy.Color(0, 150, 255) # Blue for Dijkstra
|
||||
START_COLOR = mcrfpy.Color(255, 100, 100) # Red for start
|
||||
END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
mode = "ASTAR"
|
||||
start_pos = (5, 10)
|
||||
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
|
||||
|
||||
def create_map():
|
||||
"""Create a map with obstacles to show pathfinding differences"""
|
||||
global grid
|
||||
|
||||
mcrfpy.createScene("pathfinding_comparison")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Initialize all as floor
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Create obstacles that make A* and Dijkstra differ
|
||||
obstacles = [
|
||||
# Vertical wall with gaps
|
||||
[(15, y) for y in range(3, 17) if y not in [8, 12]],
|
||||
# Horizontal walls
|
||||
[(x, 5) for x in range(10, 20)],
|
||||
[(x, 15) for x in range(10, 20)],
|
||||
# Maze-like structure
|
||||
[(x, 10) for x in range(20, 25)],
|
||||
[(25, y) for y in range(5, 15)],
|
||||
]
|
||||
|
||||
for obstacle_group in obstacles:
|
||||
for x, y in obstacle_group:
|
||||
grid.at(x, y).walkable = False
|
||||
grid.at(x, y).color = WALL_COLOR
|
||||
|
||||
# Mark start and end
|
||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
||||
|
||||
def clear_paths():
|
||||
"""Clear path highlighting"""
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
# Restore start and end colors
|
||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
||||
|
||||
def show_astar():
|
||||
"""Show A* path"""
|
||||
clear_paths()
|
||||
|
||||
# Compute A* path
|
||||
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
|
||||
# Color the path
|
||||
for i, (x, y) in enumerate(path):
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = ASTAR_COLOR
|
||||
|
||||
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
|
||||
status_text.fill_color = ASTAR_COLOR
|
||||
|
||||
def show_dijkstra():
|
||||
"""Show Dijkstra exploration"""
|
||||
clear_paths()
|
||||
|
||||
# Compute Dijkstra from start
|
||||
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
||||
|
||||
# Color cells by distance (showing exploration)
|
||||
max_dist = 40.0
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
if grid.at(x, y).walkable:
|
||||
dist = grid.get_dijkstra_distance(x, y)
|
||||
if dist is not None and dist < max_dist:
|
||||
# Color based on distance
|
||||
intensity = int(255 * (1 - dist / max_dist))
|
||||
grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity)
|
||||
|
||||
# Get the actual path
|
||||
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
||||
|
||||
# Highlight the actual path more brightly
|
||||
for x, y in path:
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = DIJKSTRA_COLOR
|
||||
|
||||
# Restore start and end
|
||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
||||
|
||||
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
|
||||
status_text.fill_color = DIJKSTRA_COLOR
|
||||
|
||||
def show_both():
|
||||
"""Show both paths overlaid"""
|
||||
clear_paths()
|
||||
|
||||
# Get both paths
|
||||
astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
||||
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
||||
|
||||
print(astar_path, dijkstra_path)
|
||||
|
||||
# Color Dijkstra path first (blue)
|
||||
for x, y in dijkstra_path:
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = DIJKSTRA_COLOR
|
||||
|
||||
# Then A* path (green) - will overwrite shared cells
|
||||
for x, y in astar_path:
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = ASTAR_COLOR
|
||||
|
||||
# Mark differences
|
||||
different_cells = []
|
||||
for cell in dijkstra_path:
|
||||
if cell not in astar_path:
|
||||
different_cells.append(cell)
|
||||
|
||||
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
|
||||
if different_cells:
|
||||
info_text.text = f"Paths differ at {len(different_cells)} cells"
|
||||
else:
|
||||
info_text.text = "Paths are identical"
|
||||
|
||||
def handle_keypress(key_str, state):
|
||||
"""Handle keyboard input"""
|
||||
global mode
|
||||
if state == "end": return
|
||||
print(key_str)
|
||||
if key_str == "Esc" or key_str == "Q":
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif key_str == "A" or key_str == "1":
|
||||
mode = "ASTAR"
|
||||
show_astar()
|
||||
elif key_str == "D" or key_str == "2":
|
||||
mode = "DIJKSTRA"
|
||||
show_dijkstra()
|
||||
elif key_str == "B" or key_str == "3":
|
||||
mode = "BOTH"
|
||||
show_both()
|
||||
elif key_str == "Space":
|
||||
# Refresh current mode
|
||||
if mode == "ASTAR":
|
||||
show_astar()
|
||||
elif mode == "DIJKSTRA":
|
||||
show_dijkstra()
|
||||
else:
|
||||
show_both()
|
||||
|
||||
# Create the demo
|
||||
print("A* vs Dijkstra Pathfinding Comparison")
|
||||
print("=====================================")
|
||||
print("Controls:")
|
||||
print(" A or 1 - Show A* path (green)")
|
||||
print(" D or 2 - Show Dijkstra (blue gradient)")
|
||||
print(" B or 3 - Show both paths")
|
||||
print(" Q/ESC - Quit")
|
||||
print()
|
||||
print("A* is optimized for single-target pathfinding")
|
||||
print("Dijkstra explores in all directions (good for multiple targets)")
|
||||
|
||||
create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("pathfinding_comparison")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (600, 400) # 30*20, 20*20
|
||||
grid.position = (100, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status
|
||||
status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add info
|
||||
info_text = mcrfpy.Caption("", 100, 520)
|
||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info_text)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540)
|
||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Set scene and input
|
||||
mcrfpy.setScene("pathfinding_comparison")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Show initial A* path
|
||||
show_astar()
|
||||
|
||||
print("\nDemo ready!")
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug the astar_vs_dijkstra demo issue"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Same setup as the demo
|
||||
start_pos = (5, 10)
|
||||
end_pos = (25, 10)
|
||||
|
||||
print("Debugging A* vs Dijkstra demo...")
|
||||
print(f"Start: {start_pos}, End: {end_pos}")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("debug")
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
|
||||
# Initialize all as floor
|
||||
print("\nInitializing 30x20 grid...")
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Test path before obstacles
|
||||
print("\nTest 1: Path with no obstacles")
|
||||
path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}")
|
||||
print(f" Length: {len(path1)}")
|
||||
|
||||
# Add obstacles from the demo
|
||||
obstacles = [
|
||||
# Vertical wall with gaps
|
||||
[(15, y) for y in range(3, 17) if y not in [8, 12]],
|
||||
# Horizontal walls
|
||||
[(x, 5) for x in range(10, 20)],
|
||||
[(x, 15) for x in range(10, 20)],
|
||||
# Maze-like structure
|
||||
[(x, 10) for x in range(20, 25)],
|
||||
[(25, y) for y in range(5, 15)],
|
||||
]
|
||||
|
||||
print("\nAdding obstacles...")
|
||||
wall_count = 0
|
||||
for obstacle_group in obstacles:
|
||||
for x, y in obstacle_group:
|
||||
grid.at(x, y).walkable = False
|
||||
wall_count += 1
|
||||
if wall_count <= 5:
|
||||
print(f" Wall at ({x}, {y})")
|
||||
|
||||
print(f" Total walls added: {wall_count}")
|
||||
|
||||
# Check specific cells
|
||||
print(f"\nChecking key positions:")
|
||||
print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}")
|
||||
print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}")
|
||||
|
||||
# Check if path is blocked
|
||||
print(f"\nChecking horizontal line at y=10:")
|
||||
blocked_x = []
|
||||
for x in range(30):
|
||||
if not grid.at(x, 10).walkable:
|
||||
blocked_x.append(x)
|
||||
|
||||
print(f" Blocked x positions: {blocked_x}")
|
||||
|
||||
# Test path with obstacles
|
||||
print("\nTest 2: Path with obstacles")
|
||||
path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
print(f" Path: {path2}")
|
||||
print(f" Length: {len(path2)}")
|
||||
|
||||
# Check if there's any path at all
|
||||
if not path2:
|
||||
print("\n No path found! Checking why...")
|
||||
|
||||
# Check if we can reach the vertical wall gap
|
||||
print("\n Testing path to wall gap at (15, 8):")
|
||||
path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8)
|
||||
print(f" Path to gap: {path_to_gap}")
|
||||
|
||||
# Check from gap to end
|
||||
print("\n Testing path from gap (15, 8) to end:")
|
||||
path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1])
|
||||
print(f" Path from gap: {path_from_gap}")
|
||||
|
||||
# Check walls more carefully
|
||||
print("\nDetailed wall analysis:")
|
||||
print(" Walls at x=25 (blocking end?):")
|
||||
for y in range(5, 15):
|
||||
print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}")
|
||||
|
||||
def timer_cb(dt):
|
||||
sys.exit(0)
|
||||
|
||||
ui = mcrfpy.sceneUI("debug")
|
||||
ui.append(grid)
|
||||
mcrfpy.setScene("debug")
|
||||
mcrfpy.setTimer("exit", timer_cb, 100)
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug empty paths issue"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
print("Debugging empty paths...")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("debug")
|
||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||
|
||||
# Initialize grid - all walkable
|
||||
print("\nInitializing grid...")
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Test simple path
|
||||
print("\nTest 1: Simple path from (0,0) to (5,5)")
|
||||
path = grid.compute_astar_path(0, 0, 5, 5)
|
||||
print(f" A* path: {path}")
|
||||
print(f" Path length: {len(path)}")
|
||||
|
||||
# Test with Dijkstra
|
||||
print("\nTest 2: Same path with Dijkstra")
|
||||
grid.compute_dijkstra(0, 0)
|
||||
dpath = grid.get_dijkstra_path(5, 5)
|
||||
print(f" Dijkstra path: {dpath}")
|
||||
print(f" Path length: {len(dpath)}")
|
||||
|
||||
# Check if grid is properly initialized
|
||||
print("\nTest 3: Checking grid cells")
|
||||
for y in range(3):
|
||||
for x in range(3):
|
||||
cell = grid.at(x, y)
|
||||
print(f" Cell ({x},{y}): walkable={cell.walkable}")
|
||||
|
||||
# Test with walls
|
||||
print("\nTest 4: Path with wall")
|
||||
grid.at(2, 2).walkable = False
|
||||
grid.at(3, 2).walkable = False
|
||||
grid.at(4, 2).walkable = False
|
||||
print(" Added wall at y=2, x=2,3,4")
|
||||
|
||||
path2 = grid.compute_astar_path(0, 0, 5, 5)
|
||||
print(f" A* path with wall: {path2}")
|
||||
print(f" Path length: {len(path2)}")
|
||||
|
||||
# Test invalid paths
|
||||
print("\nTest 5: Path to blocked cell")
|
||||
grid.at(9, 9).walkable = False
|
||||
path3 = grid.compute_astar_path(0, 0, 9, 9)
|
||||
print(f" Path to blocked cell: {path3}")
|
||||
|
||||
# Check TCOD map sync
|
||||
print("\nTest 6: Verify TCOD map is synced")
|
||||
# Try to force a sync
|
||||
print(" Checking if syncTCODMap exists...")
|
||||
if hasattr(grid, 'sync_tcod_map'):
|
||||
print(" Calling sync_tcod_map()")
|
||||
grid.sync_tcod_map()
|
||||
else:
|
||||
print(" No sync_tcod_map method found")
|
||||
|
||||
# Try path again
|
||||
print("\nTest 7: Path after potential sync")
|
||||
path4 = grid.compute_astar_path(0, 0, 5, 5)
|
||||
print(f" A* path: {path4}")
|
||||
|
||||
def timer_cb(dt):
|
||||
sys.exit(0)
|
||||
|
||||
# Quick UI setup
|
||||
ui = mcrfpy.sceneUI("debug")
|
||||
ui.append(grid)
|
||||
mcrfpy.setScene("debug")
|
||||
mcrfpy.setTimer("exit", timer_cb, 100)
|
||||
|
||||
print("\nStarting timer...")
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug visibility crash"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
print("Debug visibility...")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("debug")
|
||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||
|
||||
# Initialize grid
|
||||
print("Initializing grid...")
|
||||
for y in range(5):
|
||||
for x in range(5):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
# Create entity
|
||||
print("Creating entity...")
|
||||
entity = mcrfpy.Entity(2, 2)
|
||||
entity.sprite_index = 64
|
||||
grid.entities.append(entity)
|
||||
print(f"Entity at ({entity.x}, {entity.y})")
|
||||
|
||||
# Check gridstate
|
||||
print(f"\nGridstate length: {len(entity.gridstate)}")
|
||||
print(f"Expected: {5 * 5}")
|
||||
|
||||
# Try to access gridstate
|
||||
print("\nChecking gridstate access...")
|
||||
try:
|
||||
if len(entity.gridstate) > 0:
|
||||
state = entity.gridstate[0]
|
||||
print(f"First state: visible={state.visible}, discovered={state.discovered}")
|
||||
except Exception as e:
|
||||
print(f"Error accessing gridstate: {e}")
|
||||
|
||||
# Try update_visibility
|
||||
print("\nTrying update_visibility...")
|
||||
try:
|
||||
entity.update_visibility()
|
||||
print("update_visibility succeeded")
|
||||
except Exception as e:
|
||||
print(f"Error in update_visibility: {e}")
|
||||
|
||||
# Try perspective
|
||||
print("\nTesting perspective...")
|
||||
print(f"Initial perspective: {grid.perspective}")
|
||||
try:
|
||||
grid.perspective = 0
|
||||
print(f"Set perspective to 0: {grid.perspective}")
|
||||
except Exception as e:
|
||||
print(f"Error setting perspective: {e}")
|
||||
|
||||
print("\nTest complete")
|
||||
sys.exit(0)
|
|
@ -0,0 +1,234 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dijkstra Demo - Shows ALL Path Combinations (Including Invalid)
|
||||
===============================================================
|
||||
|
||||
Cycles through every possible entity pair to demonstrate both
|
||||
valid paths and properly handled invalid paths (empty lists).
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green
|
||||
START_COLOR = mcrfpy.Color(255, 100, 100) # Light red
|
||||
END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
|
||||
NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
current_combo_index = 0
|
||||
all_combinations = [] # All possible pairs
|
||||
current_path = []
|
||||
|
||||
def create_map():
|
||||
"""Create the map with entities"""
|
||||
global grid, entities, all_combinations
|
||||
|
||||
mcrfpy.createScene("dijkstra_all")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout - Entity 1 is intentionally trapped!
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2 - Entity 1 TRAPPED at (10,2)
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4 - Entity 2 at (6,4)
|
||||
"E.W...........", # Row 5 - Entity 3 at (0,5)
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
print("Map Analysis:")
|
||||
print("=============")
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
print(f"Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Generate ALL combinations (including invalid ones)
|
||||
all_combinations = []
|
||||
for i in range(len(entities)):
|
||||
for j in range(len(entities)):
|
||||
if i != j: # Skip self-paths
|
||||
all_combinations.append((i, j))
|
||||
|
||||
print(f"\nTotal path combinations to test: {len(all_combinations)}")
|
||||
|
||||
def clear_path_colors():
|
||||
"""Reset all floor tiles to original color"""
|
||||
global current_path
|
||||
|
||||
for y in range(grid.grid_y):
|
||||
for x in range(grid.grid_x):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
current_path = []
|
||||
|
||||
def show_combination(index):
|
||||
"""Show a specific path combination (valid or invalid)"""
|
||||
global current_combo_index, current_path
|
||||
|
||||
current_combo_index = index % len(all_combinations)
|
||||
from_idx, to_idx = all_combinations[current_combo_index]
|
||||
|
||||
# Clear previous path
|
||||
clear_path_colors()
|
||||
|
||||
# Get entities
|
||||
e_from = entities[from_idx]
|
||||
e_to = entities[to_idx]
|
||||
|
||||
# Calculate path
|
||||
path = e_from.path_to(int(e_to.x), int(e_to.y))
|
||||
current_path = path if path else []
|
||||
|
||||
# Always color start and end positions
|
||||
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
|
||||
grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR
|
||||
|
||||
# Color the path if it exists
|
||||
if path:
|
||||
# Color intermediate steps
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps"
|
||||
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid
|
||||
|
||||
# Show path steps
|
||||
path_display = []
|
||||
for i, (x, y) in enumerate(path[:5]):
|
||||
path_display.append(f"({x},{y})")
|
||||
if len(path) > 5:
|
||||
path_display.append("...")
|
||||
path_text.text = "Path: " + " → ".join(path_display)
|
||||
else:
|
||||
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!"
|
||||
status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid
|
||||
path_text.text = "Path: [] (No valid path exists)"
|
||||
|
||||
# Update info
|
||||
info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})"
|
||||
|
||||
def handle_keypress(key_str, state):
|
||||
"""Handle keyboard input"""
|
||||
global current_combo_index
|
||||
if state == "end": return
|
||||
|
||||
if key_str == "Esc" or key_str == "Q":
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif key_str == "Space" or key_str == "N":
|
||||
show_combination(current_combo_index + 1)
|
||||
elif key_str == "P":
|
||||
show_combination(current_combo_index - 1)
|
||||
elif key_str == "R":
|
||||
show_combination(current_combo_index)
|
||||
elif key_str in "123456":
|
||||
combo_num = int(key_str) - 1 # 0-based index
|
||||
if combo_num < len(all_combinations):
|
||||
show_combination(combo_num)
|
||||
|
||||
# Create the demo
|
||||
print("Dijkstra All Paths Demo")
|
||||
print("=======================")
|
||||
print("Shows ALL path combinations including invalid ones")
|
||||
print("Entity 1 is trapped - paths to/from it will be empty!")
|
||||
print()
|
||||
|
||||
create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_all")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (560, 400)
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status (will change color based on validity)
|
||||
status_text = mcrfpy.Caption("Ready", 120, 60)
|
||||
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add info
|
||||
info_text = mcrfpy.Caption("", 120, 80)
|
||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info_text)
|
||||
|
||||
# Add path display
|
||||
path_text = mcrfpy.Caption("Path: None", 120, 520)
|
||||
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(path_text)
|
||||
|
||||
# Add controls
|
||||
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560)
|
||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend)
|
||||
|
||||
# Expected results info
|
||||
expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580)
|
||||
expected.fill_color = mcrfpy.Color(255, 150, 150)
|
||||
ui.append(expected)
|
||||
|
||||
# Set scene first, then set up input handler
|
||||
mcrfpy.setScene("dijkstra_all")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Show first combination
|
||||
show_combination(0)
|
||||
|
||||
print("\nDemo ready!")
|
||||
print("Expected results:")
|
||||
print(" Path 1: Entity 1→2 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 2: Entity 1→3 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 3: Entity 2→1 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 4: Entity 2→3 = Valid path")
|
||||
print(" Path 5: Entity 3→1 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 6: Entity 3→2 = Valid path")
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dijkstra Demo - Cycles Through Different Path Combinations
|
||||
==========================================================
|
||||
|
||||
Shows paths between different entity pairs, skipping impossible paths.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green
|
||||
START_COLOR = mcrfpy.Color(255, 100, 100) # Light red
|
||||
END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
current_path_index = 0
|
||||
path_combinations = []
|
||||
current_path = []
|
||||
|
||||
def create_map():
|
||||
"""Create the map with entities"""
|
||||
global grid, entities
|
||||
|
||||
mcrfpy.createScene("dijkstra_cycle")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2 - Entity 1 at (10,2) is TRAPPED!
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4 - Entity 2 at (6,4)
|
||||
"E.W...........", # Row 5 - Entity 3 at (0,5)
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
print("Entities created:")
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
print(f" Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Check which entity is trapped
|
||||
print("\nChecking accessibility:")
|
||||
for i, e in enumerate(entities):
|
||||
# Try to path to each other entity
|
||||
can_reach = []
|
||||
for j, other in enumerate(entities):
|
||||
if i != j:
|
||||
path = e.path_to(int(other.x), int(other.y))
|
||||
if path:
|
||||
can_reach.append(j+1)
|
||||
|
||||
if not can_reach:
|
||||
print(f" Entity {i+1} at ({int(e.x)}, {int(e.y)}) is TRAPPED!")
|
||||
else:
|
||||
print(f" Entity {i+1} can reach entities: {can_reach}")
|
||||
|
||||
# Generate valid path combinations (excluding trapped entity)
|
||||
global path_combinations
|
||||
path_combinations = []
|
||||
|
||||
# Only paths between entities 2 and 3 (indices 1 and 2) will work
|
||||
# since entity 1 (index 0) is trapped
|
||||
if len(entities) >= 3:
|
||||
# Entity 2 to Entity 3
|
||||
path = entities[1].path_to(int(entities[2].x), int(entities[2].y))
|
||||
if path:
|
||||
path_combinations.append((1, 2, path))
|
||||
|
||||
# Entity 3 to Entity 2
|
||||
path = entities[2].path_to(int(entities[1].x), int(entities[1].y))
|
||||
if path:
|
||||
path_combinations.append((2, 1, path))
|
||||
|
||||
print(f"\nFound {len(path_combinations)} valid paths")
|
||||
|
||||
def clear_path_colors():
|
||||
"""Reset all floor tiles to original color"""
|
||||
global current_path
|
||||
|
||||
for y in range(grid.grid_y):
|
||||
for x in range(grid.grid_x):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
current_path = []
|
||||
|
||||
def show_path(index):
|
||||
"""Show a specific path combination"""
|
||||
global current_path_index, current_path
|
||||
|
||||
if not path_combinations:
|
||||
status_text.text = "No valid paths available (Entity 1 is trapped!)"
|
||||
return
|
||||
|
||||
current_path_index = index % len(path_combinations)
|
||||
from_idx, to_idx, path = path_combinations[current_path_index]
|
||||
|
||||
# Clear previous path
|
||||
clear_path_colors()
|
||||
|
||||
# Get entities
|
||||
e_from = entities[from_idx]
|
||||
e_to = entities[to_idx]
|
||||
|
||||
# Color the path
|
||||
current_path = path
|
||||
if path:
|
||||
# Color start and end
|
||||
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
|
||||
grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR
|
||||
|
||||
# Color intermediate steps
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
# Update status
|
||||
status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)"
|
||||
|
||||
# Update path display
|
||||
path_display = []
|
||||
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
|
||||
path_display.append(f"({x},{y})")
|
||||
if len(path) > 5:
|
||||
path_display.append("...")
|
||||
path_text.text = "Path: " + " → ".join(path_display) if path_display else "Path: None"
|
||||
|
||||
def handle_keypress(key_str, state):
|
||||
"""Handle keyboard input"""
|
||||
global current_path_index
|
||||
if state == "end": return
|
||||
if key_str == "Esc":
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif key_str == "N" or key_str == "Space":
|
||||
show_path(current_path_index + 1)
|
||||
elif key_str == "P":
|
||||
show_path(current_path_index - 1)
|
||||
elif key_str == "R":
|
||||
show_path(current_path_index)
|
||||
|
||||
# Create the demo
|
||||
print("Dijkstra Path Cycling Demo")
|
||||
print("==========================")
|
||||
print("Note: Entity 1 is trapped by walls!")
|
||||
print()
|
||||
|
||||
create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_cycle")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (560, 400)
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status
|
||||
status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60)
|
||||
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add path display
|
||||
path_text = mcrfpy.Caption("Path: None", 120, 520)
|
||||
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(path_text)
|
||||
|
||||
# Add controls
|
||||
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560)
|
||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend)
|
||||
|
||||
# Show first valid path
|
||||
mcrfpy.setScene("dijkstra_cycle")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Display initial path
|
||||
if path_combinations:
|
||||
show_path(0)
|
||||
else:
|
||||
status_text.text = "No valid paths! Entity 1 is trapped!"
|
||||
|
||||
print("\nDemo ready!")
|
||||
print("Controls:")
|
||||
print(" SPACE or N - Next path")
|
||||
print(" P - Previous path")
|
||||
print(" R - Refresh current path")
|
||||
print(" Q - Quit")
|
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug version of Dijkstra pathfinding to diagnose visualization issues
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
|
||||
PATH_COLOR = mcrfpy.Color(200, 250, 220)
|
||||
ENTITY_COLORS = [
|
||||
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
|
||||
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
|
||||
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
|
||||
]
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
first_point = None
|
||||
second_point = None
|
||||
|
||||
def create_simple_map():
|
||||
"""Create a simple test map"""
|
||||
global grid, entities
|
||||
|
||||
mcrfpy.createScene("dijkstra_debug")
|
||||
|
||||
# Small grid for easy debugging
|
||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
print("Initializing 10x10 grid...")
|
||||
|
||||
# Initialize all as floor
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Add a simple wall
|
||||
print("Adding walls at:")
|
||||
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
|
||||
for x, y in walls:
|
||||
print(f" Wall at ({x}, {y})")
|
||||
grid.at(x, y).walkable = False
|
||||
grid.at(x, y).color = WALL_COLOR
|
||||
|
||||
# Create 3 entities
|
||||
entity_positions = [(2, 5), (8, 5), (5, 8)]
|
||||
entities = []
|
||||
|
||||
print("\nCreating entities at:")
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
print(f" Entity {i+1} at ({x}, {y})")
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
return grid
|
||||
|
||||
def test_path_highlighting():
|
||||
"""Test path highlighting with debug output"""
|
||||
print("\n" + "="*50)
|
||||
print("Testing path highlighting...")
|
||||
|
||||
# Select first two entities
|
||||
e1 = entities[0]
|
||||
e2 = entities[1]
|
||||
|
||||
print(f"\nEntity 1 position: ({e1.x}, {e1.y})")
|
||||
print(f"Entity 2 position: ({e2.x}, {e2.y})")
|
||||
|
||||
# Use entity.path_to()
|
||||
print("\nCalling entity.path_to()...")
|
||||
path = e1.path_to(int(e2.x), int(e2.y))
|
||||
|
||||
print(f"Path returned: {path}")
|
||||
print(f"Path length: {len(path)} steps")
|
||||
|
||||
if path:
|
||||
print("\nHighlighting path cells:")
|
||||
for i, (x, y) in enumerate(path):
|
||||
print(f" Step {i}: ({x}, {y})")
|
||||
# Get current color for debugging
|
||||
cell = grid.at(x, y)
|
||||
old_color = (cell.color.r, cell.color.g, cell.color.b)
|
||||
|
||||
# Set new color
|
||||
cell.color = PATH_COLOR
|
||||
new_color = (cell.color.r, cell.color.g, cell.color.b)
|
||||
|
||||
print(f" Color changed from {old_color} to {new_color}")
|
||||
print(f" Walkable: {cell.walkable}")
|
||||
|
||||
# Also test grid's Dijkstra methods
|
||||
print("\n" + "-"*30)
|
||||
print("Testing grid Dijkstra methods...")
|
||||
|
||||
grid.compute_dijkstra(int(e1.x), int(e1.y))
|
||||
grid_path = grid.get_dijkstra_path(int(e2.x), int(e2.y))
|
||||
distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y))
|
||||
|
||||
print(f"Grid path: {grid_path}")
|
||||
print(f"Grid distance: {distance}")
|
||||
|
||||
# Verify colors were set
|
||||
print("\nVerifying cell colors after highlighting:")
|
||||
for x, y in path[:3]: # Check first 3 cells
|
||||
cell = grid.at(x, y)
|
||||
color = (cell.color.r, cell.color.g, cell.color.b)
|
||||
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
|
||||
match = color == expected
|
||||
print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}")
|
||||
|
||||
def handle_keypress(scene_name, keycode):
|
||||
"""Simple keypress handler"""
|
||||
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting debug...")
|
||||
sys.exit(0)
|
||||
elif keycode == 32: # Space
|
||||
print("\nSpace pressed - retesting path highlighting...")
|
||||
test_path_highlighting()
|
||||
|
||||
# Create the map
|
||||
print("Dijkstra Debug Test")
|
||||
print("===================")
|
||||
grid = create_simple_map()
|
||||
|
||||
# Initial path test
|
||||
test_path_highlighting()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_debug")
|
||||
ui.append(grid)
|
||||
|
||||
# Position and scale
|
||||
grid.position = (50, 50)
|
||||
grid.size = (400, 400) # 10*40
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add debug info
|
||||
info = mcrfpy.Caption("Check console for debug output", 50, 470)
|
||||
info.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info)
|
||||
|
||||
# Set up scene
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
mcrfpy.setScene("dijkstra_debug")
|
||||
|
||||
print("\nScene ready. The path should be highlighted in cyan.")
|
||||
print("If you don't see the path, there may be a rendering issue.")
|
||||
print("Press SPACE to retest, Q to quit.")
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Working Dijkstra Demo with Clear Visual Feedback
|
||||
================================================
|
||||
|
||||
This demo shows pathfinding with high-contrast colors.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths
|
||||
START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start
|
||||
END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end
|
||||
|
||||
print("Dijkstra Demo - High Contrast")
|
||||
print("==============================")
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("dijkstra_demo")
|
||||
|
||||
# Create grid with exact layout from user
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4
|
||||
"E.W...........", # Row 5
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
print(f"Map created: {grid.grid_x}x{grid.grid_y}")
|
||||
print(f"Entity positions: {entity_positions}")
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
print(f"Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Highlight a path immediately
|
||||
if len(entities) >= 2:
|
||||
e1, e2 = entities[0], entities[1]
|
||||
print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...")
|
||||
|
||||
path = e1.path_to(int(e2.x), int(e2.y))
|
||||
print(f"Path found: {path}")
|
||||
print(f"Path length: {len(path)} steps")
|
||||
|
||||
if path:
|
||||
print("\nHighlighting path in bright green...")
|
||||
# Color start and end specially
|
||||
grid.at(int(e1.x), int(e1.y)).color = START_COLOR
|
||||
grid.at(int(e2.x), int(e2.y)).color = END_COLOR
|
||||
|
||||
# Color the path
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1: # Skip start and end
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
print(f" Colored ({x}, {y}) green")
|
||||
|
||||
# Keypress handler
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif keycode == 32: # Space
|
||||
print("\nRefreshing path colors...")
|
||||
# Re-color the path to ensure it's visible
|
||||
if len(entities) >= 2 and path:
|
||||
for x, y in path[1:-1]:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_demo")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale grid
|
||||
grid.size = (560, 400) # 14*40, 10*40
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520)
|
||||
legend1.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Entity info
|
||||
info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60)
|
||||
info.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(info)
|
||||
|
||||
# Set up input
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
mcrfpy.setScene("dijkstra_demo")
|
||||
|
||||
print("\nDemo ready! The path should be clearly visible in bright green.")
|
||||
print("Red = Start, Blue = End, Green = Path")
|
||||
print("Press SPACE to refresh colors if needed.")
|
|
@ -17,10 +17,10 @@ The path between selected entities is automatically highlighted.
|
|||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
# Colors - using more distinct values
|
||||
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
|
||||
PATH_COLOR = mcrfpy.Color(200, 250, 220)
|
||||
FLOOR_COLOR = mcrfpy.Color(100, 100, 120) # Darker floor for better contrast
|
||||
PATH_COLOR = mcrfpy.Color(50, 255, 50) # Bright green for path
|
||||
ENTITY_COLORS = [
|
||||
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
|
||||
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive Visibility Demo
|
||||
==========================
|
||||
|
||||
Controls:
|
||||
- WASD: Move the player (green @)
|
||||
- Arrow keys: Move enemy (red E)
|
||||
- Tab: Cycle perspective (Omniscient → Player → Enemy → Omniscient)
|
||||
- Space: Update visibility for current entity
|
||||
- R: Reset positions
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("visibility_demo")
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
|
||||
|
||||
# Initialize grid - all walkable and transparent
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(100, 100, 120) # Floor color
|
||||
|
||||
# Create walls
|
||||
walls = [
|
||||
# Central cross
|
||||
[(15, y) for y in range(8, 12)],
|
||||
[(x, 10) for x in range(13, 18)],
|
||||
|
||||
# Rooms
|
||||
# Top-left room
|
||||
[(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)],
|
||||
[(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)],
|
||||
|
||||
# Top-right room
|
||||
[(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)],
|
||||
[(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)],
|
||||
|
||||
# Bottom-left room
|
||||
[(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)],
|
||||
[(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)],
|
||||
|
||||
# Bottom-right room
|
||||
[(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)],
|
||||
[(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)],
|
||||
]
|
||||
|
||||
for wall_group in walls:
|
||||
for x, y in wall_group:
|
||||
if 0 <= x < 30 and 0 <= y < 20:
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = mcrfpy.Color(40, 20, 20) # Wall color
|
||||
|
||||
# Create entities
|
||||
player = mcrfpy.Entity(5, 10, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
enemy = mcrfpy.Entity(25, 10, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Update initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Global state
|
||||
current_perspective = -1
|
||||
perspective_names = ["Omniscient", "Player", "Enemy"]
|
||||
|
||||
# UI Setup
|
||||
ui = mcrfpy.sceneUI("visibility_demo")
|
||||
ui.append(grid)
|
||||
grid.position = (50, 100)
|
||||
grid.size = (900, 600) # 30*30, 20*30
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Info displays
|
||||
perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50)
|
||||
perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(perspective_label)
|
||||
|
||||
controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50)
|
||||
player_info.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(player_info)
|
||||
|
||||
enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70)
|
||||
enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
ui.append(enemy_info)
|
||||
|
||||
# Helper functions
|
||||
def move_entity(entity, dx, dy):
|
||||
"""Move entity if target is walkable"""
|
||||
new_x = int(entity.x + dx)
|
||||
new_y = int(entity.y + dy)
|
||||
|
||||
if 0 <= new_x < 30 and 0 <= new_y < 20:
|
||||
cell = grid.at(new_x, new_y)
|
||||
if cell.walkable:
|
||||
entity.x = new_x
|
||||
entity.y = new_y
|
||||
entity.update_visibility()
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_info():
|
||||
"""Update info displays"""
|
||||
player_info.text = f"Player: ({int(player.x)}, {int(player.y)})"
|
||||
enemy_info.text = f"Enemy: ({int(enemy.x)}, {int(enemy.y)})"
|
||||
|
||||
def cycle_perspective():
|
||||
"""Cycle through perspectives"""
|
||||
global current_perspective
|
||||
|
||||
# Cycle: -1 → 0 → 1 → -1
|
||||
current_perspective = (current_perspective + 2) % 3 - 1
|
||||
|
||||
grid.perspective = current_perspective
|
||||
name = perspective_names[current_perspective + 1]
|
||||
perspective_label.text = f"Perspective: {name}"
|
||||
|
||||
# Key handlers
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "end": return
|
||||
key = key.lower()
|
||||
# Player movement (WASD)
|
||||
if key == "w":
|
||||
move_entity(player, 0, -1)
|
||||
elif key == "s":
|
||||
move_entity(player, 0, 1)
|
||||
elif key == "a":
|
||||
move_entity(player, -1, 0)
|
||||
elif key == "d":
|
||||
move_entity(player, 1, 0)
|
||||
|
||||
# Enemy movement (Arrows)
|
||||
elif key == "up":
|
||||
move_entity(enemy, 0, -1)
|
||||
elif key == "down":
|
||||
move_entity(enemy, 0, 1)
|
||||
elif key == "left":
|
||||
move_entity(enemy, -1, 0)
|
||||
elif key == "right":
|
||||
move_entity(enemy, 1, 0)
|
||||
|
||||
# Tab to cycle perspective
|
||||
elif key == "tab":
|
||||
cycle_perspective()
|
||||
|
||||
# Space to update visibility
|
||||
elif key == "space":
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
print("Updated visibility for both entities")
|
||||
|
||||
# R to reset
|
||||
elif key == "r":
|
||||
player.x, player.y = 5, 10
|
||||
enemy.x, enemy.y = 25, 10
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
update_info()
|
||||
print("Reset positions")
|
||||
|
||||
# Q to quit
|
||||
elif key == "q":
|
||||
print("Exiting...")
|
||||
sys.exit(0)
|
||||
|
||||
update_info()
|
||||
|
||||
# Set scene first
|
||||
mcrfpy.setScene("visibility_demo")
|
||||
|
||||
# Register key handler (operates on current scene)
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
print("Interactive Visibility Demo")
|
||||
print("===========================")
|
||||
print("WASD: Move player (green @)")
|
||||
print("Arrows: Move enemy (red E)")
|
||||
print("Tab: Cycle perspective")
|
||||
print("Space: Update visibility")
|
||||
print("R: Reset positions")
|
||||
print("Q: Quit")
|
||||
print("\nCurrent perspective: Omniscient (shows all)")
|
||||
print("Try moving entities and switching perspectives!")
|
|
@ -0,0 +1,375 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Path & Vision Sizzle Reel (Fixed)
|
||||
=================================
|
||||
|
||||
Fixed version with proper animation chaining to prevent glitches.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
class PathAnimator:
|
||||
"""Handles step-by-step animation with proper completion tracking"""
|
||||
|
||||
def __init__(self, entity, name="animator"):
|
||||
self.entity = entity
|
||||
self.name = name
|
||||
self.path = []
|
||||
self.current_index = 0
|
||||
self.step_duration = 0.4
|
||||
self.animating = False
|
||||
self.on_step = None
|
||||
self.on_complete = None
|
||||
|
||||
def set_path(self, path):
|
||||
"""Set the path to animate along"""
|
||||
self.path = path
|
||||
self.current_index = 0
|
||||
|
||||
def start(self):
|
||||
"""Start animating"""
|
||||
if not self.path:
|
||||
return
|
||||
|
||||
self.animating = True
|
||||
self.current_index = 0
|
||||
self._move_to_next()
|
||||
|
||||
def stop(self):
|
||||
"""Stop animating"""
|
||||
self.animating = False
|
||||
mcrfpy.delTimer(f"{self.name}_check")
|
||||
|
||||
def _move_to_next(self):
|
||||
"""Move to next position in path"""
|
||||
if not self.animating or self.current_index >= len(self.path):
|
||||
self.animating = False
|
||||
if self.on_complete:
|
||||
self.on_complete()
|
||||
return
|
||||
|
||||
# Get next position
|
||||
x, y = self.path[self.current_index]
|
||||
|
||||
# Create animations
|
||||
anim_x = mcrfpy.Animation("x", float(x), self.step_duration, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(y), self.step_duration, "easeInOut")
|
||||
|
||||
anim_x.start(self.entity)
|
||||
anim_y.start(self.entity)
|
||||
|
||||
# Update visibility
|
||||
self.entity.update_visibility()
|
||||
|
||||
# Callback for each step
|
||||
if self.on_step:
|
||||
self.on_step(self.current_index, x, y)
|
||||
|
||||
# Schedule next move
|
||||
delay = int(self.step_duration * 1000) + 50 # Add small buffer
|
||||
mcrfpy.setTimer(f"{self.name}_next", self._handle_next, delay)
|
||||
|
||||
def _handle_next(self, dt):
|
||||
"""Timer callback to move to next position"""
|
||||
self.current_index += 1
|
||||
mcrfpy.delTimer(f"{self.name}_next")
|
||||
self._move_to_next()
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemy = None
|
||||
player_animator = None
|
||||
enemy_animator = None
|
||||
demo_phase = 0
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo environment"""
|
||||
global grid, player, enemy
|
||||
|
||||
mcrfpy.createScene("fixed_demo")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Simple dungeon layout
|
||||
map_layout = [
|
||||
"##############################",
|
||||
"#......#########.....#########",
|
||||
"#......#########.....#########",
|
||||
"#......#.........#...#########",
|
||||
"#......#.........#...#########",
|
||||
"####.###.........#.###########",
|
||||
"####.............#.###########",
|
||||
"####.............#.###########",
|
||||
"####.###.........#.###########",
|
||||
"#......#.........#...#########",
|
||||
"#......#.........#...#########",
|
||||
"#......#########.#...........#",
|
||||
"#......#########.#...........#",
|
||||
"#......#########.#...........#",
|
||||
"#......#########.#############",
|
||||
"####.###########.............#",
|
||||
"####.........................#",
|
||||
"####.###########.............#",
|
||||
"#......#########.............#",
|
||||
"##############################",
|
||||
]
|
||||
|
||||
# Build map
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
if char == '#':
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = mcrfpy.Color(40, 30, 30)
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(80, 80, 100)
|
||||
|
||||
# Create entities
|
||||
player = mcrfpy.Entity(3, 3, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
|
||||
enemy = mcrfpy.Entity(26, 16, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Set initial perspective
|
||||
grid.perspective = 0
|
||||
|
||||
def setup_ui():
|
||||
"""Create UI elements"""
|
||||
ui = mcrfpy.sceneUI("fixed_demo")
|
||||
ui.append(grid)
|
||||
|
||||
grid.position = (50, 80)
|
||||
grid.size = (700, 500)
|
||||
|
||||
title = mcrfpy.Caption("Path & Vision Demo (Fixed)", 300, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
global status_text, perspective_text
|
||||
status_text = mcrfpy.Caption("Initializing...", 50, 50)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(perspective_text)
|
||||
|
||||
controls = mcrfpy.Caption("Space: Start/Pause | R: Restart | Q: Quit", 250, 600)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
def update_camera_smooth(target, duration=0.3):
|
||||
"""Smoothly move camera to entity"""
|
||||
center_x = target.x * 23 # Approximate pixel size
|
||||
center_y = target.y * 23
|
||||
|
||||
cam_anim = mcrfpy.Animation("center", (center_x, center_y), duration, "easeOut")
|
||||
cam_anim.start(grid)
|
||||
|
||||
def start_demo():
|
||||
"""Start the demo sequence"""
|
||||
global demo_phase, player_animator, enemy_animator
|
||||
|
||||
demo_phase = 1
|
||||
status_text.text = "Phase 1: Player movement with camera follow"
|
||||
|
||||
# Player path
|
||||
player_path = [
|
||||
(3, 3), (3, 6), (4, 6), (7, 6), (7, 8),
|
||||
(10, 8), (13, 8), (16, 8), (16, 10),
|
||||
(16, 13), (16, 16), (20, 16), (24, 16)
|
||||
]
|
||||
|
||||
# Setup player animator
|
||||
player_animator = PathAnimator(player, "player")
|
||||
player_animator.set_path(player_path)
|
||||
player_animator.step_duration = 0.5
|
||||
|
||||
def on_player_step(index, x, y):
|
||||
"""Called for each player step"""
|
||||
status_text.text = f"Player step {index+1}/{len(player_path)}"
|
||||
if grid.perspective == 0:
|
||||
update_camera_smooth(player, 0.4)
|
||||
|
||||
def on_player_complete():
|
||||
"""Called when player path is complete"""
|
||||
start_phase_2()
|
||||
|
||||
player_animator.on_step = on_player_step
|
||||
player_animator.on_complete = on_player_complete
|
||||
player_animator.start()
|
||||
|
||||
def start_phase_2():
|
||||
"""Start enemy movement phase"""
|
||||
global demo_phase
|
||||
|
||||
demo_phase = 2
|
||||
status_text.text = "Phase 2: Enemy movement (may enter player's view)"
|
||||
|
||||
# Enemy path
|
||||
enemy_path = [
|
||||
(26, 16), (22, 16), (18, 16), (16, 16),
|
||||
(16, 13), (16, 10), (16, 8), (13, 8),
|
||||
(10, 8), (7, 8), (7, 6), (4, 6)
|
||||
]
|
||||
|
||||
# Setup enemy animator
|
||||
enemy_animator.set_path(enemy_path)
|
||||
enemy_animator.step_duration = 0.4
|
||||
|
||||
def on_enemy_step(index, x, y):
|
||||
"""Check if enemy is visible to player"""
|
||||
if grid.perspective == 0:
|
||||
# Check if enemy is in player's view
|
||||
enemy_idx = int(y) * grid.grid_x + int(x)
|
||||
if enemy_idx < len(player.gridstate) and player.gridstate[enemy_idx].visible:
|
||||
status_text.text = "Enemy spotted in player's view!"
|
||||
|
||||
def on_enemy_complete():
|
||||
"""Start perspective transition"""
|
||||
start_phase_3()
|
||||
|
||||
enemy_animator.on_step = on_enemy_step
|
||||
enemy_animator.on_complete = on_enemy_complete
|
||||
enemy_animator.start()
|
||||
|
||||
def start_phase_3():
|
||||
"""Dramatic perspective shift"""
|
||||
global demo_phase
|
||||
|
||||
demo_phase = 3
|
||||
status_text.text = "Phase 3: Perspective shift..."
|
||||
|
||||
# Stop any ongoing animations
|
||||
player_animator.stop()
|
||||
enemy_animator.stop()
|
||||
|
||||
# Zoom out
|
||||
zoom_out = mcrfpy.Animation("zoom", 0.6, 2.0, "easeInExpo")
|
||||
zoom_out.start(grid)
|
||||
|
||||
# Schedule perspective switch
|
||||
mcrfpy.setTimer("switch_persp", switch_perspective, 2100)
|
||||
|
||||
def switch_perspective(dt):
|
||||
"""Switch to enemy perspective"""
|
||||
grid.perspective = 1
|
||||
perspective_text.text = "Perspective: Enemy"
|
||||
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
|
||||
# Update camera
|
||||
update_camera_smooth(enemy, 0.5)
|
||||
|
||||
# Zoom back in
|
||||
zoom_in = mcrfpy.Animation("zoom", 1.0, 2.0, "easeOutExpo")
|
||||
zoom_in.start(grid)
|
||||
|
||||
status_text.text = "Now following enemy perspective"
|
||||
|
||||
# Clean up timer
|
||||
mcrfpy.delTimer("switch_persp")
|
||||
|
||||
# Continue enemy movement after transition
|
||||
mcrfpy.setTimer("continue_enemy", continue_enemy_movement, 2500)
|
||||
|
||||
def continue_enemy_movement(dt):
|
||||
"""Continue enemy movement after perspective shift"""
|
||||
mcrfpy.delTimer("continue_enemy")
|
||||
|
||||
# Continue path
|
||||
enemy_path_2 = [
|
||||
(4, 6), (3, 6), (3, 3), (3, 2), (3, 1)
|
||||
]
|
||||
|
||||
enemy_animator.set_path(enemy_path_2)
|
||||
|
||||
def on_step(index, x, y):
|
||||
update_camera_smooth(enemy, 0.4)
|
||||
status_text.text = f"Following enemy: step {index+1}"
|
||||
|
||||
def on_complete():
|
||||
status_text.text = "Demo complete! Press R to restart"
|
||||
|
||||
enemy_animator.on_step = on_step
|
||||
enemy_animator.on_complete = on_complete
|
||||
enemy_animator.start()
|
||||
|
||||
# Control state
|
||||
running = False
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
global running
|
||||
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
key = key.lower()
|
||||
|
||||
if key == "q":
|
||||
sys.exit(0)
|
||||
elif key == "space":
|
||||
if not running:
|
||||
running = True
|
||||
start_demo()
|
||||
else:
|
||||
running = False
|
||||
player_animator.stop()
|
||||
enemy_animator.stop()
|
||||
status_text.text = "Paused"
|
||||
elif key == "r":
|
||||
# Reset everything
|
||||
player.x, player.y = 3, 3
|
||||
enemy.x, enemy.y = 26, 16
|
||||
grid.perspective = 0
|
||||
perspective_text.text = "Perspective: Player"
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
grid.zoom = 1.0
|
||||
update_camera_smooth(player, 0.5)
|
||||
|
||||
if running:
|
||||
player_animator.stop()
|
||||
enemy_animator.stop()
|
||||
running = False
|
||||
|
||||
status_text.text = "Reset - Press SPACE to start"
|
||||
|
||||
# Initialize
|
||||
create_scene()
|
||||
setup_ui()
|
||||
|
||||
# Setup animators
|
||||
player_animator = PathAnimator(player, "player")
|
||||
enemy_animator = PathAnimator(enemy, "enemy")
|
||||
|
||||
# Set scene
|
||||
mcrfpy.setScene("fixed_demo")
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Initial camera
|
||||
grid.zoom = 1.0
|
||||
update_camera_smooth(player, 0.5)
|
||||
|
||||
print("Path & Vision Demo (Fixed)")
|
||||
print("==========================")
|
||||
print("This version properly chains animations to prevent glitches.")
|
||||
print()
|
||||
print("The demo will:")
|
||||
print("1. Move player with camera following")
|
||||
print("2. Move enemy (may enter player's view)")
|
||||
print("3. Dramatic perspective shift to enemy")
|
||||
print("4. Continue following enemy")
|
||||
print()
|
||||
print("Press SPACE to start, Q to quit")
|
|
@ -0,0 +1,391 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Path & Vision Sizzle Reel
|
||||
=========================
|
||||
|
||||
A choreographed demo showing:
|
||||
- Smooth entity movement along paths
|
||||
- Camera following with grid center animation
|
||||
- Field of view updates as entities move
|
||||
- Dramatic perspective transitions with zoom effects
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(80, 80, 100)
|
||||
PATH_COLOR = mcrfpy.Color(120, 120, 180)
|
||||
DARK_FLOOR = mcrfpy.Color(40, 40, 50)
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemy = None
|
||||
sequence_step = 0
|
||||
player_path = []
|
||||
enemy_path = []
|
||||
player_path_index = 0
|
||||
enemy_path_index = 0
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo environment"""
|
||||
global grid, player, enemy
|
||||
|
||||
mcrfpy.createScene("path_vision_demo")
|
||||
|
||||
# Create larger grid for more dramatic movement
|
||||
grid = mcrfpy.Grid(grid_x=40, grid_y=25)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Map layout - interconnected rooms with corridors
|
||||
map_layout = [
|
||||
"########################################", # 0
|
||||
"#......##########......################", # 1
|
||||
"#......##########......################", # 2
|
||||
"#......##########......################", # 3
|
||||
"#......#.........#.....################", # 4
|
||||
"#......#.........#.....################", # 5
|
||||
"####.###.........####.#################", # 6
|
||||
"####.....................##############", # 7
|
||||
"####.....................##############", # 8
|
||||
"####.###.........####.#################", # 9
|
||||
"#......#.........#.....################", # 10
|
||||
"#......#.........#.....################", # 11
|
||||
"#......#.........#.....################", # 12
|
||||
"#......###.....###.....################", # 13
|
||||
"#......###.....###.....################", # 14
|
||||
"#......###.....###.....#########......#", # 15
|
||||
"#......###.....###.....#########......#", # 16
|
||||
"#......###.....###.....#########......#", # 17
|
||||
"#####.############.#############......#", # 18
|
||||
"#####...........................#.....#", # 19
|
||||
"#####...........................#.....#", # 20
|
||||
"#####.############.#############......#", # 21
|
||||
"#......###########.##########.........#", # 22
|
||||
"#......###########.##########.........#", # 23
|
||||
"########################################", # 24
|
||||
]
|
||||
|
||||
# Build the map
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
if char == '#':
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
# Create player in top-left room
|
||||
player = mcrfpy.Entity(3, 3, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
|
||||
# Create enemy in bottom-right area
|
||||
enemy = mcrfpy.Entity(35, 20, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Set initial perspective to player
|
||||
grid.perspective = 0
|
||||
|
||||
def setup_paths():
|
||||
"""Define the paths for entities"""
|
||||
global player_path, enemy_path
|
||||
|
||||
# Player path: Top-left room → corridor → middle room
|
||||
player_waypoints = [
|
||||
(3, 3), # Start
|
||||
(3, 8), # Move down
|
||||
(7, 8), # Enter corridor
|
||||
(16, 8), # Through corridor
|
||||
(16, 12), # Enter middle room
|
||||
(12, 12), # Move in room
|
||||
(12, 16), # Move down
|
||||
(16, 16), # Move right
|
||||
(16, 19), # Exit room
|
||||
(25, 19), # Move right
|
||||
(30, 19), # Continue
|
||||
(35, 19), # Near enemy start
|
||||
]
|
||||
|
||||
# Enemy path: Bottom-right → around → approach player area
|
||||
enemy_waypoints = [
|
||||
(35, 20), # Start
|
||||
(30, 20), # Move left
|
||||
(25, 20), # Continue
|
||||
(20, 20), # Continue
|
||||
(16, 20), # Corridor junction
|
||||
(16, 16), # Move up (might see player)
|
||||
(16, 12), # Continue up
|
||||
(16, 8), # Top corridor
|
||||
(10, 8), # Move left
|
||||
(7, 8), # Continue
|
||||
(3, 8), # Player's area
|
||||
(3, 12), # Move down
|
||||
]
|
||||
|
||||
# Calculate full paths using pathfinding
|
||||
player_path = []
|
||||
for i in range(len(player_waypoints) - 1):
|
||||
x1, y1 = player_waypoints[i]
|
||||
x2, y2 = player_waypoints[i + 1]
|
||||
|
||||
# Use grid's A* pathfinding
|
||||
segment = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
if segment:
|
||||
# Add segment (avoiding duplicates)
|
||||
if not player_path or segment[0] != player_path[-1]:
|
||||
player_path.extend(segment)
|
||||
else:
|
||||
player_path.extend(segment[1:])
|
||||
|
||||
enemy_path = []
|
||||
for i in range(len(enemy_waypoints) - 1):
|
||||
x1, y1 = enemy_waypoints[i]
|
||||
x2, y2 = enemy_waypoints[i + 1]
|
||||
|
||||
segment = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
if segment:
|
||||
if not enemy_path or segment[0] != enemy_path[-1]:
|
||||
enemy_path.extend(segment)
|
||||
else:
|
||||
enemy_path.extend(segment[1:])
|
||||
|
||||
print(f"Player path: {len(player_path)} steps")
|
||||
print(f"Enemy path: {len(enemy_path)} steps")
|
||||
|
||||
def setup_ui():
|
||||
"""Create UI elements"""
|
||||
ui = mcrfpy.sceneUI("path_vision_demo")
|
||||
ui.append(grid)
|
||||
|
||||
# Position and size grid
|
||||
grid.position = (50, 80)
|
||||
grid.size = (700, 500) # Adjust based on zoom
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Status
|
||||
global status_text, perspective_text
|
||||
status_text = mcrfpy.Caption("Starting demo...", 50, 50)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(perspective_text)
|
||||
|
||||
# Controls
|
||||
controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Animation control
|
||||
paused = False
|
||||
move_timer = 0
|
||||
zoom_transition = False
|
||||
|
||||
def move_entity_smooth(entity, target_x, target_y, duration=0.3):
|
||||
"""Smoothly animate entity to position"""
|
||||
# Create position animation
|
||||
anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut")
|
||||
|
||||
anim_x.start(entity)
|
||||
anim_y.start(entity)
|
||||
|
||||
def update_camera_smooth(center_x, center_y, duration=0.3):
|
||||
"""Smoothly move camera center"""
|
||||
# Convert grid coords to pixel coords (assuming 16x16 tiles)
|
||||
pixel_x = center_x * 16
|
||||
pixel_y = center_y * 16
|
||||
|
||||
anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut")
|
||||
anim.start(grid)
|
||||
|
||||
def start_perspective_transition():
|
||||
"""Begin the dramatic perspective shift"""
|
||||
global zoom_transition, sequence_step
|
||||
zoom_transition = True
|
||||
sequence_step = 100 # Special sequence number
|
||||
|
||||
status_text.text = "Perspective shift: Zooming out..."
|
||||
|
||||
# Zoom out with elastic easing
|
||||
zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo")
|
||||
zoom_out.start(grid)
|
||||
|
||||
# Schedule the perspective switch
|
||||
mcrfpy.setTimer("switch_perspective", switch_perspective, 2100)
|
||||
|
||||
def switch_perspective(dt):
|
||||
"""Switch perspective at the peak of zoom"""
|
||||
global sequence_step
|
||||
|
||||
# Switch to enemy perspective
|
||||
grid.perspective = 1
|
||||
perspective_text.text = "Perspective: Enemy"
|
||||
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
|
||||
status_text.text = "Perspective shift: Following enemy..."
|
||||
|
||||
# Update camera to enemy position
|
||||
update_camera_smooth(enemy.x, enemy.y, 0.1)
|
||||
|
||||
# Zoom back in
|
||||
zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo")
|
||||
zoom_in.start(grid)
|
||||
|
||||
# Resume sequence
|
||||
mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100)
|
||||
|
||||
# Cancel this timer
|
||||
mcrfpy.delTimer("switch_perspective")
|
||||
|
||||
def resume_enemy_sequence(dt):
|
||||
"""Resume following enemy after perspective shift"""
|
||||
global sequence_step, zoom_transition
|
||||
zoom_transition = False
|
||||
sequence_step = 101 # Continue with enemy movement
|
||||
mcrfpy.delTimer("resume_enemy")
|
||||
|
||||
def sequence_tick(dt):
|
||||
"""Main sequence controller"""
|
||||
global sequence_step, player_path_index, enemy_path_index, move_timer
|
||||
|
||||
if paused or zoom_transition:
|
||||
return
|
||||
|
||||
move_timer += dt
|
||||
if move_timer < 400: # Move every 400ms
|
||||
return
|
||||
move_timer = 0
|
||||
|
||||
if sequence_step < 50:
|
||||
# Phase 1: Follow player movement
|
||||
if player_path_index < len(player_path):
|
||||
x, y = player_path[player_path_index]
|
||||
move_entity_smooth(player, x, y)
|
||||
player.update_visibility()
|
||||
|
||||
# Camera follows player
|
||||
if grid.perspective == 0:
|
||||
update_camera_smooth(player.x, player.y)
|
||||
|
||||
player_path_index += 1
|
||||
status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}"
|
||||
|
||||
# Start enemy movement after player has moved a bit
|
||||
if player_path_index == 10:
|
||||
sequence_step = 1 # Enable enemy movement
|
||||
else:
|
||||
# Player reached destination, start perspective transition
|
||||
start_perspective_transition()
|
||||
|
||||
if sequence_step >= 1 and sequence_step < 50:
|
||||
# Phase 2: Enemy movement (concurrent with player)
|
||||
if enemy_path_index < len(enemy_path):
|
||||
x, y = enemy_path[enemy_path_index]
|
||||
move_entity_smooth(enemy, x, y)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Check if enemy is visible to player
|
||||
if grid.perspective == 0:
|
||||
enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x)
|
||||
if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible:
|
||||
status_text.text = "Enemy spotted!"
|
||||
|
||||
enemy_path_index += 1
|
||||
|
||||
elif sequence_step == 101:
|
||||
# Phase 3: Continue following enemy after perspective shift
|
||||
if enemy_path_index < len(enemy_path):
|
||||
x, y = enemy_path[enemy_path_index]
|
||||
move_entity_smooth(enemy, x, y)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Camera follows enemy
|
||||
update_camera_smooth(enemy.x, enemy.y)
|
||||
|
||||
enemy_path_index += 1
|
||||
status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}"
|
||||
else:
|
||||
status_text.text = "Demo complete! Press R to restart"
|
||||
sequence_step = 200 # Done
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
global paused, sequence_step, player_path_index, enemy_path_index, move_timer
|
||||
key = key.lower()
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "q":
|
||||
print("Exiting sizzle reel...")
|
||||
sys.exit(0)
|
||||
elif key == "space":
|
||||
paused = not paused
|
||||
status_text.text = "PAUSED" if paused else "Running..."
|
||||
elif key == "r":
|
||||
# Reset everything
|
||||
player.x, player.y = 3, 3
|
||||
enemy.x, enemy.y = 35, 20
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
grid.perspective = 0
|
||||
perspective_text.text = "Perspective: Player"
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
sequence_step = 0
|
||||
player_path_index = 0
|
||||
enemy_path_index = 0
|
||||
move_timer = 0
|
||||
update_camera_smooth(player.x, player.y, 0.5)
|
||||
|
||||
# Reset zoom
|
||||
zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut")
|
||||
zoom_reset.start(grid)
|
||||
|
||||
status_text.text = "Demo restarted!"
|
||||
|
||||
# Initialize everything
|
||||
print("Path & Vision Sizzle Reel")
|
||||
print("=========================")
|
||||
print("Demonstrating:")
|
||||
print("- Smooth entity movement along calculated paths")
|
||||
print("- Camera following with animated grid centering")
|
||||
print("- Field of view updates as entities move")
|
||||
print("- Dramatic perspective transitions with zoom effects")
|
||||
print()
|
||||
|
||||
create_scene()
|
||||
setup_paths()
|
||||
setup_ui()
|
||||
|
||||
# Set scene and input
|
||||
mcrfpy.setScene("path_vision_demo")
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Initial camera setup
|
||||
grid.zoom = 1.2
|
||||
update_camera_smooth(player.x, player.y, 0.1)
|
||||
|
||||
# Start the sequence
|
||||
mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms
|
||||
|
||||
print("Demo started!")
|
||||
print("- Player (@) will navigate through rooms")
|
||||
print("- Enemy (E) will move on a different path")
|
||||
print("- Watch for the dramatic perspective shift!")
|
||||
print()
|
||||
print("Controls: Space=Pause, R=Restart, Q=Quit")
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple interactive visibility test"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create scene and grid
|
||||
print("Creating scene...")
|
||||
mcrfpy.createScene("vis_test")
|
||||
|
||||
print("Creating grid...")
|
||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||
|
||||
# Initialize grid
|
||||
print("Initializing grid...")
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(100, 100, 120)
|
||||
|
||||
# Create entity
|
||||
print("Creating entity...")
|
||||
entity = mcrfpy.Entity(5, 5, grid=grid)
|
||||
entity.sprite_index = 64
|
||||
|
||||
print("Updating visibility...")
|
||||
entity.update_visibility()
|
||||
|
||||
# Set up UI
|
||||
print("Setting up UI...")
|
||||
ui = mcrfpy.sceneUI("vis_test")
|
||||
ui.append(grid)
|
||||
grid.position = (50, 50)
|
||||
grid.size = (300, 300)
|
||||
|
||||
# Test perspective
|
||||
print("Testing perspective...")
|
||||
grid.perspective = -1 # Omniscient
|
||||
print(f"Perspective set to: {grid.perspective}")
|
||||
|
||||
print("Setting scene...")
|
||||
mcrfpy.setScene("vis_test")
|
||||
|
||||
print("Ready!")
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple visibility test without entity append"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
print("Simple visibility test...")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("simple")
|
||||
print("Scene created")
|
||||
|
||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||
print("Grid created")
|
||||
|
||||
# Create entity without appending
|
||||
entity = mcrfpy.Entity(2, 2, grid=grid)
|
||||
print(f"Entity created at ({entity.x}, {entity.y})")
|
||||
|
||||
# Check if gridstate is initialized
|
||||
print(f"Gridstate length: {len(entity.gridstate)}")
|
||||
|
||||
# Try to access at method
|
||||
try:
|
||||
state = entity.at(0, 0)
|
||||
print(f"at(0,0) returned: {state}")
|
||||
print(f"visible: {state.visible}, discovered: {state.discovered}")
|
||||
except Exception as e:
|
||||
print(f"Error in at(): {e}")
|
||||
|
||||
# Try update_visibility
|
||||
try:
|
||||
entity.update_visibility()
|
||||
print("update_visibility() succeeded")
|
||||
except Exception as e:
|
||||
print(f"Error in update_visibility(): {e}")
|
||||
|
||||
print("Test complete")
|
||||
sys.exit(0)
|
Loading…
Reference in New Issue