diff --git a/.gitignore b/.gitignore index edae67d..39c46e1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,9 +33,6 @@ __lib_windows/ build-windows/ build_windows/ _oldscripts/ - -# Audit tooling virtualenv (tools/audit_pymethoddef.py) -.venv-audit/ assets/ cellular_automata_fire/ deps/ diff --git a/ROADMAP.md b/ROADMAP.md index 9fa4178..9468f3a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # McRogueFace - Development Roadmap -**Version**: 0.2.7-prerelease | **Era**: McRogueFace (2D roguelikes) -- on the road to 1.0 +**Version**: 0.2.6-prerelease | **Era**: McRogueFace (2D roguelikes) For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap). @@ -10,7 +10,7 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi **Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving. -**0.2 series** (Jan-Mar 2026) -- Weekly updates to GitHub. Key additions: +**0.2 series** (Jan-Feb 2026) -- Weekly updates to GitHub. Key additions: - 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization - Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap - Tiled and LDtk import with Wang tile / AutoRule resolution @@ -19,52 +19,37 @@ For detailed architecture, philosophy, and decision framework, see the [Strategi - Multi-layer grid system with chunk-based rendering and dirty-flag caching - Documentation macro system with auto-generated API docs, man pages, and type stubs - Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio -- Behavior/Trigger turn manager: `grid.step()`, entity labels, `cell_pos`, Dijkstra-backed pathfinding (#295-#303) -**Proving grounds**: Crypt of Sokoban (7DRL 2025), then 7DRL 2026 -- both shipped on the same engine. The 2026 jam surfaced hotfix-worthy issues (SDL key scancodes, composite textures) that have since landed on master. +**Proving grounds**: Crypt of Sokoban (7DRL 2025) was the first complete game. 7DRL 2026 is the current target. --- -## Current Focus: API Freeze + Memory Safety Sweep +## Current Focus: 7DRL 2026 -7DRL 2026 is behind us (Feb 28 -- Mar 8). The engine has two concurrent tracks to 1.0: +**Dates**: February 28 -- March 8, 2026 -### Track 1: API Freeze -The process is underway. Closed in this pass: camelCase module functions (#304), deprecated `sprite_number` (#305), legacy string enum comparisons (#306), `Color.__eq__`/`__ne__` (#307), `Grid.position` alias (#308). +Engine preparation is complete. All 2D systems are production-ready. The jam will expose remaining rough edges in the workflow of building a complete game on McRogueFace. -Remaining freeze work: -1. Catalog every public Python class, method, and property -- audit against `stubs/mcrfpy.pyi` and generated docs -2. Identify any last naming/signature/default changes before committing -3. Final breaking-change pass, bundled +Open prep items: +- **#248** -- Crypt of Sokoban Remaster (game content for the jam) + +--- + +## Post-7DRL: The Road to 1.0 + +After 7DRL, the priority shifts from feature development to **API stability**. 1.0 means the Python API is frozen: documented, stable, and not going to break. + +### API Freeze Process +1. Catalog every public Python class, method, and property +2. Identify anything that should change before committing (naming, signatures, defaults) +3. Make breaking changes in a single coordinated pass 4. Document the stable API as the contract -5. Experimental modules (3D/Voxel) stay out of the freeze with an `experimental` label +5. Experimental modules (3D/Voxel) get an explicit `experimental` label and are exempt from the freeze -### Track 2: Fuzz-Driven Bug Sweep -The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing (W1), native harness (W2/W3), then six targeted fuzzers under `tests/fuzz/`: -- `fuzz_grid_entity` -- EntityCollection lifetime (W4, fixed #258-#263, #273, #274) -- `fuzz_property_types` -- refcount / type confusion (W5, fixed #267, #268, #272) -- `fuzz_anim_timer_scene` -- animation/timer/scene lifecycles (W6) -- `fuzz_fov` -- compute_fov parameters (W8, fixed #310) -- `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7) -- `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311) - -The active tier1 queue is empty. The last three findings (#309 Caption float→uint, #310 FOV enum, #311 DijkstraMap OOB) all landed on master in mid-April. Coverage extension to remaining public API surface is tracked under #312. - -### Recently Shipped (April 2026) -- **#294** -- `entity.perspective_map` replaces flat `vector` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered. -- **#315** -- Pathfinding API extended with built-in heuristics (Euclidean/Manhattan/Chebyshev/Diagonal/Zero), multi-root Dijkstra, FLEE primitives (invert + descent), and an interactive demo. EntityBehavior SEEK/FLEE refactored to a `PathProvider` strategy. -- **Phase 5.2** -- six performance benchmark scripts under `tests/benchmarks/` covering grid.step(), FOV writeback cost, spatial hash vs. O(n), pathfinding with collision labels, multi-GridView render, and Dijkstra variants. Baselines under `tests/benchmarks/baseline/phase5_2/`. -- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source. - -### Active Follow-Ups -- **#312** Extend fuzz coverage to remaining public API surface -- **#313** Migrate `UIEntity::grid` from `shared_ptr` to `shared_ptr` (post-#252 refactor cleanup) -- **#314** API audit follow-through: close gaps from `docs/api-audit-2026-04.md` -- **#316** Sparse perspective writeback in `UIEntity::updateVisibility` (Phase 5.2 finding: full-grid demote+promote dominates over TCOD FOV cost) - -### Other Post-7DRL Priorities +### Post-Jam Priorities +- Fix pain points discovered during actual 7DRL game development - Progress on the r/roguelikedev tutorial series (#167) -- Complete the API freeze catalog pass (#314) +- API consistency audit and freeze - Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter --- @@ -113,18 +98,15 @@ Rather than inverting the architecture to make McRogueFace a pip-installable pac ## Open Issues by Area -25 open issues across the tracker. Key groupings: +30 open issues across the tracker. Key groupings: -- **Recent follow-ups** (#312, #313, #314, #316) -- Fuzz coverage extension, UIEntity grid refactor, API audit follow-through, sparse perspective writeback -- **7DRL 2026 carry-over** (#248) -- Crypt of Sokoban remaster, superseded by the 7DRL 2026 entry but still relevant as a demo -- **Tooling / infrastructure** (#282, #255) -- Modern Clang for TSan/fuzzing, performance profiling -- **Demos / tutorials** (#167, #154, #156, #55) -- r/roguelikedev series, LLM agent simulations -- **Grid enhancements** (#152, #67) -- Sparse layers, infinite worlds +- **Multi-tile entities** (#233-#237) -- Oversized sprites, composite entities, origin offsets +- **Grid enhancements** (#152, #149, #67) -- Sparse layers, refactoring, infinite worlds - **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse +- **LLM agent testbed** (#154, #156, #55) -- Multi-agent simulation, turn-based orchestration - **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods -- **WASM tooling** (#239) -- Automated browser testing -- **Rendering** (#107) -- Particle system -- **Deferred** (#220, #46, #45) -- Subinterpreter support / tests, accessibility modes +- **WASM tooling** (#238-#240) -- Debug infrastructure, automated browser testing, troubleshooting docs +- **Rendering** (#107, #218) -- Particle system, Color/Vector animation targets See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status. diff --git a/docs/API_REFERENCE_DYNAMIC.md b/docs/API_REFERENCE_DYNAMIC.md index 26b7299..9c225ec 100644 --- a/docs/API_REFERENCE_DYNAMIC.md +++ b/docs/API_REFERENCE_DYNAMIC.md @@ -1,6 +1,6 @@ # McRogueFace API Reference -*Generated on 2026-04-18 13:35:02* +*Generated on 2026-04-18 07:28:57* *This documentation was dynamically generated from the compiled module.* @@ -10,6 +10,7 @@ - [Classes](#classes) - [AStarPath](#astarpath) - [Alignment](#alignment) + - [Animation](#animation) - [Arc](#arc) - [AutoRuleSet](#autoruleset) - [BSP](#bsp) @@ -34,7 +35,6 @@ - [Grid](#grid) - [GridView](#gridview) - [HeightMap](#heightmap) - - [Heuristic](#heuristic) - [InputState](#inputstate) - [Key](#key) - [Keyboard](#keyboard) @@ -356,6 +356,122 @@ Return an array of bytes representing an integer. If signed is False and a negative integer is given, an OverflowError is raised. +### Animation + +Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None) + +Create an animation that interpolates a property value over time. + +Args: + property: Property name to animate. Valid properties depend on target type: + - Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size' + - Appearance: 'fill_color', 'outline_color', 'outline', 'opacity' + - Sprite: 'sprite_index', 'scale' + - Grid: 'center', 'zoom' + - Caption: 'text' + - Sub-properties: 'fill_color.r', 'fill_color.g', 'fill_color.b', 'fill_color.a' + target: Target value for the animation. Type depends on property: + - float: For numeric properties (x, y, w, h, scale, opacity, zoom) + - int: For integer properties (sprite_index) + - tuple (r, g, b[, a]): For color properties + - tuple (x, y): For vector properties (pos, size, center) + - list[int]: For sprite animation sequences + - str: For text animation + duration: Animation duration in seconds. + easing: Easing function name. Options: + - 'linear' (default) + - 'easeIn', 'easeOut', 'easeInOut' + - 'easeInQuad', 'easeOutQuad', 'easeInOutQuad' + - 'easeInCubic', 'easeOutCubic', 'easeInOutCubic' + - 'easeInQuart', 'easeOutQuart', 'easeInOutQuart' + - 'easeInSine', 'easeOutSine', 'easeInOutSine' + - 'easeInExpo', 'easeOutExpo', 'easeInOutExpo' + - 'easeInCirc', 'easeOutCirc', 'easeInOutCirc' + - 'easeInElastic', 'easeOutElastic', 'easeInOutElastic' + - 'easeInBack', 'easeOutBack', 'easeInOutBack' + - 'easeInBounce', 'easeOutBounce', 'easeInOutBounce' + delta: If True, target is relative to start value (additive). Default False. + loop: If True, animation repeats from start when it reaches the end. Default False. + callback: Function(target, property, value) called when animation completes. + Not called for looping animations (since they never complete). + +Example: + # Move a frame from current position to x=500 over 2 seconds + anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut') + anim.start(my_frame) + + # Looping sprite animation + walk = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.6, loop=True) + walk.start(my_sprite) + + +**Properties:** +- `duration` *(read-only)*: Animation duration in seconds (float, read-only). Total time for the animation to complete. +- `elapsed` *(read-only)*: Elapsed time in seconds (float, read-only). Time since the animation started. +- `is_complete` *(read-only)*: Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called. +- `is_delta` *(read-only)*: Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value. +- `is_looping` *(read-only)*: Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end. +- `property` *(read-only)*: Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index'). + +**Methods:** + +#### `complete() -> None` + +Complete the animation immediately by jumping to the final value. + +Note: + +**Returns:** None Sets elapsed = duration and applies target value immediately. Completion callback will be called if set. + +#### `get_current_value() -> Any` + +Get the current interpolated value of the animation. + +Note: + +**Returns:** Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str) 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). + +#### `hasValidTarget() -> bool` + +Check if the animation still has a valid target. + +Note: + +**Returns:** bool: True if the target still exists, False if it was destroyed Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed. + +#### `start(target: UIDrawable, conflict_mode: str = 'replace') -> None` + +Start the animation on a target UI element. + +Note: + +**Arguments:** +- `target`: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity) +- `conflict_mode`: How to handle conflicts if property is already animating: 'replace' (default) - complete existing animation and start new one; 'queue' - wait for existing animation to complete; 'error' - raise RuntimeError if property is busy + +**Returns:** None + +**Raises:** RuntimeError: When conflict_mode='error' and property is already animating The animation will automatically stop if the target is destroyed. + +#### `stop() -> None` + +Stop the animation without completing it. + +Note: + +**Returns:** None Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. The animation is simply cancelled and will be removed from the AnimationManager. + +#### `update(delta_time: float) -> bool` + +Update the animation by the given time delta. + +Note: + +**Arguments:** +- `delta_time`: Time elapsed since last update in seconds + +**Returns:** bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control. + ### Arc *Inherits from: Drawable* @@ -1302,18 +1418,6 @@ Example: **Methods:** -#### `descent_step(pos) -> Vector | None` - -Get the adjacent cell with the lowest distance (steepest descent). -Unlike step_from (which follows the path set by path_from), descent_step -always returns the best neighbor in a single hop. Useful for AI that -reacts to the current distance field rather than following a fixed path. - -**Arguments:** -- `pos`: Current position as Vector, Entity, or (x, y) tuple. - -**Returns:** Next position as Vector, or None if pos is a local minimum or off-grid. - #### `distance(pos) -> float | None` Get distance from position to root. @@ -1323,16 +1427,6 @@ Get distance from position to root. **Returns:** Float distance, or None if position is unreachable. -#### `invert() -> DijkstraMap` - -Return a NEW DijkstraMap whose distance field is the safety field. -Cells near a root become high values and cells far from any root become -low values. Combined with step_from or descent_step, this gives flee -behavior: descend the inverted map to move away from the original roots. -The original DijkstraMap is unchanged. - -**Returns:** New DijkstraMap with inverted distances. - #### `path_from(pos) -> AStarPath` Get full path from position to root. @@ -2958,99 +3052,6 @@ Return NEW HeightMap with uniform value where in range, 0.0 elsewhere. **Raises:** ValueError: min > max -### Heuristic - -*Inherits from: IntEnum* - -Built-in A* heuristic function selector. - -Values: - EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default. - MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids. - CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1). - DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)). - ZERO: Always returns 0. A* degenerates to Dijkstra. - - -**Properties:** -- `denominator`: the denominator of a rational number in lowest terms -- `imag`: the imaginary part of a complex number -- `numerator`: the numerator of a rational number in lowest terms -- `real`: the real part of a complex number - -**Methods:** - -#### `as_integer_ratio(...)` - -Return a pair of integers, whose ratio is equal to the original int. -The ratio is in lowest terms and has a positive denominator. ->>> (10).as_integer_ratio() -(10, 1) ->>> (-10).as_integer_ratio() -(-10, 1) ->>> (0).as_integer_ratio() -(0, 1) - -#### `bit_count(...)` - -Number of ones in the binary representation of the absolute value of self. -Also known as the population count. ->>> bin(13) -'0b1101' ->>> (13).bit_count() -3 - -#### `bit_length(...)` - -Number of bits necessary to represent self in binary. ->>> bin(37) -'0b100101' ->>> (37).bit_length() -6 - -#### `conjugate(...)` - -Returns self, the complex conjugate of any int. - -#### `from_bytes(...)` - -Return the integer represented by the given array of bytes. - bytes - Holds the array of bytes to convert. The argument must either - support the buffer protocol or be an iterable object producing bytes. - Bytes and bytearray are examples of built-in objects that support the - buffer protocol. - byteorder - The byte order used to represent the integer. If byteorder is 'big', - the most significant byte is at the beginning of the byte array. If - byteorder is 'little', the most significant byte is at the end of the - byte array. To request the native byte order of the host system, use - sys.byteorder as the byte order value. Default is to use 'big'. - signed - Indicates whether two's complement is used to represent the integer. - -#### `is_integer(...)` - -Returns True. Exists for duck type compatibility with float.is_integer. - -#### `to_bytes(...)` - -Return an array of bytes representing an integer. - length - Length of bytes object to use. An OverflowError is raised if the - integer is not representable with the given number of bytes. Default - is length 1. - byteorder - The byte order used to represent the integer. If byteorder is 'big', - the most significant byte is at the beginning of the byte array. If - byteorder is 'little', the most significant byte is at the end of the - byte array. To request the native byte order of the host system, use - sys.byteorder as the byte order value. Default is to use 'big'. - signed - Determines whether two's complement is used to represent the integer. - If signed is False and a negative integer is given, an OverflowError - is raised. - ### InputState *Inherits from: IntEnum* diff --git a/docs/api_reference_dynamic.html b/docs/api_reference_dynamic.html index f33bb90..48b4abd 100644 --- a/docs/api_reference_dynamic.html +++ b/docs/api_reference_dynamic.html @@ -108,7 +108,7 @@

McRogueFace API Reference

-

Generated on 2026-04-18 13:35:02

+

Generated on 2026-04-18 07:28:57

This documentation was dynamically generated from the compiled module.

@@ -119,6 +119,7 @@
+
+

Animation

+

Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None) + +Create an animation that interpolates a property value over time. + +Args: + property: Property name to animate. Valid properties depend on target type: + - Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size' + - Appearance: 'fill_color', 'outline_color', 'outline', 'opacity' + - Sprite: 'sprite_index', 'scale' + - Grid: 'center', 'zoom' + - Caption: 'text' + - Sub-properties: 'fill_color.r', 'fill_color.g', 'fill_color.b', 'fill_color.a' + target: Target value for the animation. Type depends on property: + - float: For numeric properties (x, y, w, h, scale, opacity, zoom) + - int: For integer properties (sprite_index) + - tuple (r, g, b[, a]): For color properties + - tuple (x, y): For vector properties (pos, size, center) + - list[int]: For sprite animation sequences + - str: For text animation + duration: Animation duration in seconds. + easing: Easing function name. Options: + - 'linear' (default) + - 'easeIn', 'easeOut', 'easeInOut' + - 'easeInQuad', 'easeOutQuad', 'easeInOutQuad' + - 'easeInCubic', 'easeOutCubic', 'easeInOutCubic' + - 'easeInQuart', 'easeOutQuart', 'easeInOutQuart' + - 'easeInSine', 'easeOutSine', 'easeInOutSine' + - 'easeInExpo', 'easeOutExpo', 'easeInOutExpo' + - 'easeInCirc', 'easeOutCirc', 'easeInOutCirc' + - 'easeInElastic', 'easeOutElastic', 'easeInOutElastic' + - 'easeInBack', 'easeOutBack', 'easeInOutBack' + - 'easeInBounce', 'easeOutBounce', 'easeInOutBounce' + delta: If True, target is relative to start value (additive). Default False. + loop: If True, animation repeats from start when it reaches the end. Default False. + callback: Function(target, property, value) called when animation completes. + Not called for looping animations (since they never complete). + +Example: + # Move a frame from current position to x=500 over 2 seconds + anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut') + anim.start(my_frame) + + # Looping sprite animation + walk = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.6, loop=True) + walk.start(my_sprite) +

+

Properties:

+
    +
  • duration (read-only): Animation duration in seconds (float, read-only). Total time for the animation to complete.
  • +
  • elapsed (read-only): Elapsed time in seconds (float, read-only). Time since the animation started.
  • +
  • is_complete (read-only): Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called.
  • +
  • is_delta (read-only): Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value.
  • +
  • is_looping (read-only): Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end.
  • +
  • property (read-only): Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index').
  • +
+

Methods:

+ +
+
complete() -> None
+

Complete the animation immediately by jumping to the final value. + +Note:

+

Returns: None Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.

+
+ +
+
get_current_value() -> Any
+

Get the current interpolated value of the animation. + +Note:

+

Returns: Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str) 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).

+
+ +
+
hasValidTarget() -> bool
+

Check if the animation still has a valid target. + +Note:

+

Returns: bool: True if the target still exists, False if it was destroyed Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.

+
+ +
+
start(target: UIDrawable, conflict_mode: str = 'replace') -> None
+

Start the animation on a target UI element. + +Note:

+
+
target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)
+
conflict_mode: How to handle conflicts if property is already animating: 'replace' (default) - complete existing animation and start new one; 'queue' - wait for existing animation to complete; 'error' - raise RuntimeError if property is busy
+
+

Returns: None

+

Raises: RuntimeError: When conflict_mode='error' and property is already animating The animation will automatically stop if the target is destroyed.

+
+ +
+
stop() -> None
+

Stop the animation without completing it. + +Note:

+

Returns: None Unlike complete(), this does NOT apply the final value and does NOT trigger the callback. The animation is simply cancelled and will be removed from the AnimationManager.

+
+ +
+
update(delta_time: float) -> bool
+

Update the animation by the given time delta. + +Note:

+
+
delta_time: Time elapsed since last update in seconds
+
+

Returns: bool: True if animation is still running, False if complete Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.

+
+
+

Arc

Inherits from: Drawable

@@ -1451,18 +1567,6 @@ Example:

Methods:

-
-
descent_step(pos) -> Vector | None
-

Get the adjacent cell with the lowest distance (steepest descent). -Unlike step_from (which follows the path set by path_from), descent_step -always returns the best neighbor in a single hop. Useful for AI that -reacts to the current distance field rather than following a fixed path.

-
-
pos: Current position as Vector, Entity, or (x, y) tuple.
-
-

Returns: Next position as Vector, or None if pos is a local minimum or off-grid.

-
-
distance(pos) -> float | None

Get distance from position to root.

@@ -1472,16 +1576,6 @@ reacts to the current distance field rather than following a fixed path.

Returns: Float distance, or None if position is unreachable.

-
-
invert() -> DijkstraMap
-

Return a NEW DijkstraMap whose distance field is the safety field. -Cells near a root become high values and cells far from any root become -low values. Combined with step_from or descent_step, this gives flee -behavior: descend the inverted map to move away from the original roots. -The original DijkstraMap is unchanged.

-

Returns: New DijkstraMap with inverted distances.

-
-
path_from(pos) -> AStarPath

Get full path from position to root.

@@ -3153,106 +3247,6 @@ Note:

-
-

Heuristic

-

Inherits from: IntEnum

-

Built-in A* heuristic function selector. - -Values: - EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default. - MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids. - CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1). - DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)). - ZERO: Always returns 0. A* degenerates to Dijkstra. -

-

Properties:

-
    -
  • denominator: the denominator of a rational number in lowest terms
  • -
  • imag: the imaginary part of a complex number
  • -
  • numerator: the numerator of a rational number in lowest terms
  • -
  • real: the real part of a complex number
  • -
-

Methods:

- -
-
as_integer_ratio(...)
-

Return a pair of integers, whose ratio is equal to the original int. -The ratio is in lowest terms and has a positive denominator. ->>> (10).as_integer_ratio() -(10, 1) ->>> (-10).as_integer_ratio() -(-10, 1) ->>> (0).as_integer_ratio() -(0, 1)

-
- -
-
bit_count(...)
-

Number of ones in the binary representation of the absolute value of self. -Also known as the population count. ->>> bin(13) -'0b1101' ->>> (13).bit_count() -3

-
- -
-
bit_length(...)
-

Number of bits necessary to represent self in binary. ->>> bin(37) -'0b100101' ->>> (37).bit_length() -6

-
- -
-
conjugate(...)
-

Returns self, the complex conjugate of any int.

-
- -
-
from_bytes(...)
-

Return the integer represented by the given array of bytes. - bytes - Holds the array of bytes to convert. The argument must either - support the buffer protocol or be an iterable object producing bytes. - Bytes and bytearray are examples of built-in objects that support the - buffer protocol. - byteorder - The byte order used to represent the integer. If byteorder is 'big', - the most significant byte is at the beginning of the byte array. If - byteorder is 'little', the most significant byte is at the end of the - byte array. To request the native byte order of the host system, use - sys.byteorder as the byte order value. Default is to use 'big'. - signed - Indicates whether two's complement is used to represent the integer.

-
- -
-
is_integer(...)
-

Returns True. Exists for duck type compatibility with float.is_integer.

-
- -
-
to_bytes(...)
-

Return an array of bytes representing an integer. - length - Length of bytes object to use. An OverflowError is raised if the - integer is not representable with the given number of bytes. Default - is length 1. - byteorder - The byte order used to represent the integer. If byteorder is 'big', - the most significant byte is at the beginning of the byte array. If - byteorder is 'little', the most significant byte is at the end of the - byte array. To request the native byte order of the host system, use - sys.byteorder as the byte order value. Default is to use 'big'. - signed - Determines whether two's complement is used to represent the integer. - If signed is False and a negative integer is given, an OverflowError - is raised.

-
-
-

InputState

Inherits from: IntEnum

diff --git a/docs/mcrfpy.3 b/docs/mcrfpy.3 index 0008515..a9f78ab 100644 --- a/docs/mcrfpy.3 +++ b/docs/mcrfpy.3 @@ -14,11 +14,11 @@ . ftr VB CB . ftr VBI CBI .\} -.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-93-g0f7254e" "" +.TH "MCRFPY" "3" "2026-04-18" "McRogueFace 0.2.7-prerelease-7drl2026-79-g59e7221" "" .hy .SH McRogueFace API Reference .PP -\f[I]Generated on 2026-04-18 13:35:02\f[R] +\f[I]Generated on 2026-04-18 07:28:57\f[R] .PP \f[I]This documentation was dynamically generated from the compiled module.\f[R] @@ -33,6 +33,8 @@ AStarPath .IP \[bu] 2 Alignment .IP \[bu] 2 +Animation +.IP \[bu] 2 Arc .IP \[bu] 2 AutoRuleSet @@ -81,8 +83,6 @@ GridView .IP \[bu] 2 HeightMap .IP \[bu] 2 -Heuristic -.IP \[bu] 2 InputState .IP \[bu] 2 Key @@ -436,6 +436,146 @@ signed Determines whether two\[cq]s complement is used to represent the integer. If signed is False and a negative integer is given, an OverflowError is raised. +.SS Animation +.PP +Animation(property: str, target: Any, duration: float, easing: str = +`linear', delta: bool = False, loop: bool = False, callback: Callable = +None) +.PP +Create an animation that interpolates a property value over time. +.PP +Args: property: Property name to animate. +Valid properties depend on target type: - Position/Size: `x', `y', `w', +`h', `pos', `size' - Appearance: `fill_color', `outline_color', +`outline', `opacity' - Sprite: `sprite_index', `scale' - Grid: `center', +`zoom' - Caption: `text' - Sub-properties: `fill_color.r', +`fill_color.g', `fill_color.b', `fill_color.a' target: Target value for +the animation. +Type depends on property: - float: For numeric properties (x, y, w, h, +scale, opacity, zoom) - int: For integer properties (sprite_index) - +tuple (r, g, b[, a]): For color properties - tuple (x, y): For vector +properties (pos, size, center) - list[int]: For sprite animation +sequences - str: For text animation duration: Animation duration in +seconds. +easing: Easing function name. +Options: - `linear' (default) - `easeIn', `easeOut', `easeInOut' - +`easeInQuad', `easeOutQuad', `easeInOutQuad' - `easeInCubic', +`easeOutCubic', `easeInOutCubic' - `easeInQuart', `easeOutQuart', +`easeInOutQuart' - `easeInSine', `easeOutSine', `easeInOutSine' - +`easeInExpo', `easeOutExpo', `easeInOutExpo' - `easeInCirc', +`easeOutCirc', `easeInOutCirc' - `easeInElastic', `easeOutElastic', +`easeInOutElastic' - `easeInBack', `easeOutBack', `easeInOutBack' - +`easeInBounce', `easeOutBounce', `easeInOutBounce' delta: If True, +target is relative to start value (additive). +Default False. +loop: If True, animation repeats from start when it reaches the end. +Default False. +callback: Function(target, property, value) called when animation +completes. +Not called for looping animations (since they never complete). +.PP +Example: # Move a frame from current position to x=500 over 2 seconds +anim = mcrfpy.Animation(`x', 500.0, 2.0, `easeInOut') +anim.start(my_frame) +.IP +.nf +\f[C] +# Looping sprite animation +walk = mcrfpy.Animation(\[aq]sprite_index\[aq], [0,1,2,3,2,1], 0.6, loop=True) +walk.start(my_sprite) +\f[R] +.fi +.PP +\f[B]Properties:\f[R] - \f[V]duration\f[R] \f[I](read-only)\f[R]: +Animation duration in seconds (float, read-only). +Total time for the animation to complete. +- \f[V]elapsed\f[R] \f[I](read-only)\f[R]: Elapsed time in seconds +(float, read-only). +Time since the animation started. +- \f[V]is_complete\f[R] \f[I](read-only)\f[R]: Whether animation is +complete (bool, read-only). +True when elapsed >= duration or complete() was called. +- \f[V]is_delta\f[R] \f[I](read-only)\f[R]: Whether animation uses delta +mode (bool, read-only). +In delta mode, the target value is added to the starting value. +- \f[V]is_looping\f[R] \f[I](read-only)\f[R]: Whether animation loops +(bool, read-only). +Looping animations repeat from the start when they reach the end. +- \f[V]property\f[R] \f[I](read-only)\f[R]: Target property name (str, +read-only). +The property being animated (e.g., `pos', `opacity', `sprite_index'). +.PP +\f[B]Methods:\f[R] +.SS \f[V]complete() -> None\f[R] +.PP +Complete the animation immediately by jumping to the final value. +.PP +Note: +.PP +\f[B]Returns:\f[R] None Sets elapsed = duration and applies target value +immediately. +Completion callback will be called if set. +.SS \f[V]get_current_value() -> Any\f[R] +.PP +Get the current interpolated value of the animation. +.PP +Note: +.PP +\f[B]Returns:\f[R] Any: Current value (type depends on property: float, +int, Color tuple, Vector tuple, or str) 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). +.SS \f[V]hasValidTarget() -> bool\f[R] +.PP +Check if the animation still has a valid target. +.PP +Note: +.PP +\f[B]Returns:\f[R] bool: True if the target still exists, False if it +was destroyed Animations automatically clean up when targets are +destroyed. +Use this to check if manual cleanup is needed. +.SS \f[V]start(target: UIDrawable, conflict_mode: str = \[aq]replace\[aq]) -> None\f[R] +.PP +Start the animation on a target UI element. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]target\f[R]: The UI element to animate +(Frame, Caption, Sprite, Grid, or Entity) - \f[V]conflict_mode\f[R]: How +to handle conflicts if property is already animating: `replace' +(default) - complete existing animation and start new one; `queue' - +wait for existing animation to complete; `error' - raise RuntimeError if +property is busy +.PP +\f[B]Returns:\f[R] None +.PP +\f[B]Raises:\f[R] RuntimeError: When conflict_mode=`error' and property +is already animating The animation will automatically stop if the target +is destroyed. +.SS \f[V]stop() -> None\f[R] +.PP +Stop the animation without completing it. +.PP +Note: +.PP +\f[B]Returns:\f[R] None Unlike complete(), this does NOT apply the final +value and does NOT trigger the callback. +The animation is simply cancelled and will be removed from the +AnimationManager. +.SS \f[V]update(delta_time: float) -> bool\f[R] +.PP +Update the animation by the given time delta. +.PP +Note: +.PP +\f[B]Arguments:\f[R] - \f[V]delta_time\f[R]: Time elapsed since last +update in seconds +.PP +\f[B]Returns:\f[R] bool: True if animation is still running, False if +complete Typically called by AnimationManager automatically. +Manual calls only needed for custom animation control. .SS Arc .PP \f[I]Inherits from: Drawable\f[R] @@ -1462,19 +1602,6 @@ enemies: dist = dijkstra.distance(enemy.pos) if dist and dist < 10: step position that distances are measured from (Vector, read-only). .PP \f[B]Methods:\f[R] -.SS \f[V]descent_step(pos) -> Vector | None\f[R] -.PP -Get the adjacent cell with the lowest distance (steepest descent). -Unlike step_from (which follows the path set by path_from), descent_step -always returns the best neighbor in a single hop. -Useful for AI that reacts to the current distance field rather than -following a fixed path. -.PP -\f[B]Arguments:\f[R] - \f[V]pos\f[R]: Current position as Vector, -Entity, or (x, y) tuple. -.PP -\f[B]Returns:\f[R] Next position as Vector, or None if pos is a local -minimum or off-grid. .SS \f[V]distance(pos) -> float | None\f[R] .PP Get distance from position to root. @@ -1483,16 +1610,6 @@ Get distance from position to root. y) tuple. .PP \f[B]Returns:\f[R] Float distance, or None if position is unreachable. -.SS \f[V]invert() -> DijkstraMap\f[R] -.PP -Return a NEW DijkstraMap whose distance field is the safety field. -Cells near a root become high values and cells far from any root become -low values. -Combined with step_from or descent_step, this gives flee behavior: -descend the inverted map to move away from the original roots. -The original DijkstraMap is unchanged. -.PP -\f[B]Returns:\f[R] New DijkstraMap with inverted distances. .SS \f[V]path_from(pos) -> AStarPath\f[R] .PP Get full path from position to root. @@ -3190,90 +3307,6 @@ or list, inclusive - \f[V]value\f[R]: Value to set for cells in range \f[B]Returns:\f[R] HeightMap: New HeightMap (original is unchanged) .PP \f[B]Raises:\f[R] ValueError: min > max -.SS Heuristic -.PP -\f[I]Inherits from: IntEnum\f[R] -.PP -Built-in A* heuristic function selector. -.PP -Values: EUCLIDEAN: sqrt((dx)\[ha]2 + (dy)\[ha]2). -Admissible, default. -MANHATTAN: |dx| + |dy|. -Admissible on 4-connected grids. -CHEBYSHEV: max(|dx|, |dy|). -Admissible on 8-connected (diag=1). -DIAGONAL: Octile distance. -Admissible on 8-connected (diag=sqrt(2)). -ZERO: Always returns 0. -A* degenerates to Dijkstra. -.PP -\f[B]Properties:\f[R] - \f[V]denominator\f[R]: the denominator of a -rational number in lowest terms - \f[V]imag\f[R]: the imaginary part of -a complex number - \f[V]numerator\f[R]: the numerator of a rational -number in lowest terms - \f[V]real\f[R]: the real part of a complex -number -.PP -\f[B]Methods:\f[R] -.SS \f[V]as_integer_ratio(...)\f[R] -.PP -Return a pair of integers, whose ratio is equal to the original int. -The ratio is in lowest terms and has a positive denominator. ->>> (10).as_integer_ratio() (10, 1) >>> (-10).as_integer_ratio() (-10, -1) >>> (0).as_integer_ratio() (0, 1) -.SS \f[V]bit_count(...)\f[R] -.PP -Number of ones in the binary representation of the absolute value of -self. -Also known as the population count. ->>> bin(13) `0b1101' >>> (13).bit_count() 3 -.SS \f[V]bit_length(...)\f[R] -.PP -Number of bits necessary to represent self in binary. ->>> bin(37) `0b100101' >>> (37).bit_length() 6 -.SS \f[V]conjugate(...)\f[R] -.PP -Returns self, the complex conjugate of any int. -.SS \f[V]from_bytes(...)\f[R] -.PP -Return the integer represented by the given array of bytes. -bytes Holds the array of bytes to convert. -The argument must either support the buffer protocol or be an iterable -object producing bytes. -Bytes and bytearray are examples of built-in objects that support the -buffer protocol. -byteorder The byte order used to represent the integer. -If byteorder is `big', the most significant byte is at the beginning of -the byte array. -If byteorder is `little', the most significant byte is at the end of the -byte array. -To request the native byte order of the host system, use sys.byteorder -as the byte order value. -Default is to use `big'. -signed Indicates whether two\[cq]s complement is used to represent the -integer. -.SS \f[V]is_integer(...)\f[R] -.PP -Returns True. -Exists for duck type compatibility with float.is_integer. -.SS \f[V]to_bytes(...)\f[R] -.PP -Return an array of bytes representing an integer. -length Length of bytes object to use. -An OverflowError is raised if the integer is not representable with the -given number of bytes. -Default is length 1. -byteorder The byte order used to represent the integer. -If byteorder is `big', the most significant byte is at the beginning of -the byte array. -If byteorder is `little', the most significant byte is at the end of the -byte array. -To request the native byte order of the host system, use sys.byteorder -as the byte order value. -Default is to use `big'. -signed Determines whether two\[cq]s complement is used to represent the -integer. -If signed is False and a negative integer is given, an OverflowError is -raised. .SS InputState .PP \f[I]Inherits from: IntEnum\f[R] diff --git a/src/GridLayers.cpp b/src/GridLayers.cpp index 770fc93..f5d452e 100644 --- a/src/GridLayers.cpp +++ b/src/GridLayers.cpp @@ -814,24 +814,14 @@ PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = { }; int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { - // 1.0 API freeze: positional order is now (name, z_index, ...). - static const char* kwlist[] = {"name", "z_index", "grid_size", "grid", NULL}; + static const char* kwlist[] = {"z_index", "name", "grid_size", NULL}; int z_index = -1; const char* name_str = nullptr; PyObject* grid_size_obj = nullptr; - PyObject* grid_obj = nullptr; int grid_x = 0, grid_y = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOO", const_cast(kwlist), - &name_str, &z_index, &grid_size_obj, &grid_obj)) { - return -1; - } - - // Validate grid kwarg type up-front (before allocating data). - if (grid_obj && grid_obj != Py_None && - !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) && - !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) { - PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izO", const_cast(kwlist), + &z_index, &name_str, &grid_size_obj)) { return -1; } @@ -861,15 +851,6 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py } self->grid.reset(); - // Auto-attach to grid via grid.add_layer(self) if grid= was supplied. - if (grid_obj && grid_obj != Py_None) { - PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self); - if (!result) { - return -1; - } - Py_DECREF(result); - } - return 0; } @@ -964,90 +945,6 @@ PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* arg Py_RETURN_NONE; } -// ============================================================================= -// Subscript protocol: layer[x, y] / layer[x, y] = value -// ============================================================================= - -// Helper: parse a 2-tuple key into (x, y) ints. Sets TypeError on bad keys. -static bool parse_subscript_key(PyObject* key, int* x, int* y) { - if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) { - PyErr_SetString(PyExc_TypeError, - "Layer indices must be a 2-tuple (x, y)"); - return false; - } - PyObject* x_obj = PyTuple_GetItem(key, 0); - PyObject* y_obj = PyTuple_GetItem(key, 1); - if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) { - PyErr_SetString(PyExc_TypeError, "Layer indices must be integers"); - return false; - } - *x = (int)PyLong_AsLong(x_obj); - *y = (int)PyLong_AsLong(y_obj); - return true; -} - -PyObject* PyGridLayerAPI::ColorLayer_subscript(PyColorLayerObject* self, PyObject* key) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); - return NULL; - } - int x, y; - if (!parse_subscript_key(key, &x, &y)) return NULL; - - if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { - PyErr_Format(PyExc_IndexError, - "Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)", - x, y, self->data->grid_x, self->data->grid_y); - return NULL; - } - - const sf::Color& color = self->data->at(x, y); - - // Wrap as mcrfpy.Color - auto* color_type = (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Color"); - if (!color_type) return NULL; - PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0); - Py_DECREF(color_type); - if (!color_obj) return NULL; - color_obj->data = color; - return (PyObject*)color_obj; -} - -int PyGridLayerAPI::ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); - return -1; - } - if (value == nullptr) { - PyErr_SetString(PyExc_TypeError, "cannot delete ColorLayer cells"); - return -1; - } - int x, y; - if (!parse_subscript_key(key, &x, &y)) return -1; - - if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { - PyErr_Format(PyExc_IndexError, - "Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)", - x, y, self->data->grid_x, self->data->grid_y); - return -1; - } - - // PyColor::fromPy accepts Color objects, tuples, lists, ints; sets PyErr on failure. - sf::Color color = PyColor::fromPy(value); - if (PyErr_Occurred()) return -1; - - self->data->at(x, y) = color; - self->data->markDirty(x, y); - return 0; -} - -PyMappingMethods PyGridLayerAPI::ColorLayer_mapping_methods = { - .mp_length = nullptr, - .mp_subscript = (binaryfunc)PyGridLayerAPI::ColorLayer_subscript, - .mp_ass_subscript = (objobjargproc)PyGridLayerAPI::ColorLayer_subscript_assign, -}; - PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) { PyObject* color_obj; if (!PyArg_ParseTuple(args, "O", &color_obj)) { @@ -1947,25 +1844,15 @@ PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = { }; int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) { - // 1.0 API freeze: positional order is now (name, z_index, ...). - static const char* kwlist[] = {"name", "z_index", "texture", "grid_size", "grid", NULL}; + static const char* kwlist[] = {"z_index", "name", "texture", "grid_size", NULL}; int z_index = -1; const char* name_str = nullptr; PyObject* texture_obj = nullptr; PyObject* grid_size_obj = nullptr; - PyObject* grid_obj = nullptr; int grid_x = 0, grid_y = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ziOOO", const_cast(kwlist), - &name_str, &z_index, &texture_obj, &grid_size_obj, &grid_obj)) { - return -1; - } - - // Validate grid kwarg type up-front (before allocating data). - if (grid_obj && grid_obj != Py_None && - !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) && - !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) { - PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izOO", const_cast(kwlist), + &z_index, &name_str, &texture_obj, &grid_size_obj)) { return -1; } @@ -2016,15 +1903,6 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb } self->grid.reset(); - // Auto-attach to grid via grid.add_layer(self) if grid= was supplied. - if (grid_obj && grid_obj != Py_None) { - PyObject* result = PyObject_CallMethod(grid_obj, "add_layer", "O", (PyObject*)self); - if (!result) { - return -1; - } - Py_DECREF(result); - } - return 0; } @@ -2074,64 +1952,6 @@ PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) Py_RETURN_NONE; } -// ============================================================================= -// TileLayer subscript: tl[x, y] / tl[x, y] = index -// ============================================================================= - -PyObject* PyGridLayerAPI::TileLayer_subscript(PyTileLayerObject* self, PyObject* key) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); - return NULL; - } - int x, y; - if (!parse_subscript_key(key, &x, &y)) return NULL; - - if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { - PyErr_Format(PyExc_IndexError, - "Position (%d, %d) out of bounds for TileLayer of size (%d, %d)", - x, y, self->data->grid_x, self->data->grid_y); - return NULL; - } - - return PyLong_FromLong(self->data->at(x, y)); -} - -int PyGridLayerAPI::TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); - return -1; - } - if (value == nullptr) { - PyErr_SetString(PyExc_TypeError, "cannot delete TileLayer cells"); - return -1; - } - int x, y; - if (!parse_subscript_key(key, &x, &y)) return -1; - - if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { - PyErr_Format(PyExc_IndexError, - "Position (%d, %d) out of bounds for TileLayer of size (%d, %d)", - x, y, self->data->grid_x, self->data->grid_y); - return -1; - } - - if (!PyLong_Check(value)) { - PyErr_SetString(PyExc_TypeError, "tile index must be an int"); - return -1; - } - int index = (int)PyLong_AsLong(value); - - self->data->at(x, y) = index; - self->data->markDirty(x, y); - return 0; -} - -PyMappingMethods PyGridLayerAPI::TileLayer_mapping_methods = { - .mp_length = nullptr, - .mp_subscript = (binaryfunc)PyGridLayerAPI::TileLayer_subscript, - .mp_ass_subscript = (objobjargproc)PyGridLayerAPI::TileLayer_subscript_assign, -}; - PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) { int index; if (!PyArg_ParseTuple(args, "i", &index)) { diff --git a/src/GridLayers.h b/src/GridLayers.h index 00da248..94ebe43 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -218,11 +218,6 @@ public: static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure); static PyObject* ColorLayer_repr(PyColorLayerObject* self); - // Subscript protocol: layer[x, y] / layer[x, y] = value - static PyObject* ColorLayer_subscript(PyColorLayerObject* self, PyObject* key); - static int ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value); - static PyMappingMethods ColorLayer_mapping_methods; - // TileLayer methods static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds); static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds); @@ -243,11 +238,6 @@ public: static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure); static PyObject* TileLayer_repr(PyTileLayerObject* self); - // Subscript protocol: layer[x, y] / layer[x, y] = value - static PyObject* TileLayer_subscript(PyTileLayerObject* self, PyObject* key); - static int TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value); - static PyMappingMethods TileLayer_mapping_methods; - // Method and getset arrays static PyMethodDef ColorLayer_methods[]; static PyGetSetDef ColorLayer_getsetters[]; @@ -269,7 +259,6 @@ namespace mcrfpydef { Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr, - .tp_as_mapping = &PyGridLayerAPI::ColorLayer_mapping_methods, // layer[x, y] .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("ColorLayer(z_index=-1, name=None, grid_size=None)\n\n" "A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n" @@ -323,7 +312,6 @@ namespace mcrfpydef { Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr, - .tp_as_mapping = &PyGridLayerAPI::TileLayer_mapping_methods, // layer[x, y] .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("TileLayer(z_index=-1, name=None, texture=None, grid_size=None)\n\n" "A grid layer that stores sprite indices per cell for tile-based rendering.\n\n" diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 23d5f24..e944be5 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -497,6 +497,9 @@ PyObject* PyInit_mcrfpy() /*grid layers (#147)*/ &PyColorLayerType, &PyTileLayerType, + /*animation*/ + &PyAnimationType, + /*timer*/ &PyTimerType, @@ -571,10 +574,6 @@ PyObject* PyInit_mcrfpy() /*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/ &mcrfpydef::PyUniformCollectionType, - /*animation - constructed internally by drawable.animate() and returned by mcrfpy.animations, - but not directly instantiable from Python (pre-1.0 API freeze)*/ - &PyAnimationType, - nullptr}; // Set up PyWindowType methods and getsetters before PyType_Ready diff --git a/src/UIArc.cpp b/src/UIArc.cpp index d7c5b12..2f4a7eb 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -2,10 +2,6 @@ #include "McRFPy_API.h" #include "PythonObjectCache.h" #include "PyAlignment.h" -#include "UIFrame.h" // parent= kwarg: Frame parent type -#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation -#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type -#include "PySceneObject.h" // parent= kwarg: Scene parent type #include #include @@ -560,22 +556,19 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) { float margin = 0.0f; float horiz_margin = -1.0f; float vert_margin = -1.0f; - PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid) static const char* kwlist[] = { "center", "radius", "start_angle", "end_angle", "color", "thickness", "on_click", "visible", "opacity", "z_index", "name", "align", "margin", "horiz_margin", "vert_margin", - "parent", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfffO", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast(kwlist), ¢er_obj, &radius, &start_angle, &end_angle, &color_obj, &thickness, &click_handler, &visible, &opacity, &z_index, &name, - &align_obj, &margin, &horiz_margin, &vert_margin, - &parent_obj)) { + &align_obj, &margin, &horiz_margin, &vert_margin)) { return -1; } @@ -646,8 +639,5 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) { // #184: Check if this is a Python subclass (for callback method support) self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIArcType; - // Auto-attach to parent's children collection if parent= was supplied - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - return 0; } diff --git a/src/UIBase.h b/src/UIBase.h index 06d1ed3..c09cac5 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -158,33 +158,6 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds) } \ } while (0) -// Macro for auto-attaching a newly constructed UI drawable to a parent's children. -// Usage: UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); -// parent_obj must be a Frame, Scene, or Grid (anything with a .children UICollection). -// Returns -1 on error (suitable for use in tp_init functions). No-op when parent_obj is null/None. -#define UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self) \ - do { \ - if ((parent_obj) && (parent_obj) != Py_None) { \ - if (!PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIFrameType) && \ - !PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PySceneType) && \ - !PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridType) && \ - !PyObject_IsInstance((parent_obj), (PyObject*)&mcrfpydef::PyUIGridViewType)) { \ - PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Scene, or Grid"); \ - return -1; \ - } \ - PyObject* _children = PyObject_GetAttrString((parent_obj), "children"); \ - if (!_children) { \ - return -1; \ - } \ - PyObject* _result = PyObject_CallMethod(_children, "append", "O", (PyObject*)(self)); \ - Py_DECREF(_children); \ - if (!_result) { \ - return -1; \ - } \ - Py_DECREF(_result); \ - } \ - } while (0) - // Property getters/setters for visible and opacity template static PyObject* UIDrawable_get_visible(T* self, void* closure) diff --git a/src/UICaption.cpp b/src/UICaption.cpp index bd25c70..33ccb33 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -7,9 +7,6 @@ #include "PyAlignment.h" #include "PyShader.h" // #106: Shader support #include "PyUniformCollection.h" // #106: Uniform collection support -#include "UIFrame.h" // parent= kwarg: Frame parent type -#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type -#include "PySceneObject.h" // parent= kwarg: Scene parent type // UIDrawable methods now in UIBase.h #include @@ -451,27 +448,23 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) float margin = 0.0f; float horiz_margin = -1.0f; float vert_margin = -1.0f; - PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid) - // Keywords list: pos and text are positional-or-keyword. Everything after - // the '$' separator (font and friends) is keyword-only. + // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { - "pos", "text", - // Keyword-only args follow: - "font", "fill_color", "outline_color", "outline", "font_size", "on_click", + "pos", "font", "text", // Positional args (as per spec) + // Keyword-only args + "fill_color", "outline_color", "outline", "font_size", "on_click", "visible", "opacity", "z_index", "name", "x", "y", "align", "margin", "horiz_margin", "vert_margin", - "parent", nullptr }; - // '$' marker makes all following args keyword-only (Python 3.3+ format extension). - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oz$OOOffOifizffOfffO", const_cast(kwlist), - &pos_obj, &text, // pos+text are positional-or-keyword - &font, &fill_color, &outline_color, &outline, &font_size, &click_handler, + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast(kwlist), + &pos_obj, &font, &text, // Positional + &fill_color, &outline_color, &outline, &font_size, &click_handler, &visible, &opacity, &z_index, &name, &x, &y, - &align_obj, &margin, &horiz_margin, &vert_margin, - &parent_obj)) { + &align_obj, &margin, &horiz_margin, &vert_margin)) { return -1; } @@ -604,9 +597,6 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) // #184: Check if this is a Python subclass (for callback method support) self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICaptionType; - // Auto-attach to parent's children collection if parent= was supplied - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - return 0; } diff --git a/src/UICircle.cpp b/src/UICircle.cpp index 7127d46..7f503ed 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -5,10 +5,6 @@ #include "PyColor.h" #include "PythonObjectCache.h" #include "PyAlignment.h" -#include "UIFrame.h" // parent= kwarg: Frame parent type -#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation -#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type -#include "PySceneObject.h" // parent= kwarg: Scene parent type #include UICircle::UICircle() @@ -483,13 +479,10 @@ PyObject* UICircle::repr(PyUICircleObject* self) { } int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { - // 1.0 API freeze: positional order is now (center, radius, ...). The old - // (radius, center) ordering is no longer supported. static const char* kwlist[] = { - "center", "radius", "fill_color", "outline_color", "outline", + "radius", "center", "fill_color", "outline_color", "outline", "on_click", "visible", "opacity", "z_index", "name", - "align", "margin", "horiz_margin", "vert_margin", - "parent", NULL + "align", "margin", "horiz_margin", "vert_margin", NULL }; float radius = 10.0f; @@ -508,13 +501,11 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { float margin = 0.0f; float horiz_margin = -1.0f; float vert_margin = -1.0f; - PyObject* parent_obj = NULL; // Auto-attach parent (Frame, Scene, or Grid) - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfOOfOpfisOfffO", (char**)kwlist, - ¢er_obj, &radius, &fill_color_obj, &outline_color_obj, &outline, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist, + &radius, ¢er_obj, &fill_color_obj, &outline_color_obj, &outline, &click_obj, &visible, &opacity_val, &z_index, &name, - &align_obj, &margin, &horiz_margin, &vert_margin, - &parent_obj)) { + &align_obj, &margin, &horiz_margin, &vert_margin)) { return -1; } @@ -606,8 +597,5 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { // #184: Check if this is a Python subclass (for callback method support) self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUICircleType; - // Auto-attach to parent's children collection if parent= was supplied - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - return 0; } diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index e2b89ad..ff86acf 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -5,7 +5,6 @@ #include "UICaption.h" #include "UISprite.h" #include "UIGrid.h" -#include "PySceneObject.h" // parent= kwarg: Scene parent type #include "McRFPy_API.h" #include "PythonObjectCache.h" #include "PyAlignment.h" @@ -602,7 +601,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) float margin = 0.0f; float horiz_margin = -1.0f; float vert_margin = -1.0f; - PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid) // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { @@ -611,17 +609,15 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) "fill_color", "outline_color", "outline", "children", "on_click", "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree", "align", "margin", "horiz_margin", "vert_margin", - "parent", nullptr }; // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfffO", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast(kwlist), &pos_obj, &size_obj, // Positional &fill_color, &outline_color, &outline, &children_arg, &click_handler, &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree, - &align_obj, &margin, &horiz_margin, &vert_margin, - &parent_obj)) { + &align_obj, &margin, &horiz_margin, &vert_margin)) { return -1; } @@ -802,9 +798,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) // #184: Check if this is a Python subclass (for callback method support) self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIFrameType; - // Auto-attach to parent's children collection if parent= was supplied - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - return 0; } diff --git a/src/UIGridPyMethods.cpp b/src/UIGridPyMethods.cpp index cfabe1e..0562287 100644 --- a/src/UIGridPyMethods.cpp +++ b/src/UIGridPyMethods.cpp @@ -82,19 +82,10 @@ PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key) return (PyObject*)obj; } -// Setitem on _GridData / Grid: GridPoints are views, not assignable. -static int UIGrid_subscript_assign(PyUIGridObject* self, PyObject* key, PyObject* value) -{ - (void)self; (void)key; (void)value; - PyErr_SetString(PyExc_TypeError, - "Grid points are not assignable; modify properties on the returned point"); - return -1; -} - PyMappingMethods UIGrid::mpmethods = { .mp_length = NULL, .mp_subscript = (binaryfunc)UIGrid::subscript, - .mp_ass_subscript = (objobjargproc)UIGrid_subscript_assign, + .mp_ass_subscript = NULL }; // ========================================================================= diff --git a/src/UIGridView.cpp b/src/UIGridView.cpp index d005e6b..16d253b 100644 --- a/src/UIGridView.cpp +++ b/src/UIGridView.cpp @@ -1,7 +1,6 @@ // UIGridView.cpp - Rendering view for GridData (#252) #include "UIGridView.h" #include "UIGrid.h" -#include "UIGridPoint.h" #include "UIEntity.h" #include "GameEngine.h" #include "McRFPy_API.h" @@ -12,7 +11,6 @@ #include "PyPositionHelper.h" #include "PyVector.h" #include "PythonObjectCache.h" -#include "PySceneObject.h" // parent= kwarg: Scene is a valid parent type #include #include @@ -373,45 +371,15 @@ bool UIGridView::hasProperty(const std::string& name) const int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds) { - // Extract parent= up front so it doesn't confuse downstream parsing in - // either init mode. parent_obj is borrowed from the original kwds (which - // the caller owns and outlives this function), so no INCREF is needed -- - // but we must not delete from the caller's dict, so make a working copy. - PyObject* parent_obj = nullptr; - PyObject* dispatch_kwds = kwds; - if (kwds) { - parent_obj = PyDict_GetItemString(kwds, "parent"); // borrowed ref - if (parent_obj) { - dispatch_kwds = PyDict_Copy(kwds); - if (!dispatch_kwds) return -1; - PyDict_DelItemString(dispatch_kwds, "parent"); - } - } - // Determine mode by checking for 'grid' kwarg PyObject* grid_kwarg = nullptr; - if (dispatch_kwds) { - grid_kwarg = PyDict_GetItemString(dispatch_kwds, "grid"); // borrowed ref + if (kwds) { + grid_kwarg = PyDict_GetItemString(kwds, "grid"); // borrowed ref } bool explicit_view = (grid_kwarg && grid_kwarg != Py_None); - int rc = explicit_view - ? init_explicit_view(self, args, dispatch_kwds) - : init_with_data(self, args, dispatch_kwds); - - if (dispatch_kwds != kwds) Py_DECREF(dispatch_kwds); - if (rc != 0) return rc; - - if (parent_obj) { - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - } - return 0; -} - -int UIGridView::init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds) -{ - { + if (explicit_view) { // Mode 1: View of existing grid data static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr}; PyObject* grid_obj = nullptr; @@ -485,6 +453,9 @@ int UIGridView::init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyO self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType; return 0; + } else { + // Mode 2: Factory mode - create UIGrid internally + return init_with_data(self, args, kwds); } } @@ -809,69 +780,6 @@ int UIGridView::set_float_member_gv(PyUIGridViewObject* self, PyObject* value, v return 0; } -// ========================================================================= -// Subscript protocol: grid[x, y] -> GridPoint (delegates to GridData). -// Setitem raises TypeError (GridPoints are views, not assignable). -// ========================================================================= -PyObject* UIGridView::subscript(PyUIGridViewObject* self, PyObject* key) -{ - if (!self->data || !self->data->grid_data) { - PyErr_SetString(PyExc_RuntimeError, "Grid has no underlying data"); - return NULL; - } - if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) { - PyErr_SetString(PyExc_TypeError, "Grid indices must be a 2-tuple (x, y)"); - return NULL; - } - PyObject* x_obj = PyTuple_GetItem(key, 0); - PyObject* y_obj = PyTuple_GetItem(key, 1); - if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) { - PyErr_SetString(PyExc_TypeError, "Grid indices must be integers"); - return NULL; - } - int x = (int)PyLong_AsLong(x_obj); - int y = (int)PyLong_AsLong(y_obj); - - auto& grid_data = self->data->grid_data; - if (x < 0 || x >= grid_data->grid_w) { - PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", - x, grid_data->grid_w); - return NULL; - } - if (y < 0 || y >= grid_data->grid_h) { - PyErr_Format(PyExc_IndexError, "y index %d is out of range [0, %d)", - y, grid_data->grid_h); - return NULL; - } - - // Reconstruct shared_ptr from GridData via aliasing constructor - // (mirrors UIGridView::get_grid). - auto grid_ptr = static_cast(grid_data.get()); - auto grid_as_uigrid = std::shared_ptr(grid_data, grid_ptr); - - auto type = &mcrfpydef::PyUIGridPointType; - auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0); - if (!obj) return NULL; - obj->grid = grid_as_uigrid; - obj->x = x; - obj->y = y; - return (PyObject*)obj; -} - -int UIGridView::subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value) -{ - (void)self; (void)key; (void)value; - PyErr_SetString(PyExc_TypeError, - "Grid points are not assignable; modify properties on the returned point"); - return -1; -} - -PyMappingMethods UIGridView::mpmethods = { - .mp_length = NULL, - .mp_subscript = (binaryfunc)UIGridView::subscript, - .mp_ass_subscript = (objobjargproc)UIGridView::subscript_assign, -}; - // #252: PyObjectType typedef for UIDRAWABLE_* macros typedef PyUIGridViewObject PyObjectType; diff --git a/src/UIGridView.h b/src/UIGridView.h index ee46af7..db054bd 100644 --- a/src/UIGridView.h +++ b/src/UIGridView.h @@ -86,7 +86,6 @@ public: // Python API // ========================================================================= static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds); - static int init_explicit_view(PyUIGridViewObject* self, PyObject* args, PyObject* kwds); static int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds); static PyObject* repr(PyUIGridViewObject* self); @@ -111,12 +110,6 @@ public: static PyMethodDef methods[]; static PyGetSetDef getsetters[]; - - // Subscript protocol: grid[x, y] (delegates to underlying GridData). - // Setitem raises TypeError (GridPoints are views). - static PyObject* subscript(PyUIGridViewObject* self, PyObject* key); - static int subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value); - static PyMappingMethods mpmethods; }; // Forward declaration of methods array @@ -146,7 +139,6 @@ namespace mcrfpydef { Py_TYPE(self)->tp_free(self); }, .tp_repr = (reprfunc)UIGridView::repr, - .tp_as_mapping = &UIGridView::mpmethods, // grid[x, y] (delegates to GridData) .tp_getattro = UIGridView::getattro, // #252: attribute delegation to Grid .tp_setattro = UIGridView::setattro, // #252: attribute delegation to Grid .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, diff --git a/src/UILine.cpp b/src/UILine.cpp index f85f090..c02b399 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -5,10 +5,6 @@ #include "PyColor.h" #include "PythonObjectCache.h" #include "PyAlignment.h" -#include "UIFrame.h" // parent= kwarg: Frame parent type -#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation -#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type -#include "PySceneObject.h" // parent= kwarg: Scene parent type #include UILine::UILine() @@ -569,21 +565,18 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) { float margin = 0.0f; float horiz_margin = -1.0f; float vert_margin = -1.0f; - PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid) static const char* kwlist[] = { "start", "end", "thickness", "color", "on_click", "visible", "opacity", "z_index", "name", "align", "margin", "horiz_margin", "vert_margin", - "parent", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfffO", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast(kwlist), &start_obj, &end_obj, &thickness, &color_obj, &click_handler, &visible, &opacity, &z_index, &name, - &align_obj, &margin, &horiz_margin, &vert_margin, - &parent_obj)) { + &align_obj, &margin, &horiz_margin, &vert_margin)) { return -1; } @@ -670,8 +663,5 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) { // #184: Check if this is a Python subclass (for callback method support) self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUILineType; - // Auto-attach to parent's children collection if parent= was supplied - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - return 0; } diff --git a/src/UISprite.cpp b/src/UISprite.cpp index c8d5ef6..474e2c9 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -3,9 +3,6 @@ #include "PyVector.h" #include "PythonObjectCache.h" #include "UIFrame.h" // #144: For snapshot= parameter -#include "UICaption.h" // parent= kwarg: needed for ATTACH macro instantiation -#include "UIGrid.h" // parent= kwarg: Grid/GridView parent type -#include "PySceneObject.h" // parent= kwarg: Scene parent type #include "PyAlignment.h" #include "PyShader.h" // #106: Shader support #include "PyUniformCollection.h" // #106: Uniform collection support @@ -479,7 +476,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) float margin = 0.0f; float horiz_margin = -1.0f; float vert_margin = -1.0f; - PyObject* parent_obj = nullptr; // Auto-attach parent (Frame, Scene, or Grid) // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { @@ -488,17 +484,15 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) "scale", "scale_x", "scale_y", "on_click", "visible", "opacity", "z_index", "name", "x", "y", "snapshot", "align", "margin", "horiz_margin", "vert_margin", - "parent", nullptr }; // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfffO", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast(kwlist), &pos_obj, &texture, &sprite_index, // Positional &scale, &scale_x, &scale_y, &click_handler, &visible, &opacity, &z_index, &name, &x, &y, &snapshot, - &align_obj, &margin, &horiz_margin, &vert_margin, - &parent_obj)) { + &align_obj, &margin, &horiz_margin, &vert_margin)) { return -1; } @@ -633,9 +627,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) // #184: Check if this is a Python subclass (for callback method support) self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUISpriteType; - // Auto-attach to parent's children collection if parent= was supplied - UIDRAWABLE_ATTACH_TO_PARENT(parent_obj, self); - return 0; } diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi index b635983..9852545 100644 --- a/stubs/mcrfpy.pyi +++ b/stubs/mcrfpy.pyi @@ -92,14 +92,6 @@ class FOV(IntEnum): SHADOW: int SYMMETRIC_SHADOWCAST: int -class Heuristic(IntEnum): - """Built-in A* heuristic function selector.""" - CHEBYSHEV: int - DIAGONAL: int - EUCLIDEAN: int - MANHATTAN: int - ZERO: int - class InputState(IntEnum): """Enum representing input event states (pressed/released).""" PRESSED: int @@ -263,6 +255,34 @@ class AStarPath: """Get and consume next step in the path.""" ... +class Animation: + """Create an animation that interpolates a property value over time.""" + def __init__(self, property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None) -> None: ... + duration: float # Animation duration in seconds (float, read-only). Total time for the animation to complete. + elapsed: float # Elapsed time in seconds (float, read-only). Time since the animation started. + is_complete: bool # Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called. + is_delta: bool # Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value. + is_looping: bool # Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end. + property: str # Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index'). + def complete(self) -> None: + """Complete the animation immediately by jumping to the final value.""" + ... + def get_current_value(self) -> Any: + """Get the current interpolated value of the animation.""" + ... + def hasValidTarget(self) -> bool: + """Check if the animation still has a valid target.""" + ... + def start(self, target: UIDrawable, conflict_mode: str = 'replace') -> None: + """Start the animation on a target UI element.""" + ... + def stop(self) -> None: + """Stop the animation without completing it.""" + ... + def update(self, delta_time: float) -> bool: + """Update the animation by the given time delta.""" + ... + class Arc: """An arc UI element for drawing curved line segments.""" def __init__(self, center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs) -> None: ... @@ -538,15 +558,9 @@ class DijkstraMap: """A Dijkstra distance map from a fixed root position.""" def __init__(self, *args, **kwargs) -> None: ... root: Vector # Root position that distances are measured from (Vector, read-only). - def descent_step(self, pos) -> Vector | None: - """Get the adjacent cell with the lowest distance (steepest descent).""" - ... def distance(self, pos) -> float | None: """Get distance from position to root.""" ... - def invert(self) -> DijkstraMap: - """Return a NEW DijkstraMap whose distance field is the safety field.""" - ... def path_from(self, pos) -> AStarPath: """Get full path from position to root.""" ... diff --git a/tools/audit_pymethoddef.py b/tools/audit_pymethoddef.py deleted file mode 100755 index f3f86ce..0000000 --- a/tools/audit_pymethoddef.py +++ /dev/null @@ -1,463 +0,0 @@ -#!/usr/bin/env python3 -""" -audit_pymethoddef.py - Static-analysis tool for McRogueFace Python bindings. - -Walks src/**/*.cpp, parses each file with tree-sitter-cpp, and locates every -`PyMethodDef [] = {...}` and `PyGetSetDef [] = {...}` declaration. -For each entry inside those array initializers, classifies the docstring slot: - - MACRO - uses MCRF_METHOD(...) or MCRF_PROPERTY(...) - RAW_STRING - inline C string literal (or concatenated string literals) - NULL - explicit NULL literal - MISSING - entry too short to have a doc field (probably malformed) - -The `MACRO` classification is the project's compliance target. RAW_STRING and -NULL entries should be migrated to the macro system before the 1.0 API freeze. - -Sentinel terminator entries (e.g. `{NULL}`, `{0}`) are skipped. - -Usage: - python3 tools/audit_pymethoddef.py [--strict] [--quiet] - [--paths PATH [PATH ...]] - -Flags: - --strict Exit nonzero if any non-MACRO entries are found (CI mode). - --quiet Suppress per-file output, print only the summary. - --paths Restrict scan to the given files/directories. Defaults to src/. -""" -from __future__ import annotations - -import argparse -import os -import sys -from collections import Counter, defaultdict -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable, List, Optional, Tuple - -try: - import tree_sitter_cpp - from tree_sitter import Language, Parser -except ImportError as e: - sys.stderr.write( - "ERROR: tree-sitter / tree-sitter-cpp not installed.\n" - "Activate the audit venv first:\n" - " source .venv-audit/bin/activate\n" - f"(import error: {e})\n" - ) - sys.exit(2) - - -# --------------------------------------------------------------------------- -# Tree-sitter setup -# --------------------------------------------------------------------------- - -_LANG = Language(tree_sitter_cpp.language()) -_PARSER = Parser(_LANG) - -# Indices of the docstring field in the struct initializer. -# PyMethodDef: {ml_name, ml_meth, ml_flags, ml_doc} -> idx 3 -# PyGetSetDef: {name, get, set, doc, closure} -> idx 3 -DOC_FIELD_INDEX = 3 -EXPECTED_MIN_FIELDS = { - "PyMethodDef": 4, # need at least 4 to have ml_doc - "PyGetSetDef": 4, # need at least 4 to have doc (closure can be omitted) -} - -# Punctuation/structural child node types we ignore when walking entry fields. -_PUNCT_TYPES = {"{", "}", ",", "(", ")", "[", "]", ";"} - -# Macros that mark a docstring slot as compliant. -_COMPLIANT_MACROS = {"MCRF_METHOD", "MCRF_PROPERTY"} - - -# --------------------------------------------------------------------------- -# Data records -# --------------------------------------------------------------------------- - -@dataclass -class EntryRecord: - file_path: Path - line: int # 1-based - array_kind: str # "PyMethodDef" or "PyGetSetDef" - array_name: str # e.g. "PyAnimation::methods" - entry_name: str # ml_name / name string, or "" - classification: str # MACRO / RAW_STRING / NULL / MISSING - - -# --------------------------------------------------------------------------- -# Tree-sitter helpers -# --------------------------------------------------------------------------- - -def _node_text(src: bytes, node) -> str: - return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace") - - -def _meaningful_children(node) -> List: - """Return children of an initializer_list, skipping punctuation tokens.""" - return [c for c in node.children if c.type not in _PUNCT_TYPES] - - -def _classify_doc_field(src: bytes, doc_node) -> str: - """Map a docstring AST node to a classification string.""" - t = doc_node.type - if t == "null": - return "NULL" - if t in ("string_literal", "concatenated_string", "raw_string_literal"): - return "RAW_STRING" - if t == "call_expression": - # The first child of call_expression is the callee identifier. - callee = doc_node.child_by_field_name("function") - if callee is None and doc_node.children: - callee = doc_node.children[0] - if callee is not None: - name = _node_text(src, callee).strip() - if name in _COMPLIANT_MACROS: - return "MACRO" - return "RAW_STRING" # call expression to something non-MACRO - if t == "identifier": - # Bare identifier in the doc slot - could be a #define alias. Treat as - # raw (non-compliant) so the user investigates it. - text = _node_text(src, doc_node).strip() - if text == "NULL": - return "NULL" - return "RAW_STRING" - # Anything else (parenthesized_expression, etc.) - inspect text fallback. - text = _node_text(src, doc_node).strip() - stripped = text.lstrip("(").lstrip() - for macro in _COMPLIANT_MACROS: - if stripped.startswith(macro + "("): - return "MACRO" - if stripped == "NULL": - return "NULL" - return "RAW_STRING" - - -def _entry_name(src: bytes, entry_node) -> str: - """Return the first string literal in an entry initializer (the ml_name).""" - for c in _meaningful_children(entry_node): - if c.type == "string_literal": - # string_literal contains string_content children - for sub in c.children: - if sub.type == "string_content": - return _node_text(src, sub) - return _node_text(src, c).strip('"') - if c.type == "concatenated_string": - for lit in c.children: - if lit.type == "string_literal": - for sub in lit.children: - if sub.type == "string_content": - return _node_text(src, sub) - return _node_text(src, c) - # Stop at the first non-string field - the name should come first. - break - return "" - - -def _is_sentinel(src: bytes, entry_node) -> bool: - """True if the entry looks like a sentinel terminator (e.g. {NULL} / {0}).""" - fields = _meaningful_children(entry_node) - if not fields: - return True - if len(fields) == 1: - only = fields[0] - if only.type == "null": - return True - if only.type == "number_literal" and _node_text(src, only).strip() == "0": - return True - # Some codebases write {NULL, NULL, NULL, NULL}. Treat all-NULL/0 as sentinel. - for f in fields: - if f.type == "null": - continue - if f.type == "number_literal" and _node_text(src, f).strip() == "0": - continue - return False - return True - - -def _array_name_from_init_declarator(src: bytes, init_decl) -> Optional[str]: - """Extract the array name from an init_declarator containing array_declarator.""" - arr = None - for c in init_decl.children: - if c.type == "array_declarator": - arr = c - break - if arr is None: - return None - # The first child is the declarator (identifier or qualified_identifier). - for c in arr.children: - if c.type in ("identifier", "qualified_identifier", "field_identifier"): - return _node_text(src, c) - return None - - -def _outer_initializer_list(init_decl): - """Get the top-level initializer_list child of an init_declarator, if any.""" - for c in init_decl.children: - if c.type == "initializer_list": - return c - return None - - -# --------------------------------------------------------------------------- -# Per-file scan -# --------------------------------------------------------------------------- - -def _walk_declarations(node, out: list) -> None: - """Collect all `declaration` nodes under `node` (recursive).""" - if node.type == "declaration": - out.append(node) - for c in node.children: - _walk_declarations(c, out) - - -def scan_file(path: Path) -> List[EntryRecord]: - try: - src = path.read_bytes() - except OSError as e: - sys.stderr.write(f"WARNING: cannot read {path}: {e}\n") - return [] - - tree = _PARSER.parse(src) - decls: list = [] - _walk_declarations(tree.root_node, decls) - - records: List[EntryRecord] = [] - for decl in decls: - # Find the type_identifier child to determine if this is one of ours. - type_kind = None - for c in decl.children: - if c.type == "type_identifier": - txt = _node_text(src, c).strip() - if txt in EXPECTED_MIN_FIELDS: - type_kind = txt - break - if type_kind is None: - continue - - # Each declaration may have multiple init_declarators (rare for arrays - # but cheap to handle). - for c in decl.children: - if c.type != "init_declarator": - continue - outer_init = _outer_initializer_list(c) - if outer_init is None: - continue # forward decl or extern - no initializer - array_name = _array_name_from_init_declarator(src, c) or "" - - # Each direct child initializer_list is an entry. - for entry in outer_init.children: - if entry.type != "initializer_list": - continue - if _is_sentinel(src, entry): - continue - - fields = _meaningful_children(entry) - line = entry.start_point[0] + 1 # tree-sitter is 0-based - name = _entry_name(src, entry) - - if len(fields) <= DOC_FIELD_INDEX: - records.append(EntryRecord( - file_path=path, - line=line, - array_kind=type_kind, - array_name=array_name, - entry_name=name, - classification="MISSING", - )) - continue - - doc_node = fields[DOC_FIELD_INDEX] - classification = _classify_doc_field(src, doc_node) - records.append(EntryRecord( - file_path=path, - line=line, - array_kind=type_kind, - array_name=array_name, - entry_name=name, - classification=classification, - )) - return records - - -# --------------------------------------------------------------------------- -# Path resolution -# --------------------------------------------------------------------------- - -def _iter_cpp_files(roots: Iterable[Path]) -> Iterable[Path]: - for root in roots: - if root.is_file(): - if root.suffix == ".cpp": - yield root - continue - if not root.exists(): - sys.stderr.write(f"WARNING: path does not exist: {root}\n") - continue - for dirpath, _dirnames, filenames in os.walk(root): - for fn in filenames: - if fn.endswith(".cpp"): - yield Path(dirpath) / fn - - -# --------------------------------------------------------------------------- -# Reporting -# --------------------------------------------------------------------------- - -def _print_file_table(records: List[EntryRecord], project_root: Path) -> None: - by_file: dict[Path, List[EntryRecord]] = defaultdict(list) - for r in records: - by_file[r.file_path].append(r) - - for path in sorted(by_file): - try: - rel = path.relative_to(project_root) - except ValueError: - rel = path - entries = sorted(by_file[path], key=lambda r: r.line) - - # Compute column widths for this file. - loc_w = max(len(f"{rel}:{e.line}") for e in entries) - arr_w = max(len(e.array_name) for e in entries) - ent_w = max(len(e.entry_name) for e in entries) - loc_w = max(loc_w, len("file:line")) - arr_w = max(arr_w, len("array")) - ent_w = max(ent_w, len("entry")) - - header = ( - f"{'file:line':<{loc_w}} " - f"{'array':<{arr_w}} " - f"{'entry':<{ent_w}} " - f"classification" - ) - print(header) - print("-" * len(header)) - for e in entries: - loc = f"{rel}:{e.line}" - print( - f"{loc:<{loc_w}} " - f"{e.array_name:<{arr_w}} " - f"{e.entry_name:<{ent_w}} " - f"{e.classification}" - ) - print() - - -def _print_summary(records: List[EntryRecord]) -> None: - total = len(records) - counts = Counter(r.classification for r in records) - macro = counts.get("MACRO", 0) - raw = counts.get("RAW_STRING", 0) - null = counts.get("NULL", 0) - missing = counts.get("MISSING", 0) - pct = (macro / total * 100.0) if total else 0.0 - - print("=" * 60) - print("PyMethodDef / PyGetSetDef Documentation Audit Summary") - print("=" * 60) - print(f"Total entries scanned : {total}") - print(f" MACRO compliant : {macro}") - print(f" RAW_STRING : {raw}") - print(f" NULL : {null}") - print(f" MISSING : {missing}") - print(f"MACRO compliance : {pct:.1f}%") - - # Per-kind breakdown. - by_kind = defaultdict(Counter) - for r in records: - by_kind[r.array_kind][r.classification] += 1 - if by_kind: - print() - print("Breakdown by kind:") - for kind in sorted(by_kind): - kc = by_kind[kind] - kt = sum(kc.values()) - kp = (kc.get("MACRO", 0) / kt * 100.0) if kt else 0.0 - print( - f" {kind:<13} total={kt:<4} " - f"MACRO={kc.get('MACRO', 0):<4} " - f"RAW={kc.get('RAW_STRING', 0):<4} " - f"NULL={kc.get('NULL', 0):<4} " - f"MISSING={kc.get('MISSING', 0):<4} " - f"({kp:.1f}% compliant)" - ) - - # Top offenders. - offenders: dict[Path, int] = defaultdict(int) - for r in records: - if r.classification != "MACRO": - offenders[r.file_path] += 1 - if offenders: - print() - print("Top non-compliant files:") - ranked = sorted(offenders.items(), key=lambda kv: kv[1], reverse=True) - for path, count in ranked[:10]: - try: - rel = path.relative_to(Path.cwd()) - except ValueError: - rel = path - print(f" {count:>4} {rel}") - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -def _default_roots() -> List[Path]: - cwd = Path.cwd() - src = cwd / "src" - return [src] if src.exists() else [cwd] - - -def main(argv: Optional[List[str]] = None) -> int: - parser = argparse.ArgumentParser( - description=( - "Audit PyMethodDef / PyGetSetDef entries in McRogueFace C++ " - "sources for MCRF_METHOD / MCRF_PROPERTY documentation macro use." - ), - ) - parser.add_argument( - "--strict", action="store_true", - help="Exit nonzero if any non-MACRO entries are found (CI mode)." - ) - parser.add_argument( - "--quiet", action="store_true", - help="Print only the summary, omit per-file tables." - ) - parser.add_argument( - "--paths", nargs="+", type=Path, default=None, - help="Files or directories to scan (default: ./src)." - ) - args = parser.parse_args(argv) - - roots = args.paths if args.paths else _default_roots() - project_root = Path.cwd() - - files = sorted(set(_iter_cpp_files(roots))) - if not files: - sys.stderr.write("WARNING: no .cpp files found.\n") - return 0 - - all_records: List[EntryRecord] = [] - for f in files: - all_records.extend(scan_file(f)) - - if not args.quiet: - if all_records: - _print_file_table(all_records, project_root) - else: - print("(no PyMethodDef / PyGetSetDef arrays found)") - print() - - _print_summary(all_records) - - if args.strict: - non_macro = sum( - 1 for r in all_records if r.classification != "MACRO" - ) - if non_macro: - return 1 - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/capture_audio_synth_header.py b/tools/capture_audio_synth_header.py deleted file mode 100644 index 9f71584..0000000 --- a/tools/capture_audio_synth_header.py +++ /dev/null @@ -1,16 +0,0 @@ -import mcrfpy -from mcrfpy import automation -import runpy, sys, pathlib - -OUT = sys.argv[1] if len(sys.argv) > 1 else "image00.png" - -demo_path = (pathlib.Path(__file__).resolve().parent.parent / - "tests" / "demo" / "audio_synth_demo.py") -runpy.run_path(str(demo_path), run_name="__demo__") - -def shot(timer, runtime): - automation.screenshot(OUT) - print(f"Wrote {OUT}") - sys.exit(0) - -mcrfpy.Timer("header_shot", shot, 150) diff --git a/tools/generate_all_docs.sh b/tools/generate_all_docs.sh index 024c3d3..7c0234d 100755 --- a/tools/generate_all_docs.sh +++ b/tools/generate_all_docs.sh @@ -26,24 +26,3 @@ echo " HTML: docs/api_reference_dynamic.html" echo " Markdown: docs/API_REFERENCE_DYNAMIC.md" echo " Man page: docs/mcrfpy.3" echo " Stubs: stubs/mcrfpy.pyi" - -# --------------------------------------------------------------------------- -# Static-analysis audit: report MCRF_METHOD / MCRF_PROPERTY macro compliance -# across every PyMethodDef / PyGetSetDef array in src/. This is informational -# only (no --strict) so it cannot break the doc build, but the summary makes -# pre-1.0 documentation drift visible alongside doc generation. -# -# Requires the .venv-audit virtual environment with tree-sitter + -# tree-sitter-cpp installed. The audit is skipped silently if absent so -# contributors without the venv aren't blocked. -# --------------------------------------------------------------------------- -if [ -x "./.venv-audit/bin/python3" ] && [ -f "./tools/audit_pymethoddef.py" ]; then - echo "" - echo "=== PyMethodDef / PyGetSetDef Macro Compliance Audit ===" - ./.venv-audit/bin/python3 ./tools/audit_pymethoddef.py --quiet || true -elif [ -f "./tools/audit_pymethoddef.py" ]; then - echo "" - echo "(skipping audit_pymethoddef.py: .venv-audit not found - run" - echo " 'python3 -m venv .venv-audit && .venv-audit/bin/pip install" - echo " tree-sitter tree-sitter-cpp' to enable)" -fi