Compare commits
5 commits
c025cd7da3
...
f62362032e
| Author | SHA1 | Date | |
|---|---|---|---|
| f62362032e | |||
| 05f28ef7cd | |||
| e64c5c147f | |||
| b863698f6e | |||
| 9f481a2e4a |
75 changed files with 15145 additions and 594 deletions
119
CLAUDE.md
119
CLAUDE.md
|
|
@ -625,4 +625,123 @@ After modifying C++ inline documentation with MCRF_* macros:
|
||||||
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
||||||
|
|
||||||
The macro system ensures complete, consistent documentation across all Python bindings.
|
The macro system ensures complete, consistent documentation across all Python bindings.
|
||||||
|
|
||||||
|
### Adding Documentation for New Python Types
|
||||||
|
|
||||||
|
When adding a new Python class/type to the engine, follow these steps to ensure it's properly documented:
|
||||||
|
|
||||||
|
#### 1. Class Docstring (tp_doc)
|
||||||
|
|
||||||
|
In the `PyTypeObject` definition (usually in the header file), set `tp_doc` with a comprehensive docstring:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// In PyMyClass.h
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"MyClass(arg1: type, arg2: type)\n\n"
|
||||||
|
"Brief description of what this class does.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" arg1: Description of first argument.\n"
|
||||||
|
" arg2: Description of second argument.\n\n"
|
||||||
|
"Properties:\n"
|
||||||
|
" prop1 (type, read-only): Description of property.\n"
|
||||||
|
" prop2 (type): Description of writable property.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" obj = mcrfpy.MyClass('example', 42)\n"
|
||||||
|
" print(obj.prop1)\n"
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Method Documentation (PyMethodDef)
|
||||||
|
|
||||||
|
For each method in the `methods[]` array, use the MCRF_* macros:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// In PyMyClass.cpp
|
||||||
|
PyMethodDef PyMyClass::methods[] = {
|
||||||
|
{"do_something", (PyCFunction)do_something, METH_VARARGS,
|
||||||
|
MCRF_METHOD(MyClass, do_something,
|
||||||
|
MCRF_SIG("(value: int)", "bool"),
|
||||||
|
MCRF_DESC("Does something with the value."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("value", "The value to process")
|
||||||
|
MCRF_RETURNS("True if successful, False otherwise")
|
||||||
|
)},
|
||||||
|
{NULL} // Sentinel
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Property Documentation (PyGetSetDef)
|
||||||
|
|
||||||
|
For each property in the `getsetters[]` array, include a docstring:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// In PyMyClass.cpp
|
||||||
|
PyGetSetDef PyMyClass::getsetters[] = {
|
||||||
|
{"property_name", (getter)get_property, (setter)set_property,
|
||||||
|
"Property description. Include (type, read-only) if not writable.",
|
||||||
|
NULL},
|
||||||
|
{NULL} // Sentinel
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important for read-only properties:** Include "read-only" in the docstring so the doc generator detects it:
|
||||||
|
```cpp
|
||||||
|
{"name", (getter)get_name, NULL, // NULL setter = read-only
|
||||||
|
"Object name (str, read-only). Unique identifier.",
|
||||||
|
NULL},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Register Type in Module
|
||||||
|
|
||||||
|
Ensure the type is properly registered in `McRFPy_API.cpp` and its methods/getsetters are assigned:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Set methods and getsetters before PyType_Ready
|
||||||
|
mcrfpydef::PyMyClassType.tp_methods = PyMyClass::methods;
|
||||||
|
mcrfpydef::PyMyClassType.tp_getset = PyMyClass::getsetters;
|
||||||
|
|
||||||
|
// Then call PyType_Ready and add to module
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Regenerate Documentation
|
||||||
|
|
||||||
|
After adding the new type, regenerate all docs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -j4 # Rebuild with new documentation
|
||||||
|
cd build
|
||||||
|
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
|
||||||
|
cp docs/API_REFERENCE_DYNAMIC.md ../docs/
|
||||||
|
cp docs/api_reference_dynamic.html ../docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Update Type Stubs (Optional)
|
||||||
|
|
||||||
|
For IDE support, update `stubs/mcrfpy.pyi` with the new class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyClass:
|
||||||
|
"""Brief description."""
|
||||||
|
def __init__(self, arg1: str, arg2: int) -> None: ...
|
||||||
|
@property
|
||||||
|
def prop1(self) -> str: ...
|
||||||
|
def do_something(self, value: int) -> bool: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation Extraction Details
|
||||||
|
|
||||||
|
The doc generator (`tools/generate_dynamic_docs.py`) uses Python introspection:
|
||||||
|
|
||||||
|
- **Classes**: Detected via `inspect.isclass()`, docstring from `cls.__doc__`
|
||||||
|
- **Methods**: Detected via `callable()` check on class attributes
|
||||||
|
- **Properties**: Detected via `types.GetSetDescriptorType` (C++ extension) or `property` (Python)
|
||||||
|
- **Read-only detection**: Checks if "read-only" appears in property docstring
|
||||||
|
|
||||||
|
If documentation isn't appearing, verify:
|
||||||
|
1. The type is exported to the `mcrfpy` module
|
||||||
|
2. Methods/getsetters arrays are properly assigned before `PyType_Ready()`
|
||||||
|
3. Docstrings don't contain null bytes or invalid UTF-8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# McRogueFace API Reference
|
# McRogueFace API Reference
|
||||||
|
|
||||||
*Generated on 2025-12-28 14:29:42*
|
*Generated on 2025-12-29 14:24:58*
|
||||||
|
|
||||||
*This documentation was dynamically generated from the compiled module.*
|
*This documentation was dynamically generated from the compiled module.*
|
||||||
|
|
||||||
|
|
@ -289,6 +289,13 @@ Note:
|
||||||
|
|
||||||
Animation object for animating UI properties
|
Animation object for animating UI properties
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `duration` *(read-only)*: Animation duration in seconds (float, read-only). Total time for the animation to complete.
|
||||||
|
- `elapsed` *(read-only)*: Elapsed time in seconds (float, read-only). Time since the animation started.
|
||||||
|
- `is_complete` *(read-only)*: Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
|
||||||
|
- `is_delta` *(read-only)*: Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
|
||||||
|
- `property` *(read-only)*: Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `complete() -> None`
|
#### `complete() -> None`
|
||||||
|
|
@ -376,6 +383,28 @@ Attributes:
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
|
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `center`: Center position of the arc
|
||||||
|
- `color`: Arc color
|
||||||
|
- `end_angle`: Ending angle in degrees
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `name`: Name for finding this element.
|
||||||
|
- `on_click`: Callable executed when arc is clicked.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `pos`: Position as a Vector (same as center).
|
||||||
|
- `radius`: Arc radius in pixels
|
||||||
|
- `start_angle`: Starting angle in degrees
|
||||||
|
- `thickness`: Line thickness
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first).
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -447,6 +476,29 @@ Attributes:
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
w, h (float): Read-only computed size based on text and font
|
w, h (float): Read-only computed size based on text and font
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `fill_color`: Fill color of the text
|
||||||
|
- `font_size`: Font size (integer) in points
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `name`: Name for finding elements
|
||||||
|
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `outline`: Thickness of the border
|
||||||
|
- `outline_color`: Outline color of the text
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `pos`: (x, y) vector
|
||||||
|
- `text`: The text displayed
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `x`: X coordinate of top-left corner
|
||||||
|
- `y`: Y coordinate of top-left corner
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -511,6 +563,27 @@ Attributes:
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
|
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `center`: Center position of the circle
|
||||||
|
- `fill_color`: Fill color of the circle
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `name`: Name for finding this element.
|
||||||
|
- `on_click`: Callable executed when circle is clicked.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `outline`: Outline thickness (0 for no outline)
|
||||||
|
- `outline_color`: Outline color of the circle
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `pos`: Position as a Vector (same as center).
|
||||||
|
- `radius`: Circle radius in pixels
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first).
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -545,6 +618,12 @@ Note:
|
||||||
|
|
||||||
SFML Color Object
|
SFML Color Object
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `a`: Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range.
|
||||||
|
- `b`: Blue component (0-255). Automatically clamped to valid range.
|
||||||
|
- `g`: Green component (0-255). Automatically clamped to valid range.
|
||||||
|
- `r`: Red component (0-255). Automatically clamped to valid range.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `from_hex(hex_string: str) -> Color`
|
#### `from_hex(hex_string: str) -> Color`
|
||||||
|
|
@ -600,6 +679,11 @@ Methods:
|
||||||
set(x, y, color): Set color at cell position
|
set(x, y, color): Set color at cell position
|
||||||
fill(color): Fill entire layer with color
|
fill(color): Fill entire layer with color
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `grid_size`: Layer dimensions as (width, height) tuple.
|
||||||
|
- `visible`: Whether the layer is rendered.
|
||||||
|
- `z_index`: Layer z-order. Negative values render below entities.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `apply_perspective(entity, visible=None, discovered=None, unknown=None)`
|
#### `apply_perspective(entity, visible=None, discovered=None, unknown=None)`
|
||||||
|
|
@ -644,6 +728,12 @@ Call this after the entity moves to update the visibility layer.
|
||||||
|
|
||||||
Base class for all drawable UI elements
|
Base class for all drawable UI elements
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -703,6 +793,19 @@ Attributes:
|
||||||
opacity (float): Opacity value
|
opacity (float): Opacity value
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `draw_pos`: Entity position (graphically)
|
||||||
|
- `grid`: Grid this entity belongs to. Get: Returns the Grid or None. Set: Assign a Grid to move entity, or None to remove from grid.
|
||||||
|
- `gridstate`: Grid point states for the entity
|
||||||
|
- `name`: Name for finding elements
|
||||||
|
- `opacity`: Opacity (0.0 = transparent, 1.0 = opaque)
|
||||||
|
- `pos`: Entity position (integer grid coordinates)
|
||||||
|
- `sprite_index`: Sprite index on the texture on the display
|
||||||
|
- `sprite_number`: Sprite index (DEPRECATED: use sprite_index instead)
|
||||||
|
- `visible`: Visibility flag
|
||||||
|
- `x`: Entity x position
|
||||||
|
- `y`: Entity y position
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `at(...)`
|
#### `at(...)`
|
||||||
|
|
@ -810,6 +913,12 @@ Remove first occurrence of entity. Raises ValueError if not found.
|
||||||
|
|
||||||
*Inherits from: IntEnum*
|
*Inherits from: IntEnum*
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `denominator`: the denominator of a rational number in lowest terms
|
||||||
|
- `imag`: the imaginary part of a complex number
|
||||||
|
- `numerator`: the numerator of a rational number in lowest terms
|
||||||
|
- `real`: the real part of a complex number
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `as_integer_ratio(...)`
|
#### `as_integer_ratio(...)`
|
||||||
|
|
@ -887,6 +996,10 @@ Return an array of bytes representing an integer.
|
||||||
|
|
||||||
SFML Font Object
|
SFML Font Object
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `family` *(read-only)*: Font family name (str, read-only). Retrieved from font metadata.
|
||||||
|
- `source` *(read-only)*: Source filename path (str, read-only). The path used to load this font.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
### Frame
|
### Frame
|
||||||
|
|
@ -933,6 +1046,32 @@ Attributes:
|
||||||
clip_children (bool): Whether to clip children to frame bounds
|
clip_children (bool): Whether to clip children to frame bounds
|
||||||
cache_subtree (bool): Cache subtree rendering to texture
|
cache_subtree (bool): Cache subtree rendering to texture
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `cache_subtree`: #144: Cache subtree rendering to texture for performance
|
||||||
|
- `children`: UICollection of objects on top of this one
|
||||||
|
- `clip_children`: Whether to clip children to frame bounds
|
||||||
|
- `fill_color`: Fill color of the rectangle
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `h`: height of the rectangle
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `name`: Name for finding elements
|
||||||
|
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `outline`: Thickness of the border
|
||||||
|
- `outline_color`: Outline color of the rectangle
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `pos`: Position as a Vector
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `w`: width of the rectangle
|
||||||
|
- `x`: X coordinate of top-left corner
|
||||||
|
- `y`: Y coordinate of top-left corner
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -1015,6 +1154,48 @@ Attributes:
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `center`: Grid coordinate at the center of the Grid's view (pan)
|
||||||
|
- `center_x`: center of the view X-coordinate
|
||||||
|
- `center_y`: center of the view Y-coordinate
|
||||||
|
- `children`: UICollection of UIDrawable children (speech bubbles, effects, overlays)
|
||||||
|
- `entities`: EntityCollection of entities on this grid
|
||||||
|
- `fill_color`: Background fill color of the grid
|
||||||
|
- `fov`: FOV algorithm for this grid (mcrfpy.FOV enum). Used by entity.updateVisibility() and layer methods when fov=None.
|
||||||
|
- `fov_radius`: Default FOV radius for this grid. Used when radius not specified.
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `grid_size`: Grid dimensions (grid_x, grid_y)
|
||||||
|
- `grid_x`: Grid x dimension
|
||||||
|
- `grid_y`: Grid y dimension
|
||||||
|
- `h`: visible widget height
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `hovered_cell`: Currently hovered cell as (x, y) tuple, or None if not hovering.
|
||||||
|
- `layers`: List of grid layers (ColorLayer, TileLayer) sorted by z_index
|
||||||
|
- `name`: Name for finding elements
|
||||||
|
- `on_cell_click`: Callback when a grid cell is clicked. Called with (cell_x, cell_y).
|
||||||
|
- `on_cell_enter`: Callback when mouse enters a grid cell. Called with (cell_x, cell_y).
|
||||||
|
- `on_cell_exit`: Callback when mouse exits a grid cell. Called with (cell_x, cell_y).
|
||||||
|
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `perspective`: Entity whose perspective to use for FOV rendering (None for omniscient view). Setting an entity automatically enables perspective mode.
|
||||||
|
- `perspective_enabled`: Whether to use perspective-based FOV rendering. When True with no valid entity, all cells appear undiscovered.
|
||||||
|
- `pos`: Position of the grid as Vector
|
||||||
|
- `position`: Position of the grid (x, y)
|
||||||
|
- `size`: Size of the grid (width, height)
|
||||||
|
- `texture`: Texture of the grid
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `w`: visible widget width
|
||||||
|
- `x`: top-left corner X-coordinate
|
||||||
|
- `y`: top-left corner Y-coordinate
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
|
||||||
|
- `zoom`: zoom factor for displaying the Grid
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer`
|
#### `add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer`
|
||||||
|
|
@ -1165,12 +1346,22 @@ Note:
|
||||||
|
|
||||||
UIGridPoint object
|
UIGridPoint object
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `entities` *(read-only)*: List of entities at this grid cell (read-only)
|
||||||
|
- `transparent`: Is the GridPoint transparent
|
||||||
|
- `walkable`: Is the GridPoint walkable
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
### GridPointState
|
### GridPointState
|
||||||
|
|
||||||
UIGridPointState object
|
UIGridPointState object
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `discovered`: Has the GridPointState been discovered
|
||||||
|
- `point`: GridPoint at this position (None if not discovered)
|
||||||
|
- `visible`: Is the GridPointState visible
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
### Line
|
### Line
|
||||||
|
|
@ -1205,6 +1396,26 @@ Attributes:
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
|
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `color`: Line color as a Color object.
|
||||||
|
- `end`: Ending point of the line as a Vector.
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `name`: Name for finding this element.
|
||||||
|
- `on_click`: Callable executed when line is clicked.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `pos`: Position as a Vector (midpoint of line).
|
||||||
|
- `start`: Starting point of the line as a Vector.
|
||||||
|
- `thickness`: Line thickness in pixels.
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first).
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -1237,7 +1448,56 @@ Note:
|
||||||
|
|
||||||
### Scene
|
### Scene
|
||||||
|
|
||||||
Base class for object-oriented scenes
|
Scene(name: str)
|
||||||
|
|
||||||
|
Object-oriented scene management with lifecycle callbacks.
|
||||||
|
|
||||||
|
This is the recommended approach for scene management, replacing module-level
|
||||||
|
functions like createScene(), setScene(), and sceneUI(). Key advantage: you can
|
||||||
|
set on_key handlers on ANY scene, not just the currently active one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique identifier for this scene. Used for scene transitions.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
name (str, read-only): Scene's unique identifier.
|
||||||
|
active (bool, read-only): Whether this scene is currently displayed.
|
||||||
|
children (UICollection, read-only): UI elements in this scene. Modify to add/remove elements.
|
||||||
|
on_key (callable): Keyboard handler. Set on ANY scene, regardless of which is active!
|
||||||
|
pos (Vector): Position offset for all UI elements.
|
||||||
|
visible (bool): Whether the scene renders.
|
||||||
|
opacity (float): Scene transparency (0.0-1.0).
|
||||||
|
|
||||||
|
Lifecycle Callbacks (override in subclass):
|
||||||
|
on_enter(): Called when scene becomes active via activate().
|
||||||
|
on_exit(): Called when scene is deactivated (another scene activates).
|
||||||
|
on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.
|
||||||
|
update(dt: float): Called every frame with delta time in seconds.
|
||||||
|
on_resize(width: int, height: int): Called when window is resized.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Basic usage (replacing module functions):
|
||||||
|
scene = mcrfpy.Scene('main_menu')
|
||||||
|
scene.children.append(mcrfpy.Caption(text='Welcome', pos=(100, 100)))
|
||||||
|
scene.on_key = lambda key, action: print(f'Key: {key}')
|
||||||
|
scene.activate() # Switch to this scene
|
||||||
|
|
||||||
|
# Subclassing for lifecycle:
|
||||||
|
class GameScene(mcrfpy.Scene):
|
||||||
|
def on_enter(self):
|
||||||
|
print('Game started!')
|
||||||
|
def update(self, dt):
|
||||||
|
self.player.move(dt)
|
||||||
|
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `active` *(read-only)*: Whether this scene is currently active (bool, read-only). Only one scene can be active at a time.
|
||||||
|
- `children` *(read-only)*: UI element collection for this scene (UICollection, read-only). Use to add, remove, or iterate over UI elements. Changes are reflected immediately.
|
||||||
|
- `name` *(read-only)*: Scene name (str, read-only). Unique identifier for this scene.
|
||||||
|
- `on_key`: Keyboard event handler (callable or None). Function receives (key: str, action: str) for keyboard events. Set to None to remove the handler.
|
||||||
|
- `opacity`: Scene opacity (0.0-1.0). Applied to all UI elements during rendering.
|
||||||
|
- `pos`: Scene position offset (Vector). Applied to all UI elements during rendering.
|
||||||
|
- `visible`: Scene visibility (bool). If False, scene is not rendered.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
|
|
@ -1299,6 +1559,30 @@ Attributes:
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
w, h (float): Read-only computed size based on texture and scale
|
w, h (float): Read-only computed size based on texture and scale
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `bounds`: Bounding rectangle (x, y, width, height) in local coordinates.
|
||||||
|
- `global_bounds`: Bounding rectangle (x, y, width, height) in screen coordinates.
|
||||||
|
- `global_position` *(read-only)*: Global screen position (read-only). Calculates absolute position by walking up the parent chain.
|
||||||
|
- `hovered` *(read-only)*: Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.
|
||||||
|
- `name`: Name for finding elements
|
||||||
|
- `on_click`: Callable executed when object is clicked. Function receives (x, y) coordinates of click.
|
||||||
|
- `on_enter`: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.
|
||||||
|
- `on_exit`: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.
|
||||||
|
- `on_move`: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.
|
||||||
|
- `opacity`: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].
|
||||||
|
- `parent`: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.
|
||||||
|
- `pos`: Position as a Vector
|
||||||
|
- `scale`: Uniform size factor
|
||||||
|
- `scale_x`: Horizontal scale factor
|
||||||
|
- `scale_y`: Vertical scale factor
|
||||||
|
- `sprite_index`: Which sprite on the texture is shown
|
||||||
|
- `sprite_number`: Sprite index (DEPRECATED: use sprite_index instead)
|
||||||
|
- `texture`: Texture object
|
||||||
|
- `visible`: Whether the object is visible (bool). Invisible objects are not rendered or clickable.
|
||||||
|
- `x`: X coordinate of top-left corner
|
||||||
|
- `y`: Y coordinate of top-left corner
|
||||||
|
- `z_index`: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `get_bounds() -> tuple`
|
#### `get_bounds() -> tuple`
|
||||||
|
|
@ -1333,6 +1617,14 @@ Note:
|
||||||
|
|
||||||
SFML Texture Object
|
SFML Texture Object
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `sheet_height` *(read-only)*: Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height.
|
||||||
|
- `sheet_width` *(read-only)*: Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width.
|
||||||
|
- `source` *(read-only)*: Source filename path (str, read-only). The path used to load this texture.
|
||||||
|
- `sprite_count` *(read-only)*: Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height.
|
||||||
|
- `sprite_height` *(read-only)*: Height of each sprite in pixels (int, read-only). Specified during texture initialization.
|
||||||
|
- `sprite_width` *(read-only)*: Width of each sprite in pixels (int, read-only). Specified during texture initialization.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
### TileLayer
|
### TileLayer
|
||||||
|
|
@ -1357,6 +1649,12 @@ Methods:
|
||||||
set(x, y, index): Set tile index at cell position
|
set(x, y, index): Set tile index at cell position
|
||||||
fill(index): Fill entire layer with tile index
|
fill(index): Fill entire layer with tile index
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `grid_size`: Layer dimensions as (width, height) tuple.
|
||||||
|
- `texture`: Texture atlas for tile sprites.
|
||||||
|
- `visible`: Whether the layer is rendered.
|
||||||
|
- `z_index`: Layer z-order. Negative values render below entities.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `at(x, y) -> int`
|
#### `at(x, y) -> int`
|
||||||
|
|
@ -1412,6 +1710,15 @@ Example:
|
||||||
timer.resume() # Resume timer
|
timer.resume() # Resume timer
|
||||||
timer.once = True # Make it one-shot
|
timer.once = True # Make it one-shot
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `active` *(read-only)*: Whether the timer is active and not paused (bool, read-only). False if cancelled or paused.
|
||||||
|
- `callback`: The callback function to be called when timer fires (callable). Can be changed while timer is running.
|
||||||
|
- `interval`: Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running.
|
||||||
|
- `name` *(read-only)*: Timer name (str, read-only). Unique identifier for this timer.
|
||||||
|
- `once`: Whether the timer stops after firing once (bool). If False, timer repeats indefinitely.
|
||||||
|
- `paused` *(read-only)*: Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time.
|
||||||
|
- `remaining` *(read-only)*: Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `cancel() -> None`
|
#### `cancel() -> None`
|
||||||
|
|
@ -1508,6 +1815,11 @@ Iterator for a collection of UI objects
|
||||||
|
|
||||||
SFML Vector Object
|
SFML Vector Object
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `int` *(read-only)*: Integer tuple (floor of x and y) for use as dict keys. Read-only.
|
||||||
|
- `x`: X coordinate of the vector (float)
|
||||||
|
- `y`: Y coordinate of the vector (float)
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `angle() -> float`
|
#### `angle() -> float`
|
||||||
|
|
@ -1574,6 +1886,16 @@ Note:
|
||||||
|
|
||||||
Window singleton for accessing and modifying the game window properties
|
Window singleton for accessing and modifying the game window properties
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
- `framerate_limit`: Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate.
|
||||||
|
- `fullscreen`: Window fullscreen state (bool). Setting this recreates the window.
|
||||||
|
- `game_resolution`: Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling.
|
||||||
|
- `resolution`: Window resolution as (width, height) tuple. Setting this recreates the window.
|
||||||
|
- `scaling_mode`: Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio).
|
||||||
|
- `title`: Window title string (str). Displayed in the window title bar.
|
||||||
|
- `visible`: Window visibility state (bool). Hidden windows still process events.
|
||||||
|
- `vsync`: Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate.
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
|
|
||||||
#### `center() -> None`
|
#### `center() -> None`
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>McRogueFace API Reference</h1>
|
<h1>McRogueFace API Reference</h1>
|
||||||
<p><em>Generated on 2025-12-28 14:29:42</em></p>
|
<p><em>Generated on 2025-12-29 14:23:40</em></p>
|
||||||
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
|
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
|
||||||
|
|
||||||
<div class="toc">
|
<div class="toc">
|
||||||
|
|
@ -410,6 +410,14 @@ Note:</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Animation"><span class="class-name">Animation</span></h3>
|
<h3 id="Animation"><span class="class-name">Animation</span></h3>
|
||||||
<p>Animation object for animating UI properties</p>
|
<p>Animation object for animating UI properties</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>duration</span> (read-only): Animation duration in seconds (float, read-only). Total time for the animation to complete.</li>
|
||||||
|
<li><span class='property-name'>elapsed</span> (read-only): Elapsed time in seconds (float, read-only). Time since the animation started.</li>
|
||||||
|
<li><span class='property-name'>is_complete</span> (read-only): Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.</li>
|
||||||
|
<li><span class='property-name'>is_delta</span> (read-only): Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.</li>
|
||||||
|
<li><span class='property-name'>property</span> (read-only): Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -495,6 +503,29 @@ Attributes:
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
</p>
|
</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>center</span>: Center position of the arc</li>
|
||||||
|
<li><span class='property-name'>color</span>: Arc color</li>
|
||||||
|
<li><span class='property-name'>end_angle</span>: Ending angle in degrees</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding this element.</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when arc is clicked.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Position as a Vector (same as center).</li>
|
||||||
|
<li><span class='property-name'>radius</span>: Arc radius in pixels</li>
|
||||||
|
<li><span class='property-name'>start_angle</span>: Starting angle in degrees</li>
|
||||||
|
<li><span class='property-name'>thickness</span>: Line thickness</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first).</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -567,6 +598,30 @@ Attributes:
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
w, h (float): Read-only computed size based on text and font</p>
|
w, h (float): Read-only computed size based on text and font</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>fill_color</span>: Fill color of the text</li>
|
||||||
|
<li><span class='property-name'>font_size</span>: Font size (integer) in points</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding elements</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>outline</span>: Thickness of the border</li>
|
||||||
|
<li><span class='property-name'>outline_color</span>: Outline color of the text</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: (x, y) vector</li>
|
||||||
|
<li><span class='property-name'>text</span>: The text displayed</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>x</span>: X coordinate of top-left corner</li>
|
||||||
|
<li><span class='property-name'>y</span>: Y coordinate of top-left corner</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -632,6 +687,28 @@ Attributes:
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
</p>
|
</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>center</span>: Center position of the circle</li>
|
||||||
|
<li><span class='property-name'>fill_color</span>: Fill color of the circle</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding this element.</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when circle is clicked.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>outline</span>: Outline thickness (0 for no outline)</li>
|
||||||
|
<li><span class='property-name'>outline_color</span>: Outline color of the circle</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Position as a Vector (same as center).</li>
|
||||||
|
<li><span class='property-name'>radius</span>: Circle radius in pixels</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first).</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -668,6 +745,13 @@ Note:</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Color"><span class="class-name">Color</span></h3>
|
<h3 id="Color"><span class="class-name">Color</span></h3>
|
||||||
<p>SFML Color Object</p>
|
<p>SFML Color Object</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>a</span>: Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range.</li>
|
||||||
|
<li><span class='property-name'>b</span>: Blue component (0-255). Automatically clamped to valid range.</li>
|
||||||
|
<li><span class='property-name'>g</span>: Green component (0-255). Automatically clamped to valid range.</li>
|
||||||
|
<li><span class='property-name'>r</span>: Red component (0-255). Automatically clamped to valid range.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -722,6 +806,12 @@ Methods:
|
||||||
at(x, y): Get color at cell position
|
at(x, y): Get color at cell position
|
||||||
set(x, y, color): Set color at cell position
|
set(x, y, color): Set color at cell position
|
||||||
fill(color): Fill entire layer with color</p>
|
fill(color): Fill entire layer with color</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>grid_size</span>: Layer dimensions as (width, height) tuple.</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the layer is rendered.</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Layer z-order. Negative values render below entities.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -774,6 +864,13 @@ Call this after the entity moves to update the visibility layer.</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Drawable"><span class="class-name">Drawable</span></h3>
|
<h3 id="Drawable"><span class="class-name">Drawable</span></h3>
|
||||||
<p>Base class for all drawable UI elements</p>
|
<p>Base class for all drawable UI elements</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -835,6 +932,20 @@ Attributes:
|
||||||
visible (bool): Visibility state
|
visible (bool): Visibility state
|
||||||
opacity (float): Opacity value
|
opacity (float): Opacity value
|
||||||
name (str): Element name</p>
|
name (str): Element name</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>draw_pos</span>: Entity position (graphically)</li>
|
||||||
|
<li><span class='property-name'>grid</span>: Grid this entity belongs to. Get: Returns the Grid or None. Set: Assign a Grid to move entity, or None to remove from grid.</li>
|
||||||
|
<li><span class='property-name'>gridstate</span>: Grid point states for the entity</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding elements</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity (0.0 = transparent, 1.0 = opaque)</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Entity position (integer grid coordinates)</li>
|
||||||
|
<li><span class='property-name'>sprite_index</span>: Sprite index on the texture on the display</li>
|
||||||
|
<li><span class='property-name'>sprite_number</span>: Sprite index (DEPRECATED: use sprite_index instead)</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Visibility flag</li>
|
||||||
|
<li><span class='property-name'>x</span>: Entity x position</li>
|
||||||
|
<li><span class='property-name'>y</span>: Entity y position</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -956,6 +1067,13 @@ when the entity moves if it has a grid with perspective set.</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="FOV"><span class="class-name">FOV</span></h3>
|
<h3 id="FOV"><span class="class-name">FOV</span></h3>
|
||||||
<p><em>Inherits from: IntEnum</em></p>
|
<p><em>Inherits from: IntEnum</em></p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>denominator</span>: the denominator of a rational number in lowest terms</li>
|
||||||
|
<li><span class='property-name'>imag</span>: the imaginary part of a complex number</li>
|
||||||
|
<li><span class='property-name'>numerator</span>: the numerator of a rational number in lowest terms</li>
|
||||||
|
<li><span class='property-name'>real</span>: the real part of a complex number</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1040,6 +1158,11 @@ Also known as the population count.
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Font"><span class="class-name">Font</span></h3>
|
<h3 id="Font"><span class="class-name">Font</span></h3>
|
||||||
<p>SFML Font Object</p>
|
<p>SFML Font Object</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>family</span> (read-only): Font family name (str, read-only). Retrieved from font metadata.</li>
|
||||||
|
<li><span class='property-name'>source</span> (read-only): Source filename path (str, read-only). The path used to load this font.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1085,6 +1208,33 @@ Attributes:
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
clip_children (bool): Whether to clip children to frame bounds
|
clip_children (bool): Whether to clip children to frame bounds
|
||||||
cache_subtree (bool): Cache subtree rendering to texture</p>
|
cache_subtree (bool): Cache subtree rendering to texture</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>cache_subtree</span>: #144: Cache subtree rendering to texture for performance</li>
|
||||||
|
<li><span class='property-name'>children</span>: UICollection of objects on top of this one</li>
|
||||||
|
<li><span class='property-name'>clip_children</span>: Whether to clip children to frame bounds</li>
|
||||||
|
<li><span class='property-name'>fill_color</span>: Fill color of the rectangle</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>h</span>: height of the rectangle</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding elements</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>outline</span>: Thickness of the border</li>
|
||||||
|
<li><span class='property-name'>outline_color</span>: Outline color of the rectangle</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Position as a Vector</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>w</span>: width of the rectangle</li>
|
||||||
|
<li><span class='property-name'>x</span>: X coordinate of top-left corner</li>
|
||||||
|
<li><span class='property-name'>y</span>: Y coordinate of top-left corner</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1168,6 +1318,49 @@ Attributes:
|
||||||
opacity (float): Opacity value
|
opacity (float): Opacity value
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name</p>
|
name (str): Element name</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>center</span>: Grid coordinate at the center of the Grid's view (pan)</li>
|
||||||
|
<li><span class='property-name'>center_x</span>: center of the view X-coordinate</li>
|
||||||
|
<li><span class='property-name'>center_y</span>: center of the view Y-coordinate</li>
|
||||||
|
<li><span class='property-name'>children</span>: UICollection of UIDrawable children (speech bubbles, effects, overlays)</li>
|
||||||
|
<li><span class='property-name'>entities</span>: EntityCollection of entities on this grid</li>
|
||||||
|
<li><span class='property-name'>fill_color</span>: Background fill color of the grid</li>
|
||||||
|
<li><span class='property-name'>fov</span>: FOV algorithm for this grid (mcrfpy.FOV enum). Used by entity.updateVisibility() and layer methods when fov=None.</li>
|
||||||
|
<li><span class='property-name'>fov_radius</span>: Default FOV radius for this grid. Used when radius not specified.</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>grid_size</span>: Grid dimensions (grid_x, grid_y)</li>
|
||||||
|
<li><span class='property-name'>grid_x</span>: Grid x dimension</li>
|
||||||
|
<li><span class='property-name'>grid_y</span>: Grid y dimension</li>
|
||||||
|
<li><span class='property-name'>h</span>: visible widget height</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>hovered_cell</span>: Currently hovered cell as (x, y) tuple, or None if not hovering.</li>
|
||||||
|
<li><span class='property-name'>layers</span>: List of grid layers (ColorLayer, TileLayer) sorted by z_index</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding elements</li>
|
||||||
|
<li><span class='property-name'>on_cell_click</span>: Callback when a grid cell is clicked. Called with (cell_x, cell_y).</li>
|
||||||
|
<li><span class='property-name'>on_cell_enter</span>: Callback when mouse enters a grid cell. Called with (cell_x, cell_y).</li>
|
||||||
|
<li><span class='property-name'>on_cell_exit</span>: Callback when mouse exits a grid cell. Called with (cell_x, cell_y).</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>perspective</span>: Entity whose perspective to use for FOV rendering (None for omniscient view). Setting an entity automatically enables perspective mode.</li>
|
||||||
|
<li><span class='property-name'>perspective_enabled</span>: Whether to use perspective-based FOV rendering. When True with no valid entity, all cells appear undiscovered.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Position of the grid as Vector</li>
|
||||||
|
<li><span class='property-name'>position</span>: Position of the grid (x, y)</li>
|
||||||
|
<li><span class='property-name'>size</span>: Size of the grid (width, height)</li>
|
||||||
|
<li><span class='property-name'>texture</span>: Texture of the grid</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>w</span>: visible widget width</li>
|
||||||
|
<li><span class='property-name'>x</span>: top-left corner X-coordinate</li>
|
||||||
|
<li><span class='property-name'>y</span>: top-left corner Y-coordinate</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
|
||||||
|
<li><span class='property-name'>zoom</span>: zoom factor for displaying the Grid</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1325,12 +1518,24 @@ Note:</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="GridPoint"><span class="class-name">GridPoint</span></h3>
|
<h3 id="GridPoint"><span class="class-name">GridPoint</span></h3>
|
||||||
<p>UIGridPoint object</p>
|
<p>UIGridPoint object</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>entities</span> (read-only): List of entities at this grid cell (read-only)</li>
|
||||||
|
<li><span class='property-name'>transparent</span>: Is the GridPoint transparent</li>
|
||||||
|
<li><span class='property-name'>walkable</span>: Is the GridPoint walkable</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="GridPointState"><span class="class-name">GridPointState</span></h3>
|
<h3 id="GridPointState"><span class="class-name">GridPointState</span></h3>
|
||||||
<p>UIGridPointState object</p>
|
<p>UIGridPointState object</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>discovered</span>: Has the GridPointState been discovered</li>
|
||||||
|
<li><span class='property-name'>point</span>: GridPoint at this position (None if not discovered)</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Is the GridPointState visible</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1364,6 +1569,27 @@ Attributes:
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
</p>
|
</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>color</span>: Line color as a Color object.</li>
|
||||||
|
<li><span class='property-name'>end</span>: Ending point of the line as a Vector.</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding this element.</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when line is clicked.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Position as a Vector (midpoint of line).</li>
|
||||||
|
<li><span class='property-name'>start</span>: Starting point of the line as a Vector.</li>
|
||||||
|
<li><span class='property-name'>thickness</span>: Line thickness in pixels.</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first).</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1399,7 +1625,57 @@ Note:</p>
|
||||||
|
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Scene"><span class="class-name">Scene</span></h3>
|
<h3 id="Scene"><span class="class-name">Scene</span></h3>
|
||||||
<p>Base class for object-oriented scenes</p>
|
<p>Scene(name: str)
|
||||||
|
|
||||||
|
Object-oriented scene management with lifecycle callbacks.
|
||||||
|
|
||||||
|
This is the recommended approach for scene management, replacing module-level
|
||||||
|
functions like createScene(), setScene(), and sceneUI(). Key advantage: you can
|
||||||
|
set on_key handlers on ANY scene, not just the currently active one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique identifier for this scene. Used for scene transitions.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
name (str, read-only): Scene's unique identifier.
|
||||||
|
active (bool, read-only): Whether this scene is currently displayed.
|
||||||
|
children (UICollection, read-only): UI elements in this scene. Modify to add/remove elements.
|
||||||
|
on_key (callable): Keyboard handler. Set on ANY scene, regardless of which is active!
|
||||||
|
pos (Vector): Position offset for all UI elements.
|
||||||
|
visible (bool): Whether the scene renders.
|
||||||
|
opacity (float): Scene transparency (0.0-1.0).
|
||||||
|
|
||||||
|
Lifecycle Callbacks (override in subclass):
|
||||||
|
on_enter(): Called when scene becomes active via activate().
|
||||||
|
on_exit(): Called when scene is deactivated (another scene activates).
|
||||||
|
on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.
|
||||||
|
update(dt: float): Called every frame with delta time in seconds.
|
||||||
|
on_resize(width: int, height: int): Called when window is resized.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# Basic usage (replacing module functions):
|
||||||
|
scene = mcrfpy.Scene('main_menu')
|
||||||
|
scene.children.append(mcrfpy.Caption(text='Welcome', pos=(100, 100)))
|
||||||
|
scene.on_key = lambda key, action: print(f'Key: {key}')
|
||||||
|
scene.activate() # Switch to this scene
|
||||||
|
|
||||||
|
# Subclassing for lifecycle:
|
||||||
|
class GameScene(mcrfpy.Scene):
|
||||||
|
def on_enter(self):
|
||||||
|
print('Game started!')
|
||||||
|
def update(self, dt):
|
||||||
|
self.player.move(dt)
|
||||||
|
</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>active</span> (read-only): Whether this scene is currently active (bool, read-only). Only one scene can be active at a time.</li>
|
||||||
|
<li><span class='property-name'>children</span> (read-only): UI element collection for this scene (UICollection, read-only). Use to add, remove, or iterate over UI elements. Changes are reflected immediately.</li>
|
||||||
|
<li><span class='property-name'>name</span> (read-only): Scene name (str, read-only). Unique identifier for this scene.</li>
|
||||||
|
<li><span class='property-name'>on_key</span>: Keyboard event handler (callable or None). Function receives (key: str, action: str) for keyboard events. Set to None to remove the handler.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Scene opacity (0.0-1.0). Applied to all UI elements during rendering.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Scene position offset (Vector). Applied to all UI elements during rendering.</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Scene visibility (bool). If False, scene is not rendered.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1459,6 +1735,31 @@ Attributes:
|
||||||
z_index (int): Rendering order
|
z_index (int): Rendering order
|
||||||
name (str): Element name
|
name (str): Element name
|
||||||
w, h (float): Read-only computed size based on texture and scale</p>
|
w, h (float): Read-only computed size based on texture and scale</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>bounds</span>: Bounding rectangle (x, y, width, height) in local coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_bounds</span>: Bounding rectangle (x, y, width, height) in screen coordinates.</li>
|
||||||
|
<li><span class='property-name'>global_position</span> (read-only): Global screen position (read-only). Calculates absolute position by walking up the parent chain.</li>
|
||||||
|
<li><span class='property-name'>hovered</span> (read-only): Whether mouse is currently over this element (read-only). Updated automatically by the engine during mouse movement.</li>
|
||||||
|
<li><span class='property-name'>name</span>: Name for finding elements</li>
|
||||||
|
<li><span class='property-name'>on_click</span>: Callable executed when object is clicked. Function receives (x, y) coordinates of click.</li>
|
||||||
|
<li><span class='property-name'>on_enter</span>: Callback for mouse enter events. Called with (x, y, button, action) when mouse enters this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_exit</span>: Callback for mouse exit events. Called with (x, y, button, action) when mouse leaves this element's bounds.</li>
|
||||||
|
<li><span class='property-name'>on_move</span>: Callback for mouse movement within bounds. Called with (x, y, button, action) for each mouse movement while inside. Performance note: Called frequently during movement - keep handlers fast.</li>
|
||||||
|
<li><span class='property-name'>opacity</span>: Opacity level (0.0 = transparent, 1.0 = opaque). Automatically clamped to valid range [0.0, 1.0].</li>
|
||||||
|
<li><span class='property-name'>parent</span>: Parent drawable. Get: Returns the parent Frame/Grid if nested, or None if at scene level. Set: Assign a Frame/Grid to reparent, or None to remove from parent.</li>
|
||||||
|
<li><span class='property-name'>pos</span>: Position as a Vector</li>
|
||||||
|
<li><span class='property-name'>scale</span>: Uniform size factor</li>
|
||||||
|
<li><span class='property-name'>scale_x</span>: Horizontal scale factor</li>
|
||||||
|
<li><span class='property-name'>scale_y</span>: Vertical scale factor</li>
|
||||||
|
<li><span class='property-name'>sprite_index</span>: Which sprite on the texture is shown</li>
|
||||||
|
<li><span class='property-name'>sprite_number</span>: Sprite index (DEPRECATED: use sprite_index instead)</li>
|
||||||
|
<li><span class='property-name'>texture</span>: Texture object</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the object is visible (bool). Invisible objects are not rendered or clickable.</li>
|
||||||
|
<li><span class='property-name'>x</span>: X coordinate of top-left corner</li>
|
||||||
|
<li><span class='property-name'>y</span>: Y coordinate of top-left corner</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Z-order for rendering (lower values rendered first). Automatically triggers scene resort when changed.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1495,6 +1796,15 @@ Note:</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Texture"><span class="class-name">Texture</span></h3>
|
<h3 id="Texture"><span class="class-name">Texture</span></h3>
|
||||||
<p>SFML Texture Object</p>
|
<p>SFML Texture Object</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>sheet_height</span> (read-only): Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height.</li>
|
||||||
|
<li><span class='property-name'>sheet_width</span> (read-only): Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width.</li>
|
||||||
|
<li><span class='property-name'>source</span> (read-only): Source filename path (str, read-only). The path used to load this texture.</li>
|
||||||
|
<li><span class='property-name'>sprite_count</span> (read-only): Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height.</li>
|
||||||
|
<li><span class='property-name'>sprite_height</span> (read-only): Height of each sprite in pixels (int, read-only). Specified during texture initialization.</li>
|
||||||
|
<li><span class='property-name'>sprite_width</span> (read-only): Width of each sprite in pixels (int, read-only). Specified during texture initialization.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1519,6 +1829,13 @@ Methods:
|
||||||
at(x, y): Get tile index at cell position
|
at(x, y): Get tile index at cell position
|
||||||
set(x, y, index): Set tile index at cell position
|
set(x, y, index): Set tile index at cell position
|
||||||
fill(index): Fill entire layer with tile index</p>
|
fill(index): Fill entire layer with tile index</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>grid_size</span>: Layer dimensions as (width, height) tuple.</li>
|
||||||
|
<li><span class='property-name'>texture</span>: Texture atlas for tile sprites.</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Whether the layer is rendered.</li>
|
||||||
|
<li><span class='property-name'>z_index</span>: Layer z-order. Negative values render below entities.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1578,6 +1895,16 @@ Example:
|
||||||
timer.pause() # Pause timer
|
timer.pause() # Pause timer
|
||||||
timer.resume() # Resume timer
|
timer.resume() # Resume timer
|
||||||
timer.once = True # Make it one-shot</p>
|
timer.once = True # Make it one-shot</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>active</span> (read-only): Whether the timer is active and not paused (bool, read-only). False if cancelled or paused.</li>
|
||||||
|
<li><span class='property-name'>callback</span>: The callback function to be called when timer fires (callable). Can be changed while timer is running.</li>
|
||||||
|
<li><span class='property-name'>interval</span>: Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running.</li>
|
||||||
|
<li><span class='property-name'>name</span> (read-only): Timer name (str, read-only). Unique identifier for this timer.</li>
|
||||||
|
<li><span class='property-name'>once</span>: Whether the timer stops after firing once (bool). If False, timer repeats indefinitely.</li>
|
||||||
|
<li><span class='property-name'>paused</span> (read-only): Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time.</li>
|
||||||
|
<li><span class='property-name'>remaining</span> (read-only): Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1681,6 +2008,12 @@ Use name-based .find() for stable element access.</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Vector"><span class="class-name">Vector</span></h3>
|
<h3 id="Vector"><span class="class-name">Vector</span></h3>
|
||||||
<p>SFML Vector Object</p>
|
<p>SFML Vector Object</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>int</span> (read-only): Integer tuple (floor of x and y) for use as dict keys. Read-only.</li>
|
||||||
|
<li><span class='property-name'>x</span>: X coordinate of the vector (float)</li>
|
||||||
|
<li><span class='property-name'>y</span>: Y coordinate of the vector (float)</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
@ -1747,6 +2080,17 @@ Note:</p>
|
||||||
<div class="method-section">
|
<div class="method-section">
|
||||||
<h3 id="Window"><span class="class-name">Window</span></h3>
|
<h3 id="Window"><span class="class-name">Window</span></h3>
|
||||||
<p>Window singleton for accessing and modifying the game window properties</p>
|
<p>Window singleton for accessing and modifying the game window properties</p>
|
||||||
|
<h4>Properties:</h4>
|
||||||
|
<ul>
|
||||||
|
<li><span class='property-name'>framerate_limit</span>: Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate.</li>
|
||||||
|
<li><span class='property-name'>fullscreen</span>: Window fullscreen state (bool). Setting this recreates the window.</li>
|
||||||
|
<li><span class='property-name'>game_resolution</span>: Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling.</li>
|
||||||
|
<li><span class='property-name'>resolution</span>: Window resolution as (width, height) tuple. Setting this recreates the window.</li>
|
||||||
|
<li><span class='property-name'>scaling_mode</span>: Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio).</li>
|
||||||
|
<li><span class='property-name'>title</span>: Window title string (str). Displayed in the window title bar.</li>
|
||||||
|
<li><span class='property-name'>visible</span>: Window visibility state (bool). Hidden windows still process events.</li>
|
||||||
|
<li><span class='property-name'>vsync</span>: Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate.</li>
|
||||||
|
</ul>
|
||||||
<h4>Methods:</h4>
|
<h4>Methods:</h4>
|
||||||
|
|
||||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||||
|
|
|
||||||
30
docs/tutorials/part_00_setup/part_00_setup.py
Normal file
30
docs/tutorials/part_00_setup/part_00_setup.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""McRogueFace - Part 0: Setting Up McRogueFace
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_00_setup
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_00_setup/part_00_setup.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create a Scene object - this is the preferred approach
|
||||||
|
scene = mcrfpy.Scene("hello")
|
||||||
|
|
||||||
|
# Create a caption to display text
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(512, 300),
|
||||||
|
text="Hello, Roguelike!"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 32
|
||||||
|
|
||||||
|
# Add the caption to the scene's UI collection
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
# Activate the scene to display it
|
||||||
|
scene.activate()
|
||||||
|
|
||||||
|
# Note: There is no run() function!
|
||||||
|
# The engine is already running - your script is imported by it.
|
||||||
121
docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
Normal file
121
docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
"""McRogueFace - Part 1: The '@' and the Dungeon Grid
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_01_grid_movement
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_AT = 64 # '@' - player character
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor tile
|
||||||
|
|
||||||
|
# Grid dimensions (in tiles)
|
||||||
|
GRID_WIDTH = 20
|
||||||
|
GRID_HEIGHT = 15
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load the texture (sprite sheet)
|
||||||
|
# Parameters: path, sprite_width, sprite_height
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
# The grid displays tiles and contains entities
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(100, 80), # Position on screen (pixels)
|
||||||
|
size=(640, 480), # Display size (pixels)
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT), # Size in tiles
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the zoom level for better visibility
|
||||||
|
grid.zoom = 2.0
|
||||||
|
|
||||||
|
# Fill the grid with floor tiles
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
|
||||||
|
# Create the player entity at the center of the grid
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(GRID_WIDTH // 2, GRID_HEIGHT // 2), # Grid coordinates, not pixels!
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_AT
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player to the grid
|
||||||
|
# Option 1: Use the grid parameter in constructor
|
||||||
|
# player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=SPRITE_AT, grid=grid)
|
||||||
|
|
||||||
|
# Option 2: Append to grid.entities (what we will use)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(100, 20),
|
||||||
|
text="Part 1: Grid Movement - Use Arrow Keys or WASD"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 18
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
# Add a position display
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(100, 50),
|
||||||
|
text=f"Player Position: ({player.x}, {player.y})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input to move the player.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key that was pressed (e.g., "W", "Up", "Space")
|
||||||
|
action: Either "start" (key pressed) or "end" (key released)
|
||||||
|
"""
|
||||||
|
# Only respond to key press, not release
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current player position
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
|
||||||
|
# Calculate new position based on key
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
py -= 1 # Up decreases Y
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
py += 1 # Down increases Y
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
px -= 1 # Left decreases X
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
px += 1 # Right increases X
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update player position
|
||||||
|
player.x = px
|
||||||
|
player.y = py
|
||||||
|
|
||||||
|
# Update the position display
|
||||||
|
pos_display.text = f"Player Position: ({player.x}, {player.y})"
|
||||||
|
|
||||||
|
# Set the key handler on the scene
|
||||||
|
# This is the preferred approach - works on ANY scene, not just the active one
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# Activate the scene
|
||||||
|
scene.activate()
|
||||||
|
|
||||||
|
print("Part 1 loaded! Use WASD or Arrow keys to move.")
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
"""McRogueFace - Part 2: Walls, Floors, and Collision
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_02_tiles_collision
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 30
|
||||||
|
GRID_HEIGHT = 20
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Map Creation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def create_map(grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the grid with walls and floors.
|
||||||
|
|
||||||
|
Creates a simple room with walls around the edges and floor in the middle.
|
||||||
|
"""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
|
# Place walls around the edges
|
||||||
|
if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1:
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
else:
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
|
||||||
|
# Add some interior walls to make it interesting
|
||||||
|
# Vertical wall
|
||||||
|
for y in range(5, 15):
|
||||||
|
cell = grid.at(10, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
|
||||||
|
# Horizontal wall
|
||||||
|
for x in range(15, 25):
|
||||||
|
cell = grid.at(x, 10)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
|
||||||
|
# Leave gaps for doorways
|
||||||
|
grid.at(10, 10).tilesprite = SPRITE_FLOOR
|
||||||
|
grid.at(10, 10).walkable = True
|
||||||
|
grid.at(20, 10).tilesprite = SPRITE_FLOOR
|
||||||
|
grid.at(20, 10).walkable = True
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
x: Target X coordinate (in tiles)
|
||||||
|
y: Target Y coordinate (in tiles)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the position is walkable, False otherwise
|
||||||
|
"""
|
||||||
|
# Check grid bounds first
|
||||||
|
if x < 0 or x >= GRID_WIDTH:
|
||||||
|
return False
|
||||||
|
if y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if the tile is walkable
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
return cell.walkable
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(80, 100),
|
||||||
|
size=(720, 480),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.5
|
||||||
|
|
||||||
|
# Build the map
|
||||||
|
create_map(grid)
|
||||||
|
|
||||||
|
# Create the player in the center of the left room
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(5, 10),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(80, 20),
|
||||||
|
text="Part 2: Walls and Collision"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(80, 55),
|
||||||
|
text="WASD or Arrow Keys to move | Walls block movement"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(80, 600),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
status_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 600),
|
||||||
|
text="Status: Ready"
|
||||||
|
)
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
status_display.font_size = 16
|
||||||
|
scene.children.append(status_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input with collision detection."""
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current position
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
|
||||||
|
# Calculate intended new position
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return # Ignore other keys
|
||||||
|
|
||||||
|
# Check collision before moving
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
status_display.text = "Status: Moved"
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
else:
|
||||||
|
status_display.text = "Status: Blocked!"
|
||||||
|
status_display.fill_color = mcrfpy.Color(200, 100, 100)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 2 loaded! Try walking into walls.")
|
||||||
|
|
@ -0,0 +1,356 @@
|
||||||
|
"""McRogueFace - Part 3: Procedural Dungeon Generation
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_03_dungeon_generation
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
"""Create a new room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Left edge X coordinate
|
||||||
|
y: Top edge Y coordinate
|
||||||
|
width: Room width in tiles
|
||||||
|
height: Room height in tiles
|
||||||
|
"""
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
"""Return the center coordinates of the room."""
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
"""Return the inner area of the room (excluding walls).
|
||||||
|
|
||||||
|
The inner area is one tile smaller on each side to leave room
|
||||||
|
for walls between adjacent rooms.
|
||||||
|
"""
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
"""Check if this room overlaps with another room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: Another RectangularRoom to check against
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the rooms overlap, False otherwise
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
|
||||||
|
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
room: The room to carve
|
||||||
|
"""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
x1: Starting X coordinate
|
||||||
|
x2: Ending X coordinate
|
||||||
|
y: Y coordinate of the tunnel
|
||||||
|
"""
|
||||||
|
start_x = min(x1, x2)
|
||||||
|
end_x = max(x1, x2)
|
||||||
|
for x in range(start_x, end_x + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
y1: Starting Y coordinate
|
||||||
|
y2: Ending Y coordinate
|
||||||
|
x: X coordinate of the tunnel
|
||||||
|
"""
|
||||||
|
start_y = min(y1, y2)
|
||||||
|
end_y = max(y1, y2)
|
||||||
|
for y in range(start_y, end_y + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points.
|
||||||
|
|
||||||
|
Randomly chooses to go horizontal-then-vertical or vertical-then-horizontal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
start: Starting (x, y) coordinates
|
||||||
|
end: Ending (x, y) coordinates
|
||||||
|
"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
# Randomly choose whether to go horizontal or vertical first
|
||||||
|
if random.random() < 0.5:
|
||||||
|
# Horizontal first, then vertical
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
# Vertical first, then horizontal
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y2)
|
||||||
|
|
||||||
|
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||||
|
"""Generate a dungeon with rooms and tunnels.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid to generate the dungeon in
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The (x, y) coordinates where the player should start
|
||||||
|
"""
|
||||||
|
# Start with all walls
|
||||||
|
fill_with_walls(grid)
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
# Random room dimensions
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
|
||||||
|
# Random position (leaving 1-tile border)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
# Check for overlap with existing rooms
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue # Skip this room, try another
|
||||||
|
|
||||||
|
# No overlap - carve out the room
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
# Connect to previous room with a tunnel
|
||||||
|
if rooms:
|
||||||
|
# Tunnel from this room's center to the previous room's center
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Return the center of the first room as the player start position
|
||||||
|
if rooms:
|
||||||
|
return rooms[0].center
|
||||||
|
else:
|
||||||
|
# Fallback if no rooms were generated
|
||||||
|
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement."""
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
return grid.at(x, y).walkable
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 560),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate the dungeon and get player start position
|
||||||
|
player_start_x, player_start_y = generate_dungeon(grid)
|
||||||
|
|
||||||
|
# Create the player at the starting position
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 3: Procedural Dungeon Generation"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move | R: Regenerate dungeon | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 660),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
room_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 660),
|
||||||
|
text="Press R to regenerate the dungeon"
|
||||||
|
)
|
||||||
|
room_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
room_display.font_size = 16
|
||||||
|
scene.children.append(room_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def regenerate_dungeon() -> None:
|
||||||
|
"""Generate a new dungeon and reposition the player."""
|
||||||
|
new_x, new_y = generate_dungeon(grid)
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
room_display.text = "New dungeon generated!"
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "R":
|
||||||
|
regenerate_dungeon()
|
||||||
|
return
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 3 loaded! Explore the dungeon or press R to regenerate.")
|
||||||
363
docs/tutorials/part_04_fov/part_04_fov.py
Normal file
363
docs/tutorials/part_04_fov/part_04_fov.py
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
"""McRogueFace - Part 4: Field of View
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_04_fov
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_04_fov/part_04_fov.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# FOV settings
|
||||||
|
FOV_RADIUS = 8
|
||||||
|
|
||||||
|
# Visibility colors (applied as overlays)
|
||||||
|
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) # Fully transparent - show tile
|
||||||
|
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) # Dark blue tint - dimmed
|
||||||
|
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) # Solid black - hidden
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class (from Part 3)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exploration Tracking
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Track which tiles have been discovered (seen at least once)
|
||||||
|
explored: list[list[bool]] = []
|
||||||
|
|
||||||
|
def init_explored() -> None:
|
||||||
|
"""Initialize the explored array to all False."""
|
||||||
|
global explored
|
||||||
|
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
|
||||||
|
def mark_explored(x: int, y: int) -> None:
|
||||||
|
"""Mark a tile as explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
explored[y][x] = True
|
||||||
|
|
||||||
|
def is_explored(x: int, y: int) -> bool:
|
||||||
|
"""Check if a tile has been explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
return explored[y][x]
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation (from Part 3, with transparent property)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False # Walls block line of sight
|
||||||
|
|
||||||
|
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor."""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True # Floors allow line of sight
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel."""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel."""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points."""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
carve_tunnel_vertical(grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(grid, x1, x2, y2)
|
||||||
|
|
||||||
|
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||||
|
"""Generate a dungeon with rooms and tunnels."""
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored() # Reset exploration when generating new dungeon
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
return rooms[0].center
|
||||||
|
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Field of View
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None:
|
||||||
|
"""Update the field of view visualization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The game grid
|
||||||
|
fov_layer: The ColorLayer for FOV visualization
|
||||||
|
player_x: Player's X position
|
||||||
|
player_y: Player's Y position
|
||||||
|
"""
|
||||||
|
# Compute FOV from player position
|
||||||
|
grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
|
# Update each tile's visibility
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if grid.is_in_fov(x, y):
|
||||||
|
# Currently visible - mark as explored and show clearly
|
||||||
|
mark_explored(x, y)
|
||||||
|
fov_layer.set(x, y, COLOR_VISIBLE)
|
||||||
|
elif is_explored(x, y):
|
||||||
|
# Previously seen but not currently visible - show dimmed
|
||||||
|
fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||||
|
else:
|
||||||
|
# Never seen - hide completely
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement."""
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
return grid.at(x, y).walkable
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 560),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
player_start_x, player_start_y = generate_dungeon(grid)
|
||||||
|
|
||||||
|
# Add a color layer for FOV visualization (below entities)
|
||||||
|
fov_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
|
# Initialize the FOV layer to all black (unknown)
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Create the player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Calculate initial FOV
|
||||||
|
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 4: Field of View"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 660),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
fov_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 660),
|
||||||
|
text=f"FOV Radius: {FOV_RADIUS}"
|
||||||
|
)
|
||||||
|
fov_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
fov_display.font_size = 16
|
||||||
|
scene.children.append(fov_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def regenerate_dungeon() -> None:
|
||||||
|
"""Generate a new dungeon and reposition the player."""
|
||||||
|
new_x, new_y = generate_dungeon(grid)
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
|
||||||
|
# Reset FOV layer to unknown
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Calculate new FOV
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "R":
|
||||||
|
regenerate_dungeon()
|
||||||
|
return
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
|
||||||
|
# Update FOV after movement
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 4 loaded! Explore the dungeon - watch the fog of war!")
|
||||||
685
docs/tutorials/part_05_enemies/part_05_enemies.py
Normal file
685
docs/tutorials/part_05_enemies/part_05_enemies.py
Normal file
|
|
@ -0,0 +1,685 @@
|
||||||
|
"""McRogueFace - Part 5: Placing Enemies
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_05_enemies
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_05_enemies/part_05_enemies.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
|
||||||
|
# Enemy sprites (lowercase letters in CP437)
|
||||||
|
SPRITE_GOBLIN = 103 # 'g'
|
||||||
|
SPRITE_ORC = 111 # 'o'
|
||||||
|
SPRITE_TROLL = 116 # 't'
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# Enemy spawn parameters
|
||||||
|
MAX_ENEMIES_PER_ROOM = 3
|
||||||
|
|
||||||
|
# FOV settings
|
||||||
|
FOV_RADIUS = 8
|
||||||
|
|
||||||
|
# Visibility colors
|
||||||
|
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||||
|
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
|
||||||
|
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy Data
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Enemy templates - stats for each enemy type
|
||||||
|
ENEMY_TEMPLATES = {
|
||||||
|
"goblin": {
|
||||||
|
"sprite": SPRITE_GOBLIN,
|
||||||
|
"hp": 6,
|
||||||
|
"max_hp": 6,
|
||||||
|
"attack": 3,
|
||||||
|
"defense": 0,
|
||||||
|
"color": mcrfpy.Color(100, 200, 100) # Greenish
|
||||||
|
},
|
||||||
|
"orc": {
|
||||||
|
"sprite": SPRITE_ORC,
|
||||||
|
"hp": 10,
|
||||||
|
"max_hp": 10,
|
||||||
|
"attack": 4,
|
||||||
|
"defense": 1,
|
||||||
|
"color": mcrfpy.Color(100, 150, 100) # Darker green
|
||||||
|
},
|
||||||
|
"troll": {
|
||||||
|
"sprite": SPRITE_TROLL,
|
||||||
|
"hp": 16,
|
||||||
|
"max_hp": 16,
|
||||||
|
"attack": 6,
|
||||||
|
"defense": 2,
|
||||||
|
"color": mcrfpy.Color(50, 150, 50) # Dark green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global storage for entity data
|
||||||
|
# Maps entity objects to their data dictionaries
|
||||||
|
entity_data: dict = {}
|
||||||
|
|
||||||
|
# Global references
|
||||||
|
player = None
|
||||||
|
grid = None
|
||||||
|
fov_layer = None
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class (from Part 3)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exploration Tracking (from Part 4)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
explored: list[list[bool]] = []
|
||||||
|
|
||||||
|
def init_explored() -> None:
|
||||||
|
"""Initialize the explored array to all False."""
|
||||||
|
global explored
|
||||||
|
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
|
||||||
|
def mark_explored(x: int, y: int) -> None:
|
||||||
|
"""Mark a tile as explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
explored[y][x] = True
|
||||||
|
|
||||||
|
def is_explored(x: int, y: int) -> bool:
|
||||||
|
"""Check if a tile has been explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
return explored[y][x]
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation (from Part 4)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
|
||||||
|
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor."""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel."""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel."""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
target_grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points."""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y2)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy Management
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, texture: mcrfpy.Texture) -> mcrfpy.Entity:
|
||||||
|
"""Spawn an enemy at the given position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
x: X position in tiles
|
||||||
|
y: Y position in tiles
|
||||||
|
enemy_type: Type of enemy ("goblin", "orc", or "troll")
|
||||||
|
texture: The texture to use for the sprite
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created enemy Entity
|
||||||
|
"""
|
||||||
|
template = ENEMY_TEMPLATES[enemy_type]
|
||||||
|
|
||||||
|
enemy = mcrfpy.Entity(
|
||||||
|
grid_pos=(x, y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=template["sprite"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Start hidden until player sees them
|
||||||
|
enemy.visible = False
|
||||||
|
|
||||||
|
# Add to grid
|
||||||
|
target_grid.entities.append(enemy)
|
||||||
|
|
||||||
|
# Store enemy data
|
||||||
|
entity_data[enemy] = {
|
||||||
|
"type": enemy_type,
|
||||||
|
"name": enemy_type.capitalize(),
|
||||||
|
"hp": template["hp"],
|
||||||
|
"max_hp": template["max_hp"],
|
||||||
|
"attack": template["attack"],
|
||||||
|
"defense": template["defense"],
|
||||||
|
"is_player": False
|
||||||
|
}
|
||||||
|
|
||||||
|
return enemy
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, texture: mcrfpy.Texture) -> None:
|
||||||
|
"""Spawn random enemies in a room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
room: The room to spawn enemies in
|
||||||
|
texture: The texture to use for sprites
|
||||||
|
"""
|
||||||
|
# Random number of enemies (0 to MAX_ENEMIES_PER_ROOM)
|
||||||
|
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
|
||||||
|
|
||||||
|
for _ in range(num_enemies):
|
||||||
|
# Random position within the room's inner area
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
x = random.randint(inner_x.start, inner_x.stop - 1)
|
||||||
|
y = random.randint(inner_y.start, inner_y.stop - 1)
|
||||||
|
|
||||||
|
# Check if position is already occupied
|
||||||
|
if get_blocking_entity_at(target_grid, x, y) is not None:
|
||||||
|
continue # Skip this spawn attempt
|
||||||
|
|
||||||
|
# Choose enemy type based on weighted random
|
||||||
|
roll = random.random()
|
||||||
|
if roll < 0.6:
|
||||||
|
enemy_type = "goblin" # 60% chance
|
||||||
|
elif roll < 0.9:
|
||||||
|
enemy_type = "orc" # 30% chance
|
||||||
|
else:
|
||||||
|
enemy_type = "troll" # 10% chance
|
||||||
|
|
||||||
|
spawn_enemy(target_grid, x, y, enemy_type, texture)
|
||||||
|
|
||||||
|
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> mcrfpy.Entity | None:
|
||||||
|
"""Get any entity that blocks movement at the given position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
x: X position to check
|
||||||
|
y: Y position to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The blocking entity, or None if no entity blocks this position
|
||||||
|
"""
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if int(entity.x) == x and int(entity.y) == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Remove all enemies from the grid."""
|
||||||
|
global entity_data
|
||||||
|
|
||||||
|
# Get list of enemies to remove (not the player)
|
||||||
|
enemies_to_remove = []
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity in entity_data and not entity_data[entity].get("is_player", False):
|
||||||
|
enemies_to_remove.append(entity)
|
||||||
|
|
||||||
|
# Remove from grid and entity_data
|
||||||
|
for enemy in enemies_to_remove:
|
||||||
|
# Find and remove from grid.entities
|
||||||
|
for i, e in enumerate(target_grid.entities):
|
||||||
|
if e == enemy:
|
||||||
|
target_grid.entities.remove(i)
|
||||||
|
break
|
||||||
|
# Remove from entity_data
|
||||||
|
if enemy in entity_data:
|
||||||
|
del entity_data[enemy]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Entity Visibility
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Update visibility of all entities based on FOV.
|
||||||
|
|
||||||
|
Entities outside the player's field of view are hidden.
|
||||||
|
"""
|
||||||
|
global player
|
||||||
|
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
# Player is always visible
|
||||||
|
if entity == player:
|
||||||
|
entity.visible = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Other entities are only visible if in FOV
|
||||||
|
ex, ey = int(entity.x), int(entity.y)
|
||||||
|
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Field of View (from Part 4)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||||
|
"""Update the field of view visualization."""
|
||||||
|
# Compute FOV from player position
|
||||||
|
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
|
# Update each tile's visibility
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if target_grid.is_in_fov(x, y):
|
||||||
|
mark_explored(x, y)
|
||||||
|
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||||
|
elif is_explored(x, y):
|
||||||
|
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||||
|
else:
|
||||||
|
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Update entity visibility
|
||||||
|
update_entity_visibility(target_grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Collision Detection
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||||
|
"""Check if a position is valid for movement.
|
||||||
|
|
||||||
|
A position is valid if:
|
||||||
|
1. It is within grid bounds
|
||||||
|
2. The tile is walkable
|
||||||
|
3. No entity is blocking it
|
||||||
|
"""
|
||||||
|
# Check bounds
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check tile walkability
|
||||||
|
if not target_grid.at(x, y).walkable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for blocking entities
|
||||||
|
if get_blocking_entity_at(target_grid, x, y) is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation with Enemies
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple[int, int]:
|
||||||
|
"""Generate a dungeon with rooms, tunnels, and enemies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_grid: The game grid
|
||||||
|
texture: The texture for entity sprites
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The (x, y) coordinates where the player should start
|
||||||
|
"""
|
||||||
|
# Clear any existing enemies
|
||||||
|
clear_enemies(target_grid)
|
||||||
|
|
||||||
|
# Fill with walls
|
||||||
|
fill_with_walls(target_grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(target_grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(target_grid, new_room.center, rooms[-1].center)
|
||||||
|
# Spawn enemies in all rooms except the first (player starting room)
|
||||||
|
spawn_enemies_in_room(target_grid, new_room, texture)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
return rooms[0].center
|
||||||
|
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 560),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate the dungeon (without player first to get starting position)
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Get player starting position
|
||||||
|
if rooms:
|
||||||
|
player_start_x, player_start_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# Add FOV layer
|
||||||
|
fov_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Create the player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Store player data
|
||||||
|
entity_data[player] = {
|
||||||
|
"type": "player",
|
||||||
|
"name": "Player",
|
||||||
|
"hp": 30,
|
||||||
|
"max_hp": 30,
|
||||||
|
"attack": 5,
|
||||||
|
"defense": 2,
|
||||||
|
"is_player": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Now spawn enemies in rooms (except the first one)
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue # Skip player's starting room
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Calculate initial FOV
|
||||||
|
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 5: Placing Enemies"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 660),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
status_display = mcrfpy.Caption(
|
||||||
|
pos=(400, 660),
|
||||||
|
text="Explore the dungeon..."
|
||||||
|
)
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
status_display.font_size = 16
|
||||||
|
scene.children.append(status_display)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def regenerate_dungeon() -> None:
|
||||||
|
"""Generate a new dungeon and reposition the player."""
|
||||||
|
global player, grid, fov_layer, rooms
|
||||||
|
|
||||||
|
# Clear enemies
|
||||||
|
clear_enemies(grid)
|
||||||
|
|
||||||
|
# Regenerate dungeon structure
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Reposition player
|
||||||
|
if rooms:
|
||||||
|
new_x, new_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
|
||||||
|
# Spawn new enemies
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Reset FOV layer
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Update FOV
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
status_display.text = "New dungeon generated!"
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
global player, grid, fov_layer
|
||||||
|
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
elif key == "R":
|
||||||
|
regenerate_dungeon()
|
||||||
|
return
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for blocking entity (potential combat target)
|
||||||
|
blocker = get_blocking_entity_at(grid, new_x, new_y)
|
||||||
|
if blocker is not None and blocker != player:
|
||||||
|
# For now, just report that we bumped into an enemy
|
||||||
|
if blocker in entity_data:
|
||||||
|
enemy_name = entity_data[blocker]["name"]
|
||||||
|
status_display.text = f"A {enemy_name} blocks your path!"
|
||||||
|
status_display.fill_color = mcrfpy.Color(200, 150, 100)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if we can move
|
||||||
|
if can_move_to(grid, new_x, new_y):
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
status_display.text = "Exploring..."
|
||||||
|
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||||
|
|
||||||
|
# Update FOV after movement
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 5 loaded! Enemies lurk in the dungeon...")
|
||||||
940
docs/tutorials/part_06_combat/part_06_combat.py
Normal file
940
docs/tutorials/part_06_combat/part_06_combat.py
Normal file
|
|
@ -0,0 +1,940 @@
|
||||||
|
"""McRogueFace - Part 6: Combat System
|
||||||
|
|
||||||
|
Documentation: https://mcrogueface.github.io/tutorial/part_06_combat
|
||||||
|
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_06_combat/part_06_combat.py
|
||||||
|
|
||||||
|
This code is extracted from the McRogueFace documentation and can be
|
||||||
|
run directly with: ./mcrogueface path/to/this/file.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Sprite indices for CP437 tileset
|
||||||
|
SPRITE_WALL = 35 # '#' - wall
|
||||||
|
SPRITE_FLOOR = 46 # '.' - floor
|
||||||
|
SPRITE_PLAYER = 64 # '@' - player
|
||||||
|
SPRITE_CORPSE = 37 # '%' - remains
|
||||||
|
|
||||||
|
# Enemy sprites
|
||||||
|
SPRITE_GOBLIN = 103 # 'g'
|
||||||
|
SPRITE_ORC = 111 # 'o'
|
||||||
|
SPRITE_TROLL = 116 # 't'
|
||||||
|
|
||||||
|
# Grid dimensions
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 35
|
||||||
|
|
||||||
|
# Room generation parameters
|
||||||
|
ROOM_MIN_SIZE = 6
|
||||||
|
ROOM_MAX_SIZE = 12
|
||||||
|
MAX_ROOMS = 8
|
||||||
|
|
||||||
|
# Enemy spawn parameters
|
||||||
|
MAX_ENEMIES_PER_ROOM = 3
|
||||||
|
|
||||||
|
# FOV settings
|
||||||
|
FOV_RADIUS = 8
|
||||||
|
|
||||||
|
# Visibility colors
|
||||||
|
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||||
|
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
|
||||||
|
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||||
|
|
||||||
|
# Message log settings
|
||||||
|
MAX_MESSAGES = 5
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Fighter Component
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Fighter:
|
||||||
|
"""Combat stats for an entity."""
|
||||||
|
hp: int
|
||||||
|
max_hp: int
|
||||||
|
attack: int
|
||||||
|
defense: int
|
||||||
|
name: str
|
||||||
|
is_player: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self) -> bool:
|
||||||
|
"""Check if this fighter is still alive."""
|
||||||
|
return self.hp > 0
|
||||||
|
|
||||||
|
def take_damage(self, amount: int) -> int:
|
||||||
|
"""Apply damage and return actual damage taken."""
|
||||||
|
actual_damage = min(self.hp, amount)
|
||||||
|
self.hp -= actual_damage
|
||||||
|
return actual_damage
|
||||||
|
|
||||||
|
def heal(self, amount: int) -> int:
|
||||||
|
"""Heal and return actual amount healed."""
|
||||||
|
actual_heal = min(self.max_hp - self.hp, amount)
|
||||||
|
self.hp += actual_heal
|
||||||
|
return actual_heal
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy Templates
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
ENEMY_TEMPLATES = {
|
||||||
|
"goblin": {
|
||||||
|
"sprite": SPRITE_GOBLIN,
|
||||||
|
"hp": 6,
|
||||||
|
"attack": 3,
|
||||||
|
"defense": 0,
|
||||||
|
"color": mcrfpy.Color(100, 200, 100)
|
||||||
|
},
|
||||||
|
"orc": {
|
||||||
|
"sprite": SPRITE_ORC,
|
||||||
|
"hp": 10,
|
||||||
|
"attack": 4,
|
||||||
|
"defense": 1,
|
||||||
|
"color": mcrfpy.Color(100, 150, 100)
|
||||||
|
},
|
||||||
|
"troll": {
|
||||||
|
"sprite": SPRITE_TROLL,
|
||||||
|
"hp": 16,
|
||||||
|
"attack": 6,
|
||||||
|
"defense": 2,
|
||||||
|
"color": mcrfpy.Color(50, 150, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Global State
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Entity data storage
|
||||||
|
entity_data: dict[mcrfpy.Entity, Fighter] = {}
|
||||||
|
|
||||||
|
# Global references
|
||||||
|
player: Optional[mcrfpy.Entity] = None
|
||||||
|
grid: Optional[mcrfpy.Grid] = None
|
||||||
|
fov_layer = None
|
||||||
|
texture: Optional[mcrfpy.Texture] = None
|
||||||
|
|
||||||
|
# Game state
|
||||||
|
game_over: bool = False
|
||||||
|
|
||||||
|
# Message log
|
||||||
|
messages: list[tuple[str, mcrfpy.Color]] = []
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Room Class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size."""
|
||||||
|
|
||||||
|
def __init__(self, x: int, y: int, width: int, height: int):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self) -> tuple[int, int]:
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self) -> tuple[slice, slice]:
|
||||||
|
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||||
|
|
||||||
|
def intersects(self, other: "RectangularRoom") -> bool:
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2 and
|
||||||
|
self.x2 >= other.x1 and
|
||||||
|
self.y1 <= other.y2 and
|
||||||
|
self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exploration Tracking
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
explored: list[list[bool]] = []
|
||||||
|
|
||||||
|
def init_explored() -> None:
|
||||||
|
"""Initialize the explored array to all False."""
|
||||||
|
global explored
|
||||||
|
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||||
|
|
||||||
|
def mark_explored(x: int, y: int) -> None:
|
||||||
|
"""Mark a tile as explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
explored[y][x] = True
|
||||||
|
|
||||||
|
def is_explored(x: int, y: int) -> bool:
|
||||||
|
"""Check if a tile has been explored."""
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
return explored[y][x]
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Message Log
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def add_message(text: str, color: mcrfpy.Color = None) -> None:
|
||||||
|
"""Add a message to the log.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The message text
|
||||||
|
color: Optional color (defaults to white)
|
||||||
|
"""
|
||||||
|
if color is None:
|
||||||
|
color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
|
messages.append((text, color))
|
||||||
|
|
||||||
|
# Keep only the most recent messages
|
||||||
|
while len(messages) > MAX_MESSAGES:
|
||||||
|
messages.pop(0)
|
||||||
|
|
||||||
|
# Update the message display
|
||||||
|
update_message_display()
|
||||||
|
|
||||||
|
def update_message_display() -> None:
|
||||||
|
"""Update the message log UI."""
|
||||||
|
if message_log_caption is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Combine messages into a single string
|
||||||
|
lines = []
|
||||||
|
for text, color in messages:
|
||||||
|
lines.append(text)
|
||||||
|
|
||||||
|
message_log_caption.text = "\n".join(lines)
|
||||||
|
|
||||||
|
def clear_messages() -> None:
|
||||||
|
"""Clear all messages."""
|
||||||
|
global messages
|
||||||
|
messages = []
|
||||||
|
update_message_display()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Dungeon Generation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Fill the entire grid with wall tiles."""
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_WALL
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
|
||||||
|
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||||
|
"""Carve out a room by setting its inner tiles to floor."""
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
for y in range(inner_y.start, inner_y.stop):
|
||||||
|
for x in range(inner_x.start, inner_x.stop):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||||
|
"""Carve a horizontal tunnel."""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||||
|
"""Carve a vertical tunnel."""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||||
|
cell = target_grid.at(x, y)
|
||||||
|
cell.tilesprite = SPRITE_FLOOR
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
|
||||||
|
def carve_l_tunnel(
|
||||||
|
target_grid: mcrfpy.Grid,
|
||||||
|
start: tuple[int, int],
|
||||||
|
end: tuple[int, int]
|
||||||
|
) -> None:
|
||||||
|
"""Carve an L-shaped tunnel between two points."""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y1)
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x2)
|
||||||
|
else:
|
||||||
|
carve_tunnel_vertical(target_grid, y1, y2, x1)
|
||||||
|
carve_tunnel_horizontal(target_grid, x1, x2, y2)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Entity Management
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
|
||||||
|
"""Spawn an enemy at the given position."""
|
||||||
|
template = ENEMY_TEMPLATES[enemy_type]
|
||||||
|
|
||||||
|
enemy = mcrfpy.Entity(
|
||||||
|
grid_pos=(x, y),
|
||||||
|
texture=tex,
|
||||||
|
sprite_index=template["sprite"]
|
||||||
|
)
|
||||||
|
|
||||||
|
enemy.visible = False
|
||||||
|
|
||||||
|
target_grid.entities.append(enemy)
|
||||||
|
|
||||||
|
# Create Fighter component for this enemy
|
||||||
|
entity_data[enemy] = Fighter(
|
||||||
|
hp=template["hp"],
|
||||||
|
max_hp=template["hp"],
|
||||||
|
attack=template["attack"],
|
||||||
|
defense=template["defense"],
|
||||||
|
name=enemy_type.capitalize(),
|
||||||
|
is_player=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return enemy
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
|
||||||
|
"""Spawn random enemies in a room."""
|
||||||
|
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
|
||||||
|
|
||||||
|
for _ in range(num_enemies):
|
||||||
|
inner_x, inner_y = room.inner
|
||||||
|
x = random.randint(inner_x.start, inner_x.stop - 1)
|
||||||
|
y = random.randint(inner_y.start, inner_y.stop - 1)
|
||||||
|
|
||||||
|
if get_entity_at(target_grid, x, y) is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
roll = random.random()
|
||||||
|
if roll < 0.6:
|
||||||
|
enemy_type = "goblin"
|
||||||
|
elif roll < 0.9:
|
||||||
|
enemy_type = "orc"
|
||||||
|
else:
|
||||||
|
enemy_type = "troll"
|
||||||
|
|
||||||
|
spawn_enemy(target_grid, x, y, enemy_type, tex)
|
||||||
|
|
||||||
|
def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
|
||||||
|
"""Get any entity at the given position."""
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if int(entity.x) == x and int(entity.y) == y:
|
||||||
|
# Check if this entity is alive (or is a non-Fighter entity)
|
||||||
|
if entity in entity_data:
|
||||||
|
if entity_data[entity].is_alive:
|
||||||
|
return entity
|
||||||
|
else:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
|
||||||
|
"""Get any living entity that blocks movement at the given position."""
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity == exclude:
|
||||||
|
continue
|
||||||
|
if int(entity.x) == x and int(entity.y) == y:
|
||||||
|
if entity in entity_data and entity_data[entity].is_alive:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||||
|
"""Remove an entity from the grid and data storage."""
|
||||||
|
# Find and remove from grid
|
||||||
|
for i, e in enumerate(target_grid.entities):
|
||||||
|
if e == entity:
|
||||||
|
target_grid.entities.remove(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Remove from entity data
|
||||||
|
if entity in entity_data:
|
||||||
|
del entity_data[entity]
|
||||||
|
|
||||||
|
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Remove all enemies from the grid."""
|
||||||
|
enemies_to_remove = []
|
||||||
|
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity in entity_data and not entity_data[entity].is_player:
|
||||||
|
enemies_to_remove.append(entity)
|
||||||
|
|
||||||
|
for enemy in enemies_to_remove:
|
||||||
|
remove_entity(target_grid, enemy)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Combat System
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
|
||||||
|
"""Calculate damage dealt from attacker to defender.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attacker: The attacking Fighter
|
||||||
|
defender: The defending Fighter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The amount of damage to deal (minimum 0)
|
||||||
|
"""
|
||||||
|
damage = max(0, attacker.attack - defender.defense)
|
||||||
|
return damage
|
||||||
|
|
||||||
|
def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
|
||||||
|
"""Execute an attack from one entity to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attacker_entity: The entity performing the attack
|
||||||
|
defender_entity: The entity being attacked
|
||||||
|
"""
|
||||||
|
global game_over
|
||||||
|
|
||||||
|
attacker = entity_data.get(attacker_entity)
|
||||||
|
defender = entity_data.get(defender_entity)
|
||||||
|
|
||||||
|
if attacker is None or defender is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate and apply damage
|
||||||
|
damage = calculate_damage(attacker, defender)
|
||||||
|
defender.take_damage(damage)
|
||||||
|
|
||||||
|
# Generate combat message
|
||||||
|
if damage > 0:
|
||||||
|
if attacker.is_player:
|
||||||
|
add_message(
|
||||||
|
f"You hit the {defender.name} for {damage} damage!",
|
||||||
|
mcrfpy.Color(200, 200, 200)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
add_message(
|
||||||
|
f"The {attacker.name} hits you for {damage} damage!",
|
||||||
|
mcrfpy.Color(255, 150, 150)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if attacker.is_player:
|
||||||
|
add_message(
|
||||||
|
f"You hit the {defender.name} but deal no damage.",
|
||||||
|
mcrfpy.Color(150, 150, 150)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
add_message(
|
||||||
|
f"The {attacker.name} hits you but deals no damage.",
|
||||||
|
mcrfpy.Color(150, 150, 200)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for death
|
||||||
|
if not defender.is_alive:
|
||||||
|
handle_death(defender_entity, defender)
|
||||||
|
|
||||||
|
def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
|
||||||
|
"""Handle the death of an entity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity: The entity that died
|
||||||
|
fighter: The Fighter component of the dead entity
|
||||||
|
"""
|
||||||
|
global game_over, grid
|
||||||
|
|
||||||
|
if fighter.is_player:
|
||||||
|
# Player death
|
||||||
|
add_message("You have died!", mcrfpy.Color(255, 50, 50))
|
||||||
|
add_message("Press R to restart or Escape to quit.", mcrfpy.Color(200, 200, 200))
|
||||||
|
game_over = True
|
||||||
|
|
||||||
|
# Change player sprite to corpse
|
||||||
|
entity.sprite_index = SPRITE_CORPSE
|
||||||
|
else:
|
||||||
|
# Enemy death
|
||||||
|
add_message(f"The {fighter.name} dies!", mcrfpy.Color(100, 255, 100))
|
||||||
|
|
||||||
|
# Replace with corpse
|
||||||
|
entity.sprite_index = SPRITE_CORPSE
|
||||||
|
|
||||||
|
# Mark as dead (hp is already 0)
|
||||||
|
# Remove blocking but keep visual corpse
|
||||||
|
# Actually remove the entity and its data
|
||||||
|
remove_entity(grid, entity)
|
||||||
|
|
||||||
|
# Update HP display
|
||||||
|
update_hp_display()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Field of View
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
||||||
|
"""Update visibility of all entities based on FOV."""
|
||||||
|
global player
|
||||||
|
|
||||||
|
for entity in target_grid.entities:
|
||||||
|
if entity == player:
|
||||||
|
entity.visible = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
ex, ey = int(entity.x), int(entity.y)
|
||||||
|
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||||
|
|
||||||
|
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||||
|
"""Update the field of view visualization."""
|
||||||
|
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
if target_grid.is_in_fov(x, y):
|
||||||
|
mark_explored(x, y)
|
||||||
|
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||||
|
elif is_explored(x, y):
|
||||||
|
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||||
|
else:
|
||||||
|
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
update_entity_visibility(target_grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Movement and Actions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool:
|
||||||
|
"""Check if a position is valid for movement."""
|
||||||
|
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not target_grid.at(x, y).walkable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover)
|
||||||
|
if blocker is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def try_move_or_attack(dx: int, dy: int) -> None:
|
||||||
|
"""Attempt to move the player or attack if blocked by enemy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dx: Change in X position (-1, 0, or 1)
|
||||||
|
dy: Change in Y position (-1, 0, or 1)
|
||||||
|
"""
|
||||||
|
global player, grid, fov_layer, game_over
|
||||||
|
|
||||||
|
if game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
target_x = px + dx
|
||||||
|
target_y = py + dy
|
||||||
|
|
||||||
|
# Check bounds
|
||||||
|
if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for blocking entity
|
||||||
|
blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
|
||||||
|
|
||||||
|
if blocker is not None:
|
||||||
|
# Attack the blocking entity
|
||||||
|
perform_attack(player, blocker)
|
||||||
|
# After player attacks, enemies take their turn
|
||||||
|
enemy_turn()
|
||||||
|
elif grid.at(target_x, target_y).walkable:
|
||||||
|
# Move to the empty tile
|
||||||
|
player.x = target_x
|
||||||
|
player.y = target_y
|
||||||
|
pos_display.text = f"Position: ({target_x}, {target_y})"
|
||||||
|
|
||||||
|
# Update FOV after movement
|
||||||
|
update_fov(grid, fov_layer, target_x, target_y)
|
||||||
|
|
||||||
|
# Enemies take their turn after player moves
|
||||||
|
enemy_turn()
|
||||||
|
|
||||||
|
# Update HP display
|
||||||
|
update_hp_display()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Enemy AI
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def enemy_turn() -> None:
|
||||||
|
"""Execute enemy actions."""
|
||||||
|
global player, grid, game_over
|
||||||
|
|
||||||
|
if game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
player_x, player_y = int(player.x), int(player.y)
|
||||||
|
|
||||||
|
# Collect enemies that can act
|
||||||
|
enemies = []
|
||||||
|
for entity in grid.entities:
|
||||||
|
if entity == player:
|
||||||
|
continue
|
||||||
|
if entity in entity_data and entity_data[entity].is_alive:
|
||||||
|
enemies.append(entity)
|
||||||
|
|
||||||
|
for enemy in enemies:
|
||||||
|
fighter = entity_data.get(enemy)
|
||||||
|
if fighter is None or not fighter.is_alive:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ex, ey = int(enemy.x), int(enemy.y)
|
||||||
|
|
||||||
|
# Only act if in player's FOV (aware of player)
|
||||||
|
if not grid.is_in_fov(ex, ey):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if adjacent to player
|
||||||
|
dx = player_x - ex
|
||||||
|
dy = player_y - ey
|
||||||
|
|
||||||
|
if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
|
||||||
|
# Adjacent - attack!
|
||||||
|
perform_attack(enemy, player)
|
||||||
|
else:
|
||||||
|
# Not adjacent - try to move toward player
|
||||||
|
move_toward_player(enemy, ex, ey, player_x, player_y)
|
||||||
|
|
||||||
|
def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None:
|
||||||
|
"""Move an enemy one step toward the player.
|
||||||
|
|
||||||
|
Uses simple greedy movement - not true pathfinding.
|
||||||
|
"""
|
||||||
|
global grid
|
||||||
|
|
||||||
|
# Calculate direction to player
|
||||||
|
dx = 0
|
||||||
|
dy = 0
|
||||||
|
|
||||||
|
if px < ex:
|
||||||
|
dx = -1
|
||||||
|
elif px > ex:
|
||||||
|
dx = 1
|
||||||
|
|
||||||
|
if py < ey:
|
||||||
|
dy = -1
|
||||||
|
elif py > ey:
|
||||||
|
dy = 1
|
||||||
|
|
||||||
|
# Try to move in the desired direction
|
||||||
|
# First try the combined direction
|
||||||
|
new_x = ex + dx
|
||||||
|
new_y = ey + dy
|
||||||
|
|
||||||
|
if can_move_to(grid, new_x, new_y, enemy):
|
||||||
|
enemy.x = new_x
|
||||||
|
enemy.y = new_y
|
||||||
|
elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy):
|
||||||
|
# Try horizontal only
|
||||||
|
enemy.x = ex + dx
|
||||||
|
elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
|
||||||
|
# Try vertical only
|
||||||
|
enemy.y = ey + dy
|
||||||
|
# If all fail, enemy stays in place
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Updates
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def update_hp_display() -> None:
|
||||||
|
"""Update the HP display in the UI."""
|
||||||
|
global player
|
||||||
|
|
||||||
|
if hp_display is None or player is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if player in entity_data:
|
||||||
|
fighter = entity_data[player]
|
||||||
|
hp_display.text = f"HP: {fighter.hp}/{fighter.max_hp}"
|
||||||
|
|
||||||
|
# Color based on health percentage
|
||||||
|
hp_percent = fighter.hp / fighter.max_hp
|
||||||
|
if hp_percent > 0.6:
|
||||||
|
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
|
elif hp_percent > 0.3:
|
||||||
|
hp_display.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
else:
|
||||||
|
hp_display.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game Setup
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Create the scene
|
||||||
|
scene = mcrfpy.Scene("game")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the grid
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(800, 480),
|
||||||
|
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
grid.zoom = 1.0
|
||||||
|
|
||||||
|
# Generate initial dungeon structure
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
|
||||||
|
rooms: list[RectangularRoom] = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Get player starting position
|
||||||
|
if rooms:
|
||||||
|
player_start_x, player_start_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# Add FOV layer
|
||||||
|
fov_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Create the player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(player_start_x, player_start_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Create player Fighter component
|
||||||
|
entity_data[player] = Fighter(
|
||||||
|
hp=30,
|
||||||
|
max_hp=30,
|
||||||
|
attack=5,
|
||||||
|
defense=2,
|
||||||
|
name="Player",
|
||||||
|
is_player=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spawn enemies in all rooms except the first
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Calculate initial FOV
|
||||||
|
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||||
|
|
||||||
|
# Add grid to scene
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Elements
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
title = mcrfpy.Caption(
|
||||||
|
pos=(50, 15),
|
||||||
|
text="Part 6: Combat System"
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 24
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
pos=(50, 50),
|
||||||
|
text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit"
|
||||||
|
)
|
||||||
|
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
|
instructions.font_size = 16
|
||||||
|
scene.children.append(instructions)
|
||||||
|
|
||||||
|
# Position display
|
||||||
|
pos_display = mcrfpy.Caption(
|
||||||
|
pos=(50, 580),
|
||||||
|
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||||
|
)
|
||||||
|
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
pos_display.font_size = 16
|
||||||
|
scene.children.append(pos_display)
|
||||||
|
|
||||||
|
# HP display
|
||||||
|
hp_display = mcrfpy.Caption(
|
||||||
|
pos=(300, 580),
|
||||||
|
text="HP: 30/30"
|
||||||
|
)
|
||||||
|
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
|
hp_display.font_size = 16
|
||||||
|
scene.children.append(hp_display)
|
||||||
|
|
||||||
|
# Message log (positioned below the grid)
|
||||||
|
message_log_caption = mcrfpy.Caption(
|
||||||
|
pos=(50, 610),
|
||||||
|
text=""
|
||||||
|
)
|
||||||
|
message_log_caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
message_log_caption.font_size = 14
|
||||||
|
scene.children.append(message_log_caption)
|
||||||
|
|
||||||
|
# Initial message
|
||||||
|
add_message("Welcome to the dungeon! Find and defeat the enemies.", mcrfpy.Color(100, 100, 255))
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def restart_game() -> None:
|
||||||
|
"""Restart the game with a new dungeon."""
|
||||||
|
global player, grid, fov_layer, game_over, entity_data, rooms
|
||||||
|
|
||||||
|
game_over = False
|
||||||
|
|
||||||
|
# Clear all entities and data
|
||||||
|
entity_data.clear()
|
||||||
|
|
||||||
|
# Remove all entities from grid
|
||||||
|
while len(grid.entities) > 0:
|
||||||
|
grid.entities.remove(0)
|
||||||
|
|
||||||
|
# Regenerate dungeon
|
||||||
|
fill_with_walls(grid)
|
||||||
|
init_explored()
|
||||||
|
clear_messages()
|
||||||
|
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
for _ in range(MAX_ROOMS):
|
||||||
|
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||||
|
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||||
|
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
overlaps = False
|
||||||
|
for other_room in rooms:
|
||||||
|
if new_room.intersects(other_room):
|
||||||
|
overlaps = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if overlaps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
carve_room(grid, new_room)
|
||||||
|
|
||||||
|
if rooms:
|
||||||
|
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||||
|
|
||||||
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
# Get new player starting position
|
||||||
|
if rooms:
|
||||||
|
new_x, new_y = rooms[0].center
|
||||||
|
else:
|
||||||
|
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||||
|
|
||||||
|
# Recreate player
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(new_x, new_y),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=SPRITE_PLAYER
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
entity_data[player] = Fighter(
|
||||||
|
hp=30,
|
||||||
|
max_hp=30,
|
||||||
|
attack=5,
|
||||||
|
defense=2,
|
||||||
|
name="Player",
|
||||||
|
is_player=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spawn enemies
|
||||||
|
for i, room in enumerate(rooms):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
spawn_enemies_in_room(grid, room, texture)
|
||||||
|
|
||||||
|
# Reset FOV layer
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||||
|
|
||||||
|
# Update displays
|
||||||
|
update_fov(grid, fov_layer, new_x, new_y)
|
||||||
|
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||||
|
update_hp_display()
|
||||||
|
|
||||||
|
add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255))
|
||||||
|
|
||||||
|
def handle_keys(key: str, action: str) -> None:
|
||||||
|
"""Handle keyboard input."""
|
||||||
|
global game_over
|
||||||
|
|
||||||
|
if action != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle restart
|
||||||
|
if key == "R":
|
||||||
|
restart_game()
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == "Escape":
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore other input if game is over
|
||||||
|
if game_over:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement and attack
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
try_move_or_attack(0, -1)
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
try_move_or_attack(0, 1)
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
try_move_or_attack(-1, 0)
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
try_move_or_attack(1, 0)
|
||||||
|
|
||||||
|
scene.on_key = handle_keys
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start the Game
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
print("Part 6 loaded! Combat is now active. Good luck!")
|
||||||
1035
docs/tutorials/part_07_ui/part_07_ui.py
Normal file
1035
docs/tutorials/part_07_ui/part_07_ui.py
Normal file
File diff suppressed because it is too large
Load diff
1275
docs/tutorials/part_08_items/part_08_items.py
Normal file
1275
docs/tutorials/part_08_items/part_08_items.py
Normal file
File diff suppressed because it is too large
Load diff
1396
docs/tutorials/part_09_ranged/part_09_ranged.py
Normal file
1396
docs/tutorials/part_09_ranged/part_09_ranged.py
Normal file
File diff suppressed because it is too large
Load diff
1565
docs/tutorials/part_10_save_load/part_10_save_load.py
Normal file
1565
docs/tutorials/part_10_save_load/part_10_save_load.py
Normal file
File diff suppressed because it is too large
Load diff
1735
docs/tutorials/part_11_levels/part_11_levels.py
Normal file
1735
docs/tutorials/part_11_levels/part_11_levels.py
Normal file
File diff suppressed because it is too large
Load diff
1850
docs/tutorials/part_12_experience/part_12_experience.py
Normal file
1850
docs/tutorials/part_12_experience/part_12_experience.py
Normal file
File diff suppressed because it is too large
Load diff
1798
docs/tutorials/part_13_equipment/part_13_equipment.py
Normal file
1798
docs/tutorials/part_13_equipment/part_13_equipment.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -53,7 +53,41 @@ namespace mcrfpydef {
|
||||||
.tp_dealloc = (destructor)PySceneClass::__dealloc,
|
.tp_dealloc = (destructor)PySceneClass::__dealloc,
|
||||||
.tp_repr = (reprfunc)PySceneClass::__repr__,
|
.tp_repr = (reprfunc)PySceneClass::__repr__,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing
|
||||||
.tp_doc = PyDoc_STR("Base class for object-oriented scenes"),
|
.tp_doc = PyDoc_STR(
|
||||||
|
"Scene(name: str)\n\n"
|
||||||
|
"Object-oriented scene management with lifecycle callbacks.\n\n"
|
||||||
|
"This is the recommended approach for scene management, replacing module-level\n"
|
||||||
|
"functions like createScene(), setScene(), and sceneUI(). Key advantage: you can\n"
|
||||||
|
"set on_key handlers on ANY scene, not just the currently active one.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" name: Unique identifier for this scene. Used for scene transitions.\n\n"
|
||||||
|
"Properties:\n"
|
||||||
|
" name (str, read-only): Scene's unique identifier.\n"
|
||||||
|
" active (bool, read-only): Whether this scene is currently displayed.\n"
|
||||||
|
" children (UICollection, read-only): UI elements in this scene. Modify to add/remove elements.\n"
|
||||||
|
" on_key (callable): Keyboard handler. Set on ANY scene, regardless of which is active!\n"
|
||||||
|
" pos (Vector): Position offset for all UI elements.\n"
|
||||||
|
" visible (bool): Whether the scene renders.\n"
|
||||||
|
" opacity (float): Scene transparency (0.0-1.0).\n\n"
|
||||||
|
"Lifecycle Callbacks (override in subclass):\n"
|
||||||
|
" on_enter(): Called when scene becomes active via activate().\n"
|
||||||
|
" on_exit(): Called when scene is deactivated (another scene activates).\n"
|
||||||
|
" on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.\n"
|
||||||
|
" update(dt: float): Called every frame with delta time in seconds.\n"
|
||||||
|
" on_resize(width: int, height: int): Called when window is resized.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" # Basic usage (replacing module functions):\n"
|
||||||
|
" scene = mcrfpy.Scene('main_menu')\n"
|
||||||
|
" scene.children.append(mcrfpy.Caption(text='Welcome', pos=(100, 100)))\n"
|
||||||
|
" scene.on_key = lambda key, action: print(f'Key: {key}')\n"
|
||||||
|
" scene.activate() # Switch to this scene\n\n"
|
||||||
|
" # Subclassing for lifecycle:\n"
|
||||||
|
" class GameScene(mcrfpy.Scene):\n"
|
||||||
|
" def on_enter(self):\n"
|
||||||
|
" print('Game started!')\n"
|
||||||
|
" def update(self, dt):\n"
|
||||||
|
" self.player.move(dt)\n"
|
||||||
|
),
|
||||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
.tp_methods = nullptr, // Set in McRFPy_API.cpp
|
||||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
.tp_getset = nullptr, // Set in McRFPy_API.cpp
|
||||||
.tp_init = (initproc)PySceneClass::__init__,
|
.tp_init = (initproc)PySceneClass::__init__,
|
||||||
|
|
|
||||||
103
src/UIGrid.cpp
103
src/UIGrid.cpp
|
|
@ -6,8 +6,9 @@
|
||||||
#include "Profiler.h"
|
#include "Profiler.h"
|
||||||
#include "PyFOV.h"
|
#include "PyFOV.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath> // #142 - for std::floor
|
#include <cmath> // #142 - for std::floor, std::isnan
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
#include <limits> // #169 - for std::numeric_limits
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIGrid::UIGrid()
|
UIGrid::UIGrid()
|
||||||
|
|
@ -735,7 +736,9 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
PyObject* fill_color = nullptr;
|
PyObject* fill_color = nullptr;
|
||||||
PyObject* click_handler = nullptr;
|
PyObject* click_handler = nullptr;
|
||||||
PyObject* layers_obj = nullptr; // #150 - layers dict
|
PyObject* layers_obj = nullptr; // #150 - layers dict
|
||||||
float center_x = 0.0f, center_y = 0.0f;
|
// #169 - Use NaN as sentinel to detect if user provided center values
|
||||||
|
float center_x = std::numeric_limits<float>::quiet_NaN();
|
||||||
|
float center_y = std::numeric_limits<float>::quiet_NaN();
|
||||||
float zoom = 1.0f;
|
float zoom = 1.0f;
|
||||||
// perspective is now handled via properties, not init args
|
// perspective is now handled via properties, not init args
|
||||||
int visible = 1;
|
int visible = 1;
|
||||||
|
|
@ -862,9 +865,19 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
||||||
|
|
||||||
// Set additional properties
|
// Set additional properties
|
||||||
|
self->data->zoom = zoom; // Set zoom first, needed for default center calculation
|
||||||
|
|
||||||
|
// #169 - Calculate default center if not provided by user
|
||||||
|
// Default: tile (0,0) at top-left of widget
|
||||||
|
if (std::isnan(center_x)) {
|
||||||
|
// Center = half widget size (in pixels), so tile 0,0 appears at top-left
|
||||||
|
center_x = w / (2.0f * zoom);
|
||||||
|
}
|
||||||
|
if (std::isnan(center_y)) {
|
||||||
|
center_y = h / (2.0f * zoom);
|
||||||
|
}
|
||||||
self->data->center_x = center_x;
|
self->data->center_x = center_x;
|
||||||
self->data->center_y = center_y;
|
self->data->center_y = center_y;
|
||||||
self->data->zoom = zoom;
|
|
||||||
// perspective is now handled by perspective_entity and perspective_enabled
|
// perspective is now handled by perspective_entity and perspective_enabled
|
||||||
// self->data->perspective = perspective;
|
// self->data->perspective = perspective;
|
||||||
self->data->visible = visible;
|
self->data->visible = visible;
|
||||||
|
|
@ -1730,6 +1743,72 @@ PyObject* UIGrid::py_entities_in_radius(PyUIGridObject* self, PyObject* args, Py
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #169 - center_camera implementations
|
||||||
|
void UIGrid::center_camera() {
|
||||||
|
// Center on grid's middle tile
|
||||||
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
center_x = (grid_x / 2.0f) * cell_width;
|
||||||
|
center_y = (grid_y / 2.0f) * cell_height;
|
||||||
|
markDirty(); // #144 - View change affects content
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGrid::center_camera(float tile_x, float tile_y) {
|
||||||
|
// Position specified tile at top-left of widget
|
||||||
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
// To put tile (tx, ty) at top-left: center = tile_pos + half_viewport
|
||||||
|
float half_viewport_x = box.getSize().x / zoom / 2.0f;
|
||||||
|
float half_viewport_y = box.getSize().y / zoom / 2.0f;
|
||||||
|
center_x = tile_x * cell_width + half_viewport_x;
|
||||||
|
center_y = tile_y * cell_height + half_viewport_y;
|
||||||
|
markDirty(); // #144 - View change affects content
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
|
||||||
|
PyObject* pos_arg = nullptr;
|
||||||
|
|
||||||
|
// Parse optional positional argument (tuple of tile coordinates)
|
||||||
|
if (!PyArg_ParseTuple(args, "|O", &pos_arg)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos_arg == nullptr || pos_arg == Py_None) {
|
||||||
|
// No args: center on grid's middle tile
|
||||||
|
self->data->center_camera();
|
||||||
|
} else if (PyTuple_Check(pos_arg) && PyTuple_Size(pos_arg) == 2) {
|
||||||
|
// Tuple provided: center on (tile_x, tile_y)
|
||||||
|
PyObject* x_obj = PyTuple_GetItem(pos_arg, 0);
|
||||||
|
PyObject* y_obj = PyTuple_GetItem(pos_arg, 1);
|
||||||
|
|
||||||
|
float tile_x, tile_y;
|
||||||
|
if (PyFloat_Check(x_obj)) {
|
||||||
|
tile_x = PyFloat_AsDouble(x_obj);
|
||||||
|
} else if (PyLong_Check(x_obj)) {
|
||||||
|
tile_x = (float)PyLong_AsLong(x_obj);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PyFloat_Check(y_obj)) {
|
||||||
|
tile_y = PyFloat_AsDouble(y_obj);
|
||||||
|
} else if (PyLong_Check(y_obj)) {
|
||||||
|
tile_y = (float)PyLong_AsLong(y_obj);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "tile coordinates must be numeric");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->center_camera(tile_x, tile_y);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "center_camera() takes an optional tuple (tile_x, tile_y)");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
PyMethodDef UIGrid::methods[] = {
|
PyMethodDef UIGrid::methods[] = {
|
||||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
|
@ -1818,6 +1897,15 @@ PyMethodDef UIGrid::methods[] = {
|
||||||
" radius: Search radius\n\n"
|
" radius: Search radius\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" List of Entity objects within the radius."},
|
" List of Entity objects within the radius."},
|
||||||
|
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
|
||||||
|
"center_camera(pos: tuple = None) -> None\n\n"
|
||||||
|
"Center the camera on a tile coordinate.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" grid.center_camera() # Center on middle of grid\n"
|
||||||
|
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
||||||
|
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
||||||
{NULL, NULL, 0, NULL}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1929,6 +2017,15 @@ PyMethodDef UIGrid_all_methods[] = {
|
||||||
" radius: Search radius\n\n"
|
" radius: Search radius\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" List of Entity objects within the radius."},
|
" List of Entity objects within the radius."},
|
||||||
|
{"center_camera", (PyCFunction)UIGrid::py_center_camera, METH_VARARGS,
|
||||||
|
"center_camera(pos: tuple = None) -> None\n\n"
|
||||||
|
"Center the camera on a tile coordinate.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Optional (tile_x, tile_y) tuple. If None, centers on grid's middle tile.\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" grid.center_camera() # Center on middle of grid\n"
|
||||||
|
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
|
||||||
|
" grid.center_camera((0, 0)) # Center on tile (0, 0)"},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,12 @@ public:
|
||||||
static PyObject* py_get_dijkstra_path(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 PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
||||||
|
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
||||||
|
|
||||||
|
// #169 - Camera positioning
|
||||||
|
void center_camera(); // Center on grid's middle tile
|
||||||
|
void center_camera(float tile_x, float tile_y); // Center on specific tile
|
||||||
|
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
static PyObject* get_entities(PyUIGridObject* self, void* closure);
|
static PyObject* get_entities(PyUIGridObject* self, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,15 @@ grid = mcrfpy.Grid(
|
||||||
size=(1024, 768)
|
size=(1024, 768)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add color layer for floor pattern
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Simple floor pattern
|
# Simple floor pattern
|
||||||
for x in range(100):
|
for x in range(100):
|
||||||
for y in range(100):
|
for y in range(100):
|
||||||
cell = grid.at((x, y))
|
cell = grid.at(x, y)
|
||||||
cell.tilesprite = 0
|
cell.tilesprite = 0
|
||||||
cell.color = (40, 40, 40, 255)
|
color_layer.set(x, y, mcrfpy.Color(40, 40, 40, 255))
|
||||||
|
|
||||||
# Create 50 entities with random positions and velocities
|
# Create 50 entities with random positions and velocities
|
||||||
entities = []
|
entities = []
|
||||||
|
|
@ -47,15 +50,15 @@ ENTITY_COUNT = 50
|
||||||
|
|
||||||
for i in range(ENTITY_COUNT):
|
for i in range(ENTITY_COUNT):
|
||||||
entity = mcrfpy.Entity(
|
entity = mcrfpy.Entity(
|
||||||
grid_pos=(random.randint(0, 99), random.randint(0, 99)),
|
(random.randint(0, 99), random.randint(0, 99)),
|
||||||
sprite_index=random.randint(10, 20) # Use varied sprites
|
sprite_index=random.randint(10, 20), # Use varied sprites
|
||||||
|
grid=grid
|
||||||
)
|
)
|
||||||
|
|
||||||
# Give each entity a random velocity
|
# Give each entity a random velocity (stored as Python attributes)
|
||||||
entity.velocity_x = random.uniform(-0.5, 0.5)
|
entity.velocity_x = random.uniform(-0.5, 0.5)
|
||||||
entity.velocity_y = random.uniform(-0.5, 0.5)
|
entity.velocity_y = random.uniform(-0.5, 0.5)
|
||||||
|
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
|
||||||
|
|
@ -282,23 +282,23 @@ def setup_grid_stress():
|
||||||
grid.center = (400, 400) # Center view
|
grid.center = (400, 400) # Center view
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
# Fill with alternating colors
|
# Add color layer and fill with alternating colors
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
for y in range(50):
|
for y in range(50):
|
||||||
for x in range(50):
|
for x in range(50):
|
||||||
cell = grid.at(x, y)
|
|
||||||
if (x + y) % 2 == 0:
|
if (x + y) % 2 == 0:
|
||||||
cell.color = mcrfpy.Color(60, 60, 80)
|
color_layer.set(x, y, mcrfpy.Color(60, 60, 80))
|
||||||
else:
|
else:
|
||||||
cell.color = mcrfpy.Color(40, 40, 60)
|
color_layer.set(x, y, mcrfpy.Color(40, 40, 60))
|
||||||
|
|
||||||
# Add 50 entities
|
# Add 50 entities
|
||||||
try:
|
try:
|
||||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
for i in range(50):
|
for i in range(50):
|
||||||
# Entity takes positional args: (position, texture, sprite_index, grid)
|
# Entity takes tuple position and keyword args
|
||||||
pos = mcrfpy.Vector(random.randint(5, 45), random.randint(5, 45))
|
pos = (random.randint(5, 45), random.randint(5, 45))
|
||||||
entity = mcrfpy.Entity(pos, texture, random.randint(0, 100), grid)
|
entity = mcrfpy.Entity(pos, texture=texture, sprite_index=random.randint(0, 100), grid=grid)
|
||||||
grid.entities.append(entity)
|
grid.entities.append(entity)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" Note: Could not create entities: {e}")
|
print(f" Note: Could not create entities: {e}")
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,14 @@ Uses C++ benchmark logger (start_benchmark/end_benchmark) for accurate timing.
|
||||||
Results written to JSON files for analysis.
|
Results written to JSON files for analysis.
|
||||||
|
|
||||||
Compares rendering performance between:
|
Compares rendering performance between:
|
||||||
1. Traditional grid.at(x,y).color API (no caching)
|
1. ColorLayer with per-cell modifications (no caching benefit)
|
||||||
2. New layer system with dirty flag caching
|
2. ColorLayer with dirty flag caching (static after fill)
|
||||||
3. Various layer configurations
|
3. Various layer configurations
|
||||||
|
|
||||||
|
NOTE: The old grid.at(x,y).color API no longer exists. All color operations
|
||||||
|
now go through the ColorLayer system. This benchmark compares different
|
||||||
|
layer usage patterns to measure caching effectiveness.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
./mcrogueface --exec tests/benchmarks/layer_performance_test.py
|
./mcrogueface --exec tests/benchmarks/layer_performance_test.py
|
||||||
# Results in benchmark_*.json files
|
# Results in benchmark_*.json files
|
||||||
|
|
@ -94,7 +98,7 @@ def run_next_test():
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def setup_base_layer_static():
|
def setup_base_layer_static():
|
||||||
"""Traditional grid.at(x,y).color API - no modifications during render."""
|
"""ColorLayer with per-cell set() calls - static after initial fill."""
|
||||||
mcrfpy.createScene("test_base_static")
|
mcrfpy.createScene("test_base_static")
|
||||||
ui = mcrfpy.sceneUI("test_base_static")
|
ui = mcrfpy.sceneUI("test_base_static")
|
||||||
|
|
||||||
|
|
@ -102,17 +106,17 @@ def setup_base_layer_static():
|
||||||
pos=(10, 10), size=(600, 600))
|
pos=(10, 10), size=(600, 600))
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
# Fill base layer using traditional API
|
# Fill using ColorLayer with per-cell set() calls (baseline)
|
||||||
|
layer = grid.add_layer("color", z_index=-1)
|
||||||
for y in range(GRID_SIZE):
|
for y in range(GRID_SIZE):
|
||||||
for x in range(GRID_SIZE):
|
for x in range(GRID_SIZE):
|
||||||
cell = grid.at(x, y)
|
layer.set(x, y, mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255))
|
||||||
cell.color = mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255)
|
|
||||||
|
|
||||||
mcrfpy.setScene("test_base_static")
|
mcrfpy.setScene("test_base_static")
|
||||||
|
|
||||||
|
|
||||||
def setup_base_layer_modified():
|
def setup_base_layer_modified():
|
||||||
"""Traditional API with single cell modified each frame."""
|
"""ColorLayer with single cell modified each frame - tests dirty flag."""
|
||||||
mcrfpy.createScene("test_base_mod")
|
mcrfpy.createScene("test_base_mod")
|
||||||
ui = mcrfpy.sceneUI("test_base_mod")
|
ui = mcrfpy.sceneUI("test_base_mod")
|
||||||
|
|
||||||
|
|
@ -120,19 +124,16 @@ def setup_base_layer_modified():
|
||||||
pos=(10, 10), size=(600, 600))
|
pos=(10, 10), size=(600, 600))
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
# Fill base layer
|
# Fill using ColorLayer
|
||||||
for y in range(GRID_SIZE):
|
layer = grid.add_layer("color", z_index=-1)
|
||||||
for x in range(GRID_SIZE):
|
layer.fill(mcrfpy.Color(100, 100, 100, 255))
|
||||||
cell = grid.at(x, y)
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100, 255)
|
|
||||||
|
|
||||||
# Timer to modify one cell per frame
|
# Timer to modify one cell per frame (triggers dirty flag each frame)
|
||||||
mod_counter = [0]
|
mod_counter = [0]
|
||||||
def modify_cell(runtime):
|
def modify_cell(runtime):
|
||||||
x = mod_counter[0] % GRID_SIZE
|
x = mod_counter[0] % GRID_SIZE
|
||||||
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
|
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
|
||||||
cell = grid.at(x, y)
|
layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
|
||||||
cell.color = mcrfpy.Color(255, 0, 0, 255)
|
|
||||||
mod_counter[0] += 1
|
mod_counter[0] += 1
|
||||||
|
|
||||||
mcrfpy.setScene("test_base_mod")
|
mcrfpy.setScene("test_base_mod")
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,30 @@ END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end
|
||||||
|
|
||||||
# Global state
|
# Global state
|
||||||
grid = None
|
grid = None
|
||||||
|
color_layer = None
|
||||||
mode = "ASTAR"
|
mode = "ASTAR"
|
||||||
start_pos = (5, 10)
|
start_pos = (5, 10)
|
||||||
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
|
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
|
||||||
|
|
||||||
def create_map():
|
def create_map():
|
||||||
"""Create a map with obstacles to show pathfinding differences"""
|
"""Create a map with obstacles to show pathfinding differences"""
|
||||||
global grid
|
global grid, color_layer
|
||||||
|
|
||||||
mcrfpy.createScene("pathfinding_comparison")
|
mcrfpy.createScene("pathfinding_comparison")
|
||||||
|
|
||||||
# Create grid
|
# Create grid
|
||||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Initialize all as floor
|
# Initialize all as floor
|
||||||
for y in range(20):
|
for y in range(20):
|
||||||
for x in range(30):
|
for x in range(30):
|
||||||
grid.at(x, y).walkable = True
|
grid.at(x, y).walkable = True
|
||||||
grid.at(x, y).color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
# Create obstacles that make A* and Dijkstra differ
|
# Create obstacles that make A* and Dijkstra differ
|
||||||
obstacles = [
|
obstacles = [
|
||||||
# Vertical wall with gaps
|
# Vertical wall with gaps
|
||||||
|
|
@ -50,15 +54,15 @@ def create_map():
|
||||||
[(x, 10) for x in range(20, 25)],
|
[(x, 10) for x in range(20, 25)],
|
||||||
[(25, y) for y in range(5, 15)],
|
[(25, y) for y in range(5, 15)],
|
||||||
]
|
]
|
||||||
|
|
||||||
for obstacle_group in obstacles:
|
for obstacle_group in obstacles:
|
||||||
for x, y in obstacle_group:
|
for x, y in obstacle_group:
|
||||||
grid.at(x, y).walkable = False
|
grid.at(x, y).walkable = False
|
||||||
grid.at(x, y).color = WALL_COLOR
|
color_layer.set(x, y, WALL_COLOR)
|
||||||
|
|
||||||
# Mark start and end
|
# Mark start and end
|
||||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
color_layer.set(start_pos[0], start_pos[1], START_COLOR)
|
||||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
color_layer.set(end_pos[0], end_pos[1], END_COLOR)
|
||||||
|
|
||||||
def clear_paths():
|
def clear_paths():
|
||||||
"""Clear path highlighting"""
|
"""Clear path highlighting"""
|
||||||
|
|
@ -66,34 +70,34 @@ def clear_paths():
|
||||||
for x in range(30):
|
for x in range(30):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
# Restore start and end colors
|
# Restore start and end colors
|
||||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
color_layer.set(start_pos[0], start_pos[1], START_COLOR)
|
||||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
color_layer.set(end_pos[0], end_pos[1], END_COLOR)
|
||||||
|
|
||||||
def show_astar():
|
def show_astar():
|
||||||
"""Show A* path"""
|
"""Show A* path"""
|
||||||
clear_paths()
|
clear_paths()
|
||||||
|
|
||||||
# Compute A* path
|
# Compute A* path
|
||||||
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||||
|
|
||||||
# Color the path
|
# Color the path
|
||||||
for i, (x, y) in enumerate(path):
|
for i, (x, y) in enumerate(path):
|
||||||
if (x, y) != start_pos and (x, y) != end_pos:
|
if (x, y) != start_pos and (x, y) != end_pos:
|
||||||
grid.at(x, y).color = ASTAR_COLOR
|
color_layer.set(x, y, ASTAR_COLOR)
|
||||||
|
|
||||||
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
|
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
|
||||||
status_text.fill_color = ASTAR_COLOR
|
status_text.fill_color = ASTAR_COLOR
|
||||||
|
|
||||||
def show_dijkstra():
|
def show_dijkstra():
|
||||||
"""Show Dijkstra exploration"""
|
"""Show Dijkstra exploration"""
|
||||||
clear_paths()
|
clear_paths()
|
||||||
|
|
||||||
# Compute Dijkstra from start
|
# Compute Dijkstra from start
|
||||||
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
||||||
|
|
||||||
# Color cells by distance (showing exploration)
|
# Color cells by distance (showing exploration)
|
||||||
max_dist = 40.0
|
max_dist = 40.0
|
||||||
for y in range(20):
|
for y in range(20):
|
||||||
|
|
@ -103,50 +107,50 @@ def show_dijkstra():
|
||||||
if dist is not None and dist < max_dist:
|
if dist is not None and dist < max_dist:
|
||||||
# Color based on distance
|
# Color based on distance
|
||||||
intensity = int(255 * (1 - dist / max_dist))
|
intensity = int(255 * (1 - dist / max_dist))
|
||||||
grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity)
|
color_layer.set(x, y, mcrfpy.Color(0, intensity // 2, intensity))
|
||||||
|
|
||||||
# Get the actual path
|
# Get the actual path
|
||||||
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
||||||
|
|
||||||
# Highlight the actual path more brightly
|
# Highlight the actual path more brightly
|
||||||
for x, y in path:
|
for x, y in path:
|
||||||
if (x, y) != start_pos and (x, y) != end_pos:
|
if (x, y) != start_pos and (x, y) != end_pos:
|
||||||
grid.at(x, y).color = DIJKSTRA_COLOR
|
color_layer.set(x, y, DIJKSTRA_COLOR)
|
||||||
|
|
||||||
# Restore start and end
|
# Restore start and end
|
||||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
color_layer.set(start_pos[0], start_pos[1], START_COLOR)
|
||||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
color_layer.set(end_pos[0], end_pos[1], END_COLOR)
|
||||||
|
|
||||||
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
|
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
|
||||||
status_text.fill_color = DIJKSTRA_COLOR
|
status_text.fill_color = DIJKSTRA_COLOR
|
||||||
|
|
||||||
def show_both():
|
def show_both():
|
||||||
"""Show both paths overlaid"""
|
"""Show both paths overlaid"""
|
||||||
clear_paths()
|
clear_paths()
|
||||||
|
|
||||||
# Get both paths
|
# Get both paths
|
||||||
astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
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])
|
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
||||||
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
||||||
|
|
||||||
print(astar_path, dijkstra_path)
|
print(astar_path, dijkstra_path)
|
||||||
|
|
||||||
# Color Dijkstra path first (blue)
|
# Color Dijkstra path first (blue)
|
||||||
for x, y in dijkstra_path:
|
for x, y in dijkstra_path:
|
||||||
if (x, y) != start_pos and (x, y) != end_pos:
|
if (x, y) != start_pos and (x, y) != end_pos:
|
||||||
grid.at(x, y).color = DIJKSTRA_COLOR
|
color_layer.set(x, y, DIJKSTRA_COLOR)
|
||||||
|
|
||||||
# Then A* path (green) - will overwrite shared cells
|
# Then A* path (green) - will overwrite shared cells
|
||||||
for x, y in astar_path:
|
for x, y in astar_path:
|
||||||
if (x, y) != start_pos and (x, y) != end_pos:
|
if (x, y) != start_pos and (x, y) != end_pos:
|
||||||
grid.at(x, y).color = ASTAR_COLOR
|
color_layer.set(x, y, ASTAR_COLOR)
|
||||||
|
|
||||||
# Mark differences
|
# Mark differences
|
||||||
different_cells = []
|
different_cells = []
|
||||||
for cell in dijkstra_path:
|
for cell in dijkstra_path:
|
||||||
if cell not in astar_path:
|
if cell not in astar_path:
|
||||||
different_cells.append(cell)
|
different_cells.append(cell)
|
||||||
|
|
||||||
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
|
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
|
||||||
if different_cells:
|
if different_cells:
|
||||||
info_text.text = f"Paths differ at {len(different_cells)} cells"
|
info_text.text = f"Paths differ at {len(different_cells)} cells"
|
||||||
|
|
@ -202,26 +206,26 @@ grid.size = (600, 400) # 30*20, 20*20
|
||||||
grid.position = (100, 100)
|
grid.position = (100, 100)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20)
|
title = mcrfpy.Caption(pos=(250, 20), text="A* vs Dijkstra Pathfinding")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add status
|
# Add status
|
||||||
status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60)
|
status_text = mcrfpy.Caption(pos=(100, 60), text="Press A for A*, D for Dijkstra, B for Both")
|
||||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(status_text)
|
ui.append(status_text)
|
||||||
|
|
||||||
# Add info
|
# Add info
|
||||||
info_text = mcrfpy.Caption("", 100, 520)
|
info_text = mcrfpy.Caption(pos=(100, 520), text="")
|
||||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(info_text)
|
ui.append(info_text)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540)
|
legend1 = mcrfpy.Caption(pos=(100, 540), text="Red=Start, Yellow=End, Green=A*, Blue=Dijkstra")
|
||||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend1)
|
ui.append(legend1)
|
||||||
|
|
||||||
legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560)
|
legend2 = mcrfpy.Caption(pos=(100, 560), text="Dark=Walls, Light=Floor")
|
||||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend2)
|
ui.append(legend2)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ for y in range(5):
|
||||||
|
|
||||||
# Create entity
|
# Create entity
|
||||||
print("Creating entity...")
|
print("Creating entity...")
|
||||||
entity = mcrfpy.Entity(2, 2)
|
entity = mcrfpy.Entity((2, 2), grid=grid)
|
||||||
entity.sprite_index = 64
|
entity.sprite_index = 64
|
||||||
grid.entities.append(entity)
|
|
||||||
print(f"Entity at ({entity.x}, {entity.y})")
|
print(f"Entity at ({entity.x}, {entity.y})")
|
||||||
|
|
||||||
# Check gridstate
|
# Check gridstate
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable
|
||||||
|
|
||||||
# Global state
|
# Global state
|
||||||
grid = None
|
grid = None
|
||||||
|
color_layer = None
|
||||||
entities = []
|
entities = []
|
||||||
current_combo_index = 0
|
current_combo_index = 0
|
||||||
all_combinations = [] # All possible pairs
|
all_combinations = [] # All possible pairs
|
||||||
|
|
@ -27,14 +28,17 @@ current_path = []
|
||||||
|
|
||||||
def create_map():
|
def create_map():
|
||||||
"""Create the map with entities"""
|
"""Create the map with entities"""
|
||||||
global grid, entities, all_combinations
|
global grid, color_layer, entities, all_combinations
|
||||||
|
|
||||||
mcrfpy.createScene("dijkstra_all")
|
mcrfpy.createScene("dijkstra_all")
|
||||||
|
|
||||||
# Create grid
|
# Create grid
|
||||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Map layout - Entity 1 is intentionally trapped!
|
# Map layout - Entity 1 is intentionally trapped!
|
||||||
map_layout = [
|
map_layout = [
|
||||||
"..............", # Row 0
|
"..............", # Row 0
|
||||||
|
|
@ -48,29 +52,28 @@ def create_map():
|
||||||
"..W.WWW.......", # Row 8
|
"..W.WWW.......", # Row 8
|
||||||
"..............", # Row 9
|
"..............", # Row 9
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create the map
|
# Create the map
|
||||||
entity_positions = []
|
entity_positions = []
|
||||||
for y, row in enumerate(map_layout):
|
for y, row in enumerate(map_layout):
|
||||||
for x, char in enumerate(row):
|
for x, char in enumerate(row):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
if char == 'W':
|
if char == 'W':
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.color = WALL_COLOR
|
color_layer.set(x, y, WALL_COLOR)
|
||||||
else:
|
else:
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
if char == 'E':
|
if char == 'E':
|
||||||
entity_positions.append((x, y))
|
entity_positions.append((x, y))
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
entities = []
|
entities = []
|
||||||
for i, (x, y) in enumerate(entity_positions):
|
for i, (x, y) in enumerate(entity_positions):
|
||||||
entity = mcrfpy.Entity(x, y)
|
entity = mcrfpy.Entity((x, y), grid=grid)
|
||||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
print("Map Analysis:")
|
print("Map Analysis:")
|
||||||
|
|
@ -90,47 +93,47 @@ def create_map():
|
||||||
def clear_path_colors():
|
def clear_path_colors():
|
||||||
"""Reset all floor tiles to original color"""
|
"""Reset all floor tiles to original color"""
|
||||||
global current_path
|
global current_path
|
||||||
|
|
||||||
for y in range(grid.grid_y):
|
for y in range(grid.grid_y):
|
||||||
for x in range(grid.grid_x):
|
for x in range(grid.grid_x):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
current_path = []
|
current_path = []
|
||||||
|
|
||||||
def show_combination(index):
|
def show_combination(index):
|
||||||
"""Show a specific path combination (valid or invalid)"""
|
"""Show a specific path combination (valid or invalid)"""
|
||||||
global current_combo_index, current_path
|
global current_combo_index, current_path
|
||||||
|
|
||||||
current_combo_index = index % len(all_combinations)
|
current_combo_index = index % len(all_combinations)
|
||||||
from_idx, to_idx = all_combinations[current_combo_index]
|
from_idx, to_idx = all_combinations[current_combo_index]
|
||||||
|
|
||||||
# Clear previous path
|
# Clear previous path
|
||||||
clear_path_colors()
|
clear_path_colors()
|
||||||
|
|
||||||
# Get entities
|
# Get entities
|
||||||
e_from = entities[from_idx]
|
e_from = entities[from_idx]
|
||||||
e_to = entities[to_idx]
|
e_to = entities[to_idx]
|
||||||
|
|
||||||
# Calculate path
|
# Calculate path
|
||||||
path = e_from.path_to(int(e_to.x), int(e_to.y))
|
path = e_from.path_to(int(e_to.x), int(e_to.y))
|
||||||
current_path = path if path else []
|
current_path = path if path else []
|
||||||
|
|
||||||
# Always color start and end positions
|
# Always color start and end positions
|
||||||
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
|
color_layer.set(int(e_from.x), int(e_from.y), START_COLOR)
|
||||||
grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR
|
color_layer.set(int(e_to.x), int(e_to.y), NO_PATH_COLOR if not path else END_COLOR)
|
||||||
|
|
||||||
# Color the path if it exists
|
# Color the path if it exists
|
||||||
if path:
|
if path:
|
||||||
# Color intermediate steps
|
# Color intermediate steps
|
||||||
for i, (x, y) in enumerate(path):
|
for i, (x, y) in enumerate(path):
|
||||||
if i > 0 and i < len(path) - 1:
|
if i > 0 and i < len(path) - 1:
|
||||||
grid.at(x, y).color = PATH_COLOR
|
color_layer.set(x, y, 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.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
|
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid
|
||||||
|
|
||||||
# Show path steps
|
# Show path steps
|
||||||
path_display = []
|
path_display = []
|
||||||
for i, (x, y) in enumerate(path[:5]):
|
for i, (x, y) in enumerate(path[:5]):
|
||||||
|
|
@ -142,7 +145,7 @@ def show_combination(index):
|
||||||
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!"
|
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
|
status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid
|
||||||
path_text.text = "Path: [] (No valid path exists)"
|
path_text.text = "Path: [] (No valid path exists)"
|
||||||
|
|
||||||
# Update info
|
# 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)})"
|
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)})"
|
||||||
|
|
||||||
|
|
@ -183,37 +186,37 @@ grid.size = (560, 400)
|
||||||
grid.position = (120, 100)
|
grid.position = (120, 100)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20)
|
title = mcrfpy.Caption(pos=(200, 20), text="Dijkstra - All Paths (Valid & Invalid)")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add status (will change color based on validity)
|
# Add status (will change color based on validity)
|
||||||
status_text = mcrfpy.Caption("Ready", 120, 60)
|
status_text = mcrfpy.Caption(pos=(120, 60), text="Ready")
|
||||||
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
ui.append(status_text)
|
ui.append(status_text)
|
||||||
|
|
||||||
# Add info
|
# Add info
|
||||||
info_text = mcrfpy.Caption("", 120, 80)
|
info_text = mcrfpy.Caption(pos=(120, 80), text="")
|
||||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(info_text)
|
ui.append(info_text)
|
||||||
|
|
||||||
# Add path display
|
# Add path display
|
||||||
path_text = mcrfpy.Caption("Path: None", 120, 520)
|
path_text = mcrfpy.Caption(pos=(120, 520), text="Path: None")
|
||||||
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(path_text)
|
ui.append(path_text)
|
||||||
|
|
||||||
# Add controls
|
# Add controls
|
||||||
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540)
|
controls = mcrfpy.Caption(pos=(120, 540), text="SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit")
|
||||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560)
|
legend = mcrfpy.Caption(pos=(120, 560), text="Red Start→Blue End (valid) | Red Start→Red End (invalid)")
|
||||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend)
|
ui.append(legend)
|
||||||
|
|
||||||
# Expected results info
|
# Expected results info
|
||||||
expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580)
|
expected = mcrfpy.Caption(pos=(120, 580), text="Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail")
|
||||||
expected.fill_color = mcrfpy.Color(255, 150, 150)
|
expected.fill_color = mcrfpy.Color(255, 150, 150)
|
||||||
ui.append(expected)
|
ui.append(expected)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
|
||||||
|
|
||||||
# Global state
|
# Global state
|
||||||
grid = None
|
grid = None
|
||||||
|
color_layer = None
|
||||||
entities = []
|
entities = []
|
||||||
current_path_index = 0
|
current_path_index = 0
|
||||||
path_combinations = []
|
path_combinations = []
|
||||||
|
|
@ -25,14 +26,17 @@ current_path = []
|
||||||
|
|
||||||
def create_map():
|
def create_map():
|
||||||
"""Create the map with entities"""
|
"""Create the map with entities"""
|
||||||
global grid, entities
|
global grid, color_layer, entities
|
||||||
|
|
||||||
mcrfpy.createScene("dijkstra_cycle")
|
mcrfpy.createScene("dijkstra_cycle")
|
||||||
|
|
||||||
# Create grid
|
# Create grid
|
||||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Map layout
|
# Map layout
|
||||||
map_layout = [
|
map_layout = [
|
||||||
"..............", # Row 0
|
"..............", # Row 0
|
||||||
|
|
@ -46,29 +50,28 @@ def create_map():
|
||||||
"..W.WWW.......", # Row 8
|
"..W.WWW.......", # Row 8
|
||||||
"..............", # Row 9
|
"..............", # Row 9
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create the map
|
# Create the map
|
||||||
entity_positions = []
|
entity_positions = []
|
||||||
for y, row in enumerate(map_layout):
|
for y, row in enumerate(map_layout):
|
||||||
for x, char in enumerate(row):
|
for x, char in enumerate(row):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
if char == 'W':
|
if char == 'W':
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.color = WALL_COLOR
|
color_layer.set(x, y, WALL_COLOR)
|
||||||
else:
|
else:
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
if char == 'E':
|
if char == 'E':
|
||||||
entity_positions.append((x, y))
|
entity_positions.append((x, y))
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
entities = []
|
entities = []
|
||||||
for i, (x, y) in enumerate(entity_positions):
|
for i, (x, y) in enumerate(entity_positions):
|
||||||
entity = mcrfpy.Entity(x, y)
|
entity = mcrfpy.Entity((x, y), grid=grid)
|
||||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
print("Entities created:")
|
print("Entities created:")
|
||||||
|
|
@ -113,48 +116,48 @@ def create_map():
|
||||||
def clear_path_colors():
|
def clear_path_colors():
|
||||||
"""Reset all floor tiles to original color"""
|
"""Reset all floor tiles to original color"""
|
||||||
global current_path
|
global current_path
|
||||||
|
|
||||||
for y in range(grid.grid_y):
|
for y in range(grid.grid_y):
|
||||||
for x in range(grid.grid_x):
|
for x in range(grid.grid_x):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
current_path = []
|
current_path = []
|
||||||
|
|
||||||
def show_path(index):
|
def show_path(index):
|
||||||
"""Show a specific path combination"""
|
"""Show a specific path combination"""
|
||||||
global current_path_index, current_path
|
global current_path_index, current_path
|
||||||
|
|
||||||
if not path_combinations:
|
if not path_combinations:
|
||||||
status_text.text = "No valid paths available (Entity 1 is trapped!)"
|
status_text.text = "No valid paths available (Entity 1 is trapped!)"
|
||||||
return
|
return
|
||||||
|
|
||||||
current_path_index = index % len(path_combinations)
|
current_path_index = index % len(path_combinations)
|
||||||
from_idx, to_idx, path = path_combinations[current_path_index]
|
from_idx, to_idx, path = path_combinations[current_path_index]
|
||||||
|
|
||||||
# Clear previous path
|
# Clear previous path
|
||||||
clear_path_colors()
|
clear_path_colors()
|
||||||
|
|
||||||
# Get entities
|
# Get entities
|
||||||
e_from = entities[from_idx]
|
e_from = entities[from_idx]
|
||||||
e_to = entities[to_idx]
|
e_to = entities[to_idx]
|
||||||
|
|
||||||
# Color the path
|
# Color the path
|
||||||
current_path = path
|
current_path = path
|
||||||
if path:
|
if path:
|
||||||
# Color start and end
|
# Color start and end
|
||||||
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
|
color_layer.set(int(e_from.x), int(e_from.y), START_COLOR)
|
||||||
grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR
|
color_layer.set(int(e_to.x), int(e_to.y), END_COLOR)
|
||||||
|
|
||||||
# Color intermediate steps
|
# Color intermediate steps
|
||||||
for i, (x, y) in enumerate(path):
|
for i, (x, y) in enumerate(path):
|
||||||
if i > 0 and i < len(path) - 1:
|
if i > 0 and i < len(path) - 1:
|
||||||
grid.at(x, y).color = PATH_COLOR
|
color_layer.set(x, y, PATH_COLOR)
|
||||||
|
|
||||||
# Update status
|
# 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)"
|
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
|
# Update path display
|
||||||
path_display = []
|
path_display = []
|
||||||
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
|
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
|
||||||
|
|
@ -194,27 +197,27 @@ grid.size = (560, 400)
|
||||||
grid.position = (120, 100)
|
grid.position = (120, 100)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20)
|
title = mcrfpy.Caption(pos=(200, 20), text="Dijkstra Pathfinding - Cycle Paths")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add status
|
# Add status
|
||||||
status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60)
|
status_text = mcrfpy.Caption(pos=(120, 60), text="Press SPACE to cycle paths")
|
||||||
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
ui.append(status_text)
|
ui.append(status_text)
|
||||||
|
|
||||||
# Add path display
|
# Add path display
|
||||||
path_text = mcrfpy.Caption("Path: None", 120, 520)
|
path_text = mcrfpy.Caption(pos=(120, 520), text="Path: None")
|
||||||
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(path_text)
|
ui.append(path_text)
|
||||||
|
|
||||||
# Add controls
|
# Add controls
|
||||||
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540)
|
controls = mcrfpy.Caption(pos=(120, 540), text="SPACE/N=Next, P=Previous, R=Refresh, Q=Quit")
|
||||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560)
|
legend = mcrfpy.Caption(pos=(120, 560), text="Red=Start, Blue=End, Green=Path, Dark=Wall")
|
||||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend)
|
ui.append(legend)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,49 +18,52 @@ ENTITY_COLORS = [
|
||||||
|
|
||||||
# Global state
|
# Global state
|
||||||
grid = None
|
grid = None
|
||||||
|
color_layer = None
|
||||||
entities = []
|
entities = []
|
||||||
first_point = None
|
first_point = None
|
||||||
second_point = None
|
second_point = None
|
||||||
|
|
||||||
def create_simple_map():
|
def create_simple_map():
|
||||||
"""Create a simple test map"""
|
"""Create a simple test map"""
|
||||||
global grid, entities
|
global grid, color_layer, entities
|
||||||
|
|
||||||
mcrfpy.createScene("dijkstra_debug")
|
mcrfpy.createScene("dijkstra_debug")
|
||||||
|
|
||||||
# Small grid for easy debugging
|
# Small grid for easy debugging
|
||||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
print("Initializing 10x10 grid...")
|
print("Initializing 10x10 grid...")
|
||||||
|
|
||||||
# Initialize all as floor
|
# Initialize all as floor
|
||||||
for y in range(10):
|
for y in range(10):
|
||||||
for x in range(10):
|
for x in range(10):
|
||||||
grid.at(x, y).walkable = True
|
grid.at(x, y).walkable = True
|
||||||
grid.at(x, y).transparent = True
|
grid.at(x, y).transparent = True
|
||||||
grid.at(x, y).color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
# Add a simple wall
|
# Add a simple wall
|
||||||
print("Adding walls at:")
|
print("Adding walls at:")
|
||||||
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
|
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
|
||||||
for x, y in walls:
|
for x, y in walls:
|
||||||
print(f" Wall at ({x}, {y})")
|
print(f" Wall at ({x}, {y})")
|
||||||
grid.at(x, y).walkable = False
|
grid.at(x, y).walkable = False
|
||||||
grid.at(x, y).color = WALL_COLOR
|
color_layer.set(x, y, WALL_COLOR)
|
||||||
|
|
||||||
# Create 3 entities
|
# Create 3 entities
|
||||||
entity_positions = [(2, 5), (8, 5), (5, 8)]
|
entity_positions = [(2, 5), (8, 5), (5, 8)]
|
||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
print("\nCreating entities at:")
|
print("\nCreating entities at:")
|
||||||
for i, (x, y) in enumerate(entity_positions):
|
for i, (x, y) in enumerate(entity_positions):
|
||||||
print(f" Entity {i+1} at ({x}, {y})")
|
print(f" Entity {i+1} at ({x}, {y})")
|
||||||
entity = mcrfpy.Entity(x, y)
|
entity = mcrfpy.Entity((x, y), grid=grid)
|
||||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def test_path_highlighting():
|
def test_path_highlighting():
|
||||||
|
|
@ -88,12 +91,14 @@ def test_path_highlighting():
|
||||||
print(f" Step {i}: ({x}, {y})")
|
print(f" Step {i}: ({x}, {y})")
|
||||||
# Get current color for debugging
|
# Get current color for debugging
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
old_color = (cell.color.r, cell.color.g, cell.color.b)
|
old_c = color_layer.at(x, y)
|
||||||
|
old_color = (old_c.r, old_c.g, old_c.b)
|
||||||
|
|
||||||
# Set new color
|
# Set new color
|
||||||
cell.color = PATH_COLOR
|
color_layer.set(x, y, PATH_COLOR)
|
||||||
new_color = (cell.color.r, cell.color.g, cell.color.b)
|
new_c = color_layer.at(x, y)
|
||||||
|
new_color = (new_c.r, new_c.g, new_c.b)
|
||||||
|
|
||||||
print(f" Color changed from {old_color} to {new_color}")
|
print(f" Color changed from {old_color} to {new_color}")
|
||||||
print(f" Walkable: {cell.walkable}")
|
print(f" Walkable: {cell.walkable}")
|
||||||
|
|
||||||
|
|
@ -111,8 +116,8 @@ def test_path_highlighting():
|
||||||
# Verify colors were set
|
# Verify colors were set
|
||||||
print("\nVerifying cell colors after highlighting:")
|
print("\nVerifying cell colors after highlighting:")
|
||||||
for x, y in path[:3]: # Check first 3 cells
|
for x, y in path[:3]: # Check first 3 cells
|
||||||
cell = grid.at(x, y)
|
c = color_layer.at(x, y)
|
||||||
color = (cell.color.r, cell.color.g, cell.color.b)
|
color = (c.r, c.g, c.b)
|
||||||
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
|
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
|
||||||
match = color == expected
|
match = color == expected
|
||||||
print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}")
|
print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}")
|
||||||
|
|
@ -143,12 +148,12 @@ grid.position = (50, 50)
|
||||||
grid.size = (400, 400) # 10*40
|
grid.size = (400, 400) # 10*40
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10)
|
title = mcrfpy.Caption(pos=(50, 10), text="Dijkstra Debug - Press SPACE to retest, Q to quit")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add debug info
|
# Add debug info
|
||||||
info = mcrfpy.Caption("Check console for debug output", 50, 470)
|
info = mcrfpy.Caption(pos=(50, 470), text="Check console for debug output")
|
||||||
info.fill_color = mcrfpy.Color(200, 200, 200)
|
info.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(info)
|
ui.append(info)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,24 @@ ENTITY_COLORS = [
|
||||||
|
|
||||||
# Global state
|
# Global state
|
||||||
grid = None
|
grid = None
|
||||||
|
color_layer = None
|
||||||
entities = []
|
entities = []
|
||||||
first_point = None
|
first_point = None
|
||||||
second_point = None
|
second_point = None
|
||||||
|
|
||||||
def create_map():
|
def create_map():
|
||||||
"""Create the interactive map with the layout specified by the user"""
|
"""Create the interactive map with the layout specified by the user"""
|
||||||
global grid, entities
|
global grid, color_layer, entities
|
||||||
|
|
||||||
mcrfpy.createScene("dijkstra_interactive")
|
mcrfpy.createScene("dijkstra_interactive")
|
||||||
|
|
||||||
# Create grid - 14x10 as specified
|
# Create grid - 14x10 as specified
|
||||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Define the map layout from user's specification
|
# Define the map layout from user's specification
|
||||||
# . = floor, W = wall, E = entity position
|
# . = floor, W = wall, E = entity position
|
||||||
map_layout = [
|
map_layout = [
|
||||||
|
|
@ -57,36 +61,35 @@ def create_map():
|
||||||
"..W.WWW.......", # Row 8
|
"..W.WWW.......", # Row 8
|
||||||
"..............", # Row 9
|
"..............", # Row 9
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create the map
|
# Create the map
|
||||||
entity_positions = []
|
entity_positions = []
|
||||||
for y, row in enumerate(map_layout):
|
for y, row in enumerate(map_layout):
|
||||||
for x, char in enumerate(row):
|
for x, char in enumerate(row):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
if char == 'W':
|
if char == 'W':
|
||||||
# Wall
|
# Wall
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.color = WALL_COLOR
|
color_layer.set(x, y, WALL_COLOR)
|
||||||
else:
|
else:
|
||||||
# Floor
|
# Floor
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
if char == 'E':
|
if char == 'E':
|
||||||
# Entity position
|
# Entity position
|
||||||
entity_positions.append((x, y))
|
entity_positions.append((x, y))
|
||||||
|
|
||||||
# Create entities at marked positions
|
# Create entities at marked positions
|
||||||
entities = []
|
entities = []
|
||||||
for i, (x, y) in enumerate(entity_positions):
|
for i, (x, y) in enumerate(entity_positions):
|
||||||
entity = mcrfpy.Entity(x, y)
|
entity = mcrfpy.Entity((x, y), grid=grid)
|
||||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def clear_path_highlight():
|
def clear_path_highlight():
|
||||||
|
|
@ -96,37 +99,37 @@ def clear_path_highlight():
|
||||||
for x in range(grid.grid_x):
|
for x in range(grid.grid_x):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
def highlight_path():
|
def highlight_path():
|
||||||
"""Highlight the path between selected entities"""
|
"""Highlight the path between selected entities"""
|
||||||
if first_point is None or second_point is None:
|
if first_point is None or second_point is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear previous highlighting
|
# Clear previous highlighting
|
||||||
clear_path_highlight()
|
clear_path_highlight()
|
||||||
|
|
||||||
# Get entities
|
# Get entities
|
||||||
entity1 = entities[first_point]
|
entity1 = entities[first_point]
|
||||||
entity2 = entities[second_point]
|
entity2 = entities[second_point]
|
||||||
|
|
||||||
# Compute Dijkstra from first entity
|
# Compute Dijkstra from first entity
|
||||||
grid.compute_dijkstra(int(entity1.x), int(entity1.y))
|
grid.compute_dijkstra(int(entity1.x), int(entity1.y))
|
||||||
|
|
||||||
# Get path to second entity
|
# Get path to second entity
|
||||||
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
|
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
# Highlight the path
|
# Highlight the path
|
||||||
for x, y in path:
|
for x, y in path:
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
cell.color = PATH_COLOR
|
color_layer.set(x, y, PATH_COLOR)
|
||||||
|
|
||||||
# Also highlight start and end with entity colors
|
# Also highlight start and end with entity colors
|
||||||
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
|
color_layer.set(int(entity1.x), int(entity1.y), ENTITY_COLORS[first_point])
|
||||||
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
|
color_layer.set(int(entity2.x), int(entity2.y), ENTITY_COLORS[second_point])
|
||||||
|
|
||||||
# Update info
|
# Update info
|
||||||
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
|
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
|
||||||
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
|
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
|
||||||
|
|
@ -199,34 +202,33 @@ grid.size = (560, 400) # 14*40, 10*40
|
||||||
grid.position = (120, 60)
|
grid.position = (120, 60)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10)
|
title = mcrfpy.Caption(pos=(250, 10), text="Dijkstra Pathfinding Interactive")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add status text
|
# Add status text
|
||||||
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
|
status_text = mcrfpy.Caption(pos=(120, 480), text="Press 1/2/3 for first entity, A/B/C for second")
|
||||||
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(status_text)
|
ui.append(status_text)
|
||||||
|
|
||||||
# Add info text
|
# Add info text
|
||||||
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
|
info_text = mcrfpy.Caption(pos=(120, 500), text="Space to clear, Q to quit")
|
||||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(info_text)
|
ui.append(info_text)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540)
|
legend1 = mcrfpy.Caption(pos=(120, 540), text="Entities: 1=Red 2=Green 3=Blue")
|
||||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend1)
|
ui.append(legend1)
|
||||||
|
|
||||||
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560)
|
legend2 = mcrfpy.Caption(pos=(120, 560), text="Colors: Dark=Wall Light=Floor Cyan=Path")
|
||||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend2)
|
ui.append(legend2)
|
||||||
|
|
||||||
# Mark entity positions with colored indicators
|
# Mark entity positions with colored indicators
|
||||||
for i, entity in enumerate(entities):
|
for i, entity in enumerate(entities):
|
||||||
marker = mcrfpy.Caption(str(i+1),
|
marker = mcrfpy.Caption(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
|
||||||
120 + int(entity.x) * 40 + 15,
|
text=str(i+1))
|
||||||
60 + int(entity.y) * 40 + 10)
|
|
||||||
marker.fill_color = ENTITY_COLORS[i]
|
marker.fill_color = ENTITY_COLORS[i]
|
||||||
marker.outline = 1
|
marker.outline = 1
|
||||||
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ ENTITY_COLORS = [
|
||||||
|
|
||||||
# Global state
|
# Global state
|
||||||
grid = None
|
grid = None
|
||||||
|
color_layer = None
|
||||||
entities = []
|
entities = []
|
||||||
first_point = None
|
first_point = None
|
||||||
second_point = None
|
second_point = None
|
||||||
|
|
@ -43,14 +44,17 @@ original_positions = [] # Store original entity positions
|
||||||
|
|
||||||
def create_map():
|
def create_map():
|
||||||
"""Create the interactive map with the layout specified by the user"""
|
"""Create the interactive map with the layout specified by the user"""
|
||||||
global grid, entities, original_positions
|
global grid, color_layer, entities, original_positions
|
||||||
|
|
||||||
mcrfpy.createScene("dijkstra_enhanced")
|
mcrfpy.createScene("dijkstra_enhanced")
|
||||||
|
|
||||||
# Create grid - 14x10 as specified
|
# Create grid - 14x10 as specified
|
||||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Define the map layout from user's specification
|
# Define the map layout from user's specification
|
||||||
# . = floor, W = wall, E = entity position
|
# . = floor, W = wall, E = entity position
|
||||||
map_layout = [
|
map_layout = [
|
||||||
|
|
@ -65,87 +69,86 @@ def create_map():
|
||||||
"..W.WWW.......", # Row 8
|
"..W.WWW.......", # Row 8
|
||||||
"..............", # Row 9
|
"..............", # Row 9
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create the map
|
# Create the map
|
||||||
entity_positions = []
|
entity_positions = []
|
||||||
for y, row in enumerate(map_layout):
|
for y, row in enumerate(map_layout):
|
||||||
for x, char in enumerate(row):
|
for x, char in enumerate(row):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
|
|
||||||
if char == 'W':
|
if char == 'W':
|
||||||
# Wall
|
# Wall
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.color = WALL_COLOR
|
color_layer.set(x, y, WALL_COLOR)
|
||||||
else:
|
else:
|
||||||
# Floor
|
# Floor
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
if char == 'E':
|
if char == 'E':
|
||||||
# Entity position
|
# Entity position
|
||||||
entity_positions.append((x, y))
|
entity_positions.append((x, y))
|
||||||
|
|
||||||
# Create entities at marked positions
|
# Create entities at marked positions
|
||||||
entities = []
|
entities = []
|
||||||
original_positions = []
|
original_positions = []
|
||||||
for i, (x, y) in enumerate(entity_positions):
|
for i, (x, y) in enumerate(entity_positions):
|
||||||
entity = mcrfpy.Entity(x, y)
|
entity = mcrfpy.Entity((x, y), grid=grid)
|
||||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||||
grid.entities.append(entity)
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
original_positions.append((x, y))
|
original_positions.append((x, y))
|
||||||
|
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def clear_path_highlight():
|
def clear_path_highlight():
|
||||||
"""Clear any existing path highlighting"""
|
"""Clear any existing path highlighting"""
|
||||||
global current_path
|
global current_path
|
||||||
|
|
||||||
# Reset all floor tiles to original color
|
# Reset all floor tiles to original color
|
||||||
for y in range(grid.grid_y):
|
for y in range(grid.grid_y):
|
||||||
for x in range(grid.grid_x):
|
for x in range(grid.grid_x):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
cell.color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
current_path = []
|
current_path = []
|
||||||
|
|
||||||
def highlight_path():
|
def highlight_path():
|
||||||
"""Highlight the path between selected entities using entity.path_to()"""
|
"""Highlight the path between selected entities using entity.path_to()"""
|
||||||
global current_path
|
global current_path
|
||||||
|
|
||||||
if first_point is None or second_point is None:
|
if first_point is None or second_point is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear previous highlighting
|
# Clear previous highlighting
|
||||||
clear_path_highlight()
|
clear_path_highlight()
|
||||||
|
|
||||||
# Get entities
|
# Get entities
|
||||||
entity1 = entities[first_point]
|
entity1 = entities[first_point]
|
||||||
entity2 = entities[second_point]
|
entity2 = entities[second_point]
|
||||||
|
|
||||||
# Use the new path_to method!
|
# Use the new path_to method!
|
||||||
path = entity1.path_to(int(entity2.x), int(entity2.y))
|
path = entity1.path_to(int(entity2.x), int(entity2.y))
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
current_path = path
|
current_path = path
|
||||||
|
|
||||||
# Highlight the path
|
# Highlight the path
|
||||||
for i, (x, y) in enumerate(path):
|
for i, (x, y) in enumerate(path):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
if cell.walkable:
|
if cell.walkable:
|
||||||
# Use gradient for path visualization
|
# Use gradient for path visualization
|
||||||
if i < len(path) - 1:
|
if i < len(path) - 1:
|
||||||
cell.color = PATH_COLOR
|
color_layer.set(x, y, PATH_COLOR)
|
||||||
else:
|
else:
|
||||||
cell.color = VISITED_COLOR
|
color_layer.set(x, y, VISITED_COLOR)
|
||||||
|
|
||||||
# Highlight start and end with entity colors
|
# Highlight start and end with entity colors
|
||||||
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
|
color_layer.set(int(entity1.x), int(entity1.y), ENTITY_COLORS[first_point])
|
||||||
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
|
color_layer.set(int(entity2.x), int(entity2.y), ENTITY_COLORS[second_point])
|
||||||
|
|
||||||
# Update info
|
# Update info
|
||||||
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
|
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
|
||||||
else:
|
else:
|
||||||
|
|
@ -291,39 +294,38 @@ grid.size = (560, 400) # 14*40, 10*40
|
||||||
grid.position = (120, 60)
|
grid.position = (120, 60)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10)
|
title = mcrfpy.Caption(pos=(250, 10), text="Enhanced Dijkstra Pathfinding")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add status text
|
# Add status text
|
||||||
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
|
status_text = mcrfpy.Caption(pos=(120, 480), text="Press 1/2/3 for first entity, A/B/C for second")
|
||||||
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(status_text)
|
ui.append(status_text)
|
||||||
|
|
||||||
# Add info text
|
# Add info text
|
||||||
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
|
info_text = mcrfpy.Caption(pos=(120, 500), text="Space to clear, Q to quit")
|
||||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(info_text)
|
ui.append(info_text)
|
||||||
|
|
||||||
# Add control text
|
# Add control text
|
||||||
control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520)
|
control_text = mcrfpy.Caption(pos=(120, 520), text="Press M to move, P to pause, R to reset")
|
||||||
control_text.fill_color = mcrfpy.Color(150, 200, 150)
|
control_text.fill_color = mcrfpy.Color(150, 200, 150)
|
||||||
ui.append(control_text)
|
ui.append(control_text)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560)
|
legend1 = mcrfpy.Caption(pos=(120, 560), text="Entities: 1=Red 2=Green 3=Blue")
|
||||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend1)
|
ui.append(legend1)
|
||||||
|
|
||||||
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580)
|
legend2 = mcrfpy.Caption(pos=(120, 580), text="Colors: Dark=Wall Light=Floor Cyan=Path")
|
||||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend2)
|
ui.append(legend2)
|
||||||
|
|
||||||
# Mark entity positions with colored indicators
|
# Mark entity positions with colored indicators
|
||||||
for i, entity in enumerate(entities):
|
for i, entity in enumerate(entities):
|
||||||
marker = mcrfpy.Caption(str(i+1),
|
marker = mcrfpy.Caption(pos=(120 + int(entity.x) * 40 + 15, 60 + int(entity.y) * 40 + 10),
|
||||||
120 + int(entity.x) * 40 + 15,
|
text=str(i+1))
|
||||||
60 + int(entity.y) * 40 + 10)
|
|
||||||
marker.fill_color = ENTITY_COLORS[i]
|
marker.fill_color = ENTITY_COLORS[i]
|
||||||
marker.outline = 1
|
marker.outline = 1
|
||||||
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
|
||||||
|
|
@ -128,12 +128,12 @@ grid.position = (50, 50)
|
||||||
grid.size = (500, 300)
|
grid.size = (500, 300)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10)
|
title = mcrfpy.Caption(pos=(200, 10), text="Dijkstra Pathfinding Test")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360)
|
legend = mcrfpy.Caption(pos=(50, 360), text="Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3")
|
||||||
legend.fill_color = mcrfpy.Color(180, 180, 180)
|
legend.fill_color = mcrfpy.Color(180, 180, 180)
|
||||||
ui.append(legend)
|
ui.append(legend)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,33 +19,36 @@ mcrfpy.createScene("visibility_demo")
|
||||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||||
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
|
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Initialize grid - all walkable and transparent
|
# Initialize grid - all walkable and transparent
|
||||||
for y in range(20):
|
for y in range(20):
|
||||||
for x in range(30):
|
for x in range(30):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120) # Floor color
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120)) # Floor color
|
||||||
|
|
||||||
# Create walls
|
# Create walls
|
||||||
walls = [
|
walls = [
|
||||||
# Central cross
|
# Central cross
|
||||||
[(15, y) for y in range(8, 12)],
|
[(15, y) for y in range(8, 12)],
|
||||||
[(x, 10) for x in range(13, 18)],
|
[(x, 10) for x in range(13, 18)],
|
||||||
|
|
||||||
# Rooms
|
# Rooms
|
||||||
# Top-left room
|
# Top-left room
|
||||||
[(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)],
|
[(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)],
|
[(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)],
|
||||||
|
|
||||||
# Top-right room
|
# Top-right room
|
||||||
[(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)],
|
[(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)],
|
[(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)],
|
||||||
|
|
||||||
# Bottom-left room
|
# Bottom-left room
|
||||||
[(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)],
|
[(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)],
|
[(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)],
|
||||||
|
|
||||||
# Bottom-right room
|
# Bottom-right room
|
||||||
[(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)],
|
[(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)],
|
[(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)],
|
||||||
|
|
@ -57,12 +60,12 @@ for wall_group in walls:
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.color = mcrfpy.Color(40, 20, 20) # Wall color
|
color_layer.set(x, y, mcrfpy.Color(40, 20, 20)) # Wall color
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
player = mcrfpy.Entity(5, 10, grid=grid)
|
player = mcrfpy.Entity((5, 10), grid=grid)
|
||||||
player.sprite_index = 64 # @
|
player.sprite_index = 64 # @
|
||||||
enemy = mcrfpy.Entity(25, 10, grid=grid)
|
enemy = mcrfpy.Entity((25, 10), grid=grid)
|
||||||
enemy.sprite_index = 69 # E
|
enemy.sprite_index = 69 # E
|
||||||
|
|
||||||
# Update initial visibility
|
# Update initial visibility
|
||||||
|
|
@ -80,24 +83,24 @@ grid.position = (50, 100)
|
||||||
grid.size = (900, 600) # 30*30, 20*30
|
grid.size = (900, 600) # 30*30, 20*30
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20)
|
title = mcrfpy.Caption(pos=(350, 20), text="Interactive Visibility Demo")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Info displays
|
# Info displays
|
||||||
perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50)
|
perspective_label = mcrfpy.Caption(pos=(50, 50), text="Perspective: Omniscient")
|
||||||
perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
|
perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(perspective_label)
|
ui.append(perspective_label)
|
||||||
|
|
||||||
controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730)
|
controls = mcrfpy.Caption(pos=(50, 730), text="WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset")
|
||||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50)
|
player_info = mcrfpy.Caption(pos=(700, 50), text="Player: (5, 10)")
|
||||||
player_info.fill_color = mcrfpy.Color(100, 255, 100)
|
player_info.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
ui.append(player_info)
|
ui.append(player_info)
|
||||||
|
|
||||||
enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70)
|
enemy_info = mcrfpy.Caption(pos=(700, 70), text="Enemy: (25, 10)")
|
||||||
enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
|
enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
ui.append(enemy_info)
|
ui.append(enemy_info)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ mcrfpy.createScene("vis_test")
|
||||||
print("Creating grid...")
|
print("Creating grid...")
|
||||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Initialize grid
|
# Initialize grid
|
||||||
print("Initializing grid...")
|
print("Initializing grid...")
|
||||||
for y in range(10):
|
for y in range(10):
|
||||||
|
|
@ -18,11 +21,11 @@ for y in range(10):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120)
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
|
||||||
|
|
||||||
# Create entity
|
# Create entity
|
||||||
print("Creating entity...")
|
print("Creating entity...")
|
||||||
entity = mcrfpy.Entity(5, 5, grid=grid)
|
entity = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
entity.sprite_index = 64
|
entity.sprite_index = 64
|
||||||
|
|
||||||
print("Updating visibility...")
|
print("Updating visibility...")
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ print("Scene created")
|
||||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||||
print("Grid created")
|
print("Grid created")
|
||||||
|
|
||||||
# Create entity without appending
|
# Create entity with grid association
|
||||||
entity = mcrfpy.Entity(2, 2, grid=grid)
|
entity = mcrfpy.Entity((2, 2), grid=grid)
|
||||||
print(f"Entity created at ({entity.x}, {entity.y})")
|
print(f"Entity created at ({entity.x}, {entity.y})")
|
||||||
|
|
||||||
# Check if gridstate is initialized
|
# Check if gridstate is initialized
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ while small grids use the original flat storage. Verifies that:
|
||||||
2. Large grids work correctly with chunks
|
2. Large grids work correctly with chunks
|
||||||
3. Cell access (read/write) works for both modes
|
3. Cell access (read/write) works for both modes
|
||||||
4. Rendering displays correctly for both modes
|
4. Rendering displays correctly for both modes
|
||||||
|
|
||||||
|
NOTE: This test uses ColorLayer for color operations since cell.color
|
||||||
|
is no longer supported. The chunk system affects internal storage, which
|
||||||
|
ColorLayer also uses.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
|
|
@ -19,22 +23,21 @@ def test_small_grid():
|
||||||
|
|
||||||
# Small grid should use flat storage
|
# Small grid should use flat storage
|
||||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(10, 10), size=(400, 400))
|
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(10, 10), size=(400, 400))
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Set some cells
|
# Set some cells
|
||||||
for y in range(50):
|
for y in range(50):
|
||||||
for x in range(50):
|
for x in range(50):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.color = mcrfpy.Color((x * 5) % 256, (y * 5) % 256, 128, 255)
|
color_layer.set(x, y, mcrfpy.Color((x * 5) % 256, (y * 5) % 256, 128, 255))
|
||||||
cell.tilesprite = -1
|
cell.tilesprite = -1
|
||||||
|
|
||||||
# Verify cells
|
# Verify cells
|
||||||
cell = grid.at(25, 25)
|
|
||||||
expected_r = (25 * 5) % 256
|
expected_r = (25 * 5) % 256
|
||||||
expected_g = (25 * 5) % 256
|
expected_g = (25 * 5) % 256
|
||||||
color = cell.color
|
color = color_layer.at(25, 25)
|
||||||
r, g = color[0], color[1]
|
if color.r != expected_r or color.g != expected_g:
|
||||||
if r != expected_r or g != expected_g:
|
print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({color.r}, {color.g})")
|
||||||
print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({r}, {g})")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(" Small grid: PASS")
|
print(" Small grid: PASS")
|
||||||
|
|
@ -46,6 +49,7 @@ def test_large_grid():
|
||||||
|
|
||||||
# Large grid should use chunk storage (100 > 64)
|
# Large grid should use chunk storage (100 > 64)
|
||||||
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
|
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Set cells across multiple chunks
|
# Set cells across multiple chunks
|
||||||
# Chunks are 64x64, so a 100x100 grid has 2x2 = 4 chunks
|
# Chunks are 64x64, so a 100x100 grid has 2x2 = 4 chunks
|
||||||
|
|
@ -61,15 +65,14 @@ def test_large_grid():
|
||||||
|
|
||||||
for x, y in test_points:
|
for x, y in test_points:
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.color = mcrfpy.Color(x, y, 100, 255)
|
color_layer.set(x, y, mcrfpy.Color(x, y, 100, 255))
|
||||||
cell.tilesprite = -1
|
cell.tilesprite = -1
|
||||||
|
|
||||||
# Verify cells
|
# Verify cells
|
||||||
for x, y in test_points:
|
for x, y in test_points:
|
||||||
cell = grid.at(x, y)
|
color = color_layer.at(x, y)
|
||||||
color = cell.color
|
if color.r != x or color.g != y:
|
||||||
if color[0] != x or color[1] != y:
|
print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color.r}, {color.g})")
|
||||||
print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color[0]}, {color[1]})")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(" Large grid cell access: PASS")
|
print(" Large grid cell access: PASS")
|
||||||
|
|
@ -81,6 +84,7 @@ def test_very_large_grid():
|
||||||
|
|
||||||
# 500x500 = 250,000 cells, should use ~64 chunks (8x8)
|
# 500x500 = 250,000 cells, should use ~64 chunks (8x8)
|
||||||
grid = mcrfpy.Grid(grid_size=(500, 500), pos=(10, 10), size=(400, 400))
|
grid = mcrfpy.Grid(grid_size=(500, 500), pos=(10, 10), size=(400, 400))
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Set some cells at various positions
|
# Set some cells at various positions
|
||||||
test_points = [
|
test_points = [
|
||||||
|
|
@ -94,14 +98,12 @@ def test_very_large_grid():
|
||||||
]
|
]
|
||||||
|
|
||||||
for x, y in test_points:
|
for x, y in test_points:
|
||||||
cell = grid.at(x, y)
|
color_layer.set(x, y, mcrfpy.Color(x % 256, y % 256, 200, 255))
|
||||||
cell.color = mcrfpy.Color(x % 256, y % 256, 200, 255)
|
|
||||||
|
|
||||||
# Verify
|
# Verify
|
||||||
for x, y in test_points:
|
for x, y in test_points:
|
||||||
cell = grid.at(x, y)
|
color = color_layer.at(x, y)
|
||||||
color = cell.color
|
if color.r != (x % 256) or color.g != (y % 256):
|
||||||
if color[0] != (x % 256) or color[1] != (y % 256):
|
|
||||||
print(f"FAIL: Very large grid cell ({x},{y}) color mismatch")
|
print(f"FAIL: Very large grid cell ({x},{y}) color mismatch")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -114,20 +116,20 @@ def test_boundary_case():
|
||||||
|
|
||||||
# 64x64 should use flat storage (not exceeding threshold)
|
# 64x64 should use flat storage (not exceeding threshold)
|
||||||
grid_64 = mcrfpy.Grid(grid_size=(64, 64), pos=(10, 10), size=(400, 400))
|
grid_64 = mcrfpy.Grid(grid_size=(64, 64), pos=(10, 10), size=(400, 400))
|
||||||
cell = grid_64.at(63, 63)
|
color_layer_64 = grid_64.add_layer("color", z_index=-1)
|
||||||
cell.color = mcrfpy.Color(255, 0, 0, 255)
|
color_layer_64.set(63, 63, mcrfpy.Color(255, 0, 0, 255))
|
||||||
color = grid_64.at(63, 63).color
|
color = color_layer_64.at(63, 63)
|
||||||
if color[0] != 255:
|
if color.r != 255:
|
||||||
print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color[0]}")
|
print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color.r}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 65x65 should use chunk storage (exceeding threshold)
|
# 65x65 should use chunk storage (exceeding threshold)
|
||||||
grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400))
|
grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400))
|
||||||
cell = grid_65.at(64, 64)
|
color_layer_65 = grid_65.add_layer("color", z_index=-1)
|
||||||
cell.color = mcrfpy.Color(0, 255, 0, 255)
|
color_layer_65.set(64, 64, mcrfpy.Color(0, 255, 0, 255))
|
||||||
color = grid_65.at(64, 64).color
|
color = color_layer_65.at(64, 64)
|
||||||
if color[1] != 255:
|
if color.g != 255:
|
||||||
print(f"FAIL: 65x65 grid cell not set correctly, got g={color[1]}")
|
print(f"FAIL: 65x65 grid cell not set correctly, got g={color.g}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(" Boundary cases: PASS")
|
print(" Boundary cases: PASS")
|
||||||
|
|
@ -139,19 +141,18 @@ def test_edge_cases():
|
||||||
|
|
||||||
# Create 100x100 grid
|
# Create 100x100 grid
|
||||||
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
|
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Test all corners
|
# Test all corners
|
||||||
corners = [(0, 0), (99, 0), (0, 99), (99, 99)]
|
corners = [(0, 0), (99, 0), (0, 99), (99, 99)]
|
||||||
for i, (x, y) in enumerate(corners):
|
for i, (x, y) in enumerate(corners):
|
||||||
cell = grid.at(x, y)
|
color_layer.set(x, y, mcrfpy.Color(i * 60, i * 60, i * 60, 255))
|
||||||
cell.color = mcrfpy.Color(i * 60, i * 60, i * 60, 255)
|
|
||||||
|
|
||||||
for i, (x, y) in enumerate(corners):
|
for i, (x, y) in enumerate(corners):
|
||||||
cell = grid.at(x, y)
|
|
||||||
expected = i * 60
|
expected = i * 60
|
||||||
color = cell.color
|
color = color_layer.at(x, y)
|
||||||
if color[0] != expected:
|
if color.r != expected:
|
||||||
print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color[0]}")
|
print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color.r}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(" Edge cases: PASS")
|
print(" Edge cases: PASS")
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import sys
|
||||||
|
|
||||||
# Create a derived Entity class
|
# Create a derived Entity class
|
||||||
class CustomEntity(mcrfpy.Entity):
|
class CustomEntity(mcrfpy.Entity):
|
||||||
def __init__(self, x, y):
|
def __init__(self, pos):
|
||||||
super().__init__(x, y)
|
super().__init__(pos)
|
||||||
self.custom_attribute = "I am custom!"
|
self.custom_attribute = "I am custom!"
|
||||||
|
|
||||||
def custom_method(self):
|
def custom_method(self):
|
||||||
return "Custom method called"
|
return "Custom method called"
|
||||||
|
|
||||||
|
|
@ -21,11 +21,11 @@ def run_test(runtime):
|
||||||
"""Test that derived entity classes maintain their type in collections"""
|
"""Test that derived entity classes maintain their type in collections"""
|
||||||
try:
|
try:
|
||||||
# Create a grid
|
# Create a grid
|
||||||
grid = mcrfpy.Grid(10, 10)
|
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||||
|
|
||||||
# Create instances of base and derived entities
|
# Create instances of base and derived entities
|
||||||
base_entity = mcrfpy.Entity(1, 1)
|
base_entity = mcrfpy.Entity((1, 1))
|
||||||
custom_entity = CustomEntity(2, 2)
|
custom_entity = CustomEntity((2, 2))
|
||||||
|
|
||||||
# Add them to the grid's entity collection
|
# Add them to the grid's entity collection
|
||||||
grid.entities.append(base_entity)
|
grid.entities.append(base_entity)
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,17 @@ mcrfpy.setScene("timer_test_scene")
|
||||||
ui = mcrfpy.sceneUI("timer_test_scene")
|
ui = mcrfpy.sceneUI("timer_test_scene")
|
||||||
|
|
||||||
# Add a bright red frame that should be visible
|
# Add a bright red frame that should be visible
|
||||||
frame = mcrfpy.Frame(100, 100, 400, 300,
|
frame = mcrfpy.Frame(pos=(100, 100), size=(400, 300),
|
||||||
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
|
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
|
||||||
outline_color=mcrfpy.Color(255, 255, 255), # White outline
|
outline_color=mcrfpy.Color(255, 255, 255), # White outline
|
||||||
outline=5.0)
|
outline=5.0)
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# Add text
|
# Add text
|
||||||
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
|
caption = mcrfpy.Caption(pos=(150, 150),
|
||||||
text="TIMER TEST - SHOULD BE VISIBLE",
|
text="TIMER TEST - SHOULD BE VISIBLE",
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
caption.size = 24
|
caption.font_size = 24
|
||||||
frame.children.append(caption)
|
frame.children.append(caption)
|
||||||
|
|
||||||
# Add click handler to demonstrate interaction
|
# Add click handler to demonstrate interaction
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
e = mcrfpy.Entity(0, 0)
|
e = mcrfpy.Entity((0, 0))
|
||||||
print("Entity attributes:", dir(e))
|
print("Entity attributes:", dir(e))
|
||||||
print("\nEntity repr:", repr(e))
|
print("\nEntity repr:", repr(e))
|
||||||
|
|
@ -22,7 +22,7 @@ print(f"UI collection type: {type(ui)}")
|
||||||
print(f"Initial UI elements: {len(ui)}")
|
print(f"Initial UI elements: {len(ui)}")
|
||||||
|
|
||||||
# Add a simple frame
|
# Add a simple frame
|
||||||
frame = mcrfpy.Frame(0, 0, 100, 100,
|
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100),
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
print(f"After adding frame: {len(ui)} elements")
|
print(f"After adding frame: {len(ui)} elements")
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,13 @@ mcrfpy.createScene("grid")
|
||||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = mcrfpy.Caption(400, 30, "Grid Example - Dungeon View")
|
title = mcrfpy.Caption(pos=(400, 30), text="Grid Example - Dungeon View")
|
||||||
title.font = mcrfpy.default_font
|
title.font = mcrfpy.default_font
|
||||||
title.font_size = 24
|
title.font_size = 24
|
||||||
title.font_color = (255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
# Create main grid (20x15 tiles, each 32x32 pixels)
|
# Create main grid (20x15 tiles, each 32x32 pixels)
|
||||||
grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32)
|
grid = mcrfpy.Grid(pos=(100, 100), grid_size=(20, 15), texture=texture, size=(640, 480))
|
||||||
grid.texture = texture
|
|
||||||
|
|
||||||
# Define tile types from Crypt of Sokoban
|
# Define tile types from Crypt of Sokoban
|
||||||
FLOOR = 58 # Stone floor
|
FLOOR = 58 # Stone floor
|
||||||
|
|
@ -63,36 +62,21 @@ grid.set_tile(12, 8, BOULDER)
|
||||||
|
|
||||||
# Create some entities on the grid
|
# Create some entities on the grid
|
||||||
# Player entity
|
# Player entity
|
||||||
player = mcrfpy.Entity(5, 7)
|
player = mcrfpy.Entity((5, 7), texture=texture, sprite_index=84, grid=grid) # Player sprite
|
||||||
player.texture = texture
|
|
||||||
player.sprite_index = 84 # Player sprite
|
|
||||||
|
|
||||||
# Enemy entities
|
# Enemy entities
|
||||||
rat1 = mcrfpy.Entity(12, 5)
|
rat1 = mcrfpy.Entity((12, 5), texture=texture, sprite_index=123, grid=grid) # Rat
|
||||||
rat1.texture = texture
|
|
||||||
rat1.sprite_index = 123 # Rat
|
|
||||||
|
|
||||||
rat2 = mcrfpy.Entity(14, 9)
|
rat2 = mcrfpy.Entity((14, 9), texture=texture, sprite_index=123, grid=grid) # Rat
|
||||||
rat2.texture = texture
|
|
||||||
rat2.sprite_index = 123 # Rat
|
|
||||||
|
|
||||||
cyclops = mcrfpy.Entity(10, 10)
|
cyclops = mcrfpy.Entity((10, 10), texture=texture, sprite_index=109, grid=grid) # Cyclops
|
||||||
cyclops.texture = texture
|
|
||||||
cyclops.sprite_index = 109 # Cyclops
|
|
||||||
|
|
||||||
# Add entities to grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.entities.append(rat1)
|
|
||||||
grid.entities.append(rat2)
|
|
||||||
grid.entities.append(cyclops)
|
|
||||||
|
|
||||||
# Create a smaller grid showing tile palette
|
# Create a smaller grid showing tile palette
|
||||||
palette_label = mcrfpy.Caption(100, 600, "Tile Types:")
|
palette_label = mcrfpy.Caption(pos=(100, 600), text="Tile Types:")
|
||||||
palette_label.font = mcrfpy.default_font
|
palette_label.font = mcrfpy.default_font
|
||||||
palette_label.font_color = (255, 255, 255)
|
palette_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32)
|
palette = mcrfpy.Grid(pos=(250, 580), grid_size=(7, 1), texture=texture, size=(224, 32))
|
||||||
palette.texture = texture
|
|
||||||
palette.set_tile(0, 0, FLOOR)
|
palette.set_tile(0, 0, FLOOR)
|
||||||
palette.set_tile(1, 0, WALL)
|
palette.set_tile(1, 0, WALL)
|
||||||
palette.set_tile(2, 0, DOOR)
|
palette.set_tile(2, 0, DOOR)
|
||||||
|
|
@ -104,17 +88,17 @@ palette.set_tile(6, 0, BOULDER)
|
||||||
# Labels for palette
|
# Labels for palette
|
||||||
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
|
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
|
||||||
for i, label in enumerate(labels):
|
for i, label in enumerate(labels):
|
||||||
l = mcrfpy.Caption(250 + i * 32, 615, label)
|
l = mcrfpy.Caption(pos=(250 + i * 32, 615), text=label)
|
||||||
l.font = mcrfpy.default_font
|
l.font = mcrfpy.default_font
|
||||||
l.font_size = 10
|
l.font_size = 10
|
||||||
l.font_color = (255, 255, 255)
|
l.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
mcrfpy.sceneUI("grid").append(l)
|
mcrfpy.sceneUI("grid").append(l)
|
||||||
|
|
||||||
# Add info caption
|
# Add info caption
|
||||||
info = mcrfpy.Caption(100, 680, "Grid supports tiles and entities. Entities can move independently of the tile grid.")
|
info = mcrfpy.Caption(pos=(100, 680), text="Grid supports tiles and entities. Entities can move independently of the tile grid.")
|
||||||
info.font = mcrfpy.default_font
|
info.font = mcrfpy.default_font
|
||||||
info.font_size = 14
|
info.font_size = 14
|
||||||
info.font_color = (200, 200, 200)
|
info.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
|
||||||
# Add all elements to scene
|
# Add all elements to scene
|
||||||
ui = mcrfpy.sceneUI("grid")
|
ui = mcrfpy.sceneUI("grid")
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,20 @@ mcrfpy.createScene("sprites")
|
||||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = mcrfpy.Caption(400, 30, "Sprite Examples")
|
title = mcrfpy.Caption(pos=(400, 30), text="Sprite Examples")
|
||||||
title.font = mcrfpy.default_font
|
title.font = mcrfpy.default_font
|
||||||
title.font_size = 24
|
title.font_size = 24
|
||||||
title.font_color = (255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
# Create a frame background
|
# Create a frame background
|
||||||
frame = mcrfpy.Frame(50, 80, 700, 500)
|
frame = mcrfpy.Frame(pos=(50, 80), size=(700, 500))
|
||||||
frame.bgcolor = (64, 64, 128)
|
frame.fill_color = mcrfpy.Color(64, 64, 128)
|
||||||
frame.outline = 2
|
frame.outline = 2
|
||||||
|
|
||||||
# Player sprite
|
# Player sprite
|
||||||
player_label = mcrfpy.Caption(100, 120, "Player")
|
player_label = mcrfpy.Caption(pos=(100, 120), text="Player")
|
||||||
player_label.font = mcrfpy.default_font
|
player_label.font = mcrfpy.default_font
|
||||||
player_label.font_color = (255, 255, 255)
|
player_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
player = mcrfpy.Sprite(120, 150)
|
player = mcrfpy.Sprite(120, 150)
|
||||||
player.texture = texture
|
player.texture = texture
|
||||||
|
|
@ -43,9 +43,9 @@ player.sprite_index = 84 # Player sprite
|
||||||
player.scale = (3.0, 3.0)
|
player.scale = (3.0, 3.0)
|
||||||
|
|
||||||
# Enemy sprites
|
# Enemy sprites
|
||||||
enemy_label = mcrfpy.Caption(250, 120, "Enemies")
|
enemy_label = mcrfpy.Caption(pos=(250, 120), text="Enemies")
|
||||||
enemy_label.font = mcrfpy.default_font
|
enemy_label.font = mcrfpy.default_font
|
||||||
enemy_label.font_color = (255, 255, 255)
|
enemy_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
rat = mcrfpy.Sprite(250, 150)
|
rat = mcrfpy.Sprite(250, 150)
|
||||||
rat.texture = texture
|
rat.texture = texture
|
||||||
|
|
@ -63,9 +63,9 @@ cyclops.sprite_index = 109 # Cyclops
|
||||||
cyclops.scale = (3.0, 3.0)
|
cyclops.scale = (3.0, 3.0)
|
||||||
|
|
||||||
# Items row
|
# Items row
|
||||||
items_label = mcrfpy.Caption(100, 250, "Items")
|
items_label = mcrfpy.Caption(pos=(100, 250), text="Items")
|
||||||
items_label.font = mcrfpy.default_font
|
items_label.font = mcrfpy.default_font
|
||||||
items_label.font_color = (255, 255, 255)
|
items_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
# Boulder
|
# Boulder
|
||||||
boulder = mcrfpy.Sprite(100, 280)
|
boulder = mcrfpy.Sprite(100, 280)
|
||||||
|
|
@ -92,9 +92,9 @@ button.sprite_index = 250 # Button
|
||||||
button.scale = (3.0, 3.0)
|
button.scale = (3.0, 3.0)
|
||||||
|
|
||||||
# UI elements row
|
# UI elements row
|
||||||
ui_label = mcrfpy.Caption(100, 380, "UI Elements")
|
ui_label = mcrfpy.Caption(pos=(100, 380), text="UI Elements")
|
||||||
ui_label.font = mcrfpy.default_font
|
ui_label.font = mcrfpy.default_font
|
||||||
ui_label.font_color = (255, 255, 255)
|
ui_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
# Hearts
|
# Hearts
|
||||||
heart_full = mcrfpy.Sprite(100, 410)
|
heart_full = mcrfpy.Sprite(100, 410)
|
||||||
|
|
@ -119,9 +119,9 @@ armor.sprite_index = 211 # Armor
|
||||||
armor.scale = (3.0, 3.0)
|
armor.scale = (3.0, 3.0)
|
||||||
|
|
||||||
# Scale demonstration
|
# Scale demonstration
|
||||||
scale_label = mcrfpy.Caption(500, 120, "Scale Demo")
|
scale_label = mcrfpy.Caption(pos=(500, 120), text="Scale Demo")
|
||||||
scale_label.font = mcrfpy.default_font
|
scale_label.font = mcrfpy.default_font
|
||||||
scale_label.font_color = (255, 255, 255)
|
scale_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
|
||||||
# Same sprite at different scales
|
# Same sprite at different scales
|
||||||
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):
|
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):
|
||||||
|
|
|
||||||
|
|
@ -17,42 +17,42 @@ def test_transparency_workaround():
|
||||||
# WORKAROUND: Create a full-window opaque frame as the first element
|
# WORKAROUND: Create a full-window opaque frame as the first element
|
||||||
# This acts as an opaque background since the scene clears with transparent
|
# This acts as an opaque background since the scene clears with transparent
|
||||||
print("Creating full-window opaque background...")
|
print("Creating full-window opaque background...")
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768,
|
background = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
|
||||||
fill_color=mcrfpy.Color(50, 50, 50), # Dark gray
|
fill_color=mcrfpy.Color(50, 50, 50), # Dark gray
|
||||||
outline_color=None,
|
outline_color=None,
|
||||||
outline=0.0)
|
outline=0.0)
|
||||||
ui.append(background)
|
ui.append(background)
|
||||||
print("✓ Added opaque background frame")
|
print("✓ Added opaque background frame")
|
||||||
|
|
||||||
# Now add normal content on top
|
# Now add normal content on top
|
||||||
print("\nAdding test content...")
|
print("\nAdding test content...")
|
||||||
|
|
||||||
# Red frame
|
# Red frame
|
||||||
frame1 = mcrfpy.Frame(100, 100, 200, 150,
|
frame1 = mcrfpy.Frame(pos=(100, 100), size=(200, 150),
|
||||||
fill_color=mcrfpy.Color(255, 0, 0),
|
fill_color=mcrfpy.Color(255, 0, 0),
|
||||||
outline_color=mcrfpy.Color(255, 255, 255),
|
outline_color=mcrfpy.Color(255, 255, 255),
|
||||||
outline=3.0)
|
outline=3.0)
|
||||||
ui.append(frame1)
|
ui.append(frame1)
|
||||||
|
|
||||||
# Green frame
|
# Green frame
|
||||||
frame2 = mcrfpy.Frame(350, 100, 200, 150,
|
frame2 = mcrfpy.Frame(pos=(350, 100), size=(200, 150),
|
||||||
fill_color=mcrfpy.Color(0, 255, 0),
|
fill_color=mcrfpy.Color(0, 255, 0),
|
||||||
outline_color=mcrfpy.Color(0, 0, 0),
|
outline_color=mcrfpy.Color(0, 0, 0),
|
||||||
outline=3.0)
|
outline=3.0)
|
||||||
ui.append(frame2)
|
ui.append(frame2)
|
||||||
|
|
||||||
# Blue frame
|
# Blue frame
|
||||||
frame3 = mcrfpy.Frame(100, 300, 200, 150,
|
frame3 = mcrfpy.Frame(pos=(100, 300), size=(200, 150),
|
||||||
fill_color=mcrfpy.Color(0, 0, 255),
|
fill_color=mcrfpy.Color(0, 0, 255),
|
||||||
outline_color=mcrfpy.Color(255, 255, 0),
|
outline_color=mcrfpy.Color(255, 255, 0),
|
||||||
outline=3.0)
|
outline=3.0)
|
||||||
ui.append(frame3)
|
ui.append(frame3)
|
||||||
|
|
||||||
# Add text
|
# Add text
|
||||||
caption = mcrfpy.Caption(mcrfpy.Vector(250, 50),
|
caption = mcrfpy.Caption(pos=(250, 50),
|
||||||
text="OPAQUE BACKGROUND TEST",
|
text="OPAQUE BACKGROUND TEST",
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
caption.size = 32
|
caption.font_size = 32
|
||||||
ui.append(caption)
|
ui.append(caption)
|
||||||
|
|
||||||
# Take screenshot
|
# Take screenshot
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ def take_screenshot(runtime):
|
||||||
mcrfpy.createScene("test")
|
mcrfpy.createScene("test")
|
||||||
|
|
||||||
# Add a visible element
|
# Add a visible element
|
||||||
caption = mcrfpy.Caption(100, 100, "Screenshot Test")
|
caption = mcrfpy.Caption(pos=(100, 100), text="Screenshot Test")
|
||||||
caption.font = mcrfpy.default_font
|
caption.font = mcrfpy.default_font
|
||||||
caption.font_color = (255, 255, 255)
|
caption.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
caption.font_size = 24
|
caption.font_size = 24
|
||||||
|
|
||||||
mcrfpy.sceneUI("test").append(caption)
|
mcrfpy.sceneUI("test").append(caption)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ mcrfpy.setScene("test")
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
# Add visible content - a white frame on default background
|
# Add visible content - a white frame on default background
|
||||||
frame = mcrfpy.Frame(100, 100, 200, 200,
|
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,9 @@ mcrfpy.createScene("chain_test")
|
||||||
grid = mcrfpy.Grid(grid_x=20, grid_y=15)
|
grid = mcrfpy.Grid(grid_x=20, grid_y=15)
|
||||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
|
||||||
|
# Add a color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Simple map
|
# Simple map
|
||||||
for y in range(15):
|
for y in range(15):
|
||||||
for x in range(20):
|
for x in range(20):
|
||||||
|
|
@ -80,17 +83,17 @@ for y in range(15):
|
||||||
if x == 0 or x == 19 or y == 0 or y == 14:
|
if x == 0 or x == 19 or y == 0 or y == 14:
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.color = mcrfpy.Color(60, 40, 40)
|
color_layer.set(x, y, mcrfpy.Color(60, 40, 40))
|
||||||
else:
|
else:
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120)
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
player = mcrfpy.Entity(2, 2, grid=grid)
|
player = mcrfpy.Entity((2, 2), grid=grid)
|
||||||
player.sprite_index = 64 # @
|
player.sprite_index = 64 # @
|
||||||
|
|
||||||
enemy = mcrfpy.Entity(17, 12, grid=grid)
|
enemy = mcrfpy.Entity((17, 12), grid=grid)
|
||||||
enemy.sprite_index = 69 # E
|
enemy.sprite_index = 69 # E
|
||||||
|
|
||||||
# UI setup
|
# UI setup
|
||||||
|
|
@ -99,15 +102,15 @@ ui.append(grid)
|
||||||
grid.position = (100, 100)
|
grid.position = (100, 100)
|
||||||
grid.size = (600, 450)
|
grid.size = (600, 450)
|
||||||
|
|
||||||
title = mcrfpy.Caption("Animation Chaining Test", 300, 20)
|
title = mcrfpy.Caption(pos=(300, 20), text="Animation Chaining Test")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
status = mcrfpy.Caption("Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit", 100, 50)
|
status = mcrfpy.Caption(pos=(100, 50), text="Press 1: Animate Player | 2: Animate Enemy | 3: Both | Q: Quit")
|
||||||
status.fill_color = mcrfpy.Color(200, 200, 200)
|
status.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(status)
|
ui.append(status)
|
||||||
|
|
||||||
info = mcrfpy.Caption("Status: Ready", 100, 70)
|
info = mcrfpy.Caption(pos=(100, 70), text="Status: Ready")
|
||||||
info.fill_color = mcrfpy.Color(100, 255, 100)
|
info.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
ui.append(info)
|
ui.append(info)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,15 @@ mcrfpy.createScene("anim_debug")
|
||||||
|
|
||||||
# Simple grid
|
# Simple grid
|
||||||
grid = mcrfpy.Grid(grid_x=15, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=15, grid_y=10)
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
for y in range(10):
|
for y in range(10):
|
||||||
for x in range(15):
|
for x in range(15):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120)
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
|
||||||
|
|
||||||
# Test entity
|
# Test entity
|
||||||
entity = mcrfpy.Entity(5, 5, grid=grid)
|
entity = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
entity.sprite_index = 64
|
entity.sprite_index = 64
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
|
@ -79,19 +80,19 @@ ui.append(grid)
|
||||||
grid.position = (100, 150)
|
grid.position = (100, 150)
|
||||||
grid.size = (450, 300)
|
grid.size = (450, 300)
|
||||||
|
|
||||||
title = mcrfpy.Caption("Animation Debug Tool", 250, 20)
|
title = mcrfpy.Caption(pos=(250, 20), text="Animation Debug Tool")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
status = mcrfpy.Caption("Press keys to test animations", 100, 50)
|
status = mcrfpy.Caption(pos=(100, 50), text="Press keys to test animations")
|
||||||
status.fill_color = mcrfpy.Color(200, 200, 200)
|
status.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(status)
|
ui.append(status)
|
||||||
|
|
||||||
pos_display = mcrfpy.Caption("", 100, 70)
|
pos_display = mcrfpy.Caption(pos=(100, 70), text="")
|
||||||
pos_display.fill_color = mcrfpy.Color(255, 255, 100)
|
pos_display.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
ui.append(pos_display)
|
ui.append(pos_display)
|
||||||
|
|
||||||
active_display = mcrfpy.Caption("Active animations: 0", 100, 90)
|
active_display = mcrfpy.Caption(pos=(100, 90), text="Active animations: 0")
|
||||||
active_display.fill_color = mcrfpy.Color(100, 255, 255)
|
active_display.fill_color = mcrfpy.Color(100, 255, 255)
|
||||||
ui.append(active_display)
|
ui.append(active_display)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ print("2. Getting UI...")
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
print("3. Creating frame...")
|
print("3. Creating frame...")
|
||||||
frame = mcrfpy.Frame(100, 100, 200, 200)
|
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
print("4. Creating Animation object...")
|
print("4. Creating Animation object...")
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ def test_1_basic_animation():
|
||||||
"""Test that basic animations still work"""
|
"""Test that basic animations still work"""
|
||||||
try:
|
try:
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
frame = mcrfpy.Frame(100, 100, 100, 100)
|
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
|
anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
|
||||||
|
|
@ -49,7 +49,7 @@ def test_2_remove_animated_object():
|
||||||
"""Test removing object with active animation"""
|
"""Test removing object with active animation"""
|
||||||
try:
|
try:
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
frame = mcrfpy.Frame(100, 100, 100, 100)
|
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# Start animation
|
# Start animation
|
||||||
|
|
@ -73,7 +73,7 @@ def test_3_complete_animation():
|
||||||
"""Test completing animation immediately"""
|
"""Test completing animation immediately"""
|
||||||
try:
|
try:
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
frame = mcrfpy.Frame(100, 100, 100, 100)
|
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# Start animation
|
# Start animation
|
||||||
|
|
@ -98,7 +98,7 @@ def test_4_multiple_animations_timer():
|
||||||
nonlocal success
|
nonlocal success
|
||||||
try:
|
try:
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
frame = mcrfpy.Frame(200, 200, 100, 100)
|
frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# Create multiple animations rapidly (this used to crash)
|
# Create multiple animations rapidly (this used to crash)
|
||||||
|
|
@ -129,7 +129,7 @@ def test_5_scene_cleanup():
|
||||||
# Add animated objects to first scene
|
# Add animated objects to first scene
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
frame = mcrfpy.Frame(50 * i, 100, 40, 40)
|
frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
|
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
|
||||||
anim.start(frame)
|
anim.start(frame)
|
||||||
|
|
@ -148,9 +148,9 @@ def test_6_animation_after_clear():
|
||||||
"""Test animations after clearing UI"""
|
"""Test animations after clearing UI"""
|
||||||
try:
|
try:
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
# Create and animate
|
# Create and animate
|
||||||
frame = mcrfpy.Frame(100, 100, 100, 100)
|
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
|
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
|
||||||
anim.start(frame)
|
anim.start(frame)
|
||||||
|
|
@ -207,7 +207,7 @@ mcrfpy.setScene("test")
|
||||||
|
|
||||||
# Add a background
|
# Add a background
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
bg = mcrfpy.Frame(0, 0, 1024, 768)
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768))
|
||||||
bg.fill_color = mcrfpy.Color(20, 20, 30)
|
bg.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
ui.append(bg)
|
ui.append(bg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,14 +42,14 @@ mcrfpy.setScene("test")
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
# Add title and subtitle (to preserve during clearing)
|
# Add title and subtitle (to preserve during clearing)
|
||||||
title = mcrfpy.Caption("Test Title", 400, 20)
|
title = mcrfpy.Caption(pos=(400, 20), text="Test Title")
|
||||||
subtitle = mcrfpy.Caption("Test Subtitle", 400, 50)
|
subtitle = mcrfpy.Caption(pos=(400, 50), text="Test Subtitle")
|
||||||
ui.extend([title, subtitle])
|
ui.extend([title, subtitle])
|
||||||
|
|
||||||
# Create initial animated objects
|
# Create initial animated objects
|
||||||
print("Creating initial animated objects...")
|
print("Creating initial animated objects...")
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
f = mcrfpy.Frame(50 + i*30, 100, 25, 25)
|
f = mcrfpy.Frame(pos=(50 + i*30, 100), size=(25, 25))
|
||||||
f.fill_color = mcrfpy.Color(255, 100, 100)
|
f.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
ui.append(f)
|
ui.append(f)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,15 @@ import sys
|
||||||
def create_test_grid():
|
def create_test_grid():
|
||||||
"""Create a test grid with obstacles"""
|
"""Create a test grid with obstacles"""
|
||||||
mcrfpy.createScene("dijkstra_test")
|
mcrfpy.createScene("dijkstra_test")
|
||||||
|
|
||||||
# Create grid
|
# Create grid
|
||||||
grid = mcrfpy.Grid(grid_x=20, grid_y=20)
|
grid = mcrfpy.Grid(grid_x=20, grid_y=20)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
# Store color_layer on grid for access elsewhere
|
||||||
|
grid._color_layer = color_layer
|
||||||
|
|
||||||
# Initialize all cells as walkable
|
# Initialize all cells as walkable
|
||||||
for y in range(grid.grid_y):
|
for y in range(grid.grid_y):
|
||||||
for x in range(grid.grid_x):
|
for x in range(grid.grid_x):
|
||||||
|
|
@ -28,8 +33,8 @@ def create_test_grid():
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.tilesprite = 46 # . period
|
cell.tilesprite = 46 # . period
|
||||||
cell.color = mcrfpy.Color(50, 50, 50)
|
color_layer.set(x, y, mcrfpy.Color(50, 50, 50))
|
||||||
|
|
||||||
# Create some walls to make pathfinding interesting
|
# Create some walls to make pathfinding interesting
|
||||||
# Vertical wall
|
# Vertical wall
|
||||||
for y in range(5, 15):
|
for y in range(5, 15):
|
||||||
|
|
@ -37,8 +42,8 @@ def create_test_grid():
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.tilesprite = 219 # Block
|
cell.tilesprite = 219 # Block
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
color_layer.set(10, y, mcrfpy.Color(100, 100, 100))
|
||||||
|
|
||||||
# Horizontal wall
|
# Horizontal wall
|
||||||
for x in range(5, 15):
|
for x in range(5, 15):
|
||||||
if x != 10: # Leave a gap
|
if x != 10: # Leave a gap
|
||||||
|
|
@ -46,8 +51,8 @@ def create_test_grid():
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.tilesprite = 219
|
cell.tilesprite = 219
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
color_layer.set(x, 10, mcrfpy.Color(100, 100, 100))
|
||||||
|
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def test_basic_dijkstra():
|
def test_basic_dijkstra():
|
||||||
|
|
@ -133,7 +138,7 @@ def test_multi_target_scenario():
|
||||||
# Mark threat position
|
# Mark threat position
|
||||||
cell = grid.at(tx, ty)
|
cell = grid.at(tx, ty)
|
||||||
cell.tilesprite = 84 # T for threat
|
cell.tilesprite = 84 # T for threat
|
||||||
cell.color = mcrfpy.Color(255, 0, 0)
|
grid._color_layer.set(tx, ty, mcrfpy.Color(255, 0, 0))
|
||||||
|
|
||||||
# Compute Dijkstra from this threat
|
# Compute Dijkstra from this threat
|
||||||
grid.compute_dijkstra(tx, ty)
|
grid.compute_dijkstra(tx, ty)
|
||||||
|
|
@ -176,7 +181,7 @@ def test_multi_target_scenario():
|
||||||
# Mark safe position
|
# Mark safe position
|
||||||
cell = grid.at(best_pos[0], best_pos[1])
|
cell = grid.at(best_pos[0], best_pos[1])
|
||||||
cell.tilesprite = 83 # S for safe
|
cell.tilesprite = 83 # S for safe
|
||||||
cell.color = mcrfpy.Color(0, 255, 0)
|
grid._color_layer.set(best_pos[0], best_pos[1], mcrfpy.Color(0, 255, 0))
|
||||||
|
|
||||||
def run_test(runtime):
|
def run_test(runtime):
|
||||||
"""Timer callback to run tests after scene loads"""
|
"""Timer callback to run tests after scene loads"""
|
||||||
|
|
@ -211,7 +216,7 @@ ui = mcrfpy.sceneUI("dijkstra_test")
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 10, 10)
|
title = mcrfpy.Caption(pos=(10, 10), text="Dijkstra Pathfinding Test")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,16 @@ mcrfpy.createScene("test_anim")
|
||||||
grid = mcrfpy.Grid(grid_x=15, grid_y=15)
|
grid = mcrfpy.Grid(grid_x=15, grid_y=15)
|
||||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
|
||||||
|
# Add a color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Initialize all cells as walkable floors
|
# Initialize all cells as walkable floors
|
||||||
for y in range(15):
|
for y in range(15):
|
||||||
for x in range(15):
|
for x in range(15):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120)
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
|
||||||
|
|
||||||
# Mark the path we'll follow with different color
|
# Mark the path we'll follow with different color
|
||||||
path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5),
|
path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5),
|
||||||
|
|
@ -32,11 +35,10 @@ path_cells = [(5,5), (6,5), (7,5), (8,5), (9,5), (10,5),
|
||||||
(5,9), (5,8), (5,7), (5,6)]
|
(5,9), (5,8), (5,7), (5,6)]
|
||||||
|
|
||||||
for x, y in path_cells:
|
for x, y in path_cells:
|
||||||
cell = grid.at(x, y)
|
color_layer.set(x, y, mcrfpy.Color(120, 120, 150))
|
||||||
cell.color = mcrfpy.Color(120, 120, 150)
|
|
||||||
|
|
||||||
# Create entity at start position
|
# Create entity at start position
|
||||||
entity = mcrfpy.Entity(5, 5, grid=grid)
|
entity = mcrfpy.Entity((5, 5), grid=grid)
|
||||||
entity.sprite_index = 64 # @
|
entity.sprite_index = 64 # @
|
||||||
|
|
||||||
# UI setup
|
# UI setup
|
||||||
|
|
@ -46,27 +48,27 @@ grid.position = (100, 100)
|
||||||
grid.size = (450, 450) # 15 * 30 pixels per cell
|
grid.size = (450, 450) # 15 * 30 pixels per cell
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = mcrfpy.Caption("Entity Animation Test - Square Path", 200, 20)
|
title = mcrfpy.Caption(pos=(200, 20), text="Entity Animation Test - Square Path")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Status display
|
# Status display
|
||||||
status = mcrfpy.Caption("Press SPACE to start animation | Q to quit", 100, 50)
|
status = mcrfpy.Caption(pos=(100, 50), text="Press SPACE to start animation | Q to quit")
|
||||||
status.fill_color = mcrfpy.Color(200, 200, 200)
|
status.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(status)
|
ui.append(status)
|
||||||
|
|
||||||
# Position display
|
# Position display
|
||||||
pos_display = mcrfpy.Caption(f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})", 100, 70)
|
pos_display = mcrfpy.Caption(pos=(100, 70), text=f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})")
|
||||||
pos_display.fill_color = mcrfpy.Color(255, 255, 100)
|
pos_display.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
ui.append(pos_display)
|
ui.append(pos_display)
|
||||||
|
|
||||||
# Animation info
|
# Animation info
|
||||||
anim_info = mcrfpy.Caption("Animation: Not started", 400, 70)
|
anim_info = mcrfpy.Caption(pos=(400, 70), text="Animation: Not started")
|
||||||
anim_info.fill_color = mcrfpy.Color(100, 255, 255)
|
anim_info.fill_color = mcrfpy.Color(100, 255, 255)
|
||||||
ui.append(anim_info)
|
ui.append(anim_info)
|
||||||
|
|
||||||
# Debug info
|
# Debug info
|
||||||
debug_info = mcrfpy.Caption("Debug: Waiting...", 100, 570)
|
debug_info = mcrfpy.Caption(pos=(100, 570), text="Debug: Waiting...")
|
||||||
debug_info.fill_color = mcrfpy.Color(150, 150, 150)
|
debug_info.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(debug_info)
|
ui.append(debug_info)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,19 @@ mcrfpy.createScene("fix_demo")
|
||||||
grid = mcrfpy.Grid(grid_x=15, grid_y=10)
|
grid = mcrfpy.Grid(grid_x=15, grid_y=10)
|
||||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Make floor
|
# Make floor
|
||||||
for y in range(10):
|
for y in range(10):
|
||||||
for x in range(15):
|
for x in range(15):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120)
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120))
|
||||||
|
|
||||||
# Create entity
|
# Create entity
|
||||||
entity = mcrfpy.Entity(2, 2, grid=grid)
|
entity = mcrfpy.Entity((2, 2), grid=grid)
|
||||||
entity.sprite_index = 64 # @
|
entity.sprite_index = 64 # @
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
|
|
@ -52,19 +55,19 @@ grid.position = (100, 150)
|
||||||
grid.size = (450, 300)
|
grid.size = (450, 300)
|
||||||
|
|
||||||
# Info displays
|
# Info displays
|
||||||
title = mcrfpy.Caption("Entity Animation Issue Demo", 250, 20)
|
title = mcrfpy.Caption(pos=(250, 20), text="Entity Animation Issue Demo")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
pos_info = mcrfpy.Caption("", 100, 50)
|
pos_info = mcrfpy.Caption(pos=(100, 50), text="")
|
||||||
pos_info.fill_color = mcrfpy.Color(255, 255, 100)
|
pos_info.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
ui.append(pos_info)
|
ui.append(pos_info)
|
||||||
|
|
||||||
sprite_info = mcrfpy.Caption("", 100, 70)
|
sprite_info = mcrfpy.Caption(pos=(100, 70), text="")
|
||||||
sprite_info.fill_color = mcrfpy.Color(255, 100, 100)
|
sprite_info.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
ui.append(sprite_info)
|
ui.append(sprite_info)
|
||||||
|
|
||||||
status = mcrfpy.Caption("Press SPACE to animate entity", 100, 100)
|
status = mcrfpy.Caption(pos=(100, 100), text="Press SPACE to animate entity")
|
||||||
status.fill_color = mcrfpy.Color(200, 200, 200)
|
status.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(status)
|
ui.append(status)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ for x, y in walls:
|
||||||
grid.at(x, y).walkable = False
|
grid.at(x, y).walkable = False
|
||||||
|
|
||||||
# Create entity
|
# Create entity
|
||||||
entity = mcrfpy.Entity(2, 2)
|
entity = mcrfpy.Entity((2, 2), grid=grid)
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
print(f"Entity at: ({entity.x}, {entity.y})")
|
print(f"Entity at: ({entity.x}, {entity.y})")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ print("=" * 50)
|
||||||
# Test 1: Entity without grid
|
# Test 1: Entity without grid
|
||||||
print("Test 1: Entity not in grid")
|
print("Test 1: Entity not in grid")
|
||||||
try:
|
try:
|
||||||
entity = mcrfpy.Entity(5, 5)
|
entity = mcrfpy.Entity((5, 5))
|
||||||
path = entity.path_to(8, 8)
|
path = entity.path_to(8, 8)
|
||||||
print(" ✗ Should have failed for entity not in grid")
|
print(" ✗ Should have failed for entity not in grid")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
@ -31,8 +31,7 @@ for y in range(5):
|
||||||
for x in range(5):
|
for x in range(5):
|
||||||
grid.at(x, 2).walkable = False
|
grid.at(x, 2).walkable = False
|
||||||
|
|
||||||
entity = mcrfpy.Entity(1, 1)
|
entity = mcrfpy.Entity((1, 1), grid=grid)
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path = entity.path_to(1, 4)
|
path = entity.path_to(1, 4)
|
||||||
|
|
|
||||||
|
|
@ -13,32 +13,28 @@ def test_grid_background():
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
# Create a grid with default background
|
# Create a grid with default background
|
||||||
grid = mcrfpy.Grid(20, 15, grid_size=(20, 15))
|
grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15))
|
||||||
grid.x = 50
|
|
||||||
grid.y = 50
|
|
||||||
grid.w = 400
|
|
||||||
grid.h = 300
|
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
# Add some tiles to see the background better
|
# Add color layer for some tiles to see the background better
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
for x in range(5, 15):
|
for x in range(5, 15):
|
||||||
for y in range(5, 10):
|
for y in range(5, 10):
|
||||||
point = grid.at(x, y)
|
color_layer.set(x, y, mcrfpy.Color(100, 150, 100))
|
||||||
point.color = mcrfpy.Color(100, 150, 100)
|
|
||||||
|
|
||||||
# Add UI to show current background color
|
# Add UI to show current background color
|
||||||
info_frame = mcrfpy.Frame(500, 50, 200, 150,
|
info_frame = mcrfpy.Frame(pos=(500, 50), size=(200, 150),
|
||||||
fill_color=mcrfpy.Color(40, 40, 40),
|
fill_color=mcrfpy.Color(40, 40, 40),
|
||||||
outline_color=mcrfpy.Color(200, 200, 200),
|
outline_color=mcrfpy.Color(200, 200, 200),
|
||||||
outline=2)
|
outline=2)
|
||||||
ui.append(info_frame)
|
ui.append(info_frame)
|
||||||
|
|
||||||
color_caption = mcrfpy.Caption(510, 60, "Background Color:")
|
color_caption = mcrfpy.Caption(pos=(510, 60), text="Background Color:")
|
||||||
color_caption.font_size = 14
|
color_caption.font_size = 14
|
||||||
color_caption.fill_color = mcrfpy.Color(255, 255, 255)
|
color_caption.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
info_frame.children.append(color_caption)
|
info_frame.children.append(color_caption)
|
||||||
|
|
||||||
color_display = mcrfpy.Caption(510, 80, "")
|
color_display = mcrfpy.Caption(pos=(510, 80), text="")
|
||||||
color_display.font_size = 12
|
color_display.font_size = 12
|
||||||
color_display.fill_color = mcrfpy.Color(200, 200, 200)
|
color_display.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
info_frame.children.append(color_display)
|
info_frame.children.append(color_display)
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ ui = mcrfpy.sceneUI("detect_test")
|
||||||
mcrfpy.setScene("detect_test")
|
mcrfpy.setScene("detect_test")
|
||||||
|
|
||||||
# Create a frame
|
# Create a frame
|
||||||
frame = mcrfpy.Frame(100, 100, 200, 200)
|
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||||
frame.fill_color = (255, 100, 100, 255)
|
frame.fill_color = mcrfpy.Color(255, 100, 100, 255)
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
def test_mode(runtime):
|
def test_mode(runtime):
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ ui = mcrfpy.sceneUI("headless_test")
|
||||||
mcrfpy.setScene("headless_test")
|
mcrfpy.setScene("headless_test")
|
||||||
|
|
||||||
# Create a visible indicator
|
# Create a visible indicator
|
||||||
frame = mcrfpy.Frame(200, 200, 400, 200)
|
frame = mcrfpy.Frame(pos=(200, 200), size=(400, 200))
|
||||||
frame.fill_color = (100, 200, 100, 255)
|
frame.fill_color = mcrfpy.Color(100, 200, 100, 255)
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
caption = mcrfpy.Caption((400, 300), "If you see this, windowed mode is working!", mcrfpy.default_font)
|
caption = mcrfpy.Caption(pos=(400, 300), text="If you see this, windowed mode is working!")
|
||||||
caption.size = 24
|
caption.font_size = 24
|
||||||
caption.fill_color = (255, 255, 255)
|
caption.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(caption)
|
ui.append(caption)
|
||||||
|
|
||||||
print("Script started. Window should appear unless --headless was specified.")
|
print("Script started. Window should appear unless --headless was specified.")
|
||||||
|
|
|
||||||
|
|
@ -115,18 +115,18 @@ mcrfpy.setScene("metrics_test")
|
||||||
ui = mcrfpy.sceneUI("metrics_test")
|
ui = mcrfpy.sceneUI("metrics_test")
|
||||||
|
|
||||||
# Create various UI elements
|
# Create various UI elements
|
||||||
frame1 = mcrfpy.Frame(10, 10, 200, 150)
|
frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
|
||||||
frame1.fill_color = (100, 100, 100, 128)
|
frame1.fill_color = mcrfpy.Color(100, 100, 100, 128)
|
||||||
ui.append(frame1)
|
ui.append(frame1)
|
||||||
|
|
||||||
caption1 = mcrfpy.Caption("Test Caption", 50, 50)
|
caption1 = mcrfpy.Caption(pos=(50, 50), text="Test Caption")
|
||||||
ui.append(caption1)
|
ui.append(caption1)
|
||||||
|
|
||||||
sprite1 = mcrfpy.Sprite(100, 100)
|
sprite1 = mcrfpy.Sprite(pos=(100, 100))
|
||||||
ui.append(sprite1)
|
ui.append(sprite1)
|
||||||
|
|
||||||
# Invisible element (should not count as visible)
|
# Invisible element (should not count as visible)
|
||||||
frame2 = mcrfpy.Frame(300, 10, 100, 100)
|
frame2 = mcrfpy.Frame(pos=(300, 10), size=(100, 100))
|
||||||
frame2.visible = False
|
frame2.visible = False
|
||||||
ui.append(frame2)
|
ui.append(frame2)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,20 @@ print("=" * 50)
|
||||||
mcrfpy.createScene("test")
|
mcrfpy.createScene("test")
|
||||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Initialize
|
# Initialize
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(5):
|
for x in range(5):
|
||||||
grid.at(x, y).walkable = True
|
grid.at(x, y).walkable = True
|
||||||
grid.at(x, y).color = mcrfpy.Color(200, 200, 200) # Light gray
|
color_layer.set(x, y, mcrfpy.Color(200, 200, 200)) # Light gray
|
||||||
|
|
||||||
# Add entities
|
# Add entities
|
||||||
e1 = mcrfpy.Entity(0, 0)
|
e1 = mcrfpy.Entity((0, 0), grid=grid)
|
||||||
e2 = mcrfpy.Entity(4, 4)
|
e2 = mcrfpy.Entity((4, 4), grid=grid)
|
||||||
grid.entities.append(e1)
|
e1.sprite_index = 64
|
||||||
grid.entities.append(e2)
|
e2.sprite_index = 69
|
||||||
|
|
||||||
print(f"Entity 1 at ({e1.x}, {e1.y})")
|
print(f"Entity 1 at ({e1.x}, {e1.y})")
|
||||||
print(f"Entity 2 at ({e2.x}, {e2.y})")
|
print(f"Entity 2 at ({e2.x}, {e2.y})")
|
||||||
|
|
@ -35,24 +38,25 @@ PATH_COLOR = mcrfpy.Color(100, 255, 100) # Green
|
||||||
print(f"\nSetting path cells to green ({PATH_COLOR.r}, {PATH_COLOR.g}, {PATH_COLOR.b})...")
|
print(f"\nSetting path cells to green ({PATH_COLOR.r}, {PATH_COLOR.g}, {PATH_COLOR.b})...")
|
||||||
|
|
||||||
for x, y in path:
|
for x, y in path:
|
||||||
cell = grid.at(x, y)
|
|
||||||
# Check before
|
# Check before
|
||||||
before = cell.color[:3] # Get RGB from tuple
|
before_c = color_layer.at(x, y)
|
||||||
|
before = (before_c.r, before_c.g, before_c.b)
|
||||||
|
|
||||||
# Set color
|
# Set color
|
||||||
cell.color = PATH_COLOR
|
color_layer.set(x, y, PATH_COLOR)
|
||||||
|
|
||||||
# Check after
|
# Check after
|
||||||
after = cell.color[:3] # Get RGB from tuple
|
after_c = color_layer.at(x, y)
|
||||||
|
after = (after_c.r, after_c.g, after_c.b)
|
||||||
|
|
||||||
print(f" Cell ({x},{y}): {before} -> {after}")
|
print(f" Cell ({x},{y}): {before} -> {after}")
|
||||||
|
|
||||||
# Verify all path cells
|
# Verify all path cells
|
||||||
print("\nVerifying all cells in grid:")
|
print("\nVerifying all cells in grid:")
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(5):
|
for x in range(5):
|
||||||
cell = grid.at(x, y)
|
c = color_layer.at(x, y)
|
||||||
color = cell.color[:3] # Get RGB from tuple
|
color = (c.r, c.g, c.b)
|
||||||
is_path = (x, y) in path
|
is_path = (x, y) in path
|
||||||
print(f" ({x},{y}): color={color}, in_path={is_path}")
|
print(f" ({x},{y}): color={color}, in_path={is_path}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,8 @@ for i in range(5):
|
||||||
grid.at(5, i + 2).walkable = False
|
grid.at(5, i + 2).walkable = False
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
e1 = mcrfpy.Entity(2, 5)
|
e1 = mcrfpy.Entity((2, 5), grid=grid)
|
||||||
e2 = mcrfpy.Entity(8, 5)
|
e2 = mcrfpy.Entity((8, 5), grid=grid)
|
||||||
grid.entities.append(e1)
|
|
||||||
grid.entities.append(e2)
|
|
||||||
|
|
||||||
# Test pathfinding between entities
|
# Test pathfinding between entities
|
||||||
print(f"Entity 1 at ({e1.x}, {e1.y})")
|
print(f"Entity 1 at ({e1.x}, {e1.y})")
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ def test_properties(runtime):
|
||||||
|
|
||||||
# Test Frame
|
# Test Frame
|
||||||
try:
|
try:
|
||||||
frame = mcrfpy.Frame(10, 10, 100, 100)
|
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
|
||||||
print(f"Frame visible: {frame.visible}")
|
print(f"Frame visible: {frame.visible}")
|
||||||
frame.visible = False
|
frame.visible = False
|
||||||
print(f"Frame visible after setting to False: {frame.visible}")
|
print(f"Frame visible after setting to False: {frame.visible}")
|
||||||
|
|
|
||||||
235
tests/unit/test_scene_object_api.py
Normal file
235
tests/unit/test_scene_object_api.py
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test the object-oriented Scene API (alternative to module-level functions).
|
||||||
|
|
||||||
|
The Scene object provides an OOP approach to scene management with several advantages:
|
||||||
|
1. `scene.on_key` can be set on ANY scene, not just the current one
|
||||||
|
2. `scene.children` provides direct access to UI elements
|
||||||
|
3. Subclassing enables lifecycle callbacks (on_enter, on_exit, update, etc.)
|
||||||
|
|
||||||
|
This is the recommended approach for new code, replacing:
|
||||||
|
- mcrfpy.createScene(name) -> scene = mcrfpy.Scene(name)
|
||||||
|
- mcrfpy.setScene(name) -> scene.activate()
|
||||||
|
- mcrfpy.sceneUI(name) -> scene.children
|
||||||
|
- mcrfpy.keypressScene(callback) -> scene.on_key = callback
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_scene_object_basics():
|
||||||
|
"""Test basic Scene object creation and properties."""
|
||||||
|
print("=== Test: Scene Object Basics ===")
|
||||||
|
|
||||||
|
# Create scene using object-oriented approach
|
||||||
|
scene = mcrfpy.Scene("oop_test")
|
||||||
|
|
||||||
|
# Check name property
|
||||||
|
assert scene.name == "oop_test", f"Expected 'oop_test', got '{scene.name}'"
|
||||||
|
print(f" name: {scene.name}")
|
||||||
|
|
||||||
|
# Check active property (should be False, not yet activated)
|
||||||
|
print(f" active: {scene.active}")
|
||||||
|
|
||||||
|
# Check children property returns UICollection
|
||||||
|
children = scene.children
|
||||||
|
print(f" children type: {type(children).__name__}")
|
||||||
|
print(f" children count: {len(children)}")
|
||||||
|
|
||||||
|
# Add UI elements via children
|
||||||
|
frame = mcrfpy.Frame(pos=(50, 50), size=(200, 100), fill_color=mcrfpy.Color(100, 100, 200))
|
||||||
|
scene.children.append(frame)
|
||||||
|
print(f" children count after append: {len(scene.children)}")
|
||||||
|
|
||||||
|
print(" PASS: Basic properties work correctly")
|
||||||
|
return scene
|
||||||
|
|
||||||
|
def test_scene_activation():
|
||||||
|
"""Test scene activation."""
|
||||||
|
print("\n=== Test: Scene Activation ===")
|
||||||
|
|
||||||
|
scene1 = mcrfpy.Scene("scene_a")
|
||||||
|
scene2 = mcrfpy.Scene("scene_b")
|
||||||
|
|
||||||
|
# Neither active yet
|
||||||
|
print(f" Before activation - scene1.active: {scene1.active}, scene2.active: {scene2.active}")
|
||||||
|
|
||||||
|
# Activate scene1
|
||||||
|
scene1.activate()
|
||||||
|
print(f" After scene1.activate() - scene1.active: {scene1.active}, scene2.active: {scene2.active}")
|
||||||
|
assert scene1.active == True, "scene1 should be active"
|
||||||
|
assert scene2.active == False, "scene2 should not be active"
|
||||||
|
|
||||||
|
# Activate scene2
|
||||||
|
scene2.activate()
|
||||||
|
print(f" After scene2.activate() - scene1.active: {scene1.active}, scene2.active: {scene2.active}")
|
||||||
|
assert scene1.active == False, "scene1 should not be active now"
|
||||||
|
assert scene2.active == True, "scene2 should be active"
|
||||||
|
|
||||||
|
print(" PASS: Scene activation works correctly")
|
||||||
|
|
||||||
|
def test_scene_on_key():
|
||||||
|
"""Test setting on_key callback on scene objects.
|
||||||
|
|
||||||
|
This is the KEY ADVANTAGE over module-level keypressScene():
|
||||||
|
You can set on_key on ANY scene, not just the current one!
|
||||||
|
"""
|
||||||
|
print("\n=== Test: Scene on_key Property ===")
|
||||||
|
|
||||||
|
scene1 = mcrfpy.Scene("keys_scene1")
|
||||||
|
scene2 = mcrfpy.Scene("keys_scene2")
|
||||||
|
|
||||||
|
# Track which callback was called
|
||||||
|
callback_log = []
|
||||||
|
|
||||||
|
def scene1_keyhandler(key, action):
|
||||||
|
callback_log.append(("scene1", key, action))
|
||||||
|
|
||||||
|
def scene2_keyhandler(key, action):
|
||||||
|
callback_log.append(("scene2", key, action))
|
||||||
|
|
||||||
|
# Set callbacks on BOTH scenes BEFORE activating either
|
||||||
|
# This is impossible with keypressScene() which only works on current scene!
|
||||||
|
scene1.on_key = scene1_keyhandler
|
||||||
|
scene2.on_key = scene2_keyhandler
|
||||||
|
|
||||||
|
print(f" scene1.on_key set: {scene1.on_key is not None}")
|
||||||
|
print(f" scene2.on_key set: {scene2.on_key is not None}")
|
||||||
|
|
||||||
|
# Verify callbacks are retrievable
|
||||||
|
assert callable(scene1.on_key), "scene1.on_key should be callable"
|
||||||
|
assert callable(scene2.on_key), "scene2.on_key should be callable"
|
||||||
|
|
||||||
|
# Test clearing callback
|
||||||
|
scene1.on_key = None
|
||||||
|
assert scene1.on_key is None, "scene1.on_key should be None after clearing"
|
||||||
|
print(" scene1.on_key cleared successfully")
|
||||||
|
|
||||||
|
# Re-set it
|
||||||
|
scene1.on_key = scene1_keyhandler
|
||||||
|
|
||||||
|
print(" PASS: on_key property works correctly")
|
||||||
|
|
||||||
|
def test_scene_visual_properties():
|
||||||
|
"""Test scene-level visual properties (pos, visible, opacity)."""
|
||||||
|
print("\n=== Test: Scene Visual Properties ===")
|
||||||
|
|
||||||
|
scene = mcrfpy.Scene("visual_props_test")
|
||||||
|
|
||||||
|
# Test pos property
|
||||||
|
print(f" Initial pos: {scene.pos}")
|
||||||
|
scene.pos = (100, 50)
|
||||||
|
print(f" After setting pos=(100, 50): {scene.pos}")
|
||||||
|
|
||||||
|
# Test visible property
|
||||||
|
print(f" Initial visible: {scene.visible}")
|
||||||
|
scene.visible = False
|
||||||
|
print(f" After setting visible=False: {scene.visible}")
|
||||||
|
assert scene.visible == False, "visible should be False"
|
||||||
|
scene.visible = True
|
||||||
|
|
||||||
|
# Test opacity property
|
||||||
|
print(f" Initial opacity: {scene.opacity}")
|
||||||
|
scene.opacity = 0.5
|
||||||
|
print(f" After setting opacity=0.5: {scene.opacity}")
|
||||||
|
assert 0.49 < scene.opacity < 0.51, f"opacity should be ~0.5, got {scene.opacity}"
|
||||||
|
|
||||||
|
print(" PASS: Visual properties work correctly")
|
||||||
|
|
||||||
|
def test_scene_subclass():
|
||||||
|
"""Test subclassing Scene for lifecycle callbacks."""
|
||||||
|
print("\n=== Test: Scene Subclass with Lifecycle ===")
|
||||||
|
|
||||||
|
class GameScene(mcrfpy.Scene):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.enter_count = 0
|
||||||
|
self.exit_count = 0
|
||||||
|
self.update_count = 0
|
||||||
|
|
||||||
|
def on_enter(self):
|
||||||
|
self.enter_count += 1
|
||||||
|
print(f" GameScene.on_enter() called (count: {self.enter_count})")
|
||||||
|
|
||||||
|
def on_exit(self):
|
||||||
|
self.exit_count += 1
|
||||||
|
print(f" GameScene.on_exit() called (count: {self.exit_count})")
|
||||||
|
|
||||||
|
def on_keypress(self, key, action):
|
||||||
|
print(f" GameScene.on_keypress({key}, {action})")
|
||||||
|
|
||||||
|
def update(self, dt):
|
||||||
|
self.update_count += 1
|
||||||
|
# Note: update is called every frame, so we don't print
|
||||||
|
|
||||||
|
game_scene = GameScene("game_scene_test")
|
||||||
|
other_scene = mcrfpy.Scene("other_scene_test")
|
||||||
|
|
||||||
|
# Add some UI to game scene
|
||||||
|
game_scene.children.append(
|
||||||
|
mcrfpy.Caption(pos=(100, 100), text="Game Scene", fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" Created GameScene with {len(game_scene.children)} children")
|
||||||
|
print(f" enter_count before activation: {game_scene.enter_count}")
|
||||||
|
|
||||||
|
# Activate - should trigger on_enter
|
||||||
|
game_scene.activate()
|
||||||
|
print(f" enter_count after activation: {game_scene.enter_count}")
|
||||||
|
|
||||||
|
# Switch away - should trigger on_exit
|
||||||
|
other_scene.activate()
|
||||||
|
print(f" exit_count after switching: {game_scene.exit_count}")
|
||||||
|
|
||||||
|
print(" PASS: Subclassing works correctly")
|
||||||
|
|
||||||
|
def test_comparison_with_module_functions():
|
||||||
|
"""Demonstrate the difference between old and new approaches."""
|
||||||
|
print("\n=== Comparison: Module Functions vs Scene Objects ===")
|
||||||
|
|
||||||
|
print("\n OLD APPROACH (module-level functions):")
|
||||||
|
print(" mcrfpy.createScene('my_scene')")
|
||||||
|
print(" mcrfpy.setScene('my_scene')")
|
||||||
|
print(" ui = mcrfpy.sceneUI('my_scene')")
|
||||||
|
print(" ui.append(mcrfpy.Frame(...))")
|
||||||
|
print(" mcrfpy.keypressScene(handler) # ONLY works on current scene!")
|
||||||
|
|
||||||
|
print("\n NEW APPROACH (Scene objects):")
|
||||||
|
print(" scene = mcrfpy.Scene('my_scene')")
|
||||||
|
print(" scene.activate()")
|
||||||
|
print(" scene.children.append(mcrfpy.Frame(...))")
|
||||||
|
print(" scene.on_key = handler # Works on ANY scene!")
|
||||||
|
|
||||||
|
print("\n KEY BENEFITS:")
|
||||||
|
print(" 1. scene.on_key can be set on non-active scenes")
|
||||||
|
print(" 2. Subclassing enables on_enter/on_exit/update callbacks")
|
||||||
|
print(" 3. Object reference makes code more readable")
|
||||||
|
print(" 4. scene.children is clearer than sceneUI(name)")
|
||||||
|
|
||||||
|
print("\n PASS: Documentation complete")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all Scene object API tests."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Scene Object API Test Suite")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_scene_object_basics()
|
||||||
|
test_scene_activation()
|
||||||
|
test_scene_on_key()
|
||||||
|
test_scene_visual_properties()
|
||||||
|
test_scene_subclass()
|
||||||
|
test_comparison_with_module_functions()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("ALL TESTS PASSED!")
|
||||||
|
print("=" * 60)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nTEST FAILED: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -11,51 +11,51 @@ def create_test_scenes():
|
||||||
# Scene 1: Red background
|
# Scene 1: Red background
|
||||||
mcrfpy.createScene("red_scene")
|
mcrfpy.createScene("red_scene")
|
||||||
ui1 = mcrfpy.sceneUI("red_scene")
|
ui1 = mcrfpy.sceneUI("red_scene")
|
||||||
bg1 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(255, 0, 0, 255))
|
bg1 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(255, 0, 0, 255))
|
||||||
label1 = mcrfpy.Caption(512, 384, "RED SCENE", font=mcrfpy.Font.font_ui)
|
label1 = mcrfpy.Caption(pos=(512, 384), text="RED SCENE", font=mcrfpy.Font.font_ui)
|
||||||
label1.color = mcrfpy.Color(255, 255, 255, 255)
|
label1.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
ui1.append(bg1)
|
ui1.append(bg1)
|
||||||
ui1.append(label1)
|
ui1.append(label1)
|
||||||
|
|
||||||
# Scene 2: Blue background
|
# Scene 2: Blue background
|
||||||
mcrfpy.createScene("blue_scene")
|
mcrfpy.createScene("blue_scene")
|
||||||
ui2 = mcrfpy.sceneUI("blue_scene")
|
ui2 = mcrfpy.sceneUI("blue_scene")
|
||||||
bg2 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 0, 255, 255))
|
bg2 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(0, 0, 255, 255))
|
||||||
label2 = mcrfpy.Caption(512, 384, "BLUE SCENE", font=mcrfpy.Font.font_ui)
|
label2 = mcrfpy.Caption(pos=(512, 384), text="BLUE SCENE", font=mcrfpy.Font.font_ui)
|
||||||
label2.color = mcrfpy.Color(255, 255, 255, 255)
|
label2.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
ui2.append(bg2)
|
ui2.append(bg2)
|
||||||
ui2.append(label2)
|
ui2.append(label2)
|
||||||
|
|
||||||
# Scene 3: Green background
|
# Scene 3: Green background
|
||||||
mcrfpy.createScene("green_scene")
|
mcrfpy.createScene("green_scene")
|
||||||
ui3 = mcrfpy.sceneUI("green_scene")
|
ui3 = mcrfpy.sceneUI("green_scene")
|
||||||
bg3 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(0, 255, 0, 255))
|
bg3 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(0, 255, 0, 255))
|
||||||
label3 = mcrfpy.Caption(512, 384, "GREEN SCENE", font=mcrfpy.Font.font_ui)
|
label3 = mcrfpy.Caption(pos=(512, 384), text="GREEN SCENE", font=mcrfpy.Font.font_ui)
|
||||||
label3.color = mcrfpy.Color(0, 0, 0, 255) # Black text on green
|
label3.fill_color = mcrfpy.Color(0, 0, 0, 255) # Black text on green
|
||||||
ui3.append(bg3)
|
ui3.append(bg3)
|
||||||
ui3.append(label3)
|
ui3.append(label3)
|
||||||
|
|
||||||
# Scene 4: Menu scene with buttons
|
# Scene 4: Menu scene with buttons
|
||||||
mcrfpy.createScene("menu_scene")
|
mcrfpy.createScene("menu_scene")
|
||||||
ui4 = mcrfpy.sceneUI("menu_scene")
|
ui4 = mcrfpy.sceneUI("menu_scene")
|
||||||
bg4 = mcrfpy.Frame(0, 0, 1024, 768, fill_color=mcrfpy.Color(50, 50, 50, 255))
|
bg4 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(50, 50, 50, 255))
|
||||||
|
|
||||||
title = mcrfpy.Caption(512, 100, "SCENE TRANSITION DEMO", font=mcrfpy.Font.font_ui)
|
title = mcrfpy.Caption(pos=(512, 100), text="SCENE TRANSITION DEMO", font=mcrfpy.Font.font_ui)
|
||||||
title.color = mcrfpy.Color(255, 255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
ui4.append(bg4)
|
ui4.append(bg4)
|
||||||
ui4.append(title)
|
ui4.append(title)
|
||||||
|
|
||||||
# Add instruction text
|
# Add instruction text
|
||||||
instructions = mcrfpy.Caption(512, 200, "Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui)
|
instructions = mcrfpy.Caption(pos=(512, 200), text="Press keys 1-6 for different transitions", font=mcrfpy.Font.font_ui)
|
||||||
instructions.color = mcrfpy.Color(200, 200, 200, 255)
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
ui4.append(instructions)
|
ui4.append(instructions)
|
||||||
|
|
||||||
controls = mcrfpy.Caption(512, 250, "1: Fade | 2: Slide Left | 3: Slide Right | 4: Slide Up | 5: Slide Down | 6: Instant", font=mcrfpy.Font.font_ui)
|
controls = mcrfpy.Caption(pos=(512, 250), text="1: Fade | 2: Slide Left | 3: Slide Right | 4: Slide Up | 5: Slide Down | 6: Instant", font=mcrfpy.Font.font_ui)
|
||||||
controls.color = mcrfpy.Color(150, 150, 150, 255)
|
controls.fill_color = mcrfpy.Color(150, 150, 150, 255)
|
||||||
ui4.append(controls)
|
ui4.append(controls)
|
||||||
|
|
||||||
scene_info = mcrfpy.Caption(512, 300, "R: Red Scene | B: Blue Scene | G: Green Scene | M: Menu", font=mcrfpy.Font.font_ui)
|
scene_info = mcrfpy.Caption(pos=(512, 300), text="R: Red Scene | B: Blue Scene | G: Green Scene | M: Menu", font=mcrfpy.Font.font_ui)
|
||||||
scene_info.color = mcrfpy.Color(150, 150, 150, 255)
|
scene_info.fill_color = mcrfpy.Color(150, 150, 150, 255)
|
||||||
ui4.append(scene_info)
|
ui4.append(scene_info)
|
||||||
|
|
||||||
print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene")
|
print("Created test scenes: red_scene, blue_scene, green_scene, menu_scene")
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ def test_scene_transitions():
|
||||||
# Scene 1
|
# Scene 1
|
||||||
mcrfpy.createScene("scene1")
|
mcrfpy.createScene("scene1")
|
||||||
ui1 = mcrfpy.sceneUI("scene1")
|
ui1 = mcrfpy.sceneUI("scene1")
|
||||||
frame1 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(255, 0, 0))
|
frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=mcrfpy.Color(255, 0, 0))
|
||||||
ui1.append(frame1)
|
ui1.append(frame1)
|
||||||
|
|
||||||
# Scene 2
|
# Scene 2
|
||||||
mcrfpy.createScene("scene2")
|
mcrfpy.createScene("scene2")
|
||||||
ui2 = mcrfpy.sceneUI("scene2")
|
ui2 = mcrfpy.sceneUI("scene2")
|
||||||
frame2 = mcrfpy.Frame(0, 0, 100, 100, fill_color=mcrfpy.Color(0, 0, 255))
|
frame2 = mcrfpy.Frame(pos=(0, 0), size=(100, 100), fill_color=mcrfpy.Color(0, 0, 255))
|
||||||
ui2.append(frame2)
|
ui2.append(frame2)
|
||||||
|
|
||||||
# Test each transition type
|
# Test each transition type
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ def simple_test(runtime):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Test basic functionality
|
# Test basic functionality
|
||||||
frame = mcrfpy.Frame(10, 10, 100, 100)
|
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
|
||||||
print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}")
|
print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}")
|
||||||
|
|
||||||
bounds = frame.get_bounds()
|
bounds = frame.get_bounds()
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ def create_demo():
|
||||||
scene = mcrfpy.sceneUI("text_demo")
|
scene = mcrfpy.sceneUI("text_demo")
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
bg = mcrfpy.Frame(0, 0, 800, 600)
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
|
||||||
bg.fill_color = (40, 40, 40, 255)
|
bg.fill_color = mcrfpy.Color(40, 40, 40, 255)
|
||||||
scene.append(bg)
|
scene.append(bg)
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = mcrfpy.Caption("Text Input Widget Demo", 20, 20)
|
title = mcrfpy.Caption(pos=(20, 20), text="Text Input Widget Demo")
|
||||||
title.fill_color = (255, 255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
scene.append(title)
|
scene.append(title)
|
||||||
|
|
||||||
# Focus manager
|
# Focus manager
|
||||||
|
|
@ -62,8 +62,8 @@ def create_demo():
|
||||||
inputs.append(comment_input)
|
inputs.append(comment_input)
|
||||||
|
|
||||||
# Status display
|
# Status display
|
||||||
status = mcrfpy.Caption("Ready for input...", 50, 360)
|
status = mcrfpy.Caption(pos=(50, 360), text="Ready for input...")
|
||||||
status.fill_color = (150, 255, 150, 255)
|
status.fill_color = mcrfpy.Color(150, 255, 150, 255)
|
||||||
scene.append(status)
|
scene.append(status)
|
||||||
|
|
||||||
# Update handler
|
# Update handler
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,11 @@ def main():
|
||||||
mcrfpy.setScene("test")
|
mcrfpy.setScene("test")
|
||||||
|
|
||||||
# Create multiple captions for testing
|
# Create multiple captions for testing
|
||||||
caption1 = mcrfpy.Caption(50, 50, "Caption 1: Normal", fill_color=(255, 255, 255))
|
caption1 = mcrfpy.Caption(pos=(50, 50), text="Caption 1: Normal", fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
caption2 = mcrfpy.Caption(50, 100, "Caption 2: Will be invisible", fill_color=(255, 200, 200))
|
caption2 = mcrfpy.Caption(pos=(50, 100), text="Caption 2: Will be invisible", fill_color=mcrfpy.Color(255, 200, 200))
|
||||||
caption3 = mcrfpy.Caption(50, 150, "Caption 3: 50% opacity", fill_color=(200, 255, 200))
|
caption3 = mcrfpy.Caption(pos=(50, 150), text="Caption 3: 50% opacity", fill_color=mcrfpy.Color(200, 255, 200))
|
||||||
caption4 = mcrfpy.Caption(50, 200, "Caption 4: 25% opacity", fill_color=(200, 200, 255))
|
caption4 = mcrfpy.Caption(pos=(50, 200), text="Caption 4: 25% opacity", fill_color=mcrfpy.Color(200, 200, 255))
|
||||||
caption5 = mcrfpy.Caption(50, 250, "Caption 5: 0% opacity", fill_color=(255, 255, 200))
|
caption5 = mcrfpy.Caption(pos=(50, 250), text="Caption 5: 0% opacity", fill_color=mcrfpy.Color(255, 255, 200))
|
||||||
|
|
||||||
# Add captions to scene
|
# Add captions to scene
|
||||||
ui = mcrfpy.sceneUI("test")
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
@ -84,7 +84,7 @@ def main():
|
||||||
ui.append(caption5)
|
ui.append(caption5)
|
||||||
|
|
||||||
# Also add a frame as background to see transparency better
|
# Also add a frame as background to see transparency better
|
||||||
frame = mcrfpy.Frame(40, 40, 400, 250, fill_color=(50, 50, 50))
|
frame = mcrfpy.Frame(pos=(40, 40), size=(400, 250), fill_color=mcrfpy.Color(50, 50, 50))
|
||||||
frame.z_index = -1 # Put it behind the captions
|
frame.z_index = -1 # Put it behind the captions
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ mcrfpy.createScene("visibility_test")
|
||||||
grid = mcrfpy.Grid(grid_x=20, grid_y=15)
|
grid = mcrfpy.Grid(grid_x=20, grid_y=15)
|
||||||
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
|
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
|
||||||
|
|
||||||
|
# Add a color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
# Initialize grid - all walkable and transparent
|
# Initialize grid - all walkable and transparent
|
||||||
print("\nInitializing 20x15 grid...")
|
print("\nInitializing 20x15 grid...")
|
||||||
for y in range(15):
|
for y in range(15):
|
||||||
|
|
@ -25,7 +28,7 @@ for y in range(15):
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = True
|
cell.walkable = True
|
||||||
cell.transparent = True
|
cell.transparent = True
|
||||||
cell.color = mcrfpy.Color(100, 100, 120) # Floor color
|
color_layer.set(x, y, mcrfpy.Color(100, 100, 120)) # Floor color
|
||||||
|
|
||||||
# Create some walls to block vision
|
# Create some walls to block vision
|
||||||
print("Adding walls...")
|
print("Adding walls...")
|
||||||
|
|
@ -47,14 +50,14 @@ for wall_group in walls:
|
||||||
cell = grid.at(x, y)
|
cell = grid.at(x, y)
|
||||||
cell.walkable = False
|
cell.walkable = False
|
||||||
cell.transparent = False
|
cell.transparent = False
|
||||||
cell.color = mcrfpy.Color(40, 20, 20) # Wall color
|
color_layer.set(x, y, mcrfpy.Color(40, 20, 20)) # Wall color
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
print("\nCreating entities...")
|
print("\nCreating entities...")
|
||||||
entities = [
|
entities = [
|
||||||
mcrfpy.Entity(2, 7), # Left side
|
mcrfpy.Entity((2, 7)), # Left side
|
||||||
mcrfpy.Entity(18, 7), # Right side
|
mcrfpy.Entity((18, 7)), # Right side
|
||||||
mcrfpy.Entity(10, 1), # Top center (above wall)
|
mcrfpy.Entity((10, 1)), # Top center (above wall)
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, entity in enumerate(entities):
|
for i, entity in enumerate(entities):
|
||||||
|
|
@ -138,17 +141,17 @@ grid.position = (50, 50)
|
||||||
grid.size = (600, 450) # 20*30, 15*30
|
grid.size = (600, 450) # 20*30, 15*30
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Knowledge Stubs 1 - Visibility Test", 200, 10)
|
title = mcrfpy.Caption(pos=(200, 10), text="Knowledge Stubs 1 - Visibility Test")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Add info
|
# Add info
|
||||||
info = mcrfpy.Caption("Perspective: -1 (omniscient)", 50, 520)
|
info = mcrfpy.Caption(pos=(50, 520), text="Perspective: -1 (omniscient)")
|
||||||
info.fill_color = mcrfpy.Color(200, 200, 200)
|
info.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
ui.append(info)
|
ui.append(info)
|
||||||
|
|
||||||
# Add legend
|
# Add legend
|
||||||
legend = mcrfpy.Caption("Black=Never seen, Dark gray=Discovered, Normal=Visible", 50, 540)
|
legend = mcrfpy.Caption(pos=(50, 540), text="Black=Never seen, Dark gray=Discovered, Normal=Visible")
|
||||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
ui.append(legend)
|
ui.append(legend)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,10 @@
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Colors as tuples (r, g, b, a)
|
# Colors
|
||||||
WALL_COLOR = (60, 30, 30, 255)
|
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||||
FLOOR_COLOR = (200, 200, 220, 255)
|
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
|
||||||
PATH_COLOR = (100, 255, 100, 255)
|
PATH_COLOR = mcrfpy.Color(100, 255, 100)
|
||||||
|
|
||||||
def check_render(dt):
|
|
||||||
"""Timer callback to verify rendering"""
|
|
||||||
print(f"\nTimer fired after {dt}ms")
|
|
||||||
|
|
||||||
# Take screenshot
|
|
||||||
from mcrfpy import automation
|
|
||||||
automation.screenshot("visual_path_test.png")
|
|
||||||
print("Screenshot saved as visual_path_test.png")
|
|
||||||
|
|
||||||
# Sample some path cells to verify colors
|
|
||||||
print("\nSampling path cell colors from grid:")
|
|
||||||
for x, y in [(1, 1), (2, 2), (3, 3)]:
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
color = cell.color
|
|
||||||
print(f" Cell ({x},{y}): color={color[:3]}")
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Create scene
|
# Create scene
|
||||||
mcrfpy.createScene("visual_test")
|
mcrfpy.createScene("visual_test")
|
||||||
|
|
@ -34,20 +16,38 @@ mcrfpy.createScene("visual_test")
|
||||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
|
||||||
|
# Add color layer for cell coloring
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
|
|
||||||
|
def check_render(dt):
|
||||||
|
"""Timer callback to verify rendering"""
|
||||||
|
print(f"\nTimer fired after {dt}ms")
|
||||||
|
|
||||||
|
# Take screenshot
|
||||||
|
from mcrfpy import automation
|
||||||
|
automation.screenshot("visual_path_test.png")
|
||||||
|
print("Screenshot saved as visual_path_test.png")
|
||||||
|
|
||||||
|
# Sample some path cells to verify colors
|
||||||
|
print("\nSampling path cell colors from grid:")
|
||||||
|
for x, y in [(1, 1), (2, 2), (3, 3)]:
|
||||||
|
color = color_layer.at(x, y)
|
||||||
|
print(f" Cell ({x},{y}): color=({color.r}, {color.g}, {color.b})")
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# Initialize all cells as floor
|
# Initialize all cells as floor
|
||||||
print("Initializing grid...")
|
print("Initializing grid...")
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(5):
|
for x in range(5):
|
||||||
grid.at(x, y).walkable = True
|
grid.at(x, y).walkable = True
|
||||||
grid.at(x, y).color = FLOOR_COLOR
|
color_layer.set(x, y, FLOOR_COLOR)
|
||||||
|
|
||||||
# Create entities
|
# Create entities
|
||||||
e1 = mcrfpy.Entity(0, 0)
|
e1 = mcrfpy.Entity((0, 0), grid=grid)
|
||||||
e2 = mcrfpy.Entity(4, 4)
|
e2 = mcrfpy.Entity((4, 4), grid=grid)
|
||||||
e1.sprite_index = 64 # @
|
e1.sprite_index = 64 # @
|
||||||
e2.sprite_index = 69 # E
|
e2.sprite_index = 69 # E
|
||||||
grid.entities.append(e1)
|
|
||||||
grid.entities.append(e2)
|
|
||||||
|
|
||||||
print(f"Entity 1 at ({e1.x}, {e1.y})")
|
print(f"Entity 1 at ({e1.x}, {e1.y})")
|
||||||
print(f"Entity 2 at ({e2.x}, {e2.y})")
|
print(f"Entity 2 at ({e2.x}, {e2.y})")
|
||||||
|
|
@ -60,7 +60,7 @@ print(f"\nPath from E1 to E2: {path}")
|
||||||
if path:
|
if path:
|
||||||
print("\nColoring path cells green...")
|
print("\nColoring path cells green...")
|
||||||
for x, y in path:
|
for x, y in path:
|
||||||
grid.at(x, y).color = PATH_COLOR
|
color_layer.set(x, y, PATH_COLOR)
|
||||||
print(f" Set ({x},{y}) to green")
|
print(f" Set ({x},{y}) to green")
|
||||||
|
|
||||||
# Set up UI
|
# Set up UI
|
||||||
|
|
@ -70,7 +70,7 @@ grid.position = (50, 50)
|
||||||
grid.size = (250, 250)
|
grid.size = (250, 250)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title = mcrfpy.Caption("Path Visualization Test", 50, 10)
|
title = mcrfpy.Caption(pos=(50, 10), text="Path Visualization Test")
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ def test_issue_38_children():
|
||||||
print("\nTest 1: Passing children argument to Frame constructor")
|
print("\nTest 1: Passing children argument to Frame constructor")
|
||||||
try:
|
try:
|
||||||
# Create some child elements
|
# Create some child elements
|
||||||
child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child 1")
|
child1 = mcrfpy.Caption(pos=(10, 10), text="Child 1")
|
||||||
child2 = mcrfpy.Sprite(mcrfpy.Vector(10, 30))
|
child2 = mcrfpy.Sprite(pos=(10, 30))
|
||||||
|
|
||||||
# Try to create frame with children argument
|
# Try to create frame with children argument
|
||||||
frame = mcrfpy.Frame(10, 10, 200, 150, children=[child1, child2])
|
frame = mcrfpy.Frame(pos=(10, 10), size=(200, 150), children=[child1, child2])
|
||||||
print("✗ UNEXPECTED: Frame accepted children argument (should fail per issue #38)")
|
print("✗ UNEXPECTED: Frame accepted children argument (should fail per issue #38)")
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
print(f"✓ EXPECTED: Frame constructor rejected children argument: {e}")
|
print(f"✓ EXPECTED: Frame constructor rejected children argument: {e}")
|
||||||
|
|
@ -30,12 +30,12 @@ def test_issue_38_children():
|
||||||
# Test 2: Verify children can be added after creation
|
# Test 2: Verify children can be added after creation
|
||||||
print("\nTest 2: Adding children after Frame creation")
|
print("\nTest 2: Adding children after Frame creation")
|
||||||
try:
|
try:
|
||||||
frame = mcrfpy.Frame(10, 10, 200, 150)
|
frame = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
# Add children via the children collection
|
# Add children via the children collection
|
||||||
child1 = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Added Child 1")
|
child1 = mcrfpy.Caption(pos=(10, 10), text="Added Child 1")
|
||||||
child2 = mcrfpy.Caption(mcrfpy.Vector(10, 30), text="Added Child 2")
|
child2 = mcrfpy.Caption(pos=(10, 30), text="Added Child 2")
|
||||||
|
|
||||||
frame.children.append(child1)
|
frame.children.append(child1)
|
||||||
frame.children.append(child2)
|
frame.children.append(child2)
|
||||||
|
|
@ -65,33 +65,33 @@ def test_issue_42_click_callback():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
frame1 = mcrfpy.Frame(10, 10, 200, 150)
|
frame1 = mcrfpy.Frame(pos=(10, 10), size=(200, 150))
|
||||||
ui.append(frame1)
|
ui.append(frame1)
|
||||||
frame1.on_click = correct_callback
|
frame1.on_click = correct_callback
|
||||||
print("✓ Click callback with correct signature assigned successfully")
|
print("✓ Click callback with correct signature assigned successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Failed to assign correct callback: {type(e).__name__}: {e}")
|
print(f"✗ Failed to assign correct callback: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
# Test 2: Callback with wrong signature (no args)
|
# Test 2: Callback with wrong signature (no args)
|
||||||
print("\nTest 2: Click callback with no arguments")
|
print("\nTest 2: Click callback with no arguments")
|
||||||
def wrong_callback_no_args():
|
def wrong_callback_no_args():
|
||||||
print(" Wrong callback called")
|
print(" Wrong callback called")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
frame2 = mcrfpy.Frame(220, 10, 200, 150)
|
frame2 = mcrfpy.Frame(pos=(220, 10), size=(200, 150))
|
||||||
ui.append(frame2)
|
ui.append(frame2)
|
||||||
frame2.on_click = wrong_callback_no_args
|
frame2.on_click = wrong_callback_no_args
|
||||||
print("✓ Click callback with no args assigned (will fail at runtime per issue #42)")
|
print("✓ Click callback with no args assigned (will fail at runtime per issue #42)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Failed to assign callback: {type(e).__name__}: {e}")
|
print(f"✗ Failed to assign callback: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
# Test 3: Callback with wrong signature (too few args)
|
# Test 3: Callback with wrong signature (too few args)
|
||||||
print("\nTest 3: Click callback with too few arguments")
|
print("\nTest 3: Click callback with too few arguments")
|
||||||
def wrong_callback_few_args(x, y):
|
def wrong_callback_few_args(x, y):
|
||||||
print(f" Wrong callback called: x={x}, y={y}")
|
print(f" Wrong callback called: x={x}, y={y}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
frame3 = mcrfpy.Frame(10, 170, 200, 150)
|
frame3 = mcrfpy.Frame(pos=(10, 170), size=(200, 150))
|
||||||
ui.append(frame3)
|
ui.append(frame3)
|
||||||
frame3.on_click = wrong_callback_few_args
|
frame3.on_click = wrong_callback_few_args
|
||||||
print("✓ Click callback with 2 args assigned (will fail at runtime per issue #42)")
|
print("✓ Click callback with 2 args assigned (will fail at runtime per issue #42)")
|
||||||
|
|
|
||||||
|
|
@ -7,24 +7,24 @@ import sys
|
||||||
def test_grid_none_texture(runtime):
|
def test_grid_none_texture(runtime):
|
||||||
"""Test Grid functionality without texture"""
|
"""Test Grid functionality without texture"""
|
||||||
print("\n=== Testing Grid with None texture ===")
|
print("\n=== Testing Grid with None texture ===")
|
||||||
|
|
||||||
# Test 1: Create Grid with None texture
|
# Test 1: Create Grid with None texture
|
||||||
try:
|
try:
|
||||||
grid = mcrfpy.Grid(10, 10, None, mcrfpy.Vector(50, 50), mcrfpy.Vector(400, 400))
|
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(50, 50), size=(400, 400))
|
||||||
print("✓ Grid created successfully with None texture")
|
print("✓ Grid created successfully with None texture")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Failed to create Grid with None texture: {e}")
|
print(f"✗ Failed to create Grid with None texture: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Add to UI
|
# Add to UI
|
||||||
ui = mcrfpy.sceneUI("grid_none_test")
|
ui = mcrfpy.sceneUI("grid_none_test")
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
# Test 2: Verify grid properties
|
# Test 2: Verify grid properties
|
||||||
try:
|
try:
|
||||||
grid_size = grid.grid_size
|
grid_size = grid.grid_size
|
||||||
print(f"✓ Grid size: {grid_size}")
|
print(f"✓ Grid size: {grid_size}")
|
||||||
|
|
||||||
# Check texture property
|
# Check texture property
|
||||||
texture = grid.texture
|
texture = grid.texture
|
||||||
if texture is None:
|
if texture is None:
|
||||||
|
|
@ -33,39 +33,41 @@ def test_grid_none_texture(runtime):
|
||||||
print(f"✗ Grid texture should be None, got: {texture}")
|
print(f"✗ Grid texture should be None, got: {texture}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Property access failed: {e}")
|
print(f"✗ Property access failed: {e}")
|
||||||
|
|
||||||
# Test 3: Access grid points and set colors
|
# Test 3: Access grid points using ColorLayer (new API)
|
||||||
|
# Note: GridPoint no longer has .color - must use ColorLayer system
|
||||||
try:
|
try:
|
||||||
|
# Add a color layer to the grid
|
||||||
|
color_layer = grid.add_layer("color", z_index=-1)
|
||||||
# Create a checkerboard pattern with colors
|
# Create a checkerboard pattern with colors
|
||||||
for x in range(10):
|
for x in range(10):
|
||||||
for y in range(10):
|
for y in range(10):
|
||||||
point = grid.at(x, y)
|
|
||||||
if (x + y) % 2 == 0:
|
if (x + y) % 2 == 0:
|
||||||
point.color = mcrfpy.Color(255, 0, 0, 255) # Red
|
color_layer.set(x, y, mcrfpy.Color(255, 0, 0, 255)) # Red
|
||||||
else:
|
else:
|
||||||
point.color = mcrfpy.Color(0, 0, 255, 255) # Blue
|
color_layer.set(x, y, mcrfpy.Color(0, 0, 255, 255)) # Blue
|
||||||
print("✓ Successfully set grid point colors")
|
print("✓ Successfully set grid colors via ColorLayer")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Failed to set grid colors: {e}")
|
print(f"✗ Failed to set grid colors: {e}")
|
||||||
|
|
||||||
# Test 4: Add entities to the grid
|
# Test 4: Add entities to the grid
|
||||||
try:
|
try:
|
||||||
# Create an entity with its own texture
|
# Create an entity with its own texture
|
||||||
entity_texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
entity_texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
entity = mcrfpy.Entity(mcrfpy.Vector(5, 5), entity_texture, 1, grid)
|
entity = mcrfpy.Entity((5, 5), texture=entity_texture, sprite_index=1, grid=grid)
|
||||||
grid.entities.append(entity)
|
grid.entities.append(entity)
|
||||||
print(f"✓ Added entity to grid, total entities: {len(grid.entities)}")
|
print(f"✓ Added entity to grid, total entities: {len(grid.entities)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Failed to add entity: {e}")
|
print(f"✗ Failed to add entity: {e}")
|
||||||
|
|
||||||
# Test 5: Test grid interaction properties
|
# Test 5: Test grid interaction properties
|
||||||
try:
|
try:
|
||||||
# Test zoom
|
# Test zoom
|
||||||
grid.zoom = 2.0
|
grid.zoom = 2.0
|
||||||
print(f"✓ Set zoom to: {grid.zoom}")
|
print(f"✓ Set zoom to: {grid.zoom}")
|
||||||
|
|
||||||
# Test center
|
# Test center (uses pixel coordinates)
|
||||||
grid.center = mcrfpy.Vector(5, 5)
|
grid.center = (200, 200)
|
||||||
print(f"✓ Set center to: {grid.center}")
|
print(f"✓ Set center to: {grid.center}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Grid properties failed: {e}")
|
print(f"✗ Grid properties failed: {e}")
|
||||||
|
|
@ -86,7 +88,7 @@ mcrfpy.setScene("grid_none_test")
|
||||||
|
|
||||||
# Add a background frame so we can see the grid
|
# Add a background frame so we can see the grid
|
||||||
ui = mcrfpy.sceneUI("grid_none_test")
|
ui = mcrfpy.sceneUI("grid_none_test")
|
||||||
background = mcrfpy.Frame(0, 0, 800, 600,
|
background = mcrfpy.Frame(pos=(0, 0), size=(800, 600),
|
||||||
fill_color=mcrfpy.Color(200, 200, 200),
|
fill_color=mcrfpy.Color(200, 200, 200),
|
||||||
outline_color=mcrfpy.Color(0, 0, 0),
|
outline_color=mcrfpy.Color(0, 0, 0),
|
||||||
outline=2.0)
|
outline=2.0)
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ def test_UICollection():
|
||||||
ui = mcrfpy.sceneUI("collection_test")
|
ui = mcrfpy.sceneUI("collection_test")
|
||||||
|
|
||||||
# Add various UI elements
|
# Add various UI elements
|
||||||
frame = mcrfpy.Frame(10, 10, 100, 100)
|
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
|
||||||
caption = mcrfpy.Caption(mcrfpy.Vector(120, 10), text="Test")
|
caption = mcrfpy.Caption(pos=(120, 10), text="Test")
|
||||||
# Skip sprite for now since it requires a texture
|
# Skip sprite for now since it requires a texture
|
||||||
|
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
@ -74,9 +74,9 @@ def test_UICollection():
|
||||||
# Test type preservation (Issue #76)
|
# Test type preservation (Issue #76)
|
||||||
try:
|
try:
|
||||||
# Add a frame with children to test nested collections
|
# Add a frame with children to test nested collections
|
||||||
parent_frame = mcrfpy.Frame(250, 10, 200, 200,
|
parent_frame = mcrfpy.Frame(pos=(250, 10), size=(200, 200),
|
||||||
fill_color=mcrfpy.Color(200, 200, 200))
|
fill_color=mcrfpy.Color(200, 200, 200))
|
||||||
child_caption = mcrfpy.Caption(mcrfpy.Vector(10, 10), text="Child")
|
child_caption = mcrfpy.Caption(pos=(10, 10), text="Child")
|
||||||
parent_frame.children.append(child_caption)
|
parent_frame.children.append(child_caption)
|
||||||
ui.append(parent_frame)
|
ui.append(parent_frame)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,50 +18,50 @@ def test_screenshot_validation():
|
||||||
print("Creating UI elements...")
|
print("Creating UI elements...")
|
||||||
|
|
||||||
# Bright red frame with white outline
|
# Bright red frame with white outline
|
||||||
frame1 = mcrfpy.Frame(50, 50, 300, 200,
|
frame1 = mcrfpy.Frame(pos=(50, 50), size=(300, 200),
|
||||||
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
|
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
|
||||||
outline_color=mcrfpy.Color(255, 255, 255), # White
|
outline_color=mcrfpy.Color(255, 255, 255), # White
|
||||||
outline=5.0)
|
outline=5.0)
|
||||||
ui.append(frame1)
|
ui.append(frame1)
|
||||||
print("Added red frame at (50, 50)")
|
print("Added red frame at (50, 50)")
|
||||||
|
|
||||||
# Bright green frame
|
# Bright green frame
|
||||||
frame2 = mcrfpy.Frame(400, 50, 300, 200,
|
frame2 = mcrfpy.Frame(pos=(400, 50), size=(300, 200),
|
||||||
fill_color=mcrfpy.Color(0, 255, 0), # Bright green
|
fill_color=mcrfpy.Color(0, 255, 0), # Bright green
|
||||||
outline_color=mcrfpy.Color(0, 0, 0), # Black
|
outline_color=mcrfpy.Color(0, 0, 0), # Black
|
||||||
outline=3.0)
|
outline=3.0)
|
||||||
ui.append(frame2)
|
ui.append(frame2)
|
||||||
print("Added green frame at (400, 50)")
|
print("Added green frame at (400, 50)")
|
||||||
|
|
||||||
# Blue frame
|
# Blue frame
|
||||||
frame3 = mcrfpy.Frame(50, 300, 300, 200,
|
frame3 = mcrfpy.Frame(pos=(50, 300), size=(300, 200),
|
||||||
fill_color=mcrfpy.Color(0, 0, 255), # Bright blue
|
fill_color=mcrfpy.Color(0, 0, 255), # Bright blue
|
||||||
outline_color=mcrfpy.Color(255, 255, 0), # Yellow
|
outline_color=mcrfpy.Color(255, 255, 0), # Yellow
|
||||||
outline=4.0)
|
outline=4.0)
|
||||||
ui.append(frame3)
|
ui.append(frame3)
|
||||||
print("Added blue frame at (50, 300)")
|
print("Added blue frame at (50, 300)")
|
||||||
|
|
||||||
# Add text captions
|
# Add text captions
|
||||||
caption1 = mcrfpy.Caption(mcrfpy.Vector(60, 60),
|
caption1 = mcrfpy.Caption(pos=(60, 60),
|
||||||
text="RED FRAME TEST",
|
text="RED FRAME TEST",
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
caption1.size = 24
|
caption1.font_size = 24
|
||||||
frame1.children.append(caption1)
|
frame1.children.append(caption1)
|
||||||
|
|
||||||
caption2 = mcrfpy.Caption(mcrfpy.Vector(410, 60),
|
caption2 = mcrfpy.Caption(pos=(410, 60),
|
||||||
text="GREEN FRAME TEST",
|
text="GREEN FRAME TEST",
|
||||||
fill_color=mcrfpy.Color(0, 0, 0))
|
fill_color=mcrfpy.Color(0, 0, 0))
|
||||||
caption2.size = 24
|
caption2.font_size = 24
|
||||||
ui.append(caption2)
|
ui.append(caption2)
|
||||||
|
|
||||||
caption3 = mcrfpy.Caption(mcrfpy.Vector(60, 310),
|
caption3 = mcrfpy.Caption(pos=(60, 310),
|
||||||
text="BLUE FRAME TEST",
|
text="BLUE FRAME TEST",
|
||||||
fill_color=mcrfpy.Color(255, 255, 0))
|
fill_color=mcrfpy.Color(255, 255, 0))
|
||||||
caption3.size = 24
|
caption3.font_size = 24
|
||||||
ui.append(caption3)
|
ui.append(caption3)
|
||||||
|
|
||||||
# White background frame to ensure non-transparent background
|
# White background frame to ensure non-transparent background
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768,
|
background = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
|
||||||
fill_color=mcrfpy.Color(200, 200, 200)) # Light gray
|
fill_color=mcrfpy.Color(200, 200, 200)) # Light gray
|
||||||
# Insert at beginning so it's behind everything
|
# Insert at beginning so it's behind everything
|
||||||
ui.remove(len(ui) - 1) # Remove to re-add at start
|
ui.remove(len(ui) - 1) # Remove to re-add at start
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,16 @@ mcrfpy.setScene("timer_works")
|
||||||
ui = mcrfpy.sceneUI("timer_works")
|
ui = mcrfpy.sceneUI("timer_works")
|
||||||
|
|
||||||
# Add visible content
|
# Add visible content
|
||||||
frame = mcrfpy.Frame(100, 100, 300, 200,
|
frame = mcrfpy.Frame(pos=(100, 100), size=(300, 200),
|
||||||
fill_color=mcrfpy.Color(255, 0, 0),
|
fill_color=mcrfpy.Color(255, 0, 0),
|
||||||
outline_color=mcrfpy.Color(255, 255, 255),
|
outline_color=mcrfpy.Color(255, 255, 255),
|
||||||
outline=3.0)
|
outline=3.0)
|
||||||
ui.append(frame)
|
ui.append(frame)
|
||||||
|
|
||||||
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
|
caption = mcrfpy.Caption(pos=(150, 150),
|
||||||
text="TIMER TEST SUCCESS",
|
text="TIMER TEST SUCCESS",
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
fill_color=mcrfpy.Color(255, 255, 255))
|
||||||
caption.size = 24
|
caption.font_size = 24
|
||||||
ui.append(caption)
|
ui.append(caption)
|
||||||
|
|
||||||
# Timer callback with correct signature
|
# Timer callback with correct signature
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import inspect
|
||||||
import datetime
|
import datetime
|
||||||
import html
|
import html
|
||||||
import re
|
import re
|
||||||
|
import types
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def transform_doc_links(docstring, format='html', base_url=''):
|
def transform_doc_links(docstring, format='html', base_url=''):
|
||||||
|
|
@ -214,11 +215,21 @@ def get_all_classes():
|
||||||
"parsed": parse_docstring(method_doc)
|
"parsed": parse_docstring(method_doc)
|
||||||
}
|
}
|
||||||
elif isinstance(attr, property):
|
elif isinstance(attr, property):
|
||||||
|
# Pure Python property
|
||||||
prop_doc = (attr.fget.__doc__ if attr.fget else "") or ""
|
prop_doc = (attr.fget.__doc__ if attr.fget else "") or ""
|
||||||
class_info["properties"][attr_name] = {
|
class_info["properties"][attr_name] = {
|
||||||
"doc": prop_doc,
|
"doc": prop_doc,
|
||||||
"readonly": attr.fset is None
|
"readonly": attr.fset is None
|
||||||
}
|
}
|
||||||
|
elif isinstance(attr, (types.GetSetDescriptorType, types.MemberDescriptorType)):
|
||||||
|
# C++ extension property (PyGetSetDef or PyMemberDef)
|
||||||
|
prop_doc = attr.__doc__ or ""
|
||||||
|
# Check if docstring indicates read-only (convention: "read-only" in description)
|
||||||
|
readonly = "read-only" in prop_doc.lower()
|
||||||
|
class_info["properties"][attr_name] = {
|
||||||
|
"doc": prop_doc,
|
||||||
|
"readonly": readonly
|
||||||
|
}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue