Compare commits
2 commits
4be2502a10
...
c23da11d7d
| Author | SHA1 | Date | |
|---|---|---|---|
| c23da11d7d | |||
| 5e45ab015c |
11 changed files with 439 additions and 51 deletions
|
|
@ -202,6 +202,16 @@ Scene* GameEngine::getScene(const std::string& name) {
|
||||||
auto it = scenes.find(name);
|
auto it = scenes.find(name);
|
||||||
return (it != scenes.end()) ? it->second : nullptr;
|
return (it != scenes.end()) ? it->second : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> GameEngine::getSceneNames() const {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
names.reserve(scenes.size());
|
||||||
|
for (const auto& [name, scene] : scenes) {
|
||||||
|
names.push_back(name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
void GameEngine::changeScene(std::string s)
|
void GameEngine::changeScene(std::string s)
|
||||||
{
|
{
|
||||||
changeScene(s, TransitionType::None, 0.0f);
|
changeScene(s, TransitionType::None, 0.0f);
|
||||||
|
|
@ -346,9 +356,10 @@ void GameEngine::run()
|
||||||
profilerOverlay->render(*render_target);
|
profilerOverlay->render(*render_target);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render ImGui console overlay
|
// Render ImGui overlays (console and scene explorer)
|
||||||
if (imguiInitialized && !headless) {
|
if (imguiInitialized && !headless) {
|
||||||
console.render();
|
console.render();
|
||||||
|
sceneExplorer.render(*this);
|
||||||
ImGui::SFML::Render(*window);
|
ImGui::SFML::Render(*window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -550,6 +561,12 @@ void GameEngine::sUserInput()
|
||||||
continue; // Don't pass grave key to game
|
continue; // Don't pass grave key to game
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle F4 for scene explorer toggle
|
||||||
|
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F4) {
|
||||||
|
sceneExplorer.toggle();
|
||||||
|
continue; // Don't pass F4 to game
|
||||||
|
}
|
||||||
|
|
||||||
// If console wants keyboard, don't pass keyboard events to game
|
// If console wants keyboard, don't pass keyboard events to game
|
||||||
if (console.wantsKeyboardInput()) {
|
if (console.wantsKeyboardInput()) {
|
||||||
// Still process non-keyboard events (mouse, window close, etc.)
|
// Still process non-keyboard events (mouse, window close, etc.)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "SceneTransition.h"
|
#include "SceneTransition.h"
|
||||||
#include "Profiler.h"
|
#include "Profiler.h"
|
||||||
#include "ImGuiConsole.h"
|
#include "ImGuiConsole.h"
|
||||||
|
#include "ImGuiSceneExplorer.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
@ -191,6 +192,7 @@ private:
|
||||||
|
|
||||||
// ImGui console overlay
|
// ImGui console overlay
|
||||||
ImGuiConsole console;
|
ImGuiConsole console;
|
||||||
|
ImGuiSceneExplorer sceneExplorer;
|
||||||
bool imguiInitialized = false;
|
bool imguiInitialized = false;
|
||||||
|
|
||||||
// #219 - Thread synchronization for background Python threads
|
// #219 - Thread synchronization for background Python threads
|
||||||
|
|
@ -214,6 +216,7 @@ public:
|
||||||
~GameEngine();
|
~GameEngine();
|
||||||
Scene* currentScene();
|
Scene* currentScene();
|
||||||
Scene* getScene(const std::string& name); // #118: Get scene by name
|
Scene* getScene(const std::string& name); // #118: Get scene by name
|
||||||
|
std::vector<std::string> getSceneNames() const; // #136: Get all scene names for explorer
|
||||||
void changeScene(std::string);
|
void changeScene(std::string);
|
||||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
||||||
void createScene(std::string);
|
void createScene(std::string);
|
||||||
|
|
|
||||||
|
|
@ -381,9 +381,9 @@ void ImGuiConsole::render() {
|
||||||
}
|
}
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
// Keep focus on input
|
// Keep focus on input only after executing a command
|
||||||
ImGui::SetItemDefaultFocus();
|
ImGui::SetItemDefaultFocus();
|
||||||
if (reclaimFocus || (visible && !ImGui::IsAnyItemActive())) {
|
if (reclaimFocus) {
|
||||||
ImGui::SetKeyboardFocusHere(-1);
|
ImGui::SetKeyboardFocusHere(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
285
src/ImGuiSceneExplorer.cpp
Normal file
285
src/ImGuiSceneExplorer.cpp
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
#include "ImGuiSceneExplorer.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "GameEngine.h"
|
||||||
|
#include "Scene.h"
|
||||||
|
#include "UIDrawable.h"
|
||||||
|
#include "UIFrame.h"
|
||||||
|
#include "UICaption.h"
|
||||||
|
#include "UISprite.h"
|
||||||
|
#include "UIGrid.h"
|
||||||
|
#include "UIEntity.h"
|
||||||
|
#include "ImGuiConsole.h"
|
||||||
|
#include "PythonObjectCache.h"
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
bool ImGuiSceneExplorer::isEnabled() {
|
||||||
|
// Use the same enabled flag as the console
|
||||||
|
return ImGuiConsole::isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiSceneExplorer::toggle() {
|
||||||
|
if (isEnabled()) {
|
||||||
|
visible = !visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiSceneExplorer::render(GameEngine& engine) {
|
||||||
|
if (!visible || !isEnabled()) return;
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// Position on the right side of the screen
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(350, io.DisplaySize.y * 0.6f), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - 360, 10), ImGuiCond_FirstUseEver);
|
||||||
|
|
||||||
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||||
|
|
||||||
|
if (!ImGui::Begin("Scene Explorer", &visible, flags)) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene tree header
|
||||||
|
ImGui::Text("Scenes (%zu):", engine.getSceneNames().size());
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// Scrollable tree region
|
||||||
|
ImGui::BeginChild("SceneTree", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
|
||||||
|
|
||||||
|
// Get all scene names and render each
|
||||||
|
std::string currentSceneName = engine.scene;
|
||||||
|
std::vector<std::string> sceneNames = engine.getSceneNames();
|
||||||
|
|
||||||
|
for (const auto& sceneName : sceneNames) {
|
||||||
|
bool isActive = (sceneName == currentSceneName);
|
||||||
|
renderSceneNode(engine, sceneName, isActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiSceneExplorer::renderSceneNode(GameEngine& engine, const std::string& sceneName, bool isActive) {
|
||||||
|
ImGuiTreeNodeFlags sceneFlags = ImGuiTreeNodeFlags_OpenOnArrow
|
||||||
|
| ImGuiTreeNodeFlags_OpenOnDoubleClick
|
||||||
|
| ImGuiTreeNodeFlags_DefaultOpen;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
sceneFlags |= ImGuiTreeNodeFlags_Selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build label with active indicator
|
||||||
|
std::string label = sceneName;
|
||||||
|
if (isActive) {
|
||||||
|
label += " [active]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene icon/indicator
|
||||||
|
bool sceneOpen = ImGui::TreeNodeEx(("##scene_" + sceneName).c_str(), sceneFlags, "%s %s",
|
||||||
|
isActive ? ">" : " ", label.c_str());
|
||||||
|
|
||||||
|
// Click to activate scene (if not already active)
|
||||||
|
if (ImGui::IsItemClicked() && !isActive) {
|
||||||
|
engine.changeScene(sceneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneOpen) {
|
||||||
|
// Get scene's UI elements
|
||||||
|
auto ui_elements = engine.scene_ui(sceneName);
|
||||||
|
if (ui_elements && !ui_elements->empty()) {
|
||||||
|
for (auto& drawable : *ui_elements) {
|
||||||
|
if (drawable) {
|
||||||
|
renderDrawableNode(drawable, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::TextDisabled(" (empty)");
|
||||||
|
}
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiSceneExplorer::renderDrawableNode(std::shared_ptr<UIDrawable> drawable, int depth) {
|
||||||
|
if (!drawable) return;
|
||||||
|
|
||||||
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow
|
||||||
|
| ImGuiTreeNodeFlags_OpenOnDoubleClick
|
||||||
|
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||||
|
|
||||||
|
// Check if this node has children
|
||||||
|
bool hasChildren = false;
|
||||||
|
UIFrame* frame = nullptr;
|
||||||
|
UIGrid* grid = nullptr;
|
||||||
|
|
||||||
|
switch (drawable->derived_type()) {
|
||||||
|
case PyObjectsEnum::UIFRAME:
|
||||||
|
frame = static_cast<UIFrame*>(drawable.get());
|
||||||
|
hasChildren = frame->children && !frame->children->empty();
|
||||||
|
break;
|
||||||
|
case PyObjectsEnum::UIGRID:
|
||||||
|
grid = static_cast<UIGrid*>(drawable.get());
|
||||||
|
hasChildren = (grid->entities && !grid->entities->empty()) ||
|
||||||
|
(grid->children && !grid->children->empty());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChildren) {
|
||||||
|
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visibility indicator
|
||||||
|
const char* visIcon = drawable->visible ? "[v]" : "[h]";
|
||||||
|
|
||||||
|
// Build display string
|
||||||
|
std::string displayName = getDisplayName(drawable.get());
|
||||||
|
std::string nodeLabel = std::string(visIcon) + " " + getTypeName(drawable.get()) + ": " + displayName;
|
||||||
|
|
||||||
|
// Use pointer as unique ID
|
||||||
|
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)drawable.get(), flags, "%s", nodeLabel.c_str());
|
||||||
|
|
||||||
|
// Double-click to toggle visibility
|
||||||
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||||
|
drawable->visible = !drawable->visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle leaf nodes (NoTreePushOnOpen means TreePop not needed)
|
||||||
|
if (!hasChildren) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeOpen) {
|
||||||
|
// Render children based on type
|
||||||
|
if (frame && frame->children) {
|
||||||
|
for (auto& child : *frame->children) {
|
||||||
|
if (child) {
|
||||||
|
renderDrawableNode(child, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grid) {
|
||||||
|
// Render entities
|
||||||
|
if (grid->entities && !grid->entities->empty()) {
|
||||||
|
ImGuiTreeNodeFlags entityGroupFlags = ImGuiTreeNodeFlags_OpenOnArrow;
|
||||||
|
bool entitiesOpen = ImGui::TreeNodeEx("Entities", entityGroupFlags, "Entities (%zu)",
|
||||||
|
grid->entities->size());
|
||||||
|
if (entitiesOpen) {
|
||||||
|
for (auto& entity : *grid->entities) {
|
||||||
|
if (entity) {
|
||||||
|
renderEntityNode(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render grid's drawable children (overlays)
|
||||||
|
if (grid->children && !grid->children->empty()) {
|
||||||
|
ImGuiTreeNodeFlags overlayGroupFlags = ImGuiTreeNodeFlags_OpenOnArrow;
|
||||||
|
bool overlaysOpen = ImGui::TreeNodeEx("Overlays", overlayGroupFlags, "Overlays (%zu)",
|
||||||
|
grid->children->size());
|
||||||
|
if (overlaysOpen) {
|
||||||
|
for (auto& child : *grid->children) {
|
||||||
|
if (child) {
|
||||||
|
renderDrawableNode(child, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiSceneExplorer::renderEntityNode(std::shared_ptr<UIEntity> entity) {
|
||||||
|
if (!entity) return;
|
||||||
|
|
||||||
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf
|
||||||
|
| ImGuiTreeNodeFlags_NoTreePushOnOpen
|
||||||
|
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||||
|
|
||||||
|
std::string displayName = getEntityDisplayName(entity.get());
|
||||||
|
std::string nodeLabel = "Entity: " + displayName;
|
||||||
|
|
||||||
|
ImGui::TreeNodeEx((void*)(intptr_t)entity.get(), flags, "%s", nodeLabel.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ImGuiSceneExplorer::getDisplayName(UIDrawable* drawable) {
|
||||||
|
if (!drawable) return "(null)";
|
||||||
|
|
||||||
|
// Try to get Python object repr from cache
|
||||||
|
if (drawable->serial_number != 0) {
|
||||||
|
PyObject* pyObj = PythonObjectCache::getInstance().lookup(drawable->serial_number);
|
||||||
|
if (pyObj) {
|
||||||
|
PyObject* repr = PyObject_Repr(pyObj);
|
||||||
|
if (repr) {
|
||||||
|
const char* repr_str = PyUnicode_AsUTF8(repr);
|
||||||
|
if (repr_str) {
|
||||||
|
std::string result(repr_str);
|
||||||
|
Py_DECREF(repr);
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Py_DECREF(repr);
|
||||||
|
}
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use name if available
|
||||||
|
if (!drawable->name.empty()) {
|
||||||
|
return "\"" + drawable->name + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to address
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "@" << std::hex << std::setw(8) << std::setfill('0') << (uintptr_t)drawable;
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ImGuiSceneExplorer::getEntityDisplayName(UIEntity* entity) {
|
||||||
|
if (!entity) return "(null)";
|
||||||
|
|
||||||
|
// Try to get Python object repr from cache
|
||||||
|
if (entity->serial_number != 0) {
|
||||||
|
PyObject* pyObj = PythonObjectCache::getInstance().lookup(entity->serial_number);
|
||||||
|
if (pyObj) {
|
||||||
|
PyObject* repr = PyObject_Repr(pyObj);
|
||||||
|
if (repr) {
|
||||||
|
const char* repr_str = PyUnicode_AsUTF8(repr);
|
||||||
|
if (repr_str) {
|
||||||
|
std::string result(repr_str);
|
||||||
|
Py_DECREF(repr);
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Py_DECREF(repr);
|
||||||
|
}
|
||||||
|
Py_DECREF(pyObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to position
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "(" << entity->position.x << ", " << entity->position.y << ")";
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) {
|
||||||
|
if (!drawable) return "null";
|
||||||
|
|
||||||
|
switch (drawable->derived_type()) {
|
||||||
|
case PyObjectsEnum::UIFRAME: return "Frame";
|
||||||
|
case PyObjectsEnum::UICAPTION: return "Caption";
|
||||||
|
case PyObjectsEnum::UISPRITE: return "Sprite";
|
||||||
|
case PyObjectsEnum::UIGRID: return "Grid";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/ImGuiSceneExplorer.h
Normal file
46
src/ImGuiSceneExplorer.h
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class GameEngine;
|
||||||
|
class UIDrawable;
|
||||||
|
class UIEntity;
|
||||||
|
class UIFrame;
|
||||||
|
class UIGrid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief ImGui-based scene tree explorer for debugging
|
||||||
|
*
|
||||||
|
* Displays hierarchical view of all scenes and their UI elements.
|
||||||
|
* Allows switching between scenes and collapsing/expanding the tree.
|
||||||
|
* Activated by F4 key. Mutually exclusive with the console (grave key).
|
||||||
|
*/
|
||||||
|
class ImGuiSceneExplorer {
|
||||||
|
public:
|
||||||
|
ImGuiSceneExplorer() = default;
|
||||||
|
|
||||||
|
// Core functionality
|
||||||
|
void render(GameEngine& engine);
|
||||||
|
void toggle();
|
||||||
|
bool isVisible() const { return visible; }
|
||||||
|
void setVisible(bool v) { visible = v; }
|
||||||
|
|
||||||
|
// Configuration - uses same enabled flag as console
|
||||||
|
static bool isEnabled();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool visible = false;
|
||||||
|
|
||||||
|
// Tree rendering helpers
|
||||||
|
void renderSceneNode(GameEngine& engine, const std::string& sceneName, bool isActive);
|
||||||
|
void renderDrawableNode(std::shared_ptr<UIDrawable> drawable, int depth = 0);
|
||||||
|
void renderEntityNode(std::shared_ptr<UIEntity> entity);
|
||||||
|
|
||||||
|
// Get display name for a drawable (name or type + address)
|
||||||
|
std::string getDisplayName(UIDrawable* drawable);
|
||||||
|
std::string getEntityDisplayName(UIEntity* entity);
|
||||||
|
|
||||||
|
// Get type name string
|
||||||
|
const char* getTypeName(UIDrawable* drawable);
|
||||||
|
};
|
||||||
|
|
@ -156,14 +156,10 @@ PyObject* PyTimer::stop(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from game engine map (but preserve the Timer data!)
|
// Just mark as stopped - do NOT erase from map here!
|
||||||
if (Resources::game && !self->name.empty()) {
|
// Removing from the map during iteration (e.g., from a timer callback)
|
||||||
auto it = Resources::game->timers.find(self->name);
|
// would invalidate iterators in testTimers(). The stopped flag tells
|
||||||
if (it != Resources::game->timers.end() && it->second == self->data) {
|
// testTimers() to safely remove this timer on its next pass.
|
||||||
Resources::game->timers.erase(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self->data->stop();
|
self->data->stop();
|
||||||
// NOTE: We do NOT reset self->data here - the timer can be restarted
|
// NOTE: We do NOT reset self->data here - the timer can be restarted
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,18 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
||||||
if (type) {
|
if (type) {
|
||||||
Py_DECREF(type);
|
Py_DECREF(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-register in cache if the object has a serial number
|
||||||
|
// This handles the case where the original Python wrapper was GC'd
|
||||||
|
// but the C++ object persists (e.g., inline-created objects added to collections)
|
||||||
|
if (obj && drawable->serial_number != 0) {
|
||||||
|
PyObject* weakref = PyWeakref_NewRef(obj, NULL);
|
||||||
|
if (weakref) {
|
||||||
|
PythonObjectCache::getInstance().registerObject(drawable->serial_number, weakref);
|
||||||
|
Py_DECREF(weakref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,18 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
|
||||||
|
|
||||||
o->data = std::static_pointer_cast<UIEntity>(target);
|
o->data = std::static_pointer_cast<UIEntity>(target);
|
||||||
o->weakreflist = NULL;
|
o->weakreflist = NULL;
|
||||||
|
|
||||||
|
// Re-register in cache if the entity has a serial number
|
||||||
|
// This handles the case where the original Python wrapper was GC'd
|
||||||
|
// but the C++ object persists (e.g., inline-created objects added to collections)
|
||||||
|
if (target->serial_number != 0) {
|
||||||
|
PyObject* weakref = PyWeakref_NewRef((PyObject*)o, NULL);
|
||||||
|
if (weakref) {
|
||||||
|
PythonObjectCache::getInstance().registerObject(target->serial_number, weakref);
|
||||||
|
Py_DECREF(weakref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (PyObject*)o;
|
return (PyObject*)o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def try_move(self, dx, dy, test=False):
|
def try_move(self, dx, dy, test=False):
|
||||||
x_max, y_max = self.grid.grid_size
|
x_max, y_max = int(self.grid.grid_size.x), int(self.grid.grid_size.y)
|
||||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||||
#for e in iterable_entities(self.grid):
|
#for e in iterable_entities(self.grid):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class TileInfo:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_grid(grid, xy:tuple):
|
def from_grid(grid, xy:tuple):
|
||||||
values = {}
|
values = {}
|
||||||
x_max, y_max = grid.grid_size
|
x_max, y_max = int(grid.grid_size.x), int(grid.grid_size.y)
|
||||||
for d in deltas:
|
for d in deltas:
|
||||||
tx, ty = d[0] + xy[0], d[1] + xy[1]
|
tx, ty = d[0] + xy[0], d[1] + xy[1]
|
||||||
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||||
|
|
@ -71,7 +71,7 @@ def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False)
|
||||||
tx, ty = xy[0] + dxy[0], xy[1] + dxy[1]
|
tx, ty = xy[0] + dxy[0], xy[1] + dxy[1]
|
||||||
#print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}")
|
#print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}")
|
||||||
if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified
|
if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified
|
||||||
x_max, y_max = grid.grid_size
|
x_max, y_max = int(grid.grid_size.x), int(grid.grid_size.y)
|
||||||
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||||
return False
|
return False
|
||||||
return grid.at((tx, ty)).tilesprite == allowed_tile
|
return grid.at((tx, ty)).tilesprite == allowed_tile
|
||||||
|
|
@ -107,7 +107,7 @@ def find_possible_tiles(grid, x, y, unverified_tiles=None, pass_unverified=False
|
||||||
return list(set(list(possible)))
|
return list(set(list(possible)))
|
||||||
|
|
||||||
def wfc_first_pass(grid):
|
def wfc_first_pass(grid):
|
||||||
w, h = grid.grid_size
|
w, h = int(grid.grid_size.x), int(grid.grid_size.y)
|
||||||
possibilities = {}
|
possibilities = {}
|
||||||
for x in range(0, w):
|
for x in range(0, w):
|
||||||
for y in range(0, h):
|
for y in range(0, h):
|
||||||
|
|
@ -122,7 +122,7 @@ def wfc_first_pass(grid):
|
||||||
return possibilities
|
return possibilities
|
||||||
|
|
||||||
def wfc_pass(grid, possibilities=None):
|
def wfc_pass(grid, possibilities=None):
|
||||||
w, h = grid.grid_size
|
w, h = int(grid.grid_size.x), int(grid.grid_size.y)
|
||||||
if possibilities is None:
|
if possibilities is None:
|
||||||
#print("first pass results:")
|
#print("first pass results:")
|
||||||
possibilities = wfc_first_pass(grid)
|
possibilities = wfc_first_pass(grid)
|
||||||
|
|
|
||||||
|
|
@ -22,28 +22,47 @@ class Resources:
|
||||||
self.sfx_volume = 100
|
self.sfx_volume = 100
|
||||||
self.master_volume = 100
|
self.master_volume = 100
|
||||||
|
|
||||||
# load the music/sfx files here
|
# Load the music/sfx files using new Sound API
|
||||||
self.splats = []
|
self.splats = []
|
||||||
for i in range(1, 10):
|
for i in range(1, 10):
|
||||||
mcrfpy.createSoundBuffer(f"assets/sfx/splat{i}.ogg")
|
try:
|
||||||
|
sound = mcrfpy.Sound(f"assets/sfx/splat{i}.ogg")
|
||||||
|
self.splats.append(sound)
|
||||||
|
except RuntimeError:
|
||||||
|
pass # Sound file not found, skip
|
||||||
|
|
||||||
|
self.music = None # Will hold Music object when loaded
|
||||||
|
|
||||||
def play_sfx(self, sfx_id):
|
def play_sfx(self, sfx_id):
|
||||||
if self.sfx_enabled and self.sfx_volume and self.master_volume:
|
if self.sfx_enabled and self.sfx_volume and self.master_volume:
|
||||||
mcrfpy.setSoundVolume(self.master_volume/100 * self.sfx_volume)
|
if sfx_id < len(self.splats):
|
||||||
mcrfpy.playSound(sfx_id)
|
sound = self.splats[sfx_id]
|
||||||
|
sound.volume = self.master_volume / 100 * self.sfx_volume
|
||||||
|
sound.play()
|
||||||
|
|
||||||
def play_music(self, track_id):
|
def play_music(self, track_path):
|
||||||
if self.music_enabled and self.music_volume and self.master_volume:
|
if self.music_enabled and self.music_volume and self.master_volume:
|
||||||
mcrfpy.setMusicVolume(self.master_volume/100 * self.music_volume)
|
try:
|
||||||
mcrfpy.playMusic(...)
|
self.music = mcrfpy.Music(track_path)
|
||||||
|
self.music.volume = self.master_volume / 100 * self.music_volume
|
||||||
|
self.music.play()
|
||||||
|
except RuntimeError:
|
||||||
|
pass # Music file not found
|
||||||
|
|
||||||
|
def set_music_volume(self, volume):
|
||||||
|
self.music_volume = volume
|
||||||
|
if self.music:
|
||||||
|
self.music.volume = self.master_volume / 100 * self.music_volume
|
||||||
|
|
||||||
|
def set_sfx_volume(self, volume):
|
||||||
|
self.sfx_volume = volume
|
||||||
|
|
||||||
resources = Resources()
|
resources = Resources()
|
||||||
|
|
||||||
class Crypt:
|
class Crypt:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
play = mcrfpy.Scene("play")
|
self.scene = mcrfpy.Scene("play")
|
||||||
self.ui = mcrfpy.sceneUI("play")
|
self.ui = self.scene.children
|
||||||
|
|
||||||
entity_frame = mcrfpy.Frame(pos=(815, 10), size=(194, 595), fill_color=frame_color)
|
entity_frame = mcrfpy.Frame(pos=(815, 10), size=(194, 595), fill_color=frame_color)
|
||||||
inventory_frame = mcrfpy.Frame(pos=(10, 610), size=(800, 143), fill_color=frame_color)
|
inventory_frame = mcrfpy.Frame(pos=(10, 610), size=(800, 143), fill_color=frame_color)
|
||||||
|
|
@ -244,8 +263,8 @@ class Crypt:
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
resources.play_sfx(1)
|
resources.play_sfx(1)
|
||||||
play.activate()
|
self.scene.activate()
|
||||||
play.on_key = self.cos_keys
|
self.scene.on_key = self.cos_keys
|
||||||
|
|
||||||
def add_entity(self, e:ce.COSEntity):
|
def add_entity(self, e:ce.COSEntity):
|
||||||
self.entities.append(e)
|
self.entities.append(e)
|
||||||
|
|
@ -402,7 +421,7 @@ class Crypt:
|
||||||
self.grid = self.level.grid
|
self.grid = self.level.grid
|
||||||
self.grid.zoom = 2.0
|
self.grid.zoom = 2.0
|
||||||
# Center the camera on the middle of the grid (pixel coordinates: cells * tile_size / 2)
|
# Center the camera on the middle of the grid (pixel coordinates: cells * tile_size / 2)
|
||||||
gw, gh = self.grid.grid_size
|
gw, gh = int(self.grid.grid_size.x), int(self.grid.grid_size.y)
|
||||||
self.grid.center = (gw * 16 / 2, gh * 16 / 2)
|
self.grid.center = (gw * 16 / 2, gh * 16 / 2)
|
||||||
# TODO, make an entity mover function
|
# TODO, make an entity mover function
|
||||||
#self.add_entity(self.player)
|
#self.add_entity(self.player)
|
||||||
|
|
@ -463,12 +482,12 @@ class SweetButton:
|
||||||
"""Helper func for when graphics changes or glitches make the button stuck down"""
|
"""Helper func for when graphics changes or glitches make the button stuck down"""
|
||||||
self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset)
|
self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset)
|
||||||
|
|
||||||
def do_click(self, x, y, mousebtn, event):
|
def do_click(self, pos, button, action):
|
||||||
if event == "start":
|
if action == "start":
|
||||||
self.main_button.x, self.main_button.y = (0, 0)
|
self.main_button.x, self.main_button.y = (0, 0)
|
||||||
elif event == "end":
|
elif action == "end":
|
||||||
self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset)
|
self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset)
|
||||||
result = self.click(self, (x, y, mousebtn, event))
|
result = self.click(self, (pos.x, pos.y, button, action))
|
||||||
if result: # return True from event function to instantly un-pop
|
if result: # return True from event function to instantly un-pop
|
||||||
self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset)
|
self.main_button.x, self.main_button.y = (self.shadow_offset, self.shadow_offset)
|
||||||
|
|
||||||
|
|
@ -490,9 +509,9 @@ class SweetButton:
|
||||||
|
|
||||||
class MainMenu:
|
class MainMenu:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
menu = mcrfpy.Scene("menu")
|
self.scene = mcrfpy.Scene("menu")
|
||||||
self.ui = mcrfpy.sceneUI("menu")
|
self.ui = self.scene.children
|
||||||
menu.activate()
|
self.scene.activate()
|
||||||
self.crypt = None
|
self.crypt = None
|
||||||
|
|
||||||
components = []
|
components = []
|
||||||
|
|
@ -501,7 +520,7 @@ class MainMenu:
|
||||||
self.grid = self.demo.grid
|
self.grid = self.demo.grid
|
||||||
self.grid.zoom = 1.75
|
self.grid.zoom = 1.75
|
||||||
# Center the camera on the middle of the grid (pixel coordinates: cells * tile_size / 2)
|
# Center the camera on the middle of the grid (pixel coordinates: cells * tile_size / 2)
|
||||||
gw, gh = self.grid.grid_size
|
gw, gh = int(self.grid.grid_size.x), int(self.grid.grid_size.y)
|
||||||
self.grid.center = (gw * 16 / 2, gh * 16 / 2)
|
self.grid.center = (gw * 16 / 2, gh * 16 / 2)
|
||||||
coords = self.demo.generate(
|
coords = self.demo.generate(
|
||||||
[("boulder", "boulder", "rat", "cyclops", "boulder"), ("spawn"), ("rat", "big rat"), ("button", "boulder", "exit")]
|
[("boulder", "boulder", "rat", "cyclops", "boulder"), ("spawn"), ("rat", "big rat"), ("button", "boulder", "exit")]
|
||||||
|
|
@ -538,14 +557,14 @@ class MainMenu:
|
||||||
#self.create_level(self.depth)
|
#self.create_level(self.depth)
|
||||||
for e in self.entities:
|
for e in self.entities:
|
||||||
self.grid.entities.append(e._entity)
|
self.grid.entities.append(e._entity)
|
||||||
def just_wiggle(*args):
|
def just_wiggle(timer, runtime):
|
||||||
try:
|
try:
|
||||||
self.player.try_move(*random.choice(((1, 0),(-1, 0),(0, 1),(0, -1))))
|
self.player.try_move(*random.choice(((1, 0),(-1, 0),(0, 1),(0, -1))))
|
||||||
for e in self.entities:
|
for e in self.entities:
|
||||||
e.act()
|
e.act()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
mcrfpy.setTimer("demo_motion", just_wiggle, 100)
|
self.demo_timer = mcrfpy.Timer("demo_motion", just_wiggle, 100)
|
||||||
components.append(
|
components.append(
|
||||||
self.demo.grid
|
self.demo.grid
|
||||||
)
|
)
|
||||||
|
|
@ -605,22 +624,22 @@ class MainMenu:
|
||||||
|
|
||||||
def toast_say(self, txt, delay=10):
|
def toast_say(self, txt, delay=10):
|
||||||
"kick off a toast event"
|
"kick off a toast event"
|
||||||
if self.toast_event is not None:
|
if self.toast_event is not None and hasattr(self, 'toast_timer'):
|
||||||
mcrfpy.delTimer("toast_timer")
|
self.toast_timer.stop()
|
||||||
self.toast.text = txt
|
self.toast.text = txt
|
||||||
self.toast_event = 350
|
self.toast_event = 350
|
||||||
self.toast.fill_color = (255, 255, 255, 255)
|
self.toast.fill_color = (255, 255, 255, 255)
|
||||||
self.toast.outline = 2
|
self.toast.outline = 2
|
||||||
self.toast.outline_color = (0, 0, 0, 255)
|
self.toast.outline_color = (0, 0, 0, 255)
|
||||||
mcrfpy.setTimer("toast_timer", self.toast_callback, 100)
|
self.toast_timer = mcrfpy.Timer("toast_timer", self.toast_callback, 100)
|
||||||
|
|
||||||
def toast_callback(self, *args):
|
def toast_callback(self, timer, runtime):
|
||||||
"fade out the toast text"
|
"fade out the toast text"
|
||||||
self.toast_event -= 5
|
self.toast_event -= 5
|
||||||
if self.toast_event < 0:
|
if self.toast_event < 0:
|
||||||
self.toast_event = None
|
self.toast_event = None
|
||||||
mcrfpy.delTimer("toast_timer")
|
self.toast_timer.stop()
|
||||||
mcrfpy.text = ""
|
self.toast.text = ""
|
||||||
return
|
return
|
||||||
a = min(self.toast_event, 255)
|
a = min(self.toast_event, 255)
|
||||||
self.toast.fill_color = (255, 255, 255, a)
|
self.toast.fill_color = (255, 255, 255, a)
|
||||||
|
|
@ -632,9 +651,8 @@ class MainMenu:
|
||||||
def play(self, sweet_btn, args):
|
def play(self, sweet_btn, args):
|
||||||
#if args[3] == "start": return # DRAMATIC on release action!
|
#if args[3] == "start": return # DRAMATIC on release action!
|
||||||
if args[3] == "end": return
|
if args[3] == "end": return
|
||||||
mcrfpy.delTimer("demo_motion") # Clean up the demo timer
|
self.demo_timer.stop() # Clean up the demo timer
|
||||||
self.crypt = Crypt()
|
self.crypt = Crypt()
|
||||||
#mcrfpy.setScene("play")
|
|
||||||
self.crypt.start()
|
self.crypt.start()
|
||||||
|
|
||||||
def scale(self, sweet_btn, args, window_scale=None):
|
def scale(self, sweet_btn, args, window_scale=None):
|
||||||
|
|
@ -658,26 +676,25 @@ class MainMenu:
|
||||||
resources.music_enabled = not resources.music_enabled
|
resources.music_enabled = not resources.music_enabled
|
||||||
print(f"music: {resources.music_enabled}")
|
print(f"music: {resources.music_enabled}")
|
||||||
if resources.music_enabled:
|
if resources.music_enabled:
|
||||||
mcrfpy.setMusicVolume(self.music_volume)
|
resources.set_music_volume(resources.music_volume)
|
||||||
sweet_btn.text = "Music is ON"
|
sweet_btn.text = "Music is ON"
|
||||||
sweet_btn.sprite_number = 12
|
sweet_btn.sprite_number = 12
|
||||||
else:
|
else:
|
||||||
self.toast_say("Use your volume keys or\nlook in Settings for a volume meter.")
|
self.toast_say("Use your volume keys or\nlook in Settings for a volume meter.")
|
||||||
mcrfpy.setMusicVolume(0)
|
resources.set_music_volume(0)
|
||||||
sweet_btn.text = "Music is OFF"
|
sweet_btn.text = "Music is OFF"
|
||||||
sweet_btn.sprite_number = 17
|
sweet_btn.sprite_number = 17
|
||||||
|
|
||||||
def sfx_toggle(self, sweet_btn, args):
|
def sfx_toggle(self, sweet_btn, args):
|
||||||
if args[3] == "end": return
|
if args[3] == "end": return
|
||||||
resources.sfx_enabled = not resources.sfx_enabled
|
resources.sfx_enabled = not resources.sfx_enabled
|
||||||
#print(f"sfx: {resources.sfx_enabled}")
|
|
||||||
if resources.sfx_enabled:
|
if resources.sfx_enabled:
|
||||||
mcrfpy.setSoundVolume(self.sfx_volume)
|
resources.set_sfx_volume(resources.sfx_volume)
|
||||||
sweet_btn.text = "SFX are ON"
|
sweet_btn.text = "SFX are ON"
|
||||||
sweet_btn.sprite_number = 0
|
sweet_btn.sprite_number = 0
|
||||||
else:
|
else:
|
||||||
self.toast_say("Use your volume keys or\nlook in Settings for a volume meter.")
|
self.toast_say("Use your volume keys or\nlook in Settings for a volume meter.")
|
||||||
mcrfpy.setSoundVolume(0)
|
resources.set_sfx_volume(0)
|
||||||
sweet_btn.text = "SFX are OFF"
|
sweet_btn.text = "SFX are OFF"
|
||||||
sweet_btn.sprite_number = 17
|
sweet_btn.sprite_number = 17
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue