diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..7f71600 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "gitea": { + "type": "stdio", + "command": "/home/john/Development/discord_for_claude/forgejo-mcp.linux.amd64", + "args": ["stdio", "--server", "https://gamedev.ffwf.net/gitea/", "--token", "f58ec698a5edee82db4960920b13d3f7d0d58d8e"] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 0dea84c..641417a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -392,67 +392,102 @@ mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds ## Documentation Guidelines -### Inline C++ Documentation Format +### Documentation Macro System -When adding new methods or modifying existing ones in C++ source files, use this documentation format in PyMethodDef arrays: +**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings. + +#### Include the Header ```cpp -{"method_name", (PyCFunction)Class::method, METH_VARARGS | METH_KEYWORDS, - "method_name(arg1: type, arg2: type = default) -> return_type\n\n" - "Brief description of what the method does.\n\n" - "Args:\n" - " arg1: Description of first argument\n" - " arg2: Description of second argument (default: value)\n\n" - "Returns:\n" - " Description of return value\n\n" - "Example:\n" - " result = obj.method_name(value1, value2)\n\n" - "Note:\n" - " Any important notes or caveats"}, +#include "McRFPy_Doc.h" ``` -For properties in PyGetSetDef arrays: +#### Documenting Methods + +For methods in PyMethodDef arrays, use `MCRF_METHOD`: + +```cpp +{"method_name", (PyCFunction)Class::method, METH_VARARGS, + MCRF_METHOD(ClassName, method_name, + MCRF_SIG("(arg1: type, arg2: type)", "return_type"), + MCRF_DESC("Brief description of what the method does."), + MCRF_ARGS_START + MCRF_ARG("arg1", "Description of first argument") + MCRF_ARG("arg2", "Description of second argument") + MCRF_RETURNS("Description of return value") + MCRF_RAISES("ValueError", "Condition that raises this exception") + MCRF_NOTE("Important notes or caveats") + MCRF_LINK("docs/guide.md", "Related Documentation") + )}, +``` + +#### Documenting Properties + +For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`: + ```cpp {"property_name", (getter)getter_func, (setter)setter_func, - "Brief description of the property. " - "Additional details about valid values, side effects, etc.", NULL}, + MCRF_PROPERTY(property_name, + "Brief description of the property. " + "Additional details about valid values, side effects, etc." + ), NULL}, ``` +#### Available Macros + +- `MCRF_SIG(params, ret)` - Method signature +- `MCRF_DESC(text)` - Description paragraph +- `MCRF_ARGS_START` - Begin arguments section +- `MCRF_ARG(name, desc)` - Individual argument +- `MCRF_RETURNS(text)` - Return value description +- `MCRF_RAISES(exception, condition)` - Exception documentation +- `MCRF_NOTE(text)` - Important notes +- `MCRF_LINK(path, text)` - Reference to external documentation + +#### Documentation Prose Guidelines + +**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides: + +```cpp +MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial") +``` + +**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale. + ### Regenerating Documentation -After modifying C++ inline documentation: +After modifying C++ inline documentation with MCRF_* macros: 1. **Rebuild the project**: `make -j$(nproc)` -2. **Generate stub files** (for IDE support): +2. **Generate documentation** (automatic from compiled module): ```bash - ./build/mcrogueface --exec generate_stubs.py - ``` - -3. **Generate dynamic documentation** (recommended): - ```bash - ./build/mcrogueface --exec generate_dynamic_docs.py + ./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py ``` This creates: - `docs/api_reference_dynamic.html` - `docs/API_REFERENCE_DYNAMIC.md` -4. **Update hardcoded documentation** (if still using old system): - - `generate_complete_api_docs.py` - Update method dictionaries - - `generate_complete_markdown_docs.py` - Update method dictionaries +3. **Generate stub files** (optional, for IDE support): + ```bash + ./build/mcrogueface --headless --exec tools/generate_stubs.py + ``` + Creates `.pyi` stub files for type checking and autocompletion ### Important Notes +- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros - **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python -- **Use --exec flag**: `./build/mcrogueface --exec script.py` or `--headless --exec` for CI/automation -- **Dynamic is better**: The new `generate_dynamic_docs.py` extracts documentation directly from compiled module -- **Keep docstrings consistent**: Follow the format above for automatic parsing +- **Use --headless --exec**: For non-interactive documentation generation +- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.) +- **No manual dictionaries**: The old hardcoded documentation system has been removed ### Documentation Pipeline Architecture -1. **C++ Source** → PyMethodDef/PyGetSetDef arrays with docstrings -2. **Compilation** → Docstrings embedded in compiled module +1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays +2. **Compilation** → Macros expand to complete docstrings embedded in module 3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract -4. **Generation** → HTML/Markdown/Stub files created +4. **Generation** → HTML/Markdown/Stub files created with transformed links +5. **No drift**: Impossible for docs and code to disagree - they're the same file! -The documentation is only as good as the C++ inline docstrings! \ No newline at end of file +The macro system ensures complete, consistent documentation across all Python bindings. \ No newline at end of file diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html index 82c247d..faa33e5 100644 --- a/docs/api_reference_dynamic.html +++ b/docs/api_reference_dynamic.html @@ -108,7 +108,7 @@

McRogueFace API Reference

-

Generated on 2025-07-15 21:28:24

+

Generated on 2025-10-30 16:58:07

This documentation was dynamically generated from the compiled module.

@@ -424,15 +424,37 @@ Attributes:

Methods:

-
get_boundsGet bounding box as (x, y, width, height)
+
get_boundsget_bounds() -> tuple
+

Get the bounding rectangle of this drawable element. + + + +Note:

+

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

-
moveMove by relative offset (dx, dy)
+
movemove(dx: float, dy: float) -> None
+

Move the element by a relative offset. + + +Note:

+
+
dx: Horizontal offset in pixels
+
dy: Vertical offset in pixels
+
-
resizeResize to new dimensions (width, height)
+
resizeresize(width: float, height: float) -> None
+

Resize the element to new dimensions. + + +Note:

+
+
width: New width in pixels
+
height: New height in pixels
+
@@ -442,17 +464,38 @@ Attributes:

Methods:

-
from_hexCreate Color from hex string (e.g., '#FF0000' or 'FF0000')
+
from_hexfrom_hex(hex_string: str) -> Color
+

Create a Color from a hexadecimal string. + + +Note:

+
+
hex_string: Hex color string (e.g., '#FF0000', 'FF0000', '#AABBCCDD' for RGBA)
+
+

Returns: Color: New Color object with values from hex string ValueError: If hex string is not 6 or 8 characters (RGB or RGBA) This is a class method. Call as Color.from_hex('#FF0000')

-
lerp(...)
-

Linearly interpolate between this color and another

+
lerplerp(other: Color, t: float) -> Color
+

Linearly interpolate between this color and another. + + +Note:

+
+
other: The target Color to interpolate towards
+
t: Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]
+
+

Returns: Color: New Color representing the interpolated value All components (r, g, b, a) are interpolated independently

-
to_hex(...)
-

Convert Color to hex string

+
to_hexto_hex() -> str
+

Convert this Color to a hexadecimal string. + + + +Note:

+

Returns: str: Hex string in format '#RRGGBB' or '#RRGGBBAA' (if alpha < 255) Alpha component is only included if not fully opaque (< 255)

@@ -462,15 +505,37 @@ Attributes:

Methods:

-
get_boundsGet bounding box as (x, y, width, height)
+
get_boundsget_bounds() -> tuple
+

Get the bounding rectangle of this drawable element. + + + +Note:

+

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

-
moveMove by relative offset (dx, dy)
+
movemove(dx: float, dy: float) -> None
+

Move the element by a relative offset. + + +Note:

+
+
dx: Horizontal offset in pixels
+
dy: Vertical offset in pixels
+
-
resizeResize to new dimensions (width, height)
+
resizeresize(width: float, height: float) -> None
+

Resize the element to new dimensions. + + +Note:

+
+
width: New width in pixels
+
height: New height in pixels
+
@@ -514,7 +579,13 @@ Attributes:
-
get_boundsGet bounding box as (x, y, width, height)
+
get_boundsget_bounds() -> tuple
+

Get the bounding rectangle of this drawable element. + + + +Note:

+

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

@@ -523,7 +594,15 @@ Attributes:
-
moveMove by relative offset (dx, dy)
+
movemove(dx: float, dy: float) -> None
+

Move the element by a relative offset. + + +Note:

+
+
dx: Horizontal offset in pixels
+
dy: Vertical offset in pixels
+
@@ -537,7 +616,15 @@ Attributes:
-
resizeResize to new dimensions (width, height)
+
resizeresize(width: float, height: float) -> None
+

Resize the element to new dimensions. + + +Note:

+
+
width: New width in pixels
+
height: New height in pixels
+
@@ -625,15 +712,37 @@ Attributes:

Methods:

-
get_boundsGet bounding box as (x, y, width, height)
+
get_boundsget_bounds() -> tuple
+

Get the bounding rectangle of this drawable element. + + + +Note:

+

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

-
moveMove by relative offset (dx, dy)
+
movemove(dx: float, dy: float) -> None
+

Move the element by a relative offset. + + +Note:

+
+
dx: Horizontal offset in pixels
+
dy: Vertical offset in pixels
+
-
resizeResize to new dimensions (width, height)
+
resizeresize(width: float, height: float) -> None
+

Resize the element to new dimensions. + + +Note:

+
+
width: New width in pixels
+
height: New height in pixels
+
@@ -717,8 +826,8 @@ Attributes:
-
compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None
-

Compute field of view from a position.

+
compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]
+

Compute field of view from a position and return visible cells.

x: X coordinate of the viewer
y: Y coordinate of the viewer
@@ -726,6 +835,7 @@ Attributes:
light_walls: Whether walls are lit when visible
algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)
+

Returns: List of tuples (x, y, visible, discovered) for all visible cells: - x, y: Grid coordinates - visible: True (all returned cells are visible) - discovered: True (FOV implies discovery)

@@ -742,7 +852,13 @@ Attributes:
-
get_boundsGet bounding box as (x, y, width, height)
+
get_boundsget_bounds() -> tuple
+

Get the bounding rectangle of this drawable element. + + + +Note:

+

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

@@ -776,11 +892,27 @@ Attributes:
-
moveMove by relative offset (dx, dy)
+
movemove(dx: float, dy: float) -> None
+

Move the element by a relative offset. + + +Note:

+
+
dx: Horizontal offset in pixels
+
dy: Vertical offset in pixels
+
-
resizeResize to new dimensions (width, height)
+
resizeresize(width: float, height: float) -> None
+

Resize the element to new dimensions. + + +Note:

+
+
width: New width in pixels
+
height: New height in pixels
+
@@ -856,15 +988,37 @@ Attributes:

Methods:

-
get_boundsGet bounding box as (x, y, width, height)
+
get_boundsget_bounds() -> tuple
+

Get the bounding rectangle of this drawable element. + + + +Note:

+

Returns: tuple: (x, y, width, height) representing the element's bounds The bounds are in screen coordinates and account for current position and size.

-
moveMove by relative offset (dx, dy)
+
movemove(dx: float, dy: float) -> None
+

Move the element by a relative offset. + + +Note:

+
+
dx: Horizontal offset in pixels
+
dy: Vertical offset in pixels
+
-
resizeResize to new dimensions (width, height)
+
resizeresize(width: float, height: float) -> None
+

Resize the element to new dimensions. + + +Note:

+
+
width: New width in pixels
+
height: New height in pixels
+
@@ -915,25 +1069,41 @@ Example:
cancelcancel() -> None

Cancel the timer and remove it from the timer system. -The timer will no longer fire and cannot be restarted.

+ + + +Note:

+

Returns: None The timer will no longer fire and cannot be restarted. The callback will not be called again.

pausepause() -> None

Pause the timer, preserving the time remaining until next trigger. -The timer can be resumed later with resume().

+ + + +Note:

+

Returns: None The timer can be resumed later with resume(). Time spent paused does not count toward the interval.

restartrestart() -> None

Restart the timer from the beginning. -Resets the timer to fire after a full interval from now.

+ + + +Note:

+

Returns: None Resets the timer to fire after a full interval from now, regardless of remaining time.

resumeresume() -> None

Resume a paused timer from where it left off. -Has no effect if the timer is not paused.

+ + + +Note:

+

Returns: None Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.

@@ -981,38 +1151,59 @@ Has no effect if the timer is not paused.

Methods:

-
angle(...)
-

Return the angle in radians from the positive X axis

+
angleangle() -> float
+

Get the angle of this vector in radians.

+

Returns: float: Angle in radians from positive x-axis

-
copy(...)
-

Return a copy of this vector

+
copycopy() -> Vector
+

Create a copy of this vector.

+

Returns: Vector: New Vector object with same x and y values

-
distance_to(...)
-

Return the distance to another vector

+
distance_todistance_to(other: Vector) -> float
+

Calculate the distance to another vector.

+
+
other: The other vector
+
+

Returns: float: Distance between the two vectors

-
dot(...)
-

Return the dot product with another vector

+
dotdot(other: Vector) -> float
+

Calculate the dot product with another vector.

+
+
other: The other vector
+
+

Returns: float: Dot product of the two vectors

-
magnitude(...)
-

Return the length of the vector

+
magnitudemagnitude() -> float
+

Calculate the length/magnitude of this vector.

+

Returns: float: The magnitude of the vector

-
magnitude_squared(...)
-

Return the squared length of the vector

+
magnitude_squaredmagnitude_squared() -> float
+

Calculate the squared magnitude of this vector. + + + +Note:

+

Returns: float: The squared magnitude (faster than magnitude()) Use this for comparisons to avoid expensive square root calculation.

-
normalize(...)
-

Return a unit vector in the same direction

+
normalizenormalize() -> Vector
+

Return a unit vector in the same direction. + + + +Note:

+

Returns: Vector: New normalized vector with magnitude 1.0 For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception

diff --git a/forgejo-mcp.linux.amd64 b/forgejo-mcp.linux.amd64 new file mode 100755 index 0000000..fe84e35 Binary files /dev/null and b/forgejo-mcp.linux.amd64 differ diff --git a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py index b738dcc..c83719b 100644 --- a/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py +++ b/roguelike_tutorial/mcrogueface_does_the_entire_tutorial_2025/Part_6/code/game.py @@ -373,7 +373,7 @@ class Engine: self.ui = mcrfpy.sceneUI("game") - background = mcrfpy.Frame(0, 0, 1024, 768) + background = mcrfpy.Frame((0, 0), (1024, 768)) background.fill_color = mcrfpy.Color(0, 0, 0) self.ui.append(background) @@ -565,4 +565,4 @@ class Engine: # Create and run the game engine = Engine() print("Part 6: Combat System!") -print("Attack enemies to defeat them, but watch your HP!") \ No newline at end of file +print("Attack enemies to defeat them, but watch your HP!") diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 7ba99ab..3947163 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -1,6 +1,7 @@ #include "McRFPy_API.h" #include "McRFPy_Automation.h" #include "McRFPy_Libtcod.h" +#include "McRFPy_Doc.h" #include "platform.h" #include "PyAnimation.h" #include "PyDrawable.h" @@ -27,188 +28,201 @@ PyObject* McRFPy_API::mcrf_module; static PyMethodDef mcrfpyMethods[] = { {"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, - "createSoundBuffer(filename: str) -> int\n\n" - "Load a sound effect from a file and return its buffer ID.\n\n" - "Args:\n" - " filename: Path to the sound file (WAV, OGG, FLAC)\n\n" - "Returns:\n" - " int: Buffer ID for use with playSound()\n\n" - "Raises:\n" - " RuntimeError: If the file cannot be loaded"}, + MCRF_FUNCTION(createSoundBuffer, + MCRF_SIG("(filename: str)", "int"), + MCRF_DESC("Load a sound effect from a file and return its buffer ID."), + MCRF_ARGS_START + MCRF_ARG("filename", "Path to the sound file (WAV, OGG, FLAC)") + MCRF_RETURNS("int: Buffer ID for use with playSound()") + MCRF_RAISES("RuntimeError", "If the file cannot be loaded") + )}, {"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, - "loadMusic(filename: str) -> None\n\n" - "Load and immediately play background music from a file.\n\n" - "Args:\n" - " filename: Path to the music file (WAV, OGG, FLAC)\n\n" - "Note:\n" - " Only one music track can play at a time. Loading new music stops the current track."}, + MCRF_FUNCTION(loadMusic, + MCRF_SIG("(filename: str)", "None"), + MCRF_DESC("Load and immediately play background music from a file."), + MCRF_ARGS_START + MCRF_ARG("filename", "Path to the music file (WAV, OGG, FLAC)") + MCRF_RETURNS("None") + MCRF_NOTE("Only one music track can play at a time. Loading new music stops the current track.") + )}, {"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS, - "setMusicVolume(volume: int) -> None\n\n" - "Set the global music volume.\n\n" - "Args:\n" - " volume: Volume level from 0 (silent) to 100 (full volume)"}, + MCRF_FUNCTION(setMusicVolume, + MCRF_SIG("(volume: int)", "None"), + MCRF_DESC("Set the global music volume."), + MCRF_ARGS_START + MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)") + MCRF_RETURNS("None") + )}, {"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS, - "setSoundVolume(volume: int) -> None\n\n" - "Set the global sound effects volume.\n\n" - "Args:\n" - " volume: Volume level from 0 (silent) to 100 (full volume)"}, + MCRF_FUNCTION(setSoundVolume, + MCRF_SIG("(volume: int)", "None"), + MCRF_DESC("Set the global sound effects volume."), + MCRF_ARGS_START + MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)") + MCRF_RETURNS("None") + )}, {"playSound", McRFPy_API::_playSound, METH_VARARGS, - "playSound(buffer_id: int) -> None\n\n" - "Play a sound effect using a previously loaded buffer.\n\n" - "Args:\n" - " buffer_id: Sound buffer ID returned by createSoundBuffer()\n\n" - "Raises:\n" - " RuntimeError: If the buffer ID is invalid"}, + MCRF_FUNCTION(playSound, + MCRF_SIG("(buffer_id: int)", "None"), + MCRF_DESC("Play a sound effect using a previously loaded buffer."), + MCRF_ARGS_START + MCRF_ARG("buffer_id", "Sound buffer ID returned by createSoundBuffer()") + MCRF_RETURNS("None") + MCRF_RAISES("RuntimeError", "If the buffer ID is invalid") + )}, {"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS, - "getMusicVolume() -> int\n\n" - "Get the current music volume level.\n\n" - "Returns:\n" - " int: Current volume (0-100)"}, + MCRF_FUNCTION(getMusicVolume, + MCRF_SIG("()", "int"), + MCRF_DESC("Get the current music volume level."), + MCRF_RETURNS("int: Current volume (0-100)") + )}, {"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS, - "getSoundVolume() -> int\n\n" - "Get the current sound effects volume level.\n\n" - "Returns:\n" - " int: Current volume (0-100)"}, + MCRF_FUNCTION(getSoundVolume, + MCRF_SIG("()", "int"), + MCRF_DESC("Get the current sound effects volume level."), + MCRF_RETURNS("int: Current volume (0-100)") + )}, {"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS, - "sceneUI(scene: str = None) -> list\n\n" - "Get all UI elements for a scene.\n\n" - "Args:\n" - " scene: Scene name. If None, uses current scene\n\n" - "Returns:\n" - " list: All UI elements (Frame, Caption, Sprite, Grid) in the scene\n\n" - "Raises:\n" - " KeyError: If the specified scene doesn't exist"}, + MCRF_FUNCTION(sceneUI, + MCRF_SIG("(scene: str = None)", "list"), + MCRF_DESC("Get all UI elements for a scene."), + MCRF_ARGS_START + MCRF_ARG("scene", "Scene name. If None, uses current scene") + MCRF_RETURNS("list: All UI elements (Frame, Caption, Sprite, Grid) in the scene") + MCRF_RAISES("KeyError", "If the specified scene doesn't exist") + )}, {"currentScene", McRFPy_API::_currentScene, METH_NOARGS, - "currentScene() -> str\n\n" - "Get the name of the currently active scene.\n\n" - "Returns:\n" - " str: Name of the current scene"}, + MCRF_FUNCTION(currentScene, + MCRF_SIG("()", "str"), + MCRF_DESC("Get the name of the currently active scene."), + MCRF_RETURNS("str: Name of the current scene") + )}, {"setScene", McRFPy_API::_setScene, METH_VARARGS, - "setScene(scene: str, transition: str = None, duration: float = 0.0) -> None\n\n" - "Switch to a different scene with optional transition effect.\n\n" - "Args:\n" - " scene: Name of the scene to switch to\n" - " transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')\n" - " duration: Transition duration in seconds (default: 0.0 for instant)\n\n" - "Raises:\n" - " KeyError: If the scene doesn't exist\n" - " ValueError: If the transition type is invalid"}, + MCRF_FUNCTION(setScene, + MCRF_SIG("(scene: str, transition: str = None, duration: float = 0.0)", "None"), + MCRF_DESC("Switch to a different scene with optional transition effect."), + MCRF_ARGS_START + MCRF_ARG("scene", "Name of the scene to switch to") + MCRF_ARG("transition", "Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')") + MCRF_ARG("duration", "Transition duration in seconds (default: 0.0 for instant)") + MCRF_RETURNS("None") + MCRF_RAISES("KeyError", "If the scene doesn't exist") + MCRF_RAISES("ValueError", "If the transition type is invalid") + )}, {"createScene", McRFPy_API::_createScene, METH_VARARGS, - "createScene(name: str) -> None\n\n" - "Create a new empty scene.\n\n" - "Args:\n" - " name: Unique name for the new scene\n\n" - "Raises:\n" - " ValueError: If a scene with this name already exists\n\n" - "Note:\n" - " The scene is created but not made active. Use setScene() to switch to it."}, + MCRF_FUNCTION(createScene, + MCRF_SIG("(name: str)", "None"), + MCRF_DESC("Create a new empty scene."), + MCRF_ARGS_START + MCRF_ARG("name", "Unique name for the new scene") + MCRF_RETURNS("None") + MCRF_RAISES("ValueError", "If a scene with this name already exists") + MCRF_NOTE("The scene is created but not made active. Use setScene() to switch to it.") + )}, {"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS, - "keypressScene(handler: callable) -> None\n\n" - "Set the keyboard event handler for the current scene.\n\n" - "Args:\n" - " handler: Callable that receives (key_name: str, is_pressed: bool)\n\n" - "Example:\n" - " def on_key(key, pressed):\n" - " if key == 'A' and pressed:\n" - " print('A key pressed')\n" - " mcrfpy.keypressScene(on_key)"}, + MCRF_FUNCTION(keypressScene, + MCRF_SIG("(handler: callable)", "None"), + MCRF_DESC("Set the keyboard event handler for the current scene."), + MCRF_ARGS_START + MCRF_ARG("handler", "Callable that receives (key_name: str, is_pressed: bool)") + MCRF_RETURNS("None") + MCRF_NOTE("Example: def on_key(key, pressed): if key == 'A' and pressed: print('A key pressed') mcrfpy.keypressScene(on_key)") + )}, {"setTimer", McRFPy_API::_setTimer, METH_VARARGS, - "setTimer(name: str, handler: callable, interval: int) -> None\n\n" - "Create or update a recurring timer.\n\n" - "Args:\n" - " name: Unique identifier for the timer\n" - " handler: Function called with (runtime: float) parameter\n" - " interval: Time between calls in milliseconds\n\n" - "Note:\n" - " If a timer with this name exists, it will be replaced.\n" - " The handler receives the total runtime in seconds as its argument."}, + MCRF_FUNCTION(setTimer, + MCRF_SIG("(name: str, handler: callable, interval: int)", "None"), + MCRF_DESC("Create or update a recurring timer."), + MCRF_ARGS_START + MCRF_ARG("name", "Unique identifier for the timer") + MCRF_ARG("handler", "Function called with (runtime: float) parameter") + MCRF_ARG("interval", "Time between calls in milliseconds") + MCRF_RETURNS("None") + MCRF_NOTE("If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.") + )}, {"delTimer", McRFPy_API::_delTimer, METH_VARARGS, - "delTimer(name: str) -> None\n\n" - "Stop and remove a timer.\n\n" - "Args:\n" - " name: Timer identifier to remove\n\n" - "Note:\n" - " No error is raised if the timer doesn't exist."}, + MCRF_FUNCTION(delTimer, + MCRF_SIG("(name: str)", "None"), + MCRF_DESC("Stop and remove a timer."), + MCRF_ARGS_START + MCRF_ARG("name", "Timer identifier to remove") + MCRF_RETURNS("None") + MCRF_NOTE("No error is raised if the timer doesn't exist.") + )}, {"exit", McRFPy_API::_exit, METH_NOARGS, - "exit() -> None\n\n" - "Cleanly shut down the game engine and exit the application.\n\n" - "Note:\n" - " This immediately closes the window and terminates the program."}, + MCRF_FUNCTION(exit, + MCRF_SIG("()", "None"), + MCRF_DESC("Cleanly shut down the game engine and exit the application."), + MCRF_RETURNS("None") + MCRF_NOTE("This immediately closes the window and terminates the program.") + )}, {"setScale", McRFPy_API::_setScale, METH_VARARGS, - "setScale(multiplier: float) -> None\n\n" - "Scale the game window size.\n\n" - "Args:\n" - " multiplier: Scale factor (e.g., 2.0 for double size)\n\n" - "Note:\n" - " The internal resolution remains 1024x768, but the window is scaled.\n" - " This is deprecated - use Window.resolution instead."}, - + MCRF_FUNCTION(setScale, + MCRF_SIG("(multiplier: float)", "None"), + MCRF_DESC("Scale the game window size."), + MCRF_ARGS_START + MCRF_ARG("multiplier", "Scale factor (e.g., 2.0 for double size)") + MCRF_RETURNS("None") + MCRF_NOTE("The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.") + )}, + {"find", McRFPy_API::_find, METH_VARARGS, - "find(name: str, scene: str = None) -> UIDrawable | None\n\n" - "Find the first UI element with the specified name.\n\n" - "Args:\n" - " name: Exact name to search for\n" - " scene: Scene to search in (default: current scene)\n\n" - "Returns:\n" - " Frame, Caption, Sprite, Grid, or Entity if found; None otherwise\n\n" - "Note:\n" - " Searches scene UI elements and entities within grids."}, + MCRF_FUNCTION(find, + MCRF_SIG("(name: str, scene: str = None)", "UIDrawable | None"), + MCRF_DESC("Find the first UI element with the specified name."), + MCRF_ARGS_START + MCRF_ARG("name", "Exact name to search for") + MCRF_ARG("scene", "Scene to search in (default: current scene)") + MCRF_RETURNS("Frame, Caption, Sprite, Grid, or Entity if found; None otherwise") + MCRF_NOTE("Searches scene UI elements and entities within grids.") + )}, {"findAll", McRFPy_API::_findAll, METH_VARARGS, - "findAll(pattern: str, scene: str = None) -> list\n\n" - "Find all UI elements matching a name pattern.\n\n" - "Args:\n" - " pattern: Name pattern with optional wildcards (* matches any characters)\n" - " scene: Scene to search in (default: current scene)\n\n" - "Returns:\n" - " list: All matching UI elements and entities\n\n" - "Example:\n" - " findAll('enemy*') # Find all elements starting with 'enemy'\n" - " findAll('*_button') # Find all elements ending with '_button'"}, - + MCRF_FUNCTION(findAll, + MCRF_SIG("(pattern: str, scene: str = None)", "list"), + MCRF_DESC("Find all UI elements matching a name pattern."), + MCRF_ARGS_START + MCRF_ARG("pattern", "Name pattern with optional wildcards (* matches any characters)") + MCRF_ARG("scene", "Scene to search in (default: current scene)") + MCRF_RETURNS("list: All matching UI elements and entities") + MCRF_NOTE("Example: findAll('enemy*') finds all elements starting with 'enemy', findAll('*_button') finds all elements ending with '_button'") + )}, + {"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS, - "getMetrics() -> dict\n\n" - "Get current performance metrics.\n\n" - "Returns:\n" - " dict: Performance data with keys:\n" - " - frame_time: Last frame duration in seconds\n" - " - avg_frame_time: Average frame time\n" - " - fps: Frames per second\n" - " - draw_calls: Number of draw calls\n" - " - ui_elements: Total UI element count\n" - " - visible_elements: Visible element count\n" - " - current_frame: Frame counter\n" - " - runtime: Total runtime in seconds"}, - + MCRF_FUNCTION(getMetrics, + MCRF_SIG("()", "dict"), + MCRF_DESC("Get current performance metrics."), + MCRF_RETURNS("dict: Performance data with keys: frame_time (last frame duration in seconds), avg_frame_time (average frame time), fps (frames per second), draw_calls (number of draw calls), ui_elements (total UI element count), visible_elements (visible element count), current_frame (frame counter), runtime (total runtime in seconds)") + )}, + {NULL, NULL, 0, NULL} }; static PyModuleDef mcrfpyModule = { PyModuleDef_HEAD_INIT, /* m_base - Always initialize this member to PyModuleDef_HEAD_INIT. */ "mcrfpy", /* m_name */ - PyDoc_STR("McRogueFace Python API\\n\\n" - "Core game engine interface for creating roguelike games with Python.\\n\\n" - "This module provides:\\n" - "- Scene management (createScene, setScene, currentScene)\\n" - "- UI components (Frame, Caption, Sprite, Grid)\\n" - "- Entity system for game objects\\n" - "- Audio playback (sound effects and music)\\n" - "- Timer system for scheduled events\\n" - "- Input handling\\n" - "- Performance metrics\\n\\n" - "Example:\\n" - " import mcrfpy\\n" - " \\n" - " # Create a new scene\\n" - " mcrfpy.createScene('game')\\n" - " mcrfpy.setScene('game')\\n" - " \\n" - " # Add UI elements\\n" - " frame = mcrfpy.Frame(10, 10, 200, 100)\\n" - " caption = mcrfpy.Caption('Hello World', 50, 50)\\n" - " mcrfpy.sceneUI().extend([frame, caption])\\n"), + PyDoc_STR("McRogueFace Python API\n\n" + "Core game engine interface for creating roguelike games with Python.\n\n" + "This module provides:\n" + "- Scene management (createScene, setScene, currentScene)\n" + "- UI components (Frame, Caption, Sprite, Grid)\n" + "- Entity system for game objects\n" + "- Audio playback (sound effects and music)\n" + "- Timer system for scheduled events\n" + "- Input handling\n" + "- Performance metrics\n\n" + "Example:\n" + " import mcrfpy\n" + " \n" + " # Create a new scene\n" + " mcrfpy.createScene('game')\n" + " mcrfpy.setScene('game')\n" + " \n" + " # Add UI elements\n" + " frame = mcrfpy.Frame(10, 10, 200, 100)\n" + " caption = mcrfpy.Caption('Hello World', 50, 50)\n" + " mcrfpy.sceneUI().extend([frame, caption])\n"), -1, /* m_size - Setting m_size to -1 means that the module does not support sub-interpreters, because it has global state. */ mcrfpyMethods, /* m_methods */ NULL, /* m_slots - An array of slot definitions ... When using single-phase initialization, m_slots must be NULL. */ diff --git a/src/McRFPy_Doc.h b/src/McRFPy_Doc.h new file mode 100644 index 0000000..22ecdea --- /dev/null +++ b/src/McRFPy_Doc.h @@ -0,0 +1,31 @@ +#ifndef MCRFPY_DOC_H +#define MCRFPY_DOC_H + +// Section builders for documentation +#define MCRF_SIG(params, ret) params " -> " ret "\n\n" +#define MCRF_DESC(text) text "\n\n" +#define MCRF_ARGS_START "Args:\n" +#define MCRF_ARG(name, desc) " " name ": " desc "\n" +#define MCRF_RETURNS(text) "\nReturns:\n " text "\n" +#define MCRF_RAISES(exc, desc) "\nRaises:\n " exc ": " desc "\n" +#define MCRF_NOTE(text) "\nNote:\n " text "\n" + +// Link to external documentation +// Format: MCRF_LINK("docs/file.md", "Link Text") +// Parsers detect this pattern and format per output type +#define MCRF_LINK(ref, text) "\nSee also: " text " (" ref ")\n" + +// Main documentation macros +#define MCRF_METHOD_DOC(name, sig, desc, ...) \ + name sig desc __VA_ARGS__ + +#define MCRF_FUNCTION(name, ...) \ + MCRF_METHOD_DOC(#name, __VA_ARGS__) + +#define MCRF_METHOD(cls, name, ...) \ + MCRF_METHOD_DOC(#name, __VA_ARGS__) + +#define MCRF_PROPERTY(name, desc) \ + desc + +#endif // MCRFPY_DOC_H diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index c81a2ea..265646d 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -1,5 +1,6 @@ #include "PyAnimation.h" #include "McRFPy_API.h" +#include "McRFPy_Doc.h" #include "UIDrawable.h" #include "UIFrame.h" #include "UICaption.h" @@ -261,33 +262,58 @@ PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) } PyGetSetDef PyAnimation::getsetters[] = { - {"property", (getter)get_property, NULL, "Target property name", NULL}, - {"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL}, - {"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL}, - {"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL}, - {"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL}, + {"property", (getter)get_property, NULL, + MCRF_PROPERTY(property, "Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index')."), NULL}, + {"duration", (getter)get_duration, NULL, + MCRF_PROPERTY(duration, "Animation duration in seconds (float, read-only). Total time for the animation to complete."), NULL}, + {"elapsed", (getter)get_elapsed, NULL, + MCRF_PROPERTY(elapsed, "Elapsed time in seconds (float, read-only). Time since the animation started."), NULL}, + {"is_complete", (getter)get_is_complete, NULL, + MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL}, + {"is_delta", (getter)get_is_delta, NULL, + MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL}, {NULL} }; PyMethodDef PyAnimation::methods[] = { {"start", (PyCFunction)start, METH_VARARGS, - "start(target) -> None\n\n" - "Start the animation on a target UI element.\n\n" - "Args:\n" - " target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n" - "Note:\n" - " The animation will automatically stop if the target is destroyed."}, + MCRF_METHOD(Animation, start, + MCRF_SIG("(target: UIDrawable)", "None"), + MCRF_DESC("Start the animation on a target UI element."), + MCRF_ARGS_START + MCRF_ARG("target", "The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)") + MCRF_RETURNS("None") + MCRF_NOTE("The animation will automatically stop if the target is destroyed. Call AnimationManager.update(delta_time) each frame to progress animations.") + )}, {"update", (PyCFunction)update, METH_VARARGS, - "Update the animation by deltaTime (returns True if still running)"}, + MCRF_METHOD(Animation, update, + MCRF_SIG("(delta_time: float)", "bool"), + MCRF_DESC("Update the animation by the given time delta."), + MCRF_ARGS_START + MCRF_ARG("delta_time", "Time elapsed since last update in seconds") + MCRF_RETURNS("bool: True if animation is still running, False if complete") + MCRF_NOTE("Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.") + )}, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, - "Get the current interpolated value"}, + MCRF_METHOD(Animation, get_current_value, + MCRF_SIG("()", "Any"), + MCRF_DESC("Get the current interpolated value of the animation."), + MCRF_RETURNS("Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str)") + MCRF_NOTE("Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).") + )}, {"complete", (PyCFunction)complete, METH_NOARGS, - "complete() -> None\n\n" - "Complete the animation immediately by jumping to the final value."}, + MCRF_METHOD(Animation, complete, + MCRF_SIG("()", "None"), + MCRF_DESC("Complete the animation immediately by jumping to the final value."), + MCRF_RETURNS("None") + MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.") + )}, {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS, - "hasValidTarget() -> bool\n\n" - "Check if the animation still has a valid target.\n\n" - "Returns:\n" - " True if the target still exists, False if it was destroyed."}, + MCRF_METHOD(Animation, hasValidTarget, + MCRF_SIG("()", "bool"), + MCRF_DESC("Check if the animation still has a valid target."), + MCRF_RETURNS("bool: True if the target still exists, False if it was destroyed") + MCRF_NOTE("Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.") + )}, {NULL} }; \ No newline at end of file diff --git a/src/PyColor.cpp b/src/PyColor.cpp index e1a0b1a..4fd2154 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -2,21 +2,50 @@ #include "McRFPy_API.h" #include "PyObjectUtils.h" #include "PyRAII.h" +#include "McRFPy_Doc.h" #include #include PyGetSetDef PyColor::getsetters[] = { - {"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0}, - {"g", (getter)PyColor::get_member, (setter)PyColor::set_member, "Green component", (void*)1}, - {"b", (getter)PyColor::get_member, (setter)PyColor::set_member, "Blue component", (void*)2}, - {"a", (getter)PyColor::get_member, (setter)PyColor::set_member, "Alpha component", (void*)3}, + {"r", (getter)PyColor::get_member, (setter)PyColor::set_member, + MCRF_PROPERTY(r, "Red component (0-255). Automatically clamped to valid range."), (void*)0}, + {"g", (getter)PyColor::get_member, (setter)PyColor::set_member, + MCRF_PROPERTY(g, "Green component (0-255). Automatically clamped to valid range."), (void*)1}, + {"b", (getter)PyColor::get_member, (setter)PyColor::set_member, + MCRF_PROPERTY(b, "Blue component (0-255). Automatically clamped to valid range."), (void*)2}, + {"a", (getter)PyColor::get_member, (setter)PyColor::set_member, + MCRF_PROPERTY(a, "Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range."), (void*)3}, {NULL} }; PyMethodDef PyColor::methods[] = { - {"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"}, - {"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"}, - {"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"}, + {"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, + MCRF_METHOD(Color, from_hex, + MCRF_SIG("(hex_string: str)", "Color"), + MCRF_DESC("Create a Color from a hexadecimal string."), + MCRF_ARGS_START + MCRF_ARG("hex_string", "Hex color string (e.g., '#FF0000', 'FF0000', '#AABBCCDD' for RGBA)") + MCRF_RETURNS("Color: New Color object with values from hex string") + MCRF_RAISES("ValueError", "If hex string is not 6 or 8 characters (RGB or RGBA)") + MCRF_NOTE("This is a class method. Call as Color.from_hex('#FF0000')") + )}, + {"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, + MCRF_METHOD(Color, to_hex, + MCRF_SIG("()", "str"), + MCRF_DESC("Convert this Color to a hexadecimal string."), + MCRF_RETURNS("str: Hex string in format '#RRGGBB' or '#RRGGBBAA' (if alpha < 255)") + MCRF_NOTE("Alpha component is only included if not fully opaque (< 255)") + )}, + {"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, + MCRF_METHOD(Color, lerp, + MCRF_SIG("(other: Color, t: float)", "Color"), + MCRF_DESC("Linearly interpolate between this color and another."), + MCRF_ARGS_START + MCRF_ARG("other", "The target Color to interpolate towards") + MCRF_ARG("t", "Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]") + MCRF_RETURNS("Color: New Color representing the interpolated value") + MCRF_NOTE("All components (r, g, b, a) are interpolated independently") + )}, {NULL} }; diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp index 7773a26..2fe0e93 100644 --- a/src/PyDrawable.cpp +++ b/src/PyDrawable.cpp @@ -1,5 +1,6 @@ #include "PyDrawable.h" #include "McRFPy_API.h" +#include "McRFPy_Doc.h" // Click property getter static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) @@ -98,14 +99,26 @@ static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* // GetSetDef array for properties static PyGetSetDef PyDrawable_getsetters[] = { - {"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, - "Callable executed when object is clicked", NULL}, + {"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, + MCRF_PROPERTY(click, + "Callable executed when object is clicked. " + "Function receives (x, y) coordinates of click." + ), NULL}, {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, - "Z-order for rendering (lower values rendered first)", NULL}, + MCRF_PROPERTY(z_index, + "Z-order for rendering (lower values rendered first). " + "Automatically triggers scene resort when changed." + ), NULL}, {"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible, - "Whether the object is visible", NULL}, + MCRF_PROPERTY(visible, + "Whether the object is visible (bool). " + "Invisible objects are not rendered or clickable." + ), NULL}, {"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity, - "Opacity level (0.0 = transparent, 1.0 = opaque)", NULL}, + MCRF_PROPERTY(opacity, + "Opacity level (0.0 = transparent, 1.0 = opaque). " + "Automatically clamped to valid range [0.0, 1.0]." + ), NULL}, {NULL} // Sentinel }; @@ -143,11 +156,30 @@ static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) // Method definitions static PyMethodDef PyDrawable_methods[] = { {"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS, - "Get bounding box as (x, y, width, height)"}, + MCRF_METHOD(Drawable, get_bounds, + MCRF_SIG("()", "tuple"), + MCRF_DESC("Get the bounding rectangle of this drawable element."), + MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") + MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") + )}, {"move", (PyCFunction)PyDrawable_move, METH_VARARGS, - "Move by relative offset (dx, dy)"}, + MCRF_METHOD(Drawable, move, + MCRF_SIG("(dx: float, dy: float)", "None"), + MCRF_DESC("Move the element by a relative offset."), + MCRF_ARGS_START + MCRF_ARG("dx", "Horizontal offset in pixels") + MCRF_ARG("dy", "Vertical offset in pixels") + MCRF_NOTE("This modifies the x and y position properties by the given amounts.") + )}, {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, - "Resize to new dimensions (width, height)"}, + MCRF_METHOD(Drawable, resize, + MCRF_SIG("(width: float, height: float)", "None"), + MCRF_DESC("Resize the element to new dimensions."), + MCRF_ARGS_START + MCRF_ARG("width", "New width in pixels") + MCRF_ARG("height", "New height in pixels") + MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") + )}, {NULL} // Sentinel }; diff --git a/src/PyFont.cpp b/src/PyFont.cpp index 157656e..22ba217 100644 --- a/src/PyFont.cpp +++ b/src/PyFont.cpp @@ -1,5 +1,6 @@ #include "PyFont.h" #include "McRFPy_API.h" +#include "McRFPy_Doc.h" PyFont::PyFont(std::string filename) @@ -73,7 +74,9 @@ PyObject* PyFont::get_source(PyFontObject* self, void* closure) } PyGetSetDef PyFont::getsetters[] = { - {"family", (getter)PyFont::get_family, NULL, "Font family name", NULL}, - {"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL}, + {"family", (getter)PyFont::get_family, NULL, + MCRF_PROPERTY(family, "Font family name (str, read-only). Retrieved from font metadata."), NULL}, + {"source", (getter)PyFont::get_source, NULL, + MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this font."), NULL}, {NULL} // Sentinel }; diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index 491024e..43489a8 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -2,6 +2,7 @@ #include "PyScene.h" #include "GameEngine.h" #include "McRFPy_API.h" +#include "McRFPy_Doc.h" #include // Static map to store Python scene objects by name @@ -213,19 +214,38 @@ void PySceneClass::call_on_resize(PySceneObject* self, int width, int height) // Properties PyGetSetDef PySceneClass::getsetters[] = { - {"name", (getter)get_name, NULL, "Scene name", NULL}, - {"active", (getter)get_active, NULL, "Whether this scene is currently active", NULL}, + {"name", (getter)get_name, NULL, + MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL}, + {"active", (getter)get_active, NULL, + MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL}, {NULL} }; // Methods PyMethodDef PySceneClass::methods[] = { {"activate", (PyCFunction)activate, METH_NOARGS, - "Make this the active scene"}, + MCRF_METHOD(SceneClass, activate, + MCRF_SIG("()", "None"), + MCRF_DESC("Make this the active scene."), + MCRF_RETURNS("None") + MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.") + )}, {"get_ui", (PyCFunction)get_ui, METH_NOARGS, - "Get the UI element collection for this scene"}, + MCRF_METHOD(SceneClass, get_ui, + MCRF_SIG("()", "UICollection"), + MCRF_DESC("Get the UI element collection for this scene."), + MCRF_RETURNS("UICollection: Collection of UI elements (Frames, Captions, Sprites, Grids) in this scene") + MCRF_NOTE("Use to add, remove, or iterate over UI elements. Changes are reflected immediately.") + )}, {"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS, - "Register a keyboard handler function (alternative to overriding on_keypress)"}, + MCRF_METHOD(SceneClass, register_keyboard, + MCRF_SIG("(callback: callable)", "None"), + MCRF_DESC("Register a keyboard event handler function."), + MCRF_ARGS_START + MCRF_ARG("callback", "Function that receives (key: str, pressed: bool) when keyboard events occur") + MCRF_RETURNS("None") + MCRF_NOTE("Alternative to overriding on_keypress() method. Handler is called for both key press and release events.") + )}, {NULL} }; diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 631d8af..1681d37 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -1,5 +1,6 @@ #include "PyTexture.h" #include "McRFPy_API.h" +#include "McRFPy_Doc.h" PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) : source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0) @@ -131,11 +132,17 @@ PyObject* PyTexture::get_source(PyTextureObject* self, void* closure) } PyGetSetDef PyTexture::getsetters[] = { - {"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL}, - {"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL}, - {"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL}, - {"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL}, - {"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL}, - {"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL}, + {"sprite_width", (getter)PyTexture::get_sprite_width, NULL, + MCRF_PROPERTY(sprite_width, "Width of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL}, + {"sprite_height", (getter)PyTexture::get_sprite_height, NULL, + MCRF_PROPERTY(sprite_height, "Height of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL}, + {"sheet_width", (getter)PyTexture::get_sheet_width, NULL, + MCRF_PROPERTY(sheet_width, "Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width."), NULL}, + {"sheet_height", (getter)PyTexture::get_sheet_height, NULL, + MCRF_PROPERTY(sheet_height, "Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height."), NULL}, + {"sprite_count", (getter)PyTexture::get_sprite_count, NULL, + MCRF_PROPERTY(sprite_count, "Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height."), NULL}, + {"source", (getter)PyTexture::get_source, NULL, + MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL}, {NULL} // Sentinel }; diff --git a/src/PyTimer.cpp b/src/PyTimer.cpp index d1e89ec..95f619a 100644 --- a/src/PyTimer.cpp +++ b/src/PyTimer.cpp @@ -3,6 +3,7 @@ #include "GameEngine.h" #include "Resources.h" #include "PythonObjectCache.h" +#include "McRFPy_Doc.h" #include PyObject* PyTimer::repr(PyObject* self) { @@ -307,38 +308,50 @@ PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) { PyGetSetDef PyTimer::getsetters[] = { {"name", (getter)PyTimer::get_name, NULL, - "Timer name (read-only)", NULL}, + MCRF_PROPERTY(name, "Timer name (str, read-only). Unique identifier for this timer."), NULL}, {"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval, - "Timer interval in milliseconds", NULL}, + MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL}, {"remaining", (getter)PyTimer::get_remaining, NULL, - "Time remaining until next trigger in milliseconds", NULL}, + MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL}, {"paused", (getter)PyTimer::get_paused, NULL, - "Whether the timer is paused", NULL}, + MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL}, {"active", (getter)PyTimer::get_active, NULL, - "Whether the timer is active and not paused", NULL}, + MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL}, {"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback, - "The callback function to be called", NULL}, + MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL}, {"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once, - "Whether the timer stops after firing once", NULL}, + MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL}, {NULL} }; PyMethodDef PyTimer::methods[] = { {"pause", (PyCFunction)PyTimer::pause, METH_NOARGS, - "pause() -> None\n\n" - "Pause the timer, preserving the time remaining until next trigger.\n" - "The timer can be resumed later with resume()."}, + MCRF_METHOD(Timer, pause, + MCRF_SIG("()", "None"), + MCRF_DESC("Pause the timer, preserving the time remaining until next trigger."), + MCRF_RETURNS("None") + MCRF_NOTE("The timer can be resumed later with resume(). Time spent paused does not count toward the interval.") + )}, {"resume", (PyCFunction)PyTimer::resume, METH_NOARGS, - "resume() -> None\n\n" - "Resume a paused timer from where it left off.\n" - "Has no effect if the timer is not paused."}, + MCRF_METHOD(Timer, resume, + MCRF_SIG("()", "None"), + MCRF_DESC("Resume a paused timer from where it left off."), + MCRF_RETURNS("None") + MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.") + )}, {"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS, - "cancel() -> None\n\n" - "Cancel the timer and remove it from the timer system.\n" - "The timer will no longer fire and cannot be restarted."}, + MCRF_METHOD(Timer, cancel, + MCRF_SIG("()", "None"), + MCRF_DESC("Cancel the timer and remove it from the timer system."), + MCRF_RETURNS("None") + MCRF_NOTE("The timer will no longer fire and cannot be restarted. The callback will not be called again.") + )}, {"restart", (PyCFunction)PyTimer::restart, METH_NOARGS, - "restart() -> None\n\n" - "Restart the timer from the beginning.\n" - "Resets the timer to fire after a full interval from now."}, + MCRF_METHOD(Timer, restart, + MCRF_SIG("()", "None"), + MCRF_DESC("Restart the timer from the beginning."), + MCRF_RETURNS("None") + MCRF_NOTE("Resets the timer to fire after a full interval from now, regardless of remaining time.") + )}, {NULL} }; \ No newline at end of file diff --git a/src/PyVector.cpp b/src/PyVector.cpp index 16acd51..acb60c0 100644 --- a/src/PyVector.cpp +++ b/src/PyVector.cpp @@ -1,21 +1,65 @@ #include "PyVector.h" #include "PyObjectUtils.h" +#include "McRFPy_Doc.h" #include PyGetSetDef PyVector::getsetters[] = { - {"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0}, - {"y", (getter)PyVector::get_member, (setter)PyVector::set_member, "Y/vertical component", (void*)1}, + {"x", (getter)PyVector::get_member, (setter)PyVector::set_member, + MCRF_PROPERTY(x, "X coordinate of the vector (float)"), (void*)0}, + {"y", (getter)PyVector::get_member, (setter)PyVector::set_member, + MCRF_PROPERTY(y, "Y coordinate of the vector (float)"), (void*)1}, {NULL} }; PyMethodDef PyVector::methods[] = { - {"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, "Return the length of the vector"}, - {"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, "Return the squared length of the vector"}, - {"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, "Return a unit vector in the same direction"}, - {"dot", (PyCFunction)PyVector::dot, METH_O, "Return the dot product with another vector"}, - {"distance_to", (PyCFunction)PyVector::distance_to, METH_O, "Return the distance to another vector"}, - {"angle", (PyCFunction)PyVector::angle, METH_NOARGS, "Return the angle in radians from the positive X axis"}, - {"copy", (PyCFunction)PyVector::copy, METH_NOARGS, "Return a copy of this vector"}, + {"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, + MCRF_METHOD(Vector, magnitude, + MCRF_SIG("()", "float"), + MCRF_DESC("Calculate the length/magnitude of this vector."), + MCRF_RETURNS("float: The magnitude of the vector") + )}, + {"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, + MCRF_METHOD(Vector, magnitude_squared, + MCRF_SIG("()", "float"), + MCRF_DESC("Calculate the squared magnitude of this vector."), + MCRF_RETURNS("float: The squared magnitude (faster than magnitude())") + MCRF_NOTE("Use this for comparisons to avoid expensive square root calculation.") + )}, + {"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, + MCRF_METHOD(Vector, normalize, + MCRF_SIG("()", "Vector"), + MCRF_DESC("Return a unit vector in the same direction."), + MCRF_RETURNS("Vector: New normalized vector with magnitude 1.0") + MCRF_NOTE("For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception") + )}, + {"dot", (PyCFunction)PyVector::dot, METH_O, + MCRF_METHOD(Vector, dot, + MCRF_SIG("(other: Vector)", "float"), + MCRF_DESC("Calculate the dot product with another vector."), + MCRF_ARGS_START + MCRF_ARG("other", "The other vector") + MCRF_RETURNS("float: Dot product of the two vectors") + )}, + {"distance_to", (PyCFunction)PyVector::distance_to, METH_O, + MCRF_METHOD(Vector, distance_to, + MCRF_SIG("(other: Vector)", "float"), + MCRF_DESC("Calculate the distance to another vector."), + MCRF_ARGS_START + MCRF_ARG("other", "The other vector") + MCRF_RETURNS("float: Distance between the two vectors") + )}, + {"angle", (PyCFunction)PyVector::angle, METH_NOARGS, + MCRF_METHOD(Vector, angle, + MCRF_SIG("()", "float"), + MCRF_DESC("Get the angle of this vector in radians."), + MCRF_RETURNS("float: Angle in radians from positive x-axis") + )}, + {"copy", (PyCFunction)PyVector::copy, METH_NOARGS, + MCRF_METHOD(Vector, copy, + MCRF_SIG("()", "Vector"), + MCRF_DESC("Create a copy of this vector."), + MCRF_RETURNS("Vector: New Vector object with same x and y values") + )}, {NULL} }; diff --git a/src/PyWindow.cpp b/src/PyWindow.cpp index c35f5c2..17fb1ba 100644 --- a/src/PyWindow.cpp +++ b/src/PyWindow.cpp @@ -1,6 +1,7 @@ #include "PyWindow.h" #include "GameEngine.h" #include "McRFPy_API.h" +#include "McRFPy_Doc.h" #include #include @@ -483,32 +484,49 @@ int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* clos // Property definitions PyGetSetDef PyWindow::getsetters[] = { - {"resolution", (getter)get_resolution, (setter)set_resolution, - "Window resolution as (width, height) tuple", NULL}, + {"resolution", (getter)get_resolution, (setter)set_resolution, + MCRF_PROPERTY(resolution, "Window resolution as (width, height) tuple. Setting this recreates the window."), NULL}, {"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen, - "Window fullscreen state", NULL}, + MCRF_PROPERTY(fullscreen, "Window fullscreen state (bool). Setting this recreates the window."), NULL}, {"vsync", (getter)get_vsync, (setter)set_vsync, - "Vertical sync enabled state", NULL}, + MCRF_PROPERTY(vsync, "Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate."), NULL}, {"title", (getter)get_title, (setter)set_title, - "Window title string", NULL}, + MCRF_PROPERTY(title, "Window title string (str). Displayed in the window title bar."), NULL}, {"visible", (getter)get_visible, (setter)set_visible, - "Window visibility state", NULL}, + MCRF_PROPERTY(visible, "Window visibility state (bool). Hidden windows still process events."), NULL}, {"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit, - "Frame rate limit (0 for unlimited)", NULL}, + MCRF_PROPERTY(framerate_limit, "Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate."), NULL}, {"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution, - "Fixed game resolution as (width, height) tuple", NULL}, + MCRF_PROPERTY(game_resolution, "Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling."), NULL}, {"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode, - "Viewport scaling mode: 'center', 'stretch', or 'fit'", NULL}, + MCRF_PROPERTY(scaling_mode, "Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio)."), NULL}, {NULL} }; // Method definitions PyMethodDef PyWindow::methods[] = { {"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS, - "Get the Window singleton instance"}, + MCRF_METHOD(Window, get, + MCRF_SIG("()", "Window"), + MCRF_DESC("Get the Window singleton instance."), + MCRF_RETURNS("Window: The global window object") + MCRF_NOTE("This is a class method. Call as Window.get(). There is only one window instance per application.") + )}, {"center", (PyCFunction)PyWindow::center, METH_NOARGS, - "Center the window on the screen"}, + MCRF_METHOD(Window, center, + MCRF_SIG("()", "None"), + MCRF_DESC("Center the window on the screen."), + MCRF_RETURNS("None") + MCRF_NOTE("Only works in windowed mode. Has no effect when fullscreen or in headless mode.") + )}, {"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS, - "Take a screenshot. Pass filename to save to file, or get raw bytes if no filename."}, + MCRF_METHOD(Window, screenshot, + MCRF_SIG("(filename: str = None)", "bytes | None"), + MCRF_DESC("Take a screenshot of the current window contents."), + MCRF_ARGS_START + MCRF_ARG("filename", "Optional path to save screenshot. If omitted, returns raw RGBA bytes.") + MCRF_RETURNS("bytes | None: Raw RGBA pixel data if no filename given, otherwise None after saving") + MCRF_NOTE("Screenshot is taken at the actual window resolution. Use after render loop update for current frame.") + )}, {NULL} }; \ No newline at end of file diff --git a/src/UIBase.h b/src/UIBase.h index d57e54c..f746168 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -1,5 +1,6 @@ #pragma once #include "Python.h" +#include "McRFPy_Doc.h" #include class UIEntity; @@ -78,11 +79,30 @@ static PyObject* UIDrawable_resize(T* self, PyObject* args) // Macro to add common UIDrawable methods to a method array #define UIDRAWABLE_METHODS \ {"get_bounds", (PyCFunction)UIDrawable_get_bounds, METH_NOARGS, \ - "Get bounding box as (x, y, width, height)"}, \ + MCRF_METHOD(Drawable, get_bounds, \ + MCRF_SIG("()", "tuple"), \ + MCRF_DESC("Get the bounding rectangle of this drawable element."), \ + MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \ + MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \ + )}, \ {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ - "Move by relative offset (dx, dy)"}, \ + MCRF_METHOD(Drawable, move, \ + MCRF_SIG("(dx: float, dy: float)", "None"), \ + MCRF_DESC("Move the element by a relative offset."), \ + MCRF_ARGS_START \ + MCRF_ARG("dx", "Horizontal offset in pixels") \ + MCRF_ARG("dy", "Vertical offset in pixels") \ + MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \ + )}, \ {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ - "Resize to new dimensions (width, height)"} + MCRF_METHOD(Drawable, resize, \ + MCRF_SIG("(width: float, height: float)", "None"), \ + MCRF_DESC("Resize the element to new dimensions."), \ + MCRF_ARGS_START \ + MCRF_ARG("width", "New width in pixels") \ + MCRF_ARG("height", "New height in pixels") \ + MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \ + )} // Property getters/setters for visible and opacity template @@ -132,8 +152,14 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) // Macro to add common UIDrawable properties to a getsetters array #define UIDRAWABLE_GETSETTERS \ {"visible", (getter)UIDrawable_get_visible, (setter)UIDrawable_set_visible, \ - "Visibility flag", NULL}, \ + MCRF_PROPERTY(visible, \ + "Whether the object is visible (bool). " \ + "Invisible objects are not rendered or clickable." \ + ), NULL}, \ {"opacity", (getter)UIDrawable_get_opacity, (setter)UIDrawable_set_opacity, \ - "Opacity (0.0 = transparent, 1.0 = opaque)", NULL} + MCRF_PROPERTY(opacity, \ + "Opacity level (0.0 = transparent, 1.0 = opaque). " \ + "Automatically clamped to valid range [0.0, 1.0]." \ + ), NULL} // UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 6ac1adb..33cff43 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -273,8 +273,16 @@ PyGetSetDef UICaption::getsetters[] = { //{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL}, {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + MCRF_PROPERTY(click, + "Callable executed when object is clicked. " + "Function receives (x, y) coordinates of click." + ), (void*)PyObjectsEnum::UICAPTION}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + MCRF_PROPERTY(z_index, + "Z-order for rendering (lower values rendered first). " + "Automatically triggers scene resort when changed." + ), (void*)PyObjectsEnum::UICAPTION}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION}, UIDRAWABLE_GETSETTERS, {NULL} diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 4ceb8b8..8eb0522 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -398,8 +398,16 @@ PyGetSetDef UIFrame::getsetters[] = { {"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Fill color of the rectangle", (void*)0}, {"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1}, {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + MCRF_PROPERTY(click, + "Callable executed when object is clicked. " + "Function receives (x, y) coordinates of click." + ), (void*)PyObjectsEnum::UIFRAME}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + MCRF_PROPERTY(z_index, + "Z-order for rendering (lower values rendered first). " + "Automatically triggers scene resort when changed." + ), (void*)PyObjectsEnum::UIFRAME}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME}, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME}, {"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL}, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index b07e596..060c9c0 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1418,7 +1418,11 @@ PyGetSetDef UIGrid::getsetters[] = { {"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5}, {"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + MCRF_PROPERTY(click, + "Callable executed when object is clicked. " + "Function receives (x, y) coordinates of click." + ), (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 {"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL}, @@ -1428,7 +1432,11 @@ PyGetSetDef UIGrid::getsetters[] = { {"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled, "Whether to use perspective-based FOV rendering. When True with no valid entity, " "all cells appear undiscovered.", NULL}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + MCRF_PROPERTY(z_index, + "Z-order for rendering (lower values rendered first). " + "Automatically triggers scene resort when changed." + ), (void*)PyObjectsEnum::UIGRID}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID}, UIDRAWABLE_GETSETTERS, {NULL} /* Sentinel */ diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 4be581c..a1d697b 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -339,8 +339,16 @@ PyGetSetDef UISprite::getsetters[] = { {"sprite_index", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL}, {"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Sprite index (DEPRECATED: use sprite_index instead)", NULL}, {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL}, - {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE}, - {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + MCRF_PROPERTY(click, + "Callable executed when object is clicked. " + "Function receives (x, y) coordinates of click." + ), (void*)PyObjectsEnum::UISPRITE}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + MCRF_PROPERTY(z_index, + "Z-order for rendering (lower values rendered first). " + "Automatically triggers scene resort when changed." + ), (void*)PyObjectsEnum::UISPRITE}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE}, {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE}, UIDRAWABLE_GETSETTERS, diff --git a/tools/generate_api_docs.py b/tools/generate_api_docs.py deleted file mode 100644 index d1e100f..0000000 --- a/tools/generate_api_docs.py +++ /dev/null @@ -1,482 +0,0 @@ -#!/usr/bin/env python3 -"""Generate API reference documentation for McRogueFace. - -This script generates comprehensive API documentation in multiple formats: -- Markdown for GitHub/documentation sites -- HTML for local browsing -- RST for Sphinx integration (future) -""" - -import os -import sys -import inspect -import datetime -from typing import Dict, List, Any, Optional -from pathlib import Path - -# We need to run this with McRogueFace as the interpreter -# so mcrfpy is available -import mcrfpy - -def escape_markdown(text: str) -> str: - """Escape special markdown characters.""" - if not text: - return "" - # Escape backticks in inline code - return text.replace("`", "\\`") - -def format_signature(name: str, doc: str) -> str: - """Extract and format function signature from docstring.""" - if not doc: - return f"{name}(...)" - - lines = doc.strip().split('\n') - if lines and '(' in lines[0]: - # First line contains signature - return lines[0].split('->')[0].strip() - - return f"{name}(...)" - -def get_class_info(cls: type) -> Dict[str, Any]: - """Extract comprehensive information about a class.""" - info = { - 'name': cls.__name__, - 'doc': cls.__doc__ or "", - 'methods': [], - 'properties': [], - 'bases': [base.__name__ for base in cls.__bases__ if base.__name__ != 'object'], - } - - # Get all attributes - for attr_name in sorted(dir(cls)): - if attr_name.startswith('_') and not attr_name.startswith('__'): - continue - - try: - attr = getattr(cls, attr_name) - - if isinstance(attr, property): - prop_info = { - 'name': attr_name, - 'doc': (attr.fget.__doc__ if attr.fget else "") or "", - 'readonly': attr.fset is None - } - info['properties'].append(prop_info) - elif callable(attr) and not attr_name.startswith('__'): - method_info = { - 'name': attr_name, - 'doc': attr.__doc__ or "", - 'signature': format_signature(attr_name, attr.__doc__) - } - info['methods'].append(method_info) - except: - pass - - return info - -def get_function_info(func: Any, name: str) -> Dict[str, Any]: - """Extract information about a function.""" - return { - 'name': name, - 'doc': func.__doc__ or "", - 'signature': format_signature(name, func.__doc__) - } - -def generate_markdown_class(cls_info: Dict[str, Any]) -> List[str]: - """Generate markdown documentation for a class.""" - lines = [] - - # Class header - lines.append(f"### class `{cls_info['name']}`") - if cls_info['bases']: - lines.append(f"*Inherits from: {', '.join(cls_info['bases'])}*") - lines.append("") - - # Class description - if cls_info['doc']: - doc_lines = cls_info['doc'].strip().split('\n') - # First line is usually the constructor signature - if doc_lines and '(' in doc_lines[0]: - lines.append(f"```python") - lines.append(doc_lines[0]) - lines.append("```") - lines.append("") - # Rest is description - if len(doc_lines) > 2: - lines.extend(doc_lines[2:]) - lines.append("") - else: - lines.extend(doc_lines) - lines.append("") - - # Properties - if cls_info['properties']: - lines.append("#### Properties") - lines.append("") - for prop in cls_info['properties']: - readonly = " *(readonly)*" if prop['readonly'] else "" - lines.append(f"- **`{prop['name']}`**{readonly}") - if prop['doc']: - lines.append(f" - {prop['doc'].strip()}") - lines.append("") - - # Methods - if cls_info['methods']: - lines.append("#### Methods") - lines.append("") - for method in cls_info['methods']: - lines.append(f"##### `{method['signature']}`") - if method['doc']: - # Parse docstring for better formatting - doc_lines = method['doc'].strip().split('\n') - # Skip the signature line if it's repeated - start = 1 if doc_lines and method['name'] in doc_lines[0] else 0 - for line in doc_lines[start:]: - lines.append(line) - lines.append("") - - lines.append("---") - lines.append("") - return lines - -def generate_markdown_function(func_info: Dict[str, Any]) -> List[str]: - """Generate markdown documentation for a function.""" - lines = [] - - lines.append(f"### `{func_info['signature']}`") - lines.append("") - - if func_info['doc']: - doc_lines = func_info['doc'].strip().split('\n') - # Skip signature line if present - start = 1 if doc_lines and func_info['name'] in doc_lines[0] else 0 - - # Process documentation sections - in_section = None - for line in doc_lines[start:]: - if line.strip() in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']: - in_section = line.strip() - lines.append(f"**{in_section}**") - elif in_section and line.strip(): - # Indent content under sections - lines.append(f"{line}") - else: - lines.append(line) - lines.append("") - - lines.append("---") - lines.append("") - return lines - -def generate_markdown_docs() -> str: - """Generate complete markdown API documentation.""" - lines = [] - - # Header - lines.append("# McRogueFace API Reference") - lines.append("") - lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") - lines.append("") - - # Module description - if mcrfpy.__doc__: - lines.append("## Overview") - lines.append("") - lines.extend(mcrfpy.__doc__.strip().split('\n')) - lines.append("") - - # Table of contents - lines.append("## Table of Contents") - lines.append("") - lines.append("- [Classes](#classes)") - lines.append("- [Functions](#functions)") - lines.append("- [Automation Module](#automation-module)") - lines.append("") - - # Collect all components - classes = [] - functions = [] - constants = [] - - for name in sorted(dir(mcrfpy)): - if name.startswith('_'): - continue - - obj = getattr(mcrfpy, name) - - if isinstance(obj, type): - classes.append((name, obj)) - elif callable(obj): - functions.append((name, obj)) - elif not inspect.ismodule(obj): - constants.append((name, obj)) - - # Document classes - lines.append("## Classes") - lines.append("") - - # Group classes by category - ui_classes = [] - collection_classes = [] - system_classes = [] - other_classes = [] - - for name, cls in classes: - if name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']: - ui_classes.append((name, cls)) - elif 'Collection' in name: - collection_classes.append((name, cls)) - elif name in ['Color', 'Vector', 'Texture', 'Font']: - system_classes.append((name, cls)) - else: - other_classes.append((name, cls)) - - # UI Classes - if ui_classes: - lines.append("### UI Components") - lines.append("") - for name, cls in ui_classes: - lines.extend(generate_markdown_class(get_class_info(cls))) - - # Collections - if collection_classes: - lines.append("### Collections") - lines.append("") - for name, cls in collection_classes: - lines.extend(generate_markdown_class(get_class_info(cls))) - - # System Classes - if system_classes: - lines.append("### System Types") - lines.append("") - for name, cls in system_classes: - lines.extend(generate_markdown_class(get_class_info(cls))) - - # Other Classes - if other_classes: - lines.append("### Other Classes") - lines.append("") - for name, cls in other_classes: - lines.extend(generate_markdown_class(get_class_info(cls))) - - # Document functions - lines.append("## Functions") - lines.append("") - - # Group functions by category - scene_funcs = [] - audio_funcs = [] - ui_funcs = [] - system_funcs = [] - - for name, func in functions: - if 'scene' in name.lower() or name in ['createScene', 'setScene']: - scene_funcs.append((name, func)) - elif any(x in name.lower() for x in ['sound', 'music', 'volume']): - audio_funcs.append((name, func)) - elif name in ['find', 'findAll']: - ui_funcs.append((name, func)) - else: - system_funcs.append((name, func)) - - # Scene Management - if scene_funcs: - lines.append("### Scene Management") - lines.append("") - for name, func in scene_funcs: - lines.extend(generate_markdown_function(get_function_info(func, name))) - - # Audio - if audio_funcs: - lines.append("### Audio") - lines.append("") - for name, func in audio_funcs: - lines.extend(generate_markdown_function(get_function_info(func, name))) - - # UI Utilities - if ui_funcs: - lines.append("### UI Utilities") - lines.append("") - for name, func in ui_funcs: - lines.extend(generate_markdown_function(get_function_info(func, name))) - - # System - if system_funcs: - lines.append("### System") - lines.append("") - for name, func in system_funcs: - lines.extend(generate_markdown_function(get_function_info(func, name))) - - # Automation module - if hasattr(mcrfpy, 'automation'): - lines.append("## Automation Module") - lines.append("") - lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.") - lines.append("") - - automation = mcrfpy.automation - auto_funcs = [] - - for name in sorted(dir(automation)): - if not name.startswith('_'): - obj = getattr(automation, name) - if callable(obj): - auto_funcs.append((name, obj)) - - for name, func in auto_funcs: - # Format as static method - func_info = get_function_info(func, name) - lines.append(f"### `automation.{func_info['signature']}`") - lines.append("") - if func_info['doc']: - lines.append(func_info['doc']) - lines.append("") - lines.append("---") - lines.append("") - - return '\n'.join(lines) - -def generate_html_docs(markdown_content: str) -> str: - """Convert markdown to HTML.""" - # Simple conversion - in production use a proper markdown parser - html = [''] - html.append('') - html.append('') - html.append('McRogueFace API Reference') - html.append('') - html.append('') - - # Very basic markdown to HTML conversion - lines = markdown_content.split('\n') - in_code_block = False - in_list = False - - for line in lines: - stripped = line.strip() - - if stripped.startswith('```'): - if in_code_block: - html.append('') - in_code_block = False - else: - lang = stripped[3:] or 'python' - html.append(f'
')
-                in_code_block = True
-            continue
-        
-        if in_code_block:
-            html.append(line)
-            continue
-        
-        # Headers
-        if stripped.startswith('#'):
-            level = len(stripped.split()[0])
-            text = stripped[level:].strip()
-            html.append(f'{text}')
-        # Lists
-        elif stripped.startswith('- '):
-            if not in_list:
-                html.append('
    ') - in_list = True - html.append(f'
  • {stripped[2:]}
  • ') - # Horizontal rule - elif stripped == '---': - if in_list: - html.append('
') - in_list = False - html.append('
') - # Emphasis - elif stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2: - html.append(f'{stripped[1:-1]}') - # Bold - elif stripped.startswith('**') and stripped.endswith('**'): - html.append(f'{stripped[2:-2]}') - # Regular paragraph - elif stripped: - if in_list: - html.append('') - in_list = False - # Convert inline code - text = stripped - if '`' in text: - import re - text = re.sub(r'`([^`]+)`', r'\1', text) - html.append(f'

{text}

') - else: - if in_list: - html.append('') - in_list = False - # Empty line - html.append('') - - if in_list: - html.append('') - if in_code_block: - html.append('
') - - html.append('') - return '\n'.join(html) - -def main(): - """Generate API documentation in multiple formats.""" - print("Generating McRogueFace API Documentation...") - - # Create docs directory - docs_dir = Path("docs") - docs_dir.mkdir(exist_ok=True) - - # Generate markdown documentation - print("- Generating Markdown documentation...") - markdown_content = generate_markdown_docs() - - # Write markdown - md_path = docs_dir / "API_REFERENCE.md" - with open(md_path, 'w') as f: - f.write(markdown_content) - print(f" ✓ Written to {md_path}") - - # Generate HTML - print("- Generating HTML documentation...") - html_content = generate_html_docs(markdown_content) - - # Write HTML - html_path = docs_dir / "api_reference.html" - with open(html_path, 'w') as f: - f.write(html_content) - print(f" ✓ Written to {html_path}") - - # Summary statistics - lines = markdown_content.split('\n') - class_count = markdown_content.count('### class') - func_count = len([l for l in lines if l.strip().startswith('### `') and 'class' not in l]) - - print("\nDocumentation Statistics:") - print(f"- Classes documented: {class_count}") - print(f"- Functions documented: {func_count}") - print(f"- Total lines: {len(lines)}") - print(f"- File size: {len(markdown_content):,} bytes") - - print("\nAPI documentation generated successfully!") - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tools/generate_api_docs_html.py b/tools/generate_api_docs_html.py deleted file mode 100644 index fe3cf08..0000000 --- a/tools/generate_api_docs_html.py +++ /dev/null @@ -1,1602 +0,0 @@ -#!/usr/bin/env python3 -"""Generate high-quality HTML API reference documentation for McRogueFace.""" - -import os -import sys -import datetime -import html -from pathlib import Path -import mcrfpy - -def escape_html(text: str) -> str: - """Escape HTML special characters.""" - return html.escape(text) if text else "" - -def format_docstring_as_html(docstring: str) -> str: - """Convert docstring to properly formatted HTML.""" - if not docstring: - return "" - - # Split and process lines - lines = docstring.strip().split('\n') - result = [] - in_code_block = False - - for line in lines: - # Convert \n to actual newlines - line = line.replace('\\n', '\n') - - # Handle code blocks - if line.strip().startswith('```'): - if in_code_block: - result.append('') - in_code_block = False - else: - result.append('
')
-                in_code_block = True
-            continue
-            
-        # Convert markdown-style code to HTML
-        if '`' in line and not in_code_block:
-            import re
-            line = re.sub(r'`([^`]+)`', r'\1', line)
-        
-        if in_code_block:
-            result.append(escape_html(line))
-        else:
-            result.append(escape_html(line) + '
') - - if in_code_block: - result.append('
') - - return '\n'.join(result) - -def get_class_details(cls): - """Get detailed information about a class.""" - info = { - 'name': cls.__name__, - 'doc': cls.__doc__ or "", - 'methods': {}, - 'properties': {}, - 'bases': [] - } - - # Get real base classes (excluding object) - for base in cls.__bases__: - if base.__name__ != 'object': - info['bases'].append(base.__name__) - - # Special handling for Entity which doesn't inherit from Drawable - if cls.__name__ == 'Entity' and 'Drawable' in info['bases']: - info['bases'].remove('Drawable') - - # Get methods and properties - for attr_name in dir(cls): - if attr_name.startswith('__') and attr_name != '__init__': - continue - - try: - attr = getattr(cls, attr_name) - - if isinstance(attr, property): - info['properties'][attr_name] = { - 'doc': (attr.fget.__doc__ if attr.fget else "") or "", - 'readonly': attr.fset is None - } - elif callable(attr) and not attr_name.startswith('_'): - info['methods'][attr_name] = attr.__doc__ or "" - except: - pass - - return info - -def generate_class_init_docs(class_name): - """Generate initialization documentation for specific classes.""" - init_docs = { - 'Entity': { - 'signature': 'Entity(x=0, y=0, sprite_id=0)', - 'description': 'Game entity that can be placed in a Grid.', - 'args': [ - ('x', 'int', 'Grid x coordinate. Default: 0'), - ('y', 'int', 'Grid y coordinate. Default: 0'), - ('sprite_id', 'int', 'Sprite index for rendering. Default: 0') - ], - 'example': '''entity = mcrfpy.Entity(5, 10, 42) -entity.move(1, 0) # Move right one tile''' - }, - 'Color': { - 'signature': 'Color(r=255, g=255, b=255, a=255)', - 'description': 'RGBA color representation.', - 'args': [ - ('r', 'int', 'Red component (0-255). Default: 255'), - ('g', 'int', 'Green component (0-255). Default: 255'), - ('b', 'int', 'Blue component (0-255). Default: 255'), - ('a', 'int', 'Alpha component (0-255). Default: 255') - ], - 'example': 'red = mcrfpy.Color(255, 0, 0)' - }, - 'Font': { - 'signature': 'Font(filename)', - 'description': 'Load a font from file.', - 'args': [ - ('filename', 'str', 'Path to font file (TTF/OTF)') - ] - }, - 'Texture': { - 'signature': 'Texture(filename)', - 'description': 'Load a texture from file.', - 'args': [ - ('filename', 'str', 'Path to image file (PNG/JPG/BMP)') - ] - }, - 'Vector': { - 'signature': 'Vector(x=0.0, y=0.0)', - 'description': '2D vector for positions and directions.', - 'args': [ - ('x', 'float', 'X component. Default: 0.0'), - ('y', 'float', 'Y component. Default: 0.0') - ] - }, - 'Animation': { - 'signature': 'Animation(property_name, start_value, end_value, duration, transition="linear", loop=False)', - 'description': 'Animate UI element properties over time.', - 'args': [ - ('property_name', 'str', 'Property to animate (e.g., "x", "y", "scale")'), - ('start_value', 'float', 'Starting value'), - ('end_value', 'float', 'Ending value'), - ('duration', 'float', 'Duration in seconds'), - ('transition', 'str', 'Easing function. Default: "linear"'), - ('loop', 'bool', 'Whether to loop. Default: False') - ], - 'properties': ['current_value', 'elapsed_time', 'is_running', 'is_finished'] - }, - 'GridPoint': { - 'description': 'Represents a single tile in a Grid.', - 'properties': ['x', 'y', 'texture_index', 'solid', 'transparent', 'color'] - }, - 'GridPointState': { - 'description': 'State information for a GridPoint.', - 'properties': ['visible', 'discovered', 'custom_flags'] - }, - 'Timer': { - 'signature': 'Timer(name, callback, interval_ms)', - 'description': 'Create a recurring timer.', - 'args': [ - ('name', 'str', 'Unique timer identifier'), - ('callback', 'callable', 'Function to call'), - ('interval_ms', 'int', 'Interval in milliseconds') - ] - } - } - - return init_docs.get(class_name, {}) - -def generate_method_docs(method_name, class_name): - """Generate documentation for specific methods.""" - method_docs = { - # Base Drawable methods (inherited by all UI elements) - 'Drawable': { - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of this drawable element.', - 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', - 'note': 'The bounds are in screen coordinates and account for current position and size.' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the element by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ], - 'note': 'This modifies the x and y position properties by the given amounts.' - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Resize the element to new dimensions.', - 'args': [ - ('width', 'float', 'New width in pixels'), - ('height', 'float', 'New height in pixels') - ], - 'note': 'Behavior varies by element type. Some elements may ignore or constrain dimensions.' - } - }, - - # Caption-specific methods - 'Caption': { - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of the text.', - 'returns': 'tuple: (x, y, width, height) based on text content and font size', - 'note': 'Bounds are automatically calculated from the rendered text dimensions.' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the caption by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ] - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Set text wrapping bounds (limited support).', - 'args': [ - ('width', 'float', 'Maximum width for text wrapping'), - ('height', 'float', 'Currently unused') - ], - 'note': 'Full text wrapping is not yet implemented. This prepares for future multiline support.' - } - }, - - # Entity-specific methods - 'Entity': { - 'at': { - 'signature': 'at(x, y)', - 'description': 'Get the GridPointState at the specified grid coordinates relative to this entity.', - 'args': [ - ('x', 'int', 'Grid x offset from entity position'), - ('y', 'int', 'Grid y offset from entity position') - ], - 'returns': 'GridPointState: State of the grid point at the specified position', - 'note': 'Requires entity to be associated with a grid. Raises ValueError if not.' - }, - 'die': { - 'signature': 'die()', - 'description': 'Remove this entity from its parent grid.', - 'returns': 'None', - 'note': 'The entity object remains valid but is no longer rendered or updated.' - }, - 'index': { - 'signature': 'index()', - 'description': 'Get the index of this entity in its grid\'s entity collection.', - 'returns': 'int: Zero-based index in the parent grid\'s entity list', - 'note': 'Raises RuntimeError if not associated with a grid, ValueError if not found.' - }, - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of the entity\'s sprite.', - 'returns': 'tuple: (x, y, width, height) of the sprite bounds', - 'note': 'Delegates to the internal sprite\'s get_bounds method.' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the entity by a relative offset in pixels.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ], - 'note': 'Updates both sprite position and entity grid position.' - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Entities do not support direct resizing.', - 'args': [ - ('width', 'float', 'Ignored'), - ('height', 'float', 'Ignored') - ], - 'note': 'This method exists for interface compatibility but has no effect.' - } - }, - - # Frame-specific methods - 'Frame': { - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of the frame.', - 'returns': 'tuple: (x, y, width, height) representing the frame bounds' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the frame and all its children by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ], - 'note': 'Child elements maintain their relative positions within the frame.' - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Resize the frame to new dimensions.', - 'args': [ - ('width', 'float', 'New width in pixels'), - ('height', 'float', 'New height in pixels') - ], - 'note': 'Does not automatically resize children. Set clip_children=True to clip overflow.' - } - }, - - # Grid-specific methods - 'Grid': { - 'at': { - 'signature': 'at(x, y) or at((x, y))', - 'description': 'Get the GridPoint at the specified grid coordinates.', - 'args': [ - ('x', 'int', 'Grid x coordinate (0-based)'), - ('y', 'int', 'Grid y coordinate (0-based)') - ], - 'returns': 'GridPoint: The grid point at (x, y)', - 'note': 'Raises IndexError if coordinates are out of range. Accepts either two arguments or a tuple.', - 'example': 'point = grid.at(5, 3) # or grid.at((5, 3))' - }, - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of the entire grid.', - 'returns': 'tuple: (x, y, width, height) of the grid\'s display area' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the grid display by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ], - 'note': 'Moves the entire grid viewport. Use center property to pan within the grid.' - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Resize the grid\'s display viewport.', - 'args': [ - ('width', 'float', 'New viewport width in pixels'), - ('height', 'float', 'New viewport height in pixels') - ], - 'note': 'Changes the visible area, not the grid dimensions. Use zoom to scale content.' - } - }, - - # Sprite-specific methods - 'Sprite': { - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of the sprite.', - 'returns': 'tuple: (x, y, width, height) based on texture size and scale', - 'note': 'Bounds account for current scale. Returns (x, y, 0, 0) if no texture.' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the sprite by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ] - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Resize the sprite by adjusting its scale.', - 'args': [ - ('width', 'float', 'Target width in pixels'), - ('height', 'float', 'Target height in pixels') - ], - 'note': 'Calculates and applies uniform scale to best fit the target dimensions.' - } - }, - - 'Animation': { - 'get_current_value': { - 'signature': 'get_current_value()', - 'description': 'Get the current interpolated value.', - 'returns': 'float: Current animation value' - }, - 'start': { - 'signature': 'start(target)', - 'description': 'Start the animation on a target UI element.', - 'args': [('target', 'UIDrawable', 'The element to animate')] - } - }, - - # Collection methods (shared by EntityCollection and UICollection) - 'EntityCollection': { - 'append': { - 'signature': 'append(entity)', - 'description': 'Add an entity to the end of the collection.', - 'args': [ - ('entity', 'Entity', 'The entity to add') - ] - }, - 'remove': { - 'signature': 'remove(entity)', - 'description': 'Remove the first occurrence of an entity from the collection.', - 'args': [ - ('entity', 'Entity', 'The entity to remove') - ], - 'note': 'Raises ValueError if entity is not found.' - }, - 'extend': { - 'signature': 'extend(iterable)', - 'description': 'Add multiple entities from an iterable.', - 'args': [ - ('iterable', 'iterable', 'An iterable of Entity objects') - ] - }, - 'count': { - 'signature': 'count(entity)', - 'description': 'Count occurrences of an entity in the collection.', - 'args': [ - ('entity', 'Entity', 'The entity to count') - ], - 'returns': 'int: Number of times the entity appears' - }, - 'index': { - 'signature': 'index(entity)', - 'description': 'Find the index of the first occurrence of an entity.', - 'args': [ - ('entity', 'Entity', 'The entity to find') - ], - 'returns': 'int: Zero-based index of the entity', - 'note': 'Raises ValueError if entity is not found.' - } - }, - - 'UICollection': { - 'append': { - 'signature': 'append(drawable)', - 'description': 'Add a drawable element to the end of the collection.', - 'args': [ - ('drawable', 'Drawable', 'Any UI element (Frame, Caption, Sprite, Grid)') - ] - }, - 'remove': { - 'signature': 'remove(drawable)', - 'description': 'Remove the first occurrence of a drawable from the collection.', - 'args': [ - ('drawable', 'Drawable', 'The drawable to remove') - ], - 'note': 'Raises ValueError if drawable is not found.' - }, - 'extend': { - 'signature': 'extend(iterable)', - 'description': 'Add multiple drawables from an iterable.', - 'args': [ - ('iterable', 'iterable', 'An iterable of Drawable objects') - ] - }, - 'count': { - 'signature': 'count(drawable)', - 'description': 'Count occurrences of a drawable in the collection.', - 'args': [ - ('drawable', 'Drawable', 'The drawable to count') - ], - 'returns': 'int: Number of times the drawable appears' - }, - 'index': { - 'signature': 'index(drawable)', - 'description': 'Find the index of the first occurrence of a drawable.', - 'args': [ - ('drawable', 'Drawable', 'The drawable to find') - ], - 'returns': 'int: Zero-based index of the drawable', - 'note': 'Raises ValueError if drawable is not found.' - } - } - } - - return method_docs.get(class_name, {}).get(method_name, {}) - -def generate_function_docs(): - """Generate documentation for all mcrfpy module functions.""" - function_docs = { - # Scene Management - 'createScene': { - 'signature': 'createScene(name: str) -> None', - 'description': 'Create a new empty scene.', - 'args': [ - ('name', 'str', 'Unique name for the new scene') - ], - 'returns': 'None', - 'exceptions': [ - ('ValueError', 'If a scene with this name already exists') - ], - 'note': 'The scene is created but not made active. Use setScene() to switch to it.', - 'example': '''mcrfpy.createScene("game") -mcrfpy.createScene("menu") -mcrfpy.setScene("game")''' - }, - - 'setScene': { - 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', - 'description': 'Switch to a different scene with optional transition effect.', - 'args': [ - ('scene', 'str', 'Name of the scene to switch to'), - ('transition', 'str', 'Transition type ("fade", "slide_left", "slide_right", "slide_up", "slide_down"). Default: None'), - ('duration', 'float', 'Transition duration in seconds. Default: 0.0 for instant') - ], - 'returns': 'None', - 'exceptions': [ - ('KeyError', 'If the scene doesn\'t exist'), - ('ValueError', 'If the transition type is invalid') - ], - 'example': '''mcrfpy.setScene("menu") -mcrfpy.setScene("game", "fade", 0.5) -mcrfpy.setScene("credits", "slide_left", 1.0)''' - }, - - 'currentScene': { - 'signature': 'currentScene() -> str', - 'description': 'Get the name of the currently active scene.', - 'args': [], - 'returns': 'str: Name of the current scene', - 'example': '''scene = mcrfpy.currentScene() -print(f"Currently in scene: {scene}")''' - }, - - 'sceneUI': { - 'signature': 'sceneUI(scene: str = None) -> list', - 'description': 'Get all UI elements for a scene.', - 'args': [ - ('scene', 'str', 'Scene name. If None, uses current scene. Default: None') - ], - 'returns': 'list: All UI elements (Frame, Caption, Sprite, Grid) in the scene', - 'exceptions': [ - ('KeyError', 'If the specified scene doesn\'t exist') - ], - 'example': '''# Get UI for current scene -ui_elements = mcrfpy.sceneUI() - -# Get UI for specific scene -menu_ui = mcrfpy.sceneUI("menu") -for element in menu_ui: - print(f"{element.name}: {type(element).__name__}")''' - }, - - 'keypressScene': { - 'signature': 'keypressScene(handler: callable) -> None', - 'description': 'Set the keyboard event handler for the current scene.', - 'args': [ - ('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)') - ], - 'returns': 'None', - 'note': 'The handler is called for every key press and release event. Key names are single characters (e.g., "A", "1") or special keys (e.g., "Space", "Enter", "Escape").', - 'example': '''def on_key(key, pressed): - if pressed: - if key == "Space": - player.jump() - elif key == "Escape": - mcrfpy.setScene("pause_menu") - else: - # Handle key release - if key in ["A", "D"]: - player.stop_moving() - -mcrfpy.keypressScene(on_key)''' - }, - - # Audio Functions - 'createSoundBuffer': { - 'signature': 'createSoundBuffer(filename: str) -> int', - 'description': 'Load a sound effect from a file and return its buffer ID.', - 'args': [ - ('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)') - ], - 'returns': 'int: Buffer ID for use with playSound()', - 'exceptions': [ - ('RuntimeError', 'If the file cannot be loaded') - ], - 'note': 'Sound buffers are stored in memory for fast playback. Load sound effects once and reuse the buffer ID.', - 'example': '''# Load sound effects -jump_sound = mcrfpy.createSoundBuffer("assets/sounds/jump.wav") -coin_sound = mcrfpy.createSoundBuffer("assets/sounds/coin.ogg") - -# Play later -mcrfpy.playSound(jump_sound)''' - }, - - 'loadMusic': { - 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', - 'description': 'Load and immediately play background music from a file.', - 'args': [ - ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), - ('loop', 'bool', 'Whether to loop the music. Default: True') - ], - 'returns': 'None', - 'note': 'Only one music track can play at a time. Loading new music stops the current track.', - 'example': '''# Play looping background music -mcrfpy.loadMusic("assets/music/theme.ogg") - -# Play music once without looping -mcrfpy.loadMusic("assets/music/victory.ogg", loop=False)''' - }, - - 'playSound': { - 'signature': 'playSound(buffer_id: int) -> None', - 'description': 'Play a sound effect using a previously loaded buffer.', - 'args': [ - ('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()') - ], - 'returns': 'None', - 'exceptions': [ - ('RuntimeError', 'If the buffer ID is invalid') - ], - 'note': 'Multiple sounds can play simultaneously. Each call creates a new sound instance.', - 'example': '''# Load once -explosion_sound = mcrfpy.createSoundBuffer("explosion.wav") - -# Play multiple times -for enemy in destroyed_enemies: - mcrfpy.playSound(explosion_sound)''' - }, - - 'getMusicVolume': { - 'signature': 'getMusicVolume() -> int', - 'description': 'Get the current music volume level.', - 'args': [], - 'returns': 'int: Current volume (0-100)', - 'example': '''volume = mcrfpy.getMusicVolume() -print(f"Music volume: {volume}%")''' - }, - - 'getSoundVolume': { - 'signature': 'getSoundVolume() -> int', - 'description': 'Get the current sound effects volume level.', - 'args': [], - 'returns': 'int: Current volume (0-100)', - 'example': '''volume = mcrfpy.getSoundVolume() -print(f"Sound effects volume: {volume}%")''' - }, - - 'setMusicVolume': { - 'signature': 'setMusicVolume(volume: int) -> None', - 'description': 'Set the global music volume.', - 'args': [ - ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)') - ], - 'returns': 'None', - 'example': '''# Mute music -mcrfpy.setMusicVolume(0) - -# Half volume -mcrfpy.setMusicVolume(50) - -# Full volume -mcrfpy.setMusicVolume(100)''' - }, - - 'setSoundVolume': { - 'signature': 'setSoundVolume(volume: int) -> None', - 'description': 'Set the global sound effects volume.', - 'args': [ - ('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)') - ], - 'returns': 'None', - 'example': '''# Audio settings from options menu -mcrfpy.setSoundVolume(sound_slider.value) -mcrfpy.setMusicVolume(music_slider.value)''' - }, - - # UI Utilities - 'find': { - 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', - 'description': 'Find the first UI element with the specified name.', - 'args': [ - ('name', 'str', 'Exact name to search for'), - ('scene', 'str', 'Scene to search in. Default: current scene') - ], - 'returns': 'Frame, Caption, Sprite, Grid, or Entity if found; None otherwise', - 'note': 'Searches scene UI elements and entities within grids. Returns the first match found.', - 'example': '''# Find in current scene -player = mcrfpy.find("player") -if player: - player.x = 100 - -# Find in specific scene -menu_button = mcrfpy.find("start_button", "main_menu")''' - }, - - 'findAll': { - 'signature': 'findAll(pattern: str, scene: str = None) -> list', - 'description': 'Find all UI elements matching a name pattern.', - 'args': [ - ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), - ('scene', 'str', 'Scene to search in. Default: current scene') - ], - 'returns': 'list: All matching UI elements and entities', - 'note': 'Supports wildcard patterns for flexible searching.', - 'example': '''# Find all enemies -enemies = mcrfpy.findAll("enemy*") -for enemy in enemies: - enemy.sprite_id = 0 # Reset sprite - -# Find all buttons -buttons = mcrfpy.findAll("*_button") -for btn in buttons: - btn.visible = True - -# Find exact matches -health_bars = mcrfpy.findAll("health_bar") # No wildcards = exact match''' - }, - - # System Functions - 'exit': { - 'signature': 'exit() -> None', - 'description': 'Cleanly shut down the game engine and exit the application.', - 'args': [], - 'returns': 'None', - 'note': 'This immediately closes the window and terminates the program. Ensure any necessary cleanup is done before calling.', - 'example': '''def quit_game(): - # Save game state - save_progress() - - # Exit - mcrfpy.exit()''' - }, - - 'getMetrics': { - 'signature': 'getMetrics() -> dict', - 'description': 'Get current performance metrics.', - 'args': [], - 'returns': '''dict: Performance data with keys: - - frame_time: Last frame duration in seconds - - avg_frame_time: Average frame time - - fps: Frames per second - - draw_calls: Number of draw calls - - ui_elements: Total UI element count - - visible_elements: Visible element count - - current_frame: Frame counter - - runtime: Total runtime in seconds''', - 'example': '''metrics = mcrfpy.getMetrics() -print(f"FPS: {metrics['fps']}") -print(f"Frame time: {metrics['frame_time']*1000:.1f}ms") -print(f"Draw calls: {metrics['draw_calls']}") -print(f"Runtime: {metrics['runtime']:.1f}s") - -# Performance monitoring -if metrics['fps'] < 30: - print("Performance warning: FPS below 30")''' - }, - - 'setTimer': { - 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', - 'description': 'Create or update a recurring timer.', - 'args': [ - ('name', 'str', 'Unique identifier for the timer'), - ('handler', 'callable', 'Function called with (runtime: float) parameter'), - ('interval', 'int', 'Time between calls in milliseconds') - ], - 'returns': 'None', - 'note': 'If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.', - 'example': '''# Simple repeating timer -def spawn_enemy(runtime): - enemy = mcrfpy.Entity() - enemy.x = random.randint(0, 800) - grid.entities.append(enemy) - -mcrfpy.setTimer("enemy_spawner", spawn_enemy, 2000) # Every 2 seconds - -# Timer with runtime check -def update_timer(runtime): - time_left = 60 - runtime - timer_text.text = f"Time: {int(time_left)}" - if time_left <= 0: - mcrfpy.delTimer("game_timer") - game_over() - -mcrfpy.setTimer("game_timer", update_timer, 100) # Update every 100ms''' - }, - - 'delTimer': { - 'signature': 'delTimer(name: str) -> None', - 'description': 'Stop and remove a timer.', - 'args': [ - ('name', 'str', 'Timer identifier to remove') - ], - 'returns': 'None', - 'note': 'No error is raised if the timer doesn\'t exist.', - 'example': '''# Stop spawning enemies -mcrfpy.delTimer("enemy_spawner") - -# Clean up all game timers -for timer_name in ["enemy_spawner", "powerup_timer", "score_updater"]: - mcrfpy.delTimer(timer_name)''' - }, - - 'setScale': { - 'signature': 'setScale(multiplier: float) -> None', - 'description': 'Scale the game window size.', - 'args': [ - ('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)') - ], - 'returns': 'None', - 'exceptions': [ - ('ValueError', 'If multiplier is not between 0.2 and 4.0') - ], - 'note': 'The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.', - 'example': '''# Double the window size -mcrfpy.setScale(2.0) - -# Half size window -mcrfpy.setScale(0.5) - -# Better approach (not deprecated): -mcrfpy.Window.resolution = (1920, 1080)''' - } - } - - return function_docs - -def generate_collection_docs(class_name): - """Generate documentation for collection classes.""" - collection_docs = { - 'EntityCollection': { - 'description': 'Container for Entity objects in a Grid. Supports iteration and indexing.', - 'methods': { - 'append': 'Add an entity to the collection', - 'remove': 'Remove an entity from the collection', - 'extend': 'Add multiple entities from an iterable', - 'count': 'Count occurrences of an entity', - 'index': 'Find the index of an entity' - } - }, - 'UICollection': { - 'description': 'Container for UI drawable elements. Supports iteration and indexing.', - 'methods': { - 'append': 'Add a UI element to the collection', - 'remove': 'Remove a UI element from the collection', - 'extend': 'Add multiple UI elements from an iterable', - 'count': 'Count occurrences of a UI element', - 'index': 'Find the index of a UI element' - } - }, - 'UICollectionIter': { - 'description': 'Iterator for UICollection. Automatically created when iterating over a UICollection.' - }, - 'UIEntityCollectionIter': { - 'description': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.' - } - } - - return collection_docs.get(class_name, {}) - -def format_class_html(cls_info, class_name): - """Format a class as HTML with proper structure.""" - html_parts = [] - - # Class header - html_parts.append(f'
') - html_parts.append(f'

class {class_name}

') - - # Inheritance - if cls_info['bases']: - html_parts.append(f'

Inherits from: {", ".join(cls_info["bases"])}

') - - # Get additional documentation - init_info = generate_class_init_docs(class_name) - collection_info = generate_collection_docs(class_name) - - # Constructor signature for classes with __init__ - if init_info.get('signature'): - html_parts.append('
') - html_parts.append('
')
-        html_parts.append(escape_html(init_info['signature']))
-        html_parts.append('
') - html_parts.append('
') - - # Description - description = "" - if collection_info.get('description'): - description = collection_info['description'] - elif init_info.get('description'): - description = init_info['description'] - elif cls_info['doc']: - # Parse description from docstring - doc_lines = cls_info['doc'].strip().split('\n') - # Skip constructor line if present - start_idx = 1 if doc_lines and '(' in doc_lines[0] else 0 - if start_idx < len(doc_lines): - description = '\n'.join(doc_lines[start_idx:]).strip() - - if description: - html_parts.append('
') - html_parts.append(f'

{format_docstring_as_html(description)}

') - html_parts.append('
') - - # Constructor arguments - if init_info.get('args'): - html_parts.append('
') - html_parts.append('

Arguments:

') - html_parts.append('
') - for arg_name, arg_type, arg_desc in init_info['args']: - html_parts.append(f'
{arg_name} ({arg_type})
') - html_parts.append(f'
{escape_html(arg_desc)}
') - html_parts.append('
') - html_parts.append('
') - - # Properties/Attributes - props = cls_info.get('properties', {}) - if props or init_info.get('properties'): - html_parts.append('
') - html_parts.append('

Attributes:

') - html_parts.append('
') - - # Add documented properties from init_info - if init_info.get('properties'): - for prop_name in init_info['properties']: - html_parts.append(f'
{prop_name}
') - html_parts.append(f'
Property of {class_name}
') - - # Add actual properties - for prop_name, prop_info in props.items(): - readonly = ' (read-only)' if prop_info.get('readonly') else '' - html_parts.append(f'
{prop_name}{readonly}
') - if prop_info.get('doc'): - html_parts.append(f'
{escape_html(prop_info["doc"])}
') - - html_parts.append('
') - html_parts.append('
') - - # Methods - methods = cls_info.get('methods', {}) - collection_methods = collection_info.get('methods', {}) - - if methods or collection_methods: - html_parts.append('
') - html_parts.append('

Methods:

') - - for method_name, method_doc in {**collection_methods, **methods}.items(): - if method_name == '__init__': - continue - - html_parts.append('
') - - # Get specific method documentation - method_info = generate_method_docs(method_name, class_name) - - if method_info: - # Use detailed documentation - html_parts.append(f'
{method_info["signature"]}
') - html_parts.append(f'

{escape_html(method_info["description"])}

') - - if method_info.get('args'): - html_parts.append('

Arguments:

') - html_parts.append('
    ') - for arg in method_info['args']: - if len(arg) == 3: - html_parts.append(f'
  • {arg[0]} ({arg[1]}): {arg[2]}
  • ') - else: - html_parts.append(f'
  • {arg[0]} ({arg[1]})
  • ') - html_parts.append('
') - - if method_info.get('returns'): - html_parts.append(f'

Returns: {escape_html(method_info["returns"])}

') - - if method_info.get('note'): - html_parts.append(f'

Note: {escape_html(method_info["note"])}

') - else: - # Use docstring - html_parts.append(f'
{method_name}(...)
') - if isinstance(method_doc, str) and method_doc: - html_parts.append(f'

{escape_html(method_doc)}

') - - html_parts.append('
') - - html_parts.append('
') - - # Example - if init_info.get('example'): - html_parts.append('
') - html_parts.append('

Example:

') - html_parts.append('
')
-        html_parts.append(escape_html(init_info['example']))
-        html_parts.append('
') - html_parts.append('
') - - html_parts.append('
') - html_parts.append('
') - - return '\n'.join(html_parts) - -def generate_html_documentation(): - """Generate complete HTML API documentation.""" - html_parts = [] - - # HTML header - html_parts.append(''' - - - - - McRogueFace API Reference - - - -
-''') - - # Title and timestamp - html_parts.append('

McRogueFace API Reference

') - html_parts.append(f'

Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

') - - # Overview - if mcrfpy.__doc__: - html_parts.append('
') - html_parts.append('

Overview

') - # Process the docstring properly - doc_lines = mcrfpy.__doc__.strip().split('\\n') - for line in doc_lines: - if line.strip().startswith('Example:'): - html_parts.append('

Example:

') - html_parts.append('
')
-            elif line.strip() and not line.startswith(' '):
-                html_parts.append(f'

{escape_html(line)}

') - elif line.strip(): - # Code line - html_parts.append(escape_html(line)) - html_parts.append('
') - html_parts.append('
') - - # Table of Contents - html_parts.append('
') - html_parts.append('

Table of Contents

') - html_parts.append('') - html_parts.append('
') - - # Collect all components - classes = {} - functions = {} - - for name in sorted(dir(mcrfpy)): - if name.startswith('_'): - continue - - obj = getattr(mcrfpy, name) - - if isinstance(obj, type): - classes[name] = obj - elif callable(obj) and not isinstance(obj, type): - # Include built-in functions and other callables (but not classes) - functions[name] = obj - - - # Classes section - html_parts.append('

Classes

') - - # Group classes - ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity'] - collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter'] - system_classes = ['Color', 'Vector', 'Texture', 'Font'] - other_classes = [name for name in classes if name not in ui_classes + collection_classes + system_classes] - - # UI Components - html_parts.append('

UI Components

') - for class_name in ui_classes: - if class_name in classes: - cls_info = get_class_details(classes[class_name]) - html_parts.append(format_class_html(cls_info, class_name)) - - # Collections - html_parts.append('

Collections

') - for class_name in collection_classes: - if class_name in classes: - cls_info = get_class_details(classes[class_name]) - html_parts.append(format_class_html(cls_info, class_name)) - - # System Types - html_parts.append('

System Types

') - for class_name in system_classes: - if class_name in classes: - cls_info = get_class_details(classes[class_name]) - html_parts.append(format_class_html(cls_info, class_name)) - - # Other Classes - html_parts.append('

Other Classes

') - for class_name in other_classes: - if class_name in classes: - cls_info = get_class_details(classes[class_name]) - html_parts.append(format_class_html(cls_info, class_name)) - - # Functions section - html_parts.append('

Functions

') - - # Group functions by category - scene_funcs = ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'] - audio_funcs = ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', - 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'] - ui_funcs = ['find', 'findAll'] - system_funcs = ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] - - # Scene Management - html_parts.append('

Scene Management

') - for func_name in scene_funcs: - if func_name in functions: - html_parts.append(format_function_html(func_name, functions[func_name])) - - # Audio - html_parts.append('

Audio

') - for func_name in audio_funcs: - if func_name in functions: - html_parts.append(format_function_html(func_name, functions[func_name])) - - # UI Utilities - html_parts.append('

UI Utilities

') - for func_name in ui_funcs: - if func_name in functions: - html_parts.append(format_function_html(func_name, functions[func_name])) - - # System - html_parts.append('

System

') - for func_name in system_funcs: - if func_name in functions: - html_parts.append(format_function_html(func_name, functions[func_name])) - - # Automation Module - if hasattr(mcrfpy, 'automation'): - html_parts.append('
') - html_parts.append('

Automation Module

') - html_parts.append('

The mcrfpy.automation module provides testing and automation capabilities for simulating user input and capturing screenshots.

') - - automation = mcrfpy.automation - auto_funcs = [] - - for name in sorted(dir(automation)): - if not name.startswith('_'): - obj = getattr(automation, name) - if callable(obj): - auto_funcs.append((name, obj)) - - for name, func in auto_funcs: - html_parts.append('
') - html_parts.append(f'

automation.{name}

') - if func.__doc__: - # Extract just the description, not the repeated signature - doc_lines = func.__doc__.strip().split(' - ') - if len(doc_lines) > 1: - description = doc_lines[1] - else: - description = func.__doc__.strip() - html_parts.append(f'

{escape_html(description)}

') - html_parts.append('
') - - html_parts.append('
') - - # Close HTML - html_parts.append(''' -
- -''') - - return '\n'.join(html_parts) - -def format_function_html(func_name, func): - """Format a function as HTML using enhanced documentation.""" - html_parts = [] - - html_parts.append('
') - - # Get enhanced documentation - func_docs = generate_function_docs() - - if func_name in func_docs: - doc_info = func_docs[func_name] - - # Signature - signature = doc_info.get('signature', f'{func_name}(...)') - html_parts.append(f'

{escape_html(signature)}

') - - # Description - if 'description' in doc_info: - html_parts.append(f'

{escape_html(doc_info["description"])}

') - - # Arguments - if 'args' in doc_info and doc_info['args']: - html_parts.append('
') - html_parts.append('
Arguments:
') - html_parts.append('
') - for arg_name, arg_type, arg_desc in doc_info['args']: - html_parts.append(f'
{escape_html(arg_name)} : {escape_html(arg_type)}
') - html_parts.append(f'
{escape_html(arg_desc)}
') - html_parts.append('
') - html_parts.append('
') - - # Returns - if 'returns' in doc_info and doc_info['returns']: - html_parts.append('
') - html_parts.append('
Returns:
') - html_parts.append(f'

{escape_html(doc_info["returns"])}

') - html_parts.append('
') - - # Exceptions - if 'exceptions' in doc_info and doc_info['exceptions']: - html_parts.append('
') - html_parts.append('
Raises:
') - html_parts.append('
') - for exc_type, exc_desc in doc_info['exceptions']: - html_parts.append(f'
{escape_html(exc_type)}
') - html_parts.append(f'
{escape_html(exc_desc)}
') - html_parts.append('
') - html_parts.append('
') - - # Note - if 'note' in doc_info: - html_parts.append('
') - html_parts.append(f'

Note: {escape_html(doc_info["note"])}

') - html_parts.append('
') - - # Example - if 'example' in doc_info: - html_parts.append('
') - html_parts.append('
Example:
') - html_parts.append('
')
-            html_parts.append(escape_html(doc_info['example']))
-            html_parts.append('
') - html_parts.append('
') - else: - # Fallback to parsing docstring if not in enhanced docs - doc = func.__doc__ or "" - lines = doc.strip().split('\n') if doc else [] - - # Extract signature - signature = func_name + '(...)' - if lines and '(' in lines[0]: - signature = lines[0].strip() - - html_parts.append(f'

{escape_html(signature)}

') - - # Process rest of docstring - if len(lines) > 1: - in_section = None - for line in lines[1:]: - stripped = line.strip() - - if stripped in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']: - in_section = stripped[:-1] - html_parts.append(f'

{in_section}:

') - elif in_section == 'Example': - if not stripped: - continue - if stripped.startswith('>>>') or (len(lines) > lines.index(line) + 1 and - lines[lines.index(line) + 1].strip().startswith('>>>')): - html_parts.append('
')
-                        html_parts.append(escape_html(stripped))
-                        # Get rest of example
-                        idx = lines.index(line) + 1
-                        while idx < len(lines) and lines[idx].strip():
-                            html_parts.append(escape_html(lines[idx]))
-                            idx += 1
-                        html_parts.append('
') - break - elif in_section and stripped: - if in_section == 'Args': - # Format arguments nicely - if ':' in stripped: - param, desc = stripped.split(':', 1) - html_parts.append(f'

{escape_html(param.strip())}: {escape_html(desc.strip())}

') - else: - html_parts.append(f'

{escape_html(stripped)}

') - else: - html_parts.append(f'

{escape_html(stripped)}

') - elif stripped and not in_section: - html_parts.append(f'

{escape_html(stripped)}

') - - html_parts.append('
') - html_parts.append('
') - - return '\n'.join(html_parts) - -def main(): - """Generate improved HTML API documentation.""" - print("Generating improved HTML API documentation...") - - # Generate HTML - html_content = generate_html_documentation() - - # Write to file - output_path = Path("docs/api_reference_improved.html") - output_path.parent.mkdir(exist_ok=True) - - with open(output_path, 'w', encoding='utf-8') as f: - f.write(html_content) - - print(f"✓ Generated {output_path}") - print(f" File size: {len(html_content):,} bytes") - - # Also generate a test to verify the HTML - test_content = '''#!/usr/bin/env python3 -"""Test the improved HTML API documentation.""" - -import os -import sys -from pathlib import Path - -def test_html_quality(): - """Test that the HTML documentation meets quality standards.""" - html_path = Path("docs/api_reference_improved.html") - - if not html_path.exists(): - print("ERROR: HTML documentation not found") - return False - - with open(html_path, 'r') as f: - content = f.read() - - # Check for common issues - issues = [] - - # Check that \\n is not present literally - if '\\\\n' in content: - issues.append("Found literal \\\\n in HTML content") - - # Check that markdown links are converted - if '[' in content and '](#' in content: - issues.append("Found unconverted markdown links") - - # Check for proper HTML structure - if '

Args:

' in content: - issues.append("Args: should not be an H4 heading") - - if '

Attributes:

' not in content: - issues.append("Missing proper Attributes: headings") - - # Check for duplicate method descriptions - if content.count('Get bounding box as (x, y, width, height)') > 20: - issues.append("Too many duplicate method descriptions") - - # Check specific improvements - if 'Entity' in content and 'Inherits from: Drawable' in content: - issues.append("Entity incorrectly shown as inheriting from Drawable") - - if not issues: - print("✓ HTML documentation passes all quality checks") - return True - else: - print("Issues found:") - for issue in issues: - print(f" - {issue}") - return False - -if __name__ == '__main__': - if test_html_quality(): - print("PASS") - sys.exit(0) - else: - print("FAIL") - sys.exit(1) -''' - - test_path = Path("tests/test_html_quality.py") - with open(test_path, 'w') as f: - f.write(test_content) - - print(f"✓ Generated test at {test_path}") - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tools/generate_api_docs_simple.py b/tools/generate_api_docs_simple.py deleted file mode 100644 index 2bb405f..0000000 --- a/tools/generate_api_docs_simple.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -"""Generate API reference documentation for McRogueFace - Simple version.""" - -import os -import sys -import datetime -from pathlib import Path - -import mcrfpy - -def generate_markdown_docs(): - """Generate markdown API documentation.""" - lines = [] - - # Header - lines.append("# McRogueFace API Reference") - lines.append("") - lines.append("*Generated on {}*".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) - lines.append("") - - # Module description - if mcrfpy.__doc__: - lines.append("## Overview") - lines.append("") - lines.extend(mcrfpy.__doc__.strip().split('\n')) - lines.append("") - - # Collect all components - classes = [] - functions = [] - - for name in sorted(dir(mcrfpy)): - if name.startswith('_'): - continue - - obj = getattr(mcrfpy, name) - - if isinstance(obj, type): - classes.append((name, obj)) - elif callable(obj): - functions.append((name, obj)) - - # Document classes - lines.append("## Classes") - lines.append("") - - for name, cls in classes: - lines.append("### class {}".format(name)) - if cls.__doc__: - doc_lines = cls.__doc__.strip().split('\n') - for line in doc_lines[:5]: # First 5 lines - lines.append(line) - lines.append("") - lines.append("---") - lines.append("") - - # Document functions - lines.append("## Functions") - lines.append("") - - for name, func in functions: - lines.append("### {}".format(name)) - if func.__doc__: - doc_lines = func.__doc__.strip().split('\n') - for line in doc_lines[:5]: # First 5 lines - lines.append(line) - lines.append("") - lines.append("---") - lines.append("") - - # Automation module - if hasattr(mcrfpy, 'automation'): - lines.append("## Automation Module") - lines.append("") - - automation = mcrfpy.automation - for name in sorted(dir(automation)): - if not name.startswith('_'): - obj = getattr(automation, name) - if callable(obj): - lines.append("### automation.{}".format(name)) - if obj.__doc__: - lines.append(obj.__doc__.strip().split('\n')[0]) - lines.append("") - - return '\n'.join(lines) - -def main(): - """Generate API documentation.""" - print("Generating McRogueFace API Documentation...") - - # Create docs directory - docs_dir = Path("docs") - docs_dir.mkdir(exist_ok=True) - - # Generate markdown - markdown_content = generate_markdown_docs() - - # Write markdown - md_path = docs_dir / "API_REFERENCE.md" - with open(md_path, 'w') as f: - f.write(markdown_content) - print("Written to {}".format(md_path)) - - # Summary - lines = markdown_content.split('\n') - class_count = markdown_content.count('### class') - func_count = markdown_content.count('### ') - class_count - markdown_content.count('### automation.') - - print("\nDocumentation Statistics:") - print("- Classes documented: {}".format(class_count)) - print("- Functions documented: {}".format(func_count)) - print("- Total lines: {}".format(len(lines))) - - print("\nAPI documentation generated successfully!") - sys.exit(0) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tools/generate_complete_api_docs.py b/tools/generate_complete_api_docs.py deleted file mode 100644 index 8b41446..0000000 --- a/tools/generate_complete_api_docs.py +++ /dev/null @@ -1,960 +0,0 @@ -#!/usr/bin/env python3 -"""Generate COMPLETE HTML API reference documentation for McRogueFace with NO missing methods.""" - -import os -import sys -import datetime -import html -from pathlib import Path -import mcrfpy - -def escape_html(text: str) -> str: - """Escape HTML special characters.""" - return html.escape(text) if text else "" - -def get_complete_method_documentation(): - """Return complete documentation for ALL methods across all classes.""" - return { - # Base Drawable methods (inherited by all UI elements) - 'Drawable': { - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of this drawable element.', - 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', - 'note': 'The bounds are in screen coordinates and account for current position and size.' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the element by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ], - 'note': 'This modifies the x and y position properties by the given amounts.' - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Resize the element to new dimensions.', - 'args': [ - ('width', 'float', 'New width in pixels'), - ('height', 'float', 'New height in pixels') - ], - 'note': 'For Caption and Sprite, this may not change actual size if determined by content.' - } - }, - - # Entity-specific methods - 'Entity': { - 'at': { - 'signature': 'at(x, y)', - 'description': 'Check if this entity is at the specified grid coordinates.', - 'args': [ - ('x', 'int', 'Grid x coordinate to check'), - ('y', 'int', 'Grid y coordinate to check') - ], - 'returns': 'bool: True if entity is at position (x, y), False otherwise' - }, - 'die': { - 'signature': 'die()', - 'description': 'Remove this entity from its parent grid.', - 'note': 'The entity object remains valid but is no longer rendered or updated.' - }, - 'index': { - 'signature': 'index()', - 'description': 'Get the index of this entity in its parent grid\'s entity list.', - 'returns': 'int: Index position, or -1 if not in a grid' - } - }, - - # Grid-specific methods - 'Grid': { - 'at': { - 'signature': 'at(x, y)', - 'description': 'Get the GridPoint at the specified grid coordinates.', - 'args': [ - ('x', 'int', 'Grid x coordinate'), - ('y', 'int', 'Grid y coordinate') - ], - 'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds' - } - }, - - # Collection methods - 'EntityCollection': { - 'append': { - 'signature': 'append(entity)', - 'description': 'Add an entity to the end of the collection.', - 'args': [('entity', 'Entity', 'The entity to add')] - }, - 'remove': { - 'signature': 'remove(entity)', - 'description': 'Remove the first occurrence of an entity from the collection.', - 'args': [('entity', 'Entity', 'The entity to remove')], - 'raises': 'ValueError: If entity is not in collection' - }, - 'extend': { - 'signature': 'extend(iterable)', - 'description': 'Add all entities from an iterable to the collection.', - 'args': [('iterable', 'Iterable[Entity]', 'Entities to add')] - }, - 'count': { - 'signature': 'count(entity)', - 'description': 'Count the number of occurrences of an entity in the collection.', - 'args': [('entity', 'Entity', 'The entity to count')], - 'returns': 'int: Number of times entity appears in collection' - }, - 'index': { - 'signature': 'index(entity)', - 'description': 'Find the index of the first occurrence of an entity.', - 'args': [('entity', 'Entity', 'The entity to find')], - 'returns': 'int: Index of entity in collection', - 'raises': 'ValueError: If entity is not in collection' - } - }, - - 'UICollection': { - 'append': { - 'signature': 'append(drawable)', - 'description': 'Add a drawable element to the end of the collection.', - 'args': [('drawable', 'UIDrawable', 'The drawable element to add')] - }, - 'remove': { - 'signature': 'remove(drawable)', - 'description': 'Remove the first occurrence of a drawable from the collection.', - 'args': [('drawable', 'UIDrawable', 'The drawable to remove')], - 'raises': 'ValueError: If drawable is not in collection' - }, - 'extend': { - 'signature': 'extend(iterable)', - 'description': 'Add all drawables from an iterable to the collection.', - 'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')] - }, - 'count': { - 'signature': 'count(drawable)', - 'description': 'Count the number of occurrences of a drawable in the collection.', - 'args': [('drawable', 'UIDrawable', 'The drawable to count')], - 'returns': 'int: Number of times drawable appears in collection' - }, - 'index': { - 'signature': 'index(drawable)', - 'description': 'Find the index of the first occurrence of a drawable.', - 'args': [('drawable', 'UIDrawable', 'The drawable to find')], - 'returns': 'int: Index of drawable in collection', - 'raises': 'ValueError: If drawable is not in collection' - } - }, - - # Animation methods - 'Animation': { - 'get_current_value': { - 'signature': 'get_current_value()', - 'description': 'Get the current interpolated value of the animation.', - 'returns': 'float: Current animation value between start and end' - }, - 'start': { - 'signature': 'start(target)', - 'description': 'Start the animation on a target UI element.', - 'args': [('target', 'UIDrawable', 'The UI element to animate')], - 'note': 'The target must have the property specified in the animation constructor.' - }, - 'update': { - 'signature': 'update(delta_time)', - 'description': 'Update the animation by the given time delta.', - 'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')], - 'returns': 'bool: True if animation is still running, False if finished' - } - }, - - # Color methods - 'Color': { - 'from_hex': { - 'signature': 'from_hex(hex_string)', - 'description': 'Create a Color from a hexadecimal color string.', - 'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')], - 'returns': 'Color: New Color object from hex string', - 'example': 'red = Color.from_hex("#FF0000")' - }, - 'to_hex': { - 'signature': 'to_hex()', - 'description': 'Convert this Color to a hexadecimal string.', - 'returns': 'str: Hex color string in format "#RRGGBB"', - 'example': 'hex_str = color.to_hex() # Returns "#FF0000"' - }, - 'lerp': { - 'signature': 'lerp(other, t)', - 'description': 'Linearly interpolate between this color and another.', - 'args': [ - ('other', 'Color', 'The color to interpolate towards'), - ('t', 'float', 'Interpolation factor from 0.0 to 1.0') - ], - 'returns': 'Color: New interpolated Color object', - 'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue' - } - }, - - # Vector methods - 'Vector': { - 'magnitude': { - 'signature': 'magnitude()', - 'description': 'Calculate the length/magnitude of this vector.', - 'returns': 'float: The magnitude of the vector', - 'example': 'length = vector.magnitude()' - }, - 'magnitude_squared': { - 'signature': 'magnitude_squared()', - 'description': 'Calculate the squared magnitude of this vector.', - 'returns': 'float: The squared magnitude (faster than magnitude())', - 'note': 'Use this for comparisons to avoid expensive square root calculation.' - }, - 'normalize': { - 'signature': 'normalize()', - 'description': 'Return a unit vector in the same direction.', - 'returns': 'Vector: New normalized vector with magnitude 1.0', - 'raises': 'ValueError: If vector has zero magnitude' - }, - 'dot': { - 'signature': 'dot(other)', - 'description': 'Calculate the dot product with another vector.', - 'args': [('other', 'Vector', 'The other vector')], - 'returns': 'float: Dot product of the two vectors' - }, - 'distance_to': { - 'signature': 'distance_to(other)', - 'description': 'Calculate the distance to another vector.', - 'args': [('other', 'Vector', 'The other vector')], - 'returns': 'float: Distance between the two vectors' - }, - 'angle': { - 'signature': 'angle()', - 'description': 'Get the angle of this vector in radians.', - 'returns': 'float: Angle in radians from positive x-axis' - }, - 'copy': { - 'signature': 'copy()', - 'description': 'Create a copy of this vector.', - 'returns': 'Vector: New Vector object with same x and y values' - } - }, - - # Scene methods - 'Scene': { - 'activate': { - 'signature': 'activate()', - 'description': 'Make this scene the active scene.', - 'note': 'Equivalent to calling setScene() with this scene\'s name.' - }, - 'get_ui': { - 'signature': 'get_ui()', - 'description': 'Get the UI element collection for this scene.', - 'returns': 'UICollection: Collection of all UI elements in this scene' - }, - 'keypress': { - 'signature': 'keypress(handler)', - 'description': 'Register a keyboard handler function for this scene.', - 'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')], - 'note': 'Alternative to overriding the on_keypress method.' - }, - 'register_keyboard': { - 'signature': 'register_keyboard(callable)', - 'description': 'Register a keyboard event handler function for the scene.', - 'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')], - 'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.', - 'example': '''def handle_keyboard(key, action): - print(f"Key '{key}' was {action}") - if key == "q" and action == "press": - # Handle quit - pass -scene.register_keyboard(handle_keyboard)''' - } - }, - - # Timer methods - 'Timer': { - 'pause': { - 'signature': 'pause()', - 'description': 'Pause the timer, stopping its callback execution.', - 'note': 'Use resume() to continue the timer from where it was paused.' - }, - 'resume': { - 'signature': 'resume()', - 'description': 'Resume a paused timer.', - 'note': 'Has no effect if timer is not paused.' - }, - 'cancel': { - 'signature': 'cancel()', - 'description': 'Cancel the timer and remove it from the system.', - 'note': 'After cancelling, the timer object cannot be reused.' - }, - 'restart': { - 'signature': 'restart()', - 'description': 'Restart the timer from the beginning.', - 'note': 'Resets the timer\'s internal clock to zero.' - } - }, - - # Window methods - 'Window': { - 'get': { - 'signature': 'get()', - 'description': 'Get the Window singleton instance.', - 'returns': 'Window: The singleton window object', - 'note': 'This is a static method that returns the same instance every time.' - }, - 'center': { - 'signature': 'center()', - 'description': 'Center the window on the screen.', - 'note': 'Only works if the window is not fullscreen.' - }, - 'screenshot': { - 'signature': 'screenshot(filename)', - 'description': 'Take a screenshot and save it to a file.', - 'args': [('filename', 'str', 'Path where to save the screenshot')], - 'note': 'Supports PNG, JPG, and BMP formats based on file extension.' - } - } - } - -def get_complete_function_documentation(): - """Return complete documentation for ALL module functions.""" - return { - # Scene Management - 'createScene': { - 'signature': 'createScene(name: str) -> None', - 'description': 'Create a new empty scene with the given name.', - 'args': [('name', 'str', 'Unique name for the new scene')], - 'raises': 'ValueError: If a scene with this name already exists', - 'note': 'The scene is created but not made active. Use setScene() to switch to it.', - 'example': 'mcrfpy.createScene("game_over")' - }, - 'setScene': { - 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', - 'description': 'Switch to a different scene with optional transition effect.', - 'args': [ - ('scene', 'str', 'Name of the scene to switch to'), - ('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'), - ('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)') - ], - 'raises': 'KeyError: If the scene doesn\'t exist', - 'example': 'mcrfpy.setScene("game", "fade", 0.5)' - }, - 'currentScene': { - 'signature': 'currentScene() -> str', - 'description': 'Get the name of the currently active scene.', - 'returns': 'str: Name of the current scene', - 'example': 'scene_name = mcrfpy.currentScene()' - }, - 'sceneUI': { - 'signature': 'sceneUI(scene: str = None) -> UICollection', - 'description': 'Get all UI elements for a scene.', - 'args': [('scene', 'str', 'Scene name. If None, uses current scene')], - 'returns': 'UICollection: All UI elements in the scene', - 'raises': 'KeyError: If the specified scene doesn\'t exist', - 'example': 'ui_elements = mcrfpy.sceneUI("game")' - }, - 'keypressScene': { - 'signature': 'keypressScene(handler: callable) -> None', - 'description': 'Set the keyboard event handler for the current scene.', - 'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')], - 'example': '''def on_key(key, pressed): - if key == "SPACE" and pressed: - player.jump() -mcrfpy.keypressScene(on_key)''' - }, - - # Audio Functions - 'createSoundBuffer': { - 'signature': 'createSoundBuffer(filename: str) -> int', - 'description': 'Load a sound effect from a file and return its buffer ID.', - 'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')], - 'returns': 'int: Buffer ID for use with playSound()', - 'raises': 'RuntimeError: If the file cannot be loaded', - 'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")' - }, - 'loadMusic': { - 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', - 'description': 'Load and immediately play background music from a file.', - 'args': [ - ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), - ('loop', 'bool', 'Whether to loop the music (default: True)') - ], - 'note': 'Only one music track can play at a time. Loading new music stops the current track.', - 'example': 'mcrfpy.loadMusic("assets/background.ogg", True)' - }, - 'playSound': { - 'signature': 'playSound(buffer_id: int) -> None', - 'description': 'Play a sound effect using a previously loaded buffer.', - 'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')], - 'raises': 'RuntimeError: If the buffer ID is invalid', - 'example': 'mcrfpy.playSound(jump_sound)' - }, - 'getMusicVolume': { - 'signature': 'getMusicVolume() -> int', - 'description': 'Get the current music volume level.', - 'returns': 'int: Current volume (0-100)', - 'example': 'current_volume = mcrfpy.getMusicVolume()' - }, - 'getSoundVolume': { - 'signature': 'getSoundVolume() -> int', - 'description': 'Get the current sound effects volume level.', - 'returns': 'int: Current volume (0-100)', - 'example': 'current_volume = mcrfpy.getSoundVolume()' - }, - 'setMusicVolume': { - 'signature': 'setMusicVolume(volume: int) -> None', - 'description': 'Set the global music volume.', - 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], - 'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume' - }, - 'setSoundVolume': { - 'signature': 'setSoundVolume(volume: int) -> None', - 'description': 'Set the global sound effects volume.', - 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], - 'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume' - }, - - # UI Utilities - 'find': { - 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', - 'description': 'Find the first UI element with the specified name.', - 'args': [ - ('name', 'str', 'Exact name to search for'), - ('scene', 'str', 'Scene to search in (default: current scene)') - ], - 'returns': 'UIDrawable or None: The found element, or None if not found', - 'note': 'Searches scene UI elements and entities within grids.', - 'example': 'button = mcrfpy.find("start_button")' - }, - 'findAll': { - 'signature': 'findAll(pattern: str, scene: str = None) -> list', - 'description': 'Find all UI elements matching a name pattern.', - 'args': [ - ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), - ('scene', 'str', 'Scene to search in (default: current scene)') - ], - 'returns': 'list: All matching UI elements and entities', - 'example': 'enemies = mcrfpy.findAll("enemy_*")' - }, - - # System Functions - 'exit': { - 'signature': 'exit() -> None', - 'description': 'Cleanly shut down the game engine and exit the application.', - 'note': 'This immediately closes the window and terminates the program.', - 'example': 'mcrfpy.exit()' - }, - 'getMetrics': { - 'signature': 'getMetrics() -> dict', - 'description': 'Get current performance metrics.', - 'returns': '''dict: Performance data with keys: -- frame_time: Last frame duration in seconds -- avg_frame_time: Average frame time -- fps: Frames per second -- draw_calls: Number of draw calls -- ui_elements: Total UI element count -- visible_elements: Visible element count -- current_frame: Frame counter -- runtime: Total runtime in seconds''', - 'example': 'metrics = mcrfpy.getMetrics()' - }, - 'setTimer': { - 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', - 'description': 'Create or update a recurring timer.', - 'args': [ - ('name', 'str', 'Unique identifier for the timer'), - ('handler', 'callable', 'Function called with (runtime: float) parameter'), - ('interval', 'int', 'Time between calls in milliseconds') - ], - 'note': 'If a timer with this name exists, it will be replaced.', - 'example': '''def update_score(runtime): - score += 1 -mcrfpy.setTimer("score_update", update_score, 1000)''' - }, - 'delTimer': { - 'signature': 'delTimer(name: str) -> None', - 'description': 'Stop and remove a timer.', - 'args': [('name', 'str', 'Timer identifier to remove')], - 'note': 'No error is raised if the timer doesn\'t exist.', - 'example': 'mcrfpy.delTimer("score_update")' - }, - 'setScale': { - 'signature': 'setScale(multiplier: float) -> None', - 'description': 'Scale the game window size.', - 'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')], - 'note': 'The internal resolution remains 1024x768, but the window is scaled.', - 'example': 'mcrfpy.setScale(2.0) # Double the window size' - } - } - -def get_complete_property_documentation(): - """Return complete documentation for ALL properties.""" - return { - 'Animation': { - 'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")', - 'duration': 'float: Total duration of the animation in seconds', - 'elapsed_time': 'float: Time elapsed since animation started (read-only)', - 'current_value': 'float: Current interpolated value of the animation (read-only)', - 'is_running': 'bool: True if animation is currently running (read-only)', - 'is_finished': 'bool: True if animation has completed (read-only)' - }, - 'GridPoint': { - 'x': 'int: Grid x coordinate of this point', - 'y': 'int: Grid y coordinate of this point', - 'texture_index': 'int: Index of the texture/sprite to display at this point', - 'solid': 'bool: Whether this point blocks movement', - 'transparent': 'bool: Whether this point allows light/vision through', - 'color': 'Color: Color tint applied to the texture at this point' - }, - 'GridPointState': { - 'visible': 'bool: Whether this point is currently visible to the player', - 'discovered': 'bool: Whether this point has been discovered/explored', - 'custom_flags': 'int: Bitfield for custom game-specific flags' - } - } - -def generate_complete_html_documentation(): - """Generate complete HTML documentation with NO missing methods.""" - - # Get all documentation data - method_docs = get_complete_method_documentation() - function_docs = get_complete_function_documentation() - property_docs = get_complete_property_documentation() - - html_parts = [] - - # HTML header with enhanced styling - html_parts.append(''' - - - - - McRogueFace API Reference - Complete Documentation - - - -
-''') - - # Title and overview - html_parts.append('

McRogueFace API Reference - Complete Documentation

') - html_parts.append(f'

Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

') - - # Table of contents - html_parts.append('
') - html_parts.append('

Table of Contents

') - html_parts.append('') - html_parts.append('
') - - # Functions section - html_parts.append('

Functions

') - - # Group functions by category - categories = { - 'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'], - 'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'], - 'UI Utilities': ['find', 'findAll'], - 'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] - } - - for category, functions in categories.items(): - html_parts.append(f'

{category}

') - for func_name in functions: - if func_name in function_docs: - html_parts.append(format_function_html(func_name, function_docs[func_name])) - - # Classes section - html_parts.append('

Classes

') - - # Get all classes from mcrfpy - classes = [] - for name in sorted(dir(mcrfpy)): - if not name.startswith('_'): - obj = getattr(mcrfpy, name) - if isinstance(obj, type): - classes.append((name, obj)) - - # Generate class documentation - for class_name, cls in classes: - html_parts.append(format_class_html_complete(class_name, cls, method_docs, property_docs)) - - # Automation section - if hasattr(mcrfpy, 'automation'): - html_parts.append('

Automation Module

') - html_parts.append('

The mcrfpy.automation module provides testing and automation capabilities.

') - - automation = mcrfpy.automation - for name in sorted(dir(automation)): - if not name.startswith('_'): - obj = getattr(automation, name) - if callable(obj): - html_parts.append(f'
') - html_parts.append(f'

automation.{name}

') - if obj.__doc__: - doc_parts = obj.__doc__.split(' - ') - if len(doc_parts) > 1: - html_parts.append(f'

{escape_html(doc_parts[1])}

') - else: - html_parts.append(f'

{escape_html(obj.__doc__)}

') - html_parts.append('
') - - html_parts.append('
') - html_parts.append('') - html_parts.append('') - - return '\n'.join(html_parts) - -def format_function_html(func_name, func_doc): - """Format a function with complete documentation.""" - html_parts = [] - - html_parts.append('
') - html_parts.append(f'

{func_doc["signature"]}

') - html_parts.append(f'

{escape_html(func_doc["description"])}

') - - # Arguments - if 'args' in func_doc: - html_parts.append('
') - html_parts.append('
Arguments:
') - for arg in func_doc['args']: - html_parts.append('
') - html_parts.append(f'{arg[0]} ') - html_parts.append(f'({arg[1]}): ') - html_parts.append(f'{escape_html(arg[2])}') - html_parts.append('
') - html_parts.append('
') - - # Returns - if 'returns' in func_doc: - html_parts.append('
') - html_parts.append(f'Returns: {escape_html(func_doc["returns"])}') - html_parts.append('
') - - # Raises - if 'raises' in func_doc: - html_parts.append('
') - html_parts.append(f'Raises: {escape_html(func_doc["raises"])}') - html_parts.append('
') - - # Note - if 'note' in func_doc: - html_parts.append('
') - html_parts.append(f'Note: {escape_html(func_doc["note"])}') - html_parts.append('
') - - # Example - if 'example' in func_doc: - html_parts.append('
') - html_parts.append('
Example:
') - html_parts.append('
')
-        html_parts.append(escape_html(func_doc['example']))
-        html_parts.append('
') - html_parts.append('
') - - html_parts.append('
') - - return '\n'.join(html_parts) - -def format_class_html_complete(class_name, cls, method_docs, property_docs): - """Format a class with complete documentation.""" - html_parts = [] - - html_parts.append('
') - html_parts.append(f'

{class_name}

') - - # Class description - if cls.__doc__: - html_parts.append(f'

{escape_html(cls.__doc__)}

') - - # Properties - if class_name in property_docs: - html_parts.append('

Properties:

') - for prop_name, prop_desc in property_docs[class_name].items(): - html_parts.append(f'
') - html_parts.append(f'{prop_name}: {escape_html(prop_desc)}') - html_parts.append('
') - - # Methods - methods_to_document = [] - - # Add inherited methods for UI classes - if any(base.__name__ == 'Drawable' for base in cls.__bases__ if hasattr(base, '__name__')): - methods_to_document.extend(['get_bounds', 'move', 'resize']) - - # Add class-specific methods - if class_name in method_docs: - methods_to_document.extend(method_docs[class_name].keys()) - - # Add methods from introspection - for attr_name in dir(cls): - if not attr_name.startswith('_') and callable(getattr(cls, attr_name)): - if attr_name not in methods_to_document: - methods_to_document.append(attr_name) - - if methods_to_document: - html_parts.append('

Methods:

') - for method_name in set(methods_to_document): - # Get method documentation - method_doc = None - if class_name in method_docs and method_name in method_docs[class_name]: - method_doc = method_docs[class_name][method_name] - elif method_name in method_docs.get('Drawable', {}): - method_doc = method_docs['Drawable'][method_name] - - if method_doc: - html_parts.append(format_method_html(method_name, method_doc)) - else: - # Basic method with no documentation - html_parts.append(f'
') - html_parts.append(f'{method_name}(...)') - html_parts.append('
') - - html_parts.append('
') - - return '\n'.join(html_parts) - -def format_method_html(method_name, method_doc): - """Format a method with complete documentation.""" - html_parts = [] - - html_parts.append('
') - html_parts.append(f'
{method_doc["signature"]}
') - html_parts.append(f'

{escape_html(method_doc["description"])}

') - - # Arguments - if 'args' in method_doc: - for arg in method_doc['args']: - html_parts.append(f'
') - html_parts.append(f'{arg[0]} ') - html_parts.append(f'({arg[1]}): ') - html_parts.append(f'{escape_html(arg[2])}') - html_parts.append('
') - - # Returns - if 'returns' in method_doc: - html_parts.append(f'
') - html_parts.append(f'Returns: {escape_html(method_doc["returns"])}') - html_parts.append('
') - - # Note - if 'note' in method_doc: - html_parts.append(f'
') - html_parts.append(f'Note: {escape_html(method_doc["note"])}') - html_parts.append('
') - - # Example - if 'example' in method_doc: - html_parts.append(f'
') - html_parts.append('Example:') - html_parts.append('
')
-        html_parts.append(escape_html(method_doc['example']))
-        html_parts.append('
') - html_parts.append('
') - - html_parts.append('
') - - return '\n'.join(html_parts) - -def main(): - """Generate complete HTML documentation with zero missing methods.""" - print("Generating COMPLETE HTML API documentation...") - - # Generate HTML - html_content = generate_complete_html_documentation() - - # Write to file - output_path = Path("docs/api_reference_complete.html") - output_path.parent.mkdir(exist_ok=True) - - with open(output_path, 'w', encoding='utf-8') as f: - f.write(html_content) - - print(f"✓ Generated {output_path}") - print(f" File size: {len(html_content):,} bytes") - - # Count "..." instances - ellipsis_count = html_content.count('...') - print(f" Ellipsis instances: {ellipsis_count}") - - if ellipsis_count == 0: - print("✅ SUCCESS: No missing documentation found!") - else: - print(f"❌ WARNING: {ellipsis_count} methods still need documentation") - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tools/generate_complete_markdown_docs.py b/tools/generate_complete_markdown_docs.py deleted file mode 100644 index 89fab79..0000000 --- a/tools/generate_complete_markdown_docs.py +++ /dev/null @@ -1,821 +0,0 @@ -#!/usr/bin/env python3 -"""Generate COMPLETE Markdown API reference documentation for McRogueFace with NO missing methods.""" - -import os -import sys -import datetime -from pathlib import Path -import mcrfpy - -def get_complete_method_documentation(): - """Return complete documentation for ALL methods across all classes.""" - return { - # Base Drawable methods (inherited by all UI elements) - 'Drawable': { - 'get_bounds': { - 'signature': 'get_bounds()', - 'description': 'Get the bounding rectangle of this drawable element.', - 'returns': 'tuple: (x, y, width, height) representing the element\'s bounds', - 'note': 'The bounds are in screen coordinates and account for current position and size.' - }, - 'move': { - 'signature': 'move(dx, dy)', - 'description': 'Move the element by a relative offset.', - 'args': [ - ('dx', 'float', 'Horizontal offset in pixels'), - ('dy', 'float', 'Vertical offset in pixels') - ], - 'note': 'This modifies the x and y position properties by the given amounts.' - }, - 'resize': { - 'signature': 'resize(width, height)', - 'description': 'Resize the element to new dimensions.', - 'args': [ - ('width', 'float', 'New width in pixels'), - ('height', 'float', 'New height in pixels') - ], - 'note': 'For Caption and Sprite, this may not change actual size if determined by content.' - } - }, - - # Entity-specific methods - 'Entity': { - 'at': { - 'signature': 'at(x, y)', - 'description': 'Check if this entity is at the specified grid coordinates.', - 'args': [ - ('x', 'int', 'Grid x coordinate to check'), - ('y', 'int', 'Grid y coordinate to check') - ], - 'returns': 'bool: True if entity is at position (x, y), False otherwise' - }, - 'die': { - 'signature': 'die()', - 'description': 'Remove this entity from its parent grid.', - 'note': 'The entity object remains valid but is no longer rendered or updated.' - }, - 'index': { - 'signature': 'index()', - 'description': 'Get the index of this entity in its parent grid\'s entity list.', - 'returns': 'int: Index position, or -1 if not in a grid' - } - }, - - # Grid-specific methods - 'Grid': { - 'at': { - 'signature': 'at(x, y)', - 'description': 'Get the GridPoint at the specified grid coordinates.', - 'args': [ - ('x', 'int', 'Grid x coordinate'), - ('y', 'int', 'Grid y coordinate') - ], - 'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds' - } - }, - - # Collection methods - 'EntityCollection': { - 'append': { - 'signature': 'append(entity)', - 'description': 'Add an entity to the end of the collection.', - 'args': [('entity', 'Entity', 'The entity to add')] - }, - 'remove': { - 'signature': 'remove(entity)', - 'description': 'Remove the first occurrence of an entity from the collection.', - 'args': [('entity', 'Entity', 'The entity to remove')], - 'raises': 'ValueError: If entity is not in collection' - }, - 'extend': { - 'signature': 'extend(iterable)', - 'description': 'Add all entities from an iterable to the collection.', - 'args': [('iterable', 'Iterable[Entity]', 'Entities to add')] - }, - 'count': { - 'signature': 'count(entity)', - 'description': 'Count the number of occurrences of an entity in the collection.', - 'args': [('entity', 'Entity', 'The entity to count')], - 'returns': 'int: Number of times entity appears in collection' - }, - 'index': { - 'signature': 'index(entity)', - 'description': 'Find the index of the first occurrence of an entity.', - 'args': [('entity', 'Entity', 'The entity to find')], - 'returns': 'int: Index of entity in collection', - 'raises': 'ValueError: If entity is not in collection' - } - }, - - 'UICollection': { - 'append': { - 'signature': 'append(drawable)', - 'description': 'Add a drawable element to the end of the collection.', - 'args': [('drawable', 'UIDrawable', 'The drawable element to add')] - }, - 'remove': { - 'signature': 'remove(drawable)', - 'description': 'Remove the first occurrence of a drawable from the collection.', - 'args': [('drawable', 'UIDrawable', 'The drawable to remove')], - 'raises': 'ValueError: If drawable is not in collection' - }, - 'extend': { - 'signature': 'extend(iterable)', - 'description': 'Add all drawables from an iterable to the collection.', - 'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')] - }, - 'count': { - 'signature': 'count(drawable)', - 'description': 'Count the number of occurrences of a drawable in the collection.', - 'args': [('drawable', 'UIDrawable', 'The drawable to count')], - 'returns': 'int: Number of times drawable appears in collection' - }, - 'index': { - 'signature': 'index(drawable)', - 'description': 'Find the index of the first occurrence of a drawable.', - 'args': [('drawable', 'UIDrawable', 'The drawable to find')], - 'returns': 'int: Index of drawable in collection', - 'raises': 'ValueError: If drawable is not in collection' - } - }, - - # Animation methods - 'Animation': { - 'get_current_value': { - 'signature': 'get_current_value()', - 'description': 'Get the current interpolated value of the animation.', - 'returns': 'float: Current animation value between start and end' - }, - 'start': { - 'signature': 'start(target)', - 'description': 'Start the animation on a target UI element.', - 'args': [('target', 'UIDrawable', 'The UI element to animate')], - 'note': 'The target must have the property specified in the animation constructor.' - }, - 'update': { - 'signature': 'update(delta_time)', - 'description': 'Update the animation by the given time delta.', - 'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')], - 'returns': 'bool: True if animation is still running, False if finished' - } - }, - - # Color methods - 'Color': { - 'from_hex': { - 'signature': 'from_hex(hex_string)', - 'description': 'Create a Color from a hexadecimal color string.', - 'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')], - 'returns': 'Color: New Color object from hex string', - 'example': 'red = Color.from_hex("#FF0000")' - }, - 'to_hex': { - 'signature': 'to_hex()', - 'description': 'Convert this Color to a hexadecimal string.', - 'returns': 'str: Hex color string in format "#RRGGBB"', - 'example': 'hex_str = color.to_hex() # Returns "#FF0000"' - }, - 'lerp': { - 'signature': 'lerp(other, t)', - 'description': 'Linearly interpolate between this color and another.', - 'args': [ - ('other', 'Color', 'The color to interpolate towards'), - ('t', 'float', 'Interpolation factor from 0.0 to 1.0') - ], - 'returns': 'Color: New interpolated Color object', - 'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue' - } - }, - - # Vector methods - 'Vector': { - 'magnitude': { - 'signature': 'magnitude()', - 'description': 'Calculate the length/magnitude of this vector.', - 'returns': 'float: The magnitude of the vector' - }, - 'magnitude_squared': { - 'signature': 'magnitude_squared()', - 'description': 'Calculate the squared magnitude of this vector.', - 'returns': 'float: The squared magnitude (faster than magnitude())', - 'note': 'Use this for comparisons to avoid expensive square root calculation.' - }, - 'normalize': { - 'signature': 'normalize()', - 'description': 'Return a unit vector in the same direction.', - 'returns': 'Vector: New normalized vector with magnitude 1.0', - 'raises': 'ValueError: If vector has zero magnitude' - }, - 'dot': { - 'signature': 'dot(other)', - 'description': 'Calculate the dot product with another vector.', - 'args': [('other', 'Vector', 'The other vector')], - 'returns': 'float: Dot product of the two vectors' - }, - 'distance_to': { - 'signature': 'distance_to(other)', - 'description': 'Calculate the distance to another vector.', - 'args': [('other', 'Vector', 'The other vector')], - 'returns': 'float: Distance between the two vectors' - }, - 'angle': { - 'signature': 'angle()', - 'description': 'Get the angle of this vector in radians.', - 'returns': 'float: Angle in radians from positive x-axis' - }, - 'copy': { - 'signature': 'copy()', - 'description': 'Create a copy of this vector.', - 'returns': 'Vector: New Vector object with same x and y values' - } - }, - - # Scene methods - 'Scene': { - 'activate': { - 'signature': 'activate()', - 'description': 'Make this scene the active scene.', - 'note': 'Equivalent to calling setScene() with this scene\'s name.' - }, - 'get_ui': { - 'signature': 'get_ui()', - 'description': 'Get the UI element collection for this scene.', - 'returns': 'UICollection: Collection of all UI elements in this scene' - }, - 'keypress': { - 'signature': 'keypress(handler)', - 'description': 'Register a keyboard handler function for this scene.', - 'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')], - 'note': 'Alternative to overriding the on_keypress method.' - }, - 'register_keyboard': { - 'signature': 'register_keyboard(callable)', - 'description': 'Register a keyboard event handler function for the scene.', - 'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')], - 'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.', - 'example': '''def handle_keyboard(key, action): - print(f"Key '{key}' was {action}") -scene.register_keyboard(handle_keyboard)''' - } - }, - - # Timer methods - 'Timer': { - 'pause': { - 'signature': 'pause()', - 'description': 'Pause the timer, stopping its callback execution.', - 'note': 'Use resume() to continue the timer from where it was paused.' - }, - 'resume': { - 'signature': 'resume()', - 'description': 'Resume a paused timer.', - 'note': 'Has no effect if timer is not paused.' - }, - 'cancel': { - 'signature': 'cancel()', - 'description': 'Cancel the timer and remove it from the system.', - 'note': 'After cancelling, the timer object cannot be reused.' - }, - 'restart': { - 'signature': 'restart()', - 'description': 'Restart the timer from the beginning.', - 'note': 'Resets the timer\'s internal clock to zero.' - } - }, - - # Window methods - 'Window': { - 'get': { - 'signature': 'get()', - 'description': 'Get the Window singleton instance.', - 'returns': 'Window: The singleton window object', - 'note': 'This is a static method that returns the same instance every time.' - }, - 'center': { - 'signature': 'center()', - 'description': 'Center the window on the screen.', - 'note': 'Only works if the window is not fullscreen.' - }, - 'screenshot': { - 'signature': 'screenshot(filename)', - 'description': 'Take a screenshot and save it to a file.', - 'args': [('filename', 'str', 'Path where to save the screenshot')], - 'note': 'Supports PNG, JPG, and BMP formats based on file extension.' - } - } - } - -def get_complete_function_documentation(): - """Return complete documentation for ALL module functions.""" - return { - # Scene Management - 'createScene': { - 'signature': 'createScene(name: str) -> None', - 'description': 'Create a new empty scene with the given name.', - 'args': [('name', 'str', 'Unique name for the new scene')], - 'raises': 'ValueError: If a scene with this name already exists', - 'note': 'The scene is created but not made active. Use setScene() to switch to it.', - 'example': 'mcrfpy.createScene("game_over")' - }, - 'setScene': { - 'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None', - 'description': 'Switch to a different scene with optional transition effect.', - 'args': [ - ('scene', 'str', 'Name of the scene to switch to'), - ('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'), - ('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)') - ], - 'raises': 'KeyError: If the scene doesn\'t exist', - 'example': 'mcrfpy.setScene("game", "fade", 0.5)' - }, - 'currentScene': { - 'signature': 'currentScene() -> str', - 'description': 'Get the name of the currently active scene.', - 'returns': 'str: Name of the current scene', - 'example': 'scene_name = mcrfpy.currentScene()' - }, - 'sceneUI': { - 'signature': 'sceneUI(scene: str = None) -> UICollection', - 'description': 'Get all UI elements for a scene.', - 'args': [('scene', 'str', 'Scene name. If None, uses current scene')], - 'returns': 'UICollection: All UI elements in the scene', - 'raises': 'KeyError: If the specified scene doesn\'t exist', - 'example': 'ui_elements = mcrfpy.sceneUI("game")' - }, - 'keypressScene': { - 'signature': 'keypressScene(handler: callable) -> None', - 'description': 'Set the keyboard event handler for the current scene.', - 'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')], - 'example': '''def on_key(key, pressed): - if key == "SPACE" and pressed: - player.jump() -mcrfpy.keypressScene(on_key)''' - }, - - # Audio Functions - 'createSoundBuffer': { - 'signature': 'createSoundBuffer(filename: str) -> int', - 'description': 'Load a sound effect from a file and return its buffer ID.', - 'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')], - 'returns': 'int: Buffer ID for use with playSound()', - 'raises': 'RuntimeError: If the file cannot be loaded', - 'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")' - }, - 'loadMusic': { - 'signature': 'loadMusic(filename: str, loop: bool = True) -> None', - 'description': 'Load and immediately play background music from a file.', - 'args': [ - ('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'), - ('loop', 'bool', 'Whether to loop the music (default: True)') - ], - 'note': 'Only one music track can play at a time. Loading new music stops the current track.', - 'example': 'mcrfpy.loadMusic("assets/background.ogg", True)' - }, - 'playSound': { - 'signature': 'playSound(buffer_id: int) -> None', - 'description': 'Play a sound effect using a previously loaded buffer.', - 'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')], - 'raises': 'RuntimeError: If the buffer ID is invalid', - 'example': 'mcrfpy.playSound(jump_sound)' - }, - 'getMusicVolume': { - 'signature': 'getMusicVolume() -> int', - 'description': 'Get the current music volume level.', - 'returns': 'int: Current volume (0-100)', - 'example': 'current_volume = mcrfpy.getMusicVolume()' - }, - 'getSoundVolume': { - 'signature': 'getSoundVolume() -> int', - 'description': 'Get the current sound effects volume level.', - 'returns': 'int: Current volume (0-100)', - 'example': 'current_volume = mcrfpy.getSoundVolume()' - }, - 'setMusicVolume': { - 'signature': 'setMusicVolume(volume: int) -> None', - 'description': 'Set the global music volume.', - 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], - 'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume' - }, - 'setSoundVolume': { - 'signature': 'setSoundVolume(volume: int) -> None', - 'description': 'Set the global sound effects volume.', - 'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')], - 'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume' - }, - - # UI Utilities - 'find': { - 'signature': 'find(name: str, scene: str = None) -> UIDrawable | None', - 'description': 'Find the first UI element with the specified name.', - 'args': [ - ('name', 'str', 'Exact name to search for'), - ('scene', 'str', 'Scene to search in (default: current scene)') - ], - 'returns': 'UIDrawable or None: The found element, or None if not found', - 'note': 'Searches scene UI elements and entities within grids.', - 'example': 'button = mcrfpy.find("start_button")' - }, - 'findAll': { - 'signature': 'findAll(pattern: str, scene: str = None) -> list', - 'description': 'Find all UI elements matching a name pattern.', - 'args': [ - ('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'), - ('scene', 'str', 'Scene to search in (default: current scene)') - ], - 'returns': 'list: All matching UI elements and entities', - 'example': 'enemies = mcrfpy.findAll("enemy_*")' - }, - - # System Functions - 'exit': { - 'signature': 'exit() -> None', - 'description': 'Cleanly shut down the game engine and exit the application.', - 'note': 'This immediately closes the window and terminates the program.', - 'example': 'mcrfpy.exit()' - }, - 'getMetrics': { - 'signature': 'getMetrics() -> dict', - 'description': 'Get current performance metrics.', - 'returns': '''dict: Performance data with keys: -- frame_time: Last frame duration in seconds -- avg_frame_time: Average frame time -- fps: Frames per second -- draw_calls: Number of draw calls -- ui_elements: Total UI element count -- visible_elements: Visible element count -- current_frame: Frame counter -- runtime: Total runtime in seconds''', - 'example': 'metrics = mcrfpy.getMetrics()' - }, - 'setTimer': { - 'signature': 'setTimer(name: str, handler: callable, interval: int) -> None', - 'description': 'Create or update a recurring timer.', - 'args': [ - ('name', 'str', 'Unique identifier for the timer'), - ('handler', 'callable', 'Function called with (runtime: float) parameter'), - ('interval', 'int', 'Time between calls in milliseconds') - ], - 'note': 'If a timer with this name exists, it will be replaced.', - 'example': '''def update_score(runtime): - score += 1 -mcrfpy.setTimer("score_update", update_score, 1000)''' - }, - 'delTimer': { - 'signature': 'delTimer(name: str) -> None', - 'description': 'Stop and remove a timer.', - 'args': [('name', 'str', 'Timer identifier to remove')], - 'note': 'No error is raised if the timer doesn\'t exist.', - 'example': 'mcrfpy.delTimer("score_update")' - }, - 'setScale': { - 'signature': 'setScale(multiplier: float) -> None', - 'description': 'Scale the game window size.', - 'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')], - 'note': 'The internal resolution remains 1024x768, but the window is scaled.', - 'example': 'mcrfpy.setScale(2.0) # Double the window size' - } - } - -def get_complete_property_documentation(): - """Return complete documentation for ALL properties.""" - return { - 'Animation': { - 'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")', - 'duration': 'float: Total duration of the animation in seconds', - 'elapsed_time': 'float: Time elapsed since animation started (read-only)', - 'current_value': 'float: Current interpolated value of the animation (read-only)', - 'is_running': 'bool: True if animation is currently running (read-only)', - 'is_finished': 'bool: True if animation has completed (read-only)' - }, - 'GridPoint': { - 'x': 'int: Grid x coordinate of this point', - 'y': 'int: Grid y coordinate of this point', - 'texture_index': 'int: Index of the texture/sprite to display at this point', - 'solid': 'bool: Whether this point blocks movement', - 'transparent': 'bool: Whether this point allows light/vision through', - 'color': 'Color: Color tint applied to the texture at this point' - }, - 'GridPointState': { - 'visible': 'bool: Whether this point is currently visible to the player', - 'discovered': 'bool: Whether this point has been discovered/explored', - 'custom_flags': 'int: Bitfield for custom game-specific flags' - } - } - -def format_method_markdown(method_name, method_doc): - """Format a method as markdown.""" - lines = [] - - lines.append(f"#### `{method_doc['signature']}`") - lines.append("") - lines.append(method_doc['description']) - lines.append("") - - # Arguments - if 'args' in method_doc: - lines.append("**Arguments:**") - for arg in method_doc['args']: - lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}") - lines.append("") - - # Returns - if 'returns' in method_doc: - lines.append(f"**Returns:** {method_doc['returns']}") - lines.append("") - - # Raises - if 'raises' in method_doc: - lines.append(f"**Raises:** {method_doc['raises']}") - lines.append("") - - # Note - if 'note' in method_doc: - lines.append(f"**Note:** {method_doc['note']}") - lines.append("") - - # Example - if 'example' in method_doc: - lines.append("**Example:**") - lines.append("```python") - lines.append(method_doc['example']) - lines.append("```") - lines.append("") - - return lines - -def format_function_markdown(func_name, func_doc): - """Format a function as markdown.""" - lines = [] - - lines.append(f"### `{func_doc['signature']}`") - lines.append("") - lines.append(func_doc['description']) - lines.append("") - - # Arguments - if 'args' in func_doc: - lines.append("**Arguments:**") - for arg in func_doc['args']: - lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}") - lines.append("") - - # Returns - if 'returns' in func_doc: - lines.append(f"**Returns:** {func_doc['returns']}") - lines.append("") - - # Raises - if 'raises' in func_doc: - lines.append(f"**Raises:** {func_doc['raises']}") - lines.append("") - - # Note - if 'note' in func_doc: - lines.append(f"**Note:** {func_doc['note']}") - lines.append("") - - # Example - if 'example' in func_doc: - lines.append("**Example:**") - lines.append("```python") - lines.append(func_doc['example']) - lines.append("```") - lines.append("") - - lines.append("---") - lines.append("") - - return lines - -def generate_complete_markdown_documentation(): - """Generate complete markdown documentation with NO missing methods.""" - - # Get all documentation data - method_docs = get_complete_method_documentation() - function_docs = get_complete_function_documentation() - property_docs = get_complete_property_documentation() - - lines = [] - - # Header - lines.append("# McRogueFace API Reference") - lines.append("") - lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") - lines.append("") - - # Overview - if mcrfpy.__doc__: - lines.append("## Overview") - lines.append("") - # Process the docstring properly - doc_text = mcrfpy.__doc__.replace('\\n', '\n') - lines.append(doc_text) - lines.append("") - - # Table of Contents - lines.append("## Table of Contents") - lines.append("") - lines.append("- [Functions](#functions)") - lines.append(" - [Scene Management](#scene-management)") - lines.append(" - [Audio](#audio)") - lines.append(" - [UI Utilities](#ui-utilities)") - lines.append(" - [System](#system)") - lines.append("- [Classes](#classes)") - lines.append(" - [UI Components](#ui-components)") - lines.append(" - [Collections](#collections)") - lines.append(" - [System Types](#system-types)") - lines.append(" - [Other Classes](#other-classes)") - lines.append("- [Automation Module](#automation-module)") - lines.append("") - - # Functions section - lines.append("## Functions") - lines.append("") - - # Group functions by category - categories = { - 'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'], - 'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'], - 'UI Utilities': ['find', 'findAll'], - 'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale'] - } - - for category, functions in categories.items(): - lines.append(f"### {category}") - lines.append("") - for func_name in functions: - if func_name in function_docs: - lines.extend(format_function_markdown(func_name, function_docs[func_name])) - - # Classes section - lines.append("## Classes") - lines.append("") - - # Get all classes from mcrfpy - classes = [] - for name in sorted(dir(mcrfpy)): - if not name.startswith('_'): - obj = getattr(mcrfpy, name) - if isinstance(obj, type): - classes.append((name, obj)) - - # Group classes - ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity'] - collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter'] - system_classes = ['Color', 'Vector', 'Texture', 'Font'] - other_classes = [name for name, _ in classes if name not in ui_classes + collection_classes + system_classes] - - # UI Components - lines.append("### UI Components") - lines.append("") - for class_name in ui_classes: - if any(name == class_name for name, _ in classes): - lines.extend(format_class_markdown(class_name, method_docs, property_docs)) - - # Collections - lines.append("### Collections") - lines.append("") - for class_name in collection_classes: - if any(name == class_name for name, _ in classes): - lines.extend(format_class_markdown(class_name, method_docs, property_docs)) - - # System Types - lines.append("### System Types") - lines.append("") - for class_name in system_classes: - if any(name == class_name for name, _ in classes): - lines.extend(format_class_markdown(class_name, method_docs, property_docs)) - - # Other Classes - lines.append("### Other Classes") - lines.append("") - for class_name in other_classes: - lines.extend(format_class_markdown(class_name, method_docs, property_docs)) - - # Automation section - if hasattr(mcrfpy, 'automation'): - lines.append("## Automation Module") - lines.append("") - lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.") - lines.append("") - - automation = mcrfpy.automation - for name in sorted(dir(automation)): - if not name.startswith('_'): - obj = getattr(automation, name) - if callable(obj): - lines.append(f"### `automation.{name}`") - lines.append("") - if obj.__doc__: - doc_parts = obj.__doc__.split(' - ') - if len(doc_parts) > 1: - lines.append(doc_parts[1]) - else: - lines.append(obj.__doc__) - lines.append("") - lines.append("---") - lines.append("") - - return '\n'.join(lines) - -def format_class_markdown(class_name, method_docs, property_docs): - """Format a class as markdown.""" - lines = [] - - lines.append(f"### class `{class_name}`") - lines.append("") - - # Class description from known info - class_descriptions = { - 'Frame': 'A rectangular frame UI element that can contain other drawable elements.', - 'Caption': 'A text display UI element with customizable font and styling.', - 'Sprite': 'A sprite UI element that displays a texture or portion of a texture atlas.', - 'Grid': 'A grid-based tilemap UI element for rendering tile-based levels and game worlds.', - 'Entity': 'Game entity that can be placed in a Grid.', - 'EntityCollection': 'Container for Entity objects in a Grid. Supports iteration and indexing.', - 'UICollection': 'Container for UI drawable elements. Supports iteration and indexing.', - 'UICollectionIter': 'Iterator for UICollection. Automatically created when iterating over a UICollection.', - 'UIEntityCollectionIter': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.', - 'Color': 'RGBA color representation.', - 'Vector': '2D vector for positions and directions.', - 'Font': 'Font object for text rendering.', - 'Texture': 'Texture object for image data.', - 'Animation': 'Animate UI element properties over time.', - 'GridPoint': 'Represents a single tile in a Grid.', - 'GridPointState': 'State information for a GridPoint.', - 'Scene': 'Base class for object-oriented scenes.', - 'Timer': 'Timer object for scheduled callbacks.', - 'Window': 'Window singleton for accessing and modifying the game window properties.', - 'Drawable': 'Base class for all drawable UI elements.' - } - - if class_name in class_descriptions: - lines.append(class_descriptions[class_name]) - lines.append("") - - # Properties - if class_name in property_docs: - lines.append("#### Properties") - lines.append("") - for prop_name, prop_desc in property_docs[class_name].items(): - lines.append(f"- **`{prop_name}`**: {prop_desc}") - lines.append("") - - # Methods - methods_to_document = [] - - # Add inherited methods for UI classes - if class_name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']: - methods_to_document.extend(['get_bounds', 'move', 'resize']) - - # Add class-specific methods - if class_name in method_docs: - methods_to_document.extend(method_docs[class_name].keys()) - - if methods_to_document: - lines.append("#### Methods") - lines.append("") - for method_name in set(methods_to_document): - # Get method documentation - method_doc = None - if class_name in method_docs and method_name in method_docs[class_name]: - method_doc = method_docs[class_name][method_name] - elif method_name in method_docs.get('Drawable', {}): - method_doc = method_docs['Drawable'][method_name] - - if method_doc: - lines.extend(format_method_markdown(method_name, method_doc)) - - lines.append("---") - lines.append("") - - return lines - -def main(): - """Generate complete markdown documentation with zero missing methods.""" - print("Generating COMPLETE Markdown API documentation...") - - # Generate markdown - markdown_content = generate_complete_markdown_documentation() - - # Write to file - output_path = Path("docs/API_REFERENCE_COMPLETE.md") - output_path.parent.mkdir(exist_ok=True) - - with open(output_path, 'w', encoding='utf-8') as f: - f.write(markdown_content) - - print(f"✓ Generated {output_path}") - print(f" File size: {len(markdown_content):,} bytes") - - # Count "..." instances - ellipsis_count = markdown_content.count('...') - print(f" Ellipsis instances: {ellipsis_count}") - - if ellipsis_count == 0: - print("✅ SUCCESS: No missing documentation found!") - else: - print(f"❌ WARNING: {ellipsis_count} methods still need documentation") - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/tools/generate_dynamic_docs.py b/tools/generate_dynamic_docs.py index 92e65cc..426fdcd 100644 --- a/tools/generate_dynamic_docs.py +++ b/tools/generate_dynamic_docs.py @@ -12,6 +12,67 @@ import html import re from pathlib import Path +def transform_doc_links(docstring, format='html', base_url=''): + """Transform MCRF_LINK patterns based on output format. + + Detects pattern: "See also: TEXT (docs/path.md)" + Transforms to appropriate format for output type. + + For HTML/web formats, properly escapes content before inserting HTML tags. + """ + if not docstring: + return docstring + + link_pattern = r'See also: ([^(]+) \(([^)]+)\)' + + def replace_link(match): + text, ref = match.group(1).strip(), match.group(2).strip() + + if format == 'html': + # Convert docs/foo.md → foo.html and escape for safe HTML + href = html.escape(ref.replace('docs/', '').replace('.md', '.html'), quote=True) + text_escaped = html.escape(text) + return f'

See also: {text_escaped}

' + + elif format == 'web': + # Link to hosted docs and escape for safe HTML + web_path = ref.replace('docs/', '').replace('.md', '') + href = html.escape(f"{base_url}/{web_path}", quote=True) + text_escaped = html.escape(text) + return f'

See also: {text_escaped}

' + + elif format == 'markdown': + # Markdown link + return f'\n**See also:** [{text}]({ref})' + + else: # 'python' or default + # Keep as plain text for Python docstrings + return match.group(0) + + # For HTML formats, escape the entire docstring first, then process links + if format in ('html', 'web'): + # Split by the link pattern, escape non-link parts, then reassemble + parts = [] + last_end = 0 + + for match in re.finditer(link_pattern, docstring): + # Escape the text before this match + if match.start() > last_end: + parts.append(html.escape(docstring[last_end:match.start()])) + + # Process the link (replace_link handles escaping internally) + parts.append(replace_link(match)) + last_end = match.end() + + # Escape any remaining text after the last match + if last_end < len(docstring): + parts.append(html.escape(docstring[last_end:])) + + return ''.join(parts) + else: + # For non-HTML formats, just do simple replacement + return re.sub(link_pattern, replace_link, docstring) + # Must be run with McRogueFace as interpreter try: import mcrfpy @@ -304,8 +365,10 @@ def generate_html_docs(): html_content += f"""

{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}

-

{html.escape(parsed['description'])}

""" + if parsed['description']: + description = transform_doc_links(parsed['description'], format='html') + html_content += f"

{description}

\n" if parsed['args']: html_content += "

Arguments:

\n
    \n" @@ -361,9 +424,10 @@ def generate_html_docs():
    {method_name}{parsed['signature'] if parsed['signature'] else '(...)'}
    """ - + if parsed['description']: - html_content += f"

    {html.escape(parsed['description'])}

    \n" + description = transform_doc_links(parsed['description'], format='html') + html_content += f"

    {description}

    \n" if parsed['args']: html_content += "
    \n" @@ -429,9 +493,10 @@ def generate_markdown_docs(): parsed = func_info["parsed"] md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n" - + if parsed['description']: - md_content += f"{parsed['description']}\n\n" + description = transform_doc_links(parsed['description'], format='markdown') + md_content += f"{description}\n\n" if parsed['args']: md_content += "**Arguments:**\n" @@ -479,9 +544,10 @@ def generate_markdown_docs(): parsed = method_info['parsed'] md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n" - + if parsed['description']: - md_content += f"{parsed['description']}\n\n" + description = transform_doc_links(parsed['description'], format='markdown') + md_content += f"{description}\n\n" if parsed['args']: md_content += "**Arguments:**\n" diff --git a/tools/test_link_transform.py b/tools/test_link_transform.py new file mode 100644 index 0000000..37e3b05 --- /dev/null +++ b/tools/test_link_transform.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Test script for link transformation function.""" + +import re + +def transform_doc_links(docstring, format='html', base_url=''): + """Transform MCRF_LINK patterns based on output format. + + Detects pattern: "See also: TEXT (docs/path.md)" + Transforms to appropriate format for output type. + """ + if not docstring: + return docstring + + link_pattern = r'See also: ([^(]+) \(([^)]+)\)' + + def replace_link(match): + text, ref = match.group(1).strip(), match.group(2).strip() + + if format == 'html': + # Convert docs/foo.md → foo.html + href = ref.replace('docs/', '').replace('.md', '.html') + return f'

    See also: {text}

    ' + + elif format == 'web': + # Link to hosted docs + web_path = ref.replace('docs/', '').replace('.md', '') + return f'

    See also: {text}

    ' + + elif format == 'markdown': + # Markdown link + return f'\n**See also:** [{text}]({ref})' + + else: # 'python' or default + # Keep as plain text for Python docstrings + return match.group(0) + + return re.sub(link_pattern, replace_link, docstring) + +# Test cases +test_doc = "Description text.\n\nSee also: Tutorial Guide (docs/guide.md)\n\nMore text." + +html_result = transform_doc_links(test_doc, format='html') +print("HTML:", html_result) +assert 'Tutorial Guide' in html_result + +md_result = transform_doc_links(test_doc, format='markdown') +print("Markdown:", md_result) +assert '[Tutorial Guide](docs/guide.md)' in md_result + +plain_result = transform_doc_links(test_doc, format='python') +print("Python:", plain_result) +assert 'See also: Tutorial Guide (docs/guide.md)' in plain_result + +print("\nSUCCESS: All transformations work correctly") diff --git a/tools/test_vector_docs.py b/tools/test_vector_docs.py new file mode 100644 index 0000000..bf07e47 --- /dev/null +++ b/tools/test_vector_docs.py @@ -0,0 +1,39 @@ +import mcrfpy +import sys + +# Check Vector.magnitude docstring +mag_doc = mcrfpy.Vector.magnitude.__doc__ +print("magnitude doc:", mag_doc) +assert "magnitude()" in mag_doc +assert "Calculate the length/magnitude" in mag_doc +assert "Returns:" in mag_doc + +# Check Vector.dot docstring +dot_doc = mcrfpy.Vector.dot.__doc__ +print("dot doc:", dot_doc) +assert "dot(other: Vector)" in dot_doc +assert "Args:" in dot_doc +assert "other:" in dot_doc + +# Check Vector.normalize docstring +normalize_doc = mcrfpy.Vector.normalize.__doc__ +print("normalize doc:", normalize_doc) +assert "normalize()" in normalize_doc +assert "Return a unit vector" in normalize_doc +assert "Returns:" in normalize_doc +assert "Note:" in normalize_doc + +# Check Vector.x property docstring +x_doc = mcrfpy.Vector.x.__doc__ +print("x property doc:", x_doc) +assert "X coordinate of the vector" in x_doc +assert "float" in x_doc + +# Check Vector.y property docstring +y_doc = mcrfpy.Vector.y.__doc__ +print("y property doc:", y_doc) +assert "Y coordinate of the vector" in y_doc +assert "float" in y_doc + +print("SUCCESS: All docstrings present and complete") +sys.exit(0)